11fd369586e2190b60c1dfae4e55fee581747026
[yaffs-website] / web / core / modules / ckeditor / js / ckeditor.admin.js
1 /**
2  * @file
3  * CKEditor button and group configuration user interface.
4  */
5
6 (function ($, Drupal, drupalSettings, _) {
7
8   'use strict';
9
10   Drupal.ckeditor = Drupal.ckeditor || {};
11
12   /**
13    * Sets config behaviour and creates config views for the CKEditor toolbar.
14    *
15    * @type {Drupal~behavior}
16    *
17    * @prop {Drupal~behaviorAttach} attach
18    *   Attaches admin behaviour to the CKEditor buttons.
19    * @prop {Drupal~behaviorDetach} detach
20    *   Detaches admin behaviour from the CKEditor buttons on 'unload'.
21    */
22   Drupal.behaviors.ckeditorAdmin = {
23     attach: function (context) {
24       // Process the CKEditor configuration fragment once.
25       var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-configuration');
26       if ($configurationForm.length) {
27         var $textarea = $configurationForm
28           // Hide the textarea that contains the serialized representation of the
29           // CKEditor configuration.
30           .find('.js-form-item-editor-settings-toolbar-button-groups')
31           .hide()
32           // Return the textarea child node from this expression.
33           .find('textarea');
34
35         // The HTML for the CKEditor configuration is assembled on the server
36         // and sent to the client as a serialized DOM fragment.
37         $configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
38
39         // Create a configuration model.
40         var model = Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
41           $textarea: $textarea,
42           activeEditorConfig: JSON.parse($textarea.val()),
43           hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig
44         });
45
46         // Create the configuration Views.
47         var viewDefaults = {
48           model: model,
49           el: $('.ckeditor-toolbar-configuration')
50         };
51         Drupal.ckeditor.views = {
52           controller: new Drupal.ckeditor.ControllerView(viewDefaults),
53           visualView: new Drupal.ckeditor.VisualView(viewDefaults),
54           keyboardView: new Drupal.ckeditor.KeyboardView(viewDefaults),
55           auralView: new Drupal.ckeditor.AuralView(viewDefaults)
56         };
57       }
58     },
59     detach: function (context, settings, trigger) {
60       // Early-return if the trigger for detachment is something else than
61       // unload.
62       if (trigger !== 'unload') {
63         return;
64       }
65
66       // We're detaching because CKEditor as text editor has been disabled; this
67       // really means that all CKEditor toolbar buttons have been removed.
68       // Hence,all editor features will be removed, so any reactions from
69       // filters will be undone.
70       var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').findOnce('ckeditor-configuration');
71       if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.Model) {
72         var config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
73         var buttons = Drupal.ckeditor.views.controller.getButtonList(config);
74         var $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
75         for (var i = 0; i < buttons.length; i++) {
76           $activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]);
77         }
78       }
79     }
80   };
81
82   /**
83    * CKEditor configuration UI methods of Backbone objects.
84    *
85    * @namespace
86    */
87   Drupal.ckeditor = {
88
89     /**
90      * A hash of View instances.
91      *
92      * @type {object}
93      */
94     views: {},
95
96     /**
97      * A hash of Model instances.
98      *
99      * @type {object}
100      */
101     models: {},
102
103     /**
104      * Translates changes in CKEditor config DOM structure to the config model.
105      *
106      * If the button is moved within an existing group, the DOM structure is
107      * simply translated to a configuration model. If the button is moved into a
108      * new group placeholder, then a process is launched to name that group
109      * before the button move is translated into configuration.
110      *
111      * @param {Backbone.View} view
112      *   The Backbone View that invoked this function.
113      * @param {jQuery} $button
114      *   A jQuery set that contains an li element that wraps a button element.
115      * @param {function} callback
116      *   A callback to invoke after the button group naming modal dialog has
117      *   been closed.
118      *
119      */
120     registerButtonMove: function (view, $button, callback) {
121       var $group = $button.closest('.ckeditor-toolbar-group');
122
123       // If dropped in a placeholder button group, the user must name it.
124       if ($group.hasClass('placeholder')) {
125         if (view.isProcessing) {
126           return;
127         }
128         view.isProcessing = true;
129
130         Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
131       }
132       else {
133         view.model.set('isDirty', true);
134         callback(true);
135       }
136     },
137
138     /**
139      * Translates changes in CKEditor config DOM structure to the config model.
140      *
141      * Each row has a placeholder group at the end of the row. A user may not
142      * move an existing button group past the placeholder group at the end of a
143      * row.
144      *
145      * @param {Backbone.View} view
146      *   The Backbone View that invoked this function.
147      * @param {jQuery} $group
148      *   A jQuery set that contains an li element that wraps a group of buttons.
149      */
150     registerGroupMove: function (view, $group) {
151       // Remove placeholder classes if necessary.
152       var $row = $group.closest('.ckeditor-row');
153       if ($row.hasClass('placeholder')) {
154         $row.removeClass('placeholder');
155       }
156       // If there are any rows with just a placeholder group, mark the row as a
157       // placeholder.
158       $row.parent().children().each(function () {
159         $row = $(this);
160         if ($row.find('.ckeditor-toolbar-group').not('.placeholder').length === 0) {
161           $row.addClass('placeholder');
162         }
163       });
164       view.model.set('isDirty', true);
165     },
166
167     /**
168      * Opens a dialog with a form for changing the title of a button group.
169      *
170      * @param {Backbone.View} view
171      *   The Backbone View that invoked this function.
172      * @param {jQuery} $group
173      *   A jQuery set that contains an li element that wraps a group of buttons.
174      * @param {function} callback
175      *   A callback to invoke after the button group naming modal dialog has
176      *   been closed.
177      */
178     openGroupNameDialog: function (view, $group, callback) {
179       callback = callback || function () {};
180
181       /**
182        * Validates the string provided as a button group title.
183        *
184        * @param {HTMLElement} form
185        *   The form DOM element that contains the input with the new button
186        *   group title string.
187        *
188        * @return {bool}
189        *   Returns true when an error exists, otherwise returns false.
190        */
191       function validateForm(form) {
192         if (form.elements[0].value.length === 0) {
193           var $form = $(form);
194           if (!$form.hasClass('errors')) {
195             $form
196               .addClass('errors')
197               .find('input')
198               .addClass('error')
199               .attr('aria-invalid', 'true');
200             $('<div class=\"description\" >' + Drupal.t('Please provide a name for the button group.') + '</div>').insertAfter(form.elements[0]);
201           }
202           return true;
203         }
204         return false;
205       }
206
207       /**
208        * Attempts to close the dialog; Validates user input.
209        *
210        * @param {string} action
211        *   The dialog action chosen by the user: 'apply' or 'cancel'.
212        * @param {HTMLElement} form
213        *   The form DOM element that contains the input with the new button
214        *   group title string.
215        */
216       function closeDialog(action, form) {
217
218         /**
219          * Closes the dialog when the user cancels or supplies valid data.
220          */
221         function shutdown() {
222           dialog.close(action);
223
224           // The processing marker can be deleted since the dialog has been
225           // closed.
226           delete view.isProcessing;
227         }
228
229         /**
230          * Applies a string as the name of a CKEditor button group.
231          *
232          * @param {jQuery} $group
233          *   A jQuery set that contains an li element that wraps a group of
234          *   buttons.
235          * @param {string} name
236          *   The new name of the CKEditor button group.
237          */
238         function namePlaceholderGroup($group, name) {
239           // If it's currently still a placeholder, then that means we're
240           // creating a new group, and we must do some extra work.
241           if ($group.hasClass('placeholder')) {
242             // Remove all whitespace from the name, lowercase it and ensure
243             // HTML-safe encoding, then use this as the group ID for CKEditor
244             // configuration UI accessibility purposes only.
245             var groupID = 'ckeditor-toolbar-group-aria-label-for-' + Drupal.checkPlain(name.toLowerCase().replace(/\s/g, '-'));
246             $group
247               // Update the group container.
248               .removeAttr('aria-label')
249               .attr('data-drupal-ckeditor-type', 'group')
250               .attr('tabindex', 0)
251               // Update the group heading.
252               .children('.ckeditor-toolbar-group-name')
253               .attr('id', groupID)
254               .end()
255               // Update the group items.
256               .children('.ckeditor-toolbar-group-buttons')
257               .attr('aria-labelledby', groupID);
258           }
259
260           $group
261             .attr('data-drupal-ckeditor-toolbar-group-name', name)
262             .children('.ckeditor-toolbar-group-name')
263             .text(name);
264         }
265
266         // Invoke a user-provided callback and indicate failure.
267         if (action === 'cancel') {
268           shutdown();
269           callback(false, $group);
270           return;
271         }
272
273         // Validate that a group name was provided.
274         if (form && validateForm(form)) {
275           return;
276         }
277
278         // React to application of a valid group name.
279         if (action === 'apply') {
280           shutdown();
281           // Apply the provided name to the button group label.
282           namePlaceholderGroup($group, Drupal.checkPlain(form.elements[0].value));
283           // Remove placeholder classes so that new placeholders will be
284           // inserted.
285           $group.closest('.ckeditor-row.placeholder').addBack().removeClass('placeholder');
286
287           // Invoke a user-provided callback and indicate success.
288           callback(true, $group);
289
290           // Signal that the active toolbar DOM structure has changed.
291           view.model.set('isDirty', true);
292         }
293       }
294
295       // Create a Drupal dialog that will get a button group name from the user.
296       var $ckeditorButtonGroupNameForm = $(Drupal.theme('ckeditorButtonGroupNameForm'));
297       var dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
298         title: Drupal.t('Button group name'),
299         dialogClass: 'ckeditor-name-toolbar-group',
300         resizable: false,
301         buttons: [
302           {
303             text: Drupal.t('Apply'),
304             click: function () {
305               closeDialog('apply', this);
306             },
307             primary: true
308           },
309           {
310             text: Drupal.t('Cancel'),
311             click: function () {
312               closeDialog('cancel');
313             }
314           }
315         ],
316         open: function () {
317           var form = this;
318           var $form = $(this);
319           var $widget = $form.parent();
320           $widget.find('.ui-dialog-titlebar-close').remove();
321           // Set a click handler on the input and button in the form.
322           $widget.on('keypress.ckeditor', 'input, button', function (event) {
323             // React to enter key press.
324             if (event.keyCode === 13) {
325               var $target = $(event.currentTarget);
326               var data = $target.data('ui-button');
327               var action = 'apply';
328               // Assume 'apply', but take into account that the user might have
329               // pressed the enter key on the dialog buttons.
330               if (data && data.options && data.options.label) {
331                 action = data.options.label.toLowerCase();
332               }
333               closeDialog(action, form);
334               event.stopPropagation();
335               event.stopImmediatePropagation();
336               event.preventDefault();
337             }
338           });
339           // Announce to the user that a modal dialog is open.
340           var text = Drupal.t('Editing the name of the new button group in a dialog.');
341           if (typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !== 'undefined') {
342             text = Drupal.t('Editing the name of the "@groupName" button group in a dialog.', {
343               '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name')
344             });
345           }
346           Drupal.announce(text);
347         },
348         close: function (event) {
349           // Automatically destroy the DOM element that was used for the dialog.
350           $(event.target).remove();
351         }
352       });
353       // A modal dialog is used because the user must provide a button group
354       // name or cancel the button placement before taking any other action.
355       dialog.showModal();
356
357       $(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input'))
358         // When editing, set the "group name" input in the form to the current
359         // value.
360         .attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
361         // Focus on the "group name" input in the form.
362         .trigger('focus');
363     }
364
365   };
366
367   /**
368    * Automatically shows/hides settings of buttons-only CKEditor plugins.
369    *
370    * @type {Drupal~behavior}
371    *
372    * @prop {Drupal~behaviorAttach} attach
373    *   Attaches show/hide behaviour to Plugin Settings buttons.
374    */
375   Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
376     attach: function (context) {
377       var $context = $(context);
378       var $ckeditorPluginSettings = $context.find('#ckeditor-plugin-settings').once('ckeditor-plugin-settings');
379       if ($ckeditorPluginSettings.length) {
380         // Hide all button-dependent plugin settings initially.
381         $ckeditorPluginSettings.find('[data-ckeditor-buttons]').each(function () {
382           var $this = $(this);
383           if ($this.data('verticalTab')) {
384             $this.data('verticalTab').tabHide();
385           }
386           else {
387             // On very narrow viewports, Vertical Tabs are disabled.
388             $this.hide();
389           }
390           $this.data('ckeditorButtonPluginSettingsActiveButtons', []);
391         });
392
393         // Whenever a button is added or removed, check if we should show or
394         // hide the corresponding plugin settings. (Note that upon
395         // initialization, each button that already is part of the toolbar still
396         // is considered "added", hence it also works correctly for buttons that
397         // were added previously.)
398         $context
399           .find('.ckeditor-toolbar-active')
400           .off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
401           .on('CKEditorToolbarChanged.ckeditorAdminPluginSettings', function (event, action, button) {
402             var $pluginSettings = $ckeditorPluginSettings
403               .find('[data-ckeditor-buttons~=' + button + ']');
404
405             // No settings for this button.
406             if ($pluginSettings.length === 0) {
407               return;
408             }
409
410             var verticalTab = $pluginSettings.data('verticalTab');
411             var activeButtons = $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons');
412             if (action === 'added') {
413               activeButtons.push(button);
414               // Show this plugin's settings if >=1 of its buttons are active.
415               if (verticalTab) {
416                 verticalTab.tabShow();
417               }
418               else {
419                 // On very narrow viewports, Vertical Tabs remain fieldsets.
420                 $pluginSettings.show();
421               }
422
423             }
424             else {
425               // Remove this button from the list of active buttons.
426               activeButtons.splice(activeButtons.indexOf(button), 1);
427               // Show this plugin's settings 0 of its buttons are active.
428               if (activeButtons.length === 0) {
429                 if (verticalTab) {
430                   verticalTab.tabHide();
431                 }
432                 else {
433                   // On very narrow viewports, Vertical Tabs are disabled.
434                   $pluginSettings.hide();
435                 }
436               }
437             }
438             $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons', activeButtons);
439           });
440       }
441     }
442   };
443
444   /**
445    * Themes a blank CKEditor row.
446    *
447    * @return {string}
448    *   A HTML string for a CKEditor row.
449    */
450   Drupal.theme.ckeditorRow = function () {
451     return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
452   };
453
454   /**
455    * Themes a blank CKEditor button group.
456    *
457    * @return {string}
458    *   A HTML string for a CKEditor button group.
459    */
460   Drupal.theme.ckeditorToolbarGroup = function () {
461     var group = '';
462     group += '<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="' + Drupal.t('Place a button to create a new button group.') + '">';
463     group += '<h3 class="ckeditor-toolbar-group-name">' + Drupal.t('New group') + '</h3>';
464     group += '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
465     group += '</li>';
466     return group;
467   };
468
469   /**
470    * Themes a form for changing the title of a CKEditor button group.
471    *
472    * @return {string}
473    *   A HTML string for the form for the title of a CKEditor button group.
474    */
475   Drupal.theme.ckeditorButtonGroupNameForm = function () {
476     return '<form><input name="group-name" required="required"></form>';
477   };
478
479   /**
480    * Themes a button that will toggle the button group names in active config.
481    *
482    * @return {string}
483    *   A HTML string for the button to toggle group names.
484    */
485   Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
486     return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
487   };
488
489   /**
490    * Themes a button that will prompt the user to name a new button group.
491    *
492    * @return {string}
493    *   A HTML string for the button to create a name for a new button group.
494    */
495   Drupal.theme.ckeditorNewButtonGroup = function () {
496     return '<li class="ckeditor-add-new-group"><button aria-label="' + Drupal.t('Add a CKEditor button group to the end of this row.') + '">' + Drupal.t('Add group') + '</button></li>';
497   };
498
499 })(jQuery, Drupal, drupalSettings, _);