
| Current Path : /var/www/html/pallets/web/core/modules/ckeditor5/js/ |
Linux ift1.ift-informatik.de 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 |
| Current File : /var/www/html/pallets/web/core/modules/ckeditor5/js/ckeditor5.es6.js |
/**
* @file
* CKEditor 5 implementation of {@link Drupal.editors} API.
*/
((Drupal, debounce, CKEditor5, $, once) => {
// CKEditor 5 is incompatible with IE11. When IE11 is detected, the CKEditor 5
// variable is null. In those instances, exit early since CKEditor 5 is not
// loaded.
if (!CKEditor5) {
return;
}
/**
* The CKEditor 5 instances.
*
* @type {Map}
*/
Drupal.CKEditor5Instances = new Map();
/**
* The callback functions.
*
* @type {Map}
*/
const callbacks = new Map();
/**
* List of element ids with the required attribute.
*
* @type {Set}
*/
const required = new Set();
/**
* Get the value of the (deep) property on name from scope.
*
* @param {object} scope
* Object used to search for the function.
* @param {string} name
* The path to access in the scope object.
*
* @return {null|function}
* The corresponding function from the scope object.
*/
function findFunc(scope, name) {
if (!scope) {
return null;
}
const parts = name.includes('.') ? name.split('.') : name;
if (parts.length > 1) {
return findFunc(scope[parts.shift()], parts);
}
return typeof scope[parts[0]] === 'function' ? scope[parts[0]] : null;
}
/**
* Transform a config key in a callback function or execute the function
* to dynamically build the configuration entry.
*
* @param {object} config
* The plugin configuration object.
*
* @return {null|function|*}
* Resulting configuration value.
*/
function buildFunc(config) {
const { func } = config;
// Assuming a global object.
const fn = findFunc(window, func.name);
if (typeof fn === 'function') {
const result = func.invoke ? fn(...func.args) : fn;
return result;
}
return null;
}
/**
* Converts a string representing regexp to a RegExp object.
*
* @param {Object} config
* An object containing configuration.
* @param {string} config.pattern
* The regexp pattern that is used to create the RegExp object.
*
* @return {RegExp}
* Regexp object built from the string regexp.
*/
function buildRegexp(config) {
const { pattern } = config.regexp;
const main = pattern.match(/\/(.+)\/.*/)[1];
const options = pattern.match(/\/.+\/(.*)/)[1];
return new RegExp(main, options);
}
/**
* Casts configuration items to correct types.
*
* @param {Object} config
* The config object.
* @return {Object}
* The config object with items transformed to correct type.
*/
function processConfig(config) {
/**
* Processes an array in config recursively.
*
* @param {Array} config
* An array that should be processed recursively.
* @return {Array}
* An array that has been processed recursively.
*/
function processArray(config) {
return config.map((item) => {
if (typeof item === 'object') {
return processConfig(item);
}
return item;
});
}
return Object.entries(config).reduce((processed, [key, value]) => {
if (typeof value === 'object') {
// Check for null values.
if (!value) {
return processed;
}
if (value.hasOwnProperty('func')) {
processed[key] = buildFunc(value);
} else if (value.hasOwnProperty('regexp')) {
processed[key] = buildRegexp(value);
} else if (Array.isArray(value)) {
processed[key] = processArray(value);
} else {
processed[key] = processConfig(value);
}
} else {
processed[key] = value;
}
return processed;
}, {});
}
/**
* Set an id to a data-attribute for registering this element instance.
*
* @param {Element} element
* An element that should receive unique ID.
*
* @return {string}
* The id to use for this element.
*/
const setElementId = (element) => {
const id = Math.random().toString().slice(2, 9);
element.setAttribute('data-ckeditor5-id', id);
return id;
};
/**
* Return a unique selector for the element.
*
* @param {HTMLElement} element
* An element which unique ID should be retrieved.
*
* @return {string}
* The id to use for this element.
*/
const getElementId = (element) => element.getAttribute('data-ckeditor5-id');
/**
* Select CKEditor 5 plugin classes to include.
*
* Found in the CKEditor 5 global JavaScript object as {package.Class}.
*
* @param {Array} plugins
* List of package and Class name of plugins
*
* @return {Array}
* List of JavaScript Classes to add in the extraPlugins property of config.
*/
function selectPlugins(plugins) {
return plugins.map((pluginDefinition) => {
const [build, name] = pluginDefinition.split('.');
if (CKEditor5[build] && CKEditor5[build][name]) {
return CKEditor5[build][name];
}
// eslint-disable-next-line no-console
console.warn(`Failed to load ${build} - ${name}`);
return null;
});
}
/**
* Process a group of CSS rules.
*
* @param {CSSGroupingRule} rulesGroup
* A complete stylesheet or a group of nested rules like @media.
*/
function processRules(rulesGroup) {
try {
// eslint-disable-next-line no-use-before-define
[...rulesGroup.cssRules].forEach(ckeditor5SelectorProcessing);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(
`Stylesheet ${rulesGroup.href} not included in CKEditor reset due to the browser's CORS policy.`,
);
}
}
/**
* Processes CSS rules dynamically to account for CKEditor 5 in off canvas.
*
* This is achieved by doing the following steps:
* - Adding a donut scope to off canvas rules, so they don't apply within the
* editor element.
* - Editor specific rules (i.e. those with .ck* selectors) are duplicated and
* prefixed with the off canvas selector to ensure they have higher
* specificity over the off canvas reset.
*
* The donut scope prevents off canvas rules from applying to the CKEditor 5
* editor element. Transforms a:
* - #drupal-off-canvas strong
* rule into:
* - #drupal-off-canvas strong:not([data-drupal-ck-style-fence] *)
*
* This means that the rule applies to all <strong> elements inside
* #drupal-off-canvas, except for <strong> elements who have a with a parent
* with the "data-drupal-ck-style-fence" attribute.
*
* For example:
* <div id="drupal-off-canvas">
* <p>
* <strong>Off canvas reset</strong>
* </p>
* <p data-drupal-ck-style-fence>
* <!--
* this strong elements matches the `[data-drupal-ck-style-fence] *`
* selector and is excluded from the off canvas reset rule.
* -->
* <strong>Off canvas reset NOT applied.</strong>
* </p>
* </div>
*
* The donut scope does not prevent CSS inheritance. There is CSS that resets
* following properties to prevent inheritance: background, border,
* box-sizing, margin, padding, position, text-decoration, transition,
* vertical-align and word-wrap.
*
* All .ck* CSS rules are duplicated and prefixed with the off canvas selector
* To ensure they have higher specificity and are not reset too aggressively.
*
* @param {CSSRule} rule
* A single CSS rule to be analysed and changed if necessary.
*/
function ckeditor5SelectorProcessing(rule) {
// Handle nested rules in @media, @support, etc.
if (rule.cssRules) {
processRules(rule);
}
if (!rule.selectorText) {
return;
}
const offCanvasId = '#drupal-off-canvas';
const CKEditorClass = '.ck';
const styleFence = '[data-drupal-ck-style-fence]';
if (
rule.selectorText.includes(offCanvasId) ||
rule.selectorText.includes(CKEditorClass)
) {
rule.selectorText = rule.selectorText
.split(/,/g)
.map((selector) => {
// Only change rules that include #drupal-off-canvas in the selector.
if (selector.includes(offCanvasId)) {
return `${selector.trim()}:not(${styleFence} *)`;
}
// Duplicate CKEditor 5 styles with higher specificity for proper
// display in off canvas elements.
if (selector.includes(CKEditorClass)) {
// Return both rules to avoid replacing the existing rules.
return [
selector.trim(),
selector
.trim()
.replace(
CKEditorClass,
`${offCanvasId} ${styleFence} ${CKEditorClass}`,
),
];
}
return selector;
})
.flat()
.join(', ');
}
}
/**
* Adds CSS to ensure proper styling of CKEditor 5 inside off-canvas dialogs.
*
* @param {HTMLElement} element
* The element the editor is attached to.
*/
function offCanvasCss(element) {
const fenceName = 'data-drupal-ck-style-fence';
const editor = Drupal.CKEditor5Instances.get(
element.getAttribute('data-ckeditor5-id'),
);
editor.ui.view.element.setAttribute(fenceName, '');
// Only proceed if the styles haven't been added yet.
if (once('ckeditor5-off-canvas-reset', 'body').length) {
// For all rules on the page, add the donut scope for
// rules containing the #drupal-off-canvas selector.
[...document.styleSheets].forEach(processRules);
const prefix = `#drupal-off-canvas [${fenceName}]`;
// Additional styles that need to be explicity added in addition to the
// prefixed versions of existing css in `existingCss`.
const addedCss = [
`${prefix} .ck.ck-content {display:block;min-height:5rem;}`,
`${prefix} .ck.ck-content * {display:initial;background:initial;color:initial;padding:initial;}`,
`${prefix} .ck.ck-content li {display:list-item}`,
`${prefix} .ck.ck-content ol li {list-style-type: decimal}`,
`${prefix} .ck[contenteditable], ${prefix} .ck[contenteditable] * {-webkit-user-modify: read-write;-moz-user-modify: read-write;}`,
];
// Styles to ensure block elements are displayed as such inside
// off-canvas dialogs. These are all element types that are styled with
// ` all: initial;` in the off-canvas reset that should default to being
// displayed as blocks within CKEditor.
// @see core/misc/dialog/off-canvas.reset.pcss.css
const blockSelectors = [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'p',
'ol',
'ul',
'address',
'article',
'aside',
'blockquote',
'body',
'dd',
'div',
'dl',
'dt',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'header',
'hgroup',
'hr',
'html',
'legend',
'main',
'menu',
'pre',
'section',
'xmp',
]
.map((blockElement) => `${prefix} .ck.ck-content ${blockElement}`)
.join(', \n');
const blockCss = `${blockSelectors} { display: block; }`;
const prefixedCss = [...addedCss, blockCss].join('\n');
// Create a new style tag with the prefixed styles added above.
const offCanvasCssStyle = document.createElement('style');
offCanvasCssStyle.textContent = prefixedCss;
offCanvasCssStyle.setAttribute('id', 'ckeditor5-off-canvas-reset');
document.body.appendChild(offCanvasCssStyle);
}
}
/**
* Integration of CKEditor 5 with the Drupal editor API.
*
* @namespace
*
* @see Drupal.editorAttach
*/
Drupal.editors.ckeditor5 = {
/**
* Editor attach callback.
*
* @param {HTMLElement} element
* The element to attach the editor to.
* @param {string} format
* The text format for the editor.
*/
attach(element, format) {
const { editorClassic } = CKEditor5;
const { toolbar, plugins, config, language } = format.editorSettings;
const extraPlugins = selectPlugins(plugins);
const pluginConfig = processConfig(config);
const editorConfig = {
extraPlugins,
toolbar,
...pluginConfig,
// Language settings have a conflict between the editor localization
// settings and the "language" plugin.
language: { ...pluginConfig.language, ...language },
};
// Set the id immediately so that it is available when onChange is called.
const id = setElementId(element);
const { ClassicEditor } = editorClassic;
ClassicEditor.create(element, editorConfig)
.then((editor) => {
// Save a reference to the initialized instance.
Drupal.CKEditor5Instances.set(id, editor);
// CKEditor 4 had a feature to remove the required attribute
// see: https://www.drupal.org/project/drupal/issues/1954968
if (element.hasAttribute('required')) {
required.add(id);
element.removeAttribute('required');
}
// Integrate CKEditor 5 viewport offset with Drupal displace.
// @see \Drupal\Tests\ckeditor5\FunctionalJavascript\CKEditor5ToolbarTest
// @see https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorui-EditorUI.html#member-viewportOffset
$(document).on(
`drupalViewportOffsetChange.ckeditor5.${id}`,
(event, offsets) => {
editor.ui.viewportOffset = offsets;
},
);
editor.model.document.on('change:data', () => {
const callback = callbacks.get(id);
if (callback) {
if (editor.plugins.has('SourceEditing')) {
// If the change:data is being called while in source editing
// mode, it means that the form is being submitted. To avoid
// race conditions, in this case the callback gets called
// without decorating the callback with debounce.
// @see https://www.drupal.org/i/3229174
// @see Drupal.editorDetach
if (editor.plugins.get('SourceEditing').isSourceEditingMode) {
callback();
return;
}
}
// Marks the field as changed.
// @see Drupal.editorAttach
debounce(callback, 400)();
}
});
const isOffCanvas = element.closest('#drupal-off-canvas');
if (isOffCanvas) {
offCanvasCss(element);
}
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
});
},
/**
* Editor detach callback.
*
* @param {HTMLElement} element
* The element to detach the editor from.
* @param {string} format
* The text format used for the editor.
* @param {string} trigger
* The event trigger for the detach.
*/
detach(element, format, trigger) {
const id = getElementId(element);
const editor = Drupal.CKEditor5Instances.get(id);
if (!editor) {
return;
}
$(document).off(`drupalViewportOffsetChange.ckeditor5.${id}`);
if (trigger === 'serialize') {
editor.updateSourceElement();
} else {
element.removeAttribute('contentEditable');
// Prepare variables that will be used when discarding Quickedit changes.
let textElement = null;
let originalValue = null;
const usingQuickEdit = (((Drupal || {}).quickedit || {}).editors || {})
.editor;
if (usingQuickEdit) {
// The revert() function in QuickEdit's text editor does not work with
// CKEditor 5, as it is triggered before CKEditor 5 is fully
// destroyed. This function is overridden so the functionality it
// provides can happen after the CKEditor destroy() promise is
// fulfilled.
// This pulls the necessary values from the QuickEdit Backbone Model
// before it is destroyed, so they can be used by
// `editor.destroy().then()` to perform the expected revert.
Drupal.quickedit.editors.editor.prototype.revert =
function revertQuickeditChanges() {
textElement = this.$textElement[0];
originalValue = this.model.get('originalValue');
};
}
editor
.destroy()
.then(() => {
// If textElement and originalValue are not null, a QuickEdit
// revert has been requested. Perform the revert here as it
// can't happen until the CKEditor instance is destroyed.
if (textElement && originalValue) {
textElement.innerHTML = originalValue;
}
// Clean up stored references.
Drupal.CKEditor5Instances.delete(id);
callbacks.delete(id);
if (required.has(id)) {
element.setAttribute('required', 'required');
required.delete(id);
}
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
});
}
},
/**
* Registers a callback which CKEditor 5 will call on change:data event.
*
* @param {HTMLElement} element
* The element where the change occurred.
* @param {function} callback
* Callback called with the value of the editor.
*/
onChange(element, callback) {
callbacks.set(getElementId(element), callback);
},
/**
* Attaches an inline editor to a DOM element.
*
* @param {HTMLElement} element
* The element to attach the editor to.
* @param {object} format
* The text format used in the editor.
* @param {string} [mainToolbarId]
* The id attribute for the main editor toolbar, if any.
*
* @see Drupal.quickedit.editors.editor
*/
attachInlineEditor(element, format, mainToolbarId) {
const { editorDecoupled } = CKEditor5;
const {
toolbar,
plugins,
config: pluginConfig,
language,
} = format.editorSettings;
const extraPlugins = selectPlugins(plugins);
const config = {
extraPlugins,
toolbar,
language,
...processConfig(pluginConfig),
};
const id = setElementId(element);
const { DecoupledEditor } = editorDecoupled;
DecoupledEditor.create(element, config)
.then((editor) => {
Drupal.CKEditor5Instances.set(id, editor);
const toolbar = document.getElementById(mainToolbarId);
toolbar.appendChild(editor.ui.view.toolbar.element);
editor.model.document.on('change:data', () => {
const callback = callbacks.get(id);
if (callback) {
// Quick Edit requires the current data to update EditorModel.
// @see Drupal.quickedit.editors.editor
debounce(callback, 400)(editor.getData());
}
});
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
});
},
};
/**
* Public API for Drupal CKEditor 5 integration.
*
* @namespace
*/
Drupal.ckeditor5 = {
/**
* Variable storing the current dialog's save callback.
*
* @type {?function}
*/
saveCallback: null,
/**
* Open a dialog for a Drupal-based plugin.
*
* This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
* framework, then opens a dialog at the specified Drupal path.
*
* @param {string} url
* The URL that contains the contents of the dialog.
* @param {function} saveCallback
* A function to be called upon saving the dialog.
* @param {object} dialogSettings
* An object containing settings to be passed to the jQuery UI.
*/
openDialog(url, saveCallback, dialogSettings) {
// Add a consistent dialog class.
const classes = dialogSettings.dialogClass
? dialogSettings.dialogClass.split(' ')
: [];
classes.push('ui-dialog--narrow');
dialogSettings.dialogClass = classes.join(' ');
dialogSettings.autoResize =
window.matchMedia('(min-width: 600px)').matches;
dialogSettings.width = 'auto';
const ckeditorAjaxDialog = Drupal.ajax({
dialog: dialogSettings,
dialogType: 'modal',
selector: '.ckeditor5-dialog-loading-link',
url,
progress: { type: 'fullscreen' },
submit: {
editor_object: {},
},
});
ckeditorAjaxDialog.execute();
// Store the save callback to be executed when this dialog is closed.
Drupal.ckeditor5.saveCallback = saveCallback;
},
};
// Redirect on hash change when the original hash has an associated CKEditor 5.
function redirectTextareaFragmentToCKEditor5Instance() {
const hash = window.location.hash.substr(1);
const element = document.getElementById(hash);
if (element) {
const editorID = getElementId(element);
const editor = Drupal.CKEditor5Instances.get(editorID);
if (editor) {
// Give the CKEditor 5 instance an ID.
editor.sourceElement.nextElementSibling.setAttribute(
'id',
`cke_${hash}`,
);
window.location.replace(`#cke_${hash}`);
}
}
}
$(window).on(
'hashchange.ckeditor',
redirectTextareaFragmentToCKEditor5Instance,
);
// Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
$(window).on('dialog:beforecreate', () => {
$('.ckeditor5-dialog-loading').animate(
{ top: '-40px' },
function removeDialogLoading() {
$(this).remove();
},
);
});
// Respond to dialogs that are saved, sending data back to CKEditor.
$(window).on('editor:dialogsave', (e, values) => {
if (Drupal.ckeditor5.saveCallback) {
Drupal.ckeditor5.saveCallback(values);
}
});
// Respond to dialogs that are closed, removing the current save handler.
$(window).on('dialog:afterclose', () => {
if (Drupal.ckeditor5.saveCallback) {
Drupal.ckeditor5.saveCallback = null;
}
});
})(Drupal, Drupal.debounce, CKEditor5, jQuery, once);