Mercurial > hg > cmmr2012-drupal-site
comparison core/modules/ckeditor/js/views/ControllerView.es6.js @ 4:a9cd425dd02b
Update, including to Drupal core 8.6.10
| author | Chris Cannam |
|---|---|
| date | Thu, 28 Feb 2019 13:11:55 +0000 |
| parents | c75dbcec494b |
| children |
comparison
equal
deleted
inserted
replaced
| 3:307d7a7fd348 | 4:a9cd425dd02b |
|---|---|
| 1 /** | 1 /** |
| 2 * @file | 2 * @file |
| 3 * A Backbone View acting as a controller for CKEditor toolbar configuration. | 3 * A Backbone View acting as a controller for CKEditor toolbar configuration. |
| 4 */ | 4 */ |
| 5 | 5 |
| 6 (function ($, Drupal, Backbone, CKEDITOR, _) { | 6 (function($, Drupal, Backbone, CKEDITOR, _) { |
| 7 Drupal.ckeditor.ControllerView = Backbone.View.extend(/** @lends Drupal.ckeditor.ControllerView# */{ | 7 Drupal.ckeditor.ControllerView = Backbone.View.extend( |
| 8 | 8 /** @lends Drupal.ckeditor.ControllerView# */ { |
| 9 /** | 9 /** |
| 10 * @type {object} | 10 * @type {object} |
| 11 */ | 11 */ |
| 12 events: {}, | 12 events: {}, |
| 13 | 13 |
| 14 /** | 14 /** |
| 15 * Backbone View acting as a controller for CKEditor toolbar configuration. | 15 * Backbone View acting as a controller for CKEditor toolbar configuration. |
| 16 * | 16 * |
| 17 * @constructs | 17 * @constructs |
| 18 * | 18 * |
| 19 * @augments Backbone.View | 19 * @augments Backbone.View |
| 20 */ | 20 */ |
| 21 initialize() { | 21 initialize() { |
| 22 this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this)); | 22 this.getCKEditorFeatures( |
| 23 | 23 this.model.get('hiddenEditorConfig'), |
| 24 // Push the active editor configuration to the textarea. | 24 this.disableFeaturesDisallowedByFilters.bind(this), |
| 25 this.model.listenTo(this.model, 'change:activeEditorConfig', this.model.sync); | 25 ); |
| 26 this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM); | 26 |
| 27 }, | 27 // Push the active editor configuration to the textarea. |
| 28 | 28 this.model.listenTo( |
| 29 /** | 29 this.model, |
| 30 * Converts the active toolbar DOM structure to an object representation. | 30 'change:activeEditorConfig', |
| 31 * | 31 this.model.sync, |
| 32 * @param {Drupal.ckeditor.ConfigurationModel} model | 32 ); |
| 33 * The state model for the CKEditor configuration. | 33 this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM); |
| 34 * @param {bool} isDirty | 34 }, |
| 35 * Tracks whether the active toolbar DOM structure has been changed. | 35 |
| 36 * isDirty is toggled back to false in this method. | 36 /** |
| 37 * @param {object} options | 37 * Converts the active toolbar DOM structure to an object representation. |
| 38 * An object that includes: | 38 * |
| 39 * @param {bool} [options.broadcast] | 39 * @param {Drupal.ckeditor.ConfigurationModel} model |
| 40 * A flag that controls whether a CKEditorToolbarChanged event should be | 40 * The state model for the CKEditor configuration. |
| 41 * fired for configuration changes. | 41 * @param {bool} isDirty |
| 42 * | 42 * Tracks whether the active toolbar DOM structure has been changed. |
| 43 * @fires event:CKEditorToolbarChanged | 43 * isDirty is toggled back to false in this method. |
| 44 */ | 44 * @param {object} options |
| 45 parseEditorDOM(model, isDirty, options) { | 45 * An object that includes: |
| 46 if (isDirty) { | 46 * @param {bool} [options.broadcast] |
| 47 const currentConfig = this.model.get('activeEditorConfig'); | 47 * A flag that controls whether a CKEditorToolbarChanged event should be |
| 48 | 48 * fired for configuration changes. |
| 49 // Process the rows. | 49 * |
| 50 const rows = []; | 50 * @fires event:CKEditorToolbarChanged |
| 51 this.$el | 51 */ |
| 52 .find('.ckeditor-active-toolbar-configuration') | 52 parseEditorDOM(model, isDirty, options) { |
| 53 .children('.ckeditor-row').each(function () { | 53 if (isDirty) { |
| 54 const groups = []; | 54 const currentConfig = this.model.get('activeEditorConfig'); |
| 55 // Process the button groups. | 55 |
| 56 $(this).find('.ckeditor-toolbar-group').each(function () { | 56 // Process the rows. |
| 57 const $group = $(this); | 57 const rows = []; |
| 58 const $buttons = $group.find('.ckeditor-button'); | 58 this.$el |
| 59 if ($buttons.length) { | 59 .find('.ckeditor-active-toolbar-configuration') |
| 60 const group = { | 60 .children('.ckeditor-row') |
| 61 name: $group.attr('data-drupal-ckeditor-toolbar-group-name'), | 61 .each(function() { |
| 62 items: [], | 62 const groups = []; |
| 63 }; | 63 // Process the button groups. |
| 64 $group.find('.ckeditor-button, .ckeditor-multiple-button').each(function () { | 64 $(this) |
| 65 group.items.push($(this).attr('data-drupal-ckeditor-button-name')); | 65 .find('.ckeditor-toolbar-group') |
| 66 .each(function() { | |
| 67 const $group = $(this); | |
| 68 const $buttons = $group.find('.ckeditor-button'); | |
| 69 if ($buttons.length) { | |
| 70 const group = { | |
| 71 name: $group.attr( | |
| 72 'data-drupal-ckeditor-toolbar-group-name', | |
| 73 ), | |
| 74 items: [], | |
| 75 }; | |
| 76 $group | |
| 77 .find('.ckeditor-button, .ckeditor-multiple-button') | |
| 78 .each(function() { | |
| 79 group.items.push( | |
| 80 $(this).attr('data-drupal-ckeditor-button-name'), | |
| 81 ); | |
| 82 }); | |
| 83 groups.push(group); | |
| 84 } | |
| 66 }); | 85 }); |
| 67 groups.push(group); | 86 if (groups.length) { |
| 87 rows.push(groups); | |
| 68 } | 88 } |
| 69 }); | 89 }); |
| 70 if (groups.length) { | 90 this.model.set('activeEditorConfig', rows); |
| 71 rows.push(groups); | 91 // Mark the model as clean. Whether or not the sync to the textfield |
| 92 // occurs depends on the activeEditorConfig attribute firing a change | |
| 93 // event. The DOM has at least been processed and posted, so as far as | |
| 94 // the model is concerned, it is clean. | |
| 95 this.model.set('isDirty', false); | |
| 96 | |
| 97 // Determine whether we should trigger an event. | |
| 98 if (options.broadcast !== false) { | |
| 99 const prev = this.getButtonList(currentConfig); | |
| 100 const next = this.getButtonList(rows); | |
| 101 if (prev.length !== next.length) { | |
| 102 this.$el | |
| 103 .find('.ckeditor-toolbar-active') | |
| 104 .trigger('CKEditorToolbarChanged', [ | |
| 105 prev.length < next.length ? 'added' : 'removed', | |
| 106 _.difference( | |
| 107 _.union(prev, next), | |
| 108 _.intersection(prev, next), | |
| 109 )[0], | |
| 110 ]); | |
| 72 } | 111 } |
| 112 } | |
| 113 } | |
| 114 }, | |
| 115 | |
| 116 /** | |
| 117 * Asynchronously retrieve the metadata for all available CKEditor features. | |
| 118 * | |
| 119 * In order to get a list of all features needed by CKEditor, we create a | |
| 120 * hidden CKEditor instance, then check the CKEditor's "allowedContent" | |
| 121 * filter settings. Because creating an instance is expensive, a callback | |
| 122 * must be provided that will receive a hash of {@link Drupal.EditorFeature} | |
| 123 * features keyed by feature (button) name. | |
| 124 * | |
| 125 * @param {object} CKEditorConfig | |
| 126 * An object that represents the configuration settings for a CKEditor | |
| 127 * editor component. | |
| 128 * @param {function} callback | |
| 129 * A function to invoke when the instanceReady event is fired by the | |
| 130 * CKEditor object. | |
| 131 */ | |
| 132 getCKEditorFeatures(CKEditorConfig, callback) { | |
| 133 const getProperties = function(CKEPropertiesList) { | |
| 134 return _.isObject(CKEPropertiesList) ? _.keys(CKEPropertiesList) : []; | |
| 135 }; | |
| 136 | |
| 137 const convertCKERulesToEditorFeature = function( | |
| 138 feature, | |
| 139 CKEFeatureRules, | |
| 140 ) { | |
| 141 for (let i = 0; i < CKEFeatureRules.length; i++) { | |
| 142 const CKERule = CKEFeatureRules[i]; | |
| 143 const rule = new Drupal.EditorFeatureHTMLRule(); | |
| 144 | |
| 145 // Tags. | |
| 146 const tags = getProperties(CKERule.elements); | |
| 147 rule.required.tags = CKERule.propertiesOnly ? [] : tags; | |
| 148 rule.allowed.tags = tags; | |
| 149 // Attributes. | |
| 150 rule.required.attributes = getProperties( | |
| 151 CKERule.requiredAttributes, | |
| 152 ); | |
| 153 rule.allowed.attributes = getProperties(CKERule.attributes); | |
| 154 // Styles. | |
| 155 rule.required.styles = getProperties(CKERule.requiredStyles); | |
| 156 rule.allowed.styles = getProperties(CKERule.styles); | |
| 157 // Classes. | |
| 158 rule.required.classes = getProperties(CKERule.requiredClasses); | |
| 159 rule.allowed.classes = getProperties(CKERule.classes); | |
| 160 // Raw. | |
| 161 rule.raw = CKERule; | |
| 162 | |
| 163 feature.addHTMLRule(rule); | |
| 164 } | |
| 165 }; | |
| 166 | |
| 167 // Create hidden CKEditor with all features enabled, retrieve metadata. | |
| 168 // @see \Drupal\ckeditor\Plugin\Editor\CKEditor::buildConfigurationForm(). | |
| 169 const hiddenCKEditorID = 'ckeditor-hidden'; | |
| 170 if (CKEDITOR.instances[hiddenCKEditorID]) { | |
| 171 CKEDITOR.instances[hiddenCKEditorID].destroy(true); | |
| 172 } | |
| 173 // Load external plugins, if any. | |
| 174 const hiddenEditorConfig = this.model.get('hiddenEditorConfig'); | |
| 175 if (hiddenEditorConfig.drupalExternalPlugins) { | |
| 176 const externalPlugins = hiddenEditorConfig.drupalExternalPlugins; | |
| 177 Object.keys(externalPlugins || {}).forEach(pluginName => { | |
| 178 CKEDITOR.plugins.addExternal( | |
| 179 pluginName, | |
| 180 externalPlugins[pluginName], | |
| 181 '', | |
| 182 ); | |
| 73 }); | 183 }); |
| 74 this.model.set('activeEditorConfig', rows); | 184 } |
| 75 // Mark the model as clean. Whether or not the sync to the textfield | 185 CKEDITOR.inline($(`#${hiddenCKEditorID}`).get(0), CKEditorConfig); |
| 76 // occurs depends on the activeEditorConfig attribute firing a change | 186 |
| 77 // event. The DOM has at least been processed and posted, so as far as | 187 // Once the instance is ready, retrieve the allowedContent filter rules |
| 78 // the model is concerned, it is clean. | 188 // and convert them to Drupal.EditorFeature objects. |
| 79 this.model.set('isDirty', false); | 189 CKEDITOR.once('instanceReady', e => { |
| 80 | 190 if (e.editor.name === hiddenCKEditorID) { |
| 81 // Determine whether we should trigger an event. | 191 // First collect all CKEditor allowedContent rules. |
| 82 if (options.broadcast !== false) { | 192 const CKEFeatureRulesMap = {}; |
| 83 const prev = this.getButtonList(currentConfig); | 193 const rules = e.editor.filter.allowedContent; |
| 84 const next = this.getButtonList(rows); | 194 let rule; |
| 85 if (prev.length !== next.length) { | 195 let name; |
| 196 for (let i = 0; i < rules.length; i++) { | |
| 197 rule = rules[i]; | |
| 198 name = rule.featureName || ':('; | |
| 199 if (!CKEFeatureRulesMap[name]) { | |
| 200 CKEFeatureRulesMap[name] = []; | |
| 201 } | |
| 202 CKEFeatureRulesMap[name].push(rule); | |
| 203 } | |
| 204 | |
| 205 // Now convert these to Drupal.EditorFeature objects. And track which | |
| 206 // buttons are mapped to which features. | |
| 207 // @see getFeatureForButton() | |
| 208 const features = {}; | |
| 209 const buttonsToFeatures = {}; | |
| 210 Object.keys(CKEFeatureRulesMap).forEach(featureName => { | |
| 211 const feature = new Drupal.EditorFeature(featureName); | |
| 212 convertCKERulesToEditorFeature( | |
| 213 feature, | |
| 214 CKEFeatureRulesMap[featureName], | |
| 215 ); | |
| 216 features[featureName] = feature; | |
| 217 const command = e.editor.getCommand(featureName); | |
| 218 if (command) { | |
| 219 buttonsToFeatures[command.uiItems[0].name] = featureName; | |
| 220 } | |
| 221 }); | |
| 222 | |
| 223 callback(features, buttonsToFeatures); | |
| 224 } | |
| 225 }); | |
| 226 }, | |
| 227 | |
| 228 /** | |
| 229 * Retrieves the feature for a given button from featuresMetadata. Returns | |
| 230 * false if the given button is in fact a divider. | |
| 231 * | |
| 232 * @param {string} button | |
| 233 * The name of a CKEditor button. | |
| 234 * | |
| 235 * @return {object} | |
| 236 * The feature metadata object for a button. | |
| 237 */ | |
| 238 getFeatureForButton(button) { | |
| 239 // Return false if the button being added is a divider. | |
| 240 if (button === '-') { | |
| 241 return false; | |
| 242 } | |
| 243 | |
| 244 // Get a Drupal.editorFeature object that contains all metadata for | |
| 245 // the feature that was just added or removed. Not every feature has | |
| 246 // such metadata. | |
| 247 let featureName = this.model.get('buttonsToFeatures')[ | |
| 248 button.toLowerCase() | |
| 249 ]; | |
| 250 // Features without an associated command do not have a 'feature name' by | |
| 251 // default, so we use the lowercased button name instead. | |
| 252 if (!featureName) { | |
| 253 featureName = button.toLowerCase(); | |
| 254 } | |
| 255 const featuresMetadata = this.model.get('featuresMetadata'); | |
| 256 if (!featuresMetadata[featureName]) { | |
| 257 featuresMetadata[featureName] = new Drupal.EditorFeature(featureName); | |
| 258 this.model.set('featuresMetadata', featuresMetadata); | |
| 259 } | |
| 260 return featuresMetadata[featureName]; | |
| 261 }, | |
| 262 | |
| 263 /** | |
| 264 * Checks buttons against filter settings; disables disallowed buttons. | |
| 265 * | |
| 266 * @param {object} features | |
| 267 * A map of {@link Drupal.EditorFeature} objects. | |
| 268 * @param {object} buttonsToFeatures | |
| 269 * Object containing the button-to-feature mapping. | |
| 270 * | |
| 271 * @see Drupal.ckeditor.ControllerView#getFeatureForButton | |
| 272 */ | |
| 273 disableFeaturesDisallowedByFilters(features, buttonsToFeatures) { | |
| 274 this.model.set('featuresMetadata', features); | |
| 275 // Store the button-to-feature mapping. Needs to happen only once, because | |
| 276 // the same buttons continue to have the same features; only the rules for | |
| 277 // specific features may change. | |
| 278 // @see getFeatureForButton() | |
| 279 this.model.set('buttonsToFeatures', buttonsToFeatures); | |
| 280 | |
| 281 // Ensure that toolbar configuration changes are broadcast. | |
| 282 this.broadcastConfigurationChanges(this.$el); | |
| 283 | |
| 284 // Initialization: not all of the default toolbar buttons may be allowed | |
| 285 // by the current filter settings. Remove any of the default toolbar | |
| 286 // buttons that require more permissive filter settings. The remaining | |
| 287 // default toolbar buttons are marked as "added". | |
| 288 let existingButtons = []; | |
| 289 // Loop through each button group after flattening the groups from the | |
| 290 // toolbar row arrays. | |
| 291 const buttonGroups = _.flatten(this.model.get('activeEditorConfig')); | |
| 292 for (let i = 0; i < buttonGroups.length; i++) { | |
| 293 // Pull the button names from each toolbar button group. | |
| 294 const buttons = buttonGroups[i].items; | |
| 295 for (let k = 0; k < buttons.length; k++) { | |
| 296 existingButtons.push(buttons[k]); | |
| 297 } | |
| 298 } | |
| 299 // Remove duplicate buttons. | |
| 300 existingButtons = _.unique(existingButtons); | |
| 301 // Prepare the active toolbar and available-button toolbars. | |
| 302 for (let n = 0; n < existingButtons.length; n++) { | |
| 303 const button = existingButtons[n]; | |
| 304 const feature = this.getFeatureForButton(button); | |
| 305 // Skip dividers. | |
| 306 if (feature === false) { | |
| 307 continue; | |
| 308 } | |
| 309 | |
| 310 if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) { | |
| 311 // Existing toolbar buttons are in fact "added features". | |
| 86 this.$el | 312 this.$el |
| 87 .find('.ckeditor-toolbar-active') | 313 .find('.ckeditor-toolbar-active') |
| 88 .trigger('CKEditorToolbarChanged', [ | 314 .trigger('CKEditorToolbarChanged', ['added', existingButtons[n]]); |
| 89 (prev.length < next.length) ? 'added' : 'removed', | 315 } else { |
| 90 _.difference(_.union(prev, next), _.intersection(prev, next))[0], | 316 // Move the button element from the active the active toolbar to the |
| 91 ]); | 317 // list of available buttons. |
| 92 } | 318 $( |
| 93 } | 319 `.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="${button}"]`, |
| 94 } | 320 ) |
| 95 }, | 321 .detach() |
| 96 | 322 .appendTo( |
| 97 /** | 323 '.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul', |
| 98 * Asynchronously retrieve the metadata for all available CKEditor features. | 324 ); |
| 99 * | 325 // Update the toolbar value field. |
| 100 * In order to get a list of all features needed by CKEditor, we create a | 326 this.model.set({ isDirty: true }, { broadcast: false }); |
| 101 * hidden CKEditor instance, then check the CKEditor's "allowedContent" | 327 } |
| 102 * filter settings. Because creating an instance is expensive, a callback | 328 } |
| 103 * must be provided that will receive a hash of {@link Drupal.EditorFeature} | 329 }, |
| 104 * features keyed by feature (button) name. | 330 |
| 105 * | 331 /** |
| 106 * @param {object} CKEditorConfig | 332 * Sets up broadcasting of CKEditor toolbar configuration changes. |
| 107 * An object that represents the configuration settings for a CKEditor | 333 * |
| 108 * editor component. | 334 * @param {jQuery} $ckeditorToolbar |
| 109 * @param {function} callback | 335 * The active toolbar DOM element wrapped in jQuery. |
| 110 * A function to invoke when the instanceReady event is fired by the | 336 */ |
| 111 * CKEditor object. | 337 broadcastConfigurationChanges($ckeditorToolbar) { |
| 112 */ | 338 const view = this; |
| 113 getCKEditorFeatures(CKEditorConfig, callback) { | 339 const hiddenEditorConfig = this.model.get('hiddenEditorConfig'); |
| 114 const getProperties = function (CKEPropertiesList) { | 340 const getFeatureForButton = this.getFeatureForButton.bind(this); |
| 115 return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : []; | 341 const getCKEditorFeatures = this.getCKEditorFeatures.bind(this); |
| 116 }; | 342 $ckeditorToolbar |
| 117 | 343 .find('.ckeditor-toolbar-active') |
| 118 const convertCKERulesToEditorFeature = function (feature, CKEFeatureRules) { | 344 // Listen for CKEditor toolbar configuration changes. When a button is |
| 119 for (let i = 0; i < CKEFeatureRules.length; i++) { | 345 // added/removed, call an appropriate Drupal.editorConfiguration method. |
| 120 const CKERule = CKEFeatureRules[i]; | 346 .on( |
| 121 const rule = new Drupal.EditorFeatureHTMLRule(); | 347 'CKEditorToolbarChanged.ckeditorAdmin', |
| 122 | 348 (event, action, button) => { |
| 123 // Tags. | 349 const feature = getFeatureForButton(button); |
| 124 const tags = getProperties(CKERule.elements); | 350 |
| 125 rule.required.tags = (CKERule.propertiesOnly) ? [] : tags; | 351 // Early-return if the button being added is a divider. |
| 126 rule.allowed.tags = tags; | 352 if (feature === false) { |
| 127 // Attributes. | 353 return; |
| 128 rule.required.attributes = getProperties(CKERule.requiredAttributes); | |
| 129 rule.allowed.attributes = getProperties(CKERule.attributes); | |
| 130 // Styles. | |
| 131 rule.required.styles = getProperties(CKERule.requiredStyles); | |
| 132 rule.allowed.styles = getProperties(CKERule.styles); | |
| 133 // Classes. | |
| 134 rule.required.classes = getProperties(CKERule.requiredClasses); | |
| 135 rule.allowed.classes = getProperties(CKERule.classes); | |
| 136 // Raw. | |
| 137 rule.raw = CKERule; | |
| 138 | |
| 139 feature.addHTMLRule(rule); | |
| 140 } | |
| 141 }; | |
| 142 | |
| 143 // Create hidden CKEditor with all features enabled, retrieve metadata. | |
| 144 // @see \Drupal\ckeditor\Plugin\Editor\CKEditor::buildConfigurationForm(). | |
| 145 const hiddenCKEditorID = 'ckeditor-hidden'; | |
| 146 if (CKEDITOR.instances[hiddenCKEditorID]) { | |
| 147 CKEDITOR.instances[hiddenCKEditorID].destroy(true); | |
| 148 } | |
| 149 // Load external plugins, if any. | |
| 150 const hiddenEditorConfig = this.model.get('hiddenEditorConfig'); | |
| 151 if (hiddenEditorConfig.drupalExternalPlugins) { | |
| 152 const externalPlugins = hiddenEditorConfig.drupalExternalPlugins; | |
| 153 Object.keys(externalPlugins || {}).forEach((pluginName) => { | |
| 154 CKEDITOR.plugins.addExternal(pluginName, externalPlugins[pluginName], ''); | |
| 155 }); | |
| 156 } | |
| 157 CKEDITOR.inline($(`#${hiddenCKEditorID}`).get(0), CKEditorConfig); | |
| 158 | |
| 159 // Once the instance is ready, retrieve the allowedContent filter rules | |
| 160 // and convert them to Drupal.EditorFeature objects. | |
| 161 CKEDITOR.once('instanceReady', (e) => { | |
| 162 if (e.editor.name === hiddenCKEditorID) { | |
| 163 // First collect all CKEditor allowedContent rules. | |
| 164 const CKEFeatureRulesMap = {}; | |
| 165 const rules = e.editor.filter.allowedContent; | |
| 166 let rule; | |
| 167 let name; | |
| 168 for (let i = 0; i < rules.length; i++) { | |
| 169 rule = rules[i]; | |
| 170 name = rule.featureName || ':('; | |
| 171 if (!CKEFeatureRulesMap[name]) { | |
| 172 CKEFeatureRulesMap[name] = []; | |
| 173 } | |
| 174 CKEFeatureRulesMap[name].push(rule); | |
| 175 } | |
| 176 | |
| 177 // Now convert these to Drupal.EditorFeature objects. And track which | |
| 178 // buttons are mapped to which features. | |
| 179 // @see getFeatureForButton() | |
| 180 const features = {}; | |
| 181 const buttonsToFeatures = {}; | |
| 182 Object.keys(CKEFeatureRulesMap).forEach((featureName) => { | |
| 183 const feature = new Drupal.EditorFeature(featureName); | |
| 184 convertCKERulesToEditorFeature(feature, CKEFeatureRulesMap[featureName]); | |
| 185 features[featureName] = feature; | |
| 186 const command = e.editor.getCommand(featureName); | |
| 187 if (command) { | |
| 188 buttonsToFeatures[command.uiItems[0].name] = featureName; | |
| 189 } | |
| 190 }); | |
| 191 | |
| 192 callback(features, buttonsToFeatures); | |
| 193 } | |
| 194 }); | |
| 195 }, | |
| 196 | |
| 197 /** | |
| 198 * Retrieves the feature for a given button from featuresMetadata. Returns | |
| 199 * false if the given button is in fact a divider. | |
| 200 * | |
| 201 * @param {string} button | |
| 202 * The name of a CKEditor button. | |
| 203 * | |
| 204 * @return {object} | |
| 205 * The feature metadata object for a button. | |
| 206 */ | |
| 207 getFeatureForButton(button) { | |
| 208 // Return false if the button being added is a divider. | |
| 209 if (button === '-') { | |
| 210 return false; | |
| 211 } | |
| 212 | |
| 213 // Get a Drupal.editorFeature object that contains all metadata for | |
| 214 // the feature that was just added or removed. Not every feature has | |
| 215 // such metadata. | |
| 216 let featureName = this.model.get('buttonsToFeatures')[button.toLowerCase()]; | |
| 217 // Features without an associated command do not have a 'feature name' by | |
| 218 // default, so we use the lowercased button name instead. | |
| 219 if (!featureName) { | |
| 220 featureName = button.toLowerCase(); | |
| 221 } | |
| 222 const featuresMetadata = this.model.get('featuresMetadata'); | |
| 223 if (!featuresMetadata[featureName]) { | |
| 224 featuresMetadata[featureName] = new Drupal.EditorFeature(featureName); | |
| 225 this.model.set('featuresMetadata', featuresMetadata); | |
| 226 } | |
| 227 return featuresMetadata[featureName]; | |
| 228 }, | |
| 229 | |
| 230 /** | |
| 231 * Checks buttons against filter settings; disables disallowed buttons. | |
| 232 * | |
| 233 * @param {object} features | |
| 234 * A map of {@link Drupal.EditorFeature} objects. | |
| 235 * @param {object} buttonsToFeatures | |
| 236 * Object containing the button-to-feature mapping. | |
| 237 * | |
| 238 * @see Drupal.ckeditor.ControllerView#getFeatureForButton | |
| 239 */ | |
| 240 disableFeaturesDisallowedByFilters(features, buttonsToFeatures) { | |
| 241 this.model.set('featuresMetadata', features); | |
| 242 // Store the button-to-feature mapping. Needs to happen only once, because | |
| 243 // the same buttons continue to have the same features; only the rules for | |
| 244 // specific features may change. | |
| 245 // @see getFeatureForButton() | |
| 246 this.model.set('buttonsToFeatures', buttonsToFeatures); | |
| 247 | |
| 248 // Ensure that toolbar configuration changes are broadcast. | |
| 249 this.broadcastConfigurationChanges(this.$el); | |
| 250 | |
| 251 // Initialization: not all of the default toolbar buttons may be allowed | |
| 252 // by the current filter settings. Remove any of the default toolbar | |
| 253 // buttons that require more permissive filter settings. The remaining | |
| 254 // default toolbar buttons are marked as "added". | |
| 255 let existingButtons = []; | |
| 256 // Loop through each button group after flattening the groups from the | |
| 257 // toolbar row arrays. | |
| 258 const buttonGroups = _.flatten(this.model.get('activeEditorConfig')); | |
| 259 for (let i = 0; i < buttonGroups.length; i++) { | |
| 260 // Pull the button names from each toolbar button group. | |
| 261 const buttons = buttonGroups[i].items; | |
| 262 for (let k = 0; k < buttons.length; k++) { | |
| 263 existingButtons.push(buttons[k]); | |
| 264 } | |
| 265 } | |
| 266 // Remove duplicate buttons. | |
| 267 existingButtons = _.unique(existingButtons); | |
| 268 // Prepare the active toolbar and available-button toolbars. | |
| 269 for (let n = 0; n < existingButtons.length; n++) { | |
| 270 const button = existingButtons[n]; | |
| 271 const feature = this.getFeatureForButton(button); | |
| 272 // Skip dividers. | |
| 273 if (feature === false) { | |
| 274 continue; | |
| 275 } | |
| 276 | |
| 277 if (Drupal.editorConfiguration.featureIsAllowedByFilters(feature)) { | |
| 278 // Existing toolbar buttons are in fact "added features". | |
| 279 this.$el.find('.ckeditor-toolbar-active').trigger('CKEditorToolbarChanged', ['added', existingButtons[n]]); | |
| 280 } | |
| 281 else { | |
| 282 // Move the button element from the active the active toolbar to the | |
| 283 // list of available buttons. | |
| 284 $(`.ckeditor-toolbar-active li[data-drupal-ckeditor-button-name="${button}"]`) | |
| 285 .detach() | |
| 286 .appendTo('.ckeditor-toolbar-disabled > .ckeditor-toolbar-available > ul'); | |
| 287 // Update the toolbar value field. | |
| 288 this.model.set({ isDirty: true }, { broadcast: false }); | |
| 289 } | |
| 290 } | |
| 291 }, | |
| 292 | |
| 293 /** | |
| 294 * Sets up broadcasting of CKEditor toolbar configuration changes. | |
| 295 * | |
| 296 * @param {jQuery} $ckeditorToolbar | |
| 297 * The active toolbar DOM element wrapped in jQuery. | |
| 298 */ | |
| 299 broadcastConfigurationChanges($ckeditorToolbar) { | |
| 300 const view = this; | |
| 301 const hiddenEditorConfig = this.model.get('hiddenEditorConfig'); | |
| 302 const getFeatureForButton = this.getFeatureForButton.bind(this); | |
| 303 const getCKEditorFeatures = this.getCKEditorFeatures.bind(this); | |
| 304 $ckeditorToolbar | |
| 305 .find('.ckeditor-toolbar-active') | |
| 306 // Listen for CKEditor toolbar configuration changes. When a button is | |
| 307 // added/removed, call an appropriate Drupal.editorConfiguration method. | |
| 308 .on('CKEditorToolbarChanged.ckeditorAdmin', (event, action, button) => { | |
| 309 const feature = getFeatureForButton(button); | |
| 310 | |
| 311 // Early-return if the button being added is a divider. | |
| 312 if (feature === false) { | |
| 313 return; | |
| 314 } | |
| 315 | |
| 316 // Trigger a standardized text editor configuration event to indicate | |
| 317 // whether a feature was added or removed, so that filters can react. | |
| 318 const configEvent = (action === 'added') ? 'addedFeature' : 'removedFeature'; | |
| 319 Drupal.editorConfiguration[configEvent](feature); | |
| 320 }) | |
| 321 // Listen for CKEditor plugin settings changes. When a plugin setting is | |
| 322 // changed, rebuild the CKEditor features metadata. | |
| 323 .on('CKEditorPluginSettingsChanged.ckeditorAdmin', (event, settingsChanges) => { | |
| 324 // Update hidden CKEditor configuration. | |
| 325 Object.keys(settingsChanges || {}).forEach((key) => { | |
| 326 hiddenEditorConfig[key] = settingsChanges[key]; | |
| 327 }); | |
| 328 | |
| 329 // Retrieve features for the updated hidden CKEditor configuration. | |
| 330 getCKEditorFeatures(hiddenEditorConfig, (features) => { | |
| 331 // Trigger a standardized text editor configuration event for each | |
| 332 // feature that was modified by the configuration changes. | |
| 333 const featuresMetadata = view.model.get('featuresMetadata'); | |
| 334 Object.keys(features || {}).forEach((name) => { | |
| 335 const feature = features[name]; | |
| 336 if (featuresMetadata.hasOwnProperty(name) && !_.isEqual(featuresMetadata[name], feature)) { | |
| 337 Drupal.editorConfiguration.modifiedFeature(feature); | |
| 338 } | 354 } |
| 339 }); | 355 |
| 340 // Update the CKEditor features metadata. | 356 // Trigger a standardized text editor configuration event to indicate |
| 341 view.model.set('featuresMetadata', features); | 357 // whether a feature was added or removed, so that filters can react. |
| 358 const configEvent = | |
| 359 action === 'added' ? 'addedFeature' : 'removedFeature'; | |
| 360 Drupal.editorConfiguration[configEvent](feature); | |
| 361 }, | |
| 362 ) | |
| 363 // Listen for CKEditor plugin settings changes. When a plugin setting is | |
| 364 // changed, rebuild the CKEditor features metadata. | |
| 365 .on( | |
| 366 'CKEditorPluginSettingsChanged.ckeditorAdmin', | |
| 367 (event, settingsChanges) => { | |
| 368 // Update hidden CKEditor configuration. | |
| 369 Object.keys(settingsChanges || {}).forEach(key => { | |
| 370 hiddenEditorConfig[key] = settingsChanges[key]; | |
| 371 }); | |
| 372 | |
| 373 // Retrieve features for the updated hidden CKEditor configuration. | |
| 374 getCKEditorFeatures(hiddenEditorConfig, features => { | |
| 375 // Trigger a standardized text editor configuration event for each | |
| 376 // feature that was modified by the configuration changes. | |
| 377 const featuresMetadata = view.model.get('featuresMetadata'); | |
| 378 Object.keys(features || {}).forEach(name => { | |
| 379 const feature = features[name]; | |
| 380 if ( | |
| 381 featuresMetadata.hasOwnProperty(name) && | |
| 382 !_.isEqual(featuresMetadata[name], feature) | |
| 383 ) { | |
| 384 Drupal.editorConfiguration.modifiedFeature(feature); | |
| 385 } | |
| 386 }); | |
| 387 // Update the CKEditor features metadata. | |
| 388 view.model.set('featuresMetadata', features); | |
| 389 }); | |
| 390 }, | |
| 391 ); | |
| 392 }, | |
| 393 | |
| 394 /** | |
| 395 * Returns the list of buttons from an editor configuration. | |
| 396 * | |
| 397 * @param {object} config | |
| 398 * A CKEditor configuration object. | |
| 399 * | |
| 400 * @return {Array} | |
| 401 * A list of buttons in the CKEditor configuration. | |
| 402 */ | |
| 403 getButtonList(config) { | |
| 404 const buttons = []; | |
| 405 // Remove the rows. | |
| 406 config = _.flatten(config); | |
| 407 | |
| 408 // Loop through the button groups and pull out the buttons. | |
| 409 config.forEach(group => { | |
| 410 group.items.forEach(button => { | |
| 411 buttons.push(button); | |
| 342 }); | 412 }); |
| 343 }); | 413 }); |
| 414 | |
| 415 // Remove the dividing elements if any. | |
| 416 return _.without(buttons, '-'); | |
| 417 }, | |
| 344 }, | 418 }, |
| 345 | 419 ); |
| 346 /** | 420 })(jQuery, Drupal, Backbone, CKEDITOR, _); |
| 347 * Returns the list of buttons from an editor configuration. | |
| 348 * | |
| 349 * @param {object} config | |
| 350 * A CKEditor configuration object. | |
| 351 * | |
| 352 * @return {Array} | |
| 353 * A list of buttons in the CKEditor configuration. | |
| 354 */ | |
| 355 getButtonList(config) { | |
| 356 const buttons = []; | |
| 357 // Remove the rows. | |
| 358 config = _.flatten(config); | |
| 359 | |
| 360 // Loop through the button groups and pull out the buttons. | |
| 361 config.forEach((group) => { | |
| 362 group.items.forEach((button) => { | |
| 363 buttons.push(button); | |
| 364 }); | |
| 365 }); | |
| 366 | |
| 367 // Remove the dividing elements if any. | |
| 368 return _.without(buttons, '-'); | |
| 369 }, | |
| 370 }); | |
| 371 }(jQuery, Drupal, Backbone, CKEDITOR, _)); |
