ckeditor5-1.0.x-dev/js/ckeditor5.admin.js
js/ckeditor5.admin.js
(function (Drupal, drupalSettings, $) {
const announcements = {
onButtonMovedActive(name) {
Drupal.announce(`Button ${name} has been moved to the active toolbar.`);
},
onButtonCopiedActive(name) {
Drupal.announce(`Button ${name} has been copied to the active toolbar.`);
},
onButtonMovedInactive(name) {
Drupal.announce(`Button ${name} has been removed from the active toolbar.`);
},
}
const toolbarHelp = [
{
message: Drupal.t(
"The toolbar buttons that don't fit the user's browser window width will be grouped in a dropdown. If multiple toolbar rows are preferred, those can be configured by adding an explicit wrapping breakpoint wherever you want to start a new row.",
null,
{
context: "CKEditor 5 toolbar help text, default, no explicit wrapping breakpoint",
}
),
button: "-",
condition: false,
},
{
message: Drupal.t(
"You have configured a multi-row toolbar by using an explicit wrapping breakpoint. This may not work well in narrow browser windows. To use automatic grouping, remove any of these divider buttons.",
null,
{
context: "CKEditor 5 toolbar help text, with explicit wrapping breakpoint",
}
),
button: "-",
condition: true,
},
];
/**
* CKEditor 5 Admin provided in admin.js, exposes mountApp() and unmountApp().
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches admin app to edit the CKEditor 5 toolbar.
* @prop {Drupal~behaviorDetach} detach
* Detaches admin app from the CKEditor 5 configuration form on 'unload'.
*
* @see https://github.com/zrpnr/ckeditor5-drupal-admin
*/
Drupal.behaviors.ckeditor5Admin = {
buttonValue: '',
attach(context) {
const container = context.querySelector('#ckeditor5-toolbar-app');
const available = context.querySelector('#ckeditor5-toolbar-buttons-available');
const selected = context.querySelector('[class*="editor-settings-toolbar-items"]');
const { language } = drupalSettings.ckeditor5;
if (container && window.CKEDITOR5_ADMIN) {
// Attempting to mount the app multiple times can cause errors.
if (!container.dataset.hasOwnProperty('vApp')) {
[available, selected]
.filter((el) => el)
.forEach((el) => {
el.classList.add('visually-hidden');
});
CKEDITOR5_ADMIN.mountApp({ announcements, toolbarHelp, language });
}
}
// Safari's focus outlines take into account absolute positioned elements.
// When a toolbar option is blurred, the portion of the focus outline
// surrounding the absolutely positioned tooltip does not go away. To
// prevent keyboard navigation users from seeing outline artifacts for
// every option they've tabbed through, we provide a keydown listener
// that can catch blur-causing events before the blur happens. If the
// tooltip is hidden before the blur event, the outline will disappear
// correctly.
once('safari-focus-fix', document.querySelectorAll('.ckeditor5-toolbar-item')).forEach((item) => {
item.addEventListener('keydown', (e) => {
const keyCodeDirections = {
9: 'tab',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
}
if (['tab', 'left', 'up', 'right', 'down'].includes(keyCodeDirections[e.keyCode])) {
let hideTip = false;
const isActive = e.target.closest('[data-button-list="ckeditor5-toolbar-active__buttons"]');
if (isActive) {
if (['tab', 'left', 'up', 'right'].includes(keyCodeDirections[e.keyCode])) {
hideTip = true;
}
} else {
if (['tab', 'down'].includes(keyCodeDirections[e.keyCode])) {
hideTip = true;
}
}
if (hideTip) {
e.target.querySelector('[data-expanded]').setAttribute('data-expanded', 'false');
}
}
});
});
/**
* Updates the UI state info in the form's 'data-drupal-ui-state' attribute.
*
* @param {object} states
* An object with one or more items with the structure { ui-property: stored-value }
*/
const updateUiStateStorage = (states) => {
const form = document.querySelector('#filter-format-edit-form, #filter-format-add-form');
// Get the current stored UI state as an object.
const currentStates = form.hasAttribute('data-drupal-ui-state') ? JSON.parse(
form.getAttribute('data-drupal-ui-state'),
) : {};
// Store the updated UI state object as an object literal in the parent
// form's 'data-drupal-ui-state' attribute.
form.setAttribute('data-drupal-ui-state', JSON.stringify({...currentStates, ...states}));
};
/**
* Gets a stored UI state property.
*
* @param {string} property
* The UI state property to retrieve the value of.
*
* @return {string|null}
* When present, the stored value of the property.
*/
const getUiStateStorage = (property) => {
const form = document.querySelector('#filter-format-edit-form, #filter-format-add-form');
if (form === null) {
return;
}
// Parse the object literal stored in the form's 'data-drupal-ui-state'
// attribute and return the value of the object property that matches
// the 'property' argument.
return form.hasAttribute('data-drupal-ui-state') ? JSON.parse(
form.getAttribute('data-drupal-ui-state'),
)[property] : null;
}
// Add an attribute to the parent form for storing UI states, so this
// information can be retrieved after AJAX rebuilds.
once('ui-state-storage', document.querySelector('#filter-format-edit-form, #filter-format-add-form')).forEach((form) => {
form.setAttribute('data-drupal-ui-state', JSON.stringify({}));
});
/**
* Maintains the active vertical tab after AJAX rebuild.
*
* @param {Element} verticalTabs
* The vertical tabs element.
*/
const maintainActiveVerticalTab = (verticalTabs) => {
const id = verticalTabs.id;
// If the UI state storage has an active tab, click that tab.
const activeTab = getUiStateStorage(`${id}-active-tab`);
if (activeTab) {
setTimeout(() => {
document.querySelector(activeTab).click();
});
}
// Add click listener that adds any tab click into UI storage.
verticalTabs.querySelectorAll('.vertical-tabs__menu').forEach((tab) => {
tab.addEventListener('click', (e) => {
const state = {}
const href = e.target.closest('[href]').getAttribute('href').split('--')[0];
state[`${id}-active-tab`] = `#${id} [href^='${href}']`
updateUiStateStorage(state);
});
});
}
once('plugin-settings', document.querySelector('#plugin-settings-wrapper')).forEach(maintainActiveVerticalTab);
once('filter-settings', document.querySelector('#filter-settings-wrapper')).forEach(maintainActiveVerticalTab);
// Add listeners to maintain focus after AJAX rebuilds.
const selectedButtons = document.querySelector('#ckeditor5-toolbar-buttons-selected');
once('textarea-listener', selectedButtons).forEach(textarea => {
textarea.addEventListener('change', (e) => {
const buttonName = document.activeElement.getAttribute('data-button-name');
if(!buttonName) {
// If there is no 'data-button-name' attribute, then the config
// is happening via mouse.
return;
}
let focusSelector = '';
// Divider elements are treated differently as there can be multiple
// elements with the same button name.
if(['divider','wrapping'].includes(buttonName)) {
const oldConfig = JSON.parse(e.detail.priorValue);
const newConfig = JSON.parse(e.target.innerHTML);
// If the divider is being removed from active buttons, it does not
// 'move' anywhere. Move focus to the prior active button
if (oldConfig.length > newConfig.length) {
for (let item = 0; item < newConfig.length; item++) {
if (newConfig[item] !== oldConfig[item]) {
focusSelector = `[data-button-list="ckeditor5-toolbar-active__buttons"] li:nth-child(${Math.min(item - 1, 0)})`;
break;
}
}
} else if (oldConfig.length < newConfig.length) {
// If the divider is being added, it will be the last active item.
focusSelector = '[data-button-list="ckeditor5-toolbar-active__buttons"] li:last-child'
} else {
// When moving a dividers position within the active buttons.
document.querySelectorAll(`[data-button-list="ckeditor5-toolbar-active__buttons"] [data-button-name='${buttonName}']`).forEach((divider, index) => {
if (divider === document.activeElement) {
focusSelector = `${buttonName}|${index}`
}
});
}
} else {
focusSelector = `[data-button-name='${buttonName}']`
}
// Store the focus selector in an attribute on the form itself, which
// will not be overwritten after the AJAX rebuild. This makes it
// the value available to the textarea focus listener that is
// triggered after the AJAX rebuild.
updateUiStateStorage({focusSelector: focusSelector});
});
textarea.addEventListener('focus', (e) => {
// The selector that should receive focus is stored in the parent
// form element. Move focus to that selector.
const focusSelector = getUiStateStorage('focusSelector');
if (focusSelector) {
// If focusSelector includes '|', it is a separator that is being
// moved within the active button list. Different logic us used to
// determine focus since there can be multiple separators of the
// same type within the active buttons list.
if (focusSelector.includes('|')) {
[buttonName, count] = focusSelector.split('|');
document.querySelectorAll(`[data-button-list="ckeditor5-toolbar-active__buttons"] [data-button-name='${buttonName}']`).forEach((item, index) => {
if (index === parseInt(count, 10)) {
item.focus();
}
});
} else {
const toFocus = document.querySelector(focusSelector);
if (toFocus) {
toFocus.focus();
}
}
}
});
});
},
detach(context, settings, trigger) {
const container = context.querySelector('#ckeditor5-toolbar-app');
if (container && trigger === 'unload' && window.CKEDITOR5_ADMIN) {
CKEDITOR5_ADMIN.unmountApp();
}
}
}
// Make a copy of the default filterStatus attach behaviors so it can be
// called within this module's override of it.
const originalFilterStatusAttach = Drupal.behaviors.filterStatus.attach;
// Overrides the default filterStatus to provided functionality needs
// specific to CKEditor 5.
Drupal.behaviors.filterStatus.attach = function(context, settings) {
// CKEditor 5 has uses cases that require updating the filter settings via
// AJAX. When this happens, the checkboxes that enable filters must be
// reprocessed by the filterStatus behavior. For this to occur:
// - The element must be unregistered with jQuery once() so the process can
// run again and take into account any filter settings elements that have
// been added or removed from the DOM.
// - Any listeners to the 'click.filterUpdate' event should be removed so
// they do not conflict with event listeners that are added as part of the
// AJAX refresh.
$(context)
.find('#filters-status-wrapper input.form-checkbox')
.removeOnce('filter-status')
.off('click.filterUpdate');
// Call the original
originalFilterStatusAttach(context, settings);
}
})(Drupal, drupalSettings, jQuery, once);
