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