diff --git a/ui/component/or-components/src/or-ace-editor.ts b/ui/component/or-components/src/or-ace-editor.ts index ddab993c97..773a6e5865 100644 --- a/ui/component/or-components/src/or-ace-editor.ts +++ b/ui/component/or-components/src/or-ace-editor.ts @@ -4,6 +4,7 @@ import ace, {Ace} from "ace-builds"; import "ace-builds/src-noconflict/mode-javascript"; import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/mode-groovy"; +import "ace-builds/src-noconflict/mode-html"; import "ace-builds/webpack-resolver"; export class OrAceEditorChangedEvent extends CustomEvent<{ value: string, valid: boolean }> { diff --git a/ui/component/or-dashboard-builder/package.json b/ui/component/or-dashboard-builder/package.json index 137adb9826..58c6171140 100644 --- a/ui/component/or-dashboard-builder/package.json +++ b/ui/component/or-dashboard-builder/package.json @@ -21,6 +21,7 @@ "@openremote/or-attribute-report": "workspace:*", "@openremote/or-chart": "workspace:*", "@openremote/rest": "workspace:*", + "dompurify": "^3.2.4", "gridstack": "^7.2.0", "lit": "^2.0.2" }, diff --git a/ui/component/or-dashboard-builder/src/index.ts b/ui/component/or-dashboard-builder/src/index.ts index 6612746ca3..5c48d7a5da 100644 --- a/ui/component/or-dashboard-builder/src/index.ts +++ b/ui/component/or-dashboard-builder/src/index.ts @@ -31,6 +31,7 @@ import {MapWidget} from "./widgets/map-widget"; import {AttributeInputWidget} from "./widgets/attribute-input-widget"; import {TableWidget} from "./widgets/table-widget"; import {GatewayWidget} from "./widgets/gateway-widget"; +import {HtmlWidget} from "./widgets/html-widget"; import {ReportWidget} from "./widgets/report-widget"; // language=CSS @@ -219,6 +220,7 @@ export function registerWidgetTypes() { widgetTypes.set("attributeinput", AttributeInputWidget.getManifest()); widgetTypes.set("table", TableWidget.getManifest()); widgetTypes.set("gateway", GatewayWidget.getManifest()); + widgetTypes.set("html", HtmlWidget.getManifest()); widgetTypes.set("report", ReportWidget.getManifest()); } diff --git a/ui/component/or-dashboard-builder/src/widgets/html-widget.ts b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts new file mode 100644 index 0000000000..a98870c0fe --- /dev/null +++ b/ui/component/or-dashboard-builder/src/widgets/html-widget.ts @@ -0,0 +1,231 @@ +import {css, html, PropertyValues, TemplateResult } from "lit"; +import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; +import {OrWidget} from "../util/or-widget"; +import {WidgetConfig} from "../util/widget-config"; +import {WidgetManifest} from "../util/or-widget"; +import {WidgetSettings} from "../util/widget-settings"; +import {OrMwcDialog, showDialog} from "@openremote/or-mwc-components/or-mwc-dialog"; +import { customElement, query} from "lit/decorators.js"; +import {createRef, Ref, ref } from "lit/directives/ref.js"; +import { InputType } from "@openremote/or-mwc-components/or-mwc-input"; +import {throttle} from "lodash"; +import {i18next} from "@openremote/or-translate"; +import {OrAceEditor} from "@openremote/or-components/or-ace-editor"; +import "ace-builds/src-noconflict/mode-html"; +import "ace-builds/webpack-resolver"; +import {showSnackbar} from "@openremote/or-mwc-components/or-mwc-snackbar"; +import DOMPurify from 'dompurify' + +export interface HtmlWidgetConfig extends WidgetConfig { + html: string, + sanitizerConfig: Object +} + +function getDefaultConfig(): HtmlWidgetConfig { + return { + html: '\n' + + '
\n' + + ' OpenRemote \n' + + ' HTML widget
Paste your HTML, use any WYSIWYG editor for easy generation.
\n' + + ' 
Write markup with HTML
', + sanitizerConfig: { + ALLOWED_TAGS: ['p', 'div', 'span', 'img', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3'], + ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'style', 'src'], + ALLOWED_STYLES: [ + 'color', 'font-size', 'text-align', 'margin', 'padding', + 'font-weight', 'font-style', 'text-decoration', 'background' + ], + ADD_TAGS: ['!doctype'], + ADD_ATTR: ['target'], // Allow target="_blank" for links + RETURN_DOM_FRAGMENT: false, + RETURN_DOM: false, + WHOLE_DOCUMENT: true, + RETURN_TRUSTED_TYPE: true + } + } +} + +const styling = css` + #widget-wrapper { + height: 100%; + justify-content: center; + align-items: center; + overflow: hidden; + } + + #error-txt { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + } + + .attr-input { + width: 100%; + box-sizing: border-box; + } + + .switch-container { + display: flex; + align-items: center; + justify-content: space-between; + } +` + +@customElement("html-widget") +export class HtmlWidget extends OrWidget { + + protected widgetConfig!: HtmlWidgetConfig; + + @query("#widget-wrapper") + protected widgetWrapperElem?: HTMLElement; + + protected resizeObserver?: ResizeObserver; + + static getManifest(): WidgetManifest { + return { + displayName: "HTML", + displayIcon: "language-html5", + getContentHtml(config: WidgetConfig): OrWidget { + return new HtmlWidget(config); + }, + getDefaultConfig(): WidgetConfig { + return getDefaultConfig(); + }, + getSettingsHtml(config: WidgetConfig): WidgetSettings { + return new HtmlSettings(config); + } + + } + } + + // TODO: Improve this to be more efficient + refreshContent(force: boolean): void { + this.widgetConfig = JSON.parse(JSON.stringify(this.widgetConfig)) as HtmlWidgetConfig; + } + + static get styles() { + return [...super.styles, styling]; + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.resizeObserver?.disconnect(); + delete this.resizeObserver; + } + + protected willUpdate(changedProps: PropertyValues) { + + // If widgetConfig, and the attributeRefs of them have changed... + if(changedProps.has("widgetConfig") && this.widgetConfig) { + } + + // Workaround for an issue with scalability of or-attribute-input when using 'display: flex'. + // The percentage slider doesn't scale properly, causing the dragging knob to glitch. + // Why? Because the Material Design element listens to a window resize, not a container resize. + // So we manually trigger this event when the attribute-input-widget changes in size. + if(!this.resizeObserver && this.widgetWrapperElem) { + this.resizeObserver = new ResizeObserver(throttle(() => { + window.dispatchEvent(new Event('resize')); + }, 200)); + this.resizeObserver.observe(this.widgetWrapperElem); + } + + return super.willUpdate(changedProps); + } + + + protected render(): TemplateResult { + const sanitizedContent = this.getSanitizedContent(); + return html` +