3 * A Backbone View acting as a controller for CKEditor toolbar configuration.
6 (function ($, Drupal, Backbone, CKEDITOR, _) {
10 Drupal.ckeditor.ControllerView = Backbone.View.extend(/** @lends Drupal.ckeditor.ControllerView# */{
18 * Backbone View acting as a controller for CKEditor toolbar configuration.
22 * @augments Backbone.View
24 initialize: function () {
25 this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this));
27 // Push the active editor configuration to the textarea.
28 this.model.listenTo(this.model, 'change:activeEditorConfig', this.model.sync);
29 this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM);
33 * Converts the active toolbar DOM structure to an object representation.
35 * @param {Drupal.ckeditor.ConfigurationModel} model
36 * The state model for the CKEditor configuration.
37 * @param {bool} isDirty
38 * Tracks whether the active toolbar DOM structure has been changed.
39 * isDirty is toggled back to false in this method.
40 * @param {object} options
41 * An object that includes:
42 * @param {bool} [options.broadcast]
43 * A flag that controls whether a CKEditorToolbarChanged event should be
44 * fired for configuration changes.
46 * @fires event:CKEditorToolbarChanged
48 parseEditorDOM: function (model, isDirty, options) {
50 var currentConfig = this.model.get('activeEditorConfig');
55 .find('.ckeditor-active-toolbar-configuration')
56 .children('.ckeditor-row').each(function () {
58 // Process the button groups.
59 $(this).find('.ckeditor-toolbar-group').each(function () {
61 var $buttons = $group.find('.ckeditor-button');
62 if ($buttons.length) {
64 name: $group.attr('data-drupal-ckeditor-toolbar-group-name'),
67 $group.find('.ckeditor-button, .ckeditor-multiple-button').each(function () {
68 group.items.push($(this).attr('data-drupal-ckeditor-button-name'));
77 this.model.set('activeEditorConfig', rows);
78 // Mark the model as clean. Whether or not the sync to the textfield
79 // occurs depends on the activeEditorConfig attribute firing a change
80 // event. The DOM has at least been processed and posted, so as far as
81 // the model is concerned, it is clean.
82 this.model.set('isDirty', false);
84 // Determine whether we should trigger an event.
85 if (options.broadcast !== false) {
86 var prev = this.getButtonList(currentConfig);
87 var next = this.getButtonList(rows);
88 if (prev.length !== next.length) {
90 .find('.ckeditor-toolbar-active')
91 .trigger('CKEditorToolbarChanged', [
92 (prev.length < next.length) ? 'added' : 'removed',
93 _.difference(_.union(prev, next), _.intersection(prev, next))[0]
101 * Asynchronously retrieve the metadata for all available CKEditor features.
103 * In order to get a list of all features needed by CKEditor, we create a
104 * hidden CKEditor instance, then check the CKEditor's "allowedContent"
105 * filter settings. Because creating an instance is expensive, a callback
106 * must be provided that will receive a hash of {@link Drupal.EditorFeature}
107 * features keyed by feature (button) name.
109 * @param {object} CKEditorConfig
110 * An object that represents the configuration settings for a CKEditor
112 * @param {function} callback
113 * A function to invoke when the instanceReady event is fired by the
116 getCKEditorFeatures: function (CKEditorConfig, callback) {
117 var getProperties = function (CKEPropertiesList) {
118 return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : [];
121 var convertCKERulesToEditorFeature = function (feature, CKEFeatureRules) {
122 for (var i = 0; i < CKEFeatureRules.length; i++) {
123 var CKERule = CKEFeatureRules[i];
124 var rule = new Drupal.EditorFeatureHTMLRule();
127 var tags = getProperties(CKERule.elements);
128 rule.required.tags = (CKERule.propertiesOnly) ? [] : tags;
129 rule.allowed.tags = tags;
131 rule.required.attributes = getProperties(CKERule.requiredAttributes);
132 rule.allowed.attributes = getProperties(CKERule.attributes);
134 rule.required.styles = getProperties(CKERule.requiredStyles);
135 rule.allowed.styles = getProperties(CKERule.styles);
137 rule.required.classes = getProperties(CKERule.requiredClasses);
138 rule.allowed.classes = getProperties(CKERule.classes);
142 feature.addHTMLRule(rule);
146 // Create hidden CKEditor with all features enabled, retrieve metadata.
147 // @see \Drupal\ckeditor\Plugin\Editor\CKEditor::buildConfigurationForm().
148 var hiddenCKEditorID = 'ckeditor-hidden';
149 if (CKEDITOR.instances[hiddenCKEditorID]) {
150 CKEDITOR.instances[hiddenCKEditorID].destroy(true);
152 // Load external plugins, if any.
153 var hiddenEditorConfig = this.model.get('hiddenEditorConfig');
154 if (hiddenEditorConfig.drupalExternalPlugins) {
155 var externalPlugins = hiddenEditorConfig.drupalExternalPlugins;
156 for (var pluginName in externalPlugins) {
157 if (externalPlugins.hasOwnProperty(pluginName)) {
158 CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], '');
162 CKEDITOR.inline($('#' + hiddenCKEditorID).get(0), CKEditorConfig);
164 // Once the instance is ready, retrieve the allowedContent filter rules
165 // and convert them to Drupal.EditorFeature objects.
166 CKEDITOR.once('instanceReady', function (e) {
167 if (e.editor.name === hiddenCKEditorID) {
168 // First collect all CKEditor allowedContent rules.
169 var CKEFeatureRulesMap = {};
170 var rules = e.editor.filter.allowedContent;
173 for (var i = 0; i < rules.length; i++) {
175 name = rule.featureName || ':(';
176 if (!CKEFeatureRulesMap[name]) {
177 CKEFeatureRulesMap[name] = [];
179 CKEFeatureRulesMap[name].push(rule);
182 // Now convert these to Drupal.EditorFeature objects. And track which
183 // buttons are mapped to which features.
184 // @see getFeatureForButton()
186 var buttonsToFeatures = {};
187 for (var featureName in CKEFeatureRulesMap) {
188 if (CKEFeatureRulesMap.hasOwnProperty(featureName)) {
189 var feature = new Drupal.EditorFeature(featureName);
190 convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]);
191 features[featureName] = feature;
192 var command = e.editor.getCommand(featureName);
194 buttonsToFeatures[command.uiItems[0].name] = featureName;
199 callback(features, buttonsToFeatures);
205 * Retrieves the feature for a given button from featuresMetadata. Returns
206 * false if the given button is in fact a divider.
208 * @param {string} button
209 * The name of a CKEditor button.
212 * The feature metadata object for a button.
214 getFeatureForButton: function (button) {
215 // Return false if the button being added is a divider.
216 if (button === '-') {
220 // Get a Drupal.editorFeature object that contains all metadata for
221 // the feature that was just added or removed. Not every feature has
223 var featureName = this.model.get('buttonsToFeatures')[button.toLowerCase()];
224 // Features without an associated command do not have a 'feature name' by
225 // default, so we use the lowercased button name instead.
227 featureName = button.toLowerCase();
229 var featuresMetadata = this.model.get('featuresMetadata');
230 if (!featuresMetadata[featureName]) {
231 featuresMetadata[featureName] = new Drupal.EditorFeature(featureName);
232 this.model.set('featuresMetadata', featuresMetadata);
234 return featuresMetadata[featureName];
238 * Checks buttons against filter settings; disables disallowed buttons.
240 * @param {object} features
241 * A map of {@link Drupal.EditorFeature} objects.
242 * @param {object} buttonsToFeatures
243 * Object containing the button-to-feature mapping.
245 * @see Drupal.ckeditor.ControllerView#getFeatureForButton
247 disableFeaturesDisallowedByFilters: function (features, buttonsToFeatures) {
248 this.model.set('featuresMetadata', features);
249 // Store the button-to-feature mapping. Needs to happen only once, because
250 // the same buttons continue to have the same features; only the rules for
251 // specific features may change.
252 // @see getFeatureForButton()
253 this.model.set('buttonsToFeatures', buttonsToFeatures);
255 // Ensure that toolbar configuration changes are broadcast.
256 this.broadcastConfigurationChanges(this.$el);
258 // Initialization: not all of the default toolbar buttons may be allowed
259 // by the current filter settings. Remove any of the default toolbar
260 // buttons that require more permissive filter settings. The remaining
261 // default toolbar buttons are marked as "added".
262 var existingButtons = [];
263 // Loop through each button group after flattening the groups from the
264 // toolbar row arrays.
265 var buttonGroups = _.flatten(this.model.get('activeEditorConfig'));
266 for (var i = 0; i < buttonGroups.length; i++) {
267 // Pull the button names from each toolbar button group.
268 var buttons = buttonGroups[i].items;
269 for (var k = 0; k < buttons.length; k++) {
270 existingButtons.push(buttons[k]);
273 // Remove duplicate buttons.
274 existingButtons = _.unique(existingButtons);
275 // Prepare the active toolbar and available-button toolbars.
276 for (var n = 0; n < existingButtons.length; n++) {
277 var button = existingButtons[n];
278 var feature = this.getFeatureForButton(button);
280 if (feature === false) {
284 if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) {
285 // Existing toolbar buttons are in fact "added features".
286 this.$el.find('.ckeditor-toolbar-active').trigger('CKEditorToolbarChanged', ['added', existingButtons[n]]);
289 // Move the button element from the active the active toolbar to the
290 // list of available buttons.
291 $('.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="' + button + '"]')
293 .appendTo('.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul');
294 // Update the toolbar value field.
295 this.model.set({isDirty: true}, {broadcast: false});
301 * Sets up broadcasting of CKEditor toolbar configuration changes.
303 * @param {jQuery} $ckeditorToolbar
304 * The active toolbar DOM element wrapped in jQuery.
306 broadcastConfigurationChanges: function ($ckeditorToolbar) {
308 var hiddenEditorConfig = this.model.get('hiddenEditorConfig');
309 var getFeatureForButton = this.getFeatureForButton.bind(this);
310 var getCKEditorFeatures = this.getCKEditorFeatures.bind(this);
312 .find('.ckeditor-toolbar-active')
313 // Listen for CKEditor toolbar configuration changes. When a button is
314 // added/removed, call an appropriate Drupal.editorConfiguration method.
315 .on('CKEditorToolbarChanged.ckeditorAdmin', function (event, action, button) {
316 var feature = getFeatureForButton(button);
318 // Early-return if the button being added is a divider.
319 if (feature === false) {
323 // Trigger a standardized text editor configuration event to indicate
324 // whether a feature was added or removed, so that filters can react.
325 var configEvent = (action === 'added') ? 'addedFeature' : 'removedFeature';
326 Drupal.editorConfiguration[configEvent](feature);
328 // Listen for CKEditor plugin settings changes. When a plugin setting is
329 // changed, rebuild the CKEditor features metadata.
330 .on('CKEditorPluginSettingsChanged.ckeditorAdmin', function (event, settingsChanges) {
331 // Update hidden CKEditor configuration.
332 for (var key in settingsChanges) {
333 if (settingsChanges.hasOwnProperty(key)) {
334 hiddenEditorConfig[key] = settingsChanges[key];
338 // Retrieve features for the updated hidden CKEditor configuration.
339 getCKEditorFeatures(hiddenEditorConfig, function (features) {
340 // Trigger a standardized text editor configuration event for each
341 // feature that was modified by the configuration changes.
342 var featuresMetadata = view.model.get('featuresMetadata');
343 for (var name in features) {
344 if (features.hasOwnProperty(name)) {
345 var feature = features[name];
346 if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) {
347 Drupal.editorConfiguration.modifiedFeature(feature);
351 // Update the CKEditor features metadata.
352 view.model.set('featuresMetadata', features);
358 * Returns the list of buttons from an editor configuration.
360 * @param {object} config
361 * A CKEditor configuration object.
364 * A list of buttons in the CKEditor configuration.
366 getButtonList: function (config) {
369 config = _.flatten(config);
371 // Loop through the button groups and pull out the buttons.
372 config.forEach(function (group) {
373 group.items.forEach(function (button) {
374 buttons.push(button);
378 // Remove the dividing elements if any.
379 return _.without(buttons, '-');
383 })(jQuery, Drupal, Backbone, CKEDITOR, _);