@ckeditor/ckeditor5-core
Version:
The core architecture of CKEditor 5 – the best browser-based rich text editor.
939 lines (938 loc) • 42.4 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module core/editor/editor
*/
import { set, get } from 'es-toolkit/compat';
import { Config, CKEditorError, ObservableMixin, logError, parseBase64EncodedObject, releaseDate, toArray, uid, crc32 } from '@ckeditor/ckeditor5-utils';
import { Conversion, DataController, EditingController, Model, StylesProcessor } from '@ckeditor/ckeditor5-engine';
import { ContextWatchdog, EditorWatchdog } from '@ckeditor/ckeditor5-watchdog';
import Context from '../context.js';
import PluginCollection from '../plugincollection.js';
import CommandCollection from '../commandcollection.js';
import EditingKeystrokeHandler from '../editingkeystrokehandler.js';
import Accessibility from '../accessibility.js';
import { getEditorUsageData } from './utils/editorusagedata.js';
/**
* The class representing a basic, generic editor.
*
* Check out the list of its subclasses to learn about specific editor implementations.
*
* All editor implementations (like {@link module:editor-classic/classiceditor~ClassicEditor} or
* {@link module:editor-inline/inlineeditor~InlineEditor}) should extend this class. They can add their
* own methods and properties.
*
* When you are implementing a plugin, this editor represents the API
* which your plugin can expect to get when using its {@link module:core/plugin~Plugin#editor} property.
*
* This API should be sufficient in order to implement the "editing" part of your feature
* (schema definition, conversion, commands, keystrokes, etc.).
* It does not define the editor UI, which is available only if
* the specific editor implements also the {@link ~Editor#ui} property
* (as most editor implementations do).
*/
class Editor extends /* #__PURE__ */ ObservableMixin() {
/**
* A required name of the editor class. The name should reflect the constructor name.
*/
static get editorName() {
return 'Editor';
}
/**
* A namespace for the accessibility features of the editor.
*/
accessibility;
/**
* Commands registered to the editor.
*
* Use the shorthand {@link #execute `editor.execute()`} method to execute commands:
*
* ```ts
* // Execute the bold command:
* editor.execute( 'bold' );
*
* // Check the state of the bold command:
* editor.commands.get( 'bold' ).value;
* ```
*/
commands;
/**
* Stores all configurations specific to this editor instance.
*
* ```ts
* editor.config.get( 'image.toolbar' );
* // -> [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ]
* ```
*/
config;
/**
* Conversion manager through which you can register model-to-view and view-to-model converters.
*
* See the {@link module:engine/conversion/conversion~Conversion} documentation to learn how to add converters.
*/
conversion;
/**
* The {@link module:engine/controller/datacontroller~DataController data controller}.
* Used e.g. for setting and retrieving the editor data.
*/
data;
/**
* The {@link module:engine/controller/editingcontroller~EditingController editing controller}.
* Controls user input and rendering the content for editing.
*/
editing;
/**
* The locale instance.
*/
locale;
/**
* The editor's model.
*
* The central point of the editor's abstract data model.
*/
model;
/**
* The plugins loaded and in use by this editor instance.
*
* ```ts
* editor.plugins.get( 'ClipboardPipeline' ); // -> An instance of the clipboard pipeline plugin.
* ```
*/
plugins;
/**
* An instance of the {@link module:core/editingkeystrokehandler~EditingKeystrokeHandler}.
*
* It allows setting simple keystrokes:
*
* ```ts
* // Execute the bold command on Ctrl+E:
* editor.keystrokes.set( 'Ctrl+E', 'bold' );
*
* // Execute your own callback:
* editor.keystrokes.set( 'Ctrl+E', ( data, cancel ) => {
* console.log( data.keyCode );
*
* // Prevent the default (native) action and stop the underlying keydown event
* // so no other editor feature will interfere.
* cancel();
* } );
* ```
*
* Note: Certain typing-oriented keystrokes (like <kbd>Backspace</kbd> or <kbd>Enter</kbd>) are handled
* by a low-level mechanism and trying to listen to them via the keystroke handler will not work reliably.
* To handle these specific keystrokes, see the events fired by the
* {@link module:engine/view/document~Document editing view document} (`editor.editing.view.document`).
*/
keystrokes;
/**
* Shorthand for {@link module:utils/locale~Locale#t}.
*
* @see module:utils/locale~Locale#t
*/
t;
/**
* The default configuration which is built into the editor class.
*
* It was used in the now deprecated CKEditor 5 builds to provide the default configuration options
* which are later used during the editor initialization.
*
* ```ts
* ClassicEditor.defaultConfig = {
* foo: 1,
* bar: 2
* };
*
* ClassicEditor
* .create( sourceElement )
* .then( editor => {
* editor.config.get( 'foo' ); // -> 1
* editor.config.get( 'bar' ); // -> 2
* } );
*
* // The default options can be overridden by the configuration passed to create().
* ClassicEditor
* .create( sourceElement, { bar: 3 } )
* .then( editor => {
* editor.config.get( 'foo' ); // -> 1
* editor.config.get( 'bar' ); // -> 3
* } );
* ```
*
* See also {@link module:core/editor/editor~Editor.builtinPlugins}.
*/
static defaultConfig;
/**
* An array of plugins built into this editor class.
*
* It is used in the now deprecated CKEditor 5 builds to provide a list of plugins which are later automatically initialized
* during the editor initialization.
*
* They will be automatically initialized by the editor, unless listed in `config.removePlugins` and
* unless `config.plugins` is passed.
*
* ```ts
* // Build some plugins into the editor class first.
* ClassicEditor.builtinPlugins = [ FooPlugin, BarPlugin ];
*
* // Normally, you need to define config.plugins, but since ClassicEditor.builtinPlugins was
* // defined, now you can call create() without any configuration.
* ClassicEditor
* .create( sourceElement )
* .then( editor => {
* editor.plugins.get( FooPlugin ); // -> An instance of the Foo plugin.
* editor.plugins.get( BarPlugin ); // -> An instance of the Bar plugin.
* } );
*
* ClassicEditor
* .create( sourceElement, {
* // Do not initialize these plugins (note: it is defined by a string):
* removePlugins: [ 'Foo' ]
* } )
* .then( editor => {
* editor.plugins.get( FooPlugin ); // -> Undefined.
* editor.config.get( BarPlugin ); // -> An instance of the Bar plugin.
* } );
*
* ClassicEditor
* .create( sourceElement, {
* // Load only this plugin. It can also be defined by a string if
* // this plugin was built into the editor class.
* plugins: [ FooPlugin ]
* } )
* .then( editor => {
* editor.plugins.get( FooPlugin ); // -> An instance of the Foo plugin.
* editor.config.get( BarPlugin ); // -> Undefined.
* } );
* ```
*
* See also {@link module:core/editor/editor~Editor.defaultConfig}.
*/
static builtinPlugins;
/**
* The editor context.
* When it is not provided through the configuration, the editor creates it.
*/
_context;
/**
* A set of lock IDs for the {@link #isReadOnly} getter.
*/
_readOnlyLocks;
/**
* Creates a new instance of the editor class.
*
* Usually, not to be used directly. See the static {@link module:core/editor/editor~Editor.create `create()`} method.
*
* @param config The editor configuration.
*/
constructor(config = {}) {
super();
if ('sanitizeHtml' in config) {
/**
* Configuration property `config.sanitizeHtml` was removed in CKEditor version 43.1.0 and is no longer supported.
*
* Please use `config.htmlEmbed.sanitizeHtml` and/or `config.mergeFields.sanitizeHtml` instead.
*
* @error editor-config-sanitizehtml-not-supported
*/
throw new CKEditorError('editor-config-sanitizehtml-not-supported');
}
const constructor = this.constructor;
// We don't pass translations to the config, because its behavior of splitting keys
// with dots (e.g. `resize.width` => `resize: { width }`) breaks the translations.
const { translations: defaultTranslations, ...defaultConfig } = constructor.defaultConfig || {};
const { translations = defaultTranslations, ...rest } = config;
// Prefer the language passed as the argument to the constructor instead of the constructor's `defaultConfig`, if both are set.
const language = config.language || defaultConfig.language;
this._context = config.context || new Context({ language, translations });
this._context._addEditor(this, !config.context);
// Clone the plugins to make sure that the plugin array will not be shared
// between editors and make the watchdog feature work correctly.
const availablePlugins = Array.from(constructor.builtinPlugins || []);
this.config = new Config(rest, defaultConfig);
this.config.define('plugins', availablePlugins);
this.config.define(this._context._getEditorConfig());
checkLicenseKeyIsDefined(this.config);
this.plugins = new PluginCollection(this, availablePlugins, this._context.plugins);
this.locale = this._context.locale;
this.t = this.locale.t;
this._readOnlyLocks = new Set();
this.commands = new CommandCollection();
this.set('state', 'initializing');
this.once('ready', () => (this.state = 'ready'), { priority: 'high' });
this.once('destroy', () => (this.state = 'destroyed'), { priority: 'high' });
this.model = new Model();
this.on('change:isReadOnly', () => {
this.model.document.isReadOnly = this.isReadOnly;
});
const stylesProcessor = new StylesProcessor();
this.data = new DataController(this.model, stylesProcessor);
this.editing = new EditingController(this.model, stylesProcessor);
this.editing.view.document.bind('isReadOnly').to(this);
this.conversion = new Conversion([this.editing.downcastDispatcher, this.data.downcastDispatcher], this.data.upcastDispatcher);
this.conversion.addAlias('dataDowncast', this.data.downcastDispatcher);
this.conversion.addAlias('editingDowncast', this.editing.downcastDispatcher);
this.keystrokes = new EditingKeystrokeHandler(this);
this.keystrokes.listenTo(this.editing.view.document);
this.accessibility = new Accessibility(this);
verifyLicenseKey(this);
// Checks if the license key is defined and throws an error if it is not.
function checkLicenseKeyIsDefined(config) {
let licenseKey = config.get('licenseKey');
if (!licenseKey && window.CKEDITOR_GLOBAL_LICENSE_KEY) {
licenseKey = window.CKEDITOR_GLOBAL_LICENSE_KEY;
config.set('licenseKey', licenseKey);
}
if (!licenseKey) {
/**
* The `licenseKey` property is missing in the editor configuration.
*
* * If you are using the editor in a commercial setup, please provide your license key.
* * If you still need to acquire a key, please [contact us](https://ckeditor.com/contact/) or
* [create a free account with a 14 day premium features trial](https://portal.ckeditor.com/checkout?plan=free).
* * If you are using the editor under a GPL license or another license from our Open Source Initiative,
* use the 'GPL' license key instead.
*
* ```js
* ClassicEditor.create( document.querySelector( '#editor' ), {
* licenseKey: '<YOUR_LICENSE_KEY>', // Or 'GPL'.
* // ... Other configuration options ...
* } ) ;
*
* @error license-key-missing
*/
throw new CKEditorError('license-key-missing');
}
}
function verifyLicenseKey(editor) {
const licenseKey = editor.config.get('licenseKey');
const distributionChannel = window[Symbol.for('cke distribution')] || 'sh';
function blockEditor(reason) {
editor.enableReadOnlyMode(Symbol('invalidLicense'));
editor._showLicenseError(reason);
}
function getPayload(licenseKey) {
const parts = licenseKey.split('.');
if (parts.length != 3) {
return null;
}
return parts[1];
}
function hasAllRequiredFields(licensePayload) {
const requiredFields = ['exp', 'jti', 'vc'];
return requiredFields.every(field => field in licensePayload);
}
function getCrcInputData(licensePayload) {
const keysToCheck = Object.getOwnPropertyNames(licensePayload).sort();
const filteredValues = keysToCheck
.filter(key => key != 'vc' && licensePayload[key] != null)
.map(key => licensePayload[key]);
return filteredValues;
}
function checkLicensedHosts(licensedHosts) {
const { hostname } = new URL(window.location.href);
if (licensedHosts.includes(hostname)) {
return true;
}
const segments = hostname.split('.');
return licensedHosts
// Filter out hosts without wildcards.
.filter(host => host.includes('*'))
// Split the hosts into segments.
.map(host => host.split('.'))
// Filter out hosts that have more segments than the current hostname.
.filter(host => host.length <= segments.length)
// Pad the beginning of the licensed host if it's shorter than the current hostname.
.map(host => Array(segments.length - host.length).fill(host[0] === '*' ? '*' : '').concat(host))
// Check if some license host matches the hostname.
.some(octets => segments.every((segment, index) => octets[index] === segment || octets[index] === '*'));
}
function warnAboutNonProductionLicenseKey(licenseType) {
const capitalizedLicenseType = licenseType[0].toUpperCase() + licenseType.slice(1);
const article = licenseType === 'evaluation' ? 'an' : 'a';
console.info(`%cCKEditor 5 ${capitalizedLicenseType} License`, 'color: #ffffff; background: #743CCD; font-size: 14px; padding: 4px 8px; border-radius: 4px;');
console.warn(`⚠️ You are using ${article} ${licenseType} license of CKEditor 5` +
`${licenseType === 'trial' ? ' which is for evaluation purposes only' : ''}. ` +
'For production usage, please obtain a production license at https://portal.ckeditor.com/');
}
if (licenseKey == 'GPL') {
if (distributionChannel == 'cloud') {
blockEditor('distributionChannel');
}
return;
}
const encodedPayload = getPayload(licenseKey);
if (!encodedPayload) {
blockEditor('invalid');
return;
}
const licensePayload = parseBase64EncodedObject(encodedPayload);
if (!licensePayload) {
blockEditor('invalid');
return;
}
if (!hasAllRequiredFields(licensePayload)) {
blockEditor('invalid');
return;
}
if (licensePayload.distributionChannel && !toArray(licensePayload.distributionChannel).includes(distributionChannel)) {
blockEditor('distributionChannel');
return;
}
if (crc32(getCrcInputData(licensePayload)) != licensePayload.vc.toLowerCase()) {
blockEditor('invalid');
return;
}
const expirationDate = new Date(licensePayload.exp * 1000);
if (expirationDate < releaseDate) {
blockEditor('expired');
return;
}
const licensedHosts = licensePayload.licensedHosts;
if (licensedHosts && licensedHosts.length > 0 && !checkLicensedHosts(licensedHosts)) {
blockEditor('domainLimit');
return;
}
if (['evaluation', 'trial'].includes(licensePayload.licenseType) && licensePayload.exp * 1000 < Date.now()) {
blockEditor('expired');
return;
}
if (['development', 'evaluation', 'trial'].includes(licensePayload.licenseType)) {
const { licenseType } = licensePayload;
window.CKEDITOR_WARNING_SUPPRESSIONS = window.CKEDITOR_WARNING_SUPPRESSIONS || {};
if (!window.CKEDITOR_WARNING_SUPPRESSIONS[licenseType]) {
warnAboutNonProductionLicenseKey(licenseType);
window.CKEDITOR_WARNING_SUPPRESSIONS[licenseType] = true;
}
}
if (['evaluation', 'trial'].includes(licensePayload.licenseType)) {
const licenseType = licensePayload.licenseType;
const timerId = setTimeout(() => {
blockEditor(`${licenseType}Limit`);
}, 600000);
editor.on('destroy', () => {
clearTimeout(timerId);
});
}
if (licensePayload.usageEndpoint) {
editor.once('ready', () => {
const request = {
requestId: uid(),
requestTime: Math.round(Date.now() / 1000),
license: licenseKey,
editor: collectUsageData(editor)
};
/**
* This part of the code is not executed in open-source implementations using a GPL key.
* It only runs when a specific license key is provided. If you are uncertain whether
* this applies to your installation, please contact our support team.
*/
editor._sendUsageRequest(licensePayload.usageEndpoint, request).then(response => {
const { status, message } = response;
if (message) {
console.warn(message);
}
if (status != 'ok') {
blockEditor('usageLimit');
}
}, () => {
/**
* Your license key cannot be validated due to a network issue.
* Please ensure that your setup does not block requests to the validation endpoint.
*
* @error license-key-validation-endpoint-not-reachable
* @param {string} url The URL that was attempted to be reached for validation.
*/
logError('license-key-validation-endpoint-not-reachable', { url: licensePayload.usageEndpoint });
});
}, { priority: 'high' });
}
}
}
/**
* Defines whether the editor is in the read-only mode.
*
* In read-only mode the editor {@link #commands commands} are disabled so it is not possible
* to modify the document by using them. Also, the editable element(s) become non-editable.
*
* In order to make the editor read-only, you need to call the {@link #enableReadOnlyMode} method:
*
* ```ts
* editor.enableReadOnlyMode( 'feature-id' );
* ```
*
* Later, to turn off the read-only mode, call {@link #disableReadOnlyMode}:
*
* ```ts
* editor.disableReadOnlyMode( 'feature-id' );
* ```
*
* @readonly
* @observable
*/
get isReadOnly() {
return this._readOnlyLocks.size > 0;
}
set isReadOnly(value) {
/**
* The {@link module:core/editor/editor~Editor#isReadOnly Editor#isReadOnly} property is read-only since version `34.0.0`
* and can be set only using {@link module:core/editor/editor~Editor#enableReadOnlyMode `Editor#enableReadOnlyMode( lockId )`} and
* {@link module:core/editor/editor~Editor#disableReadOnlyMode `Editor#disableReadOnlyMode( lockId )`}.
*
* Usage before version `34.0.0`:
*
* ```ts
* editor.isReadOnly = true;
* editor.isReadOnly = false;
* ```
*
* Usage since version `34.0.0`:
*
* ```ts
* editor.enableReadOnlyMode( 'my-feature-id' );
* editor.disableReadOnlyMode( 'my-feature-id' );
* ```
*
* @error editor-isreadonly-has-no-setter
*/
throw new CKEditorError('editor-isreadonly-has-no-setter');
}
/**
* Turns on the read-only mode in the editor.
*
* Editor can be switched to or out of the read-only mode by many features, under various circumstances. The editor supports locking
* mechanism for the read-only mode. It enables easy control over the read-only mode when many features wants to turn it on or off at
* the same time, without conflicting with each other. It guarantees that you will not make the editor editable accidentally (which
* could lead to errors).
*
* Each read-only mode request is identified by a unique id (also called "lock"). If multiple plugins requested to turn on the
* read-only mode, then, the editor will become editable only after all these plugins turn the read-only mode off (using the same ids).
*
* Note, that you cannot force the editor to disable the read-only mode if other plugins set it.
*
* After the first `enableReadOnlyMode()` call, the {@link #isReadOnly `isReadOnly` property} will be set to `true`:
*
* ```ts
* editor.isReadOnly; // `false`.
* editor.enableReadOnlyMode( 'my-feature-id' );
* editor.isReadOnly; // `true`.
* ```
*
* You can turn off the read-only mode ("clear the lock") using the {@link #disableReadOnlyMode `disableReadOnlyMode()`} method:
*
* ```ts
* editor.enableReadOnlyMode( 'my-feature-id' );
* // ...
* editor.disableReadOnlyMode( 'my-feature-id' );
* editor.isReadOnly; // `false`.
* ```
*
* All "locks" need to be removed to enable editing:
*
* ```ts
* editor.enableReadOnlyMode( 'my-feature-id' );
* editor.enableReadOnlyMode( 'my-other-feature-id' );
* // ...
* editor.disableReadOnlyMode( 'my-feature-id' );
* editor.isReadOnly; // `true`.
* editor.disableReadOnlyMode( 'my-other-feature-id' );
* editor.isReadOnly; // `false`.
* ```
*
* @param lockId A unique ID for setting the editor to the read-only state.
*/
enableReadOnlyMode(lockId) {
if (typeof lockId !== 'string' && typeof lockId !== 'symbol') {
/**
* The lock ID is missing or it is not a string or symbol.
*
* @error editor-read-only-lock-id-invalid
* @param {never} lockId Lock ID.
*/
throw new CKEditorError('editor-read-only-lock-id-invalid', null, { lockId });
}
if (this._readOnlyLocks.has(lockId)) {
return;
}
this._readOnlyLocks.add(lockId);
if (this._readOnlyLocks.size === 1) {
// Manually fire the `change:isReadOnly` event as only getter is provided.
this.fire('change:isReadOnly', 'isReadOnly', true, false);
}
}
/**
* Removes the read-only lock from the editor with given lock ID.
*
* When no lock is present on the editor anymore, then the {@link #isReadOnly `isReadOnly` property} will be set to `false`.
*
* @param lockId The lock ID for setting the editor to the read-only state.
*/
disableReadOnlyMode(lockId) {
if (typeof lockId !== 'string' && typeof lockId !== 'symbol') {
throw new CKEditorError('editor-read-only-lock-id-invalid', null, { lockId });
}
if (!this._readOnlyLocks.has(lockId)) {
return;
}
this._readOnlyLocks.delete(lockId);
if (this._readOnlyLocks.size === 0) {
// Manually fire the `change:isReadOnly` event as only getter is provided.
this.fire('change:isReadOnly', 'isReadOnly', false, true);
}
}
/**
* Sets the data in the editor.
*
* ```ts
* editor.setData( '<p>This is editor!</p>' );
* ```
*
* If your editor implementation uses multiple roots, you should pass an object with keys corresponding
* to the editor root names and values equal to the data that should be set in each root:
*
* ```ts
* editor.setData( {
* header: '<p>Content for header part.</p>',
* content: '<p>Content for main part.</p>',
* footer: '<p>Content for footer part.</p>'
* } );
* ```
*
* By default the editor accepts HTML. This can be controlled by injecting a different data processor.
* See the {@glink features/markdown Markdown output} guide for more details.
*
* @param data Input data.
*/
setData(data) {
this.data.set(data);
}
/**
* Gets the data from the editor.
*
* ```ts
* editor.getData(); // -> '<p>This is editor!</p>'
* ```
*
* If your editor implementation uses multiple roots, you should pass root name as one of the options:
*
* ```ts
* editor.getData( { rootName: 'header' } ); // -> '<p>Content for header part.</p>'
* ```
*
* By default, the editor outputs HTML. This can be controlled by injecting a different data processor.
* See the {@glink features/markdown Markdown output} guide for more details.
*
* A warning is logged when you try to retrieve data for a detached root, as most probably this is a mistake. A detached root should
* be treated like it is removed, and you should not save its data. Note, that the detached root data is always an empty string.
*
* @param options Additional configuration for the retrieved data.
* Editor features may introduce more configuration options that can be set through this parameter.
* @param options.rootName Root name. Defaults to `'main'`.
* @param options.trim Whether returned data should be trimmed. This option is set to `'empty'` by default,
* which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
* use `'none'`. In such cases exact content will be returned (for example `'<p> </p>'` for an empty editor).
* @returns Output data.
*/
getData(options) {
return this.data.get(options);
}
/**
* Loads and initializes plugins specified in the configuration.
*
* @returns A promise which resolves once the initialization is completed, providing an array of loaded plugins.
*/
initPlugins() {
const config = this.config;
const plugins = config.get('plugins');
const removePlugins = config.get('removePlugins') || [];
const extraPlugins = config.get('extraPlugins') || [];
const substitutePlugins = config.get('substitutePlugins') || [];
return this.plugins.init(plugins.concat(extraPlugins), removePlugins, substitutePlugins);
}
/**
* Destroys the editor instance, releasing all resources used by it.
*
* **Note** The editor cannot be destroyed during the initialization phase so if it is called
* while the editor {@link #state is being initialized}, it will wait for the editor initialization before destroying it.
*
* @fires destroy
* @returns A promise that resolves once the editor instance is fully destroyed.
*/
destroy() {
let readyPromise = Promise.resolve();
if (this.state == 'initializing') {
readyPromise = new Promise(resolve => this.once('ready', resolve));
}
return readyPromise
.then(() => {
this.fire('destroy');
this.stopListening();
this.commands.destroy();
})
.then(() => this.plugins.destroy())
.then(() => {
this.model.destroy();
this.data.destroy();
this.editing.destroy();
this.keystrokes.destroy();
})
// Remove the editor from the context.
// When the context was created by this editor, the context will be destroyed.
.then(() => this._context._removeEditor(this));
}
/**
* Executes the specified command with given parameters.
*
* Shorthand for:
*
* ```ts
* editor.commands.get( commandName ).execute( ... );
* ```
*
* @param commandName The name of the command to execute.
* @param commandParams Command parameters.
* @returns The value returned by the {@link module:core/commandcollection~CommandCollection#execute `commands.execute()`}.
*/
execute(commandName, ...commandParams) {
try {
return this.commands.execute(commandName, ...commandParams);
}
catch (err) {
// @if CK_DEBUG // throw err;
/* istanbul ignore next -- @preserve */
CKEditorError.rethrowUnexpectedError(err, this);
}
}
/**
* Focuses the editor.
*
* **Note** To explicitly focus the editing area of the editor, use the
* {@link module:engine/view/view~View#focus `editor.editing.view.focus()`} method of the editing view.
*
* Check out the {@glink framework/deep-dive/ui/focus-tracking#focus-in-the-editor-ui Focus in the editor UI} section
* of the {@glink framework/deep-dive/ui/focus-tracking Deep dive into focus tracking} guide to learn more.
*/
focus() {
this.editing.view.focus();
}
/* istanbul ignore next -- @preserve */
/**
* Creates and initializes a new editor instance.
*
* This is an abstract method. Every editor type needs to implement its own initialization logic.
*
* See the `create()` methods of the existing editor types to learn how to use them:
*
* * {@link module:editor-classic/classiceditor~ClassicEditor.create `ClassicEditor.create()`}
* * {@link module:editor-balloon/ballooneditor~BalloonEditor.create `BalloonEditor.create()`}
* * {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create `DecoupledEditor.create()`}
* * {@link module:editor-inline/inlineeditor~InlineEditor.create `InlineEditor.create()`}
*/
static create(...args) {
throw new Error('This is an abstract method.');
}
/**
* The {@link module:core/context~Context} class.
*
* Exposed as static editor field for easier access in editor builds.
*/
static Context = Context;
/**
* The {@link module:watchdog/editorwatchdog~EditorWatchdog} class.
*
* Exposed as static editor field for easier access in editor builds.
*/
static EditorWatchdog = EditorWatchdog;
/**
* The {@link module:watchdog/contextwatchdog~ContextWatchdog} class.
*
* Exposed as static editor field for easier access in editor builds.
*/
static ContextWatchdog = ContextWatchdog;
_showLicenseError(reason, pluginName) {
setTimeout(() => {
if (reason == 'invalid') {
/**
* The license key provided is invalid. Please ensure that it is copied correctly
* from the [Customer Portal](http://portal.ckeditor.com). If the issue persists,
* please [contact our customer support](https://ckeditor.com/contact/).
*
* @error invalid-license-key
*/
throw new CKEditorError('invalid-license-key');
}
if (reason == 'expired') {
/**
* Your license key has expired.
*
* If you used our free trial, you either need to switch to
* [open-source license](https://ckeditor.com/docs/ckeditor5/latest/getting-started/licensing/license-and-legal.html), or
* in case of a commercial plan, change the trial key to production key or development key.
* Switching from trial, you also need to align the editor configuration to the features available in your plan.
*
* If you already had one of our Cloud or Custom plans, please renew your license in the
* [Customer Portal](https://portal.ckeditor.com).
*
* @error license-key-expired
*/
throw new CKEditorError('license-key-expired');
}
if (reason == 'domainLimit') {
/**
* The provided license does not allow the editor to run on this domain.
* Some license keys are restricted to local test environments only.
* For more details, please refer to the
* {@glink getting-started/licensing/license-key-and-activation#license-key-types license key type documentation}.
*
* @error license-key-domain-limit
*/
throw new CKEditorError('license-key-domain-limit');
}
if (reason == 'featureNotAllowed') {
/**
* The plugin you are trying to use is not permitted under your current license.
* Please check the available features on the
* [Customer Portal](https://portal.ckeditor.com) or
* [contact support](https://ckeditor.com/contact/) for more information.
*
* @error license-key-plugin-not-allowed
* @param {String} pluginName The plugin you tried to load.
*/
throw new CKEditorError('license-key-plugin-not-allowed', null, { pluginName });
}
if (reason == 'evaluationLimit') {
/**
* You have exceeded the editor operation limit available for your evaluation license key.
* Please restart the editor to continue using it.
* {@glink getting-started/licensing/license-key-and-activation#license-key-types Read more about license key types}.
*
* @error license-key-evaluation-limit
*/
throw new CKEditorError('license-key-evaluation-limit');
}
if (reason == 'trialLimit') {
/**
* You have exceeded the editor operation limit for your trial license key.
* Please restart the editor to continue using it.
* {@glink getting-started/licensing/license-key-and-activation#license-key-types Read more about license key types}.
*
* @error license-key-trial-limit
*/
throw new CKEditorError('license-key-trial-limit');
}
if (reason == 'developmentLimit') {
/**
* You have exceeded the operation limit for your development license key within the editor.
* Please restart the editor to continue using it.
* {@glink getting-started/licensing/license-key-and-activation#license-key-types Read more about license key types}.
*
* @error license-key-development-limit
*/
throw new CKEditorError('license-key-development-limit');
}
if (reason == 'usageLimit') {
/**
* You have reached the usage limit of your license key. This can occur in the following situations:
*
* * You are on a free subscription without a connected payment method and have exceeded the allowed usage threshold.
* * Your account has overdue invoices and the grace period has ended.
*
* To extend the limit and restore access, please update the required details in the
* [Customer Portal](https://portal.ckeditor.com) or
* [contact our customer support](https://ckeditor.com/contact).
*
* @error license-key-usage-limit
*/
throw new CKEditorError('license-key-usage-limit');
}
if (reason == 'distributionChannel') {
/**
* Your license does not allow the current distribution channel.
*
* These are the available distribution channels:
* * Self-hosted - the editor is installed via npm or from a ZIP package
* * Cloud - the editor is run from CDN
*
* The licenses available include:
* * GPL license for open-source users.
* * Commercial plans (Cloud or sales-assisted).
*
* The relation between distribution channels and licenses works as follows:
* * With the 'GPL' license key, you may use the editor installed via npm or a ZIP package (self-hosted).
* * With the CKEditor Cloud plans, you may use the editor via our CDN.
* * With the CKEditor Custom plans, depending on your plan details, you can use the editor via npm
* or a ZIP package (self-hosted) or Cloud (CDN).
*
* {@glink getting-started/licensing/usage-based-billing#key-terms Read more about distributions in the documentation}.
* Please verify your installation or [contact support](https://ckeditor.com/contact/) for assistance.
* Should you need to migrate your installation from npm to CDN, please refer to our
* [dedicated migration guides](https://ckeditor.com/docs/ckeditor5/latest/updating/migrations/vanilla-js.html).
*
* @error license-key-invalid-distribution-channel
*/
throw new CKEditorError('license-key-invalid-distribution-channel');
}
/* istanbul ignore next -- @preserve */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const unreachable = reason;
}, 0);
this._showLicenseError = () => { };
}
/**
* This part of the code is _not_ executed in installations under the GPL license (with `config.licenseKey = 'GPL'`).
*
* It is only executed when a specific license key is provided. If you are uncertain whether
* this applies to your installation, please contact our support team.
*/
async _sendUsageRequest(endpoint, request) {
const headers = new Headers({ 'Content-Type': 'application/json' });
const response = await fetch(new URL(endpoint), {
method: 'POST',
headers,
body: JSON.stringify(request)
});
if (!response.ok) {
// TODO: refine message.
throw new Error(`HTTP Response: ${response.status}`);
}
return response.json();
}
}
export default Editor;
function collectUsageData(editor) {
const collectedData = getEditorUsageData(editor);
function setUsageData(path, value) {
if (get(collectedData, path) !== undefined) {
/**
* The error thrown when trying to set the usage data path that was already set.
* Make sure that you are not setting the same path multiple times.
*
* @error editor-usage-data-path-already-set
* @param {string} path The path that was already set.
*/
throw new CKEditorError('editor-usage-data-path-already-set', { path });
}
set(collectedData, path, value);
}
editor.fire('collectUsageData', {
setUsageData
});
return collectedData;
}
/**
* This error is thrown when trying to pass a `<textarea>` element to a `create()` function of an editor class.
*
* The only editor type which can be initialized on `<textarea>` elements is
* the {@glink getting-started/setup/editor-types#classic-editor classic editor}.
* This editor hides the passed element and inserts its own UI next to it. Other types of editors reuse the passed element as their root
* editable element and therefore `<textarea>` is not appropriate for them. Use a `<div>` or another text container instead:
*
* ```html
* <div id="editor">
* <p>Initial content.</p>
* </div>
* ```
*
* @error editor-wrong-element
*/