updated core to 8.6.1 via composer
This commit is contained in:
@@ -5,4 +5,4 @@ package: Core
|
||||
core: 8.x
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- editor
|
||||
- drupal:editor
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* CKEditor button and group configuration user interface.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _) {
|
||||
(function($, Drupal, drupalSettings, _) {
|
||||
Drupal.ckeditor = Drupal.ckeditor || {};
|
||||
|
||||
/**
|
||||
@@ -19,7 +19,9 @@
|
||||
Drupal.behaviors.ckeditorAdmin = {
|
||||
attach(context) {
|
||||
// Process the CKEditor configuration fragment once.
|
||||
const $configurationForm = $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-configuration');
|
||||
const $configurationForm = $(context)
|
||||
.find('.ckeditor-toolbar-configuration')
|
||||
.once('ckeditor-configuration');
|
||||
if ($configurationForm.length) {
|
||||
const $textarea = $configurationForm
|
||||
// Hide the textarea that contains the serialized representation of the
|
||||
@@ -64,13 +66,24 @@
|
||||
// really means that all CKEditor toolbar buttons have been removed.
|
||||
// Hence,all editor features will be removed, so any reactions from
|
||||
// filters will be undone.
|
||||
const $configurationForm = $(context).find('.ckeditor-toolbar-configuration').findOnce('ckeditor-configuration');
|
||||
if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.Model) {
|
||||
const $configurationForm = $(context)
|
||||
.find('.ckeditor-toolbar-configuration')
|
||||
.findOnce('ckeditor-configuration');
|
||||
if (
|
||||
$configurationForm.length &&
|
||||
Drupal.ckeditor.models &&
|
||||
Drupal.ckeditor.models.Model
|
||||
) {
|
||||
const config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
|
||||
const buttons = Drupal.ckeditor.views.controller.getButtonList(config);
|
||||
const $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
|
||||
const $activeToolbar = $('.ckeditor-toolbar-configuration').find(
|
||||
'.ckeditor-toolbar-active',
|
||||
);
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
$activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]);
|
||||
$activeToolbar.trigger('CKEditorToolbarChanged', [
|
||||
'removed',
|
||||
buttons[i],
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -82,7 +95,6 @@
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.ckeditor = {
|
||||
|
||||
/**
|
||||
* A hash of View instances.
|
||||
*
|
||||
@@ -125,8 +137,7 @@
|
||||
view.isProcessing = true;
|
||||
|
||||
Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
view.model.set('isDirty', true);
|
||||
callback(true);
|
||||
}
|
||||
@@ -152,12 +163,18 @@
|
||||
}
|
||||
// If there are any rows with just a placeholder group, mark the row as a
|
||||
// placeholder.
|
||||
$row.parent().children().each(function () {
|
||||
$row = $(this);
|
||||
if ($row.find('.ckeditor-toolbar-group').not('.placeholder').length === 0) {
|
||||
$row.addClass('placeholder');
|
||||
}
|
||||
});
|
||||
$row
|
||||
.parent()
|
||||
.children()
|
||||
.each(function() {
|
||||
$row = $(this);
|
||||
if (
|
||||
$row.find('.ckeditor-toolbar-group').not('.placeholder').length ===
|
||||
0
|
||||
) {
|
||||
$row.addClass('placeholder');
|
||||
}
|
||||
});
|
||||
view.model.set('isDirty', true);
|
||||
},
|
||||
|
||||
@@ -173,7 +190,7 @@
|
||||
* been closed.
|
||||
*/
|
||||
openGroupNameDialog(view, $group, callback) {
|
||||
callback = callback || function () {};
|
||||
callback = callback || function() {};
|
||||
|
||||
/**
|
||||
* Validates the string provided as a button group title.
|
||||
@@ -194,7 +211,11 @@
|
||||
.find('input')
|
||||
.addClass('error')
|
||||
.attr('aria-invalid', 'true');
|
||||
$(`<div class="description" >${Drupal.t('Please provide a name for the button group.')}</div>`).insertAfter(form.elements[0]);
|
||||
$(
|
||||
`<div class="description" >${Drupal.t(
|
||||
'Please provide a name for the button group.',
|
||||
)}</div>`,
|
||||
).insertAfter(form.elements[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -215,6 +236,7 @@
|
||||
* Closes the dialog when the user cancels or supplies valid data.
|
||||
*/
|
||||
function shutdown() {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
dialog.close(action);
|
||||
|
||||
// The processing marker can be deleted since the dialog has been
|
||||
@@ -238,7 +260,9 @@
|
||||
// Remove all whitespace from the name, lowercase it and ensure
|
||||
// HTML-safe encoding, then use this as the group ID for CKEditor
|
||||
// configuration UI accessibility purposes only.
|
||||
const groupID = `ckeditor-toolbar-group-aria-label-for-${Drupal.checkPlain(name.toLowerCase().replace(/\s/g, '-'))}`;
|
||||
const groupID = `ckeditor-toolbar-group-aria-label-for-${Drupal.checkPlain(
|
||||
name.toLowerCase().replace(/\s/g, '-'),
|
||||
)}`;
|
||||
$group
|
||||
// Update the group container.
|
||||
.removeAttr('aria-label')
|
||||
@@ -275,10 +299,16 @@
|
||||
if (action === 'apply') {
|
||||
shutdown();
|
||||
// Apply the provided name to the button group label.
|
||||
namePlaceholderGroup($group, Drupal.checkPlain(form.elements[0].value));
|
||||
namePlaceholderGroup(
|
||||
$group,
|
||||
Drupal.checkPlain(form.elements[0].value),
|
||||
);
|
||||
// Remove placeholder classes so that new placeholders will be
|
||||
// inserted.
|
||||
$group.closest('.ckeditor-row.placeholder').addBack().removeClass('placeholder');
|
||||
$group
|
||||
.closest('.ckeditor-row.placeholder')
|
||||
.addBack()
|
||||
.removeClass('placeholder');
|
||||
|
||||
// Invoke a user-provided callback and indicate success.
|
||||
callback(true, $group);
|
||||
@@ -289,7 +319,9 @@
|
||||
}
|
||||
|
||||
// Create a Drupal dialog that will get a button group name from the user.
|
||||
const $ckeditorButtonGroupNameForm = $(Drupal.theme('ckeditorButtonGroupNameForm'));
|
||||
const $ckeditorButtonGroupNameForm = $(
|
||||
Drupal.theme('ckeditorButtonGroupNameForm'),
|
||||
);
|
||||
const dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
|
||||
title: Drupal.t('Button group name'),
|
||||
dialogClass: 'ckeditor-name-toolbar-group',
|
||||
@@ -315,7 +347,7 @@
|
||||
const $widget = $form.parent();
|
||||
$widget.find('.ui-dialog-titlebar-close').remove();
|
||||
// Set a click handler on the input and button in the form.
|
||||
$widget.on('keypress.ckeditor', 'input, button', (event) => {
|
||||
$widget.on('keypress.ckeditor', 'input, button', event => {
|
||||
// React to enter key press.
|
||||
if (event.keyCode === 13) {
|
||||
const $target = $(event.currentTarget);
|
||||
@@ -333,11 +365,21 @@
|
||||
}
|
||||
});
|
||||
// Announce to the user that a modal dialog is open.
|
||||
let text = Drupal.t('Editing the name of the new button group in a dialog.');
|
||||
if (typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !== 'undefined') {
|
||||
text = Drupal.t('Editing the name of the "@groupName" button group in a dialog.', {
|
||||
'@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
|
||||
});
|
||||
let text = Drupal.t(
|
||||
'Editing the name of the new button group in a dialog.',
|
||||
);
|
||||
if (
|
||||
typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !==
|
||||
'undefined'
|
||||
) {
|
||||
text = Drupal.t(
|
||||
'Editing the name of the "@groupName" button group in a dialog.',
|
||||
{
|
||||
'@groupName': $group.attr(
|
||||
'data-drupal-ckeditor-toolbar-group-name',
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
Drupal.announce(text);
|
||||
},
|
||||
@@ -346,18 +388,22 @@
|
||||
$(event.target).remove();
|
||||
},
|
||||
});
|
||||
|
||||
// A modal dialog is used because the user must provide a button group
|
||||
// name or cancel the button placement before taking any other action.
|
||||
dialog.showModal();
|
||||
|
||||
$(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input'))
|
||||
$(
|
||||
document
|
||||
.querySelector('.ckeditor-name-toolbar-group')
|
||||
.querySelector('input'),
|
||||
)
|
||||
// When editing, set the "group name" input in the form to the current
|
||||
// value.
|
||||
.attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
|
||||
// Focus on the "group name" input in the form.
|
||||
.trigger('focus');
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -371,20 +417,23 @@
|
||||
Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
|
||||
attach(context) {
|
||||
const $context = $(context);
|
||||
const $ckeditorPluginSettings = $context.find('#ckeditor-plugin-settings').once('ckeditor-plugin-settings');
|
||||
const $ckeditorPluginSettings = $context
|
||||
.find('#ckeditor-plugin-settings')
|
||||
.once('ckeditor-plugin-settings');
|
||||
if ($ckeditorPluginSettings.length) {
|
||||
// Hide all button-dependent plugin settings initially.
|
||||
$ckeditorPluginSettings.find('[data-ckeditor-buttons]').each(function () {
|
||||
const $this = $(this);
|
||||
if ($this.data('verticalTab')) {
|
||||
$this.data('verticalTab').tabHide();
|
||||
}
|
||||
else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
$this.hide();
|
||||
}
|
||||
$this.data('ckeditorButtonPluginSettingsActiveButtons', []);
|
||||
});
|
||||
$ckeditorPluginSettings
|
||||
.find('[data-ckeditor-buttons]')
|
||||
.each(function() {
|
||||
const $this = $(this);
|
||||
if ($this.data('verticalTab')) {
|
||||
$this.data('verticalTab').tabHide();
|
||||
} else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
$this.hide();
|
||||
}
|
||||
$this.data('ckeditorButtonPluginSettingsActiveButtons', []);
|
||||
});
|
||||
|
||||
// Whenever a button is added or removed, check if we should show or
|
||||
// hide the corresponding plugin settings. (Note that upon
|
||||
@@ -394,44 +443,50 @@
|
||||
$context
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
|
||||
.on('CKEditorToolbarChanged.ckeditorAdminPluginSettings', (event, action, button) => {
|
||||
const $pluginSettings = $ckeditorPluginSettings
|
||||
.find(`[data-ckeditor-buttons~=${button}]`);
|
||||
.on(
|
||||
'CKEditorToolbarChanged.ckeditorAdminPluginSettings',
|
||||
(event, action, button) => {
|
||||
const $pluginSettings = $ckeditorPluginSettings.find(
|
||||
`[data-ckeditor-buttons~=${button}]`,
|
||||
);
|
||||
|
||||
// No settings for this button.
|
||||
if ($pluginSettings.length === 0) {
|
||||
return;
|
||||
}
|
||||
// No settings for this button.
|
||||
if ($pluginSettings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const verticalTab = $pluginSettings.data('verticalTab');
|
||||
const activeButtons = $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons');
|
||||
if (action === 'added') {
|
||||
activeButtons.push(button);
|
||||
// Show this plugin's settings if >=1 of its buttons are active.
|
||||
if (verticalTab) {
|
||||
verticalTab.tabShow();
|
||||
}
|
||||
else {
|
||||
// On very narrow viewports, Vertical Tabs remain fieldsets.
|
||||
$pluginSettings.show();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Remove this button from the list of active buttons.
|
||||
activeButtons.splice(activeButtons.indexOf(button), 1);
|
||||
// Show this plugin's settings 0 of its buttons are active.
|
||||
if (activeButtons.length === 0) {
|
||||
const verticalTab = $pluginSettings.data('verticalTab');
|
||||
const activeButtons = $pluginSettings.data(
|
||||
'ckeditorButtonPluginSettingsActiveButtons',
|
||||
);
|
||||
if (action === 'added') {
|
||||
activeButtons.push(button);
|
||||
// Show this plugin's settings if >=1 of its buttons are active.
|
||||
if (verticalTab) {
|
||||
verticalTab.tabHide();
|
||||
verticalTab.tabShow();
|
||||
} else {
|
||||
// On very narrow viewports, Vertical Tabs remain fieldsets.
|
||||
$pluginSettings.show();
|
||||
}
|
||||
else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
$pluginSettings.hide();
|
||||
} else {
|
||||
// Remove this button from the list of active buttons.
|
||||
activeButtons.splice(activeButtons.indexOf(button), 1);
|
||||
// Show this plugin's settings 0 of its buttons are active.
|
||||
if (activeButtons.length === 0) {
|
||||
if (verticalTab) {
|
||||
verticalTab.tabHide();
|
||||
} else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
$pluginSettings.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons', activeButtons);
|
||||
});
|
||||
$pluginSettings.data(
|
||||
'ckeditorButtonPluginSettingsActiveButtons',
|
||||
activeButtons,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -442,7 +497,7 @@
|
||||
* @return {string}
|
||||
* A HTML string for a CKEditor row.
|
||||
*/
|
||||
Drupal.theme.ckeditorRow = function () {
|
||||
Drupal.theme.ckeditorRow = function() {
|
||||
return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
|
||||
};
|
||||
|
||||
@@ -452,11 +507,16 @@
|
||||
* @return {string}
|
||||
* A HTML string for a CKEditor button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorToolbarGroup = function () {
|
||||
Drupal.theme.ckeditorToolbarGroup = function() {
|
||||
let group = '';
|
||||
group += `<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="${Drupal.t('Place a button to create a new button group.')}">`;
|
||||
group += `<h3 class="ckeditor-toolbar-group-name">${Drupal.t('New group')}</h3>`;
|
||||
group += '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
|
||||
group += `<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="${Drupal.t(
|
||||
'Place a button to create a new button group.',
|
||||
)}">`;
|
||||
group += `<h3 class="ckeditor-toolbar-group-name">${Drupal.t(
|
||||
'New group',
|
||||
)}</h3>`;
|
||||
group +=
|
||||
'<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
|
||||
group += '</li>';
|
||||
return group;
|
||||
};
|
||||
@@ -467,7 +527,7 @@
|
||||
* @return {string}
|
||||
* A HTML string for the form for the title of a CKEditor button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNameForm = function () {
|
||||
Drupal.theme.ckeditorButtonGroupNameForm = function() {
|
||||
return '<form><input name="group-name" required="required"></form>';
|
||||
};
|
||||
|
||||
@@ -477,7 +537,7 @@
|
||||
* @return {string}
|
||||
* A HTML string for the button to toggle group names.
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
|
||||
Drupal.theme.ckeditorButtonGroupNamesToggle = function() {
|
||||
return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
|
||||
};
|
||||
|
||||
@@ -487,7 +547,9 @@
|
||||
* @return {string}
|
||||
* A HTML string for the button to create a name for a new button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorNewButtonGroup = function () {
|
||||
return `<li class="ckeditor-add-new-group"><button aria-label="${Drupal.t('Add a CKEditor button group to the end of this row.')}">${Drupal.t('Add group')}</button></li>`;
|
||||
Drupal.theme.ckeditorNewButtonGroup = function() {
|
||||
return `<li class="ckeditor-add-new-group"><button aria-label="${Drupal.t(
|
||||
'Add a CKEditor button group to the end of this row.',
|
||||
)}">${Drupal.t('Add group')}</button></li>`;
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings, _));
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* CKEditor 'drupalimage' plugin admin behavior.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
(function($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Provides the summary for the "drupalimage" plugin settings vertical tab.
|
||||
*
|
||||
@@ -14,23 +14,32 @@
|
||||
*/
|
||||
Drupal.behaviors.ckeditorDrupalImageSettingsSummary = {
|
||||
attach() {
|
||||
$('[data-ckeditor-plugin-id="drupalimage"]').drupalSetSummary((context) => {
|
||||
const root = 'input[name="editor[settings][plugins][drupalimage][image_upload]';
|
||||
$('[data-ckeditor-plugin-id="drupalimage"]').drupalSetSummary(context => {
|
||||
const root =
|
||||
'input[name="editor[settings][plugins][drupalimage][image_upload]';
|
||||
const $status = $(`${root}[status]"]`);
|
||||
const $maxFileSize = $(`${root}[max_size]"]`);
|
||||
const $maxWidth = $(`${root}[max_dimensions][width]"]`);
|
||||
const $maxHeight = $(`${root}[max_dimensions][height]"]`);
|
||||
const $scheme = $(`${root}[scheme]"]:checked`);
|
||||
|
||||
const maxFileSize = $maxFileSize.val() ? $maxFileSize.val() : $maxFileSize.attr('placeholder');
|
||||
const maxDimensions = ($maxWidth.val() && $maxHeight.val()) ? `(${$maxWidth.val()}x${$maxHeight.val()})` : '';
|
||||
const maxFileSize = $maxFileSize.val()
|
||||
? $maxFileSize.val()
|
||||
: $maxFileSize.attr('placeholder');
|
||||
const maxDimensions =
|
||||
$maxWidth.val() && $maxHeight.val()
|
||||
? `(${$maxWidth.val()}x${$maxHeight.val()})`
|
||||
: '';
|
||||
|
||||
if (!$status.is(':checked')) {
|
||||
return Drupal.t('Uploads disabled');
|
||||
}
|
||||
|
||||
let output = '';
|
||||
output += Drupal.t('Uploads enabled, max size: @size @dimensions', { '@size': maxFileSize, '@dimensions': maxDimensions });
|
||||
output += Drupal.t('Uploads enabled, max size: @size @dimensions', {
|
||||
'@size': maxFileSize,
|
||||
'@dimensions': maxDimensions,
|
||||
});
|
||||
if ($scheme.length) {
|
||||
output += `<br />${$scheme.attr('data-label')}`;
|
||||
}
|
||||
@@ -38,4 +47,4 @@
|
||||
});
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings));
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
|
||||
@@ -24,7 +24,10 @@
|
||||
}
|
||||
|
||||
var output = '';
|
||||
output += Drupal.t('Uploads enabled, max size: @size @dimensions', { '@size': maxFileSize, '@dimensions': maxDimensions });
|
||||
output += Drupal.t('Uploads enabled, max size: @size @dimensions', {
|
||||
'@size': maxFileSize,
|
||||
'@dimensions': maxDimensions
|
||||
});
|
||||
if ($scheme.length) {
|
||||
output += '<br />' + $scheme.attr('data-label');
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
* CKEditor implementation of {@link Drupal.editors} API.
|
||||
*/
|
||||
|
||||
(function (Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) {
|
||||
(function(Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) {
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editors.ckeditor = {
|
||||
|
||||
/**
|
||||
* Editor attach callback.
|
||||
*
|
||||
@@ -31,7 +30,9 @@
|
||||
// label so that screen readers say something that is understandable
|
||||
// for end users.
|
||||
const label = $(`label[for=${element.getAttribute('id')}]`).html();
|
||||
format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', { '!label': label });
|
||||
format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', {
|
||||
'!label': label,
|
||||
});
|
||||
|
||||
return !!CKEDITOR.replace(element, format.editorSettings);
|
||||
},
|
||||
@@ -55,8 +56,7 @@
|
||||
if (editor) {
|
||||
if (trigger === 'serialize') {
|
||||
editor.updateElement();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
editor.destroy();
|
||||
element.removeAttribute('contentEditable');
|
||||
}
|
||||
@@ -79,9 +79,12 @@
|
||||
onChange(element, callback) {
|
||||
const editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
editor.on('change', debounce(() => {
|
||||
callback(editor.getData());
|
||||
}, 400));
|
||||
editor.on(
|
||||
'change',
|
||||
debounce(() => {
|
||||
callback(editor.getData());
|
||||
}, 400),
|
||||
);
|
||||
|
||||
// A temporary workaround to control scrollbar appearance when using
|
||||
// autoGrow event to control editor's height.
|
||||
@@ -89,17 +92,24 @@
|
||||
editor.on('mode', () => {
|
||||
const editable = editor.editable();
|
||||
if (!editable.isInline()) {
|
||||
editor.on('autoGrow', (evt) => {
|
||||
const doc = evt.editor.document;
|
||||
const scrollable = CKEDITOR.env.quirks ? doc.getBody() : doc.getDocumentElement();
|
||||
editor.on(
|
||||
'autoGrow',
|
||||
evt => {
|
||||
const doc = evt.editor.document;
|
||||
const scrollable = CKEDITOR.env.quirks
|
||||
? doc.getBody()
|
||||
: doc.getDocumentElement();
|
||||
|
||||
if (scrollable.$.scrollHeight < scrollable.$.clientHeight) {
|
||||
scrollable.setStyle('overflow-y', 'hidden');
|
||||
}
|
||||
else {
|
||||
scrollable.removeStyle('overflow-y');
|
||||
}
|
||||
}, null, null, 10000);
|
||||
if (scrollable.$.scrollHeight < scrollable.$.clientHeight) {
|
||||
scrollable.setStyle('overflow-y', 'hidden');
|
||||
} else {
|
||||
scrollable.removeStyle('overflow-y');
|
||||
}
|
||||
},
|
||||
null,
|
||||
null,
|
||||
10000,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -146,9 +156,17 @@
|
||||
// Find the "Source" button, if any, and replace it with "Sourcedialog".
|
||||
// (The 'sourcearea' plugin only works in CKEditor's iframe mode.)
|
||||
let sourceButtonFound = false;
|
||||
for (let i = 0; !sourceButtonFound && i < settings.toolbar.length; i++) {
|
||||
for (
|
||||
let i = 0;
|
||||
!sourceButtonFound && i < settings.toolbar.length;
|
||||
i++
|
||||
) {
|
||||
if (settings.toolbar[i] !== '/') {
|
||||
for (let j = 0; !sourceButtonFound && j < settings.toolbar[i].items.length; j++) {
|
||||
for (
|
||||
let j = 0;
|
||||
!sourceButtonFound && j < settings.toolbar[i].items.length;
|
||||
j++
|
||||
) {
|
||||
if (settings.toolbar[i].items[j] === 'Source') {
|
||||
sourceButtonFound = true;
|
||||
// Swap sourcearea's "Source" button for sourcedialog's.
|
||||
@@ -182,17 +200,19 @@
|
||||
const externalPlugins = format.editorSettings.drupalExternalPlugins;
|
||||
// Register and load additional CKEditor plugins as necessary.
|
||||
if (externalPlugins) {
|
||||
Object.keys(externalPlugins || {}).forEach((pluginName) => {
|
||||
CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], '');
|
||||
Object.keys(externalPlugins || {}).forEach(pluginName => {
|
||||
CKEDITOR.plugins.addExternal(
|
||||
pluginName,
|
||||
externalPlugins[pluginName],
|
||||
'',
|
||||
);
|
||||
});
|
||||
delete format.editorSettings.drupalExternalPlugins;
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
Drupal.ckeditor = {
|
||||
|
||||
/**
|
||||
* Variable storing the current dialog's save callback.
|
||||
*
|
||||
@@ -226,18 +246,29 @@
|
||||
}
|
||||
|
||||
// Remove any previous loading indicator.
|
||||
$target.css('position', 'relative').find('.ckeditor-dialog-loading').remove();
|
||||
$target
|
||||
.css('position', 'relative')
|
||||
.find('.ckeditor-dialog-loading')
|
||||
.remove();
|
||||
|
||||
// Add a consistent dialog class.
|
||||
const classes = dialogSettings.dialogClass ? dialogSettings.dialogClass.split(' ') : [];
|
||||
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.autoResize = window.matchMedia(
|
||||
'(min-width: 600px)',
|
||||
).matches;
|
||||
dialogSettings.width = 'auto';
|
||||
|
||||
// Add a "Loading…" message, hide it underneath the CKEditor toolbar,
|
||||
// create a Drupal.Ajax instance to load the dialog and trigger it.
|
||||
const $content = $(`<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">${Drupal.t('Loading...')}</span></div>`);
|
||||
const $content = $(
|
||||
`<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">${Drupal.t(
|
||||
'Loading...',
|
||||
)}</span></div>`,
|
||||
);
|
||||
$content.appendTo($target);
|
||||
|
||||
const ckeditorAjaxDialog = Drupal.ajax({
|
||||
@@ -269,7 +300,7 @@
|
||||
|
||||
// Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
|
||||
$(window).on('dialog:beforecreate', (e, dialog, $element, settings) => {
|
||||
$('.ckeditor-dialog-loading').animate({ top: '-40px' }, function () {
|
||||
$('.ckeditor-dialog-loading').animate({ top: '-40px' }, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
@@ -290,22 +321,27 @@
|
||||
|
||||
// Formulate a default formula for the maximum autoGrow height.
|
||||
$(document).on('drupalViewportOffsetChange', () => {
|
||||
CKEDITOR.config.autoGrow_maxHeight = 0.7 * (window.innerHeight - displace.offsets.top - displace.offsets.bottom);
|
||||
CKEDITOR.config.autoGrow_maxHeight =
|
||||
0.7 *
|
||||
(window.innerHeight - displace.offsets.top - displace.offsets.bottom);
|
||||
});
|
||||
|
||||
// Redirect on hash change when the original hash has an associated CKEditor.
|
||||
function redirectTextareaFragmentToCKEditorInstance() {
|
||||
const hash = location.hash.substr(1);
|
||||
const hash = window.location.hash.substr(1);
|
||||
const element = document.getElementById(hash);
|
||||
if (element) {
|
||||
const editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
const id = editor.container.getAttribute('id');
|
||||
location.replace(`#${id}`);
|
||||
window.location.replace(`#${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
$(window).on('hashchange.ckeditor', redirectTextareaFragmentToCKEditorInstance);
|
||||
$(window).on(
|
||||
'hashchange.ckeditor',
|
||||
redirectTextareaFragmentToCKEditorInstance,
|
||||
);
|
||||
|
||||
// Set autoGrow to make the editor grow the moment it is created.
|
||||
CKEDITOR.config.autoGrow_onStartup = true;
|
||||
@@ -330,14 +366,25 @@
|
||||
*
|
||||
* @see http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document
|
||||
*/
|
||||
AjaxCommands.prototype.ckeditor_add_stylesheet = function (ajax, response, status) {
|
||||
AjaxCommands.prototype.ckeditor_add_stylesheet = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
const editor = CKEDITOR.instances[response.editor_id];
|
||||
|
||||
if (editor) {
|
||||
response.stylesheets.forEach((url) => {
|
||||
response.stylesheets.forEach(url => {
|
||||
editor.document.appendStyleSheet(url);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}(Drupal, Drupal.debounce, CKEDITOR, jQuery, Drupal.displace, Drupal.AjaxCommands));
|
||||
})(
|
||||
Drupal,
|
||||
Drupal.debounce,
|
||||
CKEDITOR,
|
||||
jQuery,
|
||||
Drupal.displace,
|
||||
Drupal.AjaxCommands,
|
||||
);
|
||||
|
||||
Vendored
+5
-3
@@ -15,7 +15,9 @@
|
||||
};
|
||||
|
||||
var label = $('label[for=' + element.getAttribute('id') + ']').html();
|
||||
format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', { '!label': label });
|
||||
format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', {
|
||||
'!label': label
|
||||
});
|
||||
|
||||
return !!CKEDITOR.replace(element, format.editorSettings);
|
||||
},
|
||||
@@ -177,13 +179,13 @@
|
||||
});
|
||||
|
||||
function redirectTextareaFragmentToCKEditorInstance() {
|
||||
var hash = location.hash.substr(1);
|
||||
var hash = window.location.hash.substr(1);
|
||||
var element = document.getElementById(hash);
|
||||
if (element) {
|
||||
var editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
var id = editor.container.getAttribute('id');
|
||||
location.replace('#' + id);
|
||||
window.location.replace('#' + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
(function ($, Drupal) {
|
||||
(function($, Drupal) {
|
||||
/**
|
||||
* Provides the summary for the "language" plugin settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorLanguageSettingsSummary = {
|
||||
attach() {
|
||||
$('#edit-editor-settings-plugins-language').drupalSetSummary(context => $('#edit-editor-settings-plugins-language-language-list-type option:selected').text());
|
||||
$('#edit-editor-settings-plugins-language').drupalSetSummary(context =>
|
||||
$(
|
||||
'#edit-editor-settings-plugins-language-language-list-type option:selected',
|
||||
).text(),
|
||||
);
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
})(jQuery, Drupal);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* CKEditor StylesCombo admin behavior.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _) {
|
||||
(function($, Drupal, drupalSettings, _) {
|
||||
/**
|
||||
* Ensures that the "stylescombo" button's metadata remains up-to-date.
|
||||
*
|
||||
@@ -30,10 +30,12 @@
|
||||
const $ckeditorActiveToolbar = $context
|
||||
.find('.ckeditor-toolbar-configuration')
|
||||
.find('.ckeditor-toolbar-active');
|
||||
let previousStylesSet = drupalSettings.ckeditor.hiddenCKEditorConfig.stylesSet;
|
||||
let previousStylesSet =
|
||||
drupalSettings.ckeditor.hiddenCKEditorConfig.stylesSet;
|
||||
const that = this;
|
||||
$context.find('[name="editor[settings][plugins][stylescombo][styles]"]')
|
||||
.on('blur.ckeditorStylesComboSettings', function () {
|
||||
$context
|
||||
.find('[name="editor[settings][plugins][stylescombo][styles]"]')
|
||||
.on('blur.ckeditorStylesComboSettings', function() {
|
||||
const styles = $.trim($(this).val());
|
||||
const stylesSet = that._generateStylesSetSetting(styles);
|
||||
if (!_.isEqual(previousStylesSet, stylesSet)) {
|
||||
@@ -74,7 +76,10 @@
|
||||
}
|
||||
|
||||
// Validate syntax: element[.class...]|label pattern expected.
|
||||
if (style.match(/^ *[a-zA-Z0-9]+ *(\.[a-zA-Z0-9_-]+ *)*\| *.+ *$/) === null) {
|
||||
if (
|
||||
style.match(/^ *[a-zA-Z0-9]+ *(\.[a-zA-Z0-9_-]+ *)*\| *.+ *$/) ===
|
||||
null
|
||||
) {
|
||||
// Instead of failing, we just ignore any invalid styles.
|
||||
continue;
|
||||
}
|
||||
@@ -109,8 +114,12 @@
|
||||
*/
|
||||
Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
|
||||
attach() {
|
||||
$('[data-ckeditor-plugin-id="stylescombo"]').drupalSetSummary((context) => {
|
||||
const styles = $.trim($('[data-drupal-selector="edit-editor-settings-plugins-stylescombo-styles"]').val());
|
||||
$('[data-ckeditor-plugin-id="stylescombo"]').drupalSetSummary(context => {
|
||||
const styles = $.trim(
|
||||
$(
|
||||
'[data-drupal-selector="edit-editor-settings-plugins-stylescombo-styles"]',
|
||||
).val(),
|
||||
);
|
||||
if (styles.length === 0) {
|
||||
return Drupal.t('No styles configured');
|
||||
}
|
||||
@@ -120,4 +129,4 @@
|
||||
});
|
||||
},
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings, _));
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* A Backbone Model for the state of a CKEditor toolbar configuration .
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
(function(Drupal, Backbone) {
|
||||
/**
|
||||
* Backbone model for the CKEditor toolbar configuration state.
|
||||
*
|
||||
@@ -11,61 +11,63 @@
|
||||
*
|
||||
* @augments Backbone.Model
|
||||
*/
|
||||
Drupal.ckeditor.Model = Backbone.Model.extend(/** @lends Drupal.ckeditor.Model# */{
|
||||
Drupal.ckeditor.Model = Backbone.Model.extend(
|
||||
/** @lends Drupal.ckeditor.Model# */ {
|
||||
/**
|
||||
* Default values.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
defaults: /** @lends Drupal.ckeditor.Model# */ {
|
||||
/**
|
||||
* The CKEditor configuration that is being manipulated through the UI.
|
||||
*/
|
||||
activeEditorConfig: null,
|
||||
|
||||
/**
|
||||
* Default values.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
defaults: /** @lends Drupal.ckeditor.Model# */{
|
||||
/**
|
||||
* The textarea that contains the serialized representation of the active
|
||||
* CKEditor configuration.
|
||||
*/
|
||||
$textarea: null,
|
||||
|
||||
/**
|
||||
* Tracks whether the active toolbar DOM structure has been changed. When
|
||||
* true, activeEditorConfig needs to be updated, and when that is updated,
|
||||
* $textarea will also be updated.
|
||||
*/
|
||||
isDirty: false,
|
||||
|
||||
/**
|
||||
* The configuration for the hidden CKEditor instance that is used to
|
||||
* build the features metadata.
|
||||
*/
|
||||
hiddenEditorConfig: null,
|
||||
|
||||
/**
|
||||
* A hash that maps buttons to features.
|
||||
*/
|
||||
buttonsToFeatures: null,
|
||||
|
||||
/**
|
||||
* A hash, keyed by a feature name, that details CKEditor plugin features.
|
||||
*/
|
||||
featuresMetadata: null,
|
||||
|
||||
/**
|
||||
* Whether the button group names are currently visible.
|
||||
*/
|
||||
groupNamesVisible: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* The CKEditor configuration that is being manipulated through the UI.
|
||||
* @method
|
||||
*/
|
||||
activeEditorConfig: null,
|
||||
|
||||
/**
|
||||
* The textarea that contains the serialized representation of the active
|
||||
* CKEditor configuration.
|
||||
*/
|
||||
$textarea: null,
|
||||
|
||||
/**
|
||||
* Tracks whether the active toolbar DOM structure has been changed. When
|
||||
* true, activeEditorConfig needs to be updated, and when that is updated,
|
||||
* $textarea will also be updated.
|
||||
*/
|
||||
isDirty: false,
|
||||
|
||||
/**
|
||||
* The configuration for the hidden CKEditor instance that is used to
|
||||
* build the features metadata.
|
||||
*/
|
||||
hiddenEditorConfig: null,
|
||||
|
||||
/**
|
||||
* A hash that maps buttons to features.
|
||||
*/
|
||||
buttonsToFeatures: null,
|
||||
|
||||
/**
|
||||
* A hash, keyed by a feature name, that details CKEditor plugin features.
|
||||
*/
|
||||
featuresMetadata: null,
|
||||
|
||||
/**
|
||||
* Whether the button group names are currently visible.
|
||||
*/
|
||||
groupNamesVisible: false,
|
||||
sync() {
|
||||
// Push the settings into the textarea.
|
||||
this.get('$textarea').val(
|
||||
JSON.stringify(this.get('activeEditorConfig')),
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*/
|
||||
sync() {
|
||||
// Push the settings into the textarea.
|
||||
this.get('$textarea').val(JSON.stringify(this.get('activeEditorConfig')));
|
||||
},
|
||||
});
|
||||
}(Drupal, Backbone));
|
||||
);
|
||||
})(Drupal, Backbone);
|
||||
|
||||
@@ -13,7 +13,81 @@
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function ($, Drupal, CKEDITOR) {
|
||||
(function($, Drupal, CKEDITOR) {
|
||||
/**
|
||||
* Gets the focused widget, if of the type specific for this plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*
|
||||
* @return {?CKEDITOR.plugins.widget}
|
||||
* The focused image2 widget instance, or null.
|
||||
*/
|
||||
function getFocusedWidget(editor) {
|
||||
const widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrates the drupalimage widget with the drupallink plugin.
|
||||
*
|
||||
* Makes images linkable.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*/
|
||||
function linkCommandIntegrator(editor) {
|
||||
// Nothing to integrate with if the drupallink plugin is not loaded.
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Override default behaviour of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('exec', function(evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
// Override 'drupalunlink' only when link truly belongs to the widget. If
|
||||
// wrapped inline widget in a link, let default unlink work.
|
||||
// @see https://dev.ckeditor.com/ticket/11814
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
// Selection (which is fake) may not change if unlinked image in focused
|
||||
// widget, i.e. if captioned image. Let's refresh command state manually
|
||||
// here.
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
// Override default refresh of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('refresh', function(evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note that widget may be wrapped in a link, which
|
||||
// does not belong to that widget (#11814).
|
||||
this.setState(
|
||||
widget.data.link || widget.wrapper.getAscendant('a')
|
||||
? CKEDITOR.TRISTATE_OFF
|
||||
: CKEDITOR.TRISTATE_DISABLED,
|
||||
);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalimage', {
|
||||
requires: 'image2',
|
||||
icons: 'drupalimage',
|
||||
@@ -22,7 +96,7 @@
|
||||
beforeInit(editor) {
|
||||
// Override the image2 widget definition to require and handle the
|
||||
// additional data-entity-type and data-entity-uuid attributes.
|
||||
editor.on('widgetDefinition', (event) => {
|
||||
editor.on('widgetDefinition', event => {
|
||||
const widgetDefinition = event.data;
|
||||
if (widgetDefinition.name !== 'image') {
|
||||
return;
|
||||
@@ -69,26 +143,34 @@
|
||||
requiredContent.attributes['data-entity-type'] = '';
|
||||
requiredContent.attributes['data-entity-uuid'] = '';
|
||||
widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent);
|
||||
widgetDefinition.allowedContent.img.attributes['!data-entity-type'] = true;
|
||||
widgetDefinition.allowedContent.img.attributes['!data-entity-uuid'] = true;
|
||||
widgetDefinition.allowedContent.img.attributes[
|
||||
'!data-entity-type'
|
||||
] = true;
|
||||
widgetDefinition.allowedContent.img.attributes[
|
||||
'!data-entity-uuid'
|
||||
] = true;
|
||||
|
||||
// Override downcast(): since we only accept <img> in our upcast method,
|
||||
// the element is already correct. We only need to update the element's
|
||||
// data-entity-uuid attribute.
|
||||
widgetDefinition.downcast = function (element) {
|
||||
element.attributes['data-entity-type'] = this.data['data-entity-type'];
|
||||
element.attributes['data-entity-uuid'] = this.data['data-entity-uuid'];
|
||||
widgetDefinition.downcast = function(element) {
|
||||
element.attributes['data-entity-type'] = this.data[
|
||||
'data-entity-type'
|
||||
];
|
||||
element.attributes['data-entity-uuid'] = this.data[
|
||||
'data-entity-uuid'
|
||||
];
|
||||
};
|
||||
|
||||
// We want to upcast <img> elements to a DOM structure required by the
|
||||
// image2 widget; we only accept an <img> tag, and that <img> tag MAY
|
||||
// have a data-entity-type and a data-entity-uuid attribute.
|
||||
widgetDefinition.upcast = function (element, data) {
|
||||
widgetDefinition.upcast = function(element, data) {
|
||||
if (element.name !== 'img') {
|
||||
return;
|
||||
}
|
||||
// Don't initialize on pasted fake objects.
|
||||
else if (element.attributes['data-cke-realelement']) {
|
||||
if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,9 +193,11 @@
|
||||
// @see http://dev.ckeditor.com/ticket/13888
|
||||
// @see https://www.drupal.org/node/2268941
|
||||
const originalGetClasses = widgetDefinition.getClasses;
|
||||
widgetDefinition.getClasses = function () {
|
||||
widgetDefinition.getClasses = function() {
|
||||
const classes = originalGetClasses.call(this);
|
||||
const captionedClasses = (this.editor.config.image2_captionedClass || '').split(/\s+/);
|
||||
const captionedClasses = (
|
||||
this.editor.config.image2_captionedClass || ''
|
||||
).split(/\s+/);
|
||||
|
||||
if (captionedClasses.length && classes) {
|
||||
for (let i = 0; i < captionedClasses.length; i++) {
|
||||
@@ -141,20 +225,20 @@
|
||||
// Protected; transforms widget's data object to the format used by the
|
||||
// \Drupal\editor\Form\EditorImageDialog dialog, keeping only the data
|
||||
// listed in widgetDefinition._dataForDialog.
|
||||
widgetDefinition._dataToDialogValues = function (data) {
|
||||
widgetDefinition._dataToDialogValues = function(data) {
|
||||
const dialogValues = {};
|
||||
const map = widgetDefinition._mapDataToDialog;
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach((key) => {
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach(key => {
|
||||
dialogValues[map[key]] = data[key];
|
||||
});
|
||||
return dialogValues;
|
||||
};
|
||||
|
||||
// Protected; the inverse of _dataToDialogValues.
|
||||
widgetDefinition._dialogValuesToData = function (dialogReturnValues) {
|
||||
widgetDefinition._dialogValuesToData = function(dialogReturnValues) {
|
||||
const data = {};
|
||||
const map = widgetDefinition._mapDataToDialog;
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach((key) => {
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach(key => {
|
||||
if (dialogReturnValues.hasOwnProperty(map[key])) {
|
||||
data[key] = dialogReturnValues[map[key]];
|
||||
}
|
||||
@@ -163,8 +247,8 @@
|
||||
};
|
||||
|
||||
// Protected; creates Drupal dialog save callback.
|
||||
widgetDefinition._createDialogSaveCallback = function (editor, widget) {
|
||||
return function (dialogReturnValues) {
|
||||
widgetDefinition._createDialogSaveCallback = function(editor, widget) {
|
||||
return function(dialogReturnValues) {
|
||||
const firstEdit = !widget.ready;
|
||||
|
||||
// Dialog may have blurred the widget. Re-focus it first.
|
||||
@@ -181,7 +265,9 @@
|
||||
// Set the updated widget data, after the necessary conversions from
|
||||
// the dialog's return values.
|
||||
// Note: on widget#setData this widget instance might be destroyed.
|
||||
const data = widgetDefinition._dialogValuesToData(dialogReturnValues.attributes);
|
||||
const data = widgetDefinition._dialogValuesToData(
|
||||
dialogReturnValues.attributes,
|
||||
);
|
||||
widget.setData(data);
|
||||
|
||||
// Retrieve the widget once again. It could've been destroyed
|
||||
@@ -208,14 +294,20 @@
|
||||
};
|
||||
|
||||
const originalInit = widgetDefinition.init;
|
||||
widgetDefinition.init = function () {
|
||||
widgetDefinition.init = function() {
|
||||
originalInit.call(this);
|
||||
|
||||
// Update data.link object with attributes if the link has been
|
||||
// discovered.
|
||||
// @see plugins/image2/plugin.js/init() in CKEditor; this is similar.
|
||||
if (this.parts.link) {
|
||||
this.setData('link', CKEDITOR.plugins.image2.getLinkAttributesParser()(editor, this.parts.link));
|
||||
this.setData(
|
||||
'link',
|
||||
CKEDITOR.plugins.image2.getLinkAttributesParser()(
|
||||
editor,
|
||||
this.parts.link,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -224,14 +316,14 @@
|
||||
// to handle its editing with a Drupal-native dialog.
|
||||
// This includes also a case just after the image was created
|
||||
// and dialog should be opened for it for the first time.
|
||||
editor.widgets.on('instanceCreated', (event) => {
|
||||
editor.widgets.on('instanceCreated', event => {
|
||||
const widget = event.data;
|
||||
|
||||
if (widget.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.on('edit', (event) => {
|
||||
widget.on('edit', event => {
|
||||
// Cancel edit event to break image2's dialog binding
|
||||
// (and also to prevent automatic insertion before opening dialog).
|
||||
event.cancel();
|
||||
@@ -239,11 +331,16 @@
|
||||
// Open drupalimage dialog.
|
||||
editor.execCommand('editdrupalimage', {
|
||||
existingValues: widget.definition._dataToDialogValues(widget.data),
|
||||
saveCallback: widget.definition._createDialogSaveCallback(editor, widget),
|
||||
saveCallback: widget.definition._createDialogSaveCallback(
|
||||
editor,
|
||||
widget,
|
||||
),
|
||||
// Drupal.t() will not work inside CKEditor plugins because CKEditor
|
||||
// loads the JavaScript file instead of Drupal. Pull translated
|
||||
// strings from the plugin settings that are translated server-side.
|
||||
dialogTitle: widget.data.src ? editor.config.drupalImage_dialogTitleEdit : editor.config.drupalImage_dialogTitleAdd,
|
||||
dialogTitle: widget.data.src
|
||||
? editor.config.drupalImage_dialogTitleEdit
|
||||
: editor.config.drupalImage_dialogTitleAdd,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -251,7 +348,8 @@
|
||||
// Register the "editdrupalimage" command, which essentially just replaces
|
||||
// the "image" command's CKEditor dialog with a Drupal-native dialog.
|
||||
editor.addCommand('editdrupalimage', {
|
||||
allowedContent: 'img[alt,!src,width,height,!data-entity-type,!data-entity-uuid]',
|
||||
allowedContent:
|
||||
'img[alt,!src,width,height,!data-entity-type,!data-entity-uuid]',
|
||||
requiredContent: 'img[alt,src,data-entity-type,data-entity-uuid]',
|
||||
modes: { wysiwyg: 1 },
|
||||
canUndo: true,
|
||||
@@ -260,7 +358,13 @@
|
||||
title: data.dialogTitle,
|
||||
dialogClass: 'editor-image-dialog',
|
||||
};
|
||||
Drupal.ckeditor.openDialog(editor, Drupal.url(`editor/dialog/image/${editor.config.drupal.format}`), data.existingValues, data.saveCallback, dialogSettings);
|
||||
Drupal.ckeditor.openDialog(
|
||||
editor,
|
||||
Drupal.url(`editor/dialog/image/${editor.config.drupal.format}`),
|
||||
data.existingValues,
|
||||
data.saveCallback,
|
||||
dialogSettings,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -277,91 +381,19 @@
|
||||
afterInit(editor) {
|
||||
linkCommandIntegrator(editor);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
// Override image2's integration with the official CKEditor link plugin:
|
||||
// integrate with the drupallink plugin instead.
|
||||
CKEDITOR.plugins.image2.getLinkAttributesParser = function () {
|
||||
CKEDITOR.plugins.image2.getLinkAttributesParser = function() {
|
||||
return CKEDITOR.plugins.drupallink.parseLinkAttributes;
|
||||
};
|
||||
CKEDITOR.plugins.image2.getLinkAttributesGetter = function () {
|
||||
CKEDITOR.plugins.image2.getLinkAttributesGetter = function() {
|
||||
return CKEDITOR.plugins.drupallink.getLinkAttributes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Integrates the drupalimage widget with the drupallink plugin.
|
||||
*
|
||||
* Makes images linkable.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*/
|
||||
function linkCommandIntegrator(editor) {
|
||||
// Nothing to integrate with if the drupallink plugin is not loaded.
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Override default behaviour of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
// Override 'drupalunlink' only when link truly belongs to the widget. If
|
||||
// wrapped inline widget in a link, let default unlink work.
|
||||
// @see https://dev.ckeditor.com/ticket/11814
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
// Selection (which is fake) may not change if unlinked image in focused
|
||||
// widget, i.e. if captioned image. Let's refresh command state manually
|
||||
// here.
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
// Override default refresh of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note that widget may be wrapped in a link, which
|
||||
// does not belong to that widget (#11814).
|
||||
this.setState(widget.data.link || widget.wrapper.getAscendant('a') ?
|
||||
CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the focused widget, if of the type specific for this plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*
|
||||
* @return {?CKEDITOR.plugins.widget}
|
||||
* The focused image2 widget instance, or null.
|
||||
*/
|
||||
function getFocusedWidget(editor) {
|
||||
const widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expose an API for other plugins to interact with drupalimage widgets.
|
||||
CKEDITOR.plugins.drupalimage = {
|
||||
getFocusedWidget,
|
||||
};
|
||||
}(jQuery, Drupal, CKEDITOR));
|
||||
})(jQuery, Drupal, CKEDITOR);
|
||||
|
||||
@@ -6,6 +6,48 @@
|
||||
**/
|
||||
|
||||
(function ($, Drupal, CKEDITOR) {
|
||||
function getFocusedWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function linkCommandIntegrator(editor) {
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(widget.data.link || widget.wrapper.getAscendant('a') ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalimage', {
|
||||
requires: 'image2',
|
||||
icons: 'drupalimage',
|
||||
@@ -53,9 +95,11 @@
|
||||
widgetDefinition.upcast = function (element, data) {
|
||||
if (element.name !== 'img') {
|
||||
return;
|
||||
} else if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
data['data-entity-type'] = element.attributes['data-entity-type'];
|
||||
|
||||
@@ -204,48 +248,6 @@
|
||||
return CKEDITOR.plugins.drupallink.getLinkAttributes;
|
||||
};
|
||||
|
||||
function linkCommandIntegrator(editor) {
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget || !widget.parts.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(widget.data.link || widget.wrapper.getAscendant('a') ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
function getFocusedWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'image') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupalimage = {
|
||||
getFocusedWidget: getFocusedWidget
|
||||
};
|
||||
|
||||
@@ -10,260 +10,7 @@
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function (CKEDITOR) {
|
||||
CKEDITOR.plugins.add('drupalimagecaption', {
|
||||
requires: 'drupalimage',
|
||||
|
||||
beforeInit(editor) {
|
||||
// Disable default placeholder text that comes with CKEditor's image2
|
||||
// plugin: it has an inferior UX (it requires the user to manually delete
|
||||
// the place holder text).
|
||||
editor.lang.image2.captionPlaceholder = '';
|
||||
|
||||
// Drupal.t() will not work inside CKEditor plugins because CKEditor loads
|
||||
// the JavaScript file instead of Drupal. Pull translated strings from the
|
||||
// plugin settings that are translated server-side.
|
||||
const placeholderText = editor.config.drupalImageCaption_captionPlaceholderText;
|
||||
|
||||
// Override the image2 widget definition to handle the additional
|
||||
// data-align and data-caption attributes.
|
||||
editor.on('widgetDefinition', (event) => {
|
||||
const widgetDefinition = event.data;
|
||||
if (widgetDefinition.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only perform the downcasting/upcasting for to the enabled filters.
|
||||
const captionFilterEnabled = editor.config.drupalImageCaption_captionFilterEnabled;
|
||||
const alignFilterEnabled = editor.config.drupalImageCaption_alignFilterEnabled;
|
||||
|
||||
// Override default features definitions for drupalimagecaption.
|
||||
CKEDITOR.tools.extend(widgetDefinition.features, {
|
||||
caption: {
|
||||
requiredContent: 'img[data-caption]',
|
||||
},
|
||||
align: {
|
||||
requiredContent: 'img[data-align]',
|
||||
},
|
||||
}, true);
|
||||
|
||||
// Extend requiredContent & allowedContent.
|
||||
// CKEDITOR.style is an immutable object: we cannot modify its
|
||||
// definition to extend requiredContent. Hence we get the definition,
|
||||
// modify it, and pass it to a new CKEDITOR.style instance.
|
||||
const requiredContent = widgetDefinition.requiredContent.getDefinition();
|
||||
requiredContent.attributes['data-align'] = '';
|
||||
requiredContent.attributes['data-caption'] = '';
|
||||
widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent);
|
||||
widgetDefinition.allowedContent.img.attributes['!data-align'] = true;
|
||||
widgetDefinition.allowedContent.img.attributes['!data-caption'] = true;
|
||||
|
||||
// Override allowedContent setting for the 'caption' nested editable.
|
||||
// This must match what caption_filter enforces.
|
||||
// @see \Drupal\filter\Plugin\Filter\FilterCaption::process()
|
||||
// @see \Drupal\Component\Utility\Xss::filter()
|
||||
widgetDefinition.editables.caption.allowedContent = 'a[!href]; em strong cite code br';
|
||||
|
||||
// Override downcast(): ensure we *only* output <img>, but also ensure
|
||||
// we include the data-entity-type, data-entity-uuid, data-align and
|
||||
// data-caption attributes.
|
||||
const originalDowncast = widgetDefinition.downcast;
|
||||
widgetDefinition.downcast = function (element) {
|
||||
const img = findElementByName(element, 'img');
|
||||
originalDowncast.call(this, img);
|
||||
|
||||
const caption = this.editables.caption;
|
||||
const captionHtml = caption && caption.getData();
|
||||
const attrs = img.attributes;
|
||||
|
||||
if (captionFilterEnabled) {
|
||||
// If image contains a non-empty caption, serialize caption to the
|
||||
// data-caption attribute.
|
||||
if (captionHtml) {
|
||||
attrs['data-caption'] = captionHtml;
|
||||
}
|
||||
}
|
||||
if (alignFilterEnabled) {
|
||||
if (this.data.align !== 'none') {
|
||||
attrs['data-align'] = this.data.align;
|
||||
}
|
||||
}
|
||||
|
||||
// If img is wrapped with a link, we want to return that link.
|
||||
if (img.parent.name === 'a') {
|
||||
return img.parent;
|
||||
}
|
||||
|
||||
return img;
|
||||
};
|
||||
|
||||
// We want to upcast <img> elements to a DOM structure required by the
|
||||
// image2 widget. Depending on a case it may be:
|
||||
// - just an <img> tag (non-captioned, not-centered image),
|
||||
// - <img> tag in a paragraph (non-captioned, centered image),
|
||||
// - <figure> tag (captioned image).
|
||||
// We take the same attributes into account as downcast() does.
|
||||
const originalUpcast = widgetDefinition.upcast;
|
||||
widgetDefinition.upcast = function (element, data) {
|
||||
if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) {
|
||||
return;
|
||||
}
|
||||
// Don't initialize on pasted fake objects.
|
||||
else if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
element = originalUpcast.call(this, element, data);
|
||||
const attrs = element.attributes;
|
||||
|
||||
if (element.parent.name === 'a') {
|
||||
element = element.parent;
|
||||
}
|
||||
|
||||
let retElement = element;
|
||||
let caption;
|
||||
|
||||
// We won't need the attributes during editing: we'll use widget.data
|
||||
// to store them (except the caption, which is stored in the DOM).
|
||||
if (captionFilterEnabled) {
|
||||
caption = attrs['data-caption'];
|
||||
delete attrs['data-caption'];
|
||||
}
|
||||
if (alignFilterEnabled) {
|
||||
data.align = attrs['data-align'];
|
||||
delete attrs['data-align'];
|
||||
}
|
||||
data['data-entity-type'] = attrs['data-entity-type'];
|
||||
delete attrs['data-entity-type'];
|
||||
data['data-entity-uuid'] = attrs['data-entity-uuid'];
|
||||
delete attrs['data-entity-uuid'];
|
||||
|
||||
if (captionFilterEnabled) {
|
||||
// Unwrap from <p> wrapper created by HTML parser for a captioned
|
||||
// image. The captioned image will be transformed to <figure>, so we
|
||||
// don't want the <p> anymore.
|
||||
if (element.parent.name === 'p' && caption) {
|
||||
let index = element.getIndex();
|
||||
const splitBefore = index > 0;
|
||||
const splitAfter = index + 1 < element.parent.children.length;
|
||||
|
||||
if (splitBefore) {
|
||||
element.parent.split(index);
|
||||
}
|
||||
index = element.getIndex();
|
||||
if (splitAfter) {
|
||||
element.parent.split(index + 1);
|
||||
}
|
||||
|
||||
element.parent.replaceWith(element);
|
||||
retElement = element;
|
||||
}
|
||||
|
||||
// If this image has a caption, create a full <figure> structure.
|
||||
if (caption) {
|
||||
const figure = new CKEDITOR.htmlParser.element('figure');
|
||||
caption = new CKEDITOR.htmlParser.fragment.fromHtml(caption, 'figcaption');
|
||||
|
||||
// Use Drupal's data-placeholder attribute to insert a CSS-based,
|
||||
// translation-ready placeholder for empty captions. Note that it
|
||||
// also must to be done for new instances (see
|
||||
// widgetDefinition._createDialogSaveCallback).
|
||||
caption.attributes['data-placeholder'] = placeholderText;
|
||||
|
||||
element.replaceWith(figure);
|
||||
figure.add(element);
|
||||
figure.add(caption);
|
||||
figure.attributes.class = editor.config.image2_captionedClass;
|
||||
retElement = figure;
|
||||
}
|
||||
}
|
||||
|
||||
if (alignFilterEnabled) {
|
||||
// If this image doesn't have a caption (or the caption filter is
|
||||
// disabled), but it is centered, make sure that it's wrapped with
|
||||
// <p>, which will become a part of the widget.
|
||||
if (data.align === 'center' && (!captionFilterEnabled || !caption)) {
|
||||
const p = new CKEDITOR.htmlParser.element('p');
|
||||
element.replaceWith(p);
|
||||
p.add(element);
|
||||
// Apply the class for centered images.
|
||||
p.addClass(editor.config.image2_alignClasses[1]);
|
||||
retElement = p;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the upcasted element (<img>, <figure> or <p>).
|
||||
return retElement;
|
||||
};
|
||||
|
||||
// Protected; keys of the widget data to be sent to the Drupal dialog.
|
||||
// Append to the values defined by the drupalimage plugin.
|
||||
// @see core/modules/ckeditor/js/plugins/drupalimage/plugin.js
|
||||
CKEDITOR.tools.extend(widgetDefinition._mapDataToDialog, {
|
||||
align: 'data-align',
|
||||
'data-caption': 'data-caption',
|
||||
hasCaption: 'hasCaption',
|
||||
});
|
||||
|
||||
// Override Drupal dialog save callback.
|
||||
const originalCreateDialogSaveCallback = widgetDefinition._createDialogSaveCallback;
|
||||
widgetDefinition._createDialogSaveCallback = function (editor, widget) {
|
||||
const saveCallback = originalCreateDialogSaveCallback.call(this, editor, widget);
|
||||
|
||||
return function (dialogReturnValues) {
|
||||
// Ensure hasCaption is a boolean. image2 assumes it always works
|
||||
// with booleans; if this is not the case, then
|
||||
// CKEDITOR.plugins.image2.stateShifter() will incorrectly mark
|
||||
// widget.data.hasCaption as "changed" (e.g. when hasCaption === 0
|
||||
// instead of hasCaption === false). This causes image2's "state
|
||||
// shifter" to enter the wrong branch of the algorithm and blow up.
|
||||
dialogReturnValues.attributes.hasCaption = !!dialogReturnValues.attributes.hasCaption;
|
||||
|
||||
const actualWidget = saveCallback(dialogReturnValues);
|
||||
|
||||
// By default, the template of captioned widget has no
|
||||
// data-placeholder attribute. Note that it also must be done when
|
||||
// upcasting existing elements (see widgetDefinition.upcast).
|
||||
if (dialogReturnValues.attributes.hasCaption) {
|
||||
actualWidget.editables.caption.setAttribute('data-placeholder', placeholderText);
|
||||
|
||||
// Some browsers will add a <br> tag to a newly created DOM
|
||||
// element with no content. Remove this <br> if it is the only
|
||||
// thing in the caption. Our placeholder support requires the
|
||||
// element be entirely empty. See filter-caption.css.
|
||||
const captionElement = actualWidget.editables.caption.$;
|
||||
if (captionElement.childNodes.length === 1 && captionElement.childNodes.item(0).nodeName === 'BR') {
|
||||
captionElement.removeChild(captionElement.childNodes.item(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
// Low priority to ensure drupalimage's event handler runs first.
|
||||
}, null, null, 20);
|
||||
},
|
||||
|
||||
afterInit(editor) {
|
||||
const disableButtonIfOnWidget = function (evt) {
|
||||
const widget = editor.widgets.focused;
|
||||
if (widget && widget.name === 'image') {
|
||||
this.setState(CKEDITOR.TRISTATE_DISABLED);
|
||||
evt.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
// Disable alignment buttons if the align filter is not enabled.
|
||||
if (editor.plugins.justify && !editor.config.drupalImageCaption_alignFilterEnabled) {
|
||||
let cmd;
|
||||
const commands = ['justifyleft', 'justifycenter', 'justifyright', 'justifyblock'];
|
||||
for (let n = 0; n < commands.length; n++) {
|
||||
cmd = editor.getCommand(commands[n]);
|
||||
cmd.contextSensitive = 1;
|
||||
cmd.on('refresh', disableButtonIfOnWidget, null, null, 4);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
(function(CKEDITOR) {
|
||||
/**
|
||||
* Finds an element by its name.
|
||||
*
|
||||
@@ -284,7 +31,7 @@
|
||||
}
|
||||
|
||||
let found = null;
|
||||
element.forEach((el) => {
|
||||
element.forEach(el => {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
// Stop here.
|
||||
@@ -293,4 +40,308 @@
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
}(CKEDITOR));
|
||||
|
||||
CKEDITOR.plugins.add('drupalimagecaption', {
|
||||
requires: 'drupalimage',
|
||||
|
||||
beforeInit(editor) {
|
||||
// Disable default placeholder text that comes with CKEditor's image2
|
||||
// plugin: it has an inferior UX (it requires the user to manually delete
|
||||
// the place holder text).
|
||||
editor.lang.image2.captionPlaceholder = '';
|
||||
|
||||
// Drupal.t() will not work inside CKEditor plugins because CKEditor loads
|
||||
// the JavaScript file instead of Drupal. Pull translated strings from the
|
||||
// plugin settings that are translated server-side.
|
||||
const placeholderText =
|
||||
editor.config.drupalImageCaption_captionPlaceholderText;
|
||||
|
||||
// Override the image2 widget definition to handle the additional
|
||||
// data-align and data-caption attributes.
|
||||
editor.on(
|
||||
'widgetDefinition',
|
||||
event => {
|
||||
const widgetDefinition = event.data;
|
||||
if (widgetDefinition.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only perform the downcasting/upcasting for to the enabled filters.
|
||||
const captionFilterEnabled =
|
||||
editor.config.drupalImageCaption_captionFilterEnabled;
|
||||
const alignFilterEnabled =
|
||||
editor.config.drupalImageCaption_alignFilterEnabled;
|
||||
|
||||
// Override default features definitions for drupalimagecaption.
|
||||
CKEDITOR.tools.extend(
|
||||
widgetDefinition.features,
|
||||
{
|
||||
caption: {
|
||||
requiredContent: 'img[data-caption]',
|
||||
},
|
||||
align: {
|
||||
requiredContent: 'img[data-align]',
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// Extend requiredContent & allowedContent.
|
||||
// CKEDITOR.style is an immutable object: we cannot modify its
|
||||
// definition to extend requiredContent. Hence we get the definition,
|
||||
// modify it, and pass it to a new CKEDITOR.style instance.
|
||||
const requiredContent = widgetDefinition.requiredContent.getDefinition();
|
||||
requiredContent.attributes['data-align'] = '';
|
||||
requiredContent.attributes['data-caption'] = '';
|
||||
widgetDefinition.requiredContent = new CKEDITOR.style(
|
||||
requiredContent,
|
||||
);
|
||||
widgetDefinition.allowedContent.img.attributes['!data-align'] = true;
|
||||
widgetDefinition.allowedContent.img.attributes[
|
||||
'!data-caption'
|
||||
] = true;
|
||||
|
||||
// Override allowedContent setting for the 'caption' nested editable.
|
||||
// This must match what caption_filter enforces.
|
||||
// @see \Drupal\filter\Plugin\Filter\FilterCaption::process()
|
||||
// @see \Drupal\Component\Utility\Xss::filter()
|
||||
widgetDefinition.editables.caption.allowedContent =
|
||||
'a[!href]; em strong cite code br';
|
||||
|
||||
// Override downcast(): ensure we *only* output <img>, but also ensure
|
||||
// we include the data-entity-type, data-entity-uuid, data-align and
|
||||
// data-caption attributes.
|
||||
const originalDowncast = widgetDefinition.downcast;
|
||||
widgetDefinition.downcast = function(element) {
|
||||
const img = findElementByName(element, 'img');
|
||||
originalDowncast.call(this, img);
|
||||
|
||||
const caption = this.editables.caption;
|
||||
const captionHtml = caption && caption.getData();
|
||||
const attrs = img.attributes;
|
||||
|
||||
if (captionFilterEnabled) {
|
||||
// If image contains a non-empty caption, serialize caption to the
|
||||
// data-caption attribute.
|
||||
if (captionHtml) {
|
||||
attrs['data-caption'] = captionHtml;
|
||||
}
|
||||
}
|
||||
if (alignFilterEnabled) {
|
||||
if (this.data.align !== 'none') {
|
||||
attrs['data-align'] = this.data.align;
|
||||
}
|
||||
}
|
||||
|
||||
// If img is wrapped with a link, we want to return that link.
|
||||
if (img.parent.name === 'a') {
|
||||
return img.parent;
|
||||
}
|
||||
|
||||
return img;
|
||||
};
|
||||
|
||||
// We want to upcast <img> elements to a DOM structure required by the
|
||||
// image2 widget. Depending on a case it may be:
|
||||
// - just an <img> tag (non-captioned, not-centered image),
|
||||
// - <img> tag in a paragraph (non-captioned, centered image),
|
||||
// - <figure> tag (captioned image).
|
||||
// We take the same attributes into account as downcast() does.
|
||||
const originalUpcast = widgetDefinition.upcast;
|
||||
widgetDefinition.upcast = function(element, data) {
|
||||
if (
|
||||
element.name !== 'img' ||
|
||||
!element.attributes['data-entity-type'] ||
|
||||
!element.attributes['data-entity-uuid']
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Don't initialize on pasted fake objects.
|
||||
if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
element = originalUpcast.call(this, element, data);
|
||||
const attrs = element.attributes;
|
||||
|
||||
if (element.parent.name === 'a') {
|
||||
element = element.parent;
|
||||
}
|
||||
|
||||
let retElement = element;
|
||||
let caption;
|
||||
|
||||
// We won't need the attributes during editing: we'll use widget.data
|
||||
// to store them (except the caption, which is stored in the DOM).
|
||||
if (captionFilterEnabled) {
|
||||
caption = attrs['data-caption'];
|
||||
delete attrs['data-caption'];
|
||||
}
|
||||
if (alignFilterEnabled) {
|
||||
data.align = attrs['data-align'];
|
||||
delete attrs['data-align'];
|
||||
}
|
||||
data['data-entity-type'] = attrs['data-entity-type'];
|
||||
delete attrs['data-entity-type'];
|
||||
data['data-entity-uuid'] = attrs['data-entity-uuid'];
|
||||
delete attrs['data-entity-uuid'];
|
||||
|
||||
if (captionFilterEnabled) {
|
||||
// Unwrap from <p> wrapper created by HTML parser for a captioned
|
||||
// image. The captioned image will be transformed to <figure>, so we
|
||||
// don't want the <p> anymore.
|
||||
if (element.parent.name === 'p' && caption) {
|
||||
let index = element.getIndex();
|
||||
const splitBefore = index > 0;
|
||||
const splitAfter = index + 1 < element.parent.children.length;
|
||||
|
||||
if (splitBefore) {
|
||||
element.parent.split(index);
|
||||
}
|
||||
index = element.getIndex();
|
||||
if (splitAfter) {
|
||||
element.parent.split(index + 1);
|
||||
}
|
||||
|
||||
element.parent.replaceWith(element);
|
||||
retElement = element;
|
||||
}
|
||||
|
||||
// If this image has a caption, create a full <figure> structure.
|
||||
if (caption) {
|
||||
const figure = new CKEDITOR.htmlParser.element('figure');
|
||||
caption = new CKEDITOR.htmlParser.fragment.fromHtml(
|
||||
caption,
|
||||
'figcaption',
|
||||
);
|
||||
|
||||
// Use Drupal's data-placeholder attribute to insert a CSS-based,
|
||||
// translation-ready placeholder for empty captions. Note that it
|
||||
// also must to be done for new instances (see
|
||||
// widgetDefinition._createDialogSaveCallback).
|
||||
caption.attributes['data-placeholder'] = placeholderText;
|
||||
|
||||
element.replaceWith(figure);
|
||||
figure.add(element);
|
||||
figure.add(caption);
|
||||
figure.attributes.class = editor.config.image2_captionedClass;
|
||||
retElement = figure;
|
||||
}
|
||||
}
|
||||
|
||||
if (alignFilterEnabled) {
|
||||
// If this image doesn't have a caption (or the caption filter is
|
||||
// disabled), but it is centered, make sure that it's wrapped with
|
||||
// <p>, which will become a part of the widget.
|
||||
if (
|
||||
data.align === 'center' &&
|
||||
(!captionFilterEnabled || !caption)
|
||||
) {
|
||||
const p = new CKEDITOR.htmlParser.element('p');
|
||||
element.replaceWith(p);
|
||||
p.add(element);
|
||||
// Apply the class for centered images.
|
||||
p.addClass(editor.config.image2_alignClasses[1]);
|
||||
retElement = p;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the upcasted element (<img>, <figure> or <p>).
|
||||
return retElement;
|
||||
};
|
||||
|
||||
// Protected; keys of the widget data to be sent to the Drupal dialog.
|
||||
// Append to the values defined by the drupalimage plugin.
|
||||
// @see core/modules/ckeditor/js/plugins/drupalimage/plugin.js
|
||||
CKEDITOR.tools.extend(widgetDefinition._mapDataToDialog, {
|
||||
align: 'data-align',
|
||||
'data-caption': 'data-caption',
|
||||
hasCaption: 'hasCaption',
|
||||
});
|
||||
|
||||
// Override Drupal dialog save callback.
|
||||
const originalCreateDialogSaveCallback =
|
||||
widgetDefinition._createDialogSaveCallback;
|
||||
widgetDefinition._createDialogSaveCallback = function(
|
||||
editor,
|
||||
widget,
|
||||
) {
|
||||
const saveCallback = originalCreateDialogSaveCallback.call(
|
||||
this,
|
||||
editor,
|
||||
widget,
|
||||
);
|
||||
|
||||
return function(dialogReturnValues) {
|
||||
// Ensure hasCaption is a boolean. image2 assumes it always works
|
||||
// with booleans; if this is not the case, then
|
||||
// CKEDITOR.plugins.image2.stateShifter() will incorrectly mark
|
||||
// widget.data.hasCaption as "changed" (e.g. when hasCaption === 0
|
||||
// instead of hasCaption === false). This causes image2's "state
|
||||
// shifter" to enter the wrong branch of the algorithm and blow up.
|
||||
dialogReturnValues.attributes.hasCaption = !!dialogReturnValues
|
||||
.attributes.hasCaption;
|
||||
|
||||
const actualWidget = saveCallback(dialogReturnValues);
|
||||
|
||||
// By default, the template of captioned widget has no
|
||||
// data-placeholder attribute. Note that it also must be done when
|
||||
// upcasting existing elements (see widgetDefinition.upcast).
|
||||
if (dialogReturnValues.attributes.hasCaption) {
|
||||
actualWidget.editables.caption.setAttribute(
|
||||
'data-placeholder',
|
||||
placeholderText,
|
||||
);
|
||||
|
||||
// Some browsers will add a <br> tag to a newly created DOM
|
||||
// element with no content. Remove this <br> if it is the only
|
||||
// thing in the caption. Our placeholder support requires the
|
||||
// element be entirely empty. See filter-caption.css.
|
||||
const captionElement = actualWidget.editables.caption.$;
|
||||
if (
|
||||
captionElement.childNodes.length === 1 &&
|
||||
captionElement.childNodes.item(0).nodeName === 'BR'
|
||||
) {
|
||||
captionElement.removeChild(captionElement.childNodes.item(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
// Low priority to ensure drupalimage's event handler runs first.
|
||||
},
|
||||
null,
|
||||
null,
|
||||
20,
|
||||
);
|
||||
},
|
||||
|
||||
afterInit(editor) {
|
||||
const disableButtonIfOnWidget = function(evt) {
|
||||
const widget = editor.widgets.focused;
|
||||
if (widget && widget.name === 'image') {
|
||||
this.setState(CKEDITOR.TRISTATE_DISABLED);
|
||||
evt.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
// Disable alignment buttons if the align filter is not enabled.
|
||||
if (
|
||||
editor.plugins.justify &&
|
||||
!editor.config.drupalImageCaption_alignFilterEnabled
|
||||
) {
|
||||
let cmd;
|
||||
const commands = [
|
||||
'justifyleft',
|
||||
'justifycenter',
|
||||
'justifyright',
|
||||
'justifyblock',
|
||||
];
|
||||
for (let n = 0; n < commands.length; n++) {
|
||||
cmd = editor.getCommand(commands[n]);
|
||||
cmd.contextSensitive = 1;
|
||||
cmd.on('refresh', disableButtonIfOnWidget, null, null, 4);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
})(CKEDITOR);
|
||||
|
||||
@@ -6,6 +6,22 @@
|
||||
**/
|
||||
|
||||
(function (CKEDITOR) {
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
var found = null;
|
||||
element.forEach(function (el) {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalimagecaption', {
|
||||
requires: 'drupalimage',
|
||||
|
||||
@@ -72,9 +88,11 @@
|
||||
widgetDefinition.upcast = function (element, data) {
|
||||
if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) {
|
||||
return;
|
||||
} else if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
element = originalUpcast.call(this, element, data);
|
||||
var attrs = element.attributes;
|
||||
@@ -192,20 +210,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
var found = null;
|
||||
element.forEach(function (el) {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
})(CKEDITOR);
|
||||
@@ -5,14 +5,18 @@
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, CKEDITOR) {
|
||||
(function($, Drupal, drupalSettings, CKEDITOR) {
|
||||
function parseAttributes(editor, element) {
|
||||
const parsedAttributes = {};
|
||||
|
||||
const domElement = element.$;
|
||||
let attribute;
|
||||
let attributeName;
|
||||
for (let attrIndex = 0; attrIndex < domElement.attributes.length; attrIndex++) {
|
||||
for (
|
||||
let attrIndex = 0;
|
||||
attrIndex < domElement.attributes.length;
|
||||
attrIndex++
|
||||
) {
|
||||
attribute = domElement.attributes.item(attrIndex);
|
||||
attributeName = attribute.nodeName.toLowerCase();
|
||||
// Ignore data-cke-* attributes; they're CKEditor internals.
|
||||
@@ -21,12 +25,15 @@
|
||||
}
|
||||
// Store the value for this attribute, unless there's a data-cke-saved-
|
||||
// alternative for it, which will contain the quirk-free, original value.
|
||||
parsedAttributes[attributeName] = element.data(`cke-saved-${attributeName}`) || attribute.nodeValue;
|
||||
parsedAttributes[attributeName] =
|
||||
element.data(`cke-saved-${attributeName}`) || attribute.nodeValue;
|
||||
}
|
||||
|
||||
// Remove any cke_* classes.
|
||||
if (parsedAttributes.class) {
|
||||
parsedAttributes.class = CKEDITOR.tools.trim(parsedAttributes.class.replace(/cke_\S+/, ''));
|
||||
parsedAttributes.class = CKEDITOR.tools.trim(
|
||||
parsedAttributes.class.replace(/cke_\S+/, ''),
|
||||
);
|
||||
}
|
||||
|
||||
return parsedAttributes;
|
||||
@@ -34,7 +41,7 @@
|
||||
|
||||
function getAttributes(editor, data) {
|
||||
const set = {};
|
||||
Object.keys(data || {}).forEach((attributeName) => {
|
||||
Object.keys(data || {}).forEach(attributeName => {
|
||||
set[attributeName] = data[attributeName];
|
||||
});
|
||||
|
||||
@@ -44,7 +51,7 @@
|
||||
|
||||
// Remove all attributes which are not currently set.
|
||||
const removed = {};
|
||||
Object.keys(set).forEach((s) => {
|
||||
Object.keys(set).forEach(s => {
|
||||
delete removed[s];
|
||||
});
|
||||
|
||||
@@ -54,6 +61,42 @@
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the surrounding link element of current selection.
|
||||
*
|
||||
* The following selection will all return the link element.
|
||||
*
|
||||
* @example
|
||||
* <a href="#">li^nk</a>
|
||||
* <a href="#">[link]</a>
|
||||
* text[<a href="#">link]</a>
|
||||
* <a href="#">li[nk</a>]
|
||||
* [<b><a href="#">li]nk</a></b>]
|
||||
* [<a href="#"><b>li]nk</b></a>
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* The CKEditor editor object
|
||||
*
|
||||
* @return {?HTMLElement}
|
||||
* The selected link element, or null.
|
||||
*
|
||||
*/
|
||||
function getSelectedLink(editor) {
|
||||
const selection = editor.getSelection();
|
||||
const selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
const range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupallink', {
|
||||
icons: 'drupallink,drupalunlink',
|
||||
hidpi: true,
|
||||
@@ -79,7 +122,8 @@
|
||||
canUndo: true,
|
||||
exec(editor) {
|
||||
const drupalImageUtils = CKEDITOR.plugins.drupalimage;
|
||||
const focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
|
||||
const focusedImageWidget =
|
||||
drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
|
||||
let linkElement = getSelectedLink(editor);
|
||||
|
||||
// Set existing values based on selected element.
|
||||
@@ -94,11 +138,17 @@
|
||||
}
|
||||
|
||||
// Prepare a save callback to be used upon saving the dialog.
|
||||
const saveCallback = function (returnValues) {
|
||||
const saveCallback = function(returnValues) {
|
||||
// If an image widget is focused, we're not editing an independent
|
||||
// link, but we're wrapping an image widget in a link.
|
||||
if (focusedImageWidget) {
|
||||
focusedImageWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedImageWidget.data.link));
|
||||
focusedImageWidget.setData(
|
||||
'link',
|
||||
CKEDITOR.tools.extend(
|
||||
returnValues.attributes,
|
||||
focusedImageWidget.data.link,
|
||||
),
|
||||
);
|
||||
editor.fire('saveSnapshot');
|
||||
return;
|
||||
}
|
||||
@@ -113,13 +163,19 @@
|
||||
// Use link URL as text with a collapsed cursor.
|
||||
if (range.collapsed) {
|
||||
// Shorten mailto URLs to just the email address.
|
||||
const text = new CKEDITOR.dom.text(returnValues.attributes.href.replace(/^mailto:/, ''), editor.document);
|
||||
const text = new CKEDITOR.dom.text(
|
||||
returnValues.attributes.href.replace(/^mailto:/, ''),
|
||||
editor.document,
|
||||
);
|
||||
range.insertNode(text);
|
||||
range.selectNodeContents(text);
|
||||
}
|
||||
|
||||
// Create the new link by applying a style to the new text.
|
||||
const style = new CKEDITOR.style({ element: 'a', attributes: returnValues.attributes });
|
||||
const style = new CKEDITOR.style({
|
||||
element: 'a',
|
||||
attributes: returnValues.attributes,
|
||||
});
|
||||
style.type = CKEDITOR.STYLE_INLINE;
|
||||
style.applyToRange(range);
|
||||
range.select();
|
||||
@@ -129,7 +185,7 @@
|
||||
}
|
||||
// Update the link properties.
|
||||
else if (linkElement) {
|
||||
Object.keys(returnValues.attributes || {}).forEach((attrName) => {
|
||||
Object.keys(returnValues.attributes || {}).forEach(attrName => {
|
||||
// Update the property if a value is specified.
|
||||
if (returnValues.attributes[attrName].length > 0) {
|
||||
const value = returnValues.attributes[attrName];
|
||||
@@ -150,12 +206,20 @@
|
||||
// loads the JavaScript file instead of Drupal. Pull translated
|
||||
// strings from the plugin settings that are translated server-side.
|
||||
const dialogSettings = {
|
||||
title: linkElement ? editor.config.drupalLink_dialogTitleEdit : editor.config.drupalLink_dialogTitleAdd,
|
||||
title: linkElement
|
||||
? editor.config.drupalLink_dialogTitleEdit
|
||||
: editor.config.drupalLink_dialogTitleAdd,
|
||||
dialogClass: 'editor-link-dialog',
|
||||
};
|
||||
|
||||
// Open the dialog for the edit form.
|
||||
Drupal.ckeditor.openDialog(editor, Drupal.url(`editor/dialog/link/${editor.config.drupal.format}`), existingValues, saveCallback, dialogSettings);
|
||||
Drupal.ckeditor.openDialog(
|
||||
editor,
|
||||
Drupal.url(`editor/dialog/link/${editor.config.drupal.format}`),
|
||||
existingValues,
|
||||
saveCallback,
|
||||
dialogSettings,
|
||||
);
|
||||
},
|
||||
});
|
||||
editor.addCommand('drupalunlink', {
|
||||
@@ -168,15 +232,24 @@
|
||||
},
|
||||
}),
|
||||
exec(editor) {
|
||||
const style = new CKEDITOR.style({ element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 });
|
||||
const style = new CKEDITOR.style({
|
||||
element: 'a',
|
||||
type: CKEDITOR.STYLE_INLINE,
|
||||
alwaysRemoveElement: 1,
|
||||
});
|
||||
editor.removeStyle(style);
|
||||
},
|
||||
refresh(editor, path) {
|
||||
const element = path.lastElement && path.lastElement.getAscendant('a', true);
|
||||
if (element && element.getName() === 'a' && element.getAttribute('href') && element.getChildCount()) {
|
||||
const element =
|
||||
path.lastElement && path.lastElement.getAscendant('a', true);
|
||||
if (
|
||||
element &&
|
||||
element.getName() === 'a' &&
|
||||
element.getAttribute('href') &&
|
||||
element.getChildCount()
|
||||
) {
|
||||
this.setState(CKEDITOR.TRISTATE_OFF);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.setState(CKEDITOR.TRISTATE_DISABLED);
|
||||
}
|
||||
},
|
||||
@@ -197,7 +270,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
editor.on('doubleclick', (evt) => {
|
||||
editor.on('doubleclick', evt => {
|
||||
const element = getSelectedLink(editor) || evt.data.element;
|
||||
|
||||
if (!element.isReadOnly()) {
|
||||
@@ -240,7 +313,10 @@
|
||||
|
||||
let menu = {};
|
||||
if (anchor.getAttribute('href') && anchor.getChildCount()) {
|
||||
menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF };
|
||||
menu = {
|
||||
link: CKEDITOR.TRISTATE_OFF,
|
||||
unlink: CKEDITOR.TRISTATE_OFF,
|
||||
};
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
@@ -248,42 +324,6 @@
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the surrounding link element of current selection.
|
||||
*
|
||||
* The following selection will all return the link element.
|
||||
*
|
||||
* @example
|
||||
* <a href="#">li^nk</a>
|
||||
* <a href="#">[link]</a>
|
||||
* text[<a href="#">link]</a>
|
||||
* <a href="#">li[nk</a>]
|
||||
* [<b><a href="#">li]nk</a></b>]
|
||||
* [<a href="#"><b>li]nk</b></a>
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* The CKEditor editor object
|
||||
*
|
||||
* @return {?HTMLElement}
|
||||
* The selected link element, or null.
|
||||
*
|
||||
*/
|
||||
function getSelectedLink(editor) {
|
||||
const selection = editor.getSelection();
|
||||
const selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
const range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Expose an API for other plugins to interact with drupallink widgets.
|
||||
// (Compatible with the official CKEditor link plugin's API:
|
||||
// http://dev.ckeditor.com/ticket/13885.)
|
||||
@@ -291,4 +331,4 @@
|
||||
parseLinkAttributes: parseAttributes,
|
||||
getLinkAttributes: getAttributes,
|
||||
};
|
||||
}(jQuery, Drupal, drupalSettings, CKEDITOR));
|
||||
})(jQuery, Drupal, drupalSettings, CKEDITOR);
|
||||
|
||||
@@ -49,6 +49,22 @@
|
||||
};
|
||||
}
|
||||
|
||||
function getSelectedLink(editor) {
|
||||
var selection = editor.getSelection();
|
||||
var selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
var range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupallink', {
|
||||
icons: 'drupallink,drupalunlink',
|
||||
hidpi: true,
|
||||
@@ -102,7 +118,10 @@
|
||||
range.selectNodeContents(text);
|
||||
}
|
||||
|
||||
var style = new CKEDITOR.style({ element: 'a', attributes: returnValues.attributes });
|
||||
var style = new CKEDITOR.style({
|
||||
element: 'a',
|
||||
attributes: returnValues.attributes
|
||||
});
|
||||
style.type = CKEDITOR.STYLE_INLINE;
|
||||
style.applyToRange(range);
|
||||
range.select();
|
||||
@@ -141,7 +160,11 @@
|
||||
}
|
||||
}),
|
||||
exec: function exec(editor) {
|
||||
var style = new CKEDITOR.style({ element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 });
|
||||
var style = new CKEDITOR.style({
|
||||
element: 'a',
|
||||
type: CKEDITOR.STYLE_INLINE,
|
||||
alwaysRemoveElement: 1
|
||||
});
|
||||
editor.removeStyle(style);
|
||||
},
|
||||
refresh: function refresh(editor, path) {
|
||||
@@ -208,7 +231,10 @@
|
||||
|
||||
var menu = {};
|
||||
if (anchor.getAttribute('href') && anchor.getChildCount()) {
|
||||
menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF };
|
||||
menu = {
|
||||
link: CKEDITOR.TRISTATE_OFF,
|
||||
unlink: CKEDITOR.TRISTATE_OFF
|
||||
};
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
@@ -216,22 +242,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
function getSelectedLink(editor) {
|
||||
var selection = editor.getSelection();
|
||||
var selectedElement = selection.getSelectedElement();
|
||||
if (selectedElement && selectedElement.is('a')) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
var range = selection.getRanges(true)[0];
|
||||
|
||||
if (range) {
|
||||
range.shrink(CKEDITOR.SHRINK_TEXT);
|
||||
return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupallink = {
|
||||
parseLinkAttributes: parseAttributes,
|
||||
getLinkAttributes: getAttributes
|
||||
|
||||
@@ -4,226 +4,263 @@
|
||||
* configuration.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
Drupal.ckeditor.AuralView = Backbone.View.extend(/** @lends Drupal.ckeditor.AuralView# */{
|
||||
(function(Drupal, Backbone, $) {
|
||||
Drupal.ckeditor.AuralView = Backbone.View.extend(
|
||||
/** @lends Drupal.ckeditor.AuralView# */ {
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {
|
||||
'click .ckeditor-buttons a': 'announceButtonHelp',
|
||||
'click .ckeditor-multiple-buttons a': 'announceSeparatorHelp',
|
||||
'focus .ckeditor-button a': 'onFocus',
|
||||
'focus .ckeditor-button-separator a': 'onFocus',
|
||||
'focus .ckeditor-toolbar-group': 'onFocus',
|
||||
},
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {
|
||||
'click .ckeditor-buttons a': 'announceButtonHelp',
|
||||
'click .ckeditor-multiple-buttons a': 'announceSeparatorHelp',
|
||||
'focus .ckeditor-button a': 'onFocus',
|
||||
'focus .ckeditor-button-separator a': 'onFocus',
|
||||
'focus .ckeditor-toolbar-group': 'onFocus',
|
||||
},
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; aural UX (output only).
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize() {
|
||||
// Announce the button and group positions when the model is no longer
|
||||
// dirty.
|
||||
this.listenTo(this.model, 'change:isDirty', this.announceMove);
|
||||
},
|
||||
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; aural UX (output only).
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize() {
|
||||
// Announce the button and group positions when the model is no longer
|
||||
// dirty.
|
||||
this.listenTo(this.model, 'change:isDirty', this.announceMove);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls announce on buttons and groups when their position is changed.
|
||||
*
|
||||
* @param {Drupal.ckeditor.ConfigurationModel} model
|
||||
* The ckeditor configuration model.
|
||||
* @param {bool} isDirty
|
||||
* A model attribute that indicates if the changed toolbar configuration
|
||||
* has been stored or not.
|
||||
*/
|
||||
announceMove(model, isDirty) {
|
||||
// Announce the position of a button or group after the model has been
|
||||
// updated.
|
||||
if (!isDirty) {
|
||||
const item = document.activeElement || null;
|
||||
if (item) {
|
||||
const $item = $(item);
|
||||
if ($item.hasClass('ckeditor-toolbar-group')) {
|
||||
this.announceButtonGroupPosition($item);
|
||||
}
|
||||
else if ($item.parent().hasClass('ckeditor-button')) {
|
||||
this.announceButtonPosition($item.parent());
|
||||
/**
|
||||
* Calls announce on buttons and groups when their position is changed.
|
||||
*
|
||||
* @param {Drupal.ckeditor.ConfigurationModel} model
|
||||
* The ckeditor configuration model.
|
||||
* @param {bool} isDirty
|
||||
* A model attribute that indicates if the changed toolbar configuration
|
||||
* has been stored or not.
|
||||
*/
|
||||
announceMove(model, isDirty) {
|
||||
// Announce the position of a button or group after the model has been
|
||||
// updated.
|
||||
if (!isDirty) {
|
||||
const item = document.activeElement || null;
|
||||
if (item) {
|
||||
const $item = $(item);
|
||||
if ($item.hasClass('ckeditor-toolbar-group')) {
|
||||
this.announceButtonGroupPosition($item);
|
||||
} else if ($item.parent().hasClass('ckeditor-button')) {
|
||||
this.announceButtonPosition($item.parent());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the focus event of elements in the active and available toolbars.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The focus event that was triggered.
|
||||
*/
|
||||
onFocus(event) {
|
||||
event.stopPropagation();
|
||||
/**
|
||||
* Handles the focus event of elements in the active and available toolbars.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The focus event that was triggered.
|
||||
*/
|
||||
onFocus(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
const $originalTarget = $(event.target);
|
||||
const $currentTarget = $(event.currentTarget);
|
||||
const $parent = $currentTarget.parent();
|
||||
if ($parent.hasClass('ckeditor-button') || $parent.hasClass('ckeditor-button-separator')) {
|
||||
this.announceButtonPosition($currentTarget.parent());
|
||||
}
|
||||
else if ($originalTarget.attr('role') !== 'button' && $currentTarget.hasClass('ckeditor-toolbar-group')) {
|
||||
this.announceButtonGroupPosition($currentTarget);
|
||||
}
|
||||
},
|
||||
const $originalTarget = $(event.target);
|
||||
const $currentTarget = $(event.currentTarget);
|
||||
const $parent = $currentTarget.parent();
|
||||
if (
|
||||
$parent.hasClass('ckeditor-button') ||
|
||||
$parent.hasClass('ckeditor-button-separator')
|
||||
) {
|
||||
this.announceButtonPosition($currentTarget.parent());
|
||||
} else if (
|
||||
$originalTarget.attr('role') !== 'button' &&
|
||||
$currentTarget.hasClass('ckeditor-toolbar-group')
|
||||
) {
|
||||
this.announceButtonGroupPosition($currentTarget);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Announces the current position of a button group.
|
||||
*
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
*/
|
||||
announceButtonGroupPosition($group) {
|
||||
const $groups = $group.parent().children();
|
||||
const $row = $group.closest('.ckeditor-row');
|
||||
const $rows = $row.parent().children();
|
||||
const position = $groups.index($group) + 1;
|
||||
const positionCount = $groups.not('.placeholder').length;
|
||||
const row = $rows.index($row) + 1;
|
||||
const rowCount = $rows.not('.placeholder').length;
|
||||
let text = Drupal.t('@groupName button group in position @position of @positionCount in row @row of @rowCount.', {
|
||||
'@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
|
||||
'@position': position,
|
||||
'@positionCount': positionCount,
|
||||
'@row': row,
|
||||
'@rowCount': rowCount,
|
||||
});
|
||||
// If this position is the first in the last row then tell the user that
|
||||
// pressing the down arrow key will create a new row.
|
||||
if (position === 1 && row === rowCount) {
|
||||
text += '\n';
|
||||
text += Drupal.t('Press the down arrow key to create a new row.');
|
||||
}
|
||||
Drupal.announce(text, 'assertive');
|
||||
},
|
||||
|
||||
/**
|
||||
* Announces current button position.
|
||||
*
|
||||
* @param {jQuery} $button
|
||||
* A jQuery set that contains an li element that wraps a button.
|
||||
*/
|
||||
announceButtonPosition($button) {
|
||||
const $row = $button.closest('.ckeditor-row');
|
||||
const $rows = $row.parent().children();
|
||||
const $buttons = $button.closest('.ckeditor-buttons').children();
|
||||
const $group = $button.closest('.ckeditor-toolbar-group');
|
||||
const $groups = $group.parent().children();
|
||||
const groupPosition = $groups.index($group) + 1;
|
||||
const groupPositionCount = $groups.not('.placeholder').length;
|
||||
const position = $buttons.index($button) + 1;
|
||||
const positionCount = $buttons.length;
|
||||
const row = $rows.index($row) + 1;
|
||||
const rowCount = $rows.not('.placeholder').length;
|
||||
// The name of the button separator is 'button separator' and its type
|
||||
// is 'separator', so we do not want to print the type of this item,
|
||||
// otherwise the UA will speak 'button separator separator'.
|
||||
const type = ($button.attr('data-drupal-ckeditor-type') === 'separator') ? '' : Drupal.t('button');
|
||||
let text;
|
||||
// The button is located in the available button set.
|
||||
if ($button.closest('.ckeditor-toolbar-disabled').length > 0) {
|
||||
text = Drupal.t('@name @type.', {
|
||||
'@name': $button.children().attr('aria-label'),
|
||||
'@type': type,
|
||||
});
|
||||
text += `\n${Drupal.t('Press the down arrow key to activate.')}`;
|
||||
|
||||
Drupal.announce(text, 'assertive');
|
||||
}
|
||||
// The button is in the active toolbar.
|
||||
else if ($group.not('.placeholder').length === 1) {
|
||||
text = Drupal.t('@name @type in position @position of @positionCount in @groupName button group in row @row of @rowCount.', {
|
||||
'@name': $button.children().attr('aria-label'),
|
||||
'@type': type,
|
||||
'@position': position,
|
||||
'@positionCount': positionCount,
|
||||
'@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
|
||||
'@row': row,
|
||||
'@rowCount': rowCount,
|
||||
});
|
||||
/**
|
||||
* Announces the current position of a button group.
|
||||
*
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
*/
|
||||
announceButtonGroupPosition($group) {
|
||||
const $groups = $group.parent().children();
|
||||
const $row = $group.closest('.ckeditor-row');
|
||||
const $rows = $row.parent().children();
|
||||
const position = $groups.index($group) + 1;
|
||||
const positionCount = $groups.not('.placeholder').length;
|
||||
const row = $rows.index($row) + 1;
|
||||
const rowCount = $rows.not('.placeholder').length;
|
||||
let text = Drupal.t(
|
||||
'@groupName button group in position @position of @positionCount in row @row of @rowCount.',
|
||||
{
|
||||
'@groupName': $group.attr(
|
||||
'data-drupal-ckeditor-toolbar-group-name',
|
||||
),
|
||||
'@position': position,
|
||||
'@positionCount': positionCount,
|
||||
'@row': row,
|
||||
'@rowCount': rowCount,
|
||||
},
|
||||
);
|
||||
// If this position is the first in the last row then tell the user that
|
||||
// pressing the down arrow key will create a new row.
|
||||
if (groupPosition === 1 && position === 1 && row === rowCount) {
|
||||
if (position === 1 && row === rowCount) {
|
||||
text += '\n';
|
||||
text += Drupal.t('Press the down arrow key to create a new button group in a new row.');
|
||||
}
|
||||
// If this position is the last one in this row then tell the user that
|
||||
// moving the button to the next group will create a new group.
|
||||
if (groupPosition === groupPositionCount && position === positionCount) {
|
||||
text += '\n';
|
||||
text += Drupal.t('This is the last group. Move the button forward to create a new group.');
|
||||
text += Drupal.t('Press the down arrow key to create a new row.');
|
||||
}
|
||||
Drupal.announce(text, 'assertive');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Announces current button position.
|
||||
*
|
||||
* @param {jQuery} $button
|
||||
* A jQuery set that contains an li element that wraps a button.
|
||||
*/
|
||||
announceButtonPosition($button) {
|
||||
const $row = $button.closest('.ckeditor-row');
|
||||
const $rows = $row.parent().children();
|
||||
const $buttons = $button.closest('.ckeditor-buttons').children();
|
||||
const $group = $button.closest('.ckeditor-toolbar-group');
|
||||
const $groups = $group.parent().children();
|
||||
const groupPosition = $groups.index($group) + 1;
|
||||
const groupPositionCount = $groups.not('.placeholder').length;
|
||||
const position = $buttons.index($button) + 1;
|
||||
const positionCount = $buttons.length;
|
||||
const row = $rows.index($row) + 1;
|
||||
const rowCount = $rows.not('.placeholder').length;
|
||||
// The name of the button separator is 'button separator' and its type
|
||||
// is 'separator', so we do not want to print the type of this item,
|
||||
// otherwise the UA will speak 'button separator separator'.
|
||||
const type =
|
||||
$button.attr('data-drupal-ckeditor-type') === 'separator'
|
||||
? ''
|
||||
: Drupal.t('button');
|
||||
let text;
|
||||
// The button is located in the available button set.
|
||||
if ($button.closest('.ckeditor-toolbar-disabled').length > 0) {
|
||||
text = Drupal.t('@name @type.', {
|
||||
'@name': $button.children().attr('aria-label'),
|
||||
'@type': type,
|
||||
});
|
||||
text += `\n${Drupal.t('Press the down arrow key to activate.')}`;
|
||||
|
||||
Drupal.announce(text, 'assertive');
|
||||
}
|
||||
// The button is in the active toolbar.
|
||||
else if ($group.not('.placeholder').length === 1) {
|
||||
text = Drupal.t(
|
||||
'@name @type in position @position of @positionCount in @groupName button group in row @row of @rowCount.',
|
||||
{
|
||||
'@name': $button.children().attr('aria-label'),
|
||||
'@type': type,
|
||||
'@position': position,
|
||||
'@positionCount': positionCount,
|
||||
'@groupName': $group.attr(
|
||||
'data-drupal-ckeditor-toolbar-group-name',
|
||||
),
|
||||
'@row': row,
|
||||
'@rowCount': rowCount,
|
||||
},
|
||||
);
|
||||
// If this position is the first in the last row then tell the user that
|
||||
// pressing the down arrow key will create a new row.
|
||||
if (groupPosition === 1 && position === 1 && row === rowCount) {
|
||||
text += '\n';
|
||||
text += Drupal.t(
|
||||
'Press the down arrow key to create a new button group in a new row.',
|
||||
);
|
||||
}
|
||||
// If this position is the last one in this row then tell the user that
|
||||
// moving the button to the next group will create a new group.
|
||||
if (
|
||||
groupPosition === groupPositionCount &&
|
||||
position === positionCount
|
||||
) {
|
||||
text += '\n';
|
||||
text += Drupal.t(
|
||||
'This is the last group. Move the button forward to create a new group.',
|
||||
);
|
||||
}
|
||||
Drupal.announce(text, 'assertive');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides help information when a button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event for the button click.
|
||||
*/
|
||||
announceButtonHelp(event) {
|
||||
const $link = $(event.currentTarget);
|
||||
const $button = $link.parent();
|
||||
const enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
let message;
|
||||
|
||||
if (enabled) {
|
||||
message = Drupal.t('The "@name" button is currently enabled.', {
|
||||
'@name': $link.attr('aria-label'),
|
||||
});
|
||||
message += `\n${Drupal.t(
|
||||
'Use the keyboard arrow keys to change the position of this button.',
|
||||
)}`;
|
||||
message += `\n${Drupal.t(
|
||||
'Press the up arrow key on the top row to disable the button.',
|
||||
)}`;
|
||||
} else {
|
||||
message = Drupal.t('The "@name" button is currently disabled.', {
|
||||
'@name': $link.attr('aria-label'),
|
||||
});
|
||||
message += `\n${Drupal.t(
|
||||
'Use the down arrow key to move this button into the active toolbar.',
|
||||
)}`;
|
||||
}
|
||||
Drupal.announce(message);
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides help information when a separator is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event for the separator click.
|
||||
*/
|
||||
announceSeparatorHelp(event) {
|
||||
const $link = $(event.currentTarget);
|
||||
const $button = $link.parent();
|
||||
const enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
let message;
|
||||
|
||||
if (enabled) {
|
||||
message = Drupal.t('This @name is currently enabled.', {
|
||||
'@name': $link.attr('aria-label'),
|
||||
});
|
||||
message += `\n${Drupal.t(
|
||||
'Use the keyboard arrow keys to change the position of this separator.',
|
||||
)}`;
|
||||
} else {
|
||||
message = Drupal.t(
|
||||
'Separators are used to visually split individual buttons.',
|
||||
);
|
||||
message += `\n${Drupal.t('This @name is currently disabled.', {
|
||||
'@name': $link.attr('aria-label'),
|
||||
})}`;
|
||||
message += `\n${Drupal.t(
|
||||
'Use the down arrow key to move this separator into the active toolbar.',
|
||||
)}`;
|
||||
message += `\n${Drupal.t(
|
||||
'You may add multiple separators to each button group.',
|
||||
)}`;
|
||||
}
|
||||
Drupal.announce(message);
|
||||
event.preventDefault();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides help information when a button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event for the button click.
|
||||
*/
|
||||
announceButtonHelp(event) {
|
||||
const $link = $(event.currentTarget);
|
||||
const $button = $link.parent();
|
||||
const enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
let message;
|
||||
|
||||
if (enabled) {
|
||||
message = Drupal.t('The "@name" button is currently enabled.', {
|
||||
'@name': $link.attr('aria-label'),
|
||||
});
|
||||
message += `\n${Drupal.t('Use the keyboard arrow keys to change the position of this button.')}`;
|
||||
message += `\n${Drupal.t('Press the up arrow key on the top row to disable the button.')}`;
|
||||
}
|
||||
else {
|
||||
message = Drupal.t('The "@name" button is currently disabled.', {
|
||||
'@name': $link.attr('aria-label'),
|
||||
});
|
||||
message += `\n${Drupal.t('Use the down arrow key to move this button into the active toolbar.')}`;
|
||||
}
|
||||
Drupal.announce(message);
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides help information when a separator is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event for the separator click.
|
||||
*/
|
||||
announceSeparatorHelp(event) {
|
||||
const $link = $(event.currentTarget);
|
||||
const $button = $link.parent();
|
||||
const enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
let message;
|
||||
|
||||
if (enabled) {
|
||||
message = Drupal.t('This @name is currently enabled.', {
|
||||
'@name': $link.attr('aria-label'),
|
||||
});
|
||||
message += `\n${Drupal.t('Use the keyboard arrow keys to change the position of this separator.')}`;
|
||||
}
|
||||
else {
|
||||
message = Drupal.t('Separators are used to visually split individual buttons.');
|
||||
message += `\n${Drupal.t('This @name is currently disabled.', {
|
||||
'@name': $link.attr('aria-label'),
|
||||
})}`;
|
||||
message += `\n${Drupal.t('Use the down arrow key to move this separator into the active toolbar.')}`;
|
||||
message += `\n${Drupal.t('You may add multiple separators to each button group.')}`;
|
||||
}
|
||||
Drupal.announce(message);
|
||||
event.preventDefault();
|
||||
},
|
||||
});
|
||||
}(Drupal, Backbone, jQuery));
|
||||
);
|
||||
})(Drupal, Backbone, jQuery);
|
||||
|
||||
@@ -3,369 +3,418 @@
|
||||
* A Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, Backbone, CKEDITOR, _) {
|
||||
Drupal.ckeditor.ControllerView = Backbone.View.extend(/** @lends Drupal.ckeditor.ControllerView# */{
|
||||
(function($, Drupal, Backbone, CKEDITOR, _) {
|
||||
Drupal.ckeditor.ControllerView = Backbone.View.extend(
|
||||
/** @lends Drupal.ckeditor.ControllerView# */ {
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {},
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {},
|
||||
/**
|
||||
* Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize() {
|
||||
this.getCKEditorFeatures(
|
||||
this.model.get('hiddenEditorConfig'),
|
||||
this.disableFeaturesDisallowedByFilters.bind(this),
|
||||
);
|
||||
|
||||
/**
|
||||
* Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize() {
|
||||
this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this));
|
||||
// Push the active editor configuration to the textarea.
|
||||
this.model.listenTo(
|
||||
this.model,
|
||||
'change:activeEditorConfig',
|
||||
this.model.sync,
|
||||
);
|
||||
this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM);
|
||||
},
|
||||
|
||||
// Push the active editor configuration to the textarea.
|
||||
this.model.listenTo(this.model, 'change:activeEditorConfig', this.model.sync);
|
||||
this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM);
|
||||
},
|
||||
/**
|
||||
* Converts the active toolbar DOM structure to an object representation.
|
||||
*
|
||||
* @param {Drupal.ckeditor.ConfigurationModel} model
|
||||
* The state model for the CKEditor configuration.
|
||||
* @param {bool} isDirty
|
||||
* Tracks whether the active toolbar DOM structure has been changed.
|
||||
* isDirty is toggled back to false in this method.
|
||||
* @param {object} options
|
||||
* An object that includes:
|
||||
* @param {bool} [options.broadcast]
|
||||
* A flag that controls whether a CKEditorToolbarChanged event should be
|
||||
* fired for configuration changes.
|
||||
*
|
||||
* @fires event:CKEditorToolbarChanged
|
||||
*/
|
||||
parseEditorDOM(model, isDirty, options) {
|
||||
if (isDirty) {
|
||||
const currentConfig = this.model.get('activeEditorConfig');
|
||||
|
||||
/**
|
||||
* Converts the active toolbar DOM structure to an object representation.
|
||||
*
|
||||
* @param {Drupal.ckeditor.ConfigurationModel} model
|
||||
* The state model for the CKEditor configuration.
|
||||
* @param {bool} isDirty
|
||||
* Tracks whether the active toolbar DOM structure has been changed.
|
||||
* isDirty is toggled back to false in this method.
|
||||
* @param {object} options
|
||||
* An object that includes:
|
||||
* @param {bool} [options.broadcast]
|
||||
* A flag that controls whether a CKEditorToolbarChanged event should be
|
||||
* fired for configuration changes.
|
||||
*
|
||||
* @fires event:CKEditorToolbarChanged
|
||||
*/
|
||||
parseEditorDOM(model, isDirty, options) {
|
||||
if (isDirty) {
|
||||
const currentConfig = this.model.get('activeEditorConfig');
|
||||
|
||||
// Process the rows.
|
||||
const rows = [];
|
||||
this.$el
|
||||
.find('.ckeditor-active-toolbar-configuration')
|
||||
.children('.ckeditor-row').each(function () {
|
||||
const groups = [];
|
||||
// Process the button groups.
|
||||
$(this).find('.ckeditor-toolbar-group').each(function () {
|
||||
const $group = $(this);
|
||||
const $buttons = $group.find('.ckeditor-button');
|
||||
if ($buttons.length) {
|
||||
const group = {
|
||||
name: $group.attr('data-drupal-ckeditor-toolbar-group-name'),
|
||||
items: [],
|
||||
};
|
||||
$group.find('.ckeditor-button, .ckeditor-multiple-button').each(function () {
|
||||
group.items.push($(this).attr('data-drupal-ckeditor-button-name'));
|
||||
// Process the rows.
|
||||
const rows = [];
|
||||
this.$el
|
||||
.find('.ckeditor-active-toolbar-configuration')
|
||||
.children('.ckeditor-row')
|
||||
.each(function() {
|
||||
const groups = [];
|
||||
// Process the button groups.
|
||||
$(this)
|
||||
.find('.ckeditor-toolbar-group')
|
||||
.each(function() {
|
||||
const $group = $(this);
|
||||
const $buttons = $group.find('.ckeditor-button');
|
||||
if ($buttons.length) {
|
||||
const group = {
|
||||
name: $group.attr(
|
||||
'data-drupal-ckeditor-toolbar-group-name',
|
||||
),
|
||||
items: [],
|
||||
};
|
||||
$group
|
||||
.find('.ckeditor-button, .ckeditor-multiple-button')
|
||||
.each(function() {
|
||||
group.items.push(
|
||||
$(this).attr('data-drupal-ckeditor-button-name'),
|
||||
);
|
||||
});
|
||||
groups.push(group);
|
||||
}
|
||||
});
|
||||
groups.push(group);
|
||||
if (groups.length) {
|
||||
rows.push(groups);
|
||||
}
|
||||
});
|
||||
if (groups.length) {
|
||||
rows.push(groups);
|
||||
}
|
||||
});
|
||||
this.model.set('activeEditorConfig', rows);
|
||||
// Mark the model as clean. Whether or not the sync to the textfield
|
||||
// occurs depends on the activeEditorConfig attribute firing a change
|
||||
// event. The DOM has at least been processed and posted, so as far as
|
||||
// the model is concerned, it is clean.
|
||||
this.model.set('isDirty', false);
|
||||
this.model.set('activeEditorConfig', rows);
|
||||
// Mark the model as clean. Whether or not the sync to the textfield
|
||||
// occurs depends on the activeEditorConfig attribute firing a change
|
||||
// event. The DOM has at least been processed and posted, so as far as
|
||||
// the model is concerned, it is clean.
|
||||
this.model.set('isDirty', false);
|
||||
|
||||
// Determine whether we should trigger an event.
|
||||
if (options.broadcast !== false) {
|
||||
const prev = this.getButtonList(currentConfig);
|
||||
const next = this.getButtonList(rows);
|
||||
if (prev.length !== next.length) {
|
||||
// Determine whether we should trigger an event.
|
||||
if (options.broadcast !== false) {
|
||||
const prev = this.getButtonList(currentConfig);
|
||||
const next = this.getButtonList(rows);
|
||||
if (prev.length !== next.length) {
|
||||
this.$el
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.trigger('CKEditorToolbarChanged', [
|
||||
prev.length < next.length ? 'added' : 'removed',
|
||||
_.difference(
|
||||
_.union(prev, next),
|
||||
_.intersection(prev, next),
|
||||
)[0],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Asynchronously retrieve the metadata for all available CKEditor features.
|
||||
*
|
||||
* In order to get a list of all features needed by CKEditor, we create a
|
||||
* hidden CKEditor instance, then check the CKEditor's "allowedContent"
|
||||
* filter settings. Because creating an instance is expensive, a callback
|
||||
* must be provided that will receive a hash of {@link Drupal.EditorFeature}
|
||||
* features keyed by feature (button) name.
|
||||
*
|
||||
* @param {object} CKEditorConfig
|
||||
* An object that represents the configuration settings for a CKEditor
|
||||
* editor component.
|
||||
* @param {function} callback
|
||||
* A function to invoke when the instanceReady event is fired by the
|
||||
* CKEditor object.
|
||||
*/
|
||||
getCKEditorFeatures(CKEditorConfig, callback) {
|
||||
const getProperties = function(CKEPropertiesList) {
|
||||
return _.isObject(CKEPropertiesList) ? _.keys(CKEPropertiesList) : [];
|
||||
};
|
||||
|
||||
const convertCKERulesToEditorFeature = function(
|
||||
feature,
|
||||
CKEFeatureRules,
|
||||
) {
|
||||
for (let i = 0; i < CKEFeatureRules.length; i++) {
|
||||
const CKERule = CKEFeatureRules[i];
|
||||
const rule = new Drupal.EditorFeatureHTMLRule();
|
||||
|
||||
// Tags.
|
||||
const tags = getProperties(CKERule.elements);
|
||||
rule.required.tags = CKERule.propertiesOnly ? [] : tags;
|
||||
rule.allowed.tags = tags;
|
||||
// Attributes.
|
||||
rule.required.attributes = getProperties(
|
||||
CKERule.requiredAttributes,
|
||||
);
|
||||
rule.allowed.attributes = getProperties(CKERule.attributes);
|
||||
// Styles.
|
||||
rule.required.styles = getProperties(CKERule.requiredStyles);
|
||||
rule.allowed.styles = getProperties(CKERule.styles);
|
||||
// Classes.
|
||||
rule.required.classes = getProperties(CKERule.requiredClasses);
|
||||
rule.allowed.classes = getProperties(CKERule.classes);
|
||||
// Raw.
|
||||
rule.raw = CKERule;
|
||||
|
||||
feature.addHTMLRule(rule);
|
||||
}
|
||||
};
|
||||
|
||||
// Create hidden CKEditor with all features enabled, retrieve metadata.
|
||||
// @see \Drupal\ckeditor\Plugin\Editor\CKEditor::buildConfigurationForm().
|
||||
const hiddenCKEditorID = 'ckeditor-hidden';
|
||||
if (CKEDITOR.instances[hiddenCKEditorID]) {
|
||||
CKEDITOR.instances[hiddenCKEditorID].destroy(true);
|
||||
}
|
||||
// Load external plugins, if any.
|
||||
const hiddenEditorConfig = this.model.get('hiddenEditorConfig');
|
||||
if (hiddenEditorConfig.drupalExternalPlugins) {
|
||||
const externalPlugins = hiddenEditorConfig.drupalExternalPlugins;
|
||||
Object.keys(externalPlugins || {}).forEach(pluginName => {
|
||||
CKEDITOR.plugins.addExternal(
|
||||
pluginName,
|
||||
externalPlugins[pluginName],
|
||||
'',
|
||||
);
|
||||
});
|
||||
}
|
||||
CKEDITOR.inline($(`#${hiddenCKEditorID}`).get(0), CKEditorConfig);
|
||||
|
||||
// Once the instance is ready, retrieve the allowedContent filter rules
|
||||
// and convert them to Drupal.EditorFeature objects.
|
||||
CKEDITOR.once('instanceReady', e => {
|
||||
if (e.editor.name === hiddenCKEditorID) {
|
||||
// First collect all CKEditor allowedContent rules.
|
||||
const CKEFeatureRulesMap = {};
|
||||
const rules = e.editor.filter.allowedContent;
|
||||
let rule;
|
||||
let name;
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
rule = rules[i];
|
||||
name = rule.featureName || ':(';
|
||||
if (!CKEFeatureRulesMap[name]) {
|
||||
CKEFeatureRulesMap[name] = [];
|
||||
}
|
||||
CKEFeatureRulesMap[name].push(rule);
|
||||
}
|
||||
|
||||
// Now convert these to Drupal.EditorFeature objects. And track which
|
||||
// buttons are mapped to which features.
|
||||
// @see getFeatureForButton()
|
||||
const features = {};
|
||||
const buttonsToFeatures = {};
|
||||
Object.keys(CKEFeatureRulesMap).forEach(featureName => {
|
||||
const feature = new Drupal.EditorFeature(featureName);
|
||||
convertCKERulesToEditorFeature(
|
||||
feature,
|
||||
CKEFeatureRulesMap[featureName],
|
||||
);
|
||||
features[featureName] = feature;
|
||||
const command = e.editor.getCommand(featureName);
|
||||
if (command) {
|
||||
buttonsToFeatures[command.uiItems[0].name] = featureName;
|
||||
}
|
||||
});
|
||||
|
||||
callback(features, buttonsToFeatures);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the feature for a given button from featuresMetadata. Returns
|
||||
* false if the given button is in fact a divider.
|
||||
*
|
||||
* @param {string} button
|
||||
* The name of a CKEditor button.
|
||||
*
|
||||
* @return {object}
|
||||
* The feature metadata object for a button.
|
||||
*/
|
||||
getFeatureForButton(button) {
|
||||
// Return false if the button being added is a divider.
|
||||
if (button === '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get a Drupal.editorFeature object that contains all metadata for
|
||||
// the feature that was just added or removed. Not every feature has
|
||||
// such metadata.
|
||||
let featureName = this.model.get('buttonsToFeatures')[
|
||||
button.toLowerCase()
|
||||
];
|
||||
// Features without an associated command do not have a 'feature name' by
|
||||
// default, so we use the lowercased button name instead.
|
||||
if (!featureName) {
|
||||
featureName = button.toLowerCase();
|
||||
}
|
||||
const featuresMetadata = this.model.get('featuresMetadata');
|
||||
if (!featuresMetadata[featureName]) {
|
||||
featuresMetadata[featureName] = new Drupal.EditorFeature(featureName);
|
||||
this.model.set('featuresMetadata', featuresMetadata);
|
||||
}
|
||||
return featuresMetadata[featureName];
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks buttons against filter settings; disables disallowed buttons.
|
||||
*
|
||||
* @param {object} features
|
||||
* A map of {@link Drupal.EditorFeature} objects.
|
||||
* @param {object} buttonsToFeatures
|
||||
* Object containing the button-to-feature mapping.
|
||||
*
|
||||
* @see Drupal.ckeditor.ControllerView#getFeatureForButton
|
||||
*/
|
||||
disableFeaturesDisallowedByFilters(features, buttonsToFeatures) {
|
||||
this.model.set('featuresMetadata', features);
|
||||
// Store the button-to-feature mapping. Needs to happen only once, because
|
||||
// the same buttons continue to have the same features; only the rules for
|
||||
// specific features may change.
|
||||
// @see getFeatureForButton()
|
||||
this.model.set('buttonsToFeatures', buttonsToFeatures);
|
||||
|
||||
// Ensure that toolbar configuration changes are broadcast.
|
||||
this.broadcastConfigurationChanges(this.$el);
|
||||
|
||||
// Initialization: not all of the default toolbar buttons may be allowed
|
||||
// by the current filter settings. Remove any of the default toolbar
|
||||
// buttons that require more permissive filter settings. The remaining
|
||||
// default toolbar buttons are marked as "added".
|
||||
let existingButtons = [];
|
||||
// Loop through each button group after flattening the groups from the
|
||||
// toolbar row arrays.
|
||||
const buttonGroups = _.flatten(this.model.get('activeEditorConfig'));
|
||||
for (let i = 0; i < buttonGroups.length; i++) {
|
||||
// Pull the button names from each toolbar button group.
|
||||
const buttons = buttonGroups[i].items;
|
||||
for (let k = 0; k < buttons.length; k++) {
|
||||
existingButtons.push(buttons[k]);
|
||||
}
|
||||
}
|
||||
// Remove duplicate buttons.
|
||||
existingButtons = _.unique(existingButtons);
|
||||
// Prepare the active toolbar and available-button toolbars.
|
||||
for (let n = 0; n < existingButtons.length; n++) {
|
||||
const button = existingButtons[n];
|
||||
const feature = this.getFeatureForButton(button);
|
||||
// Skip dividers.
|
||||
if (feature === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) {
|
||||
// Existing toolbar buttons are in fact "added features".
|
||||
this.$el
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.trigger('CKEditorToolbarChanged', [
|
||||
(prev.length < next.length) ? 'added' : 'removed',
|
||||
_.difference(_.union(prev, next), _.intersection(prev, next))[0],
|
||||
]);
|
||||
.trigger('CKEditorToolbarChanged', ['added', existingButtons[n]]);
|
||||
} else {
|
||||
// Move the button element from the active the active toolbar to the
|
||||
// list of available buttons.
|
||||
$(
|
||||
`.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="${button}"]`,
|
||||
)
|
||||
.detach()
|
||||
.appendTo(
|
||||
'.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul',
|
||||
);
|
||||
// Update the toolbar value field.
|
||||
this.model.set({ isDirty: true }, { broadcast: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Asynchronously retrieve the metadata for all available CKEditor features.
|
||||
*
|
||||
* In order to get a list of all features needed by CKEditor, we create a
|
||||
* hidden CKEditor instance, then check the CKEditor's "allowedContent"
|
||||
* filter settings. Because creating an instance is expensive, a callback
|
||||
* must be provided that will receive a hash of {@link Drupal.EditorFeature}
|
||||
* features keyed by feature (button) name.
|
||||
*
|
||||
* @param {object} CKEditorConfig
|
||||
* An object that represents the configuration settings for a CKEditor
|
||||
* editor component.
|
||||
* @param {function} callback
|
||||
* A function to invoke when the instanceReady event is fired by the
|
||||
* CKEditor object.
|
||||
*/
|
||||
getCKEditorFeatures(CKEditorConfig, callback) {
|
||||
const getProperties = function (CKEPropertiesList) {
|
||||
return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : [];
|
||||
};
|
||||
/**
|
||||
* Sets up broadcasting of CKEditor toolbar configuration changes.
|
||||
*
|
||||
* @param {jQuery} $ckeditorToolbar
|
||||
* The active toolbar DOM element wrapped in jQuery.
|
||||
*/
|
||||
broadcastConfigurationChanges($ckeditorToolbar) {
|
||||
const view = this;
|
||||
const hiddenEditorConfig = this.model.get('hiddenEditorConfig');
|
||||
const getFeatureForButton = this.getFeatureForButton.bind(this);
|
||||
const getCKEditorFeatures = this.getCKEditorFeatures.bind(this);
|
||||
$ckeditorToolbar
|
||||
.find('.ckeditor-toolbar-active')
|
||||
// Listen for CKEditor toolbar configuration changes. When a button is
|
||||
// added/removed, call an appropriate Drupal.editorConfiguration method.
|
||||
.on(
|
||||
'CKEditorToolbarChanged.ckeditorAdmin',
|
||||
(event, action, button) => {
|
||||
const feature = getFeatureForButton(button);
|
||||
|
||||
const convertCKERulesToEditorFeature = function (feature, CKEFeatureRules) {
|
||||
for (let i = 0; i < CKEFeatureRules.length; i++) {
|
||||
const CKERule = CKEFeatureRules[i];
|
||||
const rule = new Drupal.EditorFeatureHTMLRule();
|
||||
|
||||
// Tags.
|
||||
const tags = getProperties(CKERule.elements);
|
||||
rule.required.tags = (CKERule.propertiesOnly) ? [] : tags;
|
||||
rule.allowed.tags = tags;
|
||||
// Attributes.
|
||||
rule.required.attributes = getProperties(CKERule.requiredAttributes);
|
||||
rule.allowed.attributes = getProperties(CKERule.attributes);
|
||||
// Styles.
|
||||
rule.required.styles = getProperties(CKERule.requiredStyles);
|
||||
rule.allowed.styles = getProperties(CKERule.styles);
|
||||
// Classes.
|
||||
rule.required.classes = getProperties(CKERule.requiredClasses);
|
||||
rule.allowed.classes = getProperties(CKERule.classes);
|
||||
// Raw.
|
||||
rule.raw = CKERule;
|
||||
|
||||
feature.addHTMLRule(rule);
|
||||
}
|
||||
};
|
||||
|
||||
// Create hidden CKEditor with all features enabled, retrieve metadata.
|
||||
// @see \Drupal\ckeditor\Plugin\Editor\CKEditor::buildConfigurationForm().
|
||||
const hiddenCKEditorID = 'ckeditor-hidden';
|
||||
if (CKEDITOR.instances[hiddenCKEditorID]) {
|
||||
CKEDITOR.instances[hiddenCKEditorID].destroy(true);
|
||||
}
|
||||
// Load external plugins, if any.
|
||||
const hiddenEditorConfig = this.model.get('hiddenEditorConfig');
|
||||
if (hiddenEditorConfig.drupalExternalPlugins) {
|
||||
const externalPlugins = hiddenEditorConfig.drupalExternalPlugins;
|
||||
Object.keys(externalPlugins || {}).forEach((pluginName) => {
|
||||
CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], '');
|
||||
});
|
||||
}
|
||||
CKEDITOR.inline($(`#${hiddenCKEditorID}`).get(0), CKEditorConfig);
|
||||
|
||||
// Once the instance is ready, retrieve the allowedContent filter rules
|
||||
// and convert them to Drupal.EditorFeature objects.
|
||||
CKEDITOR.once('instanceReady', (e) => {
|
||||
if (e.editor.name === hiddenCKEditorID) {
|
||||
// First collect all CKEditor allowedContent rules.
|
||||
const CKEFeatureRulesMap = {};
|
||||
const rules = e.editor.filter.allowedContent;
|
||||
let rule;
|
||||
let name;
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
rule = rules[i];
|
||||
name = rule.featureName || ':(';
|
||||
if (!CKEFeatureRulesMap[name]) {
|
||||
CKEFeatureRulesMap[name] = [];
|
||||
}
|
||||
CKEFeatureRulesMap[name].push(rule);
|
||||
}
|
||||
|
||||
// Now convert these to Drupal.EditorFeature objects. And track which
|
||||
// buttons are mapped to which features.
|
||||
// @see getFeatureForButton()
|
||||
const features = {};
|
||||
const buttonsToFeatures = {};
|
||||
Object.keys(CKEFeatureRulesMap).forEach((featureName) => {
|
||||
const feature = new Drupal.EditorFeature(featureName);
|
||||
convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]);
|
||||
features[featureName] = feature;
|
||||
const command = e.editor.getCommand(featureName);
|
||||
if (command) {
|
||||
buttonsToFeatures[command.uiItems[0].name] = featureName;
|
||||
}
|
||||
});
|
||||
|
||||
callback(features, buttonsToFeatures);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the feature for a given button from featuresMetadata. Returns
|
||||
* false if the given button is in fact a divider.
|
||||
*
|
||||
* @param {string} button
|
||||
* The name of a CKEditor button.
|
||||
*
|
||||
* @return {object}
|
||||
* The feature metadata object for a button.
|
||||
*/
|
||||
getFeatureForButton(button) {
|
||||
// Return false if the button being added is a divider.
|
||||
if (button === '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get a Drupal.editorFeature object that contains all metadata for
|
||||
// the feature that was just added or removed. Not every feature has
|
||||
// such metadata.
|
||||
let featureName = this.model.get('buttonsToFeatures')[button.toLowerCase()];
|
||||
// Features without an associated command do not have a 'feature name' by
|
||||
// default, so we use the lowercased button name instead.
|
||||
if (!featureName) {
|
||||
featureName = button.toLowerCase();
|
||||
}
|
||||
const featuresMetadata = this.model.get('featuresMetadata');
|
||||
if (!featuresMetadata[featureName]) {
|
||||
featuresMetadata[featureName] = new Drupal.EditorFeature(featureName);
|
||||
this.model.set('featuresMetadata', featuresMetadata);
|
||||
}
|
||||
return featuresMetadata[featureName];
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks buttons against filter settings; disables disallowed buttons.
|
||||
*
|
||||
* @param {object} features
|
||||
* A map of {@link Drupal.EditorFeature} objects.
|
||||
* @param {object} buttonsToFeatures
|
||||
* Object containing the button-to-feature mapping.
|
||||
*
|
||||
* @see Drupal.ckeditor.ControllerView#getFeatureForButton
|
||||
*/
|
||||
disableFeaturesDisallowedByFilters(features, buttonsToFeatures) {
|
||||
this.model.set('featuresMetadata', features);
|
||||
// Store the button-to-feature mapping. Needs to happen only once, because
|
||||
// the same buttons continue to have the same features; only the rules for
|
||||
// specific features may change.
|
||||
// @see getFeatureForButton()
|
||||
this.model.set('buttonsToFeatures', buttonsToFeatures);
|
||||
|
||||
// Ensure that toolbar configuration changes are broadcast.
|
||||
this.broadcastConfigurationChanges(this.$el);
|
||||
|
||||
// Initialization: not all of the default toolbar buttons may be allowed
|
||||
// by the current filter settings. Remove any of the default toolbar
|
||||
// buttons that require more permissive filter settings. The remaining
|
||||
// default toolbar buttons are marked as "added".
|
||||
let existingButtons = [];
|
||||
// Loop through each button group after flattening the groups from the
|
||||
// toolbar row arrays.
|
||||
const buttonGroups = _.flatten(this.model.get('activeEditorConfig'));
|
||||
for (let i = 0; i < buttonGroups.length; i++) {
|
||||
// Pull the button names from each toolbar button group.
|
||||
const buttons = buttonGroups[i].items;
|
||||
for (let k = 0; k < buttons.length; k++) {
|
||||
existingButtons.push(buttons[k]);
|
||||
}
|
||||
}
|
||||
// Remove duplicate buttons.
|
||||
existingButtons = _.unique(existingButtons);
|
||||
// Prepare the active toolbar and available-button toolbars.
|
||||
for (let n = 0; n < existingButtons.length; n++) {
|
||||
const button = existingButtons[n];
|
||||
const feature = this.getFeatureForButton(button);
|
||||
// Skip dividers.
|
||||
if (feature === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) {
|
||||
// Existing toolbar buttons are in fact "added features".
|
||||
this.$el.find('.ckeditor-toolbar-active').trigger('CKEditorToolbarChanged', ['added', existingButtons[n]]);
|
||||
}
|
||||
else {
|
||||
// Move the button element from the active the active toolbar to the
|
||||
// list of available buttons.
|
||||
$(`.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="${button}"]`)
|
||||
.detach()
|
||||
.appendTo('.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul');
|
||||
// Update the toolbar value field.
|
||||
this.model.set({ isDirty: true }, { broadcast: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up broadcasting of CKEditor toolbar configuration changes.
|
||||
*
|
||||
* @param {jQuery} $ckeditorToolbar
|
||||
* The active toolbar DOM element wrapped in jQuery.
|
||||
*/
|
||||
broadcastConfigurationChanges($ckeditorToolbar) {
|
||||
const view = this;
|
||||
const hiddenEditorConfig = this.model.get('hiddenEditorConfig');
|
||||
const getFeatureForButton = this.getFeatureForButton.bind(this);
|
||||
const getCKEditorFeatures = this.getCKEditorFeatures.bind(this);
|
||||
$ckeditorToolbar
|
||||
.find('.ckeditor-toolbar-active')
|
||||
// Listen for CKEditor toolbar configuration changes. When a button is
|
||||
// added/removed, call an appropriate Drupal.editorConfiguration method.
|
||||
.on('CKEditorToolbarChanged.ckeditorAdmin', (event, action, button) => {
|
||||
const feature = getFeatureForButton(button);
|
||||
|
||||
// Early-return if the button being added is a divider.
|
||||
if (feature === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger a standardized text editor configuration event to indicate
|
||||
// whether a feature was added or removed, so that filters can react.
|
||||
const configEvent = (action === 'added') ? 'addedFeature' : 'removedFeature';
|
||||
Drupal.editorConfiguration[configEvent](feature);
|
||||
})
|
||||
// Listen for CKEditor plugin settings changes. When a plugin setting is
|
||||
// changed, rebuild the CKEditor features metadata.
|
||||
.on('CKEditorPluginSettingsChanged.ckeditorAdmin', (event, settingsChanges) => {
|
||||
// Update hidden CKEditor configuration.
|
||||
Object.keys(settingsChanges || {}).forEach((key) => {
|
||||
hiddenEditorConfig[key] = settingsChanges[key];
|
||||
});
|
||||
|
||||
// Retrieve features for the updated hidden CKEditor configuration.
|
||||
getCKEditorFeatures(hiddenEditorConfig, (features) => {
|
||||
// Trigger a standardized text editor configuration event for each
|
||||
// feature that was modified by the configuration changes.
|
||||
const featuresMetadata = view.model.get('featuresMetadata');
|
||||
Object.keys(features || {}).forEach((name) => {
|
||||
const feature = features[name];
|
||||
if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) {
|
||||
Drupal.editorConfiguration.modifiedFeature(feature);
|
||||
// Early-return if the button being added is a divider.
|
||||
if (feature === false) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
// Update the CKEditor features metadata.
|
||||
view.model.set('featuresMetadata', features);
|
||||
|
||||
// Trigger a standardized text editor configuration event to indicate
|
||||
// whether a feature was added or removed, so that filters can react.
|
||||
const configEvent =
|
||||
action === 'added' ? 'addedFeature' : 'removedFeature';
|
||||
Drupal.editorConfiguration[configEvent](feature);
|
||||
},
|
||||
)
|
||||
// Listen for CKEditor plugin settings changes. When a plugin setting is
|
||||
// changed, rebuild the CKEditor features metadata.
|
||||
.on(
|
||||
'CKEditorPluginSettingsChanged.ckeditorAdmin',
|
||||
(event, settingsChanges) => {
|
||||
// Update hidden CKEditor configuration.
|
||||
Object.keys(settingsChanges || {}).forEach(key => {
|
||||
hiddenEditorConfig[key] = settingsChanges[key];
|
||||
});
|
||||
|
||||
// Retrieve features for the updated hidden CKEditor configuration.
|
||||
getCKEditorFeatures(hiddenEditorConfig, features => {
|
||||
// Trigger a standardized text editor configuration event for each
|
||||
// feature that was modified by the configuration changes.
|
||||
const featuresMetadata = view.model.get('featuresMetadata');
|
||||
Object.keys(features || {}).forEach(name => {
|
||||
const feature = features[name];
|
||||
if (
|
||||
featuresMetadata.hasOwnProperty(name) &&
|
||||
!_.isEqual(featuresMetadata[name], feature)
|
||||
) {
|
||||
Drupal.editorConfiguration.modifiedFeature(feature);
|
||||
}
|
||||
});
|
||||
// Update the CKEditor features metadata.
|
||||
view.model.set('featuresMetadata', features);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the list of buttons from an editor configuration.
|
||||
*
|
||||
* @param {object} config
|
||||
* A CKEditor configuration object.
|
||||
*
|
||||
* @return {Array}
|
||||
* A list of buttons in the CKEditor configuration.
|
||||
*/
|
||||
getButtonList(config) {
|
||||
const buttons = [];
|
||||
// Remove the rows.
|
||||
config = _.flatten(config);
|
||||
|
||||
// Loop through the button groups and pull out the buttons.
|
||||
config.forEach(group => {
|
||||
group.items.forEach(button => {
|
||||
buttons.push(button);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove the dividing elements if any.
|
||||
return _.without(buttons, '-');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the list of buttons from an editor configuration.
|
||||
*
|
||||
* @param {object} config
|
||||
* A CKEditor configuration object.
|
||||
*
|
||||
* @return {Array}
|
||||
* A list of buttons in the CKEditor configuration.
|
||||
*/
|
||||
getButtonList(config) {
|
||||
const buttons = [];
|
||||
// Remove the rows.
|
||||
config = _.flatten(config);
|
||||
|
||||
// Loop through the button groups and pull out the buttons.
|
||||
config.forEach((group) => {
|
||||
group.items.forEach((button) => {
|
||||
buttons.push(button);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove the dividing elements if any.
|
||||
return _.without(buttons, '-');
|
||||
},
|
||||
});
|
||||
}(jQuery, Drupal, Backbone, CKEDITOR, _));
|
||||
);
|
||||
})(jQuery, Drupal, Backbone, CKEDITOR, _);
|
||||
|
||||
@@ -3,281 +3,310 @@
|
||||
* Backbone View providing the aural view of CKEditor keyboard UX configuration.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, Backbone, _) {
|
||||
Drupal.ckeditor.KeyboardView = Backbone.View.extend(/** @lends Drupal.ckeditor.KeyboardView# */{
|
||||
(function($, Drupal, Backbone, _) {
|
||||
Drupal.ckeditor.KeyboardView = Backbone.View.extend(
|
||||
/** @lends Drupal.ckeditor.KeyboardView# */ {
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; keyboard UX.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize() {
|
||||
// Add keyboard arrow support.
|
||||
this.$el.on(
|
||||
'keydown.ckeditor',
|
||||
'.ckeditor-buttons a, .ckeditor-multiple-buttons a',
|
||||
this.onPressButton.bind(this),
|
||||
);
|
||||
this.$el.on(
|
||||
'keydown.ckeditor',
|
||||
'[data-drupal-ckeditor-type="group"]',
|
||||
this.onPressGroup.bind(this),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; keyboard UX.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize() {
|
||||
// Add keyboard arrow support.
|
||||
this.$el.on('keydown.ckeditor', '.ckeditor-buttons a, .ckeditor-multiple-buttons a', this.onPressButton.bind(this));
|
||||
this.$el.on('keydown.ckeditor', '[data-drupal-ckeditor-type="group"]', this.onPressGroup.bind(this));
|
||||
},
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
},
|
||||
/**
|
||||
* Handles keypresses on a CKEditor configuration button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The keypress event triggered.
|
||||
*/
|
||||
onPressButton(event) {
|
||||
const upDownKeys = [
|
||||
38, // Up arrow.
|
||||
63232, // Safari up arrow.
|
||||
40, // Down arrow.
|
||||
63233, // Safari down arrow.
|
||||
];
|
||||
const leftRightKeys = [
|
||||
37, // Left arrow.
|
||||
63234, // Safari left arrow.
|
||||
39, // Right arrow.
|
||||
63235, // Safari right arrow.
|
||||
];
|
||||
|
||||
/**
|
||||
* Handles keypresses on a CKEditor configuration button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The keypress event triggered.
|
||||
*/
|
||||
onPressButton(event) {
|
||||
const upDownKeys = [
|
||||
38, // Up arrow.
|
||||
63232, // Safari up arrow.
|
||||
40, // Down arrow.
|
||||
63233, // Safari down arrow.
|
||||
];
|
||||
const leftRightKeys = [
|
||||
37, // Left arrow.
|
||||
63234, // Safari left arrow.
|
||||
39, // Right arrow.
|
||||
63235, // Safari right arrow.
|
||||
];
|
||||
|
||||
// Respond to an enter key press. Prevent the bubbling of the enter key
|
||||
// press to the button group parent element.
|
||||
if (event.keyCode === 13) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Only take action when a direction key is pressed.
|
||||
if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
|
||||
let view = this;
|
||||
let $target = $(event.currentTarget);
|
||||
let $button = $target.parent();
|
||||
const $container = $button.parent();
|
||||
let $group = $button.closest('.ckeditor-toolbar-group');
|
||||
let $row;
|
||||
const containerType = $container.data('drupal-ckeditor-button-sorting');
|
||||
const $availableButtons = this.$el.find('[data-drupal-ckeditor-button-sorting="source"]');
|
||||
const $activeButtons = this.$el.find('.ckeditor-toolbar-active');
|
||||
// The current location of the button, just in case it needs to be put
|
||||
// back.
|
||||
const $originalGroup = $group;
|
||||
let dir;
|
||||
|
||||
// Move available buttons between their container and the active
|
||||
// toolbar.
|
||||
if (containerType === 'source') {
|
||||
// Move the button to the active toolbar configuration when the down
|
||||
// or up keys are pressed.
|
||||
if (_.indexOf([40, 63233], event.keyCode) > -1) {
|
||||
// Move the button to the first row, first button group index
|
||||
// position.
|
||||
$activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
|
||||
}
|
||||
// Respond to an enter key press. Prevent the bubbling of the enter key
|
||||
// press to the button group parent element.
|
||||
if (event.keyCode === 13) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
else if (containerType === 'target') {
|
||||
// Move buttons between sibling buttons in a group and between groups.
|
||||
if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
|
||||
// Move left.
|
||||
const $siblings = $container.children();
|
||||
const index = $siblings.index($button);
|
||||
if (_.indexOf([37, 63234], event.keyCode) > -1) {
|
||||
// Move between sibling buttons.
|
||||
if (index > 0) {
|
||||
$button.insertBefore($container.children().eq(index - 1));
|
||||
}
|
||||
// Move between button groups and rows.
|
||||
else {
|
||||
// Move between button groups.
|
||||
$group = $container.parent().prev();
|
||||
if ($group.length > 0) {
|
||||
$group.find('.ckeditor-toolbar-group-buttons').append($button);
|
||||
|
||||
// Only take action when a direction key is pressed.
|
||||
if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
|
||||
let view = this;
|
||||
let $target = $(event.currentTarget);
|
||||
let $button = $target.parent();
|
||||
const $container = $button.parent();
|
||||
let $group = $button.closest('.ckeditor-toolbar-group');
|
||||
let $row;
|
||||
const containerType = $container.data(
|
||||
'drupal-ckeditor-button-sorting',
|
||||
);
|
||||
const $availableButtons = this.$el.find(
|
||||
'[data-drupal-ckeditor-button-sorting="source"]',
|
||||
);
|
||||
const $activeButtons = this.$el.find('.ckeditor-toolbar-active');
|
||||
// The current location of the button, just in case it needs to be put
|
||||
// back.
|
||||
const $originalGroup = $group;
|
||||
let dir;
|
||||
|
||||
// Move available buttons between their container and the active
|
||||
// toolbar.
|
||||
if (containerType === 'source') {
|
||||
// Move the button to the active toolbar configuration when the down
|
||||
// or up keys are pressed.
|
||||
if (_.indexOf([40, 63233], event.keyCode) > -1) {
|
||||
// Move the button to the first row, first button group index
|
||||
// position.
|
||||
$activeButtons
|
||||
.find('.ckeditor-toolbar-group-buttons')
|
||||
.eq(0)
|
||||
.prepend($button);
|
||||
}
|
||||
} else if (containerType === 'target') {
|
||||
// Move buttons between sibling buttons in a group and between groups.
|
||||
if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
|
||||
// Move left.
|
||||
const $siblings = $container.children();
|
||||
const index = $siblings.index($button);
|
||||
if (_.indexOf([37, 63234], event.keyCode) > -1) {
|
||||
// Move between sibling buttons.
|
||||
if (index > 0) {
|
||||
$button.insertBefore($container.children().eq(index - 1));
|
||||
}
|
||||
// Wrap between rows.
|
||||
// Move between button groups and rows.
|
||||
else {
|
||||
// Move between button groups.
|
||||
$group = $container.parent().prev();
|
||||
if ($group.length > 0) {
|
||||
$group
|
||||
.find('.ckeditor-toolbar-group-buttons')
|
||||
.append($button);
|
||||
}
|
||||
// Wrap between rows.
|
||||
else {
|
||||
$container
|
||||
.closest('.ckeditor-row')
|
||||
.prev()
|
||||
.find('.ckeditor-toolbar-group')
|
||||
.not('.placeholder')
|
||||
.find('.ckeditor-toolbar-group-buttons')
|
||||
.eq(-1)
|
||||
.append($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move right.
|
||||
else if (_.indexOf([39, 63235], event.keyCode) > -1) {
|
||||
// Move between sibling buttons.
|
||||
if (index < $siblings.length - 1) {
|
||||
$button.insertAfter($container.children().eq(index + 1));
|
||||
}
|
||||
// Move between button groups. Moving right at the end of a row
|
||||
// will create a new group.
|
||||
else {
|
||||
$container
|
||||
.closest('.ckeditor-row')
|
||||
.prev()
|
||||
.find('.ckeditor-toolbar-group')
|
||||
.not('.placeholder')
|
||||
.parent()
|
||||
.next()
|
||||
.find('.ckeditor-toolbar-group-buttons')
|
||||
.eq(-1)
|
||||
.append($button);
|
||||
.prepend($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move right.
|
||||
else if (_.indexOf([39, 63235], event.keyCode) > -1) {
|
||||
// Move between sibling buttons.
|
||||
if (index < ($siblings.length - 1)) {
|
||||
$button.insertAfter($container.children().eq(index + 1));
|
||||
}
|
||||
// Move between button groups. Moving right at the end of a row
|
||||
// will create a new group.
|
||||
else {
|
||||
$container.parent().next().find('.ckeditor-toolbar-group-buttons').prepend($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move buttons between rows and the available button set.
|
||||
else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
|
||||
dir = (_.indexOf([38, 63232], event.keyCode) > -1) ? 'prev' : 'next';
|
||||
$row = $container.closest('.ckeditor-row')[dir]();
|
||||
// Move the button back into the available button set.
|
||||
if (dir === 'prev' && $row.length === 0) {
|
||||
// If this is a divider, just destroy it.
|
||||
if ($button.data('drupal-ckeditor-type') === 'separator') {
|
||||
$button
|
||||
.off()
|
||||
.remove();
|
||||
// Focus on the first button in the active toolbar.
|
||||
$activeButtons
|
||||
// Move buttons between rows and the available button set.
|
||||
else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
|
||||
dir =
|
||||
_.indexOf([38, 63232], event.keyCode) > -1 ? 'prev' : 'next';
|
||||
$row = $container.closest('.ckeditor-row')[dir]();
|
||||
// Move the button back into the available button set.
|
||||
if (dir === 'prev' && $row.length === 0) {
|
||||
// If this is a divider, just destroy it.
|
||||
if ($button.data('drupal-ckeditor-type') === 'separator') {
|
||||
$button.off().remove();
|
||||
// Focus on the first button in the active toolbar.
|
||||
$activeButtons
|
||||
.find('.ckeditor-toolbar-group-buttons')
|
||||
.eq(0)
|
||||
.children()
|
||||
.eq(0)
|
||||
.children()
|
||||
.trigger('focus');
|
||||
}
|
||||
// Otherwise, move it.
|
||||
else {
|
||||
$availableButtons.prepend($button);
|
||||
}
|
||||
} else {
|
||||
$row
|
||||
.find('.ckeditor-toolbar-group-buttons')
|
||||
.eq(0)
|
||||
.children()
|
||||
.eq(0)
|
||||
.children()
|
||||
.trigger('focus');
|
||||
.prepend($button);
|
||||
}
|
||||
// Otherwise, move it.
|
||||
}
|
||||
}
|
||||
// Move dividers between their container and the active toolbar.
|
||||
else if (containerType === 'dividers') {
|
||||
// Move the button to the active toolbar configuration when the down
|
||||
// or up keys are pressed.
|
||||
if (_.indexOf([40, 63233], event.keyCode) > -1) {
|
||||
// Move the button to the first row, first button group index
|
||||
// position.
|
||||
$button = $button.clone(true);
|
||||
$activeButtons
|
||||
.find('.ckeditor-toolbar-group-buttons')
|
||||
.eq(0)
|
||||
.prepend($button);
|
||||
$target = $button.children();
|
||||
}
|
||||
}
|
||||
|
||||
view = this;
|
||||
// Attempt to move the button to the new toolbar position.
|
||||
Drupal.ckeditor.registerButtonMove(this, $button, result => {
|
||||
// Put the button back if the registration failed.
|
||||
// If the button was in a row, then it was in the active toolbar
|
||||
// configuration. The button was probably placed in a new group, but
|
||||
// that action was canceled.
|
||||
if (!result && $originalGroup) {
|
||||
$originalGroup.find('.ckeditor-buttons').append($button);
|
||||
}
|
||||
// Otherwise refresh the sortables to acknowledge the new button
|
||||
// positions.
|
||||
else {
|
||||
view.$el.find('.ui-sortable').sortable('refresh');
|
||||
}
|
||||
// Refocus the target button so that the user can continue from a
|
||||
// known place.
|
||||
$target.trigger('focus');
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keypresses on a CKEditor configuration group.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The keypress event triggered.
|
||||
*/
|
||||
onPressGroup(event) {
|
||||
const upDownKeys = [
|
||||
38, // Up arrow.
|
||||
63232, // Safari up arrow.
|
||||
40, // Down arrow.
|
||||
63233, // Safari down arrow.
|
||||
];
|
||||
const leftRightKeys = [
|
||||
37, // Left arrow.
|
||||
63234, // Safari left arrow.
|
||||
39, // Right arrow.
|
||||
63235, // Safari right arrow.
|
||||
];
|
||||
|
||||
// Respond to an enter key press.
|
||||
if (event.keyCode === 13) {
|
||||
const view = this;
|
||||
// Open the group renaming dialog in the next evaluation cycle so that
|
||||
// this event can be cancelled and the bubbling wiped out. Otherwise,
|
||||
// Firefox has issues because the page focus is shifted to the dialog
|
||||
// along with the keydown event.
|
||||
window.setTimeout(() => {
|
||||
Drupal.ckeditor.openGroupNameDialog(view, $(event.currentTarget));
|
||||
}, 0);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Respond to direction key presses.
|
||||
if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
|
||||
const $group = $(event.currentTarget);
|
||||
const $container = $group.parent();
|
||||
const $siblings = $container.children();
|
||||
let index;
|
||||
let dir;
|
||||
// Move groups between sibling groups.
|
||||
if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
|
||||
index = $siblings.index($group);
|
||||
// Move left between sibling groups.
|
||||
if (_.indexOf([37, 63234], event.keyCode) > -1) {
|
||||
if (index > 0) {
|
||||
$group.insertBefore($siblings.eq(index - 1));
|
||||
}
|
||||
// Wrap between rows. Insert the group before the placeholder group
|
||||
// at the end of the previous row.
|
||||
else {
|
||||
$availableButtons.prepend($button);
|
||||
const $rowChildElement = $container
|
||||
.closest('.ckeditor-row')
|
||||
.prev()
|
||||
.find('.ckeditor-toolbar-groups')
|
||||
.children()
|
||||
.eq(-1);
|
||||
$group.insertBefore($rowChildElement);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$row.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
|
||||
// Move right between sibling groups.
|
||||
else if (_.indexOf([39, 63235], event.keyCode) > -1) {
|
||||
// Move to the right if the next group is not a placeholder.
|
||||
if (!$siblings.eq(index + 1).hasClass('placeholder')) {
|
||||
$group.insertAfter($container.children().eq(index + 1));
|
||||
}
|
||||
// Wrap group between rows.
|
||||
else {
|
||||
$container
|
||||
.closest('.ckeditor-row')
|
||||
.next()
|
||||
.find('.ckeditor-toolbar-groups')
|
||||
.prepend($group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move dividers between their container and the active toolbar.
|
||||
else if (containerType === 'dividers') {
|
||||
// Move the button to the active toolbar configuration when the down
|
||||
// or up keys are pressed.
|
||||
if (_.indexOf([40, 63233], event.keyCode) > -1) {
|
||||
// Move the button to the first row, first button group index
|
||||
// position.
|
||||
$button = $button.clone(true);
|
||||
$activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
|
||||
$target = $button.children();
|
||||
// Move groups between rows.
|
||||
else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
|
||||
dir = _.indexOf([38, 63232], event.keyCode) > -1 ? 'prev' : 'next';
|
||||
$group
|
||||
.closest('.ckeditor-row')
|
||||
[dir]()
|
||||
.find('.ckeditor-toolbar-groups')
|
||||
.eq(0)
|
||||
.prepend($group);
|
||||
}
|
||||
}
|
||||
|
||||
view = this;
|
||||
// Attempt to move the button to the new toolbar position.
|
||||
Drupal.ckeditor.registerButtonMove(this, $button, (result) => {
|
||||
// Put the button back if the registration failed.
|
||||
// If the button was in a row, then it was in the active toolbar
|
||||
// configuration. The button was probably placed in a new group, but
|
||||
// that action was canceled.
|
||||
if (!result && $originalGroup) {
|
||||
$originalGroup.find('.ckeditor-buttons').append($button);
|
||||
}
|
||||
// Otherwise refresh the sortables to acknowledge the new button
|
||||
// positions.
|
||||
else {
|
||||
view.$el.find('.ui-sortable').sortable('refresh');
|
||||
}
|
||||
// Refocus the target button so that the user can continue from a
|
||||
// known place.
|
||||
$target.trigger('focus');
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
Drupal.ckeditor.registerGroupMove(this, $group);
|
||||
$group.trigger('focus');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keypresses on a CKEditor configuration group.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The keypress event triggered.
|
||||
*/
|
||||
onPressGroup(event) {
|
||||
const upDownKeys = [
|
||||
38, // Up arrow.
|
||||
63232, // Safari up arrow.
|
||||
40, // Down arrow.
|
||||
63233, // Safari down arrow.
|
||||
];
|
||||
const leftRightKeys = [
|
||||
37, // Left arrow.
|
||||
63234, // Safari left arrow.
|
||||
39, // Right arrow.
|
||||
63235, // Safari right arrow.
|
||||
];
|
||||
|
||||
// Respond to an enter key press.
|
||||
if (event.keyCode === 13) {
|
||||
const view = this;
|
||||
// Open the group renaming dialog in the next evaluation cycle so that
|
||||
// this event can be cancelled and the bubbling wiped out. Otherwise,
|
||||
// Firefox has issues because the page focus is shifted to the dialog
|
||||
// along with the keydown event.
|
||||
window.setTimeout(() => {
|
||||
Drupal.ckeditor.openGroupNameDialog(view, $(event.currentTarget));
|
||||
}, 0);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Respond to direction key presses.
|
||||
if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
|
||||
const $group = $(event.currentTarget);
|
||||
const $container = $group.parent();
|
||||
const $siblings = $container.children();
|
||||
let index;
|
||||
let dir;
|
||||
// Move groups between sibling groups.
|
||||
if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
|
||||
index = $siblings.index($group);
|
||||
// Move left between sibling groups.
|
||||
if ((_.indexOf([37, 63234], event.keyCode) > -1)) {
|
||||
if (index > 0) {
|
||||
$group.insertBefore($siblings.eq(index - 1));
|
||||
}
|
||||
// Wrap between rows. Insert the group before the placeholder group
|
||||
// at the end of the previous row.
|
||||
else {
|
||||
const $rowChildElement = $container
|
||||
.closest('.ckeditor-row')
|
||||
.prev()
|
||||
.find('.ckeditor-toolbar-groups')
|
||||
.children()
|
||||
.eq(-1);
|
||||
$group.insertBefore($rowChildElement);
|
||||
}
|
||||
}
|
||||
// Move right between sibling groups.
|
||||
else if (_.indexOf([39, 63235], event.keyCode) > -1) {
|
||||
// Move to the right if the next group is not a placeholder.
|
||||
if (!$siblings.eq(index + 1).hasClass('placeholder')) {
|
||||
$group.insertAfter($container.children().eq(index + 1));
|
||||
}
|
||||
// Wrap group between rows.
|
||||
else {
|
||||
$container.closest('.ckeditor-row').next().find('.ckeditor-toolbar-groups').prepend($group);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Move groups between rows.
|
||||
else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
|
||||
dir = (_.indexOf([38, 63232], event.keyCode) > -1) ? 'prev' : 'next';
|
||||
$group
|
||||
.closest('.ckeditor-row')[dir]()
|
||||
.find('.ckeditor-toolbar-groups')
|
||||
.eq(0)
|
||||
.prepend($group);
|
||||
}
|
||||
|
||||
Drupal.ckeditor.registerGroupMove(this, $group);
|
||||
$group.trigger('focus');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
});
|
||||
}(jQuery, Drupal, Backbone, _));
|
||||
);
|
||||
})(jQuery, Drupal, Backbone, _);
|
||||
|
||||
@@ -4,265 +4,312 @@
|
||||
* configuration.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
Drupal.ckeditor.VisualView = Backbone.View.extend(/** @lends Drupal.ckeditor.VisualView# */{
|
||||
(function(Drupal, Backbone, $) {
|
||||
Drupal.ckeditor.VisualView = Backbone.View.extend(
|
||||
/** @lends Drupal.ckeditor.VisualView# */ {
|
||||
events: {
|
||||
'click .ckeditor-toolbar-group-name': 'onGroupNameClick',
|
||||
'click .ckeditor-groupnames-toggle': 'onGroupNamesToggleClick',
|
||||
'click .ckeditor-add-new-group button': 'onAddGroupButtonClick',
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .ckeditor-toolbar-group-name': 'onGroupNameClick',
|
||||
'click .ckeditor-groupnames-toggle': 'onGroupNamesToggleClick',
|
||||
'click .ckeditor-add-new-group button': 'onAddGroupButtonClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; visual UX.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize() {
|
||||
this.listenTo(this.model, 'change:isDirty change:groupNamesVisible', this.render);
|
||||
|
||||
// Add a toggle for the button group names.
|
||||
$(Drupal.theme('ckeditorButtonGroupNamesToggle'))
|
||||
.prependTo(this.$el.find('#ckeditor-active-toolbar').parent());
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* Render function for rendering the toolbar configuration.
|
||||
*
|
||||
* @param {*} model
|
||||
* Model used for the view.
|
||||
* @param {string} [value]
|
||||
* The value that was changed.
|
||||
* @param {object} changedAttributes
|
||||
* The attributes that was changed.
|
||||
*
|
||||
* @return {Drupal.ckeditor.VisualView}
|
||||
* The {@link Drupal.ckeditor.VisualView} object.
|
||||
*/
|
||||
render(model, value, changedAttributes) {
|
||||
this.insertPlaceholders();
|
||||
this.applySorting();
|
||||
|
||||
// Toggle button group names.
|
||||
let groupNamesVisible = this.model.get('groupNamesVisible');
|
||||
// If a button was just placed in the active toolbar, ensure that the
|
||||
// button group names are visible.
|
||||
if (changedAttributes && changedAttributes.changes && changedAttributes.changes.isDirty) {
|
||||
this.model.set({ groupNamesVisible: true }, { silent: true });
|
||||
groupNamesVisible = true;
|
||||
}
|
||||
this.$el.find('[data-toolbar="active"]').toggleClass('ckeditor-group-names-are-visible', groupNamesVisible);
|
||||
this.$el.find('.ckeditor-groupnames-toggle')
|
||||
.text((groupNamesVisible) ? Drupal.t('Hide group names') : Drupal.t('Show group names'))
|
||||
.attr('aria-pressed', groupNamesVisible);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles clicks to a button group name.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event on the button group.
|
||||
*/
|
||||
onGroupNameClick(event) {
|
||||
const $group = $(event.currentTarget).closest('.ckeditor-toolbar-group');
|
||||
Drupal.ckeditor.openGroupNameDialog(this, $group);
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles clicks on the button group names toggle button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event on the toggle button.
|
||||
*/
|
||||
onGroupNamesToggleClick(event) {
|
||||
this.model.set('groupNamesVisible', !this.model.get('groupNamesVisible'));
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the user to provide a name for a new button group; inserts it.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event of the button click.
|
||||
*/
|
||||
onAddGroupButtonClick(event) {
|
||||
/**
|
||||
* Inserts a new button if the openGroupNameDialog function returns true.
|
||||
* Backbone View for CKEditor toolbar configuration; visual UX.
|
||||
*
|
||||
* @param {bool} success
|
||||
* A flag that indicates if the user created a new group (true) or
|
||||
* canceled out of the dialog (false).
|
||||
* @param {jQuery} $group
|
||||
* A jQuery DOM fragment that represents the new button group. It has
|
||||
* not been added to the DOM yet.
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
function insertNewGroup(success, $group) {
|
||||
if (success) {
|
||||
$group.appendTo($(event.currentTarget).closest('.ckeditor-row').children('.ckeditor-toolbar-groups'));
|
||||
// Focus on the new group.
|
||||
$group.trigger('focus');
|
||||
initialize() {
|
||||
this.listenTo(
|
||||
this.model,
|
||||
'change:isDirty change:groupNamesVisible',
|
||||
this.render,
|
||||
);
|
||||
|
||||
// Add a toggle for the button group names.
|
||||
$(Drupal.theme('ckeditorButtonGroupNamesToggle')).prependTo(
|
||||
this.$el.find('#ckeditor-active-toolbar').parent(),
|
||||
);
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* Render function for rendering the toolbar configuration.
|
||||
*
|
||||
* @param {*} model
|
||||
* Model used for the view.
|
||||
* @param {string} [value]
|
||||
* The value that was changed.
|
||||
* @param {object} changedAttributes
|
||||
* The attributes that was changed.
|
||||
*
|
||||
* @return {Drupal.ckeditor.VisualView}
|
||||
* The {@link Drupal.ckeditor.VisualView} object.
|
||||
*/
|
||||
render(model, value, changedAttributes) {
|
||||
this.insertPlaceholders();
|
||||
this.applySorting();
|
||||
|
||||
// Toggle button group names.
|
||||
let groupNamesVisible = this.model.get('groupNamesVisible');
|
||||
// If a button was just placed in the active toolbar, ensure that the
|
||||
// button group names are visible.
|
||||
if (
|
||||
changedAttributes &&
|
||||
changedAttributes.changes &&
|
||||
changedAttributes.changes.isDirty
|
||||
) {
|
||||
this.model.set({ groupNamesVisible: true }, { silent: true });
|
||||
groupNamesVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass in a DOM fragment of a placeholder group so that the new group
|
||||
// name can be applied to it.
|
||||
Drupal.ckeditor.openGroupNameDialog(this, $(Drupal.theme('ckeditorToolbarGroup')), insertNewGroup);
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable stop sort of a button group.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the group drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
endGroupDrag(event, ui) {
|
||||
const view = this;
|
||||
Drupal.ckeditor.registerGroupMove(this, ui.item, (success) => {
|
||||
if (!success) {
|
||||
// Cancel any sorting in the configuration area.
|
||||
view.$el.find('.ckeditor-toolbar-configuration').find('.ui-sortable').sortable('cancel');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable start sort of a button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the group drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
startButtonDrag(event, ui) {
|
||||
this.$el.find('a:focus').trigger('blur');
|
||||
|
||||
// Show the button group names as soon as the user starts dragging.
|
||||
this.model.set('groupNamesVisible', true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable stop sort of a button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the button drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
endButtonDrag(event, ui) {
|
||||
const view = this;
|
||||
Drupal.ckeditor.registerButtonMove(this, ui.item, (success) => {
|
||||
if (!success) {
|
||||
// Cancel any sorting in the configuration area.
|
||||
view.$el.find('.ui-sortable').sortable('cancel');
|
||||
}
|
||||
// Refocus the target button so that the user can continue from a known
|
||||
// place.
|
||||
ui.item.find('a').trigger('focus');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invokes jQuery.sortable() on new buttons and groups in a CKEditor config.
|
||||
*/
|
||||
applySorting() {
|
||||
// Make the buttons sortable.
|
||||
this.$el.find('.ckeditor-buttons').not('.ui-sortable').sortable({
|
||||
// Change this to .ckeditor-toolbar-group-buttons.
|
||||
connectWith: '.ckeditor-buttons',
|
||||
placeholder: 'ckeditor-button-placeholder',
|
||||
forcePlaceholderSize: true,
|
||||
tolerance: 'pointer',
|
||||
cursor: 'move',
|
||||
start: this.startButtonDrag.bind(this),
|
||||
// Sorting within a sortable.
|
||||
stop: this.endButtonDrag.bind(this),
|
||||
}).disableSelection();
|
||||
|
||||
// Add the drag and drop functionality to button groups.
|
||||
this.$el.find('.ckeditor-toolbar-groups').not('.ui-sortable').sortable({
|
||||
connectWith: '.ckeditor-toolbar-groups',
|
||||
cancel: '.ckeditor-add-new-group',
|
||||
placeholder: 'ckeditor-toolbar-group-placeholder',
|
||||
forcePlaceholderSize: true,
|
||||
cursor: 'move',
|
||||
stop: this.endGroupDrag.bind(this),
|
||||
});
|
||||
|
||||
// Add the drag and drop functionality to buttons.
|
||||
this.$el.find('.ckeditor-multiple-buttons li').draggable({
|
||||
connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons',
|
||||
helper: 'clone',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps the invocation of methods to insert blank groups and rows.
|
||||
*/
|
||||
insertPlaceholders() {
|
||||
this.insertPlaceholderRow();
|
||||
this.insertNewGroupButtons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a blank row at the bottom of the CKEditor configuration.
|
||||
*/
|
||||
insertPlaceholderRow() {
|
||||
let $rows = this.$el.find('.ckeditor-row');
|
||||
// Add a placeholder row. to the end of the list if one does not exist.
|
||||
if (!$rows.eq(-1).hasClass('placeholder')) {
|
||||
this.$el
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.children('.ckeditor-active-toolbar-configuration')
|
||||
.append(Drupal.theme('ckeditorRow'));
|
||||
}
|
||||
// Update the $rows variable to include the new row.
|
||||
$rows = this.$el.find('.ckeditor-row');
|
||||
// Remove blank rows except the last one.
|
||||
const len = $rows.length;
|
||||
$rows.filter((index, row) => {
|
||||
// Do not remove the last row.
|
||||
if (index + 1 === len) {
|
||||
return false;
|
||||
}
|
||||
return $(row).find('.ckeditor-toolbar-group').not('.placeholder').length === 0;
|
||||
})
|
||||
// Then get all rows that are placeholders and remove them.
|
||||
.remove();
|
||||
},
|
||||
.find('[data-toolbar="active"]')
|
||||
.toggleClass('ckeditor-group-names-are-visible', groupNamesVisible);
|
||||
this.$el
|
||||
.find('.ckeditor-groupnames-toggle')
|
||||
.text(
|
||||
groupNamesVisible
|
||||
? Drupal.t('Hide group names')
|
||||
: Drupal.t('Show group names'),
|
||||
)
|
||||
.attr('aria-pressed', groupNamesVisible);
|
||||
|
||||
/**
|
||||
* Inserts a button in each row that will add a new CKEditor button group.
|
||||
*/
|
||||
insertNewGroupButtons() {
|
||||
// Insert an add group button to each row.
|
||||
this.$el.find('.ckeditor-row').each(function () {
|
||||
const $row = $(this);
|
||||
const $groups = $row.find('.ckeditor-toolbar-group');
|
||||
const $button = $row.find('.ckeditor-add-new-group');
|
||||
if ($button.length === 0) {
|
||||
$row.children('.ckeditor-toolbar-groups').append(Drupal.theme('ckeditorNewButtonGroup'));
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles clicks to a button group name.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event on the button group.
|
||||
*/
|
||||
onGroupNameClick(event) {
|
||||
const $group = $(event.currentTarget).closest(
|
||||
'.ckeditor-toolbar-group',
|
||||
);
|
||||
Drupal.ckeditor.openGroupNameDialog(this, $group);
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles clicks on the button group names toggle button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event on the toggle button.
|
||||
*/
|
||||
onGroupNamesToggleClick(event) {
|
||||
this.model.set(
|
||||
'groupNamesVisible',
|
||||
!this.model.get('groupNamesVisible'),
|
||||
);
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prompts the user to provide a name for a new button group; inserts it.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event of the button click.
|
||||
*/
|
||||
onAddGroupButtonClick(event) {
|
||||
/**
|
||||
* Inserts a new button if the openGroupNameDialog function returns true.
|
||||
*
|
||||
* @param {bool} success
|
||||
* A flag that indicates if the user created a new group (true) or
|
||||
* canceled out of the dialog (false).
|
||||
* @param {jQuery} $group
|
||||
* A jQuery DOM fragment that represents the new button group. It has
|
||||
* not been added to the DOM yet.
|
||||
*/
|
||||
function insertNewGroup(success, $group) {
|
||||
if (success) {
|
||||
$group.appendTo(
|
||||
$(event.currentTarget)
|
||||
.closest('.ckeditor-row')
|
||||
.children('.ckeditor-toolbar-groups'),
|
||||
);
|
||||
// Focus on the new group.
|
||||
$group.trigger('focus');
|
||||
}
|
||||
}
|
||||
// If a placeholder group exists, make sure it's at the end of the row.
|
||||
else if (!$groups.eq(-1).hasClass('ckeditor-add-new-group')) {
|
||||
$button.appendTo($row.children('.ckeditor-toolbar-groups'));
|
||||
|
||||
// Pass in a DOM fragment of a placeholder group so that the new group
|
||||
// name can be applied to it.
|
||||
Drupal.ckeditor.openGroupNameDialog(
|
||||
this,
|
||||
$(Drupal.theme('ckeditorToolbarGroup')),
|
||||
insertNewGroup,
|
||||
);
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable stop sort of a button group.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the group drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
endGroupDrag(event, ui) {
|
||||
const view = this;
|
||||
Drupal.ckeditor.registerGroupMove(this, ui.item, success => {
|
||||
if (!success) {
|
||||
// Cancel any sorting in the configuration area.
|
||||
view.$el
|
||||
.find('.ckeditor-toolbar-configuration')
|
||||
.find('.ui-sortable')
|
||||
.sortable('cancel');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable start sort of a button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the group drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
startButtonDrag(event, ui) {
|
||||
this.$el.find('a:focus').trigger('blur');
|
||||
|
||||
// Show the button group names as soon as the user starts dragging.
|
||||
this.model.set('groupNamesVisible', true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles jQuery Sortable stop sort of a button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The event triggered on the button drag.
|
||||
* @param {object} ui
|
||||
* A jQuery.ui.sortable argument that contains information about the
|
||||
* elements involved in the sort action.
|
||||
*/
|
||||
endButtonDrag(event, ui) {
|
||||
const view = this;
|
||||
Drupal.ckeditor.registerButtonMove(this, ui.item, success => {
|
||||
if (!success) {
|
||||
// Cancel any sorting in the configuration area.
|
||||
view.$el.find('.ui-sortable').sortable('cancel');
|
||||
}
|
||||
// Refocus the target button so that the user can continue from a known
|
||||
// place.
|
||||
ui.item.find('a').trigger('focus');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invokes jQuery.sortable() on new buttons and groups in a CKEditor config.
|
||||
*/
|
||||
applySorting() {
|
||||
// Make the buttons sortable.
|
||||
this.$el
|
||||
.find('.ckeditor-buttons')
|
||||
.not('.ui-sortable')
|
||||
.sortable({
|
||||
// Change this to .ckeditor-toolbar-group-buttons.
|
||||
connectWith: '.ckeditor-buttons',
|
||||
placeholder: 'ckeditor-button-placeholder',
|
||||
forcePlaceholderSize: true,
|
||||
tolerance: 'pointer',
|
||||
cursor: 'move',
|
||||
start: this.startButtonDrag.bind(this),
|
||||
// Sorting within a sortable.
|
||||
stop: this.endButtonDrag.bind(this),
|
||||
})
|
||||
.disableSelection();
|
||||
|
||||
// Add the drag and drop functionality to button groups.
|
||||
this.$el
|
||||
.find('.ckeditor-toolbar-groups')
|
||||
.not('.ui-sortable')
|
||||
.sortable({
|
||||
connectWith: '.ckeditor-toolbar-groups',
|
||||
cancel: '.ckeditor-add-new-group',
|
||||
placeholder: 'ckeditor-toolbar-group-placeholder',
|
||||
forcePlaceholderSize: true,
|
||||
cursor: 'move',
|
||||
stop: this.endGroupDrag.bind(this),
|
||||
});
|
||||
|
||||
// Add the drag and drop functionality to buttons.
|
||||
this.$el.find('.ckeditor-multiple-buttons li').draggable({
|
||||
connectToSortable: '.ckeditor-toolbar-active .ckeditor-buttons',
|
||||
helper: 'clone',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps the invocation of methods to insert blank groups and rows.
|
||||
*/
|
||||
insertPlaceholders() {
|
||||
this.insertPlaceholderRow();
|
||||
this.insertNewGroupButtons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a blank row at the bottom of the CKEditor configuration.
|
||||
*/
|
||||
insertPlaceholderRow() {
|
||||
let $rows = this.$el.find('.ckeditor-row');
|
||||
// Add a placeholder row. to the end of the list if one does not exist.
|
||||
if (!$rows.eq(-1).hasClass('placeholder')) {
|
||||
this.$el
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.children('.ckeditor-active-toolbar-configuration')
|
||||
.append(Drupal.theme('ckeditorRow'));
|
||||
}
|
||||
});
|
||||
// Update the $rows variable to include the new row.
|
||||
$rows = this.$el.find('.ckeditor-row');
|
||||
// Remove blank rows except the last one.
|
||||
const len = $rows.length;
|
||||
$rows
|
||||
.filter((index, row) => {
|
||||
// Do not remove the last row.
|
||||
if (index + 1 === len) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
$(row)
|
||||
.find('.ckeditor-toolbar-group')
|
||||
.not('.placeholder').length === 0
|
||||
);
|
||||
})
|
||||
// Then get all rows that are placeholders and remove them.
|
||||
.remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a button in each row that will add a new CKEditor button group.
|
||||
*/
|
||||
insertNewGroupButtons() {
|
||||
// Insert an add group button to each row.
|
||||
this.$el.find('.ckeditor-row').each(function() {
|
||||
const $row = $(this);
|
||||
const $groups = $row.find('.ckeditor-toolbar-group');
|
||||
const $button = $row.find('.ckeditor-add-new-group');
|
||||
if ($button.length === 0) {
|
||||
$row
|
||||
.children('.ckeditor-toolbar-groups')
|
||||
.append(Drupal.theme('ckeditorNewButtonGroup'));
|
||||
}
|
||||
// If a placeholder group exists, make sure it's at the end of the row.
|
||||
else if (!$groups.eq(-1).hasClass('ckeditor-add-new-group')) {
|
||||
$button.appendTo($row.children('.ckeditor-toolbar-groups'));
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}(Drupal, Backbone, jQuery));
|
||||
);
|
||||
})(Drupal, Backbone, jQuery);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
Drupal.ckeditor.VisualView = Backbone.View.extend({
|
||||
|
||||
events: {
|
||||
'click .ckeditor-toolbar-group-name': 'onGroupNameClick',
|
||||
'click .ckeditor-groupnames-toggle': 'onGroupNamesToggleClick',
|
||||
|
||||
@@ -70,7 +70,7 @@ class DrupalImageCaption extends PluginBase implements CKEditorPluginInterface,
|
||||
*/
|
||||
public function getCssFiles(Editor $editor) {
|
||||
return [
|
||||
drupal_get_path('module', 'ckeditor') . '/css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css'
|
||||
drupal_get_path('module', 'ckeditor') . '/css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class Language extends CKEditorPluginBase implements CKEditorPluginConfigurableI
|
||||
*/
|
||||
public function getCssFiles(Editor $editor) {
|
||||
return [
|
||||
drupal_get_path('module', 'ckeditor') . '/css/plugins/language/ckeditor.language.css'
|
||||
drupal_get_path('module', 'ckeditor') . '/css/plugins/language/ckeditor.language.css',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ class StylesCombo extends CKEditorPluginBase implements CKEditorPluginConfigurab
|
||||
];
|
||||
if (!empty($classes)) {
|
||||
$configured_style['attributes'] = [
|
||||
'class' => implode(' ', array_map('trim', $classes))
|
||||
'class' => implode(' ', array_map('trim', $classes)),
|
||||
];
|
||||
}
|
||||
$styles_set[] = $configured_style;
|
||||
|
||||
@@ -58,7 +58,7 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs a Drupal\Component\Plugin\PluginBase object.
|
||||
* Constructs a \Drupal\ckeditor\Plugin\Editor\CKEditor object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
@@ -210,8 +210,8 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
|
||||
0 => [
|
||||
'name' => 'All existing buttons',
|
||||
'items' => $all_buttons,
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'plugins' => $settings['plugins'],
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
* Contains client-side code for testing CSS delivered to CKEditor via AJAX.
|
||||
*/
|
||||
|
||||
(function (Drupal, ckeditor, editorSettings, $) {
|
||||
(function(Drupal, ckeditor, editorSettings, $) {
|
||||
Drupal.behaviors.ajaxCssForm = {
|
||||
|
||||
attach(context) {
|
||||
// Initialize an inline CKEditor on the #edit-inline element if it
|
||||
// isn't editable already.
|
||||
$(context)
|
||||
.find('#edit-inline')
|
||||
.not('[contenteditable]')
|
||||
.each(function () {
|
||||
.each(function() {
|
||||
ckeditor.attachInlineEditor(this, editorSettings.formats.test_format);
|
||||
});
|
||||
},
|
||||
};
|
||||
}(Drupal, Drupal.editors.ckeditor, drupalSettings.editor, jQuery));
|
||||
})(Drupal, Drupal.editors.ckeditor, drupalSettings.editor, jQuery);
|
||||
|
||||
@@ -32,7 +32,7 @@ class LlamaCss extends Llama implements CKEditorPluginButtonsInterface, CKEditor
|
||||
*/
|
||||
public function getCssFiles(Editor $editor) {
|
||||
return [
|
||||
drupal_get_path('module', 'ckeditor_test') . '/css/llama.css'
|
||||
drupal_get_path('module', 'ckeditor_test') . '/css/llama.css',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -216,6 +216,19 @@ class CKEditorAdminTest extends BrowserTestBase {
|
||||
$editor = Editor::load('filtered_html');
|
||||
$this->assertTrue($editor instanceof Editor, 'An Editor config entity exists.');
|
||||
$this->assertEqual($expected_settings, $editor->getSettings());
|
||||
|
||||
$this->drupalGet('admin/config/content/formats/add');
|
||||
// Now attempt to add another filter format with the same editor and same
|
||||
// machine name.
|
||||
$edit = [
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'editor[editor]' => 'ckeditor',
|
||||
];
|
||||
$this->submitForm($edit, 'editor_configure');
|
||||
$this->submitForm($edit, 'Save configuration');
|
||||
$this->assertResponse(200);
|
||||
$this->assertText('The machine-readable name is already in use. It must be unique.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,14 +4,14 @@ namespace Drupal\Tests\ckeditor\FunctionalJavascript;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
|
||||
/**
|
||||
* Tests delivery of CSS to CKEditor via AJAX.
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class AjaxCssTest extends JavascriptTestBase {
|
||||
class AjaxCssTest extends WebDriverTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
||||
@@ -7,7 +7,7 @@ use Drupal\editor\Entity\Editor;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ use Drupal\node\Entity\NodeType;
|
||||
*
|
||||
* @group ckeditor
|
||||
*/
|
||||
class CKEditorIntegrationTest extends JavascriptTestBase {
|
||||
class CKEditorIntegrationTest extends WebDriverTestBase {
|
||||
|
||||
/**
|
||||
* The account.
|
||||
@@ -92,9 +92,10 @@ class CKEditorIntegrationTest extends JavascriptTestBase {
|
||||
$session->getPage();
|
||||
|
||||
// Add a bottom margin to the title field to be sure the body field is not
|
||||
// visible. PhantomJS runs with a resolution of 1024x768px.
|
||||
$session->executeScript("document.getElementById('edit-title-0-value').style.marginBottom = '800px';");
|
||||
// visible.
|
||||
$session->executeScript("document.getElementById('edit-title-0-value').style.marginBottom = window.innerHeight*2 +'px';");
|
||||
|
||||
$this->assertSession()->waitForElementVisible('css', $ckeditor_id);
|
||||
// Check that the CKEditor-enabled body field is currently not visible in
|
||||
// the viewport.
|
||||
$web_assert->assertNotVisibleInViewport('css', $ckeditor_id, 'topLeft', 'CKEditor-enabled body field is visible.');
|
||||
|
||||
@@ -151,7 +151,7 @@ class CKEditorPluginManagerTest extends KernelTestBase {
|
||||
|
||||
// Case 2: CKEditor iframe instance CSS file.
|
||||
$expected = [
|
||||
'llama_css' => [drupal_get_path('module', 'ckeditor_test') . '/css/llama.css']
|
||||
'llama_css' => [drupal_get_path('module', 'ckeditor_test') . '/css/llama.css'],
|
||||
];
|
||||
$this->assertIdentical($expected, $this->manager->getCssFiles($editor), 'Iframe instance CSS file found.');
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class CKEditorTest extends KernelTestBase {
|
||||
/**
|
||||
* An instance of the "CKEditor" text editor plugin.
|
||||
*
|
||||
* @var \Drupal\ckeditor\Plugin\Editor\CKEditor;
|
||||
* @var \Drupal\ckeditor\Plugin\Editor\CKEditor
|
||||
*/
|
||||
protected $ckeditor;
|
||||
|
||||
@@ -50,7 +50,7 @@ class CKEditorTest extends KernelTestBase {
|
||||
'status' => 1,
|
||||
'settings' => [
|
||||
'allowed_html' => '<h2 id> <h3> <h4> <h5> <h6> <p> <br> <strong> <a href hreflang>',
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -20,7 +20,7 @@ class CKEditorPluginManagerTest extends UnitTestCase {
|
||||
return [
|
||||
'empty' => [
|
||||
[],
|
||||
[]
|
||||
[],
|
||||
],
|
||||
'1 row, 1 group' => [
|
||||
[
|
||||
@@ -28,9 +28,9 @@ class CKEditorPluginManagerTest extends UnitTestCase {
|
||||
[
|
||||
// Group 1.
|
||||
['name' => 'Formatting', 'items' => ['Bold', 'Italic']],
|
||||
]
|
||||
],
|
||||
],
|
||||
['Bold', 'Italic']
|
||||
['Bold', 'Italic'],
|
||||
],
|
||||
'1 row, >1 groups' => [
|
||||
[
|
||||
@@ -42,7 +42,7 @@ class CKEditorPluginManagerTest extends UnitTestCase {
|
||||
['name' => 'Linking', 'items' => ['Link']],
|
||||
],
|
||||
],
|
||||
['Bold', 'Italic', 'Link']
|
||||
['Bold', 'Italic', 'Link'],
|
||||
],
|
||||
'2 rows, 1 group each' => [
|
||||
[
|
||||
@@ -76,7 +76,7 @@ class CKEditorPluginManagerTest extends UnitTestCase {
|
||||
['name' => 'Advanced', 'items' => ['Llama']],
|
||||
],
|
||||
],
|
||||
['Bold', 'Italic', 'Link', 'Source', 'Llama']
|
||||
['Bold', 'Italic', 'Link', 'Source', 'Llama'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user