@progress/kendo-angular-editor
Version:
Kendo UI Editor for Angular
1,033 lines (1,029 loc) • 66.3 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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