@tinymce/tinymce-angular
Version:
Official TinyMCE Angular Component
675 lines (665 loc) • 26.9 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Directive, Output, InjectionToken, forwardRef, PLATFORM_ID, Component, ChangeDetectionStrategy, Inject, Optional, Input, NgModule } from '@angular/core';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
import { fromEvent, takeUntil, first, map, BehaviorSubject, filter, switchMap, shareReplay, Subject } from 'rxjs';
/**
* Copyright (c) 2017-present, Ephox, Inc.
*
* This source code is licensed under the Apache 2 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const getTinymce = () => {
const w = typeof window !== 'undefined' ? window : undefined;
return w && w.tinymce ? w.tinymce : null;
};
class Events {
onBeforePaste = new EventEmitter();
onBlur = new EventEmitter();
onClick = new EventEmitter();
onCompositionEnd = new EventEmitter();
onCompositionStart = new EventEmitter();
onCompositionUpdate = new EventEmitter();
onContextMenu = new EventEmitter();
onCopy = new EventEmitter();
onCut = new EventEmitter();
onDblclick = new EventEmitter();
onDrag = new EventEmitter();
onDragDrop = new EventEmitter();
onDragEnd = new EventEmitter();
onDragGesture = new EventEmitter();
onDragOver = new EventEmitter();
onDrop = new EventEmitter();
onFocus = new EventEmitter();
onFocusIn = new EventEmitter();
onFocusOut = new EventEmitter();
onKeyDown = new EventEmitter();
onKeyPress = new EventEmitter();
onKeyUp = new EventEmitter();
onMouseDown = new EventEmitter();
onMouseEnter = new EventEmitter();
onMouseLeave = new EventEmitter();
onMouseMove = new EventEmitter();
onMouseOut = new EventEmitter();
onMouseOver = new EventEmitter();
onMouseUp = new EventEmitter();
onPaste = new EventEmitter();
onSelectionChange = new EventEmitter();
onActivate = new EventEmitter();
onAddUndo = new EventEmitter();
onBeforeAddUndo = new EventEmitter();
onBeforeExecCommand = new EventEmitter();
onBeforeGetContent = new EventEmitter();
onBeforeRenderUI = new EventEmitter();
onBeforeSetContent = new EventEmitter();
onChange = new EventEmitter();
onClearUndos = new EventEmitter();
onDeactivate = new EventEmitter();
onDirty = new EventEmitter();
onExecCommand = new EventEmitter();
onGetContent = new EventEmitter();
onHide = new EventEmitter();
onInit = new EventEmitter();
onInput = new EventEmitter();
onInitNgModel = new EventEmitter();
onLoadContent = new EventEmitter();
onNodeChange = new EventEmitter();
onPostProcess = new EventEmitter();
onPostRender = new EventEmitter();
onPreInit = new EventEmitter();
onPreProcess = new EventEmitter();
onProgressState = new EventEmitter();
onRedo = new EventEmitter();
onRemove = new EventEmitter();
onReset = new EventEmitter();
onResizeEditor = new EventEmitter();
onSaveContent = new EventEmitter();
onSetAttrib = new EventEmitter();
onObjectResizeStart = new EventEmitter();
onObjectResized = new EventEmitter();
onObjectSelected = new EventEmitter();
onSetContent = new EventEmitter();
onShow = new EventEmitter();
onSubmit = new EventEmitter();
onUndo = new EventEmitter();
onVisualAid = new EventEmitter();
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: Events, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.1.1", type: Events, outputs: { onBeforePaste: "onBeforePaste", onBlur: "onBlur", onClick: "onClick", onCompositionEnd: "onCompositionEnd", onCompositionStart: "onCompositionStart", onCompositionUpdate: "onCompositionUpdate", onContextMenu: "onContextMenu", onCopy: "onCopy", onCut: "onCut", onDblclick: "onDblclick", onDrag: "onDrag", onDragDrop: "onDragDrop", onDragEnd: "onDragEnd", onDragGesture: "onDragGesture", onDragOver: "onDragOver", onDrop: "onDrop", onFocus: "onFocus", onFocusIn: "onFocusIn", onFocusOut: "onFocusOut", onKeyDown: "onKeyDown", onKeyPress: "onKeyPress", onKeyUp: "onKeyUp", onMouseDown: "onMouseDown", onMouseEnter: "onMouseEnter", onMouseLeave: "onMouseLeave", onMouseMove: "onMouseMove", onMouseOut: "onMouseOut", onMouseOver: "onMouseOver", onMouseUp: "onMouseUp", onPaste: "onPaste", onSelectionChange: "onSelectionChange", onActivate: "onActivate", onAddUndo: "onAddUndo", onBeforeAddUndo: "onBeforeAddUndo", onBeforeExecCommand: "onBeforeExecCommand", onBeforeGetContent: "onBeforeGetContent", onBeforeRenderUI: "onBeforeRenderUI", onBeforeSetContent: "onBeforeSetContent", onChange: "onChange", onClearUndos: "onClearUndos", onDeactivate: "onDeactivate", onDirty: "onDirty", onExecCommand: "onExecCommand", onGetContent: "onGetContent", onHide: "onHide", onInit: "onInit", onInput: "onInput", onInitNgModel: "onInitNgModel", onLoadContent: "onLoadContent", onNodeChange: "onNodeChange", onPostProcess: "onPostProcess", onPostRender: "onPostRender", onPreInit: "onPreInit", onPreProcess: "onPreProcess", onProgressState: "onProgressState", onRedo: "onRedo", onRemove: "onRemove", onReset: "onReset", onResizeEditor: "onResizeEditor", onSaveContent: "onSaveContent", onSetAttrib: "onSetAttrib", onObjectResizeStart: "onObjectResizeStart", onObjectResized: "onObjectResized", onObjectSelected: "onObjectSelected", onSetContent: "onSetContent", onShow: "onShow", onSubmit: "onSubmit", onUndo: "onUndo", onVisualAid: "onVisualAid" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: Events, decorators: [{
type: Directive
}], propDecorators: { onBeforePaste: [{
type: Output
}], onBlur: [{
type: Output
}], onClick: [{
type: Output
}], onCompositionEnd: [{
type: Output
}], onCompositionStart: [{
type: Output
}], onCompositionUpdate: [{
type: Output
}], onContextMenu: [{
type: Output
}], onCopy: [{
type: Output
}], onCut: [{
type: Output
}], onDblclick: [{
type: Output
}], onDrag: [{
type: Output
}], onDragDrop: [{
type: Output
}], onDragEnd: [{
type: Output
}], onDragGesture: [{
type: Output
}], onDragOver: [{
type: Output
}], onDrop: [{
type: Output
}], onFocus: [{
type: Output
}], onFocusIn: [{
type: Output
}], onFocusOut: [{
type: Output
}], onKeyDown: [{
type: Output
}], onKeyPress: [{
type: Output
}], onKeyUp: [{
type: Output
}], onMouseDown: [{
type: Output
}], onMouseEnter: [{
type: Output
}], onMouseLeave: [{
type: Output
}], onMouseMove: [{
type: Output
}], onMouseOut: [{
type: Output
}], onMouseOver: [{
type: Output
}], onMouseUp: [{
type: Output
}], onPaste: [{
type: Output
}], onSelectionChange: [{
type: Output
}], onActivate: [{
type: Output
}], onAddUndo: [{
type: Output
}], onBeforeAddUndo: [{
type: Output
}], onBeforeExecCommand: [{
type: Output
}], onBeforeGetContent: [{
type: Output
}], onBeforeRenderUI: [{
type: Output
}], onBeforeSetContent: [{
type: Output
}], onChange: [{
type: Output
}], onClearUndos: [{
type: Output
}], onDeactivate: [{
type: Output
}], onDirty: [{
type: Output
}], onExecCommand: [{
type: Output
}], onGetContent: [{
type: Output
}], onHide: [{
type: Output
}], onInit: [{
type: Output
}], onInput: [{
type: Output
}], onInitNgModel: [{
type: Output
}], onLoadContent: [{
type: Output
}], onNodeChange: [{
type: Output
}], onPostProcess: [{
type: Output
}], onPostRender: [{
type: Output
}], onPreInit: [{
type: Output
}], onPreProcess: [{
type: Output
}], onProgressState: [{
type: Output
}], onRedo: [{
type: Output
}], onRemove: [{
type: Output
}], onReset: [{
type: Output
}], onResizeEditor: [{
type: Output
}], onSaveContent: [{
type: Output
}], onSetAttrib: [{
type: Output
}], onObjectResizeStart: [{
type: Output
}], onObjectResized: [{
type: Output
}], onObjectSelected: [{
type: Output
}], onSetContent: [{
type: Output
}], onShow: [{
type: Output
}], onSubmit: [{
type: Output
}], onUndo: [{
type: Output
}], onVisualAid: [{
type: Output
}] } });
const validEvents = [
'onActivate',
'onAddUndo',
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBeforePaste',
'onBlur',
'onChange',
'onClearUndos',
'onClick',
'onCompositionEnd',
'onCompositionStart',
'onCompositionUpdate',
'onContextMenu',
'onCopy',
'onCut',
'onDblclick',
'onDeactivate',
'onDirty',
'onDrag',
'onDragDrop',
'onDragEnd',
'onDragGesture',
'onDragOver',
'onDrop',
'onExecCommand',
'onFocus',
'onFocusIn',
'onFocusOut',
'onGetContent',
'onHide',
'onInit',
'onInput',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onLoadContent',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResizeStart',
'onObjectResized',
'onObjectSelected',
'onPaste',
'onPostProcess',
'onPostRender',
'onPreProcess',
'onProgressState',
'onRedo',
'onRemove',
'onReset',
'onResizeEditor',
'onSaveContent',
'onSelectionChange',
'onSetAttrib',
'onSetContent',
'onShow',
'onSubmit',
'onUndo',
'onVisualAid'
];
/**
* Copyright (c) 2017-present, Ephox, Inc.
*
* This source code is licensed under the Apache 2 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// Caretaker note: `fromEvent` supports passing JQuery-style event targets, the editor has `on` and `off` methods which
// will be invoked upon subscription and teardown.
const listenTinyMCEEvent = (editor, eventName, destroy$) => fromEvent(editor, eventName).pipe(takeUntil(destroy$));
const bindHandlers = (ctx, editor, destroy$) => {
const allowedEvents = getValidEvents(ctx);
allowedEvents.forEach((eventName) => {
const eventEmitter = ctx[eventName];
listenTinyMCEEvent(editor, eventName.substring(2), destroy$).subscribe((event) => {
// Caretaker note: `ngZone.run()` runs change detection since it notifies the forked Angular zone that it's
// being re-entered. We don't want to run `ApplicationRef.tick()` if anyone listens to the specific event
// within the template. E.g. if the `onSelectionChange` is not listened within the template like:
// `<editor (onSelectionChange)="..."></editor>`
// then it won't be "observed", and we won't run "dead" change detection.
if (isObserved(eventEmitter)) {
ctx.ngZone.run(() => eventEmitter.emit({ event, editor }));
}
});
});
};
const getValidEvents = (ctx) => {
const ignoredEvents = parseStringProperty(ctx.ignoreEvents, []);
const allowedEvents = parseStringProperty(ctx.allowedEvents, validEvents).filter((event) => validEvents.includes(event) && !ignoredEvents.includes(event));
return allowedEvents;
};
const parseStringProperty = (property, defaultValue) => {
if (typeof property === 'string') {
return property.split(',').map((value) => value.trim());
}
if (Array.isArray(property)) {
return property;
}
return defaultValue;
};
let unique = 0;
const uuid = (prefix) => {
const date = new Date();
const time = date.getTime();
const random = Math.floor(Math.random() * 1000000000);
unique++;
return prefix + '_' + random + unique + String(time);
};
const isTextarea = (element) => typeof element !== 'undefined' && element.tagName.toLowerCase() === 'textarea';
const normalizePluginArray = (plugins) => {
if (typeof plugins === 'undefined' || plugins === '') {
return [];
}
return Array.isArray(plugins) ? plugins : plugins.split(' ');
};
const mergePlugins = (initPlugins, inputPlugins) => normalizePluginArray(initPlugins).concat(normalizePluginArray(inputPlugins));
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => { };
const isNullOrUndefined = (value) => value === null || value === undefined;
const isObserved = (o) =>
// RXJS is making the `observers` property internal in v8. So this is intended as a backwards compatible way of
// checking if a subject has observers.
o.observed || o.observers?.length > 0;
const setMode = (editor, mode) => {
if (typeof editor.mode?.set === 'function') {
editor.mode.set(mode);
}
else if ('setMode' in editor && typeof editor.setMode === 'function') {
editor.setMode(mode);
}
};
const isDisabledOptionSupported = (editor) => editor.options && editor.options.isRegistered('disabled');
/**
* Copyright (c) 2017-present, Ephox, Inc.
*
* This source code is licensed under the Apache 2 license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const firstEmission = () => (source) => source.pipe(first(), map(() => undefined));
const CreateScriptLoader = () => {
const params$ = new BehaviorSubject(null);
const loaded$ = params$.pipe(filter(Boolean), switchMap(([doc, url]) => {
const scriptTag = doc.createElement('script');
scriptTag.referrerPolicy = 'origin';
scriptTag.type = 'application/javascript';
scriptTag.src = url;
doc.head.appendChild(scriptTag);
return fromEvent(scriptTag, 'load').pipe(firstEmission());
}),
// Caretaker note: `loaded$` is a multicast observable since it's piped with `shareReplay`,
// so if there're multiple editor components simultaneously on the page, they'll subscribe to the internal
// `ReplaySubject`. The script will be loaded only once, and `ReplaySubject` will cache the result.
shareReplay({ bufferSize: 1, refCount: true }));
return {
load: (...args) => {
if (!params$.getValue()) {
params$.next(args);
}
return loaded$;
},
reinitialize: () => {
params$.next(null);
},
};
};
const ScriptLoader = CreateScriptLoader();
/* eslint-disable @typescript-eslint/no-parameter-properties */
const TINYMCE_SCRIPT_SRC = new InjectionToken('TINYMCE_SCRIPT_SRC');
const EDITOR_COMPONENT_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EditorComponent),
multi: true
};
/**
* @see {@link https://www.tiny.cloud/docs/tinymce/7/angular-ref/} for the TinyMCE Angular Technical Reference
*/
class EditorComponent extends Events {
cdRef;
platformId;
tinymceScriptSrc;
cloudChannel = '8';
apiKey = 'no-api-key';
licenseKey = 'gpl';
init;
id = '';
initialValue;
outputFormat;
inline;
tagName;
plugins;
toolbar;
modelEvents = 'change input undo redo';
allowedEvents;
ignoreEvents;
set readonly(val) {
this._readonly = val;
if (this._editor) {
setMode(this._editor, val ? 'readonly' : 'design');
}
}
get readonly() {
return this._readonly;
}
set disabled(val) {
this._disabled = val;
if (this._editor) {
if (isDisabledOptionSupported(this._editor)) {
this._editor.options.set('disabled', val ?? false);
}
else {
setMode(this._editor, val ? 'readonly' : 'design');
}
}
}
get disabled() {
return this._disabled;
}
get editor() {
return this._editor;
}
ngZone;
_elementRef;
_element;
_disabled;
_readonly;
_editor;
onTouchedCallback = noop;
onChangeCallback;
destroy$ = new Subject();
constructor(elementRef, ngZone, cdRef, platformId, tinymceScriptSrc) {
super();
this.cdRef = cdRef;
this.platformId = platformId;
this.tinymceScriptSrc = tinymceScriptSrc;
this._elementRef = elementRef;
this.ngZone = ngZone;
}
writeValue(value) {
if (this._editor && this._editor.initialized) {
this._editor.setContent(isNullOrUndefined(value) ? '' : value);
}
else {
this.initialValue = value === null ? undefined : value;
}
}
registerOnChange(fn) {
this.onChangeCallback = fn;
}
registerOnTouched(fn) {
this.onTouchedCallback = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
}
ngAfterViewInit() {
if (isPlatformBrowser(this.platformId)) {
this.id = this.id || uuid('tiny-angular');
this.inline = this.inline !== undefined ? this.inline !== false : !!(this.init?.inline);
this.createElement();
if (getTinymce() !== null) {
this.initialise();
}
else if (this._element && this._element.ownerDocument) {
// Caretaker note: the component might be destroyed before the script is loaded and its code is executed.
// This will lead to runtime exceptions if `initialise` will be called when the component has been destroyed.
ScriptLoader.load(this._element.ownerDocument, this.getScriptSrc())
.pipe(takeUntil(this.destroy$))
.subscribe(this.initialise);
}
}
}
ngOnDestroy() {
this.destroy$.next();
if (getTinymce() !== null) {
getTinymce().remove(this._editor);
}
}
createElement() {
const tagName = typeof this.tagName === 'string' ? this.tagName : 'div';
this._element = document.createElement(this.inline ? tagName : 'textarea');
if (this._element) {
const existingElement = document.getElementById(this.id);
if (existingElement && existingElement !== this._elementRef.nativeElement) {
/* eslint no-console: ["error", { allow: ["warn"] }] */
console.warn(`TinyMCE-Angular: an element with id [${this.id}] already exists. Editors with duplicate Id will not be able to mount`);
}
this._element.id = this.id;
if (isTextarea(this._element)) {
this._element.style.visibility = 'hidden';
}
this._elementRef.nativeElement.appendChild(this._element);
}
}
initialise = () => {
const finalInit = {
...this.init,
selector: undefined,
target: this._element,
inline: this.inline,
disabled: this.disabled,
readonly: this.readonly,
license_key: this.licenseKey,
plugins: mergePlugins((this.init && this.init.plugins), this.plugins),
toolbar: this.toolbar || (this.init && this.init.toolbar),
setup: (editor) => {
this._editor = editor;
listenTinyMCEEvent(editor, 'init', this.destroy$).subscribe(() => {
this.initEditor(editor);
});
bindHandlers(this, editor, this.destroy$);
if (this.init && typeof this.init.setup === 'function') {
this.init.setup(editor);
}
if (this.disabled === true) {
if (isDisabledOptionSupported(editor)) {
this._editor.options.set('disabled', this.disabled);
}
else {
this._editor.mode.set('readonly');
}
}
}
};
if (isTextarea(this._element)) {
this._element.style.visibility = '';
}
this.ngZone.runOutsideAngular(() => {
getTinymce().init(finalInit);
});
};
getScriptSrc() {
return isNullOrUndefined(this.tinymceScriptSrc) ?
`https://cdn.tiny.cloud/1/${this.apiKey}/tinymce/${this.cloudChannel}/tinymce.min.js` :
this.tinymceScriptSrc;
}
initEditor(editor) {
listenTinyMCEEvent(editor, 'blur', this.destroy$).subscribe(() => {
this.cdRef.markForCheck();
this.ngZone.run(() => this.onTouchedCallback());
});
listenTinyMCEEvent(editor, this.modelEvents, this.destroy$).subscribe(() => {
this.cdRef.markForCheck();
this.ngZone.run(() => this.emitOnChange(editor));
});
if (typeof this.initialValue === 'string') {
this.ngZone.run(() => {
editor.setContent(this.initialValue);
if (editor.getContent() !== this.initialValue) {
this.emitOnChange(editor);
}
if (this.onInitNgModel !== undefined) {
this.onInitNgModel.emit(editor);
}
});
}
}
emitOnChange(editor) {
if (this.onChangeCallback) {
this.onChangeCallback(editor.getContent({ format: this.outputFormat }));
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: EditorComponent, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: PLATFORM_ID }, { token: TINYMCE_SCRIPT_SRC, optional: true }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.1.1", type: EditorComponent, isStandalone: true, selector: "editor", inputs: { cloudChannel: "cloudChannel", apiKey: "apiKey", licenseKey: "licenseKey", init: "init", id: "id", initialValue: "initialValue", outputFormat: "outputFormat", inline: "inline", tagName: "tagName", plugins: "plugins", toolbar: "toolbar", modelEvents: "modelEvents", allowedEvents: "allowedEvents", ignoreEvents: "ignoreEvents", readonly: "readonly", disabled: "disabled" }, providers: [EDITOR_COMPONENT_VALUE_ACCESSOR], usesInheritance: true, ngImport: i0, template: '', isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: EditorComponent, decorators: [{
type: Component,
args: [{ selector: 'editor', template: '', providers: [EDITOR_COMPONENT_VALUE_ACCESSOR], standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}\n"] }]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [TINYMCE_SCRIPT_SRC]
}] }], propDecorators: { cloudChannel: [{
type: Input
}], apiKey: [{
type: Input
}], licenseKey: [{
type: Input
}], init: [{
type: Input
}], id: [{
type: Input
}], initialValue: [{
type: Input
}], outputFormat: [{
type: Input
}], inline: [{
type: Input
}], tagName: [{
type: Input
}], plugins: [{
type: Input
}], toolbar: [{
type: Input
}], modelEvents: [{
type: Input
}], allowedEvents: [{
type: Input
}], ignoreEvents: [{
type: Input
}], readonly: [{
type: Input
}], disabled: [{
type: Input
}] } });
class EditorModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: EditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.1.1", ngImport: i0, type: EditorModule, imports: [EditorComponent], exports: [EditorComponent] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: EditorModule, imports: [EditorComponent] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.1.1", ngImport: i0, type: EditorModule, decorators: [{
type: NgModule,
args: [{
imports: [EditorComponent],
exports: [EditorComponent]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { EditorComponent, EditorModule, TINYMCE_SCRIPT_SRC };
//# sourceMappingURL=tinymce-tinymce-angular.mjs.map