Update Composer, update everything
This commit is contained in:
parent
ea3e94409f
commit
dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions
|
@ -66,7 +66,7 @@ function template_preprocess_ckeditor_settings_toolbar(&$variables) {
|
|||
|
||||
$rtl = $language_interface->getDirection() === LanguageInterface::DIRECTION_RTL ? '_rtl' : '';
|
||||
|
||||
$build_button_item = function($button, $rtl) {
|
||||
$build_button_item = function ($button, $rtl) {
|
||||
// Value of the button item.
|
||||
if (isset($button['image_alternative' . $rtl])) {
|
||||
$value = $button['image_alternative' . $rtl];
|
||||
|
|
|
@ -5,4 +5,4 @@ package: Core
|
|||
core: 8.x
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- editor
|
||||
- drupal:editor
|
||||
|
|
|
@ -17,7 +17,7 @@ function ckeditor_help($route_name, RouteMatchInterface $route_match) {
|
|||
case 'help.page.ckeditor':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The CKEditor module provides a highly-accessible, highly-usable visual text editor and adds a toolbar to text fields. Users can use buttons to format content and to create semantically correct and valid HTML. The CKEditor module uses the framework provided by the <a href=":text_editor">Text Editor module</a>. It requires JavaScript to be enabled in the browser. For more information, see the <a href=":doc_url">online documentation for the CKEditor module</a> and the <a href=":cke_url">CKEditor website</a>.', [ ':doc_url' => 'https://www.drupal.org/documentation/modules/ckeditor', ':cke_url' => 'http://ckeditor.com', ':text_editor' => \Drupal::url('help.page', ['name' => 'editor'])]) . '</p>';
|
||||
$output .= '<p>' . t('The CKEditor module provides a highly-accessible, highly-usable visual text editor and adds a toolbar to text fields. Users can use buttons to format content and to create semantically correct and valid HTML. The CKEditor module uses the framework provided by the <a href=":text_editor">Text Editor module</a>. It requires JavaScript to be enabled in the browser. For more information, see the <a href=":doc_url">online documentation for the CKEditor module</a> and the <a href=":cke_url">CKEditor website</a>.', [':doc_url' => 'https://www.drupal.org/documentation/modules/ckeditor', ':cke_url' => 'http://ckeditor.com', ':text_editor' => \Drupal::url('help.page', ['name' => 'editor'])]) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Enabling CKEditor for individual text formats') . '</dt>';
|
||||
|
@ -29,7 +29,7 @@ function ckeditor_help($route_name, RouteMatchInterface $route_match) {
|
|||
$output .= '<dt>' . t('Toggling between formatted text and HTML source') . '</dt>';
|
||||
$output .= '<dd>' . t('If the <em>Source</em> button is available in the toolbar, users can click this button to disable the visual editor and edit the HTML source directly. After toggling back, the visual editor uses the allowed HTML tags to format the text — independent of whether buttons for these tags are available in the toolbar. If the text format is set to <em>limit the use of HTML tags</em>, then all excluded tags will be stripped out of the HTML source when the user toggles back to the text editor.') . '</dd>';
|
||||
$output .= '<dt>' . t('Check my spelling as I type') . '</dt>';
|
||||
$output .= '<dd>' . t('By default, CKEditor is configured to leverage your browser\'s spell check capability. Make sure your browser\'s spell checker is enabled in your browser\'s settings. To access suggested corrections for misspelled words, it may be necessary to hold the <em>Control</em> or <em>command</em> (Mac) key while right-clicking the misspelling.') . '</dd>';
|
||||
$output .= '<dd>' . t("By default, CKEditor is configured to leverage your browser's spell check capability. Make sure your browser's spell checker is enabled in your browser's settings. To access suggested corrections for misspelled words, it may be necessary to hold the <em>Control</em> or <em>command</em> (Mac) key while right-clicking the misspelling.") . '</dd>';
|
||||
$output .= '<dt>' . t('Accessibility features') . '</dt>';
|
||||
$output .= '<dd>' . t('The built in WYSIWYG editor (CKEditor) comes with a number of <a href=":features">accessibility features</a>. CKEditor comes with built in <a href=":shortcuts">keyboard shortcuts</a>, which can be beneficial for both power users and keyboard only users.', [':features' => 'http://docs.ckeditor.com/#!/guide/dev_a11y', ':shortcuts' => 'http://docs.ckeditor.com/#!/guide/dev_shortcuts']) . '</dd>';
|
||||
$output .= '<dt>' . t('Generating accessible content') . '</dt>';
|
||||
|
|
|
@ -16,7 +16,9 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
ol, ul, dl {
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
/* Preserved spaces for list items with text direction other than the list.
|
||||
* (CKEditor issues #6249,#8049) */
|
||||
padding: 0 40px;
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
* "moono".
|
||||
*/
|
||||
|
||||
|
||||
|
||||
.ckeditor-toolbar {
|
||||
border: 1px solid #b6b6b6;
|
||||
padding: 0.1667em 0.1667em 0.08em;
|
||||
|
@ -18,9 +16,9 @@
|
|||
margin: 5px 0;
|
||||
/* Disallow any user selections in the drag-and-drop toolbar config UI. */
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.ckeditor-toolbar-active {
|
||||
margin-top: 0.25em;
|
||||
|
@ -119,7 +117,7 @@
|
|||
margin: 3px 6px;
|
||||
padding: 3px;
|
||||
}
|
||||
.ckeditor-toolbar-configuration .fieldset-description{
|
||||
.ckeditor-toolbar-configuration .fieldset-description {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.ckeditor-toolbar-disabled .ckeditor-toolbar-available,
|
||||
|
@ -182,7 +180,7 @@
|
|||
padding: 4px 6px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 rgba(255,255,255,.5);
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ckeditor-toolbar-dividers {
|
||||
|
@ -298,7 +296,7 @@ ul.ckeditor-buttons li.ckeditor-button-separator a {
|
|||
height: 18px;
|
||||
width: 1px;
|
||||
display: block;
|
||||
box-shadow: 1px 0 1px rgba(255, 255, 255, 0.5)
|
||||
box-shadow: 1px 0 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.ckeditor-button-arrow {
|
||||
width: 0;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
.ckeditor-dialog-loading-link {
|
||||
border-radius: 0 0 5px 5px;
|
||||
border: 1px solid #B6B6B6;
|
||||
border: 1px solid #b6b6b6;
|
||||
border-top: none;
|
||||
background: white;
|
||||
padding: 3px 10px;
|
||||
|
|
555
web/core/modules/ckeditor/js/ckeditor.admin.es6.js
Normal file
555
web/core/modules/ckeditor/js/ckeditor.admin.es6.js
Normal file
|
@ -0,0 +1,555 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor button and group configuration user interface.
|
||||
*/
|
||||
|
||||
(function($, Drupal, drupalSettings, _) {
|
||||
Drupal.ckeditor = Drupal.ckeditor || {};
|
||||
|
||||
/**
|
||||
* Sets config behaviour and creates config views for the CKEditor toolbar.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches admin behaviour to the CKEditor buttons.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches admin behaviour from the CKEditor buttons on 'unload'.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorAdmin = {
|
||||
attach(context) {
|
||||
// Process the CKEditor configuration fragment once.
|
||||
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
|
||||
// CKEditor configuration.
|
||||
.find('.js-form-item-editor-settings-toolbar-button-groups')
|
||||
.hide()
|
||||
// Return the textarea child node from this expression.
|
||||
.find('textarea');
|
||||
|
||||
// The HTML for the CKEditor configuration is assembled on the server
|
||||
// and sent to the client as a serialized DOM fragment.
|
||||
$configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
|
||||
|
||||
// Create a configuration model.
|
||||
Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
|
||||
$textarea,
|
||||
activeEditorConfig: JSON.parse($textarea.val()),
|
||||
hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig,
|
||||
});
|
||||
|
||||
// Create the configuration Views.
|
||||
const viewDefaults = {
|
||||
model: Drupal.ckeditor.models.Model,
|
||||
el: $('.ckeditor-toolbar-configuration'),
|
||||
};
|
||||
Drupal.ckeditor.views = {
|
||||
controller: new Drupal.ckeditor.ControllerView(viewDefaults),
|
||||
visualView: new Drupal.ckeditor.VisualView(viewDefaults),
|
||||
keyboardView: new Drupal.ckeditor.KeyboardView(viewDefaults),
|
||||
auralView: new Drupal.ckeditor.AuralView(viewDefaults),
|
||||
};
|
||||
}
|
||||
},
|
||||
detach(context, settings, trigger) {
|
||||
// Early-return if the trigger for detachment is something else than
|
||||
// unload.
|
||||
if (trigger !== 'unload') {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're detaching because CKEditor as text editor has been disabled; this
|
||||
// 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 config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
|
||||
const buttons = Drupal.ckeditor.views.controller.getButtonList(config);
|
||||
const $activeToolbar = $('.ckeditor-toolbar-configuration').find(
|
||||
'.ckeditor-toolbar-active',
|
||||
);
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
$activeToolbar.trigger('CKEditorToolbarChanged', [
|
||||
'removed',
|
||||
buttons[i],
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* CKEditor configuration UI methods of Backbone objects.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.ckeditor = {
|
||||
/**
|
||||
* A hash of View instances.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
views: {},
|
||||
|
||||
/**
|
||||
* A hash of Model instances.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
models: {},
|
||||
|
||||
/**
|
||||
* Translates changes in CKEditor config DOM structure to the config model.
|
||||
*
|
||||
* If the button is moved within an existing group, the DOM structure is
|
||||
* simply translated to a configuration model. If the button is moved into a
|
||||
* new group placeholder, then a process is launched to name that group
|
||||
* before the button move is translated into configuration.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $button
|
||||
* A jQuery set that contains an li element that wraps a button element.
|
||||
* @param {function} callback
|
||||
* A callback to invoke after the button group naming modal dialog has
|
||||
* been closed.
|
||||
*
|
||||
*/
|
||||
registerButtonMove(view, $button, callback) {
|
||||
const $group = $button.closest('.ckeditor-toolbar-group');
|
||||
|
||||
// If dropped in a placeholder button group, the user must name it.
|
||||
if ($group.hasClass('placeholder')) {
|
||||
if (view.isProcessing) {
|
||||
return;
|
||||
}
|
||||
view.isProcessing = true;
|
||||
|
||||
Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
|
||||
} else {
|
||||
view.model.set('isDirty', true);
|
||||
callback(true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Translates changes in CKEditor config DOM structure to the config model.
|
||||
*
|
||||
* Each row has a placeholder group at the end of the row. A user may not
|
||||
* move an existing button group past the placeholder group at the end of a
|
||||
* row.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
*/
|
||||
registerGroupMove(view, $group) {
|
||||
// Remove placeholder classes if necessary.
|
||||
let $row = $group.closest('.ckeditor-row');
|
||||
if ($row.hasClass('placeholder')) {
|
||||
$row.removeClass('placeholder');
|
||||
}
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
view.model.set('isDirty', true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a dialog with a form for changing the title of a button group.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
* @param {function} callback
|
||||
* A callback to invoke after the button group naming modal dialog has
|
||||
* been closed.
|
||||
*/
|
||||
openGroupNameDialog(view, $group, callback) {
|
||||
callback = callback || function() {};
|
||||
|
||||
/**
|
||||
* Validates the string provided as a button group title.
|
||||
*
|
||||
* @param {HTMLElement} form
|
||||
* The form DOM element that contains the input with the new button
|
||||
* group title string.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true when an error exists, otherwise returns false.
|
||||
*/
|
||||
function validateForm(form) {
|
||||
if (form.elements[0].value.length === 0) {
|
||||
const $form = $(form);
|
||||
if (!$form.hasClass('errors')) {
|
||||
$form
|
||||
.addClass('errors')
|
||||
.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]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to close the dialog; Validates user input.
|
||||
*
|
||||
* @param {string} action
|
||||
* The dialog action chosen by the user: 'apply' or 'cancel'.
|
||||
* @param {HTMLElement} form
|
||||
* The form DOM element that contains the input with the new button
|
||||
* group title string.
|
||||
*/
|
||||
function closeDialog(action, form) {
|
||||
/**
|
||||
* 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
|
||||
// closed.
|
||||
delete view.isProcessing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a string as the name of a CKEditor button group.
|
||||
*
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of
|
||||
* buttons.
|
||||
* @param {string} name
|
||||
* The new name of the CKEditor button group.
|
||||
*/
|
||||
function namePlaceholderGroup($group, name) {
|
||||
// If it's currently still a placeholder, then that means we're
|
||||
// creating a new group, and we must do some extra work.
|
||||
if ($group.hasClass('placeholder')) {
|
||||
// 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, '-'),
|
||||
)}`;
|
||||
$group
|
||||
// Update the group container.
|
||||
.removeAttr('aria-label')
|
||||
.attr('data-drupal-ckeditor-type', 'group')
|
||||
.attr('tabindex', 0)
|
||||
// Update the group heading.
|
||||
.children('.ckeditor-toolbar-group-name')
|
||||
.attr('id', groupID)
|
||||
.end()
|
||||
// Update the group items.
|
||||
.children('.ckeditor-toolbar-group-buttons')
|
||||
.attr('aria-labelledby', groupID);
|
||||
}
|
||||
|
||||
$group
|
||||
.attr('data-drupal-ckeditor-toolbar-group-name', name)
|
||||
.children('.ckeditor-toolbar-group-name')
|
||||
.text(name);
|
||||
}
|
||||
|
||||
// Invoke a user-provided callback and indicate failure.
|
||||
if (action === 'cancel') {
|
||||
shutdown();
|
||||
callback(false, $group);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that a group name was provided.
|
||||
if (form && validateForm(form)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// React to application of a valid group name.
|
||||
if (action === 'apply') {
|
||||
shutdown();
|
||||
// Apply the provided name to the button group label.
|
||||
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');
|
||||
|
||||
// Invoke a user-provided callback and indicate success.
|
||||
callback(true, $group);
|
||||
|
||||
// Signal that the active toolbar DOM structure has changed.
|
||||
view.model.set('isDirty', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a Drupal dialog that will get a button group name from the user.
|
||||
const $ckeditorButtonGroupNameForm = $(
|
||||
Drupal.theme('ckeditorButtonGroupNameForm'),
|
||||
);
|
||||
const dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
|
||||
title: Drupal.t('Button group name'),
|
||||
dialogClass: 'ckeditor-name-toolbar-group',
|
||||
resizable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: Drupal.t('Apply'),
|
||||
click() {
|
||||
closeDialog('apply', this);
|
||||
},
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Cancel'),
|
||||
click() {
|
||||
closeDialog('cancel');
|
||||
},
|
||||
},
|
||||
],
|
||||
open() {
|
||||
const form = this;
|
||||
const $form = $(this);
|
||||
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 => {
|
||||
// React to enter key press.
|
||||
if (event.keyCode === 13) {
|
||||
const $target = $(event.currentTarget);
|
||||
const data = $target.data('ui-button');
|
||||
let action = 'apply';
|
||||
// Assume 'apply', but take into account that the user might have
|
||||
// pressed the enter key on the dialog buttons.
|
||||
if (data && data.options && data.options.label) {
|
||||
action = data.options.label.toLowerCase();
|
||||
}
|
||||
closeDialog(action, form);
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
// 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',
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
Drupal.announce(text);
|
||||
},
|
||||
close(event) {
|
||||
// Automatically destroy the DOM element that was used for the dialog.
|
||||
$(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'),
|
||||
)
|
||||
// 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');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically shows/hides settings of buttons-only CKEditor plugins.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches show/hide behaviour to Plugin Settings buttons.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
|
||||
attach(context) {
|
||||
const $context = $(context);
|
||||
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', []);
|
||||
});
|
||||
|
||||
// Whenever a button is added or removed, check if we should show or
|
||||
// hide the corresponding plugin settings. (Note that upon
|
||||
// initialization, each button that already is part of the toolbar still
|
||||
// is considered "added", hence it also works correctly for buttons that
|
||||
// were added previously.)
|
||||
$context
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
|
||||
.on(
|
||||
'CKEditorToolbarChanged.ckeditorAdminPluginSettings',
|
||||
(event, action, button) => {
|
||||
const $pluginSettings = $ckeditorPluginSettings.find(
|
||||
`[data-ckeditor-buttons~=${button}]`,
|
||||
);
|
||||
|
||||
// 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) {
|
||||
if (verticalTab) {
|
||||
verticalTab.tabHide();
|
||||
} else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
$pluginSettings.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
$pluginSettings.data(
|
||||
'ckeditorButtonPluginSettingsActiveButtons',
|
||||
activeButtons,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a blank CKEditor row.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for a CKEditor row.
|
||||
*/
|
||||
Drupal.theme.ckeditorRow = function() {
|
||||
return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a blank CKEditor button group.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for a CKEditor button group.
|
||||
*/
|
||||
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>';
|
||||
return group;
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a form for changing the title of a CKEditor button group.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for the form for the title of a CKEditor button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNameForm = function() {
|
||||
return '<form><input name="group-name" required="required"></form>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a button that will toggle the button group names in active config.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for the button to toggle group names.
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNamesToggle = function() {
|
||||
return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a button that will prompt the user to name a new button group.
|
||||
*
|
||||
* @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>`;
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
|
@ -1,51 +1,29 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor button and group configuration user interface.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor = Drupal.ckeditor || {};
|
||||
|
||||
/**
|
||||
* Sets config behaviour and creates config views for the CKEditor toolbar.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches admin behaviour to the CKEditor buttons.
|
||||
* @prop {Drupal~behaviorDetach} detach
|
||||
* Detaches admin behaviour from the CKEditor buttons on 'unload'.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorAdmin = {
|
||||
attach: function (context) {
|
||||
// Process the CKEditor configuration fragment once.
|
||||
attach: function attach(context) {
|
||||
var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-configuration');
|
||||
if ($configurationForm.length) {
|
||||
var $textarea = $configurationForm
|
||||
// Hide the textarea that contains the serialized representation of the
|
||||
// CKEditor configuration.
|
||||
.find('.js-form-item-editor-settings-toolbar-button-groups')
|
||||
.hide()
|
||||
// Return the textarea child node from this expression.
|
||||
.find('textarea');
|
||||
var $textarea = $configurationForm.find('.js-form-item-editor-settings-toolbar-button-groups').hide().find('textarea');
|
||||
|
||||
// The HTML for the CKEditor configuration is assembled on the server
|
||||
// and sent to the client as a serialized DOM fragment.
|
||||
$configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
|
||||
|
||||
// Create a configuration model.
|
||||
var model = Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
|
||||
Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
|
||||
$textarea: $textarea,
|
||||
activeEditorConfig: JSON.parse($textarea.val()),
|
||||
hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig
|
||||
});
|
||||
|
||||
// Create the configuration Views.
|
||||
var viewDefaults = {
|
||||
model: model,
|
||||
model: Drupal.ckeditor.models.Model,
|
||||
el: $('.ckeditor-toolbar-configuration')
|
||||
};
|
||||
Drupal.ckeditor.views = {
|
||||
|
@ -56,17 +34,11 @@
|
|||
};
|
||||
}
|
||||
},
|
||||
detach: function (context, settings, trigger) {
|
||||
// Early-return if the trigger for detachment is something else than
|
||||
// unload.
|
||||
detach: function detach(context, settings, trigger) {
|
||||
if (trigger !== 'unload') {
|
||||
return;
|
||||
}
|
||||
|
||||
// We're detaching because CKEditor as text editor has been disabled; this
|
||||
// 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.
|
||||
var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').findOnce('ckeditor-configuration');
|
||||
if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.Model) {
|
||||
var config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
|
||||
|
@ -79,48 +51,14 @@
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CKEditor configuration UI methods of Backbone objects.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.ckeditor = {
|
||||
|
||||
/**
|
||||
* A hash of View instances.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
views: {},
|
||||
|
||||
/**
|
||||
* A hash of Model instances.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
models: {},
|
||||
|
||||
/**
|
||||
* Translates changes in CKEditor config DOM structure to the config model.
|
||||
*
|
||||
* If the button is moved within an existing group, the DOM structure is
|
||||
* simply translated to a configuration model. If the button is moved into a
|
||||
* new group placeholder, then a process is launched to name that group
|
||||
* before the button move is translated into configuration.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $button
|
||||
* A jQuery set that contains an li element that wraps a button element.
|
||||
* @param {function} callback
|
||||
* A callback to invoke after the button group naming modal dialog has
|
||||
* been closed.
|
||||
*
|
||||
*/
|
||||
registerButtonMove: function (view, $button, callback) {
|
||||
registerButtonMove: function registerButtonMove(view, $button, callback) {
|
||||
var $group = $button.closest('.ckeditor-toolbar-group');
|
||||
|
||||
// If dropped in a placeholder button group, the user must name it.
|
||||
if ($group.hasClass('placeholder')) {
|
||||
if (view.isProcessing) {
|
||||
return;
|
||||
|
@ -128,33 +66,17 @@
|
|||
view.isProcessing = true;
|
||||
|
||||
Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
view.model.set('isDirty', true);
|
||||
callback(true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Translates changes in CKEditor config DOM structure to the config model.
|
||||
*
|
||||
* Each row has a placeholder group at the end of the row. A user may not
|
||||
* move an existing button group past the placeholder group at the end of a
|
||||
* row.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
*/
|
||||
registerGroupMove: function (view, $group) {
|
||||
// Remove placeholder classes if necessary.
|
||||
registerGroupMove: function registerGroupMove(view, $group) {
|
||||
var $row = $group.closest('.ckeditor-row');
|
||||
if ($row.hasClass('placeholder')) {
|
||||
$row.removeClass('placeholder');
|
||||
}
|
||||
// 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) {
|
||||
|
@ -163,170 +85,90 @@
|
|||
});
|
||||
view.model.set('isDirty', true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a dialog with a form for changing the title of a button group.
|
||||
*
|
||||
* @param {Backbone.View} view
|
||||
* The Backbone View that invoked this function.
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of buttons.
|
||||
* @param {function} callback
|
||||
* A callback to invoke after the button group naming modal dialog has
|
||||
* been closed.
|
||||
*/
|
||||
openGroupNameDialog: function (view, $group, callback) {
|
||||
openGroupNameDialog: function openGroupNameDialog(view, $group, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
/**
|
||||
* Validates the string provided as a button group title.
|
||||
*
|
||||
* @param {HTMLElement} form
|
||||
* The form DOM element that contains the input with the new button
|
||||
* group title string.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true when an error exists, otherwise returns false.
|
||||
*/
|
||||
function validateForm(form) {
|
||||
if (form.elements[0].value.length === 0) {
|
||||
var $form = $(form);
|
||||
if (!$form.hasClass('errors')) {
|
||||
$form
|
||||
.addClass('errors')
|
||||
.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]);
|
||||
$form.addClass('errors').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]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to close the dialog; Validates user input.
|
||||
*
|
||||
* @param {string} action
|
||||
* The dialog action chosen by the user: 'apply' or 'cancel'.
|
||||
* @param {HTMLElement} form
|
||||
* The form DOM element that contains the input with the new button
|
||||
* group title string.
|
||||
*/
|
||||
function closeDialog(action, form) {
|
||||
|
||||
/**
|
||||
* Closes the dialog when the user cancels or supplies valid data.
|
||||
*/
|
||||
function shutdown() {
|
||||
dialog.close(action);
|
||||
|
||||
// The processing marker can be deleted since the dialog has been
|
||||
// closed.
|
||||
delete view.isProcessing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a string as the name of a CKEditor button group.
|
||||
*
|
||||
* @param {jQuery} $group
|
||||
* A jQuery set that contains an li element that wraps a group of
|
||||
* buttons.
|
||||
* @param {string} name
|
||||
* The new name of the CKEditor button group.
|
||||
*/
|
||||
function namePlaceholderGroup($group, name) {
|
||||
// If it's currently still a placeholder, then that means we're
|
||||
// creating a new group, and we must do some extra work.
|
||||
if ($group.hasClass('placeholder')) {
|
||||
// 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.
|
||||
var groupID = 'ckeditor-toolbar-group-aria-label-for-' + Drupal.checkPlain(name.toLowerCase().replace(/\s/g, '-'));
|
||||
$group
|
||||
// Update the group container.
|
||||
.removeAttr('aria-label')
|
||||
.attr('data-drupal-ckeditor-type', 'group')
|
||||
.attr('tabindex', 0)
|
||||
// Update the group heading.
|
||||
.children('.ckeditor-toolbar-group-name')
|
||||
.attr('id', groupID)
|
||||
.end()
|
||||
// Update the group items.
|
||||
.children('.ckeditor-toolbar-group-buttons')
|
||||
.attr('aria-labelledby', groupID);
|
||||
$group.removeAttr('aria-label').attr('data-drupal-ckeditor-type', 'group').attr('tabindex', 0).children('.ckeditor-toolbar-group-name').attr('id', groupID).end().children('.ckeditor-toolbar-group-buttons').attr('aria-labelledby', groupID);
|
||||
}
|
||||
|
||||
$group
|
||||
.attr('data-drupal-ckeditor-toolbar-group-name', name)
|
||||
.children('.ckeditor-toolbar-group-name')
|
||||
.text(name);
|
||||
$group.attr('data-drupal-ckeditor-toolbar-group-name', name).children('.ckeditor-toolbar-group-name').text(name);
|
||||
}
|
||||
|
||||
// Invoke a user-provided callback and indicate failure.
|
||||
if (action === 'cancel') {
|
||||
shutdown();
|
||||
callback(false, $group);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that a group name was provided.
|
||||
if (form && validateForm(form)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// React to application of a valid group name.
|
||||
if (action === 'apply') {
|
||||
shutdown();
|
||||
// Apply the provided name to the button group label.
|
||||
|
||||
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');
|
||||
|
||||
// Invoke a user-provided callback and indicate success.
|
||||
callback(true, $group);
|
||||
|
||||
// Signal that the active toolbar DOM structure has changed.
|
||||
view.model.set('isDirty', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a Drupal dialog that will get a button group name from the user.
|
||||
var $ckeditorButtonGroupNameForm = $(Drupal.theme('ckeditorButtonGroupNameForm'));
|
||||
var dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
|
||||
title: Drupal.t('Button group name'),
|
||||
dialogClass: 'ckeditor-name-toolbar-group',
|
||||
resizable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: Drupal.t('Apply'),
|
||||
click: function () {
|
||||
closeDialog('apply', this);
|
||||
},
|
||||
primary: true
|
||||
buttons: [{
|
||||
text: Drupal.t('Apply'),
|
||||
click: function click() {
|
||||
closeDialog('apply', this);
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Cancel'),
|
||||
click: function () {
|
||||
closeDialog('cancel');
|
||||
}
|
||||
|
||||
primary: true
|
||||
}, {
|
||||
text: Drupal.t('Cancel'),
|
||||
click: function click() {
|
||||
closeDialog('cancel');
|
||||
}
|
||||
],
|
||||
open: function () {
|
||||
}],
|
||||
open: function open() {
|
||||
var form = this;
|
||||
var $form = $(this);
|
||||
var $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', function (event) {
|
||||
// React to enter key press.
|
||||
if (event.keyCode === 13) {
|
||||
var $target = $(event.currentTarget);
|
||||
var data = $target.data('ui-button');
|
||||
var action = 'apply';
|
||||
// Assume 'apply', but take into account that the user might have
|
||||
// pressed the enter key on the dialog buttons.
|
||||
|
||||
if (data && data.options && data.options.label) {
|
||||
action = data.options.label.toLowerCase();
|
||||
}
|
||||
|
@ -336,7 +178,7 @@
|
|||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
// Announce to the user that a modal dialog is open.
|
||||
|
||||
var 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.', {
|
||||
|
@ -345,118 +187,70 @@
|
|||
}
|
||||
Drupal.announce(text);
|
||||
},
|
||||
close: function (event) {
|
||||
// Automatically destroy the DOM element that was used for the dialog.
|
||||
close: function close(event) {
|
||||
$(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'))
|
||||
// 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');
|
||||
$(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input')).attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name')).trigger('focus');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically shows/hides settings of buttons-only CKEditor plugins.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches show/hide behaviour to Plugin Settings buttons.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
|
||||
attach: function (context) {
|
||||
attach: function attach(context) {
|
||||
var $context = $(context);
|
||||
var $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 () {
|
||||
var $this = $(this);
|
||||
if ($this.data('verticalTab')) {
|
||||
$this.data('verticalTab').tabHide();
|
||||
}
|
||||
else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
} else {
|
||||
$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
|
||||
// initialization, each button that already is part of the toolbar still
|
||||
// is considered "added", hence it also works correctly for buttons that
|
||||
// were added previously.)
|
||||
$context
|
||||
.find('.ckeditor-toolbar-active')
|
||||
.off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
|
||||
.on('CKEditorToolbarChanged.ckeditorAdminPluginSettings', function (event, action, button) {
|
||||
var $pluginSettings = $ckeditorPluginSettings
|
||||
.find('[data-ckeditor-buttons~=' + button + ']');
|
||||
$context.find('.ckeditor-toolbar-active').off('CKEditorToolbarChanged.ckeditorAdminPluginSettings').on('CKEditorToolbarChanged.ckeditorAdminPluginSettings', function (event, action, button) {
|
||||
var $pluginSettings = $ckeditorPluginSettings.find('[data-ckeditor-buttons~=' + button + ']');
|
||||
|
||||
// No settings for this button.
|
||||
if ($pluginSettings.length === 0) {
|
||||
return;
|
||||
if ($pluginSettings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var verticalTab = $pluginSettings.data('verticalTab');
|
||||
var activeButtons = $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons');
|
||||
if (action === 'added') {
|
||||
activeButtons.push(button);
|
||||
|
||||
if (verticalTab) {
|
||||
verticalTab.tabShow();
|
||||
} else {
|
||||
$pluginSettings.show();
|
||||
}
|
||||
} else {
|
||||
activeButtons.splice(activeButtons.indexOf(button), 1);
|
||||
|
||||
var verticalTab = $pluginSettings.data('verticalTab');
|
||||
var activeButtons = $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons');
|
||||
if (action === 'added') {
|
||||
activeButtons.push(button);
|
||||
// Show this plugin's settings if >=1 of its buttons are active.
|
||||
if (activeButtons.length === 0) {
|
||||
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) {
|
||||
if (verticalTab) {
|
||||
verticalTab.tabHide();
|
||||
}
|
||||
else {
|
||||
// On very narrow viewports, Vertical Tabs are disabled.
|
||||
$pluginSettings.hide();
|
||||
}
|
||||
verticalTab.tabHide();
|
||||
} else {
|
||||
$pluginSettings.hide();
|
||||
}
|
||||
}
|
||||
$pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons', activeButtons);
|
||||
});
|
||||
}
|
||||
$pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons', activeButtons);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a blank CKEditor row.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for a CKEditor row.
|
||||
*/
|
||||
Drupal.theme.ckeditorRow = function () {
|
||||
return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a blank CKEditor button group.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for a CKEditor button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorToolbarGroup = function () {
|
||||
var group = '';
|
||||
group += '<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="' + Drupal.t('Place a button to create a new button group.') + '">';
|
||||
|
@ -466,34 +260,15 @@
|
|||
return group;
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a form for changing the title of a CKEditor button group.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for the form for the title of a CKEditor button group.
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNameForm = function () {
|
||||
return '<form><input name="group-name" required="required"></form>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a button that will toggle the button group names in active config.
|
||||
*
|
||||
* @return {string}
|
||||
* A HTML string for the button to toggle group names.
|
||||
*/
|
||||
Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
|
||||
return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Themes a button that will prompt the user to name a new button group.
|
||||
*
|
||||
* @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>';
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor 'drupalimage' plugin admin behavior.
|
||||
*/
|
||||
|
||||
(function($, Drupal, drupalSettings) {
|
||||
/**
|
||||
* Provides the summary for the "drupalimage" plugin settings vertical tab.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behaviour to the "drupalimage" settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorDrupalImageSettingsSummary = {
|
||||
attach() {
|
||||
$('[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()})`
|
||||
: '';
|
||||
|
||||
if (!$status.is(':checked')) {
|
||||
return Drupal.t('Uploads disabled');
|
||||
}
|
||||
|
||||
let output = '';
|
||||
output += Drupal.t('Uploads enabled, max size: @size @dimensions', {
|
||||
'@size': maxFileSize,
|
||||
'@dimensions': maxDimensions,
|
||||
});
|
||||
if ($scheme.length) {
|
||||
output += `<br />${$scheme.attr('data-label')}`;
|
||||
}
|
||||
return output;
|
||||
});
|
||||
},
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings);
|
|
@ -1,22 +1,13 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor 'drupalimage' plugin admin behavior.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides the summary for the "drupalimage" plugin settings vertical tab.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behaviour to the "drupalimage" settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorDrupalImageSettingsSummary = {
|
||||
attach: function () {
|
||||
attach: function attach() {
|
||||
$('[data-ckeditor-plugin-id="drupalimage"]').drupalSetSummary(function (context) {
|
||||
var root = 'input[name="editor[settings][plugins][drupalimage][image_upload]';
|
||||
var $status = $(root + '[status]"]');
|
||||
|
@ -26,14 +17,17 @@
|
|||
var $scheme = $(root + '[scheme]"]:checked');
|
||||
|
||||
var maxFileSize = $maxFileSize.val() ? $maxFileSize.val() : $maxFileSize.attr('placeholder');
|
||||
var maxDimensions = ($maxWidth.val() && $maxHeight.val()) ? '(' + $maxWidth.val() + 'x' + $maxHeight.val() + ')' : '';
|
||||
var maxDimensions = $maxWidth.val() && $maxHeight.val() ? '(' + $maxWidth.val() + 'x' + $maxHeight.val() + ')' : '';
|
||||
|
||||
if (!$status.is(':checked')) {
|
||||
return Drupal.t('Uploads disabled');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
@ -41,5 +35,4 @@
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings);
|
||||
})(jQuery, Drupal, drupalSettings);
|
390
web/core/modules/ckeditor/js/ckeditor.es6.js
Normal file
390
web/core/modules/ckeditor/js/ckeditor.es6.js
Normal file
|
@ -0,0 +1,390 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor implementation of {@link Drupal.editors} API.
|
||||
*/
|
||||
|
||||
(function(Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) {
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editors.ckeditor = {
|
||||
/**
|
||||
* Editor attach callback.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to attach the editor to.
|
||||
* @param {string} format
|
||||
* The text format for the editor.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.replace()` created an editor or not.
|
||||
*/
|
||||
attach(element, format) {
|
||||
this._loadExternalPlugins(format);
|
||||
// Also pass settings that are Drupal-specific.
|
||||
format.editorSettings.drupal = {
|
||||
format: format.format,
|
||||
};
|
||||
|
||||
// Set a title on the CKEditor instance that includes the text field's
|
||||
// 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,
|
||||
});
|
||||
|
||||
return !!CKEDITOR.replace(element, format.editorSettings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Editor detach callback.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to detach the editor from.
|
||||
* @param {string} format
|
||||
* The text format used for the editor.
|
||||
* @param {string} trigger
|
||||
* The event trigger for the detach.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
|
||||
* found an editor or not.
|
||||
*/
|
||||
detach(element, format, trigger) {
|
||||
const editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
if (trigger === 'serialize') {
|
||||
editor.updateElement();
|
||||
} else {
|
||||
editor.destroy();
|
||||
element.removeAttribute('contentEditable');
|
||||
}
|
||||
}
|
||||
return !!editor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reacts on a change in the editor element.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element where the change occurred.
|
||||
* @param {function} callback
|
||||
* Callback called with the value of the editor.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
|
||||
* found an editor or not.
|
||||
*/
|
||||
onChange(element, callback) {
|
||||
const editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
editor.on(
|
||||
'change',
|
||||
debounce(() => {
|
||||
callback(editor.getData());
|
||||
}, 400),
|
||||
);
|
||||
|
||||
// A temporary workaround to control scrollbar appearance when using
|
||||
// autoGrow event to control editor's height.
|
||||
// @todo Remove when http://dev.ckeditor.com/ticket/12120 is fixed.
|
||||
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();
|
||||
|
||||
if (scrollable.$.scrollHeight < scrollable.$.clientHeight) {
|
||||
scrollable.setStyle('overflow-y', 'hidden');
|
||||
} else {
|
||||
scrollable.removeStyle('overflow-y');
|
||||
}
|
||||
},
|
||||
null,
|
||||
null,
|
||||
10000,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return !!editor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Attaches an inline editor to a DOM element.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to attach the editor to.
|
||||
* @param {object} format
|
||||
* The text format used in the editor.
|
||||
* @param {string} [mainToolbarId]
|
||||
* The id attribute for the main editor toolbar, if any.
|
||||
* @param {string} [floatedToolbarId]
|
||||
* The id attribute for the floated editor toolbar, if any.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.replace()` created an editor or not.
|
||||
*/
|
||||
attachInlineEditor(element, format, mainToolbarId, floatedToolbarId) {
|
||||
this._loadExternalPlugins(format);
|
||||
// Also pass settings that are Drupal-specific.
|
||||
format.editorSettings.drupal = {
|
||||
format: format.format,
|
||||
};
|
||||
|
||||
const settings = $.extend(true, {}, format.editorSettings);
|
||||
|
||||
// If a toolbar is already provided for "true WYSIWYG" (in-place editing),
|
||||
// then use that toolbar instead: override the default settings to render
|
||||
// CKEditor UI's top toolbar into mainToolbar, and don't render the bottom
|
||||
// toolbar at all. (CKEditor doesn't need a floated toolbar.)
|
||||
if (mainToolbarId) {
|
||||
const settingsOverride = {
|
||||
extraPlugins: 'sharedspace',
|
||||
removePlugins: 'floatingspace,elementspath',
|
||||
sharedSpaces: {
|
||||
top: mainToolbarId,
|
||||
},
|
||||
};
|
||||
|
||||
// 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++
|
||||
) {
|
||||
if (settings.toolbar[i] !== '/') {
|
||||
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.
|
||||
settings.toolbar[i].items[j] = 'Sourcedialog';
|
||||
settingsOverride.extraPlugins += ',sourcedialog';
|
||||
settingsOverride.removePlugins += ',sourcearea';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings.extraPlugins += `,${settingsOverride.extraPlugins}`;
|
||||
settings.removePlugins += `,${settingsOverride.removePlugins}`;
|
||||
settings.sharedSpaces = settingsOverride.sharedSpaces;
|
||||
}
|
||||
|
||||
// CKEditor requires an element to already have the contentEditable
|
||||
// attribute set to "true", otherwise it won't attach an inline editor.
|
||||
element.setAttribute('contentEditable', 'true');
|
||||
|
||||
return !!CKEDITOR.inline(element, settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the required external plugins for the editor.
|
||||
*
|
||||
* @param {object} format
|
||||
* The text format used in the editor.
|
||||
*/
|
||||
_loadExternalPlugins(format) {
|
||||
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],
|
||||
'',
|
||||
);
|
||||
});
|
||||
delete format.editorSettings.drupalExternalPlugins;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Drupal.ckeditor = {
|
||||
/**
|
||||
* Variable storing the current dialog's save callback.
|
||||
*
|
||||
* @type {?function}
|
||||
*/
|
||||
saveCallback: null,
|
||||
|
||||
/**
|
||||
* Open a dialog for a Drupal-based plugin.
|
||||
*
|
||||
* This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
|
||||
* framework, then opens a dialog at the specified Drupal path.
|
||||
*
|
||||
* @param {CKEditor} editor
|
||||
* The CKEditor instance that is opening the dialog.
|
||||
* @param {string} url
|
||||
* The URL that contains the contents of the dialog.
|
||||
* @param {object} existingValues
|
||||
* Existing values that will be sent via POST to the url for the dialog
|
||||
* contents.
|
||||
* @param {function} saveCallback
|
||||
* A function to be called upon saving the dialog.
|
||||
* @param {object} dialogSettings
|
||||
* An object containing settings to be passed to the jQuery UI.
|
||||
*/
|
||||
openDialog(editor, url, existingValues, saveCallback, dialogSettings) {
|
||||
// Locate a suitable place to display our loading indicator.
|
||||
let $target = $(editor.container.$);
|
||||
if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) {
|
||||
$target = $target.find('.cke_contents');
|
||||
}
|
||||
|
||||
// Remove any previous loading indicator.
|
||||
$target
|
||||
.css('position', 'relative')
|
||||
.find('.ckeditor-dialog-loading')
|
||||
.remove();
|
||||
|
||||
// Add a consistent dialog class.
|
||||
const classes = dialogSettings.dialogClass
|
||||
? dialogSettings.dialogClass.split(' ')
|
||||
: [];
|
||||
classes.push('ui-dialog--narrow');
|
||||
dialogSettings.dialogClass = classes.join(' ');
|
||||
dialogSettings.autoResize = window.matchMedia(
|
||||
'(min-width: 600px)',
|
||||
).matches;
|
||||
dialogSettings.width = 'auto';
|
||||
|
||||
// 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>`,
|
||||
);
|
||||
$content.appendTo($target);
|
||||
|
||||
const ckeditorAjaxDialog = Drupal.ajax({
|
||||
dialog: dialogSettings,
|
||||
dialogType: 'modal',
|
||||
selector: '.ckeditor-dialog-loading-link',
|
||||
url,
|
||||
progress: { type: 'throbber' },
|
||||
submit: {
|
||||
editor_object: existingValues,
|
||||
},
|
||||
});
|
||||
ckeditorAjaxDialog.execute();
|
||||
|
||||
// After a short delay, show "Loading…" message.
|
||||
window.setTimeout(() => {
|
||||
$content.find('span').animate({ top: '0px' });
|
||||
}, 1000);
|
||||
|
||||
// Store the save callback to be executed when this dialog is closed.
|
||||
Drupal.ckeditor.saveCallback = saveCallback;
|
||||
},
|
||||
};
|
||||
|
||||
// Moves the dialog to the top of the CKEDITOR stack.
|
||||
$(window).on('dialogcreate', (e, dialog, $element, settings) => {
|
||||
$('.ui-dialog--narrow').css('zIndex', CKEDITOR.config.baseFloatZIndex + 1);
|
||||
});
|
||||
|
||||
// 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() {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Respond to dialogs that are saved, sending data back to CKEditor.
|
||||
$(window).on('editor:dialogsave', (e, values) => {
|
||||
if (Drupal.ckeditor.saveCallback) {
|
||||
Drupal.ckeditor.saveCallback(values);
|
||||
}
|
||||
});
|
||||
|
||||
// Respond to dialogs that are closed, removing the current save handler.
|
||||
$(window).on('dialog:afterclose', (e, dialog, $element) => {
|
||||
if (Drupal.ckeditor.saveCallback) {
|
||||
Drupal.ckeditor.saveCallback = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Redirect on hash change when the original hash has an associated CKEditor.
|
||||
function redirectTextareaFragmentToCKEditorInstance() {
|
||||
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');
|
||||
window.location.replace(`#${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
$(window).on(
|
||||
'hashchange.ckeditor',
|
||||
redirectTextareaFragmentToCKEditorInstance,
|
||||
);
|
||||
|
||||
// Set autoGrow to make the editor grow the moment it is created.
|
||||
CKEDITOR.config.autoGrow_onStartup = true;
|
||||
|
||||
// Set the CKEditor cache-busting string to the same value as Drupal.
|
||||
CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp;
|
||||
|
||||
if (AjaxCommands) {
|
||||
/**
|
||||
* Command to add style sheets to a CKEditor instance.
|
||||
*
|
||||
* Works for both iframe and inline CKEditor instances.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
|
||||
* @param {object} response
|
||||
* The response from the Ajax request.
|
||||
* @param {string} response.editor_id
|
||||
* The CKEditor instance ID.
|
||||
* @param {number} [status]
|
||||
* The XMLHttpRequest status.
|
||||
*
|
||||
* @see http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document
|
||||
*/
|
||||
AjaxCommands.prototype.ckeditor_add_stylesheet = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
const editor = CKEDITOR.instances[response.editor_id];
|
||||
|
||||
if (editor) {
|
||||
response.stylesheets.forEach(url => {
|
||||
editor.document.appendStyleSheet(url);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
})(
|
||||
Drupal,
|
||||
Drupal.debounce,
|
||||
CKEDITOR,
|
||||
jQuery,
|
||||
Drupal.displace,
|
||||
Drupal.AjaxCommands,
|
||||
);
|
199
web/core/modules/ckeditor/js/ckeditor.js
vendored
199
web/core/modules/ckeditor/js/ckeditor.js
vendored
|
@ -1,94 +1,45 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor implementation of {@link Drupal.editors} API.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editors.ckeditor = {
|
||||
|
||||
/**
|
||||
* Editor attach callback.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to attach the editor to.
|
||||
* @param {string} format
|
||||
* The text format for the editor.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.replace()` created an editor or not.
|
||||
*/
|
||||
attach: function (element, format) {
|
||||
attach: function attach(element, format) {
|
||||
this._loadExternalPlugins(format);
|
||||
// Also pass settings that are Drupal-specific.
|
||||
|
||||
format.editorSettings.drupal = {
|
||||
format: format.format
|
||||
};
|
||||
|
||||
// Set a title on the CKEditor instance that includes the text field's
|
||||
// label so that screen readers say something that is understandable
|
||||
// for end users.
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Editor detach callback.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to detach the editor from.
|
||||
* @param {string} format
|
||||
* The text format used for the editor.
|
||||
* @param {string} trigger
|
||||
* The event trigger for the detach.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
|
||||
* found an editor or not.
|
||||
*/
|
||||
detach: function (element, format, trigger) {
|
||||
detach: function detach(element, format, trigger) {
|
||||
var editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
if (trigger === 'serialize') {
|
||||
editor.updateElement();
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
editor.destroy();
|
||||
element.removeAttribute('contentEditable');
|
||||
}
|
||||
}
|
||||
return !!editor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reacts on a change in the editor element.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element where the change occured.
|
||||
* @param {function} callback
|
||||
* Callback called with the value of the editor.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
|
||||
* found an editor or not.
|
||||
*/
|
||||
onChange: function (element, callback) {
|
||||
onChange: function onChange(element, callback) {
|
||||
var editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
editor.on('change', debounce(function () {
|
||||
callback(editor.getData());
|
||||
}, 400));
|
||||
|
||||
// A temporary workaround to control scrollbar appearance when using
|
||||
// autoGrow event to control editor's height.
|
||||
// @todo Remove when http://dev.ckeditor.com/ticket/12120 is fixed.
|
||||
editor.on('mode', function () {
|
||||
var editable = editor.editable();
|
||||
if (!editable.isInline()) {
|
||||
|
@ -98,8 +49,7 @@
|
|||
|
||||
if (scrollable.$.scrollHeight < scrollable.$.clientHeight) {
|
||||
scrollable.setStyle('overflow-y', 'hidden');
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
scrollable.removeStyle('overflow-y');
|
||||
}
|
||||
}, null, null, 10000);
|
||||
|
@ -108,35 +58,15 @@
|
|||
}
|
||||
return !!editor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Attaches an inline editor to a DOM element.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* The element to attach the editor to.
|
||||
* @param {object} format
|
||||
* The text format used in the editor.
|
||||
* @param {string} [mainToolbarId]
|
||||
* The id attribute for the main editor toolbar, if any.
|
||||
* @param {string} [floatedToolbarId]
|
||||
* The id attribute for the floated editor toolbar, if any.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the call to `CKEDITOR.replace()` created an editor or not.
|
||||
*/
|
||||
attachInlineEditor: function (element, format, mainToolbarId, floatedToolbarId) {
|
||||
attachInlineEditor: function attachInlineEditor(element, format, mainToolbarId, floatedToolbarId) {
|
||||
this._loadExternalPlugins(format);
|
||||
// Also pass settings that are Drupal-specific.
|
||||
|
||||
format.editorSettings.drupal = {
|
||||
format: format.format
|
||||
};
|
||||
|
||||
var settings = $.extend(true, {}, format.editorSettings);
|
||||
|
||||
// If a toolbar is already provided for "true WYSIWYG" (in-place editing),
|
||||
// then use that toolbar instead: override the default settings to render
|
||||
// CKEditor UI's top toolbar into mainToolbar, and don't render the bottom
|
||||
// toolbar at all. (CKEditor doesn't need a floated toolbar.)
|
||||
if (mainToolbarId) {
|
||||
var settingsOverride = {
|
||||
extraPlugins: 'sharedspace',
|
||||
|
@ -146,15 +76,13 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Find the "Source" button, if any, and replace it with "Sourcedialog".
|
||||
// (The 'sourcearea' plugin only works in CKEditor's iframe mode.)
|
||||
var sourceButtonFound = false;
|
||||
for (var i = 0; !sourceButtonFound && i < settings.toolbar.length; i++) {
|
||||
if (settings.toolbar[i] !== '/') {
|
||||
for (var 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.
|
||||
|
||||
settings.toolbar[i].items[j] = 'Sourcedialog';
|
||||
settingsOverride.extraPlugins += ',sourcedialog';
|
||||
settingsOverride.removePlugins += ',sourcearea';
|
||||
|
@ -168,80 +96,39 @@
|
|||
settings.sharedSpaces = settingsOverride.sharedSpaces;
|
||||
}
|
||||
|
||||
// CKEditor requires an element to already have the contentEditable
|
||||
// attribute set to "true", otherwise it won't attach an inline editor.
|
||||
element.setAttribute('contentEditable', 'true');
|
||||
|
||||
return !!CKEDITOR.inline(element, settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads the required external plugins for the editor.
|
||||
*
|
||||
* @param {object} format
|
||||
* The text format used in the editor.
|
||||
*/
|
||||
_loadExternalPlugins: function (format) {
|
||||
_loadExternalPlugins: function _loadExternalPlugins(format) {
|
||||
var externalPlugins = format.editorSettings.drupalExternalPlugins;
|
||||
// Register and load additional CKEditor plugins as necessary.
|
||||
|
||||
if (externalPlugins) {
|
||||
for (var pluginName in externalPlugins) {
|
||||
if (externalPlugins.hasOwnProperty(pluginName)) {
|
||||
CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], '');
|
||||
}
|
||||
}
|
||||
Object.keys(externalPlugins || {}).forEach(function (pluginName) {
|
||||
CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], '');
|
||||
});
|
||||
delete format.editorSettings.drupalExternalPlugins;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Drupal.ckeditor = {
|
||||
|
||||
/**
|
||||
* Variable storing the current dialog's save callback.
|
||||
*
|
||||
* @type {?function}
|
||||
*/
|
||||
saveCallback: null,
|
||||
|
||||
/**
|
||||
* Open a dialog for a Drupal-based plugin.
|
||||
*
|
||||
* This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
|
||||
* framework, then opens a dialog at the specified Drupal path.
|
||||
*
|
||||
* @param {CKEditor} editor
|
||||
* The CKEditor instance that is opening the dialog.
|
||||
* @param {string} url
|
||||
* The URL that contains the contents of the dialog.
|
||||
* @param {object} existingValues
|
||||
* Existing values that will be sent via POST to the url for the dialog
|
||||
* contents.
|
||||
* @param {function} saveCallback
|
||||
* A function to be called upon saving the dialog.
|
||||
* @param {object} dialogSettings
|
||||
* An object containing settings to be passed to the jQuery UI.
|
||||
*/
|
||||
openDialog: function (editor, url, existingValues, saveCallback, dialogSettings) {
|
||||
// Locate a suitable place to display our loading indicator.
|
||||
openDialog: function openDialog(editor, url, existingValues, saveCallback, dialogSettings) {
|
||||
var $target = $(editor.container.$);
|
||||
if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) {
|
||||
$target = $target.find('.cke_contents');
|
||||
}
|
||||
|
||||
// Remove any previous loading indicator.
|
||||
$target.css('position', 'relative').find('.ckeditor-dialog-loading').remove();
|
||||
|
||||
// Add a consistent dialog class.
|
||||
var classes = dialogSettings.dialogClass ? dialogSettings.dialogClass.split(' ') : [];
|
||||
classes.push('ui-dialog--narrow');
|
||||
dialogSettings.dialogClass = classes.join(' ');
|
||||
dialogSettings.autoResize = window.matchMedia('(min-width: 600px)').matches;
|
||||
dialogSettings.width = 'auto';
|
||||
|
||||
// Add a "Loading…" message, hide it underneath the CKEditor toolbar,
|
||||
// create a Drupal.Ajax instance to load the dialog and trigger it.
|
||||
var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">' + Drupal.t('Loading...') + '</span></div>');
|
||||
$content.appendTo($target);
|
||||
|
||||
|
@ -250,92 +137,65 @@
|
|||
dialogType: 'modal',
|
||||
selector: '.ckeditor-dialog-loading-link',
|
||||
url: url,
|
||||
progress: {type: 'throbber'},
|
||||
progress: { type: 'throbber' },
|
||||
submit: {
|
||||
editor_object: existingValues
|
||||
}
|
||||
});
|
||||
ckeditorAjaxDialog.execute();
|
||||
|
||||
// After a short delay, show "Loading…" message.
|
||||
window.setTimeout(function () {
|
||||
$content.find('span').animate({top: '0px'});
|
||||
$content.find('span').animate({ top: '0px' });
|
||||
}, 1000);
|
||||
|
||||
// Store the save callback to be executed when this dialog is closed.
|
||||
Drupal.ckeditor.saveCallback = saveCallback;
|
||||
}
|
||||
};
|
||||
|
||||
// Moves the dialog to the top of the CKEDITOR stack.
|
||||
$(window).on('dialogcreate', function (e, dialog, $element, settings) {
|
||||
$('.ui-dialog--narrow').css('zIndex', CKEDITOR.config.baseFloatZIndex + 1);
|
||||
});
|
||||
|
||||
// Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
|
||||
$(window).on('dialog:beforecreate', function (e, dialog, $element, settings) {
|
||||
$('.ckeditor-dialog-loading').animate({top: '-40px'}, function () {
|
||||
$('.ckeditor-dialog-loading').animate({ top: '-40px' }, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Respond to dialogs that are saved, sending data back to CKEditor.
|
||||
$(window).on('editor:dialogsave', function (e, values) {
|
||||
if (Drupal.ckeditor.saveCallback) {
|
||||
Drupal.ckeditor.saveCallback(values);
|
||||
}
|
||||
});
|
||||
|
||||
// Respond to dialogs that are closed, removing the current save handler.
|
||||
$(window).on('dialog:afterclose', function (e, dialog, $element) {
|
||||
if (Drupal.ckeditor.saveCallback) {
|
||||
Drupal.ckeditor.saveCallback = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Formulate a default formula for the maximum autoGrow height.
|
||||
$(document).on('drupalViewportOffsetChange', function () {
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
$(window).on('hashchange.ckeditor', redirectTextareaFragmentToCKEditorInstance);
|
||||
|
||||
// Set autoGrow to make the editor grow the moment it is created.
|
||||
CKEDITOR.config.autoGrow_onStartup = true;
|
||||
|
||||
// Set the CKEditor cache-busting string to the same value as Drupal.
|
||||
CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp;
|
||||
|
||||
if (AjaxCommands) {
|
||||
|
||||
/**
|
||||
* Command to add style sheets to a CKEditor instance.
|
||||
*
|
||||
* Works for both iframe and inline CKEditor instances.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
|
||||
* @param {object} response
|
||||
* The response from the Ajax request.
|
||||
* @param {string} response.editor_id
|
||||
* The CKEditor instance ID.
|
||||
* @param {number} [status]
|
||||
* The XMLHttpRequest status.
|
||||
*
|
||||
* @see http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document
|
||||
*/
|
||||
AjaxCommands.prototype.ckeditor_add_stylesheet = function (ajax, response, status) {
|
||||
var editor = CKEDITOR.instances[response.editor_id];
|
||||
|
||||
|
@ -346,5 +206,4 @@
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
})(Drupal, Drupal.debounce, CKEDITOR, jQuery, Drupal.displace, Drupal.AjaxCommands);
|
||||
})(Drupal, Drupal.debounce, CKEDITOR, jQuery, Drupal.displace, Drupal.AjaxCommands);
|
14
web/core/modules/ckeditor/js/ckeditor.language.admin.es6.js
Normal file
14
web/core/modules/ckeditor/js/ckeditor.language.admin.es6.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
(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(),
|
||||
);
|
||||
},
|
||||
};
|
||||
})(jQuery, Drupal);
|
|
@ -1,16 +1,16 @@
|
|||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides the summary for the "language" plugin settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorLanguageSettingsSummary = {
|
||||
attach: function () {
|
||||
attach: function attach() {
|
||||
$('#edit-editor-settings-plugins-language').drupalSetSummary(function (context) {
|
||||
return $('#edit-editor-settings-plugins-language-language-list-type option:selected').text();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
||||
})(jQuery, Drupal);
|
132
web/core/modules/ckeditor/js/ckeditor.stylescombo.admin.es6.js
Normal file
132
web/core/modules/ckeditor/js/ckeditor.stylescombo.admin.es6.js
Normal file
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor StylesCombo admin behavior.
|
||||
*/
|
||||
|
||||
(function($, Drupal, drupalSettings, _) {
|
||||
/**
|
||||
* Ensures that the "stylescombo" button's metadata remains up-to-date.
|
||||
*
|
||||
* Triggers the CKEditorPluginSettingsChanged event whenever the "stylescombo"
|
||||
* plugin settings change, to ensure that the corresponding feature metadata
|
||||
* is immediately updated — i.e. ensure that HTML tags and classes entered
|
||||
* here are known to be "required", which may affect filter settings.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches admin behaviour to the "stylescombo" button.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorStylesComboSettings = {
|
||||
attach(context) {
|
||||
const $context = $(context);
|
||||
|
||||
// React to changes in the list of user-defined styles: calculate the new
|
||||
// stylesSet setting up to 2 times per second, and if it is different,
|
||||
// fire the CKEditorPluginSettingsChanged event with the updated parts of
|
||||
// the CKEditor configuration. (This will, in turn, cause the hidden
|
||||
// CKEditor instance to be updated and a drupalEditorFeatureModified event
|
||||
// to fire.)
|
||||
const $ckeditorActiveToolbar = $context
|
||||
.find('.ckeditor-toolbar-configuration')
|
||||
.find('.ckeditor-toolbar-active');
|
||||
let previousStylesSet =
|
||||
drupalSettings.ckeditor.hiddenCKEditorConfig.stylesSet;
|
||||
const that = this;
|
||||
$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)) {
|
||||
previousStylesSet = stylesSet;
|
||||
$ckeditorActiveToolbar.trigger('CKEditorPluginSettingsChanged', [
|
||||
{ stylesSet },
|
||||
]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the "stylesSet" configuration part of the CKEditor JS settings.
|
||||
*
|
||||
* @see \Drupal\ckeditor\Plugin\ckeditor\plugin\StylesCombo::generateStylesSetSetting()
|
||||
*
|
||||
* Note that this is a more forgiving implementation than the PHP version:
|
||||
* the parsing works identically, but instead of failing on invalid styles,
|
||||
* we just ignore those.
|
||||
*
|
||||
* @param {string} styles
|
||||
* The "styles" setting.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array containing the "stylesSet" configuration.
|
||||
*/
|
||||
_generateStylesSetSetting(styles) {
|
||||
const stylesSet = [];
|
||||
|
||||
styles = styles.replace(/\r/g, '\n');
|
||||
const lines = styles.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const style = $.trim(lines[i]);
|
||||
|
||||
// Ignore empty lines in between non-empty lines.
|
||||
if (style.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate syntax: element[.class...]|label pattern expected.
|
||||
if (
|
||||
style.match(/^ *[a-zA-Z0-9]+ *(\.[a-zA-Z0-9_-]+ *)*\| *.+ *$/) ===
|
||||
null
|
||||
) {
|
||||
// Instead of failing, we just ignore any invalid styles.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse.
|
||||
const parts = style.split('|');
|
||||
const selector = parts[0];
|
||||
const label = parts[1];
|
||||
const classes = selector.split('.');
|
||||
const element = classes.shift();
|
||||
|
||||
// Build the data structure CKEditor's stylescombo plugin expects.
|
||||
// @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles
|
||||
stylesSet.push({
|
||||
attributes: { class: classes.join(' ') },
|
||||
element,
|
||||
name: label,
|
||||
});
|
||||
}
|
||||
|
||||
return stylesSet;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides the summary for the "stylescombo" plugin settings vertical tab.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behaviour to the plugin settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
|
||||
attach() {
|
||||
$('[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');
|
||||
}
|
||||
|
||||
const count = $.trim(styles).split('\n').length;
|
||||
return Drupal.t('@count styles configured', { '@count': count });
|
||||
});
|
||||
},
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
|
@ -1,69 +1,28 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor StylesCombo admin behavior.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Ensures that the "stylescombo" button's metadata remains up-to-date.
|
||||
*
|
||||
* Triggers the CKEditorPluginSettingsChanged event whenever the "stylescombo"
|
||||
* plugin settings change, to ensure that the corresponding feature metadata
|
||||
* is immediately updated — i.e. ensure that HTML tags and classes entered
|
||||
* here are known to be "required", which may affect filter settings.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches admin behaviour to the "stylescombo" button.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorStylesComboSettings = {
|
||||
attach: function (context) {
|
||||
attach: function attach(context) {
|
||||
var $context = $(context);
|
||||
|
||||
// React to changes in the list of user-defined styles: calculate the new
|
||||
// stylesSet setting up to 2 times per second, and if it is different,
|
||||
// fire the CKEditorPluginSettingsChanged event with the updated parts of
|
||||
// the CKEditor configuration. (This will, in turn, cause the hidden
|
||||
// CKEditor instance to be updated and a drupalEditorFeatureModified event
|
||||
// to fire.)
|
||||
var $ckeditorActiveToolbar = $context
|
||||
.find('.ckeditor-toolbar-configuration')
|
||||
.find('.ckeditor-toolbar-active');
|
||||
var $ckeditorActiveToolbar = $context.find('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
|
||||
var previousStylesSet = drupalSettings.ckeditor.hiddenCKEditorConfig.stylesSet;
|
||||
var that = this;
|
||||
$context.find('[name="editor[settings][plugins][stylescombo][styles]"]')
|
||||
.on('blur.ckeditorStylesComboSettings', function () {
|
||||
var styles = $.trim($(this).val());
|
||||
var stylesSet = that._generateStylesSetSetting(styles);
|
||||
if (!_.isEqual(previousStylesSet, stylesSet)) {
|
||||
previousStylesSet = stylesSet;
|
||||
$ckeditorActiveToolbar.trigger('CKEditorPluginSettingsChanged', [
|
||||
{stylesSet: stylesSet}
|
||||
]);
|
||||
}
|
||||
});
|
||||
$context.find('[name="editor[settings][plugins][stylescombo][styles]"]').on('blur.ckeditorStylesComboSettings', function () {
|
||||
var styles = $.trim($(this).val());
|
||||
var stylesSet = that._generateStylesSetSetting(styles);
|
||||
if (!_.isEqual(previousStylesSet, stylesSet)) {
|
||||
previousStylesSet = stylesSet;
|
||||
$ckeditorActiveToolbar.trigger('CKEditorPluginSettingsChanged', [{ stylesSet: stylesSet }]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the "stylesSet" configuration part of the CKEditor JS settings.
|
||||
*
|
||||
* @see \Drupal\ckeditor\Plugin\ckeditor\plugin\StylesCombo::generateStylesSetSetting()
|
||||
*
|
||||
* Note that this is a more forgiving implementation than the PHP version:
|
||||
* the parsing works identically, but instead of failing on invalid styles,
|
||||
* we just ignore those.
|
||||
*
|
||||
* @param {string} styles
|
||||
* The "styles" setting.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array containing the "stylesSet" configuration.
|
||||
*/
|
||||
_generateStylesSetSetting: function (styles) {
|
||||
_generateStylesSetSetting: function _generateStylesSetSetting(styles) {
|
||||
var stylesSet = [];
|
||||
|
||||
styles = styles.replace(/\r/g, '\n');
|
||||
|
@ -71,28 +30,22 @@
|
|||
for (var i = 0; i < lines.length; i++) {
|
||||
var style = $.trim(lines[i]);
|
||||
|
||||
// Ignore empty lines in between non-empty lines.
|
||||
if (style.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate syntax: element[.class...]|label pattern expected.
|
||||
if (style.match(/^ *[a-zA-Z0-9]+ *(\.[a-zA-Z0-9_-]+ *)*\| *.+ *$/) === null) {
|
||||
// Instead of failing, we just ignore any invalid styles.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse.
|
||||
var parts = style.split('|');
|
||||
var selector = parts[0];
|
||||
var label = parts[1];
|
||||
var classes = selector.split('.');
|
||||
var element = classes.shift();
|
||||
|
||||
// Build the data structure CKEditor's stylescombo plugin expects.
|
||||
// @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Styles
|
||||
stylesSet.push({
|
||||
attributes: {class: classes.join(' ')},
|
||||
attributes: { class: classes.join(' ') },
|
||||
element: element,
|
||||
name: label
|
||||
});
|
||||
|
@ -102,27 +55,17 @@
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides the summary for the "stylescombo" plugin settings vertical tab.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*
|
||||
* @prop {Drupal~behaviorAttach} attach
|
||||
* Attaches summary behaviour to the plugin settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorStylesComboSettingsSummary = {
|
||||
attach: function () {
|
||||
attach: function attach() {
|
||||
$('[data-ckeditor-plugin-id="stylescombo"]').drupalSetSummary(function (context) {
|
||||
var styles = $.trim($('[data-drupal-selector="edit-editor-settings-plugins-stylescombo-styles"]').val());
|
||||
if (styles.length === 0) {
|
||||
return Drupal.t('No styles configured');
|
||||
}
|
||||
else {
|
||||
var count = $.trim(styles).split('\n').length;
|
||||
return Drupal.t('@count styles configured', {'@count': count});
|
||||
}
|
||||
|
||||
var count = $.trim(styles).split('\n').length;
|
||||
return Drupal.t('@count styles configured', { '@count': count });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
73
web/core/modules/ckeditor/js/models/Model.es6.js
Normal file
73
web/core/modules/ckeditor/js/models/Model.es6.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone Model for the state of a CKEditor toolbar configuration .
|
||||
*/
|
||||
|
||||
(function(Drupal, Backbone) {
|
||||
/**
|
||||
* Backbone model for the CKEditor toolbar configuration state.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @augments Backbone.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,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*/
|
||||
sync() {
|
||||
// Push the settings into the textarea.
|
||||
this.get('$textarea').val(
|
||||
JSON.stringify(this.get('activeEditorConfig')),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
})(Drupal, Backbone);
|
|
@ -1,75 +1,30 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone Model for the state of a CKEditor toolbar configuration .
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Backbone model for the CKEditor toolbar configuration state.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @augments Backbone.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.
|
||||
*/
|
||||
Drupal.ckeditor.Model = Backbone.Model.extend({
|
||||
defaults: {
|
||||
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
|
||||
},
|
||||
|
||||
/**
|
||||
* @method
|
||||
*/
|
||||
sync: function () {
|
||||
// Push the settings into the textarea.
|
||||
sync: function sync() {
|
||||
this.get('$textarea').val(JSON.stringify(this.get('activeEditorConfig')));
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone);
|
||||
})(Drupal, Backbone);
|
399
web/core/modules/ckeditor/js/plugins/drupalimage/plugin.es6.js
Normal file
399
web/core/modules/ckeditor/js/plugins/drupalimage/plugin.es6.js
Normal file
|
@ -0,0 +1,399 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Image plugin.
|
||||
*
|
||||
* This alters the existing CKEditor image2 widget plugin to:
|
||||
* - require a data-entity-type and a data-entity-uuid attribute (which Drupal
|
||||
* uses to track where images are being used)
|
||||
* - use a Drupal-native dialog (that is in fact just an alterable Drupal form
|
||||
* like any other) instead of CKEditor's own dialogs.
|
||||
*
|
||||
* @see \Drupal\editor\Form\EditorImageDialog
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(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',
|
||||
hidpi: true,
|
||||
|
||||
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 => {
|
||||
const widgetDefinition = event.data;
|
||||
if (widgetDefinition.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
// First, convert requiredContent & allowedContent from the string
|
||||
// format that image2 uses for both to formats that are better suited
|
||||
// for extending, so that both this basic drupalimage plugin and Drupal
|
||||
// modules can easily extend it.
|
||||
// @see http://docs.ckeditor.com/#!/api/CKEDITOR.filter.allowedContentRules
|
||||
// Mapped from image2's allowedContent. Unlike image2, we don't allow
|
||||
// <figure>, <figcaption>, <div> or <p> in our downcast, so we omit
|
||||
// those. For the <img> tag, we list all attributes it lists, but omit
|
||||
// the classes, because the listed classes are for alignment, and for
|
||||
// alignment we use the data-align attribute.
|
||||
widgetDefinition.allowedContent = {
|
||||
img: {
|
||||
attributes: {
|
||||
'!src': true,
|
||||
'!alt': true,
|
||||
width: true,
|
||||
height: true,
|
||||
},
|
||||
classes: {},
|
||||
},
|
||||
};
|
||||
// Mapped from image2's requiredContent: "img[src,alt]". This does not
|
||||
// use the object format unlike above, but a CKEDITOR.style instance,
|
||||
// because requiredContent does not support the object format.
|
||||
// @see https://www.drupal.org/node/2585173#comment-10456981
|
||||
widgetDefinition.requiredContent = new CKEDITOR.style({
|
||||
element: 'img',
|
||||
attributes: {
|
||||
src: '',
|
||||
alt: '',
|
||||
},
|
||||
});
|
||||
|
||||
// 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-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;
|
||||
|
||||
// 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'
|
||||
];
|
||||
};
|
||||
|
||||
// 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) {
|
||||
if (element.name !== 'img') {
|
||||
return;
|
||||
}
|
||||
// Don't initialize on pasted fake objects.
|
||||
if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the data-entity-type attribute.
|
||||
data['data-entity-type'] = element.attributes['data-entity-type'];
|
||||
// Parse the data-entity-uuid attribute.
|
||||
data['data-entity-uuid'] = element.attributes['data-entity-uuid'];
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
// Overrides default implementation. Used to populate the "classes"
|
||||
// property of the widget's "data" property, which is used for the
|
||||
// "widget styles" functionality
|
||||
// (http://docs.ckeditor.com/#!/guide/dev_styles-section-widget-styles).
|
||||
// Is applied to whatever the main element of the widget is (<figure> or
|
||||
// <img>). The classes in image2_captionedClass are always added due to
|
||||
// a bug in CKEditor. In the case of drupalimage, we don't ever want to
|
||||
// add that class, because the widget template already contains it.
|
||||
// @see http://dev.ckeditor.com/ticket/13888
|
||||
// @see https://www.drupal.org/node/2268941
|
||||
const originalGetClasses = widgetDefinition.getClasses;
|
||||
widgetDefinition.getClasses = function() {
|
||||
const classes = originalGetClasses.call(this);
|
||||
const captionedClasses = (
|
||||
this.editor.config.image2_captionedClass || ''
|
||||
).split(/\s+/);
|
||||
|
||||
if (captionedClasses.length && classes) {
|
||||
for (let i = 0; i < captionedClasses.length; i++) {
|
||||
if (captionedClasses[i] in classes) {
|
||||
delete classes[captionedClasses[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
// Protected; keys of the widget data to be sent to the Drupal dialog.
|
||||
// Keys in the hash are the keys for image2's data, values are the keys
|
||||
// that the Drupal dialog uses.
|
||||
widgetDefinition._mapDataToDialog = {
|
||||
src: 'src',
|
||||
alt: 'alt',
|
||||
width: 'width',
|
||||
height: 'height',
|
||||
'data-entity-type': 'data-entity-type',
|
||||
'data-entity-uuid': 'data-entity-uuid',
|
||||
};
|
||||
|
||||
// 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) {
|
||||
const dialogValues = {};
|
||||
const map = widgetDefinition._mapDataToDialog;
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach(key => {
|
||||
dialogValues[map[key]] = data[key];
|
||||
});
|
||||
return dialogValues;
|
||||
};
|
||||
|
||||
// Protected; the inverse of _dataToDialogValues.
|
||||
widgetDefinition._dialogValuesToData = function(dialogReturnValues) {
|
||||
const data = {};
|
||||
const map = widgetDefinition._mapDataToDialog;
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach(key => {
|
||||
if (dialogReturnValues.hasOwnProperty(map[key])) {
|
||||
data[key] = dialogReturnValues[map[key]];
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
// Protected; creates Drupal dialog save callback.
|
||||
widgetDefinition._createDialogSaveCallback = function(editor, widget) {
|
||||
return function(dialogReturnValues) {
|
||||
const firstEdit = !widget.ready;
|
||||
|
||||
// Dialog may have blurred the widget. Re-focus it first.
|
||||
if (!firstEdit) {
|
||||
widget.focus();
|
||||
}
|
||||
|
||||
editor.fire('saveSnapshot');
|
||||
|
||||
// Pass `true` so DocumentFragment will also be returned.
|
||||
const container = widget.wrapper.getParent(true);
|
||||
const image = widget.parts.image;
|
||||
|
||||
// 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,
|
||||
);
|
||||
widget.setData(data);
|
||||
|
||||
// Retrieve the widget once again. It could've been destroyed
|
||||
// when shifting state, so might deal with a new instance.
|
||||
widget = editor.widgets.getByElement(image);
|
||||
|
||||
// It's first edit, just after widget instance creation, but before
|
||||
// it was inserted into DOM. So we need to retrieve the widget
|
||||
// wrapper from inside the DocumentFragment which we cached above
|
||||
// and finalize other things (like ready event and flag).
|
||||
if (firstEdit) {
|
||||
editor.widgets.finalizeCreation(container);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// (Re-)focus the widget.
|
||||
widget.focus();
|
||||
// Save snapshot for undo support.
|
||||
editor.fire('saveSnapshot');
|
||||
});
|
||||
|
||||
return widget;
|
||||
};
|
||||
};
|
||||
|
||||
const originalInit = widgetDefinition.init;
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Add a widget#edit listener to every instance of image2 widget in order
|
||||
// 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 => {
|
||||
const widget = event.data;
|
||||
|
||||
if (widget.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.on('edit', event => {
|
||||
// Cancel edit event to break image2's dialog binding
|
||||
// (and also to prevent automatic insertion before opening dialog).
|
||||
event.cancel();
|
||||
|
||||
// Open drupalimage dialog.
|
||||
editor.execCommand('editdrupalimage', {
|
||||
existingValues: widget.definition._dataToDialogValues(widget.data),
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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]',
|
||||
requiredContent: 'img[alt,src,data-entity-type,data-entity-uuid]',
|
||||
modes: { wysiwyg: 1 },
|
||||
canUndo: true,
|
||||
exec(editor, data) {
|
||||
const dialogSettings = {
|
||||
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,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Register the toolbar button.
|
||||
if (editor.ui.addButton) {
|
||||
editor.ui.addButton('DrupalImage', {
|
||||
label: Drupal.t('Image'),
|
||||
// Note that we use the original image2 command!
|
||||
command: 'image',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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() {
|
||||
return CKEDITOR.plugins.drupallink.parseLinkAttributes;
|
||||
};
|
||||
CKEDITOR.plugins.image2.getLinkAttributesGetter = function() {
|
||||
return CKEDITOR.plugins.drupallink.getLinkAttributes;
|
||||
};
|
||||
|
||||
// Expose an API for other plugins to interact with drupalimage widgets.
|
||||
CKEDITOR.plugins.drupalimage = {
|
||||
getFocusedWidget,
|
||||
};
|
||||
})(jQuery, Drupal, CKEDITOR);
|
|
@ -1,61 +1,77 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Image plugin.
|
||||
*
|
||||
* This alters the existing CKEditor image2 widget plugin to:
|
||||
* - require a data-entity-type and a data-entity-uuid attribute (which Drupal
|
||||
* uses to track where images are being used)
|
||||
* - use a Drupal-native dialog (that is in fact just an alterable Drupal form
|
||||
* like any other) instead of CKEditor's own dialogs.
|
||||
*
|
||||
* @see \Drupal\editor\Form\EditorImageDialog
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, CKEDITOR) {
|
||||
function getFocusedWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
|
||||
'use strict';
|
||||
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',
|
||||
hidpi: true,
|
||||
|
||||
beforeInit: function (editor) {
|
||||
// Override the image2 widget definition to require and handle the
|
||||
// additional data-entity-type and data-entity-uuid attributes.
|
||||
beforeInit: function beforeInit(editor) {
|
||||
editor.on('widgetDefinition', function (event) {
|
||||
var widgetDefinition = event.data;
|
||||
if (widgetDefinition.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
// First, convert requiredContent & allowedContent from the string
|
||||
// format that image2 uses for both to formats that are better suited
|
||||
// for extending, so that both this basic drupalimage plugin and Drupal
|
||||
// modules can easily extend it.
|
||||
// @see http://docs.ckeditor.com/#!/api/CKEDITOR.filter.allowedContentRules
|
||||
// Mapped from image2's allowedContent. Unlike image2, we don't allow
|
||||
// <figure>, <figcaption>, <div> or <p> in our downcast, so we omit
|
||||
// those. For the <img> tag, we list all attributes it lists, but omit
|
||||
// the classes, because the listed classes are for alignment, and for
|
||||
// alignment we use the data-align attribute.
|
||||
widgetDefinition.allowedContent = {
|
||||
img: {
|
||||
attributes: {
|
||||
'!src': true,
|
||||
'!alt': true,
|
||||
'width': true,
|
||||
'height': true
|
||||
width: true,
|
||||
height: true
|
||||
},
|
||||
classes: {}
|
||||
}
|
||||
};
|
||||
// Mapped from image2's requiredContent: "img[src,alt]". This does not
|
||||
// use the object format unlike above, but a CKEDITOR.style instance,
|
||||
// because requiredContent does not support the object format.
|
||||
// @see https://www.drupal.org/node/2585173#comment-10456981
|
||||
|
||||
widgetDefinition.requiredContent = new CKEDITOR.style({
|
||||
element: 'img',
|
||||
attributes: {
|
||||
|
@ -64,10 +80,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// 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.
|
||||
var requiredContent = widgetDefinition.requiredContent.getDefinition();
|
||||
requiredContent.attributes['data-entity-type'] = '';
|
||||
requiredContent.attributes['data-entity-uuid'] = '';
|
||||
|
@ -75,44 +87,27 @@
|
|||
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'];
|
||||
};
|
||||
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Parse the data-entity-type attribute.
|
||||
data['data-entity-type'] = element.attributes['data-entity-type'];
|
||||
// Parse the data-entity-uuid attribute.
|
||||
|
||||
data['data-entity-uuid'] = element.attributes['data-entity-uuid'];
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
// Overrides default implementation. Used to populate the "classes"
|
||||
// property of the widget's "data" property, which is used for the
|
||||
// "widget styles" functionality
|
||||
// (http://docs.ckeditor.com/#!/guide/dev_styles-section-widget-styles).
|
||||
// Is applied to whatever the main element of the widget is (<figure> or
|
||||
// <img>). The classes in image2_captionedClass are always added due to
|
||||
// a bug in CKEditor. In the case of drupalimage, we don't ever want to
|
||||
// add that class, because the widget template already contains it.
|
||||
// @see http://dev.ckeditor.com/ticket/13888
|
||||
// @see https://www.drupal.org/node/2268941
|
||||
var originalGetClasses = widgetDefinition.getClasses;
|
||||
widgetDefinition.getClasses = function () {
|
||||
var classes = originalGetClasses.call(this);
|
||||
|
@ -129,21 +124,15 @@
|
|||
return classes;
|
||||
};
|
||||
|
||||
// Protected; keys of the widget data to be sent to the Drupal dialog.
|
||||
// Keys in the hash are the keys for image2's data, values are the keys
|
||||
// that the Drupal dialog uses.
|
||||
widgetDefinition._mapDataToDialog = {
|
||||
'src': 'src',
|
||||
'alt': 'alt',
|
||||
'width': 'width',
|
||||
'height': 'height',
|
||||
src: 'src',
|
||||
alt: 'alt',
|
||||
width: 'width',
|
||||
height: 'height',
|
||||
'data-entity-type': 'data-entity-type',
|
||||
'data-entity-uuid': 'data-entity-uuid'
|
||||
};
|
||||
|
||||
// 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) {
|
||||
var dialogValues = {};
|
||||
var map = widgetDefinition._mapDataToDialog;
|
||||
|
@ -153,7 +142,6 @@
|
|||
return dialogValues;
|
||||
};
|
||||
|
||||
// Protected; the inverse of _dataToDialogValues.
|
||||
widgetDefinition._dialogValuesToData = function (dialogReturnValues) {
|
||||
var data = {};
|
||||
var map = widgetDefinition._mapDataToDialog;
|
||||
|
@ -165,44 +153,31 @@
|
|||
return data;
|
||||
};
|
||||
|
||||
// Protected; creates Drupal dialog save callback.
|
||||
widgetDefinition._createDialogSaveCallback = function (editor, widget) {
|
||||
return function (dialogReturnValues) {
|
||||
var firstEdit = !widget.ready;
|
||||
|
||||
// Dialog may have blurred the widget. Re-focus it first.
|
||||
if (!firstEdit) {
|
||||
widget.focus();
|
||||
}
|
||||
|
||||
editor.fire('saveSnapshot');
|
||||
|
||||
// Pass `true` so DocumentFragment will also be returned.
|
||||
var container = widget.wrapper.getParent(true);
|
||||
var image = widget.parts.image;
|
||||
|
||||
// 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.
|
||||
var data = widgetDefinition._dialogValuesToData(dialogReturnValues.attributes);
|
||||
widget.setData(data);
|
||||
|
||||
// Retrieve the widget once again. It could've been destroyed
|
||||
// when shifting state, so might deal with a new instance.
|
||||
widget = editor.widgets.getByElement(image);
|
||||
|
||||
// It's first edit, just after widget instance creation, but before
|
||||
// it was inserted into DOM. So we need to retrieve the widget
|
||||
// wrapper from inside the DocumentFragment which we cached above
|
||||
// and finalize other things (like ready event and flag).
|
||||
if (firstEdit) {
|
||||
editor.widgets.finalizeCreation(container);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
// (Re-)focus the widget.
|
||||
widget.focus();
|
||||
// Save snapshot for undo support.
|
||||
|
||||
editor.fire('saveSnapshot');
|
||||
});
|
||||
|
||||
|
@ -214,19 +189,12 @@
|
|||
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));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Add a widget#edit listener to every instance of image2 widget in order
|
||||
// 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', function (event) {
|
||||
var widget = event.data;
|
||||
|
||||
|
@ -235,30 +203,23 @@
|
|||
}
|
||||
|
||||
widget.on('edit', function (event) {
|
||||
// Cancel edit event to break image2's dialog binding
|
||||
// (and also to prevent automatic insertion before opening dialog).
|
||||
event.cancel();
|
||||
|
||||
// Open drupalimage dialog.
|
||||
editor.execCommand('editdrupalimage', {
|
||||
existingValues: widget.definition._dataToDialogValues(widget.data),
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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]',
|
||||
requiredContent: 'img[alt,src,data-entity-type,data-entity-uuid]',
|
||||
modes: {wysiwyg: 1},
|
||||
modes: { wysiwyg: 1 },
|
||||
canUndo: true,
|
||||
exec: function (editor, data) {
|
||||
exec: function exec(editor, data) {
|
||||
var dialogSettings = {
|
||||
title: data.dialogTitle,
|
||||
dialogClass: 'editor-image-dialog'
|
||||
|
@ -267,24 +228,19 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Register the toolbar button.
|
||||
if (editor.ui.addButton) {
|
||||
editor.ui.addButton('DrupalImage', {
|
||||
label: Drupal.t('Image'),
|
||||
// Note that we use the original image2 command!
|
||||
|
||||
command: 'image'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
afterInit: function (editor) {
|
||||
afterInit: function 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 () {
|
||||
return CKEDITOR.plugins.drupallink.parseLinkAttributes;
|
||||
};
|
||||
|
@ -292,80 +248,7 @@
|
|||
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) {
|
||||
var 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) {
|
||||
var 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) {
|
||||
var 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: getFocusedWidget
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, CKEDITOR);
|
||||
})(jQuery, Drupal, CKEDITOR);
|
|
@ -0,0 +1,347 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Image Caption plugin.
|
||||
*
|
||||
* This alters the existing CKEditor image2 widget plugin, which is already
|
||||
* altered by the Drupal Image plugin, to:
|
||||
* - allow for the data-caption and data-align attributes to be set
|
||||
* - mimic the upcasting behavior of the caption_filter filter.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function(CKEDITOR) {
|
||||
/**
|
||||
* Finds an element by its name.
|
||||
*
|
||||
* Function will check first the passed element itself and then all its
|
||||
* children in DFS order.
|
||||
*
|
||||
* @param {CKEDITOR.htmlParser.element} element
|
||||
* The element to search.
|
||||
* @param {string} name
|
||||
* The element name to search for.
|
||||
*
|
||||
* @return {?CKEDITOR.htmlParser.element}
|
||||
* The found element, or null.
|
||||
*/
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
let found = null;
|
||||
element.forEach(el => {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
// Stop here.
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
|
||||
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);
|
|
@ -1,46 +1,44 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Image Caption plugin.
|
||||
*
|
||||
* This alters the existing CKEditor image2 widget plugin, which is already
|
||||
* altered by the Drupal Image plugin, to:
|
||||
* - allow for the data-caption and data-align attributes to be set
|
||||
* - mimic the upcasting behavior of the caption_filter filter.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (CKEDITOR) {
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
'use strict';
|
||||
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',
|
||||
|
||||
beforeInit: function (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).
|
||||
beforeInit: function beforeInit(editor) {
|
||||
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.
|
||||
var placeholderText = editor.config.drupalImageCaption_captionPlaceholderText;
|
||||
|
||||
// Override the image2 widget definition to handle the additional
|
||||
// data-align and data-caption attributes.
|
||||
editor.on('widgetDefinition', function (event) {
|
||||
var widgetDefinition = event.data;
|
||||
if (widgetDefinition.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only perform the downcasting/upcasting for to the enabled filters.
|
||||
var captionFilterEnabled = editor.config.drupalImageCaption_captionFilterEnabled;
|
||||
var alignFilterEnabled = editor.config.drupalImageCaption_alignFilterEnabled;
|
||||
|
||||
// Override default features definitions for drupalimagecaption.
|
||||
CKEDITOR.tools.extend(widgetDefinition.features, {
|
||||
caption: {
|
||||
requiredContent: 'img[data-caption]'
|
||||
|
@ -50,10 +48,6 @@
|
|||
}
|
||||
}, 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.
|
||||
var requiredContent = widgetDefinition.requiredContent.getDefinition();
|
||||
requiredContent.attributes['data-align'] = '';
|
||||
requiredContent.attributes['data-caption'] = '';
|
||||
|
@ -61,15 +55,8 @@
|
|||
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.
|
||||
var originalDowncast = widgetDefinition.downcast;
|
||||
widgetDefinition.downcast = function (element) {
|
||||
var img = findElementByName(element, 'img');
|
||||
|
@ -80,8 +67,6 @@
|
|||
var 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;
|
||||
}
|
||||
|
@ -92,28 +77,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
// If img is wrapped with a link, we want to return that link.
|
||||
if (img.parent.name === 'a') {
|
||||
return img.parent;
|
||||
}
|
||||
else {
|
||||
return img;
|
||||
}
|
||||
|
||||
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.
|
||||
var 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']) {
|
||||
|
||||
if (element.attributes['data-cke-realelement']) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -125,10 +102,8 @@
|
|||
}
|
||||
|
||||
var retElement = element;
|
||||
var caption;
|
||||
var caption = void 0;
|
||||
|
||||
// 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'];
|
||||
|
@ -143,9 +118,6 @@
|
|||
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) {
|
||||
var index = element.getIndex();
|
||||
var splitBefore = index > 0;
|
||||
|
@ -163,78 +135,52 @@
|
|||
retElement = element;
|
||||
}
|
||||
|
||||
// If this image has a caption, create a full <figure> structure.
|
||||
if (caption) {
|
||||
var 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;
|
||||
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)) {
|
||||
var 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',
|
||||
align: 'data-align',
|
||||
'data-caption': 'data-caption',
|
||||
'hasCaption': 'hasCaption'
|
||||
hasCaption: 'hasCaption'
|
||||
});
|
||||
|
||||
// Override Drupal dialog save callback.
|
||||
var originalCreateDialogSaveCallback = widgetDefinition._createDialogSaveCallback;
|
||||
widgetDefinition._createDialogSaveCallback = function (editor, widget) {
|
||||
var 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;
|
||||
|
||||
var 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.
|
||||
var captionElement = actualWidget.editables.caption.$;
|
||||
if (captionElement.childNodes.length === 1 && captionElement.childNodes.item(0).nodeName === 'BR') {
|
||||
captionElement.removeChild(captionElement.childNodes.item(0));
|
||||
|
@ -242,12 +188,10 @@
|
|||
}
|
||||
};
|
||||
};
|
||||
// Low priority to ensure drupalimage's event handler runs first.
|
||||
}, null, null, 20);
|
||||
},
|
||||
|
||||
afterInit: function (editor) {
|
||||
var disableButtonIfOnWidget = function (evt) {
|
||||
afterInit: function afterInit(editor) {
|
||||
var disableButtonIfOnWidget = function disableButtonIfOnWidget(evt) {
|
||||
var widget = editor.widgets.focused;
|
||||
if (widget && widget.name === 'image') {
|
||||
this.setState(CKEDITOR.TRISTATE_DISABLED);
|
||||
|
@ -255,9 +199,8 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Disable alignment buttons if the align filter is not enabled.
|
||||
if (editor.plugins.justify && !editor.config.drupalImageCaption_alignFilterEnabled) {
|
||||
var cmd;
|
||||
var cmd = void 0;
|
||||
var commands = ['justifyleft', 'justifycenter', 'justifyright', 'justifyblock'];
|
||||
for (var n = 0; n < commands.length; n++) {
|
||||
cmd = editor.getCommand(commands[n]);
|
||||
|
@ -267,35 +210,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds an element by its name.
|
||||
*
|
||||
* Function will check first the passed element itself and then all its
|
||||
* children in DFS order.
|
||||
*
|
||||
* @param {CKEDITOR.htmlParser.element} element
|
||||
* The element to search.
|
||||
* @param {string} name
|
||||
* The element name to search for.
|
||||
*
|
||||
* @return {?CKEDITOR.htmlParser.element}
|
||||
* The found element, or null.
|
||||
*/
|
||||
function findElementByName(element, name) {
|
||||
if (element.name === name) {
|
||||
return element;
|
||||
}
|
||||
|
||||
var found = null;
|
||||
element.forEach(function (el) {
|
||||
if (el.name === name) {
|
||||
found = el;
|
||||
// Stop here.
|
||||
return false;
|
||||
}
|
||||
}, CKEDITOR.NODE_ELEMENT);
|
||||
return found;
|
||||
}
|
||||
|
||||
})(CKEDITOR);
|
||||
})(CKEDITOR);
|
334
web/core/modules/ckeditor/js/plugins/drupallink/plugin.es6.js
Normal file
334
web/core/modules/ckeditor/js/plugins/drupallink/plugin.es6.js
Normal file
|
@ -0,0 +1,334 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Link plugin.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(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++
|
||||
) {
|
||||
attribute = domElement.attributes.item(attrIndex);
|
||||
attributeName = attribute.nodeName.toLowerCase();
|
||||
// Ignore data-cke-* attributes; they're CKEditor internals.
|
||||
if (attributeName.indexOf('data-cke-') === 0) {
|
||||
continue;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Remove any cke_* classes.
|
||||
if (parsedAttributes.class) {
|
||||
parsedAttributes.class = CKEDITOR.tools.trim(
|
||||
parsedAttributes.class.replace(/cke_\S+/, ''),
|
||||
);
|
||||
}
|
||||
|
||||
return parsedAttributes;
|
||||
}
|
||||
|
||||
function getAttributes(editor, data) {
|
||||
const set = {};
|
||||
Object.keys(data || {}).forEach(attributeName => {
|
||||
set[attributeName] = data[attributeName];
|
||||
});
|
||||
|
||||
// CKEditor tracks the *actual* saved href in a data-cke-saved-* attribute
|
||||
// to work around browser quirks. We need to update it.
|
||||
set['data-cke-saved-href'] = set.href;
|
||||
|
||||
// Remove all attributes which are not currently set.
|
||||
const removed = {};
|
||||
Object.keys(set).forEach(s => {
|
||||
delete removed[s];
|
||||
});
|
||||
|
||||
return {
|
||||
set,
|
||||
removed: CKEDITOR.tools.objectKeys(removed),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
init(editor) {
|
||||
// Add the commands for link and unlink.
|
||||
editor.addCommand('drupallink', {
|
||||
allowedContent: {
|
||||
a: {
|
||||
attributes: {
|
||||
'!href': true,
|
||||
},
|
||||
classes: {},
|
||||
},
|
||||
},
|
||||
requiredContent: new CKEDITOR.style({
|
||||
element: 'a',
|
||||
attributes: {
|
||||
href: '',
|
||||
},
|
||||
}),
|
||||
modes: { wysiwyg: 1 },
|
||||
canUndo: true,
|
||||
exec(editor) {
|
||||
const drupalImageUtils = CKEDITOR.plugins.drupalimage;
|
||||
const focusedImageWidget =
|
||||
drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
|
||||
let linkElement = getSelectedLink(editor);
|
||||
|
||||
// Set existing values based on selected element.
|
||||
let existingValues = {};
|
||||
if (linkElement && linkElement.$) {
|
||||
existingValues = parseAttributes(editor, linkElement);
|
||||
}
|
||||
// Or, if an image widget is focused, we're editing a link wrapping
|
||||
// an image widget.
|
||||
else if (focusedImageWidget && focusedImageWidget.data.link) {
|
||||
existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
|
||||
}
|
||||
|
||||
// Prepare a save callback to be used upon saving the dialog.
|
||||
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,
|
||||
),
|
||||
);
|
||||
editor.fire('saveSnapshot');
|
||||
return;
|
||||
}
|
||||
|
||||
editor.fire('saveSnapshot');
|
||||
|
||||
// Create a new link element if needed.
|
||||
if (!linkElement && returnValues.attributes.href) {
|
||||
const selection = editor.getSelection();
|
||||
const range = selection.getRanges(1)[0];
|
||||
|
||||
// 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,
|
||||
);
|
||||
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,
|
||||
});
|
||||
style.type = CKEDITOR.STYLE_INLINE;
|
||||
style.applyToRange(range);
|
||||
range.select();
|
||||
|
||||
// Set the link so individual properties may be set below.
|
||||
linkElement = getSelectedLink(editor);
|
||||
}
|
||||
// Update the link properties.
|
||||
else if (linkElement) {
|
||||
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];
|
||||
linkElement.data(`cke-saved-${attrName}`, value);
|
||||
linkElement.setAttribute(attrName, value);
|
||||
}
|
||||
// Delete the property if set to an empty string.
|
||||
else {
|
||||
linkElement.removeAttribute(attrName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save snapshot for undo support.
|
||||
editor.fire('saveSnapshot');
|
||||
};
|
||||
// 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 dialogSettings = {
|
||||
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,
|
||||
);
|
||||
},
|
||||
});
|
||||
editor.addCommand('drupalunlink', {
|
||||
contextSensitive: 1,
|
||||
startDisabled: 1,
|
||||
requiredContent: new CKEDITOR.style({
|
||||
element: 'a',
|
||||
attributes: {
|
||||
href: '',
|
||||
},
|
||||
}),
|
||||
exec(editor) {
|
||||
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()
|
||||
) {
|
||||
this.setState(CKEDITOR.TRISTATE_OFF);
|
||||
} else {
|
||||
this.setState(CKEDITOR.TRISTATE_DISABLED);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// CTRL + K.
|
||||
editor.setKeystroke(CKEDITOR.CTRL + 75, 'drupallink');
|
||||
|
||||
// Add buttons for link and unlink.
|
||||
if (editor.ui.addButton) {
|
||||
editor.ui.addButton('DrupalLink', {
|
||||
label: Drupal.t('Link'),
|
||||
command: 'drupallink',
|
||||
});
|
||||
editor.ui.addButton('DrupalUnlink', {
|
||||
label: Drupal.t('Unlink'),
|
||||
command: 'drupalunlink',
|
||||
});
|
||||
}
|
||||
|
||||
editor.on('doubleclick', evt => {
|
||||
const element = getSelectedLink(editor) || evt.data.element;
|
||||
|
||||
if (!element.isReadOnly()) {
|
||||
if (element.is('a')) {
|
||||
editor.getSelection().selectElement(element);
|
||||
editor.getCommand('drupallink').exec();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If the "menu" plugin is loaded, register the menu items.
|
||||
if (editor.addMenuItems) {
|
||||
editor.addMenuItems({
|
||||
link: {
|
||||
label: Drupal.t('Edit Link'),
|
||||
command: 'drupallink',
|
||||
group: 'link',
|
||||
order: 1,
|
||||
},
|
||||
|
||||
unlink: {
|
||||
label: Drupal.t('Unlink'),
|
||||
command: 'drupalunlink',
|
||||
group: 'link',
|
||||
order: 5,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If the "contextmenu" plugin is loaded, register the listeners.
|
||||
if (editor.contextMenu) {
|
||||
editor.contextMenu.addListener((element, selection) => {
|
||||
if (!element || element.isReadOnly()) {
|
||||
return null;
|
||||
}
|
||||
const anchor = getSelectedLink(editor);
|
||||
if (!anchor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let menu = {};
|
||||
if (anchor.getAttribute('href') && anchor.getChildCount()) {
|
||||
menu = {
|
||||
link: CKEDITOR.TRISTATE_OFF,
|
||||
unlink: CKEDITOR.TRISTATE_OFF,
|
||||
};
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 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.)
|
||||
CKEDITOR.plugins.drupallink = {
|
||||
parseLinkAttributes: parseAttributes,
|
||||
getLinkAttributes: getAttributes,
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings, CKEDITOR);
|
|
@ -1,33 +1,28 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Link plugin.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, drupalSettings, CKEDITOR) {
|
||||
|
||||
'use strict';
|
||||
|
||||
function parseAttributes(editor, element) {
|
||||
var parsedAttributes = {};
|
||||
|
||||
var domElement = element.$;
|
||||
var attribute;
|
||||
var attributeName;
|
||||
var attribute = void 0;
|
||||
var attributeName = void 0;
|
||||
for (var attrIndex = 0; attrIndex < domElement.attributes.length; attrIndex++) {
|
||||
attribute = domElement.attributes.item(attrIndex);
|
||||
attributeName = attribute.nodeName.toLowerCase();
|
||||
// Ignore data-cke-* attributes; they're CKEditor internals.
|
||||
|
||||
if (attributeName.indexOf('data-cke-') === 0) {
|
||||
continue;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Remove any cke_* classes.
|
||||
if (parsedAttributes.class) {
|
||||
parsedAttributes.class = CKEDITOR.tools.trim(parsedAttributes.class.replace(/cke_\S+/, ''));
|
||||
}
|
||||
|
@ -37,23 +32,16 @@
|
|||
|
||||
function getAttributes(editor, data) {
|
||||
var set = {};
|
||||
for (var attributeName in data) {
|
||||
if (data.hasOwnProperty(attributeName)) {
|
||||
set[attributeName] = data[attributeName];
|
||||
}
|
||||
}
|
||||
Object.keys(data || {}).forEach(function (attributeName) {
|
||||
set[attributeName] = data[attributeName];
|
||||
});
|
||||
|
||||
// CKEditor tracks the *actual* saved href in a data-cke-saved-* attribute
|
||||
// to work around browser quirks. We need to update it.
|
||||
set['data-cke-saved-href'] = set.href;
|
||||
|
||||
// Remove all attributes which are not currently set.
|
||||
var removed = {};
|
||||
for (var s in set) {
|
||||
if (set.hasOwnProperty(s)) {
|
||||
delete removed[s];
|
||||
}
|
||||
}
|
||||
Object.keys(set).forEach(function (s) {
|
||||
delete removed[s];
|
||||
});
|
||||
|
||||
return {
|
||||
set: set,
|
||||
|
@ -61,12 +49,27 @@
|
|||
};
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
init: function (editor) {
|
||||
// Add the commands for link and unlink.
|
||||
init: function init(editor) {
|
||||
editor.addCommand('drupallink', {
|
||||
allowedContent: {
|
||||
a: {
|
||||
|
@ -82,28 +85,21 @@
|
|||
href: ''
|
||||
}
|
||||
}),
|
||||
modes: {wysiwyg: 1},
|
||||
modes: { wysiwyg: 1 },
|
||||
canUndo: true,
|
||||
exec: function (editor) {
|
||||
exec: function exec(editor) {
|
||||
var drupalImageUtils = CKEDITOR.plugins.drupalimage;
|
||||
var focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
|
||||
var linkElement = getSelectedLink(editor);
|
||||
|
||||
// Set existing values based on selected element.
|
||||
var existingValues = {};
|
||||
if (linkElement && linkElement.$) {
|
||||
existingValues = parseAttributes(editor, linkElement);
|
||||
}
|
||||
// Or, if an image widget is focused, we're editing a link wrapping
|
||||
// an image widget.
|
||||
else if (focusedImageWidget && focusedImageWidget.data.link) {
|
||||
existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
|
||||
}
|
||||
} else if (focusedImageWidget && focusedImageWidget.data.link) {
|
||||
existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
|
||||
}
|
||||
|
||||
// Prepare a save callback to be used upon saving the dialog.
|
||||
var 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.
|
||||
var saveCallback = function saveCallback(returnValues) {
|
||||
if (focusedImageWidget) {
|
||||
focusedImageWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedImageWidget.data.link));
|
||||
editor.fire('saveSnapshot');
|
||||
|
@ -112,58 +108,45 @@
|
|||
|
||||
editor.fire('saveSnapshot');
|
||||
|
||||
// Create a new link element if needed.
|
||||
if (!linkElement && returnValues.attributes.href) {
|
||||
var selection = editor.getSelection();
|
||||
var range = selection.getRanges(1)[0];
|
||||
|
||||
// Use link URL as text with a collapsed cursor.
|
||||
if (range.collapsed) {
|
||||
// Shorten mailto URLs to just the email address.
|
||||
var 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.
|
||||
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();
|
||||
|
||||
// Set the link so individual properties may be set below.
|
||||
linkElement = getSelectedLink(editor);
|
||||
}
|
||||
// Update the link properties.
|
||||
else if (linkElement) {
|
||||
for (var attrName in returnValues.attributes) {
|
||||
if (returnValues.attributes.hasOwnProperty(attrName)) {
|
||||
// Update the property if a value is specified.
|
||||
} else if (linkElement) {
|
||||
Object.keys(returnValues.attributes || {}).forEach(function (attrName) {
|
||||
if (returnValues.attributes[attrName].length > 0) {
|
||||
var value = returnValues.attributes[attrName];
|
||||
linkElement.data('cke-saved-' + attrName, value);
|
||||
linkElement.setAttribute(attrName, value);
|
||||
}
|
||||
// Delete the property if set to an empty string.
|
||||
else {
|
||||
linkElement.removeAttribute(attrName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
linkElement.removeAttribute(attrName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save snapshot for undo support.
|
||||
editor.fire('saveSnapshot');
|
||||
};
|
||||
// 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.
|
||||
|
||||
var dialogSettings = {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
@ -176,25 +159,26 @@
|
|||
href: ''
|
||||
}
|
||||
}),
|
||||
exec: function (editor) {
|
||||
var style = new CKEDITOR.style({element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1});
|
||||
exec: function exec(editor) {
|
||||
var style = new CKEDITOR.style({
|
||||
element: 'a',
|
||||
type: CKEDITOR.STYLE_INLINE,
|
||||
alwaysRemoveElement: 1
|
||||
});
|
||||
editor.removeStyle(style);
|
||||
},
|
||||
refresh: function (editor, path) {
|
||||
refresh: function refresh(editor, path) {
|
||||
var 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// CTRL + K.
|
||||
editor.setKeystroke(CKEDITOR.CTRL + 75, 'drupallink');
|
||||
|
||||
// Add buttons for link and unlink.
|
||||
if (editor.ui.addButton) {
|
||||
editor.ui.addButton('DrupalLink', {
|
||||
label: Drupal.t('Link'),
|
||||
|
@ -217,7 +201,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
// If the "menu" plugin is loaded, register the menu items.
|
||||
if (editor.addMenuItems) {
|
||||
editor.addMenuItems({
|
||||
link: {
|
||||
|
@ -236,7 +219,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
// If the "contextmenu" plugin is loaded, register the listeners.
|
||||
if (editor.contextMenu) {
|
||||
editor.contextMenu.addListener(function (element, selection) {
|
||||
if (!element || element.isReadOnly()) {
|
||||
|
@ -249,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;
|
||||
});
|
||||
|
@ -257,48 +242,8 @@
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.)
|
||||
CKEDITOR.plugins.drupallink = {
|
||||
parseLinkAttributes: parseAttributes,
|
||||
getLinkAttributes: getAttributes
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, CKEDITOR);
|
||||
})(jQuery, Drupal, drupalSettings, CKEDITOR);
|
266
web/core/modules/ckeditor/js/views/AuralView.es6.js
Normal file
266
web/core/modules/ckeditor/js/views/AuralView.es6.js
Normal file
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the aural view of CKEditor toolbar
|
||||
* configuration.
|
||||
*/
|
||||
|
||||
(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',
|
||||
},
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
);
|
||||
// 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();
|
||||
},
|
||||
},
|
||||
);
|
||||
})(Drupal, Backbone, jQuery);
|
|
@ -1,18 +1,12 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the aural view of CKEditor toolbar
|
||||
* configuration.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor.AuralView = Backbone.View.extend(/** @lends Drupal.ckeditor.AuralView# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
Drupal.ckeditor.AuralView = Backbone.View.extend({
|
||||
events: {
|
||||
'click .ckeditor-buttons a': 'announceButtonHelp',
|
||||
'click .ckeditor-multiple-buttons a': 'announceSeparatorHelp',
|
||||
|
@ -21,52 +15,23 @@
|
|||
'focus .ckeditor-toolbar-group': 'onFocus'
|
||||
},
|
||||
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; aural UX (output only).
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
// Announce the button and group positions when the model is no longer
|
||||
// dirty.
|
||||
initialize: function initialize() {
|
||||
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: function (model, isDirty) {
|
||||
// Announce the position of a button or group after the model has been
|
||||
// updated.
|
||||
announceMove: function announceMove(model, isDirty) {
|
||||
if (!isDirty) {
|
||||
var item = document.activeElement || null;
|
||||
if (item) {
|
||||
var $item = $(item);
|
||||
if ($item.hasClass('ckeditor-toolbar-group')) {
|
||||
this.announceButtonGroupPosition($item);
|
||||
}
|
||||
else if ($item.parent().hasClass('ckeditor-button')) {
|
||||
} 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: function (event) {
|
||||
onFocus: function onFocus(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
var $originalTarget = $(event.target);
|
||||
|
@ -74,19 +39,11 @@
|
|||
var $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')) {
|
||||
} 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: function ($group) {
|
||||
announceButtonGroupPosition: function announceButtonGroupPosition($group) {
|
||||
var $groups = $group.parent().children();
|
||||
var $row = $group.closest('.ckeditor-row');
|
||||
var $rows = $row.parent().children();
|
||||
|
@ -101,22 +58,14 @@
|
|||
'@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: function ($button) {
|
||||
announceButtonPosition: function announceButtonPosition($button) {
|
||||
var $row = $button.closest('.ckeditor-row');
|
||||
var $rows = $row.parent().children();
|
||||
var $buttons = $button.closest('.ckeditor-buttons').children();
|
||||
|
@ -128,12 +77,10 @@
|
|||
var positionCount = $buttons.length;
|
||||
var row = $rows.index($row) + 1;
|
||||
var 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'.
|
||||
var type = ($button.attr('data-drupal-ckeditor-type') === 'separator') ? '' : Drupal.t('button');
|
||||
var text;
|
||||
// The button is located in the available button set.
|
||||
|
||||
var type = $button.attr('data-drupal-ckeditor-type') === 'separator' ? '' : Drupal.t('button');
|
||||
var text = void 0;
|
||||
|
||||
if ($button.closest('.ckeditor-toolbar-disabled').length > 0) {
|
||||
text = Drupal.t('@name @type.', {
|
||||
'@name': $button.children().attr('aria-label'),
|
||||
|
@ -142,45 +89,34 @@
|
|||
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');
|
||||
}
|
||||
},
|
||||
} 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
|
||||
});
|
||||
|
||||
/**
|
||||
* Provides help information when a button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event for the button click.
|
||||
*/
|
||||
announceButtonHelp: function (event) {
|
||||
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 (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');
|
||||
}
|
||||
},
|
||||
announceButtonHelp: function announceButtonHelp(event) {
|
||||
var $link = $(event.currentTarget);
|
||||
var $button = $link.parent();
|
||||
var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
var message;
|
||||
var message = void 0;
|
||||
|
||||
if (enabled) {
|
||||
message = Drupal.t('The "@name" button is currently enabled.', {
|
||||
|
@ -188,8 +124,7 @@
|
|||
});
|
||||
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 {
|
||||
} else {
|
||||
message = Drupal.t('The "@name" button is currently disabled.', {
|
||||
'@name': $link.attr('aria-label')
|
||||
});
|
||||
|
@ -198,26 +133,18 @@
|
|||
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: function (event) {
|
||||
announceSeparatorHelp: function announceSeparatorHelp(event) {
|
||||
var $link = $(event.currentTarget);
|
||||
var $button = $link.parent();
|
||||
var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
var message;
|
||||
var message = void 0;
|
||||
|
||||
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 {
|
||||
} 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')
|
||||
|
@ -229,5 +156,4 @@
|
|||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, jQuery);
|
||||
})(Drupal, Backbone, jQuery);
|
420
web/core/modules/ckeditor/js/views/ControllerView.es6.js
Normal file
420
web/core/modules/ckeditor/js/views/ControllerView.es6.js
Normal file
|
@ -0,0 +1,420 @@
|
|||
/**
|
||||
* @file
|
||||
* 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# */ {
|
||||
/**
|
||||
* @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),
|
||||
);
|
||||
|
||||
// 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');
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
|
||||
// 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', ['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);
|
||||
}
|
||||
});
|
||||
// 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, '-');
|
||||
},
|
||||
},
|
||||
);
|
||||
})(jQuery, Drupal, Backbone, CKEDITOR, _);
|
|
@ -1,175 +1,108 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, Backbone, CKEDITOR, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor.ControllerView = Backbone.View.extend(/** @lends Drupal.ckeditor.ControllerView# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
Drupal.ckeditor.ControllerView = Backbone.View.extend({
|
||||
events: {},
|
||||
|
||||
/**
|
||||
* Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
initialize: function 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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: function (model, isDirty, options) {
|
||||
parseEditorDOM: function parseEditorDOM(model, isDirty, options) {
|
||||
if (isDirty) {
|
||||
var currentConfig = this.model.get('activeEditorConfig');
|
||||
|
||||
// Process the rows.
|
||||
var rows = [];
|
||||
this.$el
|
||||
.find('.ckeditor-active-toolbar-configuration')
|
||||
.children('.ckeditor-row').each(function () {
|
||||
var groups = [];
|
||||
// Process the button groups.
|
||||
$(this).find('.ckeditor-toolbar-group').each(function () {
|
||||
var $group = $(this);
|
||||
var $buttons = $group.find('.ckeditor-button');
|
||||
if ($buttons.length) {
|
||||
var 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);
|
||||
}
|
||||
});
|
||||
if (groups.length) {
|
||||
rows.push(groups);
|
||||
this.$el.find('.ckeditor-active-toolbar-configuration').children('.ckeditor-row').each(function () {
|
||||
var groups = [];
|
||||
|
||||
$(this).find('.ckeditor-toolbar-group').each(function () {
|
||||
var $group = $(this);
|
||||
var $buttons = $group.find('.ckeditor-button');
|
||||
if ($buttons.length) {
|
||||
var 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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
|
||||
// Determine whether we should trigger an event.
|
||||
if (options.broadcast !== false) {
|
||||
var prev = this.getButtonList(currentConfig);
|
||||
var 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]
|
||||
]);
|
||||
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: function (CKEditorConfig, callback) {
|
||||
var getProperties = function (CKEPropertiesList) {
|
||||
return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : [];
|
||||
getCKEditorFeatures: function getCKEditorFeatures(CKEditorConfig, callback) {
|
||||
var getProperties = function getProperties(CKEPropertiesList) {
|
||||
return _.isObject(CKEPropertiesList) ? _.keys(CKEPropertiesList) : [];
|
||||
};
|
||||
|
||||
var convertCKERulesToEditorFeature = function (feature, CKEFeatureRules) {
|
||||
var convertCKERulesToEditorFeature = function convertCKERulesToEditorFeature(feature, CKEFeatureRules) {
|
||||
for (var i = 0; i < CKEFeatureRules.length; i++) {
|
||||
var CKERule = CKEFeatureRules[i];
|
||||
var rule = new Drupal.EditorFeatureHTMLRule();
|
||||
|
||||
// Tags.
|
||||
var tags = getProperties(CKERule.elements);
|
||||
rule.required.tags = (CKERule.propertiesOnly) ? [] : tags;
|
||||
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().
|
||||
var hiddenCKEditorID = 'ckeditor-hidden';
|
||||
if (CKEDITOR.instances[hiddenCKEditorID]) {
|
||||
CKEDITOR.instances[hiddenCKEditorID].destroy(true);
|
||||
}
|
||||
// Load external plugins, if any.
|
||||
|
||||
var hiddenEditorConfig = this.model.get('hiddenEditorConfig');
|
||||
if (hiddenEditorConfig.drupalExternalPlugins) {
|
||||
var externalPlugins = hiddenEditorConfig.drupalExternalPlugins;
|
||||
for (var pluginName in externalPlugins) {
|
||||
if (externalPlugins.hasOwnProperty(pluginName)) {
|
||||
CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], '');
|
||||
}
|
||||
}
|
||||
Object.keys(externalPlugins || {}).forEach(function (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', function (e) {
|
||||
if (e.editor.name === hiddenCKEditorID) {
|
||||
// First collect all CKEditor allowedContent rules.
|
||||
var CKEFeatureRulesMap = {};
|
||||
var rules = e.editor.filter.allowedContent;
|
||||
var rule;
|
||||
var name;
|
||||
var rule = void 0;
|
||||
var name = void 0;
|
||||
for (var i = 0; i < rules.length; i++) {
|
||||
rule = rules[i];
|
||||
name = rule.featureName || ':(';
|
||||
|
@ -179,50 +112,29 @@
|
|||
CKEFeatureRulesMap[name].push(rule);
|
||||
}
|
||||
|
||||
// Now convert these to Drupal.EditorFeature objects. And track which
|
||||
// buttons are mapped to which features.
|
||||
// @see getFeatureForButton()
|
||||
var features = {};
|
||||
var buttonsToFeatures = {};
|
||||
for (var featureName in CKEFeatureRulesMap) {
|
||||
if (CKEFeatureRulesMap.hasOwnProperty(featureName)) {
|
||||
var feature = new Drupal.EditorFeature(featureName);
|
||||
convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]);
|
||||
features[featureName] = feature;
|
||||
var command = e.editor.getCommand(featureName);
|
||||
if (command) {
|
||||
buttonsToFeatures[command.uiItems[0].name] = featureName;
|
||||
}
|
||||
Object.keys(CKEFeatureRulesMap).forEach(function (featureName) {
|
||||
var feature = new Drupal.EditorFeature(featureName);
|
||||
convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]);
|
||||
features[featureName] = feature;
|
||||
var 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: function (button) {
|
||||
// Return false if the button being added is a divider.
|
||||
getFeatureForButton: function getFeatureForButton(button) {
|
||||
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.
|
||||
var 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();
|
||||
}
|
||||
|
@ -233,151 +145,86 @@
|
|||
}
|
||||
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: function (features, buttonsToFeatures) {
|
||||
disableFeaturesDisallowedByFilters: function 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".
|
||||
var existingButtons = [];
|
||||
// Loop through each button group after flattening the groups from the
|
||||
// toolbar row arrays.
|
||||
|
||||
var buttonGroups = _.flatten(this.model.get('activeEditorConfig'));
|
||||
for (var i = 0; i < buttonGroups.length; i++) {
|
||||
// Pull the button names from each toolbar button group.
|
||||
var buttons = buttonGroups[i].items;
|
||||
for (var 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 (var n = 0; n < existingButtons.length; n++) {
|
||||
var button = existingButtons[n];
|
||||
var 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});
|
||||
} else {
|
||||
$('.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="' + button + '"]').detach().appendTo('.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul');
|
||||
|
||||
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: function ($ckeditorToolbar) {
|
||||
broadcastConfigurationChanges: function broadcastConfigurationChanges($ckeditorToolbar) {
|
||||
var view = this;
|
||||
var hiddenEditorConfig = this.model.get('hiddenEditorConfig');
|
||||
var getFeatureForButton = this.getFeatureForButton.bind(this);
|
||||
var 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', function (event, action, button) {
|
||||
var feature = getFeatureForButton(button);
|
||||
$ckeditorToolbar.find('.ckeditor-toolbar-active').on('CKEditorToolbarChanged.ckeditorAdmin', function (event, action, button) {
|
||||
var feature = getFeatureForButton(button);
|
||||
|
||||
// Early-return if the button being added is a divider.
|
||||
if (feature === false) {
|
||||
return;
|
||||
}
|
||||
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.
|
||||
var 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', function (event, settingsChanges) {
|
||||
// Update hidden CKEditor configuration.
|
||||
for (var key in settingsChanges) {
|
||||
if (settingsChanges.hasOwnProperty(key)) {
|
||||
hiddenEditorConfig[key] = settingsChanges[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve features for the updated hidden CKEditor configuration.
|
||||
getCKEditorFeatures(hiddenEditorConfig, function (features) {
|
||||
// Trigger a standardized text editor configuration event for each
|
||||
// feature that was modified by the configuration changes.
|
||||
var featuresMetadata = view.model.get('featuresMetadata');
|
||||
for (var name in features) {
|
||||
if (features.hasOwnProperty(name)) {
|
||||
var 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);
|
||||
});
|
||||
var configEvent = action === 'added' ? 'addedFeature' : 'removedFeature';
|
||||
Drupal.editorConfiguration[configEvent](feature);
|
||||
}).on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (event, settingsChanges) {
|
||||
Object.keys(settingsChanges || {}).forEach(function (key) {
|
||||
hiddenEditorConfig[key] = settingsChanges[key];
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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: function (config) {
|
||||
getCKEditorFeatures(hiddenEditorConfig, function (features) {
|
||||
var featuresMetadata = view.model.get('featuresMetadata');
|
||||
Object.keys(features || {}).forEach(function (name) {
|
||||
var feature = features[name];
|
||||
if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) {
|
||||
Drupal.editorConfiguration.modifiedFeature(feature);
|
||||
}
|
||||
});
|
||||
|
||||
view.model.set('featuresMetadata', features);
|
||||
});
|
||||
});
|
||||
},
|
||||
getButtonList: function getButtonList(config) {
|
||||
var buttons = [];
|
||||
// Remove the rows.
|
||||
|
||||
config = _.flatten(config);
|
||||
|
||||
// Loop through the button groups and pull out the buttons.
|
||||
config.forEach(function (group) {
|
||||
group.items.forEach(function (button) {
|
||||
buttons.push(button);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove the dividing elements if any.
|
||||
return _.without(buttons, '-');
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, Backbone, CKEDITOR, _);
|
||||
})(jQuery, Drupal, Backbone, CKEDITOR, _);
|
312
web/core/modules/ckeditor/js/views/KeyboardView.es6.js
Normal file
312
web/core/modules/ckeditor/js/views/KeyboardView.es6.js
Normal file
|
@ -0,0 +1,312 @@
|
|||
/**
|
||||
* @file
|
||||
* Backbone View providing the aural view of CKEditor keyboard UX configuration.
|
||||
*/
|
||||
|
||||
(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),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @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.
|
||||
];
|
||||
|
||||
// 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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
// 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
|
||||
.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
|
||||
.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)
|
||||
.prepend($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
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, _);
|
|
@ -1,178 +1,98 @@
|
|||
/**
|
||||
* @file
|
||||
* Backbone View providing the aural view of CKEditor keyboard UX configuration.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal, Backbone, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor.KeyboardView = Backbone.View.extend(/** @lends Drupal.ckeditor.KeyboardView# */{
|
||||
|
||||
/**
|
||||
* Backbone View for CKEditor toolbar configuration; keyboard UX.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
// Add keyboard arrow support.
|
||||
Drupal.ckeditor.KeyboardView = Backbone.View.extend({
|
||||
initialize: function initialize() {
|
||||
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));
|
||||
},
|
||||
render: function render() {},
|
||||
onPressButton: function onPressButton(event) {
|
||||
var upDownKeys = [38, 63232, 40, 63233];
|
||||
var leftRightKeys = [37, 63234, 39, 63235];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
render: function () {
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keypresses on a CKEditor configuration button.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The keypress event triggered.
|
||||
*/
|
||||
onPressButton: function (event) {
|
||||
var upDownKeys = [
|
||||
38, // Up arrow.
|
||||
63232, // Safari up arrow.
|
||||
40, // Down arrow.
|
||||
63233 // Safari down arrow.
|
||||
];
|
||||
var 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) {
|
||||
var view = this;
|
||||
var $target = $(event.currentTarget);
|
||||
var $button = $target.parent();
|
||||
var $container = $button.parent();
|
||||
var $group = $button.closest('.ckeditor-toolbar-group');
|
||||
var $row;
|
||||
var $row = void 0;
|
||||
var containerType = $container.data('drupal-ckeditor-button-sorting');
|
||||
var $availableButtons = this.$el.find('[data-drupal-ckeditor-button-sorting="source"]');
|
||||
var $activeButtons = this.$el.find('.ckeditor-toolbar-active');
|
||||
// The current location of the button, just in case it needs to be put
|
||||
// back.
|
||||
var $originalGroup = $group;
|
||||
var dir;
|
||||
|
||||
// Move available buttons between their container and the active
|
||||
// toolbar.
|
||||
var $originalGroup = $group;
|
||||
var dir = void 0;
|
||||
|
||||
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.
|
||||
} else if (containerType === 'target') {
|
||||
if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
|
||||
// Move left.
|
||||
var $siblings = $container.children();
|
||||
var 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);
|
||||
}
|
||||
// Wrap between rows.
|
||||
else {
|
||||
$container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-group').not('.placeholder').find('.ckeditor-toolbar-group-buttons').eq(-1).append($button);
|
||||
} else {
|
||||
$group = $container.parent().prev();
|
||||
if ($group.length > 0) {
|
||||
$group.find('.ckeditor-toolbar-group-buttons').append($button);
|
||||
} else {
|
||||
$container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-group').not('.placeholder').find('.ckeditor-toolbar-group-buttons').eq(-1).append($button);
|
||||
}
|
||||
}
|
||||
} else if (_.indexOf([39, 63235], event.keyCode) > -1) {
|
||||
if (index < $siblings.length - 1) {
|
||||
$button.insertAfter($container.children().eq(index + 1));
|
||||
} else {
|
||||
$container.parent().next().find('.ckeditor-toolbar-group-buttons').prepend($button);
|
||||
}
|
||||
}
|
||||
} else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
|
||||
dir = _.indexOf([38, 63232], event.keyCode) > -1 ? 'prev' : 'next';
|
||||
$row = $container.closest('.ckeditor-row')[dir]();
|
||||
|
||||
if (dir === 'prev' && $row.length === 0) {
|
||||
if ($button.data('drupal-ckeditor-type') === 'separator') {
|
||||
$button.off().remove();
|
||||
|
||||
$activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).children().eq(0).children().trigger('focus');
|
||||
} else {
|
||||
$availableButtons.prepend($button);
|
||||
}
|
||||
} else {
|
||||
$row.find('.ckeditor-toolbar-group-buttons').eq(0).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);
|
||||
}
|
||||
} else if (containerType === 'dividers') {
|
||||
if (_.indexOf([40, 63233], event.keyCode) > -1) {
|
||||
$button = $button.clone(true);
|
||||
$activeButtons.find('.ckeditor-toolbar-group-buttons').eq(0).prepend($button);
|
||||
$target = $button.children();
|
||||
}
|
||||
}
|
||||
// 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).prepend($button);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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, function (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.
|
||||
Drupal.ckeditor.registerButtonMove(this, $button, function (result) {
|
||||
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.
|
||||
} else {
|
||||
view.$el.find('.ui-sortable').sortable('refresh');
|
||||
}
|
||||
|
||||
$target.trigger('focus');
|
||||
});
|
||||
|
||||
|
@ -180,34 +100,13 @@
|
|||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
onPressGroup: function onPressGroup(event) {
|
||||
var upDownKeys = [38, 63232, 40, 63233];
|
||||
var leftRightKeys = [37, 63234, 39, 63235];
|
||||
|
||||
/**
|
||||
* Handles keypresses on a CKEditor configuration group.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The keypress event triggered.
|
||||
*/
|
||||
onPressGroup: function (event) {
|
||||
var upDownKeys = [
|
||||
38, // Up arrow.
|
||||
63232, // Safari up arrow.
|
||||
40, // Down arrow.
|
||||
63233 // Safari down arrow.
|
||||
];
|
||||
var 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) {
|
||||
var 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(function () {
|
||||
Drupal.ckeditor.openGroupNameDialog(view, $(event.currentTarget));
|
||||
}, 0);
|
||||
|
@ -215,45 +114,34 @@
|
|||
event.stopPropagation();
|
||||
}
|
||||
|
||||
// Respond to direction key presses.
|
||||
if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
|
||||
var $group = $(event.currentTarget);
|
||||
var $container = $group.parent();
|
||||
var $siblings = $container.children();
|
||||
var index;
|
||||
var dir;
|
||||
// Move groups between sibling groups.
|
||||
var index = void 0;
|
||||
var dir = void 0;
|
||||
|
||||
if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
|
||||
index = $siblings.index($group);
|
||||
// Move left between sibling groups.
|
||||
if ((_.indexOf([37, 63234], event.keyCode) > -1)) {
|
||||
|
||||
if (_.indexOf([37, 63234], event.keyCode) > -1) {
|
||||
if (index > 0) {
|
||||
$group.insertBefore($siblings.eq(index - 1));
|
||||
} else {
|
||||
var $rowChildElement = $container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-groups').children().eq(-1);
|
||||
$group.insertBefore($rowChildElement);
|
||||
}
|
||||
} else if (_.indexOf([39, 63235], event.keyCode) > -1) {
|
||||
if (!$siblings.eq(index + 1).hasClass('placeholder')) {
|
||||
$group.insertAfter($container.children().eq(index + 1));
|
||||
} else {
|
||||
$container.closest('.ckeditor-row').next().find('.ckeditor-toolbar-groups').prepend($group);
|
||||
}
|
||||
}
|
||||
// Wrap between rows. Insert the group before the placeholder group
|
||||
// at the end of the previous row.
|
||||
else {
|
||||
$group.insertBefore($container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-groups').children().eq(-1));
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
// 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');
|
||||
|
@ -262,5 +150,4 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, Backbone, _);
|
||||
})(jQuery, Drupal, Backbone, _);
|
315
web/core/modules/ckeditor/js/views/VisualView.es6.js
Normal file
315
web/core/modules/ckeditor/js/views/VisualView.es6.js
Normal file
|
@ -0,0 +1,315 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the visual UX view of CKEditor toolbar
|
||||
* configuration.
|
||||
*/
|
||||
|
||||
(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',
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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');
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
|
@ -1,204 +1,99 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the visual UX view of CKEditor toolbar
|
||||
* configuration.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.ckeditor.VisualView = Backbone.View.extend(/** @lends Drupal.ckeditor.VisualView# */{
|
||||
|
||||
Drupal.ckeditor.VisualView = Backbone.View.extend({
|
||||
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: function () {
|
||||
initialize: function 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());
|
||||
$(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: function (model, value, changedAttributes) {
|
||||
render: function render(model, value, changedAttributes) {
|
||||
this.insertPlaceholders();
|
||||
this.applySorting();
|
||||
|
||||
// Toggle button group names.
|
||||
var 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});
|
||||
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);
|
||||
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: function (event) {
|
||||
onGroupNameClick: function onGroupNameClick(event) {
|
||||
var $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: function (event) {
|
||||
onGroupNamesToggleClick: function 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: function (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.
|
||||
*/
|
||||
onAddGroupButtonClick: function onAddGroupButtonClick(event) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
// 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: function (event, ui) {
|
||||
endGroupDrag: function endGroupDrag(event, ui) {
|
||||
var view = this;
|
||||
Drupal.ckeditor.registerGroupMove(this, ui.item, function (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: function (event, ui) {
|
||||
startButtonDrag: function 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: function (event, ui) {
|
||||
endButtonDrag: function endButtonDrag(event, ui) {
|
||||
var view = this;
|
||||
Drupal.ckeditor.registerButtonMove(this, ui.item, function (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: function () {
|
||||
// Make the buttons sortable.
|
||||
applySorting: function applySorting() {
|
||||
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',
|
||||
|
@ -208,66 +103,43 @@
|
|||
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: function () {
|
||||
insertPlaceholders: function insertPlaceholders() {
|
||||
this.insertPlaceholderRow();
|
||||
this.insertNewGroupButtons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a blank row at the bottom of the CKEditor configuration.
|
||||
*/
|
||||
insertPlaceholderRow: function () {
|
||||
insertPlaceholderRow: function insertPlaceholderRow() {
|
||||
var $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'));
|
||||
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.
|
||||
|
||||
var len = $rows.length;
|
||||
$rows.filter(function (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();
|
||||
}).remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a button in each row that will add a new CKEditor button group.
|
||||
*/
|
||||
insertNewGroupButtons: function () {
|
||||
// Insert an add group button to each row.
|
||||
insertNewGroupButtons: function insertNewGroupButtons() {
|
||||
this.$el.find('.ckeditor-row').each(function () {
|
||||
var $row = $(this);
|
||||
var $groups = $row.find('.ckeditor-toolbar-group');
|
||||
var $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'));
|
||||
}
|
||||
} else if (!$groups.eq(-1).hasClass('ckeditor-add-new-group')) {
|
||||
$button.appendTo($row.children('.ckeditor-toolbar-groups'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, jQuery);
|
||||
})(Drupal, Backbone, jQuery);
|
|
@ -24,6 +24,10 @@ class CKEditorPlugin extends Plugin {
|
|||
/**
|
||||
* The plugin ID.
|
||||
*
|
||||
* This MUST match the name of the CKEditor plugin itself (written in
|
||||
* JavaScript). Otherwise CKEditor will throw JavaScript errors when it runs,
|
||||
* because it fails to load this CKEditor plugin.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -101,7 +101,8 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
|
|||
public function getConfig(Editor $editor) {
|
||||
// Reasonable defaults that provide expected basic behavior.
|
||||
$config = [
|
||||
'customConfig' => '', // Don't load CKEditor's config.js file.
|
||||
// Don't load CKEditor's config.js file.
|
||||
'customConfig' => '',
|
||||
'pasteFromWordPromptCleanup' => TRUE,
|
||||
'resize_dir' => 'vertical',
|
||||
'justifyClasses' => ['text-align-left', 'text-align-center', 'text-align-right', 'text-align-justify'],
|
||||
|
@ -126,7 +127,7 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function getButtons() {
|
||||
$button = function($name, $direction = 'ltr') {
|
||||
$button = function ($name, $direction = 'ltr') {
|
||||
// In the markup below, we mostly use the name (which may include spaces),
|
||||
// but in one spot we use it as a CSS class, so strip spaces.
|
||||
// Note: this uses str_replace() instead of Html::cleanCssIdentifier()
|
||||
|
@ -420,8 +421,8 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
|
|||
}
|
||||
// Generate setting that accurately reflects allowed tags and attributes.
|
||||
else {
|
||||
$get_attribute_values = function($attribute_values, $allowed_values) {
|
||||
$values = array_keys(array_filter($attribute_values, function($value) use ($allowed_values) {
|
||||
$get_attribute_values = function ($attribute_values, $allowed_values) {
|
||||
$values = array_keys(array_filter($attribute_values, function ($value) use ($allowed_values) {
|
||||
if ($allowed_values) {
|
||||
return $value !== FALSE;
|
||||
}
|
||||
|
@ -532,7 +533,7 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
|
|||
// getConfig() method, and override the JavaScript at
|
||||
// Drupal.editors.ckeditor to somehow make validation of values for
|
||||
// attributes other than "class" and "style" work.
|
||||
$allowed_attributes = array_filter($attributes, function($value) {
|
||||
$allowed_attributes = array_filter($attributes, function ($value) {
|
||||
return $value !== FALSE;
|
||||
});
|
||||
if (count($allowed_attributes)) {
|
||||
|
@ -567,7 +568,7 @@ class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInter
|
|||
// implies that all of its possible attribute values are disallowed,
|
||||
// thus we must look at the disallowed attribute values on allowed
|
||||
// attributes.
|
||||
$disallowed_attributes = array_filter($attributes, function($value) {
|
||||
$disallowed_attributes = array_filter($attributes, function ($value) {
|
||||
return $value === FALSE;
|
||||
});
|
||||
if (count($disallowed_attributes)) {
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -101,7 +101,9 @@ class StylesCombo extends CKEditorPluginBase implements CKEditorPluginConfigurab
|
|||
$form_state->setError($element, $this->t('The provided list of styles is syntactically incorrect.'));
|
||||
}
|
||||
else {
|
||||
$style_names = array_map(function ($style) { return $style['name']; }, $styles_setting);
|
||||
$style_names = array_map(function ($style) {
|
||||
return $style['name'];
|
||||
}, $styles_setting);
|
||||
if (count($style_names) !== count(array_unique($style_names))) {
|
||||
$form_state->setError($element, $this->t('Each style must have a unique label.'));
|
||||
}
|
||||
|
@ -155,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.
|
||||
|
@ -194,7 +194,7 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
|
|||
}
|
||||
}
|
||||
// Get a list of all buttons that are provided by all plugins.
|
||||
$all_buttons = array_reduce($this->ckeditorPluginManager->getButtons(), function($result, $item) {
|
||||
$all_buttons = array_reduce($this->ckeditorPluginManager->getButtons(), function ($result, $item) {
|
||||
return array_merge($result, array_keys($item));
|
||||
}, []);
|
||||
// Build a fake Editor object, which we'll use to generate JavaScript
|
||||
|
@ -210,8 +210,8 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
|
|||
0 => [
|
||||
'name' => 'All existing buttons',
|
||||
'items' => $all_buttons,
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'plugins' => $settings['plugins'],
|
||||
|
@ -418,7 +418,7 @@ class CKEditor extends EditorBase implements ContainerFactoryPluginInterface {
|
|||
];
|
||||
$this->moduleHandler->alter('ckeditor_css', $css, $editor);
|
||||
// Get a list of all enabled plugins' iframe instance CSS files.
|
||||
$plugins_css = array_reduce($this->ckeditorPluginManager->getCssFiles($editor), function($result, $item) {
|
||||
$plugins_css = array_reduce($this->ckeditorPluginManager->getCssFiles($editor), function ($result, $item) {
|
||||
return array_merge($result, array_values($item));
|
||||
}, []);
|
||||
$css = array_merge($css, $plugins_css);
|
||||
|
|
19
web/core/modules/ckeditor/tests/modules/js/ajax-css.es6.js
Normal file
19
web/core/modules/ckeditor/tests/modules/js/ajax-css.es6.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* @file
|
||||
* Contains client-side code for testing CSS delivered to CKEditor via AJAX.
|
||||
*/
|
||||
|
||||
(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() {
|
||||
ckeditor.attachInlineEditor(this, editorSettings.formats.test_format);
|
||||
});
|
||||
},
|
||||
};
|
||||
})(Drupal, Drupal.editors.ckeditor, drupalSettings.editor, jQuery);
|
|
@ -1,24 +1,16 @@
|
|||
/**
|
||||
* @file
|
||||
* Contains client-side code for testing CSS delivered to CKEditor via AJAX.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal, ckeditor, editorSettings, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.behaviors.ajaxCssForm = {
|
||||
|
||||
attach: function (context) {
|
||||
// Initialize an inline CKEditor on the #edit-inline element if it
|
||||
// isn't editable already.
|
||||
$(context)
|
||||
.find('#edit-inline')
|
||||
.not('[contenteditable]')
|
||||
.each(function () {
|
||||
ckeditor.attachInlineEditor(this, editorSettings.formats.test_format);
|
||||
});
|
||||
attach: function attach(context) {
|
||||
$(context).find('#edit-inline').not('[contenteditable]').each(function () {
|
||||
ckeditor.attachInlineEditor(this, editorSettings.formats.test_format);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(Drupal, Drupal.editors.ckeditor, drupalSettings.editor, jQuery);
|
||||
})(Drupal, Drupal.editors.ckeditor, drupalSettings.editor, jQuery);
|
|
@ -9,6 +9,8 @@ use Drupal\Core\Form\FormStateInterface;
|
|||
|
||||
/**
|
||||
* A form for testing delivery of CSS to CKEditor via AJAX.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AjaxCssForm extends FormBase {
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ use Drupal\Core\Form\FormStateInterface;
|
|||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines a "LlamaContextualAndbutton" plugin, with a contextually OR toolbar
|
||||
* Defines a "LlamaContextualAndButton" plugin, with a contextually OR toolbar
|
||||
* builder-enabled "llama" feature.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -179,7 +179,7 @@ class CKEditorAdminTest extends BrowserTestBase {
|
|||
// JavaScript's drupalSettings, and Unicode-escaped) is correctly rendered.
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
// Create function to encode HTML as we expect it in drupalSettings.
|
||||
$json_encode = function($html) {
|
||||
$json_encode = function ($html) {
|
||||
return trim(Json::encode($html), '"');
|
||||
};
|
||||
// Check the Button separator.
|
||||
|
@ -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.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -101,13 +101,17 @@ class CKEditorLoadingTest extends BrowserTestBase {
|
|||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
|
||||
$ckeditor_plugin = $this->container->get('plugin.manager.editor')->createInstance('ckeditor');
|
||||
$editor = Editor::load('filtered_html');
|
||||
$expected = ['formats' => ['filtered_html' => [
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
'editorSettings' => $this->castSafeStrings($ckeditor_plugin->getJSSettings($editor)),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
]]];
|
||||
$expected = [
|
||||
'formats' => [
|
||||
'filtered_html' => [
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
'editorSettings' => $this->castSafeStrings($ckeditor_plugin->getJSSettings($editor)),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $this->castSafeStrings($settings['editor']), "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
|
@ -138,7 +142,9 @@ class CKEditorLoadingTest extends BrowserTestBase {
|
|||
'editorSettings' => $this->castSafeStrings($ckeditor_plugin->getJSSettings($editor)),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
]]];
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $this->castSafeStrings($settings['editor']), "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace Drupal\Tests\ckeditor\Functional;
|
||||
|
||||
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
@ -67,7 +66,7 @@ class CKEditorToolbarButtonTest extends BrowserTestBase {
|
|||
$this->drupalGet('admin/config/content/formats/manage/full_html');
|
||||
|
||||
// Check if any image button is loaded in CKEditor json.
|
||||
$json_encode = function($html) {
|
||||
$json_encode = function ($html) {
|
||||
return trim(Json::encode($html), '"');
|
||||
};
|
||||
$markup = $json_encode(file_url_transform_relative(file_create_url('core/modules/ckeditor/js/plugins/drupalimage/icons/drupalimage.png')));
|
||||
|
|
|
@ -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}
|
||||
|
@ -55,7 +55,7 @@ class AjaxCssTest extends JavascriptTestBase {
|
|||
// but not the iframe.
|
||||
$page->pressButton('Add CSS to inline CKEditor instance');
|
||||
|
||||
$result = $page->waitFor(10, function() use ($style_color) {
|
||||
$result = $page->waitFor(10, function () use ($style_color) {
|
||||
return ($this->getEditorStyle('edit-inline', 'color') == $style_color)
|
||||
&& ($this->getEditorStyle('edit-iframe-value', 'color') != $style_color);
|
||||
});
|
||||
|
@ -70,7 +70,7 @@ class AjaxCssTest extends JavascriptTestBase {
|
|||
// but not the main body.
|
||||
$page->pressButton('Add CSS to iframe CKEditor instance');
|
||||
|
||||
$result = $page->waitFor(10, function() use ($style_color) {
|
||||
$result = $page->waitFor(10, function () use ($style_color) {
|
||||
return ($this->getEditorStyle('edit-inline', 'color') != $style_color)
|
||||
&& ($this->getEditorStyle('edit-iframe-value', 'color') == $style_color);
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
@ -24,6 +24,13 @@ class CKEditorIntegrationTest extends JavascriptTestBase {
|
|||
*/
|
||||
protected $account;
|
||||
|
||||
/**
|
||||
* The FilterFormat config entity used for testing.
|
||||
*
|
||||
* @var \Drupal\filter\FilterFormatInterface
|
||||
*/
|
||||
protected $filterFormat;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -36,12 +43,12 @@ class CKEditorIntegrationTest extends JavascriptTestBase {
|
|||
parent::setUp();
|
||||
|
||||
// Create a text format and associate CKEditor.
|
||||
$filtered_html_format = FilterFormat::create([
|
||||
$this->filterFormat = FilterFormat::create([
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
]);
|
||||
$filtered_html_format->save();
|
||||
$this->filterFormat->save();
|
||||
|
||||
Editor::create([
|
||||
'format' => 'filtered_html',
|
||||
|
@ -92,9 +99,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.');
|
||||
|
@ -118,4 +126,55 @@ class CKEditorIntegrationTest extends JavascriptTestBase {
|
|||
self::assertEquals($before_url, $after_url, 'History back works.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the Image button appears and works as expected.
|
||||
*/
|
||||
public function testDrupalImageDialog() {
|
||||
$session = $this->getSession();
|
||||
$web_assert = $this->assertSession();
|
||||
|
||||
$this->drupalGet('node/add/page');
|
||||
$session->getPage();
|
||||
|
||||
// Asserts the Image button is present in the toolbar.
|
||||
$web_assert->elementExists('css', '#cke_edit-body-0-value .cke_button__drupalimage');
|
||||
|
||||
// Asserts the image dialog opens when clicking the Image button.
|
||||
$this->click('.cke_button__drupalimage');
|
||||
$this->assertNotEmpty($web_assert->waitForElement('css', '.ui-dialog'));
|
||||
|
||||
$web_assert->elementContains('css', '.ui-dialog .ui-dialog-titlebar', 'Insert Image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the Drupal Image Caption plugin appears and works as expected.
|
||||
*/
|
||||
public function testDrupalImageCaptionDialog() {
|
||||
$web_assert = $this->assertSession();
|
||||
|
||||
// Disable the caption filter.
|
||||
$this->filterFormat->setFilterConfig('filter_caption', [
|
||||
'status' => FALSE,
|
||||
]);
|
||||
$this->filterFormat->save();
|
||||
|
||||
// If the caption filter is disabled, its checkbox should be absent.
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->click('.cke_button__drupalimage');
|
||||
$this->assertNotEmpty($web_assert->waitForElement('css', '.ui-dialog'));
|
||||
$web_assert->elementNotExists('css', '.ui-dialog input[name="attributes[hasCaption]"]');
|
||||
|
||||
// Enable the caption filter again.
|
||||
$this->filterFormat->setFilterConfig('filter_caption', [
|
||||
'status' => TRUE,
|
||||
]);
|
||||
$this->filterFormat->save();
|
||||
|
||||
// If the caption filter is enabled, its checkbox should be present.
|
||||
$this->drupalGet('node/add/page');
|
||||
$this->click('.cke_button__drupalimage');
|
||||
$this->assertNotEmpty($web_assert->waitForElement('css', '.ui-dialog'));
|
||||
$web_assert->elementExists('css', '.ui-dialog input[name="attributes[hasCaption]"]');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>',
|
||||
]
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
@ -479,7 +479,7 @@ class CKEditorTest extends KernelTestBase {
|
|||
],
|
||||
[
|
||||
'name' => 'Tools',
|
||||
'items' => ['Source', ],
|
||||
'items' => ['Source'],
|
||||
],
|
||||
'/',
|
||||
];
|
|
@ -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 a new issue