annotate core/modules/ckeditor/js/views/ControllerView.es6.js @ 0:c75dbcec494b

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