UNPKG

@progress/kendo-angular-editor

Version:
1,033 lines (1,029 loc) 66.3 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, HostBinding, ViewChild, ContentChild, ViewContainerRef, Output, ElementRef, EventEmitter, forwardRef, Input, ChangeDetectorRef, NgZone, isDevMode, Renderer2 } from '@angular/core'; import { NgIf } from '@angular/common'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { BehaviorSubject, fromEvent, Subject, zip } from 'rxjs'; import { map, filter, take } from 'rxjs/operators'; import { ToolBarButtonComponent, ToolBarButtonGroupComponent, ToolBarComponent } from '@progress/kendo-angular-toolbar'; import { DialogService } from '@progress/kendo-angular-dialog'; import { guid, hasObservers, isDocumentAvailable, KendoInput, shouldShowValidationUI, getLicenseMessage, WatermarkOverlayComponent, Keys } from '@progress/kendo-angular-common'; import { buildKeymap, buildListKeymap, getHtml, pasteCleanup, sanitize, removeComments, sanitizeClassAttr, sanitizeStyleAttr, removeAttribute, placeholder, EditorView, EditorState, baseKeymap, keymap, history, parseContent, Plugin, PluginKey, TextSelection, Schema, AllSelection, gapCursor, getSelectionText, imageResizing, tableEditing, caretColor, tableResizing, cspFix } from '@progress/kendo-editor-common'; import { validatePackage } from '@progress/kendo-licensing'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { packageMetadata } from './package-metadata'; import { schema } from './config/schema'; import { editorCommands } from './config/commands'; import { getToolbarState, initialToolBarState, disabledToolBarState } from './editor-toolbar-state'; import { removeEmptyEntries, conditionallyExecute, isPresent } from './util'; import { SourceDialogComponent } from './dialogs/source-dialog.component'; import { ImageDialogComponent } from './dialogs/image-dialog.component'; import { FileLinkDialogComponent } from './dialogs/file-link-dialog.component'; import { EditorLocalizationService } from './localization/editor-localization.service'; import { defaultStyle, tablesStyles, rtlStyles } from './common/styles'; import { EditorErrorMessages } from './common/error-messages'; import { ProviderService } from './common/provider.service'; import { EditorToolsService } from './tools/tools.service'; import { EditorPasteEvent } from './preventable-events/paste-event'; import { EditorInsertImageButtonDirective } from './tools/image/editor-insert-image-button.directive'; import { EditorUnlinkButtonDirective } from './tools/link/editor-unlink-button.directive'; import { EditorCreateLinkButtonDirective } from './tools/link/editor-create-link-button.directive'; import { EditorOutdentButtonDirective } from './tools/indentation/editor-outdent-button.directive'; import { EditorIndentButtonDirective } from './tools/indentation/editor-indent-button.directive'; import { EditorInsertOrderedListButtonDirective } from './tools/list/editor-insert-ordered-list-button.directive'; import { EditorInsertUnorderedListButtonDirective } from './tools/list/editor-insert-unordered-list-button.directive'; import { EditorAlignJustifyButtonDirective } from './tools/alignment/editor-align-justify-button.directive'; import { EditorAlignRightButtonDirective } from './tools/alignment/editor-align-right-button.directive'; import { EditorAlignCenterButtonDirective } from './tools/alignment/editor-align-center-button.directive'; import { EditorAlignLeftButtonDirective } from './tools/alignment/editor-align-left-button.directive'; import { EditorFormatComponent } from './tools/format/editor-format.component'; import { EditorUnderlineButtonDirective } from './tools/typographical-emphasis/editor-underline-button.directive'; import { EditorItalicButtonDirective } from './tools/typographical-emphasis/editor-italic-button.directive'; import { EditorBoldButtonDirective } from './tools/typographical-emphasis/editor-bold-button.directive'; import { LocalizedMessagesDirective } from './localization/localized-messages.directive'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-dialog"; import * as i2 from "@progress/kendo-angular-l10n"; import * as i3 from "./common/provider.service"; import * as i4 from "./tools/tools.service"; const EMPTY_PARAGRAPH = '<p></p>'; const defaultPasteCleanupSettings = { convertMsLists: false, removeAttributes: [], removeHtmlComments: false, removeInvalidHTML: true, removeMsClasses: false, removeMsStyles: false, stripTags: [] }; const removeCommentsIf = conditionallyExecute(removeComments); const removeInvalidHTMLIf = conditionallyExecute(sanitize); const getPasteCleanupAttributes = (config) => { if (config.removeAttributes === 'all') { return { '*': removeAttribute }; } const initial = removeEmptyEntries({ style: config.removeMsStyles ? sanitizeStyleAttr : undefined, class: config.removeMsClasses ? sanitizeClassAttr : undefined }); return config.removeAttributes.reduce((acc, curr) => ({ ...acc, [curr]: removeAttribute }), initial); }; /** * Represents the [Kendo UI Editor component for Angular]({% slug overview_editor %}). * * Use the Editor to create and edit rich text content in your Angular applications. * * @example * ```html * <kendo-editor [(value)]="editorValue"></kendo-editor> * ``` * @remarks * Supported children components are: {@link CustomMessagesComponent}, {@link ToolBarComponent}. */ export class EditorComponent { dialogService; localization; cdr; ngZone; element; providerService; toolsService; renderer; /** * Sets the value of the Editor ([see example](slug:overview_editor)). * Use this property to update the Editor content programmatically. */ set value(value) { this.changeValue(value); } get value() { if (this.trOnChange) { return this.htmlOnChange; } const value = this._view ? this.getSource() : this._value; if (value === EMPTY_PARAGRAPH) { return this._value ? '' : this._value; } else { return value; } } /** * Sets the disabled state of the component. To disable the Editor in reactive forms, see [Forms Support](slug:formssupport_editor#toc-managing-the-editor-disabled-state-in-reactive-forms). */ set disabled(value) { this._disabled = value || false; if (this._view) { this._view.updateState(this._view.state); } if (this._disabled) { this.readonly = false; } if (this._disabled || this._readonly) { this.stateChange.next(disabledToolBarState); } else { this.stateChange.next(initialToolBarState); } } get disabled() { return this._disabled; } /** * Sets the read-only state of the component. * * When `true`, users cannot edit the content. * @default false */ set readonly(value) { this._readonly = value || false; if (this._view) { // remove DOM selection let win; if (this.iframe) { win = this.container.element.nativeElement.contentWindow; } else { win = window; } const focusedNode = win.getSelection().focusNode; if (this._view.dom.contains(focusedNode)) { win.getSelection().removeAllRanges(); } // remove ProseMirror selection const doc = this._view.state.doc; const tr = this._view.state.tr.setSelection(TextSelection.create(doc, 1)); this._view.dispatch(tr); } if (this._readonly) { if (this.toolbar) { this.toolbar.tabindex = -1; } this.stateChange.next(disabledToolBarState); } else { if (this.toolbar) { this.toolbar.tabindex = 0; } this.stateChange.next(initialToolBarState); } } get readonly() { return this._readonly; } /** * If set to `false`, the Editor runs in non-encapsulated style mode. * The Editor content inherits styles from the page. * @default true */ iframe = true; /** * Applies custom CSS styles to the Editor in iframe mode. * Use this property to provide additional CSS for the Editor content. */ set iframeCss(settings) { this._iframeCss = Object.assign(this._iframeCss, settings); } get iframeCss() { return this._iframeCss; } /** * When set to `true` or an `ApplyToWordOptions` object, emphasis or inline style commands apply to the whole word at the cursor. * @default false */ applyToWord = false; /** * Provides a custom schema for the Editor ([see example]({% slug schema_editor %})). */ set schema(value) { if (isDevMode) { if (!(value instanceof Schema)) { throw new Error(EditorErrorMessages.schemaType); } if (this._view) { throw new Error(EditorErrorMessages.setSchemaOnce); } } this._schema = value; } get schema() { return this._schema; } /** * Defines a function to customize the plugins used by the Editor. * The function receives the default plugins and returns the plugins to use ([see example]({% slug plugins_editor %})). */ set plugins(fn) { if (isDevMode) { if (typeof fn !== 'function') { throw new Error(EditorErrorMessages.pluginsCallbackType(fn)); } if (this._view) { throw new Error(EditorErrorMessages.setPluginsOnce); } } this._plugins = fn; } get plugins() { return this._plugins; } /** * Sets the hint text displayed when the Editor is empty. * Use this property to provide guidance to users. */ set placeholder(value) { if (isDevMode && this._view) { throw new Error(EditorErrorMessages.setPlaceHolderOnce); } this._placeholder = value; } get placeholder() { return this._placeholder; } /** * Controls whitespace handling in the Editor content. * Set to `true` to preserve whitespace and normalize newlines to spaces. * Set to `'full'` to preserve all whitespace. * @default false */ preserveWhitespace = false; /** * Configures how pasted content is cleaned before it is added to the Editor ([see example]({% slug paste_cleanup %})). * Use this property to remove unwanted HTML, styles, or attributes from pasted content. */ pasteCleanupSettings; /** * Determines whether the Editor can be resized ([see example](slug:resizing_editor#toc-resizing-the-editor)). * Set to `true` or provide an object with resizing options. * @default false */ resizable = false; /** * Fires when the Editor value changes due to user interaction. * This event does not fire when the value changes programmatically through `ngModel` or form bindings * ([see example](slug:events_editor)). */ valueChange = new EventEmitter(); /** * Fires when the Editor content area receives focus ([see example](slug:events_editor)). */ onFocus = new EventEmitter(); /** * Fires when the user paste content into the Editor ([see example](slug:events_editor)). * This event is preventable. If you cancel it, the Editor content does not change. */ paste = new EventEmitter(); /** * Fires when the Editor content area is blurred ([see example](slug:events_editor)). */ onBlur = new EventEmitter(); hostClass = true; get resizableClass() { return !!this.resizable; } get isDisabled() { return this.disabled; } get isReadonly() { return this.readonly; } get dir() { return this.direction; } get ariaDisabled() { return this.disabled; } get minWidth() { const resizableOptions = this.resizable; return resizableOptions.minWidth ? resizableOptions.minWidth + 'px' : undefined; } get maxWidth() { const resizableOptions = this.resizable; return resizableOptions.maxWidth ? resizableOptions.maxWidth + 'px' : undefined; } get minHeight() { const resizableOptions = this.resizable; return resizableOptions.minHeight ? resizableOptions.minHeight + 'px' : undefined; } get maxHeight() { const resizableOptions = this.resizable; return resizableOptions.maxHeight ? resizableOptions.maxHeight + 'px' : undefined; } /** * @hidden */ stateChange = new BehaviorSubject(initialToolBarState); /** * @hidden */ showLicenseWatermark = false; /** * @hidden */ licenseMessage; get toolbar() { return this.defaultToolbarComponent || this.userToolBarComponent; } get toolbarElement() { return this.defaultToolbar || this.userToolBarElement; } /** * Returns the ProseMirror [EditorView](https://prosemirror.net/docs/ref/#view.EditorView) object. * Use this property to access the underlying `EditorView` instance. */ get view() { return this._view; } /** * Returns the text currently selected in the Editor ([see example]({% slug selection_editor %}#toc-retrieve-the-selected-text)). */ get selectionText() { return this._view && this._view.state ? getSelectionText(this._view.state) : ''; } /** * @hidden */ valueModified = new Subject(); userToolBarComponent; userToolBarElement; dialogContainer; // Use `ViewContainerRef` instead of `ElementRef` because the dialog expects `ViewContainerRef`. container; direction; viewMountElement; /** * @hidden */ focusChangedProgrammatically; /** * @hidden */ shouldEmitFocus; /** * @hidden */ focusableId = `k-editor-${guid()}`; defaultToolbar; defaultToolbarComponent; subs; _view; _value; _disabled; _readonly = false; _schema; _plugins; _placeholder = ''; _styleObserver; trOnChange; htmlOnChange; inForm = false; _pasteEvent; _iframeCss = { keepBuiltInCss: true }; afterViewInit = new Subject(); contentAreaLoaded = new Subject(); constructor(dialogService, localization, cdr, ngZone, element, providerService, toolsService, renderer) { this.dialogService = dialogService; this.localization = localization; this.cdr = cdr; this.ngZone = ngZone; this.element = element; this.providerService = providerService; this.toolsService = toolsService; this.renderer = renderer; const isValid = validatePackage(packageMetadata); this.licenseMessage = getLicenseMessage(packageMetadata); this.showLicenseWatermark = shouldShowValidationUI(isValid); this.providerService.editor = this; this.direction = localization.rtl ? 'rtl' : 'ltr'; // https://stackoverflow.com/questions/56572483/chrome-is-synchronously-handling-iframe-loading-whereas-firefox-handles-it-asyn this.subs = zip(this.afterViewInit.asObservable(), this.contentAreaLoaded.asObservable()).subscribe(() => this.initialize()); } ngOnInit() { this.subs.add(this.localization.changes.subscribe(({ rtl }) => { this.direction = rtl ? 'rtl' : 'ltr'; })); this.subs.add(this.toolsService.needsCheck.subscribe(() => this.cdr.markForCheck())); } ngAfterViewInit() { if (!isDocumentAvailable()) { return; } this.afterViewInit.next(); if (!this.iframe) { this.contentAreaLoaded.next(); } if (this.resizable) { this.normalizeSize(); } if (this.userToolBarComponent) { this.renderer.addClass(this.userToolBarComponent.element.nativeElement, 'k-editor-toolbar'); } if (this.toolbar.overflow) { this.toolbar.onResize(); } } ngOnChanges(changes) { if (changes['value'] && this.view) { this.changeValue(changes['value'].currentValue); } if (changes['iframe'] && !changes['iframe'].isFirstChange()) { this.ngZone.onStable.pipe(take(1)).subscribe(() => this.initialize()); } if (changes['resizable'] && !changes['resizable'].isFirstChange()) { this.normalizeSize(); } } /** * @hidden */ setDisabledState(isDisabled) { this.disabled = isDisabled; } /** * @hidden */ iframeOnLoad() { this.contentAreaLoaded.next(); } /** * Executes a command on the currently selected text * ([more information and example]({% slug toolbartools_editor %}#toc-associating-toolbar-tools-with-editor-commands)). * * @param {EditorCommand} commandName - The command that will be executed. * @param {any} attr - Optional parameters for the command. */ exec(commandName, attr) { // normalizes setHTML attributes if (commandName === 'setHTML' && typeof attr === 'string') { attr = { content: attr, parseOptions: { preserveWhitespace: this.preserveWhitespace } }; } else if (['fontFamily', 'fontSize', 'foreColor', 'backColor', 'createLink'].some(name => name === commandName)) { attr = { value: attr, applyToWord: this.applyToWord }; } // Finds a command and applies the attributes. const command = editorCommands[commandName](attr); // Executes a ProseMirror command. command(this._view.state, this._view.dispatch, this._view); } /** * Opens a dialog. * @param {DialogCommand} dialogName - The name of the dialog that will open. */ openDialog(dialogName) { const editorDialogs = { createLink: { content: FileLinkDialogComponent, width: 400 }, insertFile: { content: FileLinkDialogComponent, width: 400 }, insertImage: { content: ImageDialogComponent, width: 400 }, viewSource: { content: SourceDialogComponent, height: 400, width: 500 } // tableWizard: { // content: TableWizardDialogComponent // } }; const dialog = Object.assign({ appendTo: this.dialogContainer, autoFocusedElement: '.k-input-inner' }, editorDialogs[dialogName]); this.toolbar.toggle(false); const dialogContent = this.dialogService.open(dialog).content.instance; this.renderer.addClass(dialogContent.dialog.dialog.instance.wrapper.nativeElement.querySelector('.k-window'), 'k-editor-window'); if (dialogName === 'createLink' || dialogName === 'insertFile') { dialogContent.command = dialogName; } dialogContent.editor = this; dialogContent.setData(this._view.state, { applyToWord: this.applyToWord }); } /** * Manually focus the Editor. */ focus() { this.focusChangedProgrammatically = true; this._view.dom.focus(); this.focusChangedProgrammatically = false; } /** * Manually blur the Editor. */ blur() { this.focusChangedProgrammatically = true; this._view.dom.blur(); this.focusChangedProgrammatically = false; } /** * @hidden */ getSource() { return getHtml(this._view.state); } ngOnDestroy() { if (this.subs) { this.subs.unsubscribe(); } if (this._styleObserver) { this._styleObserver.disconnect(); } } /** * @hidden */ writeValue(value) { this.inForm = true; // To avoid confusion, non-existent values are always undefined. this.value = value === null ? undefined : value; } /** * @hidden */ registerOnChange(fn) { this.onChangeCallback = fn; } /** * @hidden */ registerOnTouched(fn) { this.onTouchedCallback = fn; } /** * @hidden * Used by the TextBoxContainer to determine if the component is empty. */ isEmpty() { return false; } initialize() { if (!isDocumentAvailable()) { return; } const currentSchema = this.schema || schema; const containerNativeElement = this.container.element.nativeElement; const contentNode = parseContent((this.value || '').trim(), currentSchema, { preserveWhitespace: this.preserveWhitespace }); if (this.iframe) { const iframeDoc = containerNativeElement.contentDocument; const meta = iframeDoc.createElement('meta'); meta.setAttribute('charset', 'utf-8'); iframeDoc.head.appendChild(meta); const isCssPathSet = Boolean(this.iframeCss.path); const isCssContentSet = Boolean(this.iframeCss.content); const allStyles = ` ${tablesStyles} ${this.iframeCss.keepBuiltInCss ? defaultStyle : ''} ${this.dir === 'rtl' ? rtlStyles : ''} ${isCssContentSet ? this.iframeCss.content : ''} `; const styleEl = iframeDoc.createElement('style'); styleEl.appendChild(iframeDoc.createTextNode(allStyles)); iframeDoc.head.appendChild(styleEl); if (isCssPathSet) { const linkEl = iframeDoc.createElement('link'); linkEl.rel = 'stylesheet'; linkEl.href = this.iframeCss.path; iframeDoc.head.appendChild(linkEl); } const element = iframeDoc.createElement('div'); this.renderer.addClass(element, 'k-content'); this.renderer.setAttribute(element, 'id', this.focusableId); this.renderer.setAttribute(element, 'role', 'textbox'); iframeDoc.body.appendChild(element); } else { const element = document.createElement('div'); this.renderer.setAttribute(element, 'id', this.focusableId); this.renderer.setAttribute(element, 'role', 'textbox'); containerNativeElement.appendChild(element); } const defaultPlugins = [ new Plugin({ key: new PluginKey('editor-tabindex'), props: { attributes: () => ({ // set tabindex when contenteditable is false, so that the content area can be selected tabIndex: this.readonly ? '0' : '' }) } }), new Plugin({ key: new PluginKey('toolbar-tools-update'), view: () => ({ update: editorView => { if (!this.disabled) { this.ngZone.run(() => { this.stateChange.next(this.readonly ? disabledToolBarState : getToolbarState(editorView.state, { applyToWord: this.applyToWord })); }); } } }) }), history(), keymap(buildListKeymap(currentSchema)), keymap(buildKeymap(currentSchema, { applyToWord: this.applyToWord })), keymap(baseKeymap), gapCursor(), imageResizing(), ...tableResizing(), tableEditing(), caretColor(), cspFix() ]; if (this.placeholder) { defaultPlugins.push(placeholder(this.placeholder)); } const state = EditorState.create({ schema: currentSchema, doc: contentNode, plugins: isPresent(this.plugins) ? this.plugins(defaultPlugins) : defaultPlugins }); if (this.iframe) { this.viewMountElement = containerNativeElement.contentDocument.querySelector('div'); } else { this.viewMountElement = containerNativeElement.querySelector('div'); } this.ngZone.runOutsideAngular(() => { this._view = new EditorView({ mount: this.viewMountElement }, { state, editable: () => !this.readonly, dispatchTransaction: this.dispatchTransaction, transformPastedHTML: this.transformPastedHTML, transformPastedText: this.transformPastedText, handleDOMEvents: { paste: this.onPaste } }); }); if (this._view) { let containerElement; const contentAreaClasslist = this.element.nativeElement.querySelector('.k-editor-content').classList; if (this.iframe) { containerElement = this.container.element.nativeElement.contentDocument; } else { containerElement = this.container.element.nativeElement; } const proseMirror = containerElement.querySelector('.ProseMirror'); proseMirror.style = "'height: 100%; width: 100%; box-sizing: border-box; outline: none; overflow: auto;'"; this.subs.add(fromEvent(containerElement, 'focusin') .subscribe((e) => { if (this.readonly) { contentAreaClasslist.add('k-focus'); } if (!this.focusChangedProgrammatically || this.shouldEmitFocus) { const relatedTarget = e.relatedTarget; const isActiveColorButton = relatedTarget && relatedTarget.classList.contains('k-colorpicker'); if (!isActiveColorButton || this.shouldEmitFocus) { this.ngZone.run(() => this.onFocus.emit()); } this.shouldEmitFocus = false; } })); this.subs.add(fromEvent(containerElement, 'focusout') .subscribe((e) => { if (this.readonly) { contentAreaClasslist.remove('k-focus'); } if (!this.focusChangedProgrammatically) { const relatedTarget = e.relatedTarget; const isActiveColorButton = relatedTarget && relatedTarget.classList.contains('k-colorpicker'); if (!isActiveColorButton) { this.ngZone.run(() => this.onBlur.emit()); } } })); } this.subs.add(this.stateChange.subscribe(() => { this.ngZone.onStable.pipe(take(1)).subscribe(() => { if (this.userToolBarComponent) { this.userToolBarComponent.cdr.detectChanges(); } else { this.cdr.detectChanges(); } }); })); this.subs.add(this.valueModified.subscribe((value) => { this.onChangeCallback(value); this.valueChange.emit(value); this.cdr.markForCheck(); })); this.subs.add(fromEvent(this.viewMountElement, 'keyup') .pipe(map((e) => e.code), filter((code) => code === Keys.F10), map(() => this.toolbarElement)) .subscribe((toolbar) => toolbar.nativeElement.focus())); this.subs.add(fromEvent(this.viewMountElement, 'blur') .pipe(filter((event) => !this.viewMountElement.contains(event.relatedTarget))) .subscribe(() => this.onTouchedCallback())); } dispatchTransaction = (tr) => { const docChanged = tr.docChanged; if (this.disabled || (this.readonly && docChanged)) { return; } if (docChanged) { const doc = tr.doc; const html = getHtml({ doc }); this.trOnChange = tr; this.htmlOnChange = html; this.ngZone.run(() => { this.valueModified.next(html === EMPTY_PARAGRAPH ? '' : html); }); } if (!docChanged || this.inForm) { this.view.updateState(this.view.state.apply(tr)); this.trOnChange = null; } }; transformPastedContent = (dirtyHtml, plainText) => { if (plainText) { return this.dispatchPasteEvent(dirtyHtml, dirtyHtml); } const pasteCleanupSettings = { ...defaultPasteCleanupSettings, ...this.pasteCleanupSettings }; const clean = pasteCleanup(removeInvalidHTMLIf(pasteCleanupSettings.removeInvalidHTML)(dirtyHtml), { convertMsLists: pasteCleanupSettings.convertMsLists, stripTags: pasteCleanupSettings.stripTags.join('|'), attributes: getPasteCleanupAttributes(pasteCleanupSettings), }); return this.dispatchPasteEvent(dirtyHtml, removeCommentsIf(pasteCleanupSettings.removeHtmlComments)(clean)); }; transformPastedHTML = (html) => this.transformPastedContent(html); transformPastedText = (html) => this.transformPastedContent(html, true); changeValue = (value) => { const prev = this._value; this._value = value; if (!this._view) { return; } if (this.htmlOnChange === value && this.trOnChange) { this.view.updateState(this.view.state.apply(this.trOnChange)); } else { const newValue = (prev || '') !== (value || ''); const formReset = (this.inForm && !value); if (newValue || formReset) { const iframeContentWindowNotPresent = this.iframe && !this.container.element.nativeElement.contentWindow; if (iframeContentWindowNotPresent) { return; } const state = this.view.state; const nextDoc = parseContent(value || '', state.schema, { preserveWhitespace: this.preserveWhitespace }); const tr = state.tr .setSelection(new AllSelection(state.doc)) .replaceSelectionWith(nextDoc); this.view.updateState(state.apply(tr)); } } this.trOnChange = null; this.htmlOnChange = null; }; onChangeCallback = (value) => { this.changeValue(value); }; normalizeSize() { if (typeof this.resizable === 'object' && !this._styleObserver) { const element = this.element.nativeElement; this._styleObserver = new MutationObserver(() => { this.ngZone.runOutsideAngular(() => this.normalizeProperties(element)); }); this._styleObserver.observe(element, { attributeFilter: ['style'] }); } } normalizeProperties(element) { const props = Object.keys(this.resizable); const pixelWidth = parseInt(element.style.width, 10); const pixelHeight = parseInt(element.style.height, 10); const resizable = this.resizable; props.forEach(prop => { const isMinProp = prop.startsWith('min'); const isMaxProp = !isMinProp; const isWidthProp = prop.endsWith('Width'); const isHeightProp = !isWidthProp; if (isMinProp && isWidthProp) { if (pixelWidth < resizable.minWidth) { element.style.width = resizable.minWidth + 'px'; } } else if (isMinProp && isHeightProp) { if (pixelHeight < resizable.minHeight) { element.style.height = resizable.minHeight + 'px'; } } else if (isMaxProp && isWidthProp) { if (pixelWidth > resizable.maxWidth) { element.style.width = resizable.maxWidth + 'px'; } } else if (pixelHeight > resizable.maxHeight) { element.style.height = resizable.maxHeight + 'px'; } }); } onTouchedCallback = (_) => { }; onPaste = (_view, nativeEvent) => { this._pasteEvent = nativeEvent; return false; }; dispatchPasteEvent(originalContent, cleanContent) { if (hasObservers(this.paste)) { if (!this.iframe) { this._pasteEvent.stopImmediatePropagation(); } const event = new EditorPasteEvent(cleanContent, originalContent, this._pasteEvent); this.ngZone.run(() => this.paste.emit(event)); return event.isDefaultPrevented() ? '' : event.cleanedHtml; } return cleanContent; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: EditorComponent, deps: [{ token: i1.DialogService }, { token: i2.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i0.ElementRef }, { token: i3.ProviderService }, { token: i4.EditorToolsService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: EditorComponent, isStandalone: true, selector: "kendo-editor", inputs: { value: "value", disabled: "disabled", readonly: "readonly", iframe: "iframe", iframeCss: "iframeCss", applyToWord: "applyToWord", schema: "schema", plugins: "plugins", placeholder: "placeholder", preserveWhitespace: "preserveWhitespace", pasteCleanupSettings: "pasteCleanupSettings", resizable: "resizable" }, outputs: { valueChange: "valueChange", onFocus: "focus", paste: "paste", onBlur: "blur" }, host: { properties: { "class.k-editor": "this.hostClass", "class.k-editor-resizable": "this.resizableClass", "class.k-disabled": "this.isDisabled", "class.k-readonly": "this.isReadonly", "attr.dir": "this.dir", "attr.ariaDisabled": "this.ariaDisabled", "style.minWidth": "this.minWidth", "style.maxWidth": "this.maxWidth", "style.minHeight": "this.minHeight", "style.maxHeight": "this.maxHeight" } }, providers: [ EditorLocalizationService, ProviderService, EditorToolsService, { provide: LocalizationService, useExisting: EditorLocalizationService }, { provide: L10N_PREFIX, useValue: 'kendo.editor' }, { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EditorComponent), multi: true }, { provide: KendoInput, useExisting: forwardRef(() => EditorComponent) } ], queries: [{ propertyName: "userToolBarComponent", first: true, predicate: ToolBarComponent, descendants: true }, { propertyName: "userToolBarElement", first: true, predicate: ToolBarComponent, descendants: true, read: ElementRef }], viewQueries: [{ propertyName: "dialogContainer", first: true, predicate: ["dialogsContainer"], descendants: true, read: ViewContainerRef }, { propertyName: "container", first: true, predicate: ["content"], descendants: true, read: ViewContainerRef }, { propertyName: "defaultToolbar", first: true, predicate: ["defaultToolbar"], descendants: true, read: ElementRef }, { propertyName: "defaultToolbarComponent", first: true, predicate: ["defaultToolbar"], descendants: true, read: ToolBarComponent }], usesOnChanges: true, ngImport: i0, template: ` <ng-container kendoEditorLocalizedMessages i18n-alignCenter="kendo.editor.alignCenter|The title of the tool that aligns text in the center." alignCenter="Center text" i18n-alignJustify="kendo.editor.alignJustify|The title of the tool that justifies text both left and right." alignJustify="Justify" i18n-alignLeft="kendo.editor.alignLeft|The title of the tool that aligns text on the left." alignLeft="Align text left" i18n-alignRight="kendo.editor.alignRight|The title of the tool that aligns text on the right." alignRight="Align text right" i18n-backColor="kendo.editor.backColor|The title of the tool that changes the text background color." backColor="Background color" i18n-blockquote="kendo.editor.blockquote|The title of the tool that wraps an element in a blockquote" blockquote="Quotation" i18n-bold="kendo.editor.bold|The title of the tool that makes text bold." bold="Bold" i18n-cleanFormatting="kendo.editor.cleanFormatting|The title of the Clean Formatting tool." cleanFormatting="Clean formatting" i18n-createLink="kendo.editor.createLink|The title of the tool that creates hyperlinks." createLink="Insert link" i18n-dialogApply="kendo.editor.dialogApply|The label of the **Apply** button in all editor dialogs." dialogApply="Apply" i18n-dialogCancel="kendo.editor.dialogCancel|The label of the **Cancel** button in all editor dialogs." dialogCancel="Cancel" i18n-dialogInsert="kendo.editor.dialogInsert|The label of the **Insert** button in all editor dialogs." dialogInsert="Insert" i18n-dialogUpdate="kendo.editor.dialogUpdate|The label of the **Update** button in all editor dialogs." dialogUpdate="Update" i18n-fileText="kendo.editor.fileText|The caption for the file text in the insertFile dialog." fileText="Text" i18n-fileTitle="kendo.editor.fileTitle|The caption for the file Title in the insertFile dialog." fileTitle="Title" i18n-fileWebAddress="kendo.editor.fileWebAddress|The caption for the file URL in the insertFile dialog." fileWebAddress="Web address" i18n-fontFamily="kendo.editor.fontFamily|The title of the tool that changes the text font." fontFamily="Select font family" i18n-fontSize="kendo.editor.fontSize|The title of the tool that changes the text size." fontSize="Select font size" i18n-foreColor="kendo.editor.foreColor|The title of the tool that changes the text color." foreColor="Color" i18n-format="kendo.editor.format|The title of the tool that lets users choose block formats." format="Format" i18n-imageAltText="kendo.editor.imageAltText|The caption for the image alternate text in the insertImage dialog." imageAltText="Alternate text" i18n-imageHeight="kendo.editor.imageHeight|The caption for the image height in the insertImage dialog." imageHeight="Height (px)" i18n-imageWebAddress="kendo.editor.imageWebAddress|The caption for the image URL in the insertImage dialog." imageWebAddress="Web address" i18n-imageWidth="kendo.editor.imageWidth|The caption for the image width in the insertImage dialog." imageWidth="Width (px)" i18n-indent="kendo.editor.indent|The title of the tool that indents the content." indent="Indent" i18n-insertFile="kendo.editor.insertFile|The title of the tool that inserts links to files." insertFile="Insert file" i18n-insertImage="kendo.editor.insertImage|The title of the tool that inserts images." insertImage="Insert image" i18n-insertOrderedList="kendo.editor.insertOrderedList|The title of the tool that inserts an ordered list." insertOrderedList="Insert ordered list" i18n-insertUnorderedList="kendo.editor.insertUnorderedList|The title of the tool that inserts an unordered list." insertUnorderedList="Insert unordered list" i18n-italic="kendo.editor.italic|The title of the tool that makes text italicized." italic="Italic" i18n-linkOpenInNewWindow="kendo.editor.linkOpenInNewWindow|The caption for the checkbox for opening the link in a new window in the createLink dialog." linkOpenInNewWindow="Open link in new window" i18n-linkText="kendo.editor.linkText|The caption for the link text in the createLink dialog." linkText="Text" i18n-linkTitle="kendo.editor.linkTitle|The caption for the link title in the createLink dialog." linkTitle="Title" i18n-linkWebAddress="kendo.editor.linkWebAddress|The caption for the URL in the createLink dialog." linkWebAddress="Web address" i18n-outdent="kendo.editor.outdent|The title of the tool that outdents the content." outdent="Outdent" i18n-print="kendo.editor.print|The title of the print tool." print="Print" i18n-redo="kendo.editor.redo|The title of the tool that undos the last action." redo="Redo" i18n-selectAll="kendo.editor.selectAll|The title of the tool that selects all content." selectAll="Select All" i18n-strikethrough="kendo.editor.strikethrough|The title of the tool that strikes through text." strikethrough="Strikethrough" i18n-subscript="kendo.editor.subscript|The title of the tool that makes text subscript." subscript="Subscript" i18n-superscript="kendo.editor.superscript|The title of the tool that makes text superscript." superscript="Superscript" i18n-underline="kendo.editor.underline|The title of the tool that underlines text." underline="Underline" i18n-unlink="kendo.editor.unlink|The title of the tool that removes hyperlinks." unlink="Remove Link" i18n-undo="kendo.editor.undo|The title of the tool that undos the last action." undo="Undo" i18n-viewSource="kendo.editor.viewSource|The title of the tool that shows the editor value as HTML." viewSource="View source" i18n-insertTable="kendo.editor.insertTable|The title of the tool that inserts table." insertTable="Insert Table" i18n-insertTableHint="kendo.editor.insertTableHint|The caption for the hint in the insert table tool." insertTableHint="Create a {rows} {x} {columns} table" i18n-addColumnBefore="kendo.editor.addColumnBefore|The title of the tool that adds new column before currently selected column." addColumnBefore="Add column before" i18n-addColumnAfter="kendo.editor.addColumnAfter|The title of the tool that adds new column after currently selected column." addColumnAfter="Add column after" i18n-addRowBefore="kendo.editor.addRowBefore|The title of the tool that adds new row before currently selected row." addRowBefore="Add row before" i18n-addRowAfter="kendo.editor.addRowAfter|The title of the tool that adds new row after currently selected row." addRowAfter="Add row after" i18n-mergeCells="kendo.editor.mergeCells|The title of the tool that merges the currently selected cells." mergeCells="Merge cells" i18n-splitCell="kendo.editor.splitCell|The title of the tool that splits the currently selected cell." splitCell="Split cell" i18n-deleteColumn="kendo.editor.deleteColumn|The title of the tool that deletes a table column." deleteColumn="Delete column" i18n-deleteRow="kendo.editor.deleteRow|The title of the tool that deletes a table row." deleteRow="Delete row" i18n-deleteTable="kendo.editor.deleteTable|The title of the tool that deletes a table." deleteTable="Delete table" > </ng-container> <ng-content select="kendo-toolbar"></ng-content> <kendo-toolbar #defaultToolbar *ngIf="!userToolBarElement" class="k-editor-toolbar" [overflow]="true" [tabindex]="readonly ? -1 : 0" > <kendo-toolbar-buttongroup> <kendo-toolbar-button kendoEditorBoldButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorItalicButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorUnderlineButton></kendo-toolbar-button> </kendo-toolbar-buttongroup> <kendo-toolbar-dropdownlist kendoEditorFormat></kendo-toolbar-dropdownlist> <kendo-toolbar-buttongroup> <kendo-toolbar-button kendoEditorAlignLeftButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorAlignCenterButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorAlignRightButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorAlignJustifyButton></kendo-toolbar-button> </kendo-toolbar-buttongroup> <kendo-toolbar-buttongroup> <kendo-toolbar-button kendoEditorInsertUnorderedListButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorInsertOrderedListButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorIndentButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorOutdentButton></kendo-toolbar-button> </kendo-toolbar-buttongroup> <kendo-toolbar-buttongroup> <kendo-toolbar-button kendoEditorCreateLinkButton></kendo-toolbar-button> <kendo-toolbar-button kendoEditorUnlinkButton></kendo-toolbar-button> </kendo-toolbar-buttongroup> <kendo-toolbar-button kendoEditorInsertImageButton></kendo-toolbar-button> </kendo-toolbar> <div *ngIf="!iframe" #content [attr.dir]="direction" class="k-editor-content"></div> <div class="k-editor-content" *ngIf="iframe"> <iframe #content srcdoc="<!DOCTYPE html>" role="none" frameborder="0" class="k-iframe" [style.width.%]="100" [style.height.%]="100" [style.display]="'block'" (load)="iframeOnLoad()"></iframe> </div> <ng-container #dialogsContainer></ng-container> <div kendoWatermarkOverlay *ngIf="showLicenseWatermark" [licenseMessage]="licenseMessage"></div> `, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "[kendoEditorLocalizedMessages]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ToolBarComponent, selector: "kendo-toolbar", inputs: ["overflow", "resizable", "popupSettings", "fillMode", "tabindex", "size", "tabIndex", "showIcon", "showText"], outputs: ["open", "close"], exportAs: ["kendoToolBar"] }, { kind: "component", type: ToolBarButtonGroupComponent, selector: "kendo-toolbar-buttongroup", inputs: ["disabled", "fillMode", "selection", "width", "look"], exportAs: ["kendoToolBarButtonGroup"] }, { kind: "component", type: ToolBarButtonComponent, selector: "kendo-toolbar-button", inputs: ["showText", "showIcon", "text", "style", "className", "title", "disabled", "toggleable", "look", "togglable", "selected", "fillMode", "rounded", "themeColor", "icon", "iconClass", "svgIcon", "imageUrl"], outputs: ["click", "pointerdown", "selectedChange"], exportAs: ["kendoToolBarButton"] }, { kind: "directive", type: EditorBoldButtonDirective, selector: "kendo-toolbar-button[kendoEditorBoldButton]" }, { kind: "directive", type: EditorItalicButtonDirective, selector: "kendo-toolbar-button[kendoEditorItalicButton]" }, { kind: "directive", type: EditorUnderlineButtonDirective, selector: "kendo-toolbar-button[kendoEditorUnderlineButton]" }, { kind: "component", type: EditorFormatComponent, selector: "kendo-toolbar-dropdownlist[kendoEditorFormat]", inputs: ["data"], outputs: ["valueChange"] }, { kind: "directive", type: EditorAlignLeftButtonDirective, selector: "kendo-toolbar-button[kendoEditorAlignLeftButton]" }, { kind: "directive", type: EditorAlignCenterButtonDirective, selector: "kendo-toolbar-button[kendoEditorAlignCenterButton]" }, { kind: "directive", type: EditorAlignRightButtonDirective, selector: "kendo-toolbar-button[kendoEditorAlignRightButton]" }, { kind: "directive", type: EditorAlignJustifyButtonDirective, selector: "kendo-toolbar-button[kendoEditorAlignJustifyButton]" }, { kind: "directive", type: EditorInsertUnorderedListButtonDirective, selector: "kendo-toolbar-button[kendoEditorInsertUnorderedListButton]" }, { kind: "directive", type: EditorInsertOrderedListButtonDirective, selector: "kendo-toolbar-button[kendoEditorInsertOrderedListButton]" }, { kind: "directive", type: EditorIndentButtonDirective, selector: "kendo-toolbar-button[kendoEditorIndentButton]" }, { kind: "directive", type: EditorOutdentButtonDirective, selector: "kendo-toolbar-button[kendoEditorOutdentButton]" }, { kind: "directive", type: EditorCreateLinkButtonDirective, selector: "kendo-toolbar-button[kendoEditorCreateLinkButton]" }, { kind: "directive", typ