21 changed files with 517 additions and 25 deletions
File diff suppressed because one or more lines are too long
@ -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 {{markdownClass}}" (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, 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; |
||||
|
|
||||
|
markdownClass: string; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
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; |
||||
|
this.markdownClass = 'markdown-widget'; |
||||
|
const cssString = this.settings.markdownCss; |
||||
|
if (isNotEmptyStr(cssString)) { |
||||
|
const cssParser = new cssjs(); |
||||
|
cssParser.testMode = false; |
||||
|
this.markdownClass += '-' + hashCode(cssString); |
||||
|
cssParser.cssPreviewNamespace = 'tb-markdown-view.' + this.markdownClass; |
||||
|
cssParser.createStyleElement(this.markdownClass, cssString); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
@ -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(); |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
}; |
||||
Loading…
Reference in new issue