Mercurial > hg > cmmr2012-drupal-site
comparison 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 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:c75dbcec494b |
---|---|
1 /** | |
2 * @file | |
3 * A Backbone View acting as a controller for CKEditor toolbar configuration. | |
4 */ | |
5 | |
6 (function ($, Drupal, Backbone, CKEDITOR, _) { | |
7 Drupal.ckeditor.ControllerView = Backbone.View.extend(/** @lends Drupal.ckeditor.ControllerView# */{ | |
8 | |
9 /** | |
10 * @type {object} | |
11 */ | |
12 events: {}, | |
13 | |
14 /** | |
15 * Backbone View acting as a controller for CKEditor toolbar configuration. | |
16 * | |
17 * @constructs | |
18 * | |
19 * @augments Backbone.View | |
20 */ | |
21 initialize() { | |
22 this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this)); | |
23 | |
24 // Push the active editor configuration to the textarea. | |
25 this.model.listenTo(this.model, 'change:activeEditorConfig', this.model.sync); | |
26 this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM); | |
27 }, | |
28 | |
29 /** | |
30 * Converts the active toolbar DOM structure to an object representation. | |
31 * | |
32 * @param {Drupal.ckeditor.ConfigurationModel} model | |
33 * The state model for the CKEditor configuration. | |
34 * @param {bool} isDirty | |
35 * Tracks whether the active toolbar DOM structure has been changed. | |
36 * isDirty is toggled back to false in this method. | |
37 * @param {object} options | |
38 * An object that includes: | |
39 * @param {bool} [options.broadcast] | |
40 * A flag that controls whether a CKEditorToolbarChanged event should be | |
41 * fired for configuration changes. | |
42 * | |
43 * @fires event:CKEditorToolbarChanged | |
44 */ | |
45 parseEditorDOM(model, isDirty, options) { | |
46 if (isDirty) { | |
47 const currentConfig = this.model.get('activeEditorConfig'); | |
48 | |
49 // Process the rows. | |
50 const rows = []; | |
51 this.$el | |
52 .find('.ckeditor-active-toolbar-configuration') | |
53 .children('.ckeditor-row').each(function () { | |
54 const groups = []; | |
55 // Process the button groups. | |
56 $(this).find('.ckeditor-toolbar-group').each(function () { | |
57 const $group = $(this); | |
58 const $buttons = $group.find('.ckeditor-button'); | |
59 if ($buttons.length) { | |
60 const group = { | |
61 name: $group.attr('data-drupal-ckeditor-toolbar-group-name'), | |
62 items: [], | |
63 }; | |
64 $group.find('.ckeditor-button, .ckeditor-multiple-button').each(function () { | |
65 group.items.push($(this).attr('data-drupal-ckeditor-button-name')); | |
66 }); | |
67 groups.push(group); | |
68 } | |
69 }); | |
70 if (groups.length) { | |
71 rows.push(groups); | |
72 } | |
73 }); | |
74 this.model.set('activeEditorConfig', rows); | |
75 // Mark the model as clean. Whether or not the sync to the textfield | |
76 // occurs depends on the activeEditorConfig attribute firing a change | |
77 // event. The DOM has at least been processed and posted, so as far as | |
78 // the model is concerned, it is clean. | |
79 this.model.set('isDirty', false); | |
80 | |
81 // Determine whether we should trigger an event. | |
82 if (options.broadcast !== false) { | |
83 const prev = this.getButtonList(currentConfig); | |
84 const next = this.getButtonList(rows); | |
85 if (prev.length !== next.length) { | |
86 this.$el | |
87 .find('.ckeditor-toolbar-active') | |
88 .trigger('CKEditorToolbarChanged', [ | |
89 (prev.length < next.length) ? 'added' : 'removed', | |
90 _.difference(_.union(prev, next), _.intersection(prev, next))[0], | |
91 ]); | |
92 } | |
93 } | |
94 } | |
95 }, | |
96 | |
97 /** | |
98 * Asynchronously retrieve the metadata for all available CKEditor features. | |
99 * | |
100 * In order to get a list of all features needed by CKEditor, we create a | |
101 * hidden CKEditor instance, then check the CKEditor's "allowedContent" | |
102 * filter settings. Because creating an instance is expensive, a callback | |
103 * must be provided that will receive a hash of {@link Drupal.EditorFeature} | |
104 * features keyed by feature (button) name. | |
105 * | |
106 * @param {object} CKEditorConfig | |
107 * An object that represents the configuration settings for a CKEditor | |
108 * editor component. | |
109 * @param {function} callback | |
110 * A function to invoke when the instanceReady event is fired by the | |
111 * CKEditor object. | |
112 */ | |
113 getCKEditorFeatures(CKEditorConfig, callback) { | |
114 const getProperties = function (CKEPropertiesList) { | |
115 return (_.isObject(CKEPropertiesList)) ? _.keys(CKEPropertiesList) : []; | |
116 }; | |
117 | |
118 const convertCKERulesToEditorFeature = function (feature, CKEFeatureRules) { | |
119 for (let i = 0; i < CKEFeatureRules.length; i++) { | |
120 const CKERule = CKEFeatureRules[i]; | |
121 const rule = new Drupal.EditorFeatureHTMLRule(); | |
122 | |
123 // Tags. | |
124 const tags = getProperties(CKERule.elements); | |
125 rule.required.tags = (CKERule.propertiesOnly) ? [] : tags; | |
126 rule.allowed.tags = tags; | |
127 // Attributes. | |
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 } | |
339 }); | |
340 // Update the CKEditor features metadata. | |
341 view.model.set('featuresMetadata', features); | |
342 }); | |
343 }); | |
344 }, | |
345 | |
346 /** | |
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, _)); |