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