15 changed files with 502 additions and 11 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" (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); |
|||
} |
|||
|
|||
} |
|||
@ -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