Commit b61edd72 authored by alexpott's avatar alexpott
Browse files

Issue #2174589 by Wim Leers, ikeigenwijs, el7cosmos, setvik, YesCT: Split up ckeditor.admin.js

parent fda020ec
......@@ -23,7 +23,15 @@ drupal.ckeditor.plugins.drupalimagecaption:
drupal.ckeditor.admin:
version: VERSION
js:
# Core.
js/ckeditor.admin.js: {}
# Models.
js/models/Model.js: {}
# Views.
js/views/AuralView.js: {}
js/views/KeyboardView.js: {}
js/views/ControllerView.js: {}
js/views/VisualView.js: {}
css:
theme:
css/ckeditor.admin.css: {}
......
This diff is collapsed.
/**
* @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.
*/
Drupal.ckeditor.Model = Backbone.Model.extend({
defaults: {
// 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, keyed by a feature name, that details CKEditor plugin features.
featuresMetadata: null,
// Whether the button group names are currently visible.
groupNamesVisible: false
},
sync: function () {
// Push the settings into the textarea.
this.get('$textarea').val(JSON.stringify(this.get('activeEditorConfig')));
}
});
})(Drupal, Backbone);
/**
* @file
* A Backbone View that provides the aural view of CKEditor toolbar configuration.
*/
(function (Drupal, Backbone, $) {
"use strict";
/**
* Backbone View for CKEditor toolbar configuration; aural UX (output only).
*/
Drupal.ckeditor.AuralView = Backbone.View.extend({
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'
},
/**
* {@inheritdoc}
*/
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
* @param Boolean 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
*/
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
*/
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
*/
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);
/**
* @file
* A Backbone View acting as a controller for CKEditor toolbar configuration.
*/
(function (Drupal, Backbone, $) {
"use strict";
/**
* Backbone View acting as a controller for CKEditor toolbar configuration.
*/
Drupal.ckeditor.ControllerView = Backbone.View.extend({
events: {},
/**
* {@inheritdoc}
*/
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 Boolean 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:
* - Boolean broadcast: (optional) A flag that controls whether a
* CKEditorToolbarChanged event should be fired for configuration
* changes.
*/
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 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, 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.
var features = {};
for (var featureName in CKEFeatureRulesMap) {
if (CKEFeatureRulesMap.hasOwnProperty(featureName)) {
var feature = new Drupal.EditorFeature(featureName);
convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]);
features[featureName] = feature;
}
}
callback(features);
}
});
},
/**
* 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 = 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 Drupal.EditorFeature objects.
*/
disableFeaturesDisallowedByFilters: function (features) {
this.model.set('featuresMetadata', features);
// 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.
for (var i = 0, buttonGroups = _.flatten(this.model.get('activeEditorConfig')); i < buttonGroups.length; i++) {
// Pull the button names from each toolbar button group.
for (var k = 0, buttons = buttonGroups[i].items; k < buttons.length; k++) {
existingButtons.push(buttons[k]);
}
}
// Remove duplicate buttons.
existingButtons = _.unique(existingButtons);
// Prepare the active toolbar and available-button toolbars.
for (i = 0; i < existingButtons.length; i++) {
var button = existingButtons[i];
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[i]]);
}
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 featuresMetadata = this.model.get('featuresMetadata');
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) {