@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
623 lines (594 loc) • 46.9 kB
JavaScript
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