diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index b4afeae6c3..84134f04a9 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -59,10 +59,11 @@ export class DynamicComponentFactoryService { template: string, modules?: Type[], preserveWhitespaces?: boolean, - compileAttempt = 1): Observable> { + compileAttempt = 1, + styles?: string[]): Observable> { return from(import('@angular/compiler')).pipe( mergeMap(() => { - const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces); + const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces, styles); let moduleImports: Type[] = [CommonModule]; if (modules) { moduleImports = [...moduleImports, ...modules]; @@ -91,7 +92,7 @@ export class DynamicComponentFactoryService { catchError((error) => { if (compileAttempt === 1) { ɵresetCompiledComponents(); - return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt); + return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); } else { throw error; } @@ -110,11 +111,12 @@ export class DynamicComponentFactoryService { } } - private createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean): Type { + private createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type { // noinspection AngularMissingOrInvalidDeclarationInModule return Component({ template, - preserveWhitespaces + preserveWhitespaces, + styles })(componentType); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index d182c26dbe..5f4486dad2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -16,4 +16,6 @@ --> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index 4a922b3f71..3f3f13626e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, HostBinding, Inject, Input, OnInit, Type } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, Input, OnInit, Type } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { WidgetContext } from '@home/models/widget-component.models'; import { Store } from '@ngrx/store'; @@ -25,7 +25,8 @@ import { createLabelFromPattern, flatDataWithoutOverride, formattedDataFormDatasourceData, - hashCode, isDefinedAndNotNull, + hashCode, + isDefinedAndNotNull, isNotEmptyStr, parseFunction, safeExecute @@ -53,7 +54,6 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { settings: MarkdownWidgetSettings; markdownTextFunction: MarkdownTextFunction; - @HostBinding('class') markdownClass: string; @Input() @@ -61,6 +61,8 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { markdownText: string; + additionalStyles: string[]; + constructor(protected store: Store, private utils: UtilsService, @@ -74,14 +76,15 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { this.settings = this.ctx.settings; this.markdownTextFunction = this.settings.useMarkdownTextFunction ? parseFunction(this.settings.markdownTextFunction, ['data', 'ctx']) : null; - this.markdownClass = 'markdown-widget'; - const cssString = this.settings.markdownCss; + let cssString = this.settings.markdownCss; if (isNotEmptyStr(cssString)) { const cssParser = new cssjs(); + this.markdownClass = 'markdown-widget-' + hashCode(cssString); + cssParser.cssPreviewNamespace = this.markdownClass; cssParser.testMode = false; - this.markdownClass += '-' + hashCode(cssString); - cssParser.cssPreviewNamespace = 'tb-default .' + this.markdownClass; - cssParser.createStyleElement(this.markdownClass, cssString); + cssString = cssParser.applyNamespacing(cssString); + cssString = cssParser.getCSSForEditor(cssString); + this.additionalStyles = [cssString]; } const pageSize = isDefinedAndNotNull(this.ctx.widgetConfig.pageSize) && this.ctx.widgetConfig.pageSize > 0 ? this.ctx.widgetConfig.pageSize : 16384; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss index 07dc221e6f..d0dd324e52 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss @@ -66,13 +66,17 @@ :host ::ng-deep { .tb-multiple-input { - .mat-mdc-slide-toggle .mdc-form-field > label { - display: flex; - align-items: center; - - .mat-icon { - margin-left: 8px; - margin-right: 8px; + .mat-mdc-slide-toggle .mdc-form-field { + width: 100%; + & > label { + display: flex; + align-items: center; + width: 100%; + margin: 0; + .mat-icon { + margin-left: 8px; + margin-right: 8px; + } } } } diff --git a/ui-ngx/src/app/shared/components/markdown.component.scss b/ui-ngx/src/app/shared/components/markdown.component.scss new file mode 100644 index 0000000000..d30de48ade --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown.component.scss @@ -0,0 +1,362 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .tb-markdown-view { + display: block; + + $headings: h1, h2, h3, h4, h5, h6; + + h1 { + font-size: 32px; + padding-right: 60px; + } + + @each $heading in $headings { + #{$heading} { + color: #0F161D; + line-height: normal; + font-weight: 500; + border-bottom: none; + margin: 0; + } + & > #{$heading} { + padding: 30px 32px 10px; + } + } + + p { + font-size: 16px; + font-weight: 400; + line-height: 1.25em; + margin: 0; + } + + p + p { + margin-top: 10px; + } + + p, div { + color: rgba(15, 22, 29, 0.8); + line-height: 1.5em; + } + + & > p, & > div { + padding-right: 32px; + padding-left: 32px; + } + + ul { + padding-left: 62px; + padding-right: 32px; + color: rgba(15, 22, 29, 0.8); + margin-top: 16px; + margin-bottom: 16px; + } + + ul { + @each $heading in $headings { + & + #{$heading} { + padding-top: 14px; + } + } + } + + li { + padding-bottom: .75em; + line-height: 1.5em; + + ul { + margin-bottom: 0; + } + + p { + padding-left: 0; + } + } + + a { + font-weight: 500; + color: #2a7dec; + text-decoration: none; + border: none; + + &:hover { + color: #2a7dec; + text-decoration: underline; + border: none; + } + } + + & > table { + margin-left: 32px; + width: calc(100% - 64px); + border: 1px solid rgba(42, 125, 236, .2); + border-radius: 4px; + border-collapse: unset; + border-spacing: 0; + margin-top: 30px; + margin-bottom: 30px; + overflow: hidden; + table-layout: fixed; + + &.auto { + table-layout: auto; + } + + & > thead { + background-color: #f9fbff; + color: rgba(33, 37, 41, .6); + + & > tr { + & > th { + border-bottom: 1px solid rgba(42, 125, 236, .2); + font-size: 16px; + padding: 12px 16px; + text-align: left; + margin: 0; + @media screen and (max-width: 400px) { + font-size: 12px; + padding: 12px 4px; + code:not([class*=language-]) { + font-size: 12px; + } + } + } + } + } + + & > tbody { + & > tr:not(:last-child) { + & > td { + border-bottom: 1px solid rgba(42, 125, 236, .2); + } + } + + & > tr { + & > td { + font-size: 16px; + padding: 12px 16px; + text-align: left; + margin: 0; + color: rgba(15, 22, 29, 0.8); + @media screen and (max-width: 400px) { + font-size: 12px; + padding: 12px 4px; + code:not([class*=language-]) { + font-size: 12px; + } + } + } + } + } + + th, td { + font-size: .85em; + padding: 8px; + margin: 0; + text-align: left; + } + + td[align=center], th[align=center] { + text-align: center; + } + + td[align=right], th[align=right] { + text-align: right; + } + + tr td div { + padding-left: 0; + padding-right: 0; + } + } + + + div.divider { + padding-top: 32px; + border-bottom: 1px solid rgba(15, 22, 29, 0.1); + } + + ul + div.divider { + padding-top: 16px; + } + + img { + max-width: 100%; + } + + button.tb-button { + cursor: pointer; + display: inline-block; + border-radius: 4px; + border: none; + padding: 10px 20px; + line-height: 24px; + color: #fff; + background-color: #305680; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + text-decoration: none; + font-size: 16px; + font-weight: 500; + transition: background-color .4s cubic-bezier(.25, .8, .25, 1); + + &:hover { + background-color: #264363; + color: #fff; + text-decoration: none; + } + } + + #video { + width: 100%; + margin: 0; + position: relative; + + #video_wrapper { + position: relative; + width: 100%; + padding-bottom: 56.25%; + padding-left: 0; + padding-right: 0; + + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + } + + @media screen and (min-width: 750px) { + #video { + width: 100%; + display: block; + } + } + + @media screen and (min-width: 1025px) { + #video { + width: 50%; + position: relative; + } + } + + code:not([class*=language-]) { + color: #eb5757; + font-family: monospace; + font-size: 16px; + } + + div.code-wrapper { + position: relative; + + button.clipboard-btn { + pointer-events: none; + outline: none; + position: absolute; + width: 206px; + height: 42px; + top: 0; + right: 32px; + background: 0 0; + border: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &.multiline { + right: 44px; + } + + p { + padding: 8px; + top: 1px; + transition: .2s; + color: #2a7dec; + background: rgba(255, 255, 255, .85); + backdrop-filter: blur(4px); + opacity: 0; + font-weight: 500; + right: 32px; + position: absolute; + } + + div { + background-color: #fff; + position: absolute; + width: 38px; + height: 38px; + top: 3px; + right: 3px; + padding: 10px; + + img { + position: initial; + width: 18px; + height: 18px; + filter: invert(51%) sepia(6%) saturate(172%) hue-rotate(177deg) brightness(94%) contrast(92%); + } + } + } + + &:hover { + cursor: pointer; + + pre[class*="language-"] { + border: solid 1px #2a7dec; + } + + button.clipboard-btn { + p { + opacity: 1; + } + + div { + img { + filter: invert(49%) sepia(97%) saturate(3730%) hue-rotate(200deg) brightness(95%) contrast(95%); + } + } + } + } + } + + th, td { + div.code-wrapper { + display: inline-block; + width: 100%; + + button.clipboard-btn { + top: -10px; + right: 0; + padding: 0 3px; + } + } + } + + pre[class*="language-"] { + font-size: 16px; + border: 1px solid rgba(42, 125, 236, .2); + border-radius: 4px; + background: 0 0; + padding: 8px 16px; + color: #212529; + } + } +} diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 95c0ccd23b..24226573e6 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -18,14 +18,17 @@ import { ChangeDetectorRef, Component, ComponentFactory, - ComponentRef, ElementRef, + ComponentRef, + ElementRef, EventEmitter, Inject, Injector, - Input, OnChanges, + Input, + OnChanges, Output, SimpleChanges, - Type, ViewChild, + Type, + ViewChild, ViewContainerRef } from '@angular/core'; import { HelpService } from '@core/services/help.service'; @@ -33,12 +36,13 @@ import { MarkdownService, PrismPlugin } from 'ngx-markdown'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; -import { isDefinedAndNotNull } from '@core/utils'; +import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { Observable, of, ReplaySubject } from 'rxjs'; @Component({ selector: 'tb-markdown', - templateUrl: './markdown.component.html' + templateUrl: './markdown.component.html', + styleUrls: ['./markdown.component.scss'] }) export class TbMarkdownComponent implements OnChanges { @@ -53,8 +57,12 @@ export class TbMarkdownComponent implements OnChanges { @Input() markdownClass: string | undefined; + @Input() containerClass: string | undefined; + @Input() style: { [klass: string]: any } = {}; + @Input() additionalStyles: string[]; + @Input() get lineNumbers(): boolean { return this.lineNumbersValue; } set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } @@ -95,6 +103,9 @@ export class TbMarkdownComponent implements OnChanges { markdownClass += ` ${this.markdownClass}`; } template = `
${template}
`; + if (this.containerClass) { + template = `
${template}
`; + } this.markdownContainer.clear(); const parent = this; let readyObservable: Observable; @@ -102,6 +113,11 @@ export class TbMarkdownComponent implements OnChanges { if (this.additionalCompileModules) { compileModules = compileModules.concat(this.additionalCompileModules); } + let styles: string[] = deepClone(TbMarkdownComponent['ɵcmp'].styles); + styles[0] = styles[0].replace(/\[_nghost\-%COMP%\]/g, '').replace(/\[_ngcontent\-%COMP%\]/g, ''); + if (this.additionalStyles) { + styles = styles.concat(this.additionalStyles); + } this.dynamicComponentFactoryService.createDynamicComponentFactory( class TbMarkdownInstance { ngOnDestroy(): void { @@ -110,7 +126,7 @@ export class TbMarkdownComponent implements OnChanges { }, template, compileModules, - true + true, 1, styles ).subscribe((factory) => { this.tbMarkdownInstanceComponentFactory = factory; const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 883ccaf72f..369b3b9e3d 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -424,345 +424,24 @@ mat-label { } } -.tb-default .tb-markdown-view, .tb-dark .tb-markdown-view, .tb-markdown-view { - display: block; - - $headings: h1, h2, h3, h4, h5, h6; - - h1 { - font-size: 32px; - padding-right: 60px; - } - - @each $heading in $headings { - #{$heading} { - color: #0F161D; - line-height: normal; - font-weight: 500; - border-bottom: none; - margin: 0; - } - & > #{$heading} { - padding: 30px 32px 10px; - } - } - - p { - font-size: 16px; - font-weight: 400; - line-height: 1.25em; - margin: 0; - } - - p + p { - margin-top: 10px; - } - - p, div { - color: rgba(15, 22, 29, 0.8); - line-height: 1.5em; - } - - & > p, & > div { - padding-right: 32px; - padding-left: 32px; - } - - ul { - padding-left: 62px; - padding-right: 32px; - color: rgba(15, 22, 29, 0.8); - margin-top: 16px; - margin-bottom: 16px; - } - - ul { - @each $heading in $headings { - & + #{$heading} { - padding-top: 14px; - } - } - } - - li { - padding-bottom: .75em; - line-height: 1.5em; - ul { - margin-bottom: 0; - } - p { - padding-left: 0; - } - } - - a { - font-weight: 500; - color: #2a7dec; - text-decoration: none; - border: none; - &:hover { - color: #2a7dec; - text-decoration: underline; - border: none; - } - } - - & > table { - margin-left: 32px; - width: calc(100% - 64px); - border: 1px solid rgba(42,125,236,.2); - border-radius: 4px; - border-collapse: unset; - border-spacing: 0; - margin-top: 30px; - margin-bottom: 30px; - overflow: hidden; - table-layout: fixed; - &.auto { - table-layout: auto; - } - - & > thead { - background-color: #f9fbff; - color: rgba(33,37,41,.6); - - & > tr { - & > th { - border-bottom: 1px solid rgba(42,125,236,.2); - font-size: 16px; - padding: 12px 16px; - text-align: left; - margin: 0; - @media screen and (max-width: 400px) { - font-size: 12px; - padding: 12px 4px; - code:not([class*=language-]) { - font-size: 12px; - } - } - } - } - } - - & > tbody { - & > tr:not(:last-child) { - & > td { - border-bottom: 1px solid rgba(42,125,236,.2); - } - } - & > tr { - & > td { - font-size: 16px; - padding: 12px 16px; - text-align: left; - margin: 0; - color: rgba(15, 22, 29, 0.8); - @media screen and (max-width: 400px) { - font-size: 12px; - padding: 12px 4px; - code:not([class*=language-]) { - font-size: 12px; - } - } - } - } - } - th, td { - font-size: .85em; - padding: 8px; - margin: 0; - text-align: left; - } - td[align=center], th[align=center] { - text-align: center; - } - - td[align=right], th[align=right] { - text-align: right; - } - - tr td div { - padding-left: 0; - padding-right: 0; - } - } - - - div.divider { - padding-top: 32px; - border-bottom: 1px solid rgba(15, 22, 29, 0.1); - } - - ul + div.divider { - padding-top: 16px; - } - - img { - max-width: 100%; - } - - button.tb-button { - cursor: pointer; - display: inline-block; - border-radius: 4px; - border: none; - padding: 10px 20px; - line-height: 24px; - color: #fff; - background-color: #305680; - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2); - text-decoration: none; - font-size: 16px; - font-weight: 500; - transition: background-color .4s cubic-bezier(.25,.8,.25,1); - } - - button.tb-button:hover { - background-color: #264363; - color: #fff; - text-decoration: none; - } - - #video { - width: 100%; - margin: 0; - position: relative; - } - - - #video #video_wrapper { - position: relative; - width: 100%; - padding-bottom: 56.25%; - padding-left: 0; - padding-right: 0; - } - - #video #video_wrapper iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - @media screen and (min-width: 750px) { - #video { - width: 100%; - display: block; - } - } - - @media screen and (min-width: 1025px) { - #video { - width: 50%; - position: relative; - } - } - - code:not([class*=language-]) { - color: #eb5757; - font-family: monospace; - font-size: 16px; - } - - div.code-wrapper { - position: relative; - button.clipboard-btn { - pointer-events: none; - outline: none; - position: absolute; - width: 206px; - height: 42px; - top: 0; - right: 32px; - background: 0 0; - border: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - &.multiline { - right: 44px; - } - p { - padding: 8px; - top: 1px; - transition: .2s; - color: #2a7dec; - background: rgba(255,255,255,.85); - backdrop-filter: blur(4px); - opacity: 0; - font-weight: 500; - right: 32px; - position: absolute; - } - div { - background-color: #fff; - position: absolute; - width: 38px; - height: 38px; - top: 3px; - right: 3px; - padding: 10px; - img { - position: initial; - width: 18px; - height: 18px; - filter: invert(51%) sepia(6%) saturate(172%) hue-rotate(177deg) brightness(94%) contrast(92%); - } - } - } - - &:hover { - cursor: pointer; - pre[class*="language-"] { - border: solid 1px #2a7dec; - } - button.clipboard-btn { - p { - opacity: 1; - } - div { - img { - filter: invert(49%) sepia(97%) saturate(3730%) hue-rotate(200deg) brightness(95%) contrast(95%); - } - } - } - } - } - - th, td { - div.code-wrapper { - display: inline-block; - width: 100%; - - button.clipboard-btn { - top: -10px; - right: 0; - padding: 0 3px; - } - } - } - +.tb-markdown-view { pre[class*="language-"] { - font-size: 16px; - border: 1px solid rgba(42,125,236,.2); - border-radius: 4px; - background: 0 0; - padding: 8px 16px; - color: #212529; + .token.atrule, .token.attr-value, .token.keyword { color: #2a7dec; } + .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #eb5757; } + .token.punctuation { color: #212529; } + &.line-numbers { padding-left: 66px; + & > code { span.line-numbers-rows { top: -12px; @@ -773,14 +452,17 @@ mat-label { padding: 8px 12px 8px 18px; text-align: right; background: #f9fbff; + & > span:before { - color: rgba(33,37,41,.6); + color: rgba(33, 37, 41, .6); padding-right: 0; } } } + &.no-line-numbers { padding-left: 16px; + & > code { span.line-numbers-rows { display: none;