Add markdown/HTML widget
This commit is contained in:
		
							parent
							
								
									327607e86d
								
							
						
					
					
						commit
						9c1a2a4cb1
					
				
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -78,7 +78,8 @@
 | 
			
		||||
              "node_modules/leaflet/dist/leaflet.css",
 | 
			
		||||
              "src/app/modules/home/components/widget/lib/maps/markers.scss",
 | 
			
		||||
              "node_modules/leaflet.markercluster/dist/MarkerCluster.css",
 | 
			
		||||
              "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css"
 | 
			
		||||
              "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
 | 
			
		||||
              "node_modules/prismjs/themes/prism.css"
 | 
			
		||||
            ],
 | 
			
		||||
            "stylePreprocessorOptions": {
 | 
			
		||||
              "includePaths": [
 | 
			
		||||
@ -88,7 +89,11 @@
 | 
			
		||||
            "scripts": [
 | 
			
		||||
              "node_modules/tinycolor2/dist/tinycolor-min.js",
 | 
			
		||||
              "node_modules/split.js/dist/split.min.js",
 | 
			
		||||
              "node_modules/systemjs/dist/system.js"
 | 
			
		||||
              "node_modules/systemjs/dist/system.js",
 | 
			
		||||
              "node_modules/marked/lib/marked.js",
 | 
			
		||||
              "node_modules/prismjs/prism.js",
 | 
			
		||||
              "node_modules/prismjs/components/prism-bash.min.js",
 | 
			
		||||
              "node_modules/prismjs/components/prism-json.min.js"
 | 
			
		||||
            ],
 | 
			
		||||
            "customWebpackConfig": {
 | 
			
		||||
              "path": "./extra-webpack.config.js"
 | 
			
		||||
 | 
			
		||||
@ -72,6 +72,7 @@
 | 
			
		||||
    "ngx-drag-drop": "^2.0.0",
 | 
			
		||||
    "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master",
 | 
			
		||||
    "ngx-hm-carousel": "^2.0.0-rc.1",
 | 
			
		||||
    "ngx-markdown": "^10.1.1",
 | 
			
		||||
    "ngx-sharebuttons": "^8.0.5",
 | 
			
		||||
    "ngx-translate-messageformat-compiler": "^4.9.0",
 | 
			
		||||
    "objectpath": "^2.0.0",
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
 | 
			
		||||
import { FormattedData, MapProviders, ReplaceInfo } from '@home/components/widget/lib/maps/map-models';
 | 
			
		||||
import {
 | 
			
		||||
  createLabelFromDatasource,
 | 
			
		||||
  createLabelFromDatasource, deepClone,
 | 
			
		||||
  hashCode,
 | 
			
		||||
  isDefined,
 | 
			
		||||
  isDefinedAndNotNull,
 | 
			
		||||
@ -343,6 +343,22 @@ export function parseData(input: DatasourceData[]): FormattedData[] {
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function flatData(input: FormattedData[]): FormattedData {
 | 
			
		||||
  let result: FormattedData = {} as FormattedData;
 | 
			
		||||
  if (input.length) {
 | 
			
		||||
    for (const toMerge of input) {
 | 
			
		||||
      result = {...result, ...toMerge};
 | 
			
		||||
    }
 | 
			
		||||
    result.entityName =  input[0].entityName;
 | 
			
		||||
    result.entityId =  input[0].entityId;
 | 
			
		||||
    result.entityType =  input[0].entityType;
 | 
			
		||||
    result.$datasource =  input[0].$datasource;
 | 
			
		||||
    result.dsIndex =  input[0].dsIndex;
 | 
			
		||||
    result.deviceType =  input[0].deviceType;
 | 
			
		||||
  }
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function parseArray(input: DatasourceData[]): FormattedData[][] {
 | 
			
		||||
  return _(input).groupBy(el => el?.datasource?.entityName)
 | 
			
		||||
    .values().value().map((entityArray) =>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
<!--
 | 
			
		||||
 | 
			
		||||
    Copyright © 2016-2021 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.
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
<markdown [data]="markdownText" class="tb-markdown-view" (click)="markdownClick($event)"></markdown>
 | 
			
		||||
@ -0,0 +1,130 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2021 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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.tb-markdown-view {
 | 
			
		||||
  display: block;
 | 
			
		||||
  ::ng-deep {
 | 
			
		||||
    h1 {
 | 
			
		||||
      font-size: 32px;
 | 
			
		||||
      padding-right: 60px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    h1, h2, h3, h4, h5, h6 {
 | 
			
		||||
      line-height: normal;
 | 
			
		||||
      font-weight: 500;
 | 
			
		||||
      margin-bottom: 30px;
 | 
			
		||||
      padding-bottom: 10px;
 | 
			
		||||
      border-bottom: 1px solid #ccc;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p {
 | 
			
		||||
      font-size: 16px;
 | 
			
		||||
      font-weight: 400;
 | 
			
		||||
      line-height: 1.25em;
 | 
			
		||||
      margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p+p {
 | 
			
		||||
      margin-top: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    table {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      border: 1px solid #ccc;
 | 
			
		||||
      border-spacing: 0;
 | 
			
		||||
      margin-top: 30px;
 | 
			
		||||
      margin-bottom: 30px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    thead {
 | 
			
		||||
      background-color: #555;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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:nth-child(even) {
 | 
			
		||||
      background-color: #f7f7f7;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    code:not([class*=language-]) {
 | 
			
		||||
      background: #f5f5f5;
 | 
			
		||||
      border-radius: 2px;
 | 
			
		||||
      color: #dd4a68;
 | 
			
		||||
      padding: 2px 4px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    div.code-wrapper {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      button.clipboard-btn {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        margin: 0;
 | 
			
		||||
        border: 0;
 | 
			
		||||
        outline: none;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 5px;
 | 
			
		||||
        right: 5px;
 | 
			
		||||
        background: #fff;
 | 
			
		||||
        box-shadow: 0 1px 8px 0 rgba(0,0,0,0.2), 0 3px 4px 0 rgba(0,0,0,0.14), 0 3px 3px -2px rgba(0,0,0,0.12);
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
        transition: opacity .3s;
 | 
			
		||||
        padding: 3px 6px;
 | 
			
		||||
        line-height: 16px;
 | 
			
		||||
        img {
 | 
			
		||||
          width: 18px;
 | 
			
		||||
        }
 | 
			
		||||
        &:hover {
 | 
			
		||||
          background: #f9f9f9;
 | 
			
		||||
        }
 | 
			
		||||
        &:active {
 | 
			
		||||
          background-color: #ececec;
 | 
			
		||||
          box-shadow: inset 1px -1px 4px 0px rgba(0,0,0,0.4);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      &:hover {
 | 
			
		||||
        button.clipboard-btn {
 | 
			
		||||
          opacity: .85;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    th, td {
 | 
			
		||||
      div.code-wrapper {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        button.clipboard-btn {
 | 
			
		||||
          top: -5px;
 | 
			
		||||
          right: -30px;
 | 
			
		||||
          padding: 0 3px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,116 @@
 | 
			
		||||
///
 | 
			
		||||
/// Copyright © 2016-2021 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.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import { ChangeDetectorRef, Component, ElementRef, Input, OnInit } from '@angular/core';
 | 
			
		||||
import { PageComponent } from '@shared/components/page.component';
 | 
			
		||||
import { WidgetContext } from '@home/models/widget-component.models';
 | 
			
		||||
import { Store } from '@ngrx/store';
 | 
			
		||||
import { AppState } from '@core/core.state';
 | 
			
		||||
import { DatasourceData } from '@shared/models/widget.models';
 | 
			
		||||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
 | 
			
		||||
import {
 | 
			
		||||
  fillPattern, flatData,
 | 
			
		||||
  parseData,
 | 
			
		||||
  parseFunction,
 | 
			
		||||
  processPattern,
 | 
			
		||||
  safeExecute
 | 
			
		||||
} from '@home/components/widget/lib/maps/common-maps-utils';
 | 
			
		||||
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
 | 
			
		||||
import { hashCode, isNotEmptyStr } from '@core/utils';
 | 
			
		||||
import cssjs from '@core/css/css';
 | 
			
		||||
 | 
			
		||||
interface MarkdownWidgetSettings {
 | 
			
		||||
  markdownTextPattern: string;
 | 
			
		||||
  useMarkdownTextFunction: boolean;
 | 
			
		||||
  markdownTextFunction: string;
 | 
			
		||||
  markdownCss: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MarkdownTextFunction = (data: FormattedData[]) => string;
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-markdown-widget ',
 | 
			
		||||
  templateUrl: './markdown-widget.component.html',
 | 
			
		||||
  styleUrls: ['./markdown-widget.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class MarkdownWidgetComponent extends PageComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  settings: MarkdownWidgetSettings;
 | 
			
		||||
  markdownTextFunction: MarkdownTextFunction;
 | 
			
		||||
 | 
			
		||||
  @Input()
 | 
			
		||||
  ctx: WidgetContext;
 | 
			
		||||
 | 
			
		||||
  markdownText: string;
 | 
			
		||||
 | 
			
		||||
  constructor(protected store: Store<AppState>,
 | 
			
		||||
              private elementRef: ElementRef,
 | 
			
		||||
              private cd: ChangeDetectorRef) {
 | 
			
		||||
    super(store);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.ctx.$scope.markdownWidget = this;
 | 
			
		||||
    this.settings = this.ctx.settings;
 | 
			
		||||
    this.markdownTextFunction = this.settings.useMarkdownTextFunction ? parseFunction(this.settings.markdownTextFunction, ['data']) : null;
 | 
			
		||||
 | 
			
		||||
    const cssString = this.settings.markdownCss;
 | 
			
		||||
    if (isNotEmptyStr(cssString)) {
 | 
			
		||||
      const cssParser = new cssjs();
 | 
			
		||||
      cssParser.testMode = false;
 | 
			
		||||
      const namespace = 'entities-hierarchy-' + hashCode(cssString);
 | 
			
		||||
      cssParser.cssPreviewNamespace = namespace;
 | 
			
		||||
      cssParser.createStyleElement(namespace, cssString);
 | 
			
		||||
      $(this.elementRef.nativeElement).addClass(namespace);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public onDataUpdated() {
 | 
			
		||||
    let initialData: DatasourceData[];
 | 
			
		||||
    if (this.ctx.data?.length) {
 | 
			
		||||
      initialData = this.ctx.data;
 | 
			
		||||
    } else if (this.ctx.datasources?.length) {
 | 
			
		||||
      initialData = [
 | 
			
		||||
        {
 | 
			
		||||
          datasource: this.ctx.datasources[0],
 | 
			
		||||
          dataKey: {
 | 
			
		||||
            type: DataKeyType.attribute,
 | 
			
		||||
            name: 'empty'
 | 
			
		||||
          },
 | 
			
		||||
          data: []
 | 
			
		||||
        }
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    let markdownText: string;
 | 
			
		||||
    if (initialData) {
 | 
			
		||||
      const data = parseData(initialData);
 | 
			
		||||
      markdownText = this.settings.useMarkdownTextFunction ?
 | 
			
		||||
        safeExecute(this.markdownTextFunction, [data]) : this.settings.markdownTextPattern;
 | 
			
		||||
      const allData = flatData(data);
 | 
			
		||||
      const replaceInfo = processPattern(markdownText, allData);
 | 
			
		||||
      markdownText = fillPattern(markdownText, replaceInfo, allData);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.markdownText !== markdownText) {
 | 
			
		||||
      this.markdownText = markdownText;
 | 
			
		||||
      this.cd.detectChanges();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  markdownClick($event: MouseEvent) {
 | 
			
		||||
    this.ctx.actionsApi.elementClick($event);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -101,7 +101,7 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
 | 
			
		||||
      const dataSourceData = data[0];
 | 
			
		||||
      const pattern = this.settings.useQrCodeTextFunction ?
 | 
			
		||||
        safeExecute(this.qrCodeTextFunction, [dataSourceData]) : this.settings.qrCodeTextPattern;
 | 
			
		||||
      const replaceInfo = processPattern(pattern, data);
 | 
			
		||||
      const replaceInfo = processPattern(pattern, dataSourceData);
 | 
			
		||||
      qrCodeText = fillPattern(pattern, replaceInfo, dataSourceData);
 | 
			
		||||
    }
 | 
			
		||||
    this.updateQrCodeText(qrCodeText);
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navig
 | 
			
		||||
import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component';
 | 
			
		||||
import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component';
 | 
			
		||||
import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component';
 | 
			
		||||
import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-widget.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations:
 | 
			
		||||
@ -60,7 +61,8 @@ import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget
 | 
			
		||||
      GatewayFormComponent,
 | 
			
		||||
      NavigationCardsWidgetComponent,
 | 
			
		||||
      NavigationCardWidgetComponent,
 | 
			
		||||
      QrCodeWidgetComponent
 | 
			
		||||
      QrCodeWidgetComponent,
 | 
			
		||||
      MarkdownWidgetComponent
 | 
			
		||||
    ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
@ -83,7 +85,8 @@ import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget
 | 
			
		||||
    GatewayFormComponent,
 | 
			
		||||
    NavigationCardsWidgetComponent,
 | 
			
		||||
    NavigationCardWidgetComponent,
 | 
			
		||||
    QrCodeWidgetComponent
 | 
			
		||||
    QrCodeWidgetComponent,
 | 
			
		||||
    MarkdownWidgetComponent
 | 
			
		||||
  ],
 | 
			
		||||
  providers: [
 | 
			
		||||
    CustomDialogService,
 | 
			
		||||
 | 
			
		||||
@ -159,8 +159,8 @@ class ThingsboardAceEditor extends React.Component<ThingsboardAceEditorProps, Th
 | 
			
		||||
                  <div className='json-form-ace-editor'>
 | 
			
		||||
                      <div className='title-panel'>
 | 
			
		||||
                          <label>{this.props.mode}</label>
 | 
			
		||||
                          <Button style={ styles.tidyButtonStyle }
 | 
			
		||||
                                  className='tidy-button' onClick={this.onTidy}>Tidy</Button>
 | 
			
		||||
                          { this.props.onTidy ? <Button style={ styles.tidyButtonStyle }
 | 
			
		||||
                                                       className='tidy-button' onClick={this.onTidy}>Tidy</Button> : null }
 | 
			
		||||
                          <Button style={ styles.tidyButtonStyle }
 | 
			
		||||
                                  className='tidy-button' onClick={this.onToggleFull}>
 | 
			
		||||
                            {this.state.isFull ?
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,33 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright © 2016-2021 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.
 | 
			
		||||
 */
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import ThingsboardAceEditor from './json-form-ace-editor';
 | 
			
		||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
 | 
			
		||||
 | 
			
		||||
class ThingsboardMarkdown extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
 | 
			
		||||
 | 
			
		||||
  constructor(props) {
 | 
			
		||||
    super(props);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <ThingsboardAceEditor {...this.props} mode='markdown' {...this.state}></ThingsboardAceEditor>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ThingsboardMarkdown;
 | 
			
		||||
@ -38,6 +38,7 @@ import { JsonFormData, JsonFormProps, onChangeFn, OnColorClickFn, OnIconClickFn
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import * as tinycolor_ from 'tinycolor2';
 | 
			
		||||
import { GroupInfo } from '@shared/models/widget.models';
 | 
			
		||||
import ThingsboardMarkdown from '@shared/components/json-form/react/json-form-markdown';
 | 
			
		||||
 | 
			
		||||
const tinycolor = tinycolor_;
 | 
			
		||||
 | 
			
		||||
@ -65,6 +66,7 @@ class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
 | 
			
		||||
      json: ThingsboardJson,
 | 
			
		||||
      html: ThingsboardHtml,
 | 
			
		||||
      css: ThingsboardCss,
 | 
			
		||||
      markdown: ThingsboardMarkdown,
 | 
			
		||||
      color: ThingsboardColor,
 | 
			
		||||
      'rc-select': ThingsboardRcSelect,
 | 
			
		||||
      fieldset: ThingsboardFieldSet,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										99
									
								
								ui-ngx/src/app/shared/components/markdown.factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								ui-ngx/src/app/shared/components/markdown.factory.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
			
		||||
///
 | 
			
		||||
/// Copyright © 2016-2021 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.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import { MarkedOptions, MarkedRenderer } from 'ngx-markdown';
 | 
			
		||||
 | 
			
		||||
export function markedOptionsFactory(): MarkedOptions {
 | 
			
		||||
  const renderer = new MarkedRenderer();
 | 
			
		||||
  const renderer2 = new MarkedRenderer();
 | 
			
		||||
 | 
			
		||||
  const copyCodeBlock = '{:copy-code}';
 | 
			
		||||
 | 
			
		||||
  let id = 1;
 | 
			
		||||
 | 
			
		||||
  renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => {
 | 
			
		||||
    if (code.endsWith(copyCodeBlock)) {
 | 
			
		||||
      code = code.substring(0, code.length - copyCodeBlock.length);
 | 
			
		||||
      const content = renderer2.code(code, language, isEscaped);
 | 
			
		||||
      id++;
 | 
			
		||||
      return wrapCopyCode(id, content, code);
 | 
			
		||||
    } else {
 | 
			
		||||
      return renderer2.code(code, language, isEscaped);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  renderer.tablecell = (content: string, flags: {
 | 
			
		||||
    header: boolean;
 | 
			
		||||
    align: 'center' | 'left' | 'right' | null;
 | 
			
		||||
    }) => {
 | 
			
		||||
    if (content.endsWith(copyCodeBlock)) {
 | 
			
		||||
      content = content.substring(0, content.length - copyCodeBlock.length);
 | 
			
		||||
      id++;
 | 
			
		||||
      content = wrapCopyCode(id, content, content);
 | 
			
		||||
    }
 | 
			
		||||
    return renderer2.tablecell(content, flags);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    renderer,
 | 
			
		||||
    headerIds: true,
 | 
			
		||||
    gfm: true,
 | 
			
		||||
    breaks: false,
 | 
			
		||||
    pedantic: false,
 | 
			
		||||
    smartLists: true,
 | 
			
		||||
    smartypants: false,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function wrapCopyCode(id: number, content: string, code: string): string {
 | 
			
		||||
  return '<div class="code-wrapper">' + content + '<span id="copyCodeId' + id + '" style="display: none;">' + code + '</span>' +
 | 
			
		||||
  '<button id="copyCodeBtn' + id + '" onClick="markdownCopyCode(' + id + ')" ' +
 | 
			
		||||
  'class="clipboard-btn"><img src="https://clipboardjs.com/assets/images/clippy.svg" alt="Copy to clipboard">' +
 | 
			
		||||
  '</button></div>';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
(window as any).markdownCopyCode = (id: number) => {
 | 
			
		||||
  const text = $('#copyCodeId' + id).text();
 | 
			
		||||
  navigator.clipboard.writeText(text).then(() => {
 | 
			
		||||
    import('tooltipster').then(
 | 
			
		||||
      () => {
 | 
			
		||||
        const copyBtn = $('#copyCodeBtn' + id);
 | 
			
		||||
        if (!copyBtn.hasClass('tooltipstered')) {
 | 
			
		||||
          copyBtn.tooltipster(
 | 
			
		||||
            {
 | 
			
		||||
              content: 'Copied!',
 | 
			
		||||
              theme: 'tooltipster-shadow',
 | 
			
		||||
              delay: 0,
 | 
			
		||||
              trigger: 'custom',
 | 
			
		||||
              triggerClose: {
 | 
			
		||||
                click: true,
 | 
			
		||||
                tap: true,
 | 
			
		||||
                scroll: true,
 | 
			
		||||
                mouseleave: true
 | 
			
		||||
              },
 | 
			
		||||
              side: 'bottom',
 | 
			
		||||
              distance: 12,
 | 
			
		||||
              trackOrigin: true
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        const tooltip = copyBtn.tooltipster('instance');
 | 
			
		||||
        tooltip.open();
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
@ -14,7 +14,7 @@
 | 
			
		||||
/// limitations under the License.
 | 
			
		||||
///
 | 
			
		||||
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { NgModule, SecurityContext } from '@angular/core';
 | 
			
		||||
import { CommonModule, DatePipe } from '@angular/common';
 | 
			
		||||
import { FooterComponent } from '@shared/components/footer.component';
 | 
			
		||||
import { LogoComponent } from '@shared/components/logo.component';
 | 
			
		||||
@ -78,6 +78,7 @@ import { DatetimePeriodComponent } from '@shared/components/time/datetime-period
 | 
			
		||||
import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe';
 | 
			
		||||
import { ClipboardModule } from 'ngx-clipboard';
 | 
			
		||||
import { ValueInputComponent } from '@shared/components/value-input.component';
 | 
			
		||||
import { MarkdownModule, MarkedOptions } from 'ngx-markdown';
 | 
			
		||||
import { FullscreenDirective } from '@shared/components/fullscreen.directive';
 | 
			
		||||
import { HighlightPipe } from '@shared/pipe/highlight.pipe';
 | 
			
		||||
import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component';
 | 
			
		||||
@ -145,6 +146,7 @@ import { OtaPackageAutocompleteComponent } from '@shared/components/ota-package/
 | 
			
		||||
import { MAT_DATE_LOCALE } from '@angular/material/core';
 | 
			
		||||
import { CopyButtonComponent } from '@shared/components/button/copy-button.component';
 | 
			
		||||
import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component';
 | 
			
		||||
import { markedOptionsFactory } from '@shared/components/markdown.factory';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  providers: [
 | 
			
		||||
@ -294,7 +296,15 @@ import { TogglePasswordComponent } from '@shared/components/button/toggle-passwo
 | 
			
		||||
    NgxHmCarouselModule,
 | 
			
		||||
    DndModule,
 | 
			
		||||
    NgxFlowModule,
 | 
			
		||||
    NgxFlowchartModule
 | 
			
		||||
    NgxFlowchartModule,
 | 
			
		||||
    // ngx-markdown
 | 
			
		||||
    MarkdownModule.forRoot({
 | 
			
		||||
      sanitize: SecurityContext.NONE,
 | 
			
		||||
      markedOptions: {
 | 
			
		||||
        provide: MarkedOptions,
 | 
			
		||||
        useFactory: markedOptionsFactory
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [
 | 
			
		||||
    FooterComponent,
 | 
			
		||||
@ -386,6 +396,7 @@ import { TogglePasswordComponent } from '@shared/components/button/toggle-passwo
 | 
			
		||||
    NgxHmCarouselModule,
 | 
			
		||||
    DndModule,
 | 
			
		||||
    NgxFlowchartModule,
 | 
			
		||||
    MarkdownModule,
 | 
			
		||||
    ConfirmDialogComponent,
 | 
			
		||||
    AlertDialogComponent,
 | 
			
		||||
    TodoDialogComponent,
 | 
			
		||||
 | 
			
		||||
@ -1817,6 +1817,11 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
 | 
			
		||||
  integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
 | 
			
		||||
 | 
			
		||||
"@types/marked@^1.1.0":
 | 
			
		||||
  version "1.2.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4"
 | 
			
		||||
  integrity sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw==
 | 
			
		||||
 | 
			
		||||
"@types/minimatch@*":
 | 
			
		||||
  version "3.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
 | 
			
		||||
@ -4137,6 +4142,11 @@ emoji-regex@^8.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
 | 
			
		||||
  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 | 
			
		||||
 | 
			
		||||
emoji-toolkit@^6.0.1:
 | 
			
		||||
  version "6.6.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/emoji-toolkit/-/emoji-toolkit-6.6.0.tgz#e7287c43a96f940ec4c5428cd7100a40e57518f1"
 | 
			
		||||
  integrity sha512-pEu0kow2p1N8zCKnn/L6H0F3rWUBB3P3hVjr/O5yl1fK7N9jU4vO4G7EFapC5Y3XwZLUCY0FZbOPyTkH+4V2eQ==
 | 
			
		||||
 | 
			
		||||
emojis-list@^3.0.0:
 | 
			
		||||
  version "3.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
 | 
			
		||||
@ -6126,6 +6136,13 @@ karma@~6.3.2:
 | 
			
		||||
    ua-parser-js "^0.7.23"
 | 
			
		||||
    yargs "^16.1.1"
 | 
			
		||||
 | 
			
		||||
katex@^0.12.0:
 | 
			
		||||
  version "0.12.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/katex/-/katex-0.12.0.tgz#2fb1c665dbd2b043edcf8a1f5c555f46beaa0cb9"
 | 
			
		||||
  integrity sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    commander "^2.19.0"
 | 
			
		||||
 | 
			
		||||
killable@^1.0.1:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
 | 
			
		||||
@ -6425,6 +6442,11 @@ map-visit@^1.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    object-visit "^1.0.0"
 | 
			
		||||
 | 
			
		||||
marked@^1.1.0:
 | 
			
		||||
  version "1.2.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc"
 | 
			
		||||
  integrity sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw==
 | 
			
		||||
 | 
			
		||||
material-design-icons@^3.0.1:
 | 
			
		||||
  version "3.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf"
 | 
			
		||||
@ -6877,6 +6899,18 @@ ngx-hm-carousel@^2.0.0-rc.1:
 | 
			
		||||
    hammerjs "^2.0.8"
 | 
			
		||||
    resize-observer-polyfill "^1.5.1"
 | 
			
		||||
 | 
			
		||||
ngx-markdown@^10.1.1:
 | 
			
		||||
  version "10.1.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-10.1.1.tgz#17840c773db7ced4b18ccbf2e8cb06182e422de3"
 | 
			
		||||
  integrity sha512-bUVgN6asb35d5U4xM5CNfo7pSpuwqJSdTgK0PhNZzLiaiyPIK2owtLF6sWGhxTThJu+LngJPjj4MQ+AFe/s8XQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/marked" "^1.1.0"
 | 
			
		||||
    emoji-toolkit "^6.0.1"
 | 
			
		||||
    katex "^0.12.0"
 | 
			
		||||
    marked "^1.1.0"
 | 
			
		||||
    prismjs "^1.20.0"
 | 
			
		||||
    tslib "^2.0.0"
 | 
			
		||||
 | 
			
		||||
ngx-sharebuttons@^8.0.5:
 | 
			
		||||
  version "8.0.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ngx-sharebuttons/-/ngx-sharebuttons-8.0.5.tgz#49481fcb8bf9541747fd72093eca6f4777c1d371"
 | 
			
		||||
@ -7877,6 +7911,11 @@ pretty-bytes@^5.3.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
 | 
			
		||||
  integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
 | 
			
		||||
 | 
			
		||||
prismjs@^1.20.0:
 | 
			
		||||
  version "1.24.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036"
 | 
			
		||||
  integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==
 | 
			
		||||
 | 
			
		||||
prismjs@^1.23.0:
 | 
			
		||||
  version "1.23.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user