Browse Source

UI: Markdown component and markdown widget style improvements

pull/8203/head
Igor Kulikov 4 years ago
parent
commit
43783f0379
  1. 12
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  2. 2
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html
  3. 19
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts
  4. 18
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss
  5. 362
      ui-ngx/src/app/shared/components/markdown.component.scss
  6. 28
      ui-ngx/src/app/shared/components/markdown.component.ts
  7. 338
      ui-ngx/src/styles.scss

12
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts

@ -59,10 +59,11 @@ export class DynamicComponentFactoryService {
template: string,
modules?: Type<any>[],
preserveWhitespaces?: boolean,
compileAttempt = 1): Observable<ComponentFactory<T>> {
compileAttempt = 1,
styles?: string[]): Observable<ComponentFactory<T>> {
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<any>[] = [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<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean): Type<T> {
private createDynamicComponent<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type<T> {
// noinspection AngularMissingOrInvalidDeclarationInModule
return Component({
template,
preserveWhitespaces
preserveWhitespaces,
styles
})(componentType);
}

2
ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html

@ -16,4 +16,6 @@
-->
<tb-markdown [data]="markdownText" [additionalCompileModules]="[ homeComponentsModule ]"
[additionalStyles]="additionalStyles"
[containerClass]="markdownClass"
[context]="{ ctx: ctx }" lineNumbers fallbackToPlainMarkdown (click)="markdownClick($event)"></tb-markdown>

19
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<AppState>,
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;

18
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;
}
}
}
}

362
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;
}
}
}

28
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 = `<div [ngStyle]="style" class="${markdownClass}">${template}</div>`;
if (this.containerClass) {
template = `<div class="${this.containerClass}" style="width: 100%; height: 100%;">${template}</div>`;
}
this.markdownContainer.clear();
const parent = this;
let readyObservable: Observable<void>;
@ -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});

338
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;

Loading…
Cancel
Save