Move into nested docroot
This commit is contained in:
parent
83a0d3a149
commit
c8b70abde9
13405 changed files with 0 additions and 0 deletions
499
web/core/modules/ckeditor/js/ckeditor.admin.js
Normal file
499
web/core/modules/ckeditor/js/ckeditor.admin.js
Normal file
|
@ -0,0 +1,499 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor button and group configuration user interface.
|
||||
*/
|
||||
|
||||
(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.
|
||||
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');
|
||||
|
||||
// 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({
|
||||
$textarea: $textarea,
|
||||
activeEditorConfig: JSON.parse($textarea.val()),
|
||||
hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig
|
||||
});
|
||||
|
||||
// Create the configuration Views.
|
||||
var viewDefaults = {
|
||||
model: 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: function (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.
|
||||
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;
|
||||
var buttons = Drupal.ckeditor.views.controller.getButtonList(config);
|
||||
var $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
|
||||
for (var 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: function (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;
|
||||
}
|
||||
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: function (view, $group) {
|
||||
// Remove placeholder classes if necessary.
|
||||
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) {
|
||||
$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: function (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]);
|
||||
}
|
||||
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
|
||||
.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
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Cancel'),
|
||||
click: function () {
|
||||
closeDialog('cancel');
|
||||
}
|
||||
}
|
||||
],
|
||||
open: function () {
|
||||
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();
|
||||
}
|
||||
closeDialog(action, form);
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
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.', {
|
||||
'@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name')
|
||||
});
|
||||
}
|
||||
Drupal.announce(text);
|
||||
},
|
||||
close: function (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: function (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.
|
||||
$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 + ']');
|
||||
|
||||
// No settings for this button.
|
||||
if ($pluginSettings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 (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 () {
|
||||
var 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, _);
|
45
web/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
Normal file
45
web/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor 'drupalimage' plugin admin behavior.
|
||||
*/
|
||||
|
||||
(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 () {
|
||||
$('[data-ckeditor-plugin-id="drupalimage"]').drupalSetSummary(function (context) {
|
||||
var root = 'input[name="editor[settings][plugins][drupalimage][image_upload]';
|
||||
var $status = $(root + '[status]"]');
|
||||
var $maxFileSize = $(root + '[max_size]"]');
|
||||
var $maxWidth = $(root + '[max_dimensions][width]"]');
|
||||
var $maxHeight = $(root + '[max_dimensions][height]"]');
|
||||
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() + ')' : '';
|
||||
|
||||
if (!$status.is(':checked')) {
|
||||
return Drupal.t('Uploads disabled');
|
||||
}
|
||||
|
||||
var 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);
|
293
web/core/modules/ckeditor/js/ckeditor.js
vendored
Normal file
293
web/core/modules/ckeditor/js/ckeditor.js
vendored
Normal file
|
@ -0,0 +1,293 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor implementation of {@link Drupal.editors} API.
|
||||
*/
|
||||
|
||||
(function (Drupal, debounce, CKEDITOR, $) {
|
||||
|
||||
'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) {
|
||||
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});
|
||||
|
||||
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) {
|
||||
var 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 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) {
|
||||
var editor = CKEDITOR.dom.element.get(element).getEditor();
|
||||
if (editor) {
|
||||
editor.on('change', debounce(function () {
|
||||
callback(editor.getData());
|
||||
}, 400));
|
||||
}
|
||||
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) {
|
||||
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',
|
||||
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.)
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: function (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], '');
|
||||
}
|
||||
}
|
||||
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.
|
||||
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);
|
||||
|
||||
var ckeditorAjaxDialog = Drupal.ajax({
|
||||
dialog: dialogSettings,
|
||||
dialogType: 'modal',
|
||||
selector: '.ckeditor-dialog-loading-link',
|
||||
url: url,
|
||||
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'});
|
||||
}, 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 () {
|
||||
$(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;
|
||||
}
|
||||
});
|
||||
|
||||
// Redirect on hash change when the original hash has an associated CKEditor.
|
||||
function redirectTextareaFragmentToCKEditorInstance() {
|
||||
var hash = 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).on('hashchange.ckeditor', redirectTextareaFragmentToCKEditorInstance);
|
||||
|
||||
// Set the CKEditor cache-busting string to the same value as Drupal.
|
||||
CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp;
|
||||
|
||||
})(Drupal, Drupal.debounce, CKEDITOR, jQuery);
|
16
web/core/modules/ckeditor/js/ckeditor.language.admin.js
Normal file
16
web/core/modules/ckeditor/js/ckeditor.language.admin.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
(function ($, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Provides the summary for the "language" plugin settings vertical tab.
|
||||
*/
|
||||
Drupal.behaviors.ckeditorLanguageSettingsSummary = {
|
||||
attach: function () {
|
||||
$('#edit-editor-settings-plugins-language').drupalSetSummary(function (context) {
|
||||
return $('#edit-editor-settings-plugins-language-language-list-type option:selected').text();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
128
web/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
Normal file
128
web/core/modules/ckeditor/js/ckeditor.stylescombo.admin.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* @file
|
||||
* CKEditor StylesCombo admin behavior.
|
||||
*/
|
||||
|
||||
(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) {
|
||||
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 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}
|
||||
]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
var stylesSet = [];
|
||||
|
||||
styles = styles.replace(/\r/g, '\n');
|
||||
var lines = styles.split('\n');
|
||||
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(' ')},
|
||||
element: 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: function () {
|
||||
$('[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});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, _);
|
75
web/core/modules/ckeditor/js/models/Model.js
Normal file
75
web/core/modules/ckeditor/js/models/Model.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone Model for the state of a CKEditor toolbar configuration .
|
||||
*/
|
||||
|
||||
(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.
|
||||
*/
|
||||
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.
|
||||
this.get('$textarea').val(JSON.stringify(this.get('activeEditorConfig')));
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone);
|
Binary file not shown.
After Width: | Height: | Size: 470 B |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
371
web/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
Normal file
371
web/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
Normal file
|
@ -0,0 +1,371 @@
|
|||
/**
|
||||
* @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) {
|
||||
|
||||
'use strict';
|
||||
|
||||
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.
|
||||
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
|
||||
},
|
||||
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.
|
||||
var 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.
|
||||
else 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);
|
||||
var captionedClasses = (this.editor.config.image2_captionedClass || '').split(/\s+/);
|
||||
|
||||
if (captionedClasses.length && classes) {
|
||||
for (var 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) {
|
||||
var dialogValues = {};
|
||||
var map = widgetDefinition._mapDataToDialog;
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach(function (key) {
|
||||
dialogValues[map[key]] = data[key];
|
||||
});
|
||||
return dialogValues;
|
||||
};
|
||||
|
||||
// Protected; the inverse of _dataToDialogValues.
|
||||
widgetDefinition._dialogValuesToData = function (dialogReturnValues) {
|
||||
var data = {};
|
||||
var map = widgetDefinition._mapDataToDialog;
|
||||
Object.keys(widgetDefinition._mapDataToDialog).forEach(function (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) {
|
||||
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');
|
||||
});
|
||||
|
||||
return widget;
|
||||
};
|
||||
};
|
||||
|
||||
var 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', function (event) {
|
||||
var widget = event.data;
|
||||
|
||||
if (widget.name !== 'image') {
|
||||
return;
|
||||
}
|
||||
|
||||
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},
|
||||
canUndo: true,
|
||||
exec: function (editor, data) {
|
||||
var 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: function (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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* @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) {
|
||||
|
||||
'use strict';
|
||||
|
||||
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).
|
||||
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]'
|
||||
},
|
||||
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.
|
||||
var 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.
|
||||
var originalDowncast = widgetDefinition.downcast;
|
||||
widgetDefinition.downcast = function (element) {
|
||||
var img = findElementByName(element, 'img');
|
||||
originalDowncast.call(this, img);
|
||||
|
||||
var caption = this.editables.caption;
|
||||
var captionHtml = caption && caption.getData();
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
else {
|
||||
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']) {
|
||||
return;
|
||||
}
|
||||
|
||||
element = originalUpcast.call(this, element, data);
|
||||
var attrs = element.attributes;
|
||||
|
||||
if (element.parent.name === 'a') {
|
||||
element = element.parent;
|
||||
}
|
||||
|
||||
var retElement = element;
|
||||
var 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) {
|
||||
var index = element.getIndex();
|
||||
var splitBefore = index > 0;
|
||||
var 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) {
|
||||
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;
|
||||
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',
|
||||
'data-caption': 'data-caption',
|
||||
'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));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
// Low priority to ensure drupalimage's event handler runs first.
|
||||
}, null, null, 20);
|
||||
},
|
||||
|
||||
afterInit: function (editor) {
|
||||
var disableButtonIfOnWidget = function (evt) {
|
||||
var 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) {
|
||||
var cmd;
|
||||
var commands = ['justifyleft', 'justifycenter', 'justifyright', 'justifyblock'];
|
||||
for (var n = 0; n < commands.length; n++) {
|
||||
cmd = editor.getCommand(commands[n]);
|
||||
cmd.contextSensitive = 1;
|
||||
cmd.on('refresh', disableButtonIfOnWidget, null, null, 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);
|
Binary file not shown.
After Width: | Height: | Size: 328 B |
Binary file not shown.
After Width: | Height: | Size: 312 B |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
304
web/core/modules/ckeditor/js/plugins/drupallink/plugin.js
Normal file
304
web/core/modules/ckeditor/js/plugins/drupallink/plugin.js
Normal file
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Link plugin.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, CKEDITOR) {
|
||||
|
||||
'use strict';
|
||||
|
||||
function parseAttributes(editor, element) {
|
||||
var parsedAttributes = {};
|
||||
|
||||
var domElement = element.$;
|
||||
var attribute = null;
|
||||
var attributeName;
|
||||
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+/, ''));
|
||||
}
|
||||
|
||||
return parsedAttributes;
|
||||
}
|
||||
|
||||
function getAttributes(editor, data) {
|
||||
var set = {};
|
||||
for (var attributeName in data) {
|
||||
if (data.hasOwnProperty(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];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
set: set,
|
||||
removed: CKEDITOR.tools.objectKeys(removed)
|
||||
};
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupallink', {
|
||||
icons: 'drupallink,drupalunlink',
|
||||
hidpi: true,
|
||||
|
||||
init: function (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: function (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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
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) {
|
||||
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});
|
||||
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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
editor.addCommand('drupalunlink', {
|
||||
contextSensitive: 1,
|
||||
startDisabled: 1,
|
||||
requiredContent: new CKEDITOR.style({
|
||||
element: 'a',
|
||||
attributes: {
|
||||
href: ''
|
||||
}
|
||||
}),
|
||||
exec: function (editor) {
|
||||
var style = new CKEDITOR.style({element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1});
|
||||
editor.removeStyle(style);
|
||||
},
|
||||
refresh: function (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 {
|
||||
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', function (evt) {
|
||||
var 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(function (element, selection) {
|
||||
if (!element || element.isReadOnly()) {
|
||||
return null;
|
||||
}
|
||||
var anchor = getSelectedLink(editor);
|
||||
if (!anchor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var menu = {};
|
||||
if (anchor.getAttribute('href') && anchor.getChildCount()) {
|
||||
menu = {link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF};
|
||||
}
|
||||
return menu;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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);
|
233
web/core/modules/ckeditor/js/views/AuralView.js
Normal file
233
web/core/modules/ckeditor/js/views/AuralView.js
Normal file
|
@ -0,0 +1,233 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the aural view of CKEditor toolbar
|
||||
* configuration.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
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: function () {
|
||||
// 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: function (model, isDirty) {
|
||||
// Announce the position of a button or group after the model has been
|
||||
// updated.
|
||||
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')) {
|
||||
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) {
|
||||
event.stopPropagation();
|
||||
|
||||
var $originalTarget = $(event.target);
|
||||
var $currentTarget = $(event.currentTarget);
|
||||
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')) {
|
||||
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) {
|
||||
var $groups = $group.parent().children();
|
||||
var $row = $group.closest('.ckeditor-row');
|
||||
var $rows = $row.parent().children();
|
||||
var position = $groups.index($group) + 1;
|
||||
var positionCount = $groups.not('.placeholder').length;
|
||||
var row = $rows.index($row) + 1;
|
||||
var rowCount = $rows.not('.placeholder').length;
|
||||
var 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: function ($button) {
|
||||
var $row = $button.closest('.ckeditor-row');
|
||||
var $rows = $row.parent().children();
|
||||
var $buttons = $button.closest('.ckeditor-buttons').children();
|
||||
var $group = $button.closest('.ckeditor-toolbar-group');
|
||||
var $groups = $group.parent().children();
|
||||
var groupPosition = $groups.index($group) + 1;
|
||||
var groupPositionCount = $groups.not('.placeholder').length;
|
||||
var position = $buttons.index($button) + 1;
|
||||
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.
|
||||
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: function (event) {
|
||||
var $link = $(event.currentTarget);
|
||||
var $button = $link.parent();
|
||||
var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
var 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: function (event) {
|
||||
var $link = $(event.currentTarget);
|
||||
var $button = $link.parent();
|
||||
var enabled = $button.closest('.ckeditor-toolbar-active').length > 0;
|
||||
var 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);
|
383
web/core/modules/ckeditor/js/views/ControllerView.js
Normal file
383
web/core/modules/ckeditor/js/views/ControllerView.js
Normal file
|
@ -0,0 +1,383 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View acting as a controller for CKEditor toolbar configuration.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, Backbone, CKEDITOR, _) {
|
||||
|
||||
'use strict';
|
||||
|
||||
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: function () {
|
||||
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) {
|
||||
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.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]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) : [];
|
||||
};
|
||||
|
||||
var convertCKERulesToEditorFeature = function (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.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::settingsForm.
|
||||
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], '');
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
for (var 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()
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
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();
|
||||
}
|
||||
var 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: function (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});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up broadcasting of CKEditor toolbar configuration changes.
|
||||
*
|
||||
* @param {jQuery} $ckeditorToolbar
|
||||
* The active toolbar DOM element wrapped in jQuery.
|
||||
*/
|
||||
broadcastConfigurationChanges: function ($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);
|
||||
|
||||
// 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.
|
||||
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);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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, _);
|
266
web/core/modules/ckeditor/js/views/KeyboardView.js
Normal file
266
web/core/modules/ckeditor/js/views/KeyboardView.js
Normal file
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* @file
|
||||
* Backbone View providing the aural view of CKEditor keyboard UX configuration.
|
||||
*/
|
||||
|
||||
(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.
|
||||
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: 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 = $button.closest('.ckeditor-row');
|
||||
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.
|
||||
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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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, 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.
|
||||
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: 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);
|
||||
event.preventDefault();
|
||||
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.
|
||||
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 {
|
||||
$group.insertBefore($container.closest('.ckeditor-row').prev().find('.ckeditor-toolbar-groups').children().eq(-1));
|
||||
}
|
||||
}
|
||||
// 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, _);
|
273
web/core/modules/ckeditor/js/views/VisualView.js
Normal file
273
web/core/modules/ckeditor/js/views/VisualView.js
Normal file
|
@ -0,0 +1,273 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the visual UX view of CKEditor toolbar
|
||||
* configuration.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, $) {
|
||||
|
||||
'use strict';
|
||||
|
||||
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: function () {
|
||||
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: function (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});
|
||||
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: function (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) {
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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.
|
||||
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: function () {
|
||||
this.insertPlaceholderRow();
|
||||
this.insertNewGroupButtons();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a blank row at the bottom of the CKEditor configuration.
|
||||
*/
|
||||
insertPlaceholderRow: function () {
|
||||
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'));
|
||||
}
|
||||
// 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();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a button in each row that will add a new CKEditor button group.
|
||||
*/
|
||||
insertNewGroupButtons: function () {
|
||||
// Insert an add group button to each row.
|
||||
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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, jQuery);
|
Reference in a new issue