UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

623 lines (594 loc) 46.9 kB
import { NgIf, NgFor, NgClass, AsyncPipe } from '@angular/common'; import * as i0 from '@angular/core'; import { inject, Injectable, Input, Component, viewChild, SecurityContext, ViewChild } from '@angular/core'; import * as i2 from '@angular/forms'; import { FormsModule } from '@angular/forms'; import { Router, RouterModule } from '@angular/router'; import * as i2$1 from '@c8y/ngx-components'; import { AppStateService, Permissions, gettext, IconDirective, C8yTranslatePipe, TabsModule, LoadingComponent, OptionsService } from '@c8y/ngx-components'; import { WidgetConfigService, WidgetConfigFeedbackComponent } from '@c8y/ngx-components/context-dashboard'; import * as i1 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import * as i3 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { isEmpty } from 'lodash'; import { Subject, map, filter, withLatestFrom, switchMap, shareReplay, takeUntil, startWith, combineLatest, distinctUntilChanged, debounceTime, of, merge, from, isEmpty as isEmpty$1, catchError, EMPTY, fromEvent } from 'rxjs'; import { DomSanitizer } from '@angular/platform-browser'; import { InventoryService } from '@c8y/client'; import { kebabCase } from 'lodash-es'; import { EditorComponent } from '@c8y/ngx-components/editor'; import { KeyMod, KeyCode } from 'monaco-editor'; const INITIAL_HTML_FORMATTED = `<div> <h1>Hello from HTML widget</h1> <p> You can use HTML and Javascript template literals here: \$\{this.c8yContext ? this.c8yContext.name : 'No device selected'\} </p> <a class="btn btn-primary" href="#/group">Go to groups</a> <p> Use the CSS editor to add CSS. You can use <span class="branded">any design-token CSS variable</span> in there. </p> </div>`; const INITIAL_CSS_FORMATTED = ` :host > div { padding-left: var(--c8y-root-component-padding); padding-right: var(--c8y-root-component-padding); } span.branded { color: var(--c8y-brand-primary); }`; const defaultWebComponentName = 'DefaultWebComponent'; const defaultWebComponentAttributeNameContext = 'c8yContext'; const webComponentTemplate = (html, css, viewEncapsulation, name = defaultWebComponentName) => ` import { LitElement, html, css} from 'lit'; ${!viewEncapsulation ? `import { styleImports } from 'styles';` : ''} export default class ${name} extends LitElement { static styles = css\` ${css} \`; static properties = { // The managed object this widget is assigned to. Can be null. c8yContext: { type: Object }, }; constructor() { super(); } render() { return html\`${viewEncapsulation ? html : ` <style> \${styleImports} </style> ${html} `}\`; } } `; const legacyTemplate = (html, deviceId, deviceName) => ` import { angular } from 'angular'; // NOTE: This is a legacy template for the HTML widget. // It is used to compile the HTML content in the context of the AngularJS application. // The template is injected into the AngularJS application and compiled using the AngularJS compiler. // The template should only be used for backward compatibility purposes. // It is recommended to use a web component instead. if(!angular) { throw new Error('AngularJS is not available. Please make sure to include AngularJS in your project.'); } const $injector = angular.element(document.querySelector('c8y-ui-root')).injector(); if (!$injector) { throw new Error('AngularJS injector is not available. Maybe not an hybrid application?'); } // defining a new scope const $rootScope = $injector.get('$rootScope'); const $scope = $rootScope.$new(true); // faking the old angularjs config $scope.child = { config: { ${deviceId ? `device: { id: "${deviceId}", name: "${deviceName}" },` : ''} html: \`<div ng-controller="HtmlWidgetCtrl">${html}</div>\` } }; // load the needed services const $compile = $injector.get('$compile'); const $controller = $injector.get('$controller'); // create the element const htmlElement = angular.element($scope.child.config.html); // The default controller providing the context $controller('HtmlWidgetCtrl', { $scope }); // Compile the element $compile(htmlElement)($scope); // Apply the scope changes $rootScope.$apply(); export default htmlElement[0];`; class HtmlWidgetConfigService { constructor() { this.DEFAULT_AUTO_SAVE_DEBOUNCE = 1000; this.codeChange$ = new Subject(); this.widgetConfigService = inject(WidgetConfigService); this.appState = inject(AppStateService); this.destroy$ = new Subject(); this.init$ = this.widgetConfigService.currentConfig$.pipe(map(current => { if (current.html) { current.config = this.mapLegacyConfig(current); } return (current.config || {}); }), filter(config => !!config), withLatestFrom(this.appState.currentApplicationConfig), switchMap(([widgetConfig, appConfig]) => this.initConfig(appConfig, widgetConfig)), shareReplay(), takeUntil(this.destroy$)); this.config$ = this.init$.pipe(switchMap(initValue => this.configChanged$.pipe(startWith(initValue)))); this.codeEditorChangeConfig$ = combineLatest([ this.codeChange$.pipe(startWith(undefined)), this.config$ ]).pipe(distinctUntilChanged(), takeUntil(this.destroy$), debounceTime(this.DEFAULT_AUTO_SAVE_DEBOUNCE), map(([change, config]) => { if (!change) { return config; } if (change.type === 'css') { config.css = change.value; } else { config.code = change.value; } return { ...config }; })); this.configChanged$ = new Subject(); } initConfig(appConfig, widgetConfig) { const defaultToAdvancedMode = appConfig?.htmlWidgetDefaultToAdvancedMode ?? false; const isEmptyConfig = isEmpty(widgetConfig); if (isEmptyConfig && !defaultToAdvancedMode) { widgetConfig = this.initDefaultMode(!appConfig.htmlWidgetDisableSanitization); this.save(widgetConfig); return of(widgetConfig); } if (isEmptyConfig) { widgetConfig = this.enableAdvancedMode(widgetConfig); } // new config is needed to trigger ngOnChanges const newConfig = { ...widgetConfig }; this.save(newConfig); return of(newConfig); } destroy() { this.destroy$.next(); this.codeChange$.complete(); this.configChanged$.complete(); } save(config) { this.widgetConfigService.updateConfig({ config }); } changeCode(value) { this.codeChange$.next({ value, type: 'code' }); } changeCss(value) { this.codeChange$.next({ value, type: 'css' }); } enableAdvancedMode(currentConfig) { const currentHTML = currentConfig?.code || INITIAL_HTML_FORMATTED; const currentCSS = currentConfig?.css || INITIAL_CSS_FORMATTED; const code = currentConfig?.legacy ? legacyTemplate(currentHTML, this.widgetConfigService.currentConfig?.device?.id, this.widgetConfigService.currentConfig?.device?.name) : webComponentTemplate(currentHTML, currentCSS, false); currentConfig = { css: '', code, legacy: false, devMode: true, options: { cssEncapsulation: false, advancedSecurity: false } }; return currentConfig; } initDefaultMode(advancedSecurity = true) { return { css: INITIAL_CSS_FORMATTED, code: INITIAL_HTML_FORMATTED, legacy: false, devMode: false, options: { cssEncapsulation: false, advancedSecurity } }; } mapLegacyConfig(current) { const isAlreadyInAdvancedMode = current?.config?.devMode === true; if (isAlreadyInAdvancedMode) { return current.config; } const isAlreadyMapped = current?.config?.legacy === true; if (isAlreadyMapped) { return current.config; } return { code: current.html, css: '', legacy: true, devMode: false, options: { cssEncapsulation: false, advancedSecurity: current.sanitization === 'strict' } }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlWidgetConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlWidgetConfigService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlWidgetConfigService, decorators: [{ type: Injectable }] }); class AdvancedSettingsComponent { constructor() { this.widgetConfigService = inject(WidgetConfigService); this.htmlWidgetConfigService = inject(HtmlWidgetConfigService); this.router = inject(Router); this.permissionService = inject(Permissions); this.canChangeSettings = false; this.CSS_ENCAPSULATION_HELP_CONTEXT = gettext('If enabled, the CSS will be encapsulated and no platform styling will be applied.'); } ngOnInit() { this.canChangeSettings = this.permissionService.hasAnyRole([ Permissions.ROLE_APPLICATION_MANAGEMENT_ADMIN, Permissions.ROLE_TENANT_ADMIN ]); } disableAdvancedMode() { const config = this.htmlWidgetConfigService.initDefaultMode(); this.htmlWidgetConfigService.save(config); this.htmlWidgetConfigService.configChanged$.next(config); } enableAdvancedMode() { let { config } = this.widgetConfigService.currentConfig; config = this.htmlWidgetConfigService.enableAdvancedMode(config); this.htmlWidgetConfigService.save(config); this.htmlWidgetConfigService.configChanged$.next(config); } toggleAdvancedMode() { this.devMode ? this.disableAdvancedMode() : this.enableAdvancedMode(); this.devMode = !this.devMode; } async changeOption(option) { const { config } = this.widgetConfigService.currentConfig; config.options[option] = !config.options[option]; this.htmlWidgetConfigService.save(config); this.htmlWidgetConfigService.configChanged$.next(config); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AdvancedSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: AdvancedSettingsComponent, isStandalone: true, selector: "c8y-html-widget-advanced-settings", inputs: { devMode: "devMode", cssEncapsulation: "cssEncapsulation" }, ngImport: i0, template: "<fieldset class=\"c8y-fieldset m-t-0\">\n <legend>{{ 'Developer mode' | translate }}</legend>\n\n <div class=\"d-flex a-i-center p-b-16\">\n <label class=\"c8y-switch\">\n <input\n type=\"checkbox\"\n [ngModel]=\"devMode\"\n (change)=\"toggleAdvancedMode()\"\n [disabled]=\"!canChangeSettings\"\n />\n <span></span>\n <span>{{ 'Advanced developer mode' | translate }}</span>\n </label>\n\n <button class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"devMode ? disableAdvanced : enableAdvanced\"\n container=\"body\"\n placement=\"right\"\n triggers=\"focus\"\n type=\"button\"\n ></button>\n\n <ng-template #enableAdvanced>\n <p class=\"text-16 text-bold p-b-8\">\n <i [c8yIcon]=\"'imac-settings'\"></i>\n {{ 'Advanced developer mode' | translate }}\n </p>\n <p class=\"p-b-8\" translate>\n Create custom widgets by modifying a basic WebComponent with HTML and JavaScript. This\n <strong>unsupported</strong>\n feature is ideal for rapid prototyping and simple customizations.\n </p>\n <p class=\"p-b-8\" translate>\n For production environments, we recommend our fully-supported Angular-based\n <a href=\"https://styleguide.cumulocity.com\" target=\"_blank\">Web SDK</a>.\n <br />\n Enable advanced developer mode to start coding!\n </p>\n </ng-template>\n\n <ng-template #disableAdvanced>\n <p class=\"text-16 text-bold p-b-8\">\n {{ 'Advanced developer mode' | translate }}\n </p>\n <p class=\"p-b-8\" translate>\n The advanced developer mode is enabled for this widget allowing to build extensive Web\n Components.\n </p>\n <p class=\"p-b-8\" translate>\n You can disable this mode again, but it will reset the current code.\n </p>\n </ng-template>\n\n <ng-container *ngIf=\"!devMode\">\n <label\n class=\"c8y-switch m-l-auto\"\n >\n <input\n type=\"checkbox\"\n (change)=\"changeOption('cssEncapsulation')\"\n [disabled]=\"!canChangeSettings\"\n [ngModel]=\"cssEncapsulation\"\n />\n <span></span>\n <span>{{ 'CSS encapsulation' | translate }}</span>\n </label>\n <button\n class=\"btn-help m-0\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ CSS_ENCAPSULATION_HELP_CONTEXT | translate }}\"\n triggers=\"focus\"\n placement=\"left\"\n container=\"body\"\n type=\"button\"\n ></button>\n </ng-container>\n\n </div>\n</fieldset>\n", dependencies: [{ kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i1.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AdvancedSettingsComponent, decorators: [{ type: Component, args: [{ standalone: true, imports: [IconDirective, NgIf, TooltipModule, PopoverModule, C8yTranslatePipe, FormsModule], selector: 'c8y-html-widget-advanced-settings', template: "<fieldset class=\"c8y-fieldset m-t-0\">\n <legend>{{ 'Developer mode' | translate }}</legend>\n\n <div class=\"d-flex a-i-center p-b-16\">\n <label class=\"c8y-switch\">\n <input\n type=\"checkbox\"\n [ngModel]=\"devMode\"\n (change)=\"toggleAdvancedMode()\"\n [disabled]=\"!canChangeSettings\"\n />\n <span></span>\n <span>{{ 'Advanced developer mode' | translate }}</span>\n </label>\n\n <button class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"devMode ? disableAdvanced : enableAdvanced\"\n container=\"body\"\n placement=\"right\"\n triggers=\"focus\"\n type=\"button\"\n ></button>\n\n <ng-template #enableAdvanced>\n <p class=\"text-16 text-bold p-b-8\">\n <i [c8yIcon]=\"'imac-settings'\"></i>\n {{ 'Advanced developer mode' | translate }}\n </p>\n <p class=\"p-b-8\" translate>\n Create custom widgets by modifying a basic WebComponent with HTML and JavaScript. This\n <strong>unsupported</strong>\n feature is ideal for rapid prototyping and simple customizations.\n </p>\n <p class=\"p-b-8\" translate>\n For production environments, we recommend our fully-supported Angular-based\n <a href=\"https://styleguide.cumulocity.com\" target=\"_blank\">Web SDK</a>.\n <br />\n Enable advanced developer mode to start coding!\n </p>\n </ng-template>\n\n <ng-template #disableAdvanced>\n <p class=\"text-16 text-bold p-b-8\">\n {{ 'Advanced developer mode' | translate }}\n </p>\n <p class=\"p-b-8\" translate>\n The advanced developer mode is enabled for this widget allowing to build extensive Web\n Components.\n </p>\n <p class=\"p-b-8\" translate>\n You can disable this mode again, but it will reset the current code.\n </p>\n </ng-template>\n\n <ng-container *ngIf=\"!devMode\">\n <label\n class=\"c8y-switch m-l-auto\"\n >\n <input\n type=\"checkbox\"\n (change)=\"changeOption('cssEncapsulation')\"\n [disabled]=\"!canChangeSettings\"\n [ngModel]=\"cssEncapsulation\"\n />\n <span></span>\n <span>{{ 'CSS encapsulation' | translate }}</span>\n </label>\n <button\n class=\"btn-help m-0\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ CSS_ENCAPSULATION_HELP_CONTEXT | translate }}\"\n triggers=\"focus\"\n placement=\"left\"\n container=\"body\"\n type=\"button\"\n ></button>\n </ng-container>\n\n </div>\n</fieldset>\n" }] }], propDecorators: { devMode: [{ type: Input }], cssEncapsulation: [{ type: Input }] } }); class HtmlFrameComponent { constructor() { /** * If set to true, it will be ensured that a unique hash is generated * for every webcomponent. This is useful if configured as otherwise it might * happen that the same code is already used in another webcomponent and the * error messages can not be assigned correctly. */ this.useSalt = false; this.alerts = []; this.sanitizer = inject(DomSanitizer); this.destroy$ = new Subject(); this.hostElement = viewChild('hostElement'); this.reload$ = new Subject(); this.htmlContentInitialization$ = this.reload$.pipe(filter(() => !!this.hostElement()), map(() => this.hostElement().nativeElement), switchMap((div) => merge(this.listenToErrors(), from(this.initDiv(div)).pipe(isEmpty$1(), filter(isEmpty => !!isEmpty), catchError(error => from([{ text: error, type: 'danger' }]))))), filter(alert => !!alert), takeUntil(this.destroy$)); this.inventoryService = inject(InventoryService); this.htmlContentInitialization$.pipe(takeUntil(this.destroy$)).subscribe((alert) => { this.alerts.push(alert); console.error(alert.text); }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } ngOnChanges(changes) { if (changes.config?.currentValue || (this.config && changes.device?.previousValue !== changes.device?.currentValue)) { this.reloadComponent(); } } reloadComponent() { this.alerts = []; const div = this.hostElement(); div.nativeElement.innerHTML = ''; this.reload$.next(); } async initDiv(divHostElement) { const code = this.getCode(); const hash = await this.generateHash(code, this.useSalt); const webComponentName = kebabCase(defaultWebComponentName) + hash; const context = await this.getContext(this.device); if (customElements.get(webComponentName)) { return this.createWebComponent(webComponentName, divHostElement, context); } const url = this.generateUrl(code); const defaultModule = await this.loadScript(url); // if the default module is a string, we will not use a webcomponent // instead we will simply parse the string and add it to the div // this is the case for legacy HTML widgets if (typeof defaultModule.default === 'string') { divHostElement.innerHTML = defaultModule.default; return EMPTY; } // same goes for an HTML Element if (defaultModule.default instanceof HTMLElement) { divHostElement.appendChild(defaultModule.default); // Find and execute scripts const scripts = divHostElement.querySelectorAll('script'); scripts.forEach(script => { const newScript = document.createElement('script'); Array.from(script.attributes).forEach(attr => { newScript.setAttribute(attr.name, attr.value); }); newScript.textContent = script.textContent; script.parentNode.replaceChild(newScript, script); }); return EMPTY; } // as a race condition can happen on loading, we need // to check again if the web component is already defined if (!customElements.get(webComponentName)) { customElements.define(webComponentName, defaultModule.default); } this.createWebComponent(webComponentName, divHostElement, context); return EMPTY; } async getContext(device) { if (!device) { return; } if (!device.self) { const { data } = await this.inventoryService.detail(device.id); return data; } return device; } async loadScript(url) { const module = await import(/* webpackIgnore: true */ url); if (!module.default) { throw 'No default export found. Add an "export default" statement to your code.'; } return module; } generateUrl(script) { const blob = new Blob([script], { type: 'application/javascript' }); const url = URL.createObjectURL(blob); if (this.latestUrl) { URL.revokeObjectURL(this.latestUrl); } this.latestUrl = url; return url; } listenToErrors() { const errorEvents$ = fromEvent(window, 'error').pipe(filter(event => event.filename?.includes(this.latestUrl)), map(event => this.mapErrorEventToAlert(event))); const rejectionEvents$ = fromEvent(window, 'unhandledrejection').pipe(filter(event => event.reason?.stack?.includes(this.latestUrl)), map(event => this.mapErrorEventToAlert(event)), takeUntil(this.destroy$)); return merge(errorEvents$, rejectionEvents$); } createWebComponent(webComponentName, divHostElement, context) { const webComponent = document.createElement(webComponentName); webComponent.c8yContext = context; divHostElement.appendChild(webComponent); return webComponent; } mapErrorEventToAlert(event) { const hasReason = 'reason' in event; if (hasReason && event.reason?.name === ReferenceError.name) { const undefinedVar = event.reason.message.split(' ')[0]; return { text: undefinedVar + ' is not defined', type: 'info' }; } return { text: hasReason ? event.reason.message : event.message, type: 'danger' }; } getCode() { const isDevMode = this.config.devMode; if (isDevMode) { return this.config.code; } return this.createDefaultWebcomponentCode(); } async generateHash(value, useSalt) { const encoder = new TextEncoder(); const data = encoder.encode(value + (useSalt ? Math.random() : '')); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); return hashHex; } createDefaultWebcomponentCode() { if (this.config.legacy) { const legacyWebComponent = legacyTemplate(this.config.options.advancedSecurity ? this.sanitizer.sanitize(SecurityContext.HTML, this.config.code) : this.config.code, this.device?.id, this.device?.name); return legacyWebComponent; } const webComponentScript = webComponentTemplate(this.config.options.advancedSecurity ? this.sanitizer.sanitize(SecurityContext.HTML, this.config.code) : this.config.code, this.config.options.advancedSecurity ? this.sanitizer.sanitize(SecurityContext.STYLE, this.config.css) : this.config.css, this.config.options.cssEncapsulation); return webComponentScript; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.14", type: HtmlFrameComponent, isStandalone: true, selector: "c8y-html-frame", inputs: { config: "config", device: "device", useSalt: "useSalt" }, host: { classAttribute: "d-contents" }, viewQueries: [{ propertyName: "hostElement", first: true, predicate: ["hostElement"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *ngFor=\"let alert of alerts\">\n <div\n class=\"alert m-8\"\n role=\"alert\"\n [ngClass]=\"{\n 'alert-danger': alert.type === 'danger',\n 'alert-warning': alert.type === 'warning',\n 'alert-info': alert.type === 'info',\n 'alert-success': alert.type === 'success'\n }\"\n >\n <p><strong translate>There was an issue in the HTML widget:</strong></p>\n <pre>{{ alert.text }}</pre>\n </div>\n</ng-container>\n<div\n class=\"fit-w fit-h\"\n #hostElement\n></div>\n", dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlFrameComponent, decorators: [{ type: Component, args: [{ standalone: true, imports: [NgFor, NgClass], selector: 'c8y-html-frame', host: { class: 'd-contents' }, template: "<ng-container *ngFor=\"let alert of alerts\">\n <div\n class=\"alert m-8\"\n role=\"alert\"\n [ngClass]=\"{\n 'alert-danger': alert.type === 'danger',\n 'alert-warning': alert.type === 'warning',\n 'alert-info': alert.type === 'info',\n 'alert-success': alert.type === 'success'\n }\"\n >\n <p><strong translate>There was an issue in the HTML widget:</strong></p>\n <pre>{{ alert.text }}</pre>\n </div>\n</ng-container>\n<div\n class=\"fit-w fit-h\"\n #hostElement\n></div>\n" }] }], ctorParameters: () => [], propDecorators: { config: [{ type: Input }], device: [{ type: Input }], useSalt: [{ type: Input }] } }); class WidgetCodeEditorComponent { constructor() { this.mode = 'code'; this.configService = inject(HtmlWidgetConfigService); this.isAutoSaveEnabled = true; this.language = 'html'; this.isLoading = false; this.TAB_WEBCOMPONENT_LABEL = gettext('Web Component`Tab label of HTML Widget`'); this.TAB_HTML_LABEL = gettext('HTML`Tab label of HTML Widget`'); this.TAB_CSS_LABEL = gettext('CSS`Tab label of HTML Widget`'); this.BUTTON_DISABLE_AUTOSAVE_LABEL = gettext('Disable auto save`An action you can do on the html widget editor`'); this.BUTTON_ENABLE_AUTOSAVE_LABEL = gettext('Enable auto save`An action you can do on the html widget editor`'); this.TAB_OUTLET_NAME = 'html-widget-tab-outlet'; this.destroy$ = new Subject(); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } ngOnChanges(changes) { if (changes.config?.currentValue) { this.loadCode(); } } loadCode() { this.isLoading = true; const isInDevMode = this.config?.devMode; this.language = 'html'; if (isInDevMode) { this.language = 'javascript'; } if (this.mode === 'css') { this.language = 'css'; } this.value = this.mode === 'code' ? this.config.code : this.config.css; this.isLoading = false; if (this.editor) { queueMicrotask(() => this.formatCode()); } } switchMode(mode) { this.mode = mode; this.loadCode(); } editorLoaded(editor) { this.editor = editor; this.editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, () => { this.saveCode(); }); } formatCode() { this.editor.getAction('editor.action.formatDocument').run(); } redo() { this.editor.trigger('keyboard', 'redo', null); } undo() { this.editor.trigger('keyboard', 'undo', null); } changeCode($event) { if (this.isAutoSaveEnabled) { this.saveCode($event); } } saveCode(codeStr) { const code = codeStr || this.editor.getValue(); if (this.mode === 'code') { this.configService.changeCode(code); return; } this.configService.changeCss(code); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetCodeEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: WidgetCodeEditorComponent, isStandalone: true, selector: "c8y-widget-code-editor", inputs: { mode: "mode", config: "config" }, usesOnChanges: true, ngImport: i0, template: "<c8y-widget-config-feedback>\n <div class=\"d-flex\">\n <span\n class=\"tag tag--warning text-12\"\n *ngIf=\"config?.devMode && !config?.legacy\"\n translate\n >\n Advanced developer mode\n </span>\n </div>\n <div class=\"d-flex\">\n <span\n class=\"tag tag--warning text-12\"\n [title]=\"\n 'This widget is in legacy mode. Consider to upgrade this to a new HTML widget. Read our documentation on details to transform your widget'\n | translate\n \"\n *ngIf=\"config?.legacy\"\n translate\n >\n Legacy mode\n </span>\n </div>\n</c8y-widget-config-feedback>\n\n<div class=\"d-flex d-col fit-h fit-w\">\n <c8y-html-widget-advanced-settings\n [devMode]=\"config?.devMode\"\n [cssEncapsulation]=\"config?.options?.cssEncapsulation\"\n ></c8y-html-widget-advanced-settings>\n\n <fieldset class=\"c8y-fieldset p-0 overflow-hidden\">\n <legend class=\"m-l-16 p-l-0\">{{ 'Code' | translate }}</legend>\n\n <div class=\"btn-group btn-group-sm m-l-0 p-t-8 p-b-8 p-l-16 p-r-16 fit-w d-flex\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Undo' | translate\"\n [tooltip]=\"'Undo' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"undo()\"\n >\n <i [c8yIcon]=\"'undo'\"></i>\n </button>\n\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Redo' | translate\"\n [tooltip]=\"'Redo' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"redo()\"\n >\n <i [c8yIcon]=\"'redo'\"></i>\n </button>\n\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Format code' | translate\"\n [tooltip]=\"'Format code' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"formatCode()\"\n >\n <i [c8yIcon]=\"'format-align-left'\"></i>\n </button>\n\n <label class=\"c8y-switch m-l-auto\">\n <input\n type=\"checkbox\"\n [checked]=\"isAutoSaveEnabled\"\n (change)=\"isAutoSaveEnabled = !isAutoSaveEnabled\"\n />\n <span></span>\n <span translate>Auto save</span>\n </label>\n </div>\n\n <div\n class=\"btn-toolbar m-0 p-relative\"\n role=\"toolbar\"\n >\n <c8y-tabs-outlet\n class=\"elevation-none\"\n [outletName]=\"TAB_OUTLET_NAME\"\n [orientation]=\"'horizontal'\"\n [openFirstTab]=\"false\"\n ></c8y-tabs-outlet>\n <c8y-tab\n [icon]=\"'code'\"\n [label]=\"(config?.devMode ? TAB_WEBCOMPONENT_LABEL : TAB_HTML_LABEL) | translate\"\n [priority]=\"100\"\n [showAlways]=\"true\"\n [tabsOutlet]=\"TAB_OUTLET_NAME\"\n [isActive]=\"mode === 'code'\"\n (onSelect)=\"switchMode('code')\"\n ></c8y-tab>\n <c8y-tab\n [icon]=\"'c8y-css'\"\n [label]=\"TAB_CSS_LABEL | translate\"\n [priority]=\"0\"\n [tabsOutlet]=\"TAB_OUTLET_NAME\"\n [isActive]=\"mode === 'css'\"\n (onSelect)=\"switchMode('css')\"\n *ngIf=\"!config?.devMode && !config?.legacy\"\n ></c8y-tab>\n </div>\n\n <ng-container *ngIf=\"!isLoading; else loading\">\n <c8y-editor\n class=\"flex-grow d-block\"\n style=\"height: 450px\"\n *ngIf=\"!(mode === 'css' && config?.devMode)\"\n [ngModel]=\"value\"\n (ngModelChange)=\"changeCode($event)\"\n [editorOptions]=\"{\n language,\n tabSize: 2,\n insertSpaces: true,\n minimap: { enabled: false }\n }\"\n (editorInit)=\"editorLoaded($event)\"\n ></c8y-editor>\n </ng-container>\n <ng-template #loading>\n <c8y-loading></c8y-loading>\n </ng-template>\n </fieldset>\n</div>\n", dependencies: [{ kind: "component", type: EditorComponent, selector: "c8y-editor", inputs: ["editorOptions", "theme"], outputs: ["editorInit"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: WidgetConfigFeedbackComponent, selector: "c8y-widget-config-feedback" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i2$1.TabsOutletComponent, selector: "c8y-tabs-outlet,c8y-ui-tabs", inputs: ["tabs", "orientation", "navigatorOpen", "outletName", "context", "openFirstTab", "hasHeader"] }, { kind: "component", type: i2$1.TabComponent, selector: "c8y-tab", inputs: ["path", "label", "icon", "priority", "orientation", "injector", "tabsOutlet", "isActive", "showAlways"], outputs: ["onSelect"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i3.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "component", type: AdvancedSettingsComponent, selector: "c8y-html-widget-advanced-settings", inputs: ["devMode", "cssEncapsulation"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: WidgetCodeEditorComponent, decorators: [{ type: Component, args: [{ standalone: true, imports: [ EditorComponent, FormsModule, IconDirective, C8yTranslatePipe, NgIf, WidgetConfigFeedbackComponent, TabsModule, TooltipModule, PopoverModule, LoadingComponent, AdvancedSettingsComponent ], selector: 'c8y-widget-code-editor', template: "<c8y-widget-config-feedback>\n <div class=\"d-flex\">\n <span\n class=\"tag tag--warning text-12\"\n *ngIf=\"config?.devMode && !config?.legacy\"\n translate\n >\n Advanced developer mode\n </span>\n </div>\n <div class=\"d-flex\">\n <span\n class=\"tag tag--warning text-12\"\n [title]=\"\n 'This widget is in legacy mode. Consider to upgrade this to a new HTML widget. Read our documentation on details to transform your widget'\n | translate\n \"\n *ngIf=\"config?.legacy\"\n translate\n >\n Legacy mode\n </span>\n </div>\n</c8y-widget-config-feedback>\n\n<div class=\"d-flex d-col fit-h fit-w\">\n <c8y-html-widget-advanced-settings\n [devMode]=\"config?.devMode\"\n [cssEncapsulation]=\"config?.options?.cssEncapsulation\"\n ></c8y-html-widget-advanced-settings>\n\n <fieldset class=\"c8y-fieldset p-0 overflow-hidden\">\n <legend class=\"m-l-16 p-l-0\">{{ 'Code' | translate }}</legend>\n\n <div class=\"btn-group btn-group-sm m-l-0 p-t-8 p-b-8 p-l-16 p-r-16 fit-w d-flex\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Undo' | translate\"\n [tooltip]=\"'Undo' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"undo()\"\n >\n <i [c8yIcon]=\"'undo'\"></i>\n </button>\n\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Redo' | translate\"\n [tooltip]=\"'Redo' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"redo()\"\n >\n <i [c8yIcon]=\"'redo'\"></i>\n </button>\n\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Format code' | translate\"\n [tooltip]=\"'Format code' | translate\"\n placement=\"top\"\n container=\"body\"\n type=\"button\"\n [delay]=\"500\"\n (click)=\"formatCode()\"\n >\n <i [c8yIcon]=\"'format-align-left'\"></i>\n </button>\n\n <label class=\"c8y-switch m-l-auto\">\n <input\n type=\"checkbox\"\n [checked]=\"isAutoSaveEnabled\"\n (change)=\"isAutoSaveEnabled = !isAutoSaveEnabled\"\n />\n <span></span>\n <span translate>Auto save</span>\n </label>\n </div>\n\n <div\n class=\"btn-toolbar m-0 p-relative\"\n role=\"toolbar\"\n >\n <c8y-tabs-outlet\n class=\"elevation-none\"\n [outletName]=\"TAB_OUTLET_NAME\"\n [orientation]=\"'horizontal'\"\n [openFirstTab]=\"false\"\n ></c8y-tabs-outlet>\n <c8y-tab\n [icon]=\"'code'\"\n [label]=\"(config?.devMode ? TAB_WEBCOMPONENT_LABEL : TAB_HTML_LABEL) | translate\"\n [priority]=\"100\"\n [showAlways]=\"true\"\n [tabsOutlet]=\"TAB_OUTLET_NAME\"\n [isActive]=\"mode === 'code'\"\n (onSelect)=\"switchMode('code')\"\n ></c8y-tab>\n <c8y-tab\n [icon]=\"'c8y-css'\"\n [label]=\"TAB_CSS_LABEL | translate\"\n [priority]=\"0\"\n [tabsOutlet]=\"TAB_OUTLET_NAME\"\n [isActive]=\"mode === 'css'\"\n (onSelect)=\"switchMode('css')\"\n *ngIf=\"!config?.devMode && !config?.legacy\"\n ></c8y-tab>\n </div>\n\n <ng-container *ngIf=\"!isLoading; else loading\">\n <c8y-editor\n class=\"flex-grow d-block\"\n style=\"height: 450px\"\n *ngIf=\"!(mode === 'css' && config?.devMode)\"\n [ngModel]=\"value\"\n (ngModelChange)=\"changeCode($event)\"\n [editorOptions]=\"{\n language,\n tabSize: 2,\n insertSpaces: true,\n minimap: { enabled: false }\n }\"\n (editorInit)=\"editorLoaded($event)\"\n ></c8y-editor>\n </ng-container>\n <ng-template #loading>\n <c8y-loading></c8y-loading>\n </ng-template>\n </fieldset>\n</div>\n" }] }], propDecorators: { mode: [{ type: Input }], config: [{ type: Input }] } }); class HtmlWidgetConfigComponent { constructor() { this.options = inject(OptionsService); this.htmlWidgetConfigService = inject(HtmlWidgetConfigService); this.widgetConfigService = inject(WidgetConfigService); } set htmlPreviewTemplate(template) { if (template) { this.widgetConfigService.setPreview(template); } else { this.widgetConfigService.setPreview(null); } } ngOnDestroy() { // sadly the service is component scoped // but still not recycled correctly. That is why we do // it here. this.htmlWidgetConfigService.destroy(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlWidgetConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: HtmlWidgetConfigComponent, isStandalone: true, selector: "c8y-html-widget-config", viewQueries: [{ propertyName: "htmlPreviewTemplate", first: true, predicate: ["htmlPreview"], descendants: true }], ngImport: i0, template: "<c8y-widget-code-editor\n [config]=\"htmlWidgetConfigService.config$ | async\"\n [mode]=\"'code'\"\n></c8y-widget-code-editor>\n\n<ng-template #htmlPreview>\n <c8y-html-frame\n [config]=\"htmlWidgetConfigService.codeEditorChangeConfig$ | async\"\n [device]=\"(widgetConfigService.currentConfig$ | async).device\"\n [useSalt]=\"true\"\n ></c8y-html-frame>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "ngmodule", type: FormsModule }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "component", type: HtmlFrameComponent, selector: "c8y-html-frame", inputs: ["config", "device", "useSalt"] }, { kind: "component", type: WidgetCodeEditorComponent, selector: "c8y-widget-code-editor", inputs: ["mode", "config"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlWidgetConfigComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-html-widget-config', standalone: true, imports: [RouterModule, FormsModule, AsyncPipe, HtmlFrameComponent, WidgetCodeEditorComponent], template: "<c8y-widget-code-editor\n [config]=\"htmlWidgetConfigService.config$ | async\"\n [mode]=\"'code'\"\n></c8y-widget-code-editor>\n\n<ng-template #htmlPreview>\n <c8y-html-frame\n [config]=\"htmlWidgetConfigService.codeEditorChangeConfig$ | async\"\n [device]=\"(widgetConfigService.currentConfig$ | async).device\"\n [useSalt]=\"true\"\n ></c8y-html-frame>\n</ng-template>\n" }] }], propDecorators: { htmlPreviewTemplate: [{ type: ViewChild, args: ['htmlPreview'] }] } }); class HtmlWidgetComponent { ngOnInit() { if (this.config.html && !this.config.config) { this.config.config = this.mapLegacyConfig(this.config); } } mapLegacyConfig(current) { const isAlreadyInAdvancedMode = current?.config?.devMode === true; if (isAlreadyInAdvancedMode) { return current.config; } return { code: current.html, css: '', legacy: true, devMode: false, options: { cssEncapsulation: false, advancedSecurity: current.sanitization === 'strict' } }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: HtmlWidgetComponent, isStandalone: true, selector: "c8y-html-widget", inputs: { config: "config" }, ngImport: i0, template: "<c8y-html-frame\n [config]=\"config.config\"\n [device]=\"config.device\"\n></c8y-html-frame>\n\n", dependencies: [{ kind: "ngmodule", type: RouterModule }, { kind: "component", type: HtmlFrameComponent, selector: "c8y-html-frame", inputs: ["config", "device", "useSalt"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: HtmlWidgetComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-html-widget', standalone: true, imports: [RouterModule, HtmlFrameComponent], template: "<c8y-html-frame\n [config]=\"config.config\"\n [device]=\"config.device\"\n></c8y-html-frame>\n\n" }] }], propDecorators: { config: [{ type: Input }] } }); /** * Generated bundle index. Do not edit. */ export { AdvancedSettingsComponent, HtmlFrameComponent, HtmlWidgetComponent, HtmlWidgetConfigComponent, HtmlWidgetConfigService, INITIAL_CSS_FORMATTED, INITIAL_HTML_FORMATTED, WidgetCodeEditorComponent, defaultWebComponentAttributeNameContext, defaultWebComponentName, legacyTemplate, webComponentTemplate }; //# sourceMappingURL=c8y-ngx-components-widgets-implementations-html-widget.mjs.map