
| 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.admin.es6.js |
/**
* @file
* Provides the admin UI for CKEditor 5.
*/
((Drupal, drupalSettings, $, JSON, once, Sortable, { tabbable }) => {
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: 'wrapping',
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: 'wrapping',
condition: true,
},
];
/**
* Allows attaching listeners to a value.
*
* @type {Observable}
*/
const Observable = class {
/**
* Creates new Observable with a value.
*
* @param {*} value
* The value to be observed.
*/
constructor(value) {
this._listeners = [];
this._value = value;
}
/**
* Notifies subscribers about new values.
*/
notify() {
this._listeners.forEach((listener) => listener(this._value));
}
/**
* Subscribes a listener callback to changes.
*
* @param {Function} listener
* The function to be called when a new value is set.
*/
subscribe(listener) {
this._listeners.push(listener);
}
/**
* The value of the observable.
*
* @return {*}
* The current value.
*/
get value() {
return this._value;
}
/**
* Sets the value of the observable and notifies subscribers.
*
* @param {*} val
* The new value of the observable.
*/
set value(val) {
if (val !== this._value) {
this._value = val;
this.notify();
}
}
};
/**
* Gets selected buttons.
*
* @param {Array} selected
* The selected buttons retrieved from state.
* @param {Array} dividers
* The available dividers.
* @param {Array} available
* The available buttons.
* @return {Array}
* An array containing selected buttons.
*/
const getSelectedButtons = (selected, dividers, available) => {
return selected.map((id) => ({
...[...dividers, ...available].find((button) => button.id === id),
}));
};
/**
* Updates selected buttons to the textarea.
*
* @param {Array} selection
* The current selection.
* @param {Element} textarea
* The textarea element.
*/
const updateSelectedButtons = (selection, textarea) => {
const newValue = JSON.stringify(selection);
const priorValue = textarea.innerHTML;
textarea.value = newValue;
textarea.innerHTML = newValue;
// The textarea is programmatically updated, so no native JavaScript
// event is triggered. Event listeners need to be aware of this config
// update, so a custom event is dispatched immediately after the
// config update.
textarea.dispatchEvent(
new CustomEvent('change', {
detail: {
priorValue,
},
}),
);
};
/**
* Function to add button to selected buttons.
*
* @param {Observable} selection
* The currently selected buttons.
* @param {Element} element
* The element which is being added.
* @param {function} announceChange
* Function to call to announce the change.
*/
const addToSelectedButtons = (selection, element, announceChange) => {
const list = [...selection.value];
list.push(element.dataset.id);
selection.value = list;
if (announceChange) {
setTimeout(() => {
announceChange(element.dataset.label);
});
}
};
/**
* Function to remove button from selected buttons.
*
* @param {Observable} selection
* The currently selected buttons.
* @param {Element} element
* The element which is being removed.
* @param {function} announceChange
* Function to call to announce the change.
*/
const removeFromSelectedButtons = (selection, element, announceChange) => {
const list = [...selection.value];
const index = Array.from(element.parentElement.children).findIndex(
(child) => {
return child === element;
},
);
list.splice(index, 1);
selection.value = list;
if (announceChange) {
setTimeout(() => {
announceChange(element.dataset.label);
});
}
};
/**
* Moves element within active buttons.
*
* @param {Observable} selection
* The currently selected buttons.
* @param {Element} element
* The element being moved.
* @param {Number} dir
* The direction which the element is being moved.
*/
const moveWithinSelectedButtons = (selection, element, dir) => {
const list = [...selection.value];
const index = Array.from(element.parentElement.children).findIndex(
(child) => {
return child === element;
},
);
// If moving up, check it is not the first, else check it is not the last.
const condition = dir < 0 ? index > 0 : index < list.length - 1;
if (condition) {
list.splice(index + dir, 0, list.splice(index, 1)[0]);
selection.value = list;
}
};
/**
* Copies element to active buttons.
*
* @param {Observable} selection
* The currently selected buttons.
* @param {Element} element
* The element to be copied.
* @param {Function} announceChange
* A function to call to announce the change.
*/
const copyToActiveButtons = (selection, element, announceChange) => {
const list = [...selection.value];
list.push(element.dataset.id);
selection.value = list;
setTimeout(() => {
if (announceChange) {
announceChange(element.dataset.label);
}
});
};
/**
* Renders the CKEditor 5 button admin UI.
*
* @param {Element} root
* The element where the admin UI should be rendered.
* @param {Observable} selectedButtons
* An Observable object containing selected buttons.
* @param {Array} availableButtons
* An array containing available buttons.
* @param {Array} dividerButtons
* An array containing available divider buttons.
*/
const render = (root, selectedButtons, availableButtons, dividerButtons) => {
const toolbarHelpText = toolbarHelp
.filter(
(helpItem) =>
selectedButtons.value.includes(helpItem.button) ===
helpItem.condition,
)
.map((helpItem) => helpItem.message);
// Get the existing toolbar help message.
const existingToolbarHelpText = document.querySelector(
'[data-drupal-selector="ckeditor5-admin-help-message"]',
);
// If the existing toolbar help message does not match the message that is
// about to be rendered, it is new information that should be conveyed to
// assistive tech via announce().
if (
existingToolbarHelpText &&
toolbarHelpText.join('').trim() !==
existingToolbarHelpText.textContent.trim()
) {
Drupal.announce(toolbarHelpText.join(' '));
}
root.innerHTML = Drupal.theme.ckeditor5Admin({
availableButtons: Drupal.theme.ckeditor5AvailableButtons({
buttons: availableButtons.filter(
(button) => !selectedButtons.value.includes(button.id),
),
}),
dividerButtons: Drupal.theme.ckeditor5DividerButtons({
buttons: dividerButtons,
}),
activeToolbar: Drupal.theme.ckeditor5SelectedButtons({
buttons: getSelectedButtons(
selectedButtons.value,
dividerButtons,
availableButtons,
),
}),
helpMessage: toolbarHelpText,
});
// Create sortable groups for available buttons, current toolbar items,
// and dividers.
new Sortable(
root.querySelector(
'[data-button-list="ckeditor5-toolbar-active-buttons"]',
),
{
group: { name: 'toolbar', put: ['divider', 'available'] },
sort: true,
store: {
set: (sortable) => {
selectedButtons.value = sortable.toArray();
},
},
},
);
const toolbarAvailableButtons = new Sortable(
root.querySelector(
'[data-button-list="ckeditor5-toolbar-available-buttons"]',
),
{
group: { name: 'available', put: ['toolbar'] },
sort: false,
onAdd: (event) => {
// If the moved item is a divider, it should not be added to
// the available buttons list.
if (
dividerButtons.find(
(dividerButton) => dividerButton.id === event.item.dataset.id,
)
) {
const { newIndex } = event;
setTimeout(() => {
// Remove the divider button from the available buttons
// list. Draggable reassesses the lists during each drag
// event, so the DOM removal should not be disruptive.
document
.querySelectorAll('.ckeditor5-toolbar-available__buttons li')
[newIndex].remove();
});
}
},
},
);
new Sortable(
root.querySelector(
'[data-button-list="ckeditor5-toolbar-divider-buttons"]',
),
{
group: { name: 'divider', put: false, pull: 'clone', sort: 'false' },
},
);
root
.querySelectorAll('[data-drupal-selector="ckeditor5-toolbar-button"]')
.forEach((element) => {
const expandButton = (event) => {
event.currentTarget
.querySelectorAll('.ckeditor5-toolbar-button')
.forEach((buttonElement) => {
buttonElement.setAttribute('data-expanded', true);
});
};
const retractButton = (event) => {
event.currentTarget
.querySelectorAll('.ckeditor5-toolbar-button')
.forEach((buttonElement) => {
buttonElement.setAttribute('data-expanded', false);
});
};
element.addEventListener('mouseenter', expandButton);
element.addEventListener('focus', expandButton);
element.addEventListener('mouseleave', retractButton);
element.addEventListener('blur', retractButton);
element.addEventListener('keyup', (event) => {
// Keys supported by the admin UI. Depending on the element that is
// triggering the event, the event could trigger changes in the state.
// Changes to the state trigger re-rendering of the admin UI, which
// means that for consistent navigation, each action modifying state
// needs to set focus back on the button that is being moved. The state
// change also triggers an AJAX request which re-renders parts of the
// form, and moves the focus to the triggering form element, meaning
// that focus needs to be set back on the button again.
const supportedKeys = [
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown',
];
const dir = document.documentElement.dir;
if (supportedKeys.includes(event.key)) {
if (event.currentTarget.dataset.divider.toLowerCase() === 'true') {
switch (event.key) {
case 'ArrowDown': {
const announceChange = (name) => {
Drupal.announce(
Drupal.t(
'Button @name has been copied to the active toolbar.',
{ '@name': name },
),
);
};
copyToActiveButtons(
selectedButtons,
event.currentTarget,
announceChange,
);
// Focus the last button since new button is always added to the
// end of the list.
root
.querySelector(
'[data-button-list="ckeditor5-toolbar-active-buttons"] li:last-child',
)
.focus();
break;
}
}
} else if (
selectedButtons.value.includes(event.currentTarget.dataset.id)
) {
const index = Array.from(
element.parentElement.children,
).findIndex((child) => {
return child === element;
});
switch (event.key) {
case 'ArrowLeft': {
const leftOffset = dir === 'ltr' ? -1 : 1;
moveWithinSelectedButtons(
selectedButtons,
event.currentTarget,
leftOffset,
);
// Move focus to left or right from the current index depending
// on current language direction. Use index instead of the
// data-id because dividers don't have a unique ID.
root
.querySelectorAll(
'[data-button-list="ckeditor5-toolbar-active-buttons"] li',
)
[index + leftOffset].focus();
break;
}
case 'ArrowRight': {
const rightOffset = dir === 'ltr' ? 1 : -1;
moveWithinSelectedButtons(
selectedButtons,
event.currentTarget,
rightOffset,
);
// Move focus to right or left from the current index depending
// on current language direction. Use index instead of the
// data-id because dividers don't have a unique ID.
root
.querySelectorAll(
'[data-button-list="ckeditor5-toolbar-active-buttons"] li',
)
[index + rightOffset].focus();
break;
}
case 'ArrowUp': {
const announceChange = (name) => {
Drupal.announce(
Drupal.t(
'Button @name has been removed from the active toolbar.',
{ '@name': name },
),
);
};
removeFromSelectedButtons(
selectedButtons,
event.currentTarget,
announceChange,
);
// Focus only if the button wasn't a divider because dividers
// are simply removed from the active buttons instead of moving
// to another list.
if (
!dividerButtons.find(
(dividerButton) =>
event.currentTarget.dataset.id === dividerButton.id,
)
) {
// Focus button based on the data-id attribute from the
// available buttons list.
root
.querySelector(
`[data-button-list="ckeditor5-toolbar-available-buttons"] [data-id="${event.currentTarget.dataset.id}"]`,
)
.focus();
}
break;
}
}
} else if (
toolbarAvailableButtons
.toArray()
.includes(event.currentTarget.dataset.id)
) {
switch (event.key) {
case 'ArrowDown': {
const announceChange = (name) => {
Drupal.announce(
Drupal.t(
'Button @name has been moved to the active toolbar.',
{ '@name': name },
),
);
};
addToSelectedButtons(
selectedButtons,
event.currentTarget,
announceChange,
);
// Focus the last button since new button is always added to the
// end of the list.
root
.querySelector(
'[data-button-list="ckeditor5-toolbar-active-buttons"] li:last-child',
)
.focus();
break;
}
}
}
}
});
});
};
/**
* Attach CKEditor 5 admin UI.
*
* @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'.
*/
Drupal.behaviors.ckeditor5Admin = {
attach(context) {
once('ckeditor5-admin-toolbar', '#ckeditor5-toolbar-app').forEach(
(container) => {
const selectedTextarea = context.querySelector(
'#ckeditor5-toolbar-buttons-selected',
);
const available = Object.entries(
JSON.parse(
context.querySelector('#ckeditor5-toolbar-buttons-available')
.innerHTML,
),
).map(([name, attrs]) => ({ name, id: name, ...attrs }));
const dividers = [
{
id: 'divider',
name: '|',
label: Drupal.t('Divider'),
},
{
id: 'wrapping',
name: '-',
label: Drupal.t('Wrapping'),
},
];
// Selected is used for managing the state. Sortable is handling updates
// to the state when the system is operated by mouse. There are
// functions making direct modifications to the state when system is
// operated by keyboard.
const selected = new Observable(
JSON.parse(selectedTextarea.innerHTML).map((name) => {
return [...dividers, ...available].find((button) => {
return button.name === name;
}).id;
}),
);
const mapSelection = (selection) => {
return selection.map((id) => {
return [...dividers, ...available].find((button) => {
return button.id === id;
}).name;
});
};
// Whenever the state is changed, update the textarea with the changes.
// This will also trigger re-render of the admin UI to reinitialize the
// Sortable state.
selected.subscribe((selection) => {
updateSelectedButtons(mapSelection(selection), selectedTextarea);
render(container, selected, available, dividers);
});
[
context.querySelector('#ckeditor5-toolbar-buttons-available'),
context.querySelector('[class*="editor-settings-toolbar-items"]'),
]
.filter((el) => el)
.forEach((el) => {
el.classList.add('visually-hidden');
});
render(container, selected, available, dividers);
},
);
// 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', '.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',
'#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(() => {
const activeTabLink = document.querySelector(activeTab);
activeTabLink.click();
// Only change focus on the plugin-settings-wrapper element.
if (id !== 'plugin-settings-wrapper') {
return;
}
// If the current focused element is not the body, then the user
// navigated away from the vertical tab area and is somewhere else
// within the form. Do not change the current focus.
if (document.activeElement !== document.body) {
return;
}
// If the active element is the body then we can assume that the
// focus was on an element that was replaced by an ajax command.
// If that is the case restore the focus to the active tab that
// was just rebuilt.
const targetTabPane = document.querySelector(
activeTabLink.getAttribute('href'),
);
if (targetTabPane) {
const tabbableElements = tabbable(targetTabPane);
if (tabbableElements.length) {
tabbableElements[0].focus();
}
}
});
}
// 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(
'maintainActiveVerticalTab',
'#plugin-settings-wrapper, #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-id');
if (!buttonName) {
// If there is no 'data-id' 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-id='${buttonName}']`,
)
.forEach((divider, index) => {
if (divider === document.activeElement) {
focusSelector = `${buttonName}|${index}`;
}
});
}
} else {
focusSelector = `[data-id='${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 });
});
textarea.addEventListener('focus', () => {
// 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('|')) {
const [buttonName, count] = focusSelector.split('|');
document
.querySelectorAll(
`[data-button-list="ckeditor5-toolbar-active-buttons"] [data-id='${buttonName}']`,
)
.forEach((item, index) => {
if (index === parseInt(count, 10)) {
item.focus();
}
});
} else {
const toFocus = document.querySelector(focusSelector);
if (toFocus) {
toFocus.focus();
}
}
}
});
});
},
};
/**
* Theme function for CKEditor 5 selected buttons.
*
* @param {Object} options
* An object containing options.
* @param {Array} options.buttons
* An array of selected buttons.
* @return {string}
* The selected buttons markup.
*
* @private
*/
Drupal.theme.ckeditor5SelectedButtons = ({ buttons }) => {
return `
<ul class="ckeditor5-toolbar-tray ckeditor5-toolbar-active__buttons" data-button-list="ckeditor5-toolbar-active-buttons" role="listbox" aria-orientation="horizontal" aria-labelledby="ckeditor5-toolbar-active-buttons-label">
${buttons
.map((button) =>
Drupal.theme.ckeditor5Button({ button, listType: 'active' }),
)
.join('')}
</ul>
`;
};
/**
* Theme function for CKEditor 5 divider buttons.
*
* @param {Object} options
* An object containing options.
* @param {Array} options.buttons
* An array of divider buttons.
* @return {string}
* The CKEditor 5 divider buttons markup.
*
* @private
*/
Drupal.theme.ckeditor5DividerButtons = ({ buttons }) => {
return `
<ul class="ckeditor5-toolbar-tray ckeditor5-toolbar-divider__buttons" data-button-list="ckeditor5-toolbar-divider-buttons" role="listbox" aria-orientation="horizontal" aria-labelledby="ckeditor5-toolbar-divider-buttons-label">
${buttons
.map((button) =>
Drupal.theme.ckeditor5Button({ button, listType: 'divider' }),
)
.join('')}
</ul>
`;
};
/**
* Theme function for CKEditor 5 available buttons.
*
* @param {Object} options
* An object containing options.
* @param {Array} options.buttons
* An array of available buttons.
* @return {string}
* The CKEditor 5 available buttons markup.
*
* @private
*/
Drupal.theme.ckeditor5AvailableButtons = ({ buttons }) => {
return `
<ul class="ckeditor5-toolbar-tray ckeditor5-toolbar-available__buttons" data-button-list="ckeditor5-toolbar-available-buttons" role="listbox" aria-orientation="horizontal" aria-labelledby="ckeditor5-toolbar-available-buttons-label">
${buttons
.map((button) =>
Drupal.theme.ckeditor5Button({ button, listType: 'available' }),
)
.join('')}
</ul>
`;
};
/**
* Theme function for CKEditor 5 buttons.
*
* @param {Object} options
* An object containing options.
* @param {Object} options.button
* An object containing button options.
* @param {String} options.button.label
* Button label.
* @param {String} options.button.id
* Button id.
* @param {String} options.listType
* The type of the list.
* @return {string}
* The CKEditor 5 buttons markup.
*
* @private
*/
Drupal.theme.ckeditor5Button = ({ button: { label, id }, listType }) => {
const buttonInstructions = {
divider: Drupal.t(
'Press the down arrow key to use this divider in the active button list',
),
available: Drupal.t('Press the down arrow key to activate'),
active: Drupal.t(
'Press the up arrow key to deactivate. Use the right and left arrow keys to move position',
),
};
const visuallyHiddenLabel = Drupal.t(`@listType button @label`, {
'@listType': listType !== 'divider' ? listType : 'available',
'@label': label,
});
return `
<li class="ckeditor5-toolbar-item ckeditor5-toolbar-item-${id}" role="option" tabindex="0" data-drupal-selector="ckeditor5-toolbar-button" data-id="${id}" data-label="${label}" data-divider="${
listType === 'divider'
}">
<span class="ckeditor5-toolbar-button ckeditor5-toolbar-button-${id}">
<span class="visually-hidden">${visuallyHiddenLabel}. ${
buttonInstructions[listType]
}</span>
</span>
<span class="ckeditor5-toolbar-tooltip" aria-hidden="true">${label} </span>
</li>
`;
};
/**
* Theme function for CKEditor 5 admin UI.
*
* @param {Object} options
* An object containing options.
* @param {String} options.availableButtons
* Markup for available buttons.
* @param {String} options.dividerButtons
* Markup for divider buttons.
* @param {String} options.activeToolbar
* Markup for active toolbar.
* @param {Array} options.helpMessage
* An array of help messages.
* @return {string}
* The CKEditor 5 admin UI markup.
*
* @private
*/
Drupal.theme.ckeditor5Admin = ({
availableButtons,
dividerButtons,
activeToolbar,
helpMessage,
}) => {
return `
<div data-drupal-selector="ckeditor5-admin-help-message">
<p>${helpMessage.join('</p><p>')}</p>
</div>
<div class="ckeditor5-toolbar-disabled">
<div class="ckeditor5-toolbar-available">
<label id="ckeditor5-toolbar-available-buttons-label">${Drupal.t(
'Available buttons',
)}</label>
${availableButtons}
</div>
<div class="ckeditor5-toolbar-divider">
<label id="ckeditor5-toolbar-divider-buttons-label">${Drupal.t(
'Button divider',
)}</label>
${dividerButtons}
</div>
</div>
<div class="ckeditor5-toolbar-active">
<label id="ckeditor5-toolbar-active-buttons-label">${Drupal.t(
'Active toolbar',
)}</label>
${activeToolbar}
</div>
`;
};
// 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 = (context, settings) => {
const filterStatusCheckboxes = document.querySelectorAll(
'#filters-status-wrapper input.form-checkbox',
);
// 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:
// 1. 'filter-status' must be removed from the element's once registry so
// the process can run again and take into account any filter settings
// elements that have been added or removed from the DOM.
// @see core/assets/vendor/once/once.js
once.remove('filter-status', filterStatusCheckboxes);
// 2. 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.
$(filterStatusCheckboxes).off('click.filterUpdate');
// Call the original behavior.
originalFilterStatusAttach(context, settings);
};
// Activates otherwise-inactive tabs that have form elements with validation
// errors.
// @todo Remove when https://www.drupal.org/project/drupal/issues/2911932 lands.
Drupal.behaviors.tabErrorsVisible = {
attach(context) {
context.querySelectorAll('details .form-item .error').forEach((item) => {
const details = item.closest('details');
if (details.style.display === 'none') {
const tabSelect = document.querySelector(`[href='#${details.id}']`);
if (tabSelect) {
tabSelect.click();
}
}
});
},
};
})(Drupal, drupalSettings, jQuery, JSON, once, Sortable, tabbable);