2ce678f7b23c7ab80176b9a4ffc12e4a3419b081
[yaffs-website] / web / core / modules / editor / js / editor.es6.js
1 /**
2  * @file
3  * Attaches behavior for the Editor module.
4  */
5
6 (function($, Drupal, drupalSettings) {
7   /**
8    * Finds the text area field associated with the given text format selector.
9    *
10    * @param {jQuery} $formatSelector
11    *   A text format selector DOM element.
12    *
13    * @return {HTMLElement}
14    *   The text area DOM element, if it was found.
15    */
16   function findFieldForFormatSelector($formatSelector) {
17     const fieldId = $formatSelector.attr('data-editor-for');
18     // This selector will only find text areas in the top-level document. We do
19     // not support attaching editors on text areas within iframes.
20     return $(`#${fieldId}`).get(0);
21   }
22
23   /**
24    * Filter away XSS attack vectors when switching text formats.
25    *
26    * @param {HTMLElement} field
27    *   The textarea DOM element.
28    * @param {object} format
29    *   The text format that's being activated, from
30    *   drupalSettings.editor.formats.
31    * @param {string} originalFormatID
32    *   The text format ID of the original text format.
33    * @param {function} callback
34    *   A callback to be called (with no parameters) after the field's value has
35    *   been XSS filtered.
36    */
37   function filterXssWhenSwitching(field, format, originalFormatID, callback) {
38     // A text editor that already is XSS-safe needs no additional measures.
39     if (format.editor.isXssSafe) {
40       callback(field, format);
41     }
42     // Otherwise, ensure XSS safety: let the server XSS filter this value.
43     else {
44       $.ajax({
45         url: Drupal.url(`editor/filter_xss/${format.format}`),
46         type: 'POST',
47         data: {
48           value: field.value,
49           original_format_id: originalFormatID,
50         },
51         dataType: 'json',
52         success(xssFilteredValue) {
53           // If the server returns false, then no XSS filtering is needed.
54           if (xssFilteredValue !== false) {
55             field.value = xssFilteredValue;
56           }
57           callback(field, format);
58         },
59       });
60     }
61   }
62
63   /**
64    * Changes the text editor on a text area.
65    *
66    * @param {HTMLElement} field
67    *   The text area DOM element.
68    * @param {string} newFormatID
69    *   The text format we're changing to; the text editor for the currently
70    *   active text format will be detached, and the text editor for the new text
71    *   format will be attached.
72    */
73   function changeTextEditor(field, newFormatID) {
74     const previousFormatID = field.getAttribute(
75       'data-editor-active-text-format',
76     );
77
78     // Detach the current editor (if any) and attach a new editor.
79     if (drupalSettings.editor.formats[previousFormatID]) {
80       Drupal.editorDetach(
81         field,
82         drupalSettings.editor.formats[previousFormatID],
83       );
84     }
85     // When no text editor is currently active, stop tracking changes.
86     else {
87       $(field).off('.editor');
88     }
89
90     // Attach the new text editor (if any).
91     if (drupalSettings.editor.formats[newFormatID]) {
92       const format = drupalSettings.editor.formats[newFormatID];
93       filterXssWhenSwitching(
94         field,
95         format,
96         previousFormatID,
97         Drupal.editorAttach,
98       );
99     }
100
101     // Store the new active format.
102     field.setAttribute('data-editor-active-text-format', newFormatID);
103   }
104
105   /**
106    * Handles changes in text format.
107    *
108    * @param {jQuery.Event} event
109    *   The text format change event.
110    */
111   function onTextFormatChange(event) {
112     const $select = $(event.target);
113     const field = event.data.field;
114     const activeFormatID = field.getAttribute('data-editor-active-text-format');
115     const newFormatID = $select.val();
116
117     // Prevent double-attaching if the change event is triggered manually.
118     if (newFormatID === activeFormatID) {
119       return;
120     }
121
122     // When changing to a text format that has a text editor associated
123     // with it that supports content filtering, then first ask for
124     // confirmation, because switching text formats might cause certain
125     // markup to be stripped away.
126     const supportContentFiltering =
127       drupalSettings.editor.formats[newFormatID] &&
128       drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering;
129     // If there is no content yet, it's always safe to change the text format.
130     const hasContent = field.value !== '';
131     if (hasContent && supportContentFiltering) {
132       const message = Drupal.t(
133         'Changing the text format to %text_format will permanently remove content that is not allowed in that text format.<br><br>Save your changes before switching the text format to avoid losing data.',
134         {
135           '%text_format': $select.find('option:selected').text(),
136         },
137       );
138       const confirmationDialog = Drupal.dialog(`<div>${message}</div>`, {
139         title: Drupal.t('Change text format?'),
140         dialogClass: 'editor-change-text-format-modal',
141         resizable: false,
142         buttons: [
143           {
144             text: Drupal.t('Continue'),
145             class: 'button button--primary',
146             click() {
147               changeTextEditor(field, newFormatID);
148               confirmationDialog.close();
149             },
150           },
151           {
152             text: Drupal.t('Cancel'),
153             class: 'button',
154             click() {
155               // Restore the active format ID: cancel changing text format. We
156               // cannot simply call event.preventDefault() because jQuery's
157               // change event is only triggered after the change has already
158               // been accepted.
159               $select.val(activeFormatID);
160               confirmationDialog.close();
161             },
162           },
163         ],
164         // Prevent this modal from being closed without the user making a choice
165         // as per http://stackoverflow.com/a/5438771.
166         closeOnEscape: false,
167         create() {
168           $(this)
169             .parent()
170             .find('.ui-dialog-titlebar-close')
171             .remove();
172         },
173         beforeClose: false,
174         close(event) {
175           // Automatically destroy the DOM element that was used for the dialog.
176           $(event.target).remove();
177         },
178       });
179
180       confirmationDialog.showModal();
181     } else {
182       changeTextEditor(field, newFormatID);
183     }
184   }
185
186   /**
187    * Initialize an empty object for editors to place their attachment code.
188    *
189    * @namespace
190    */
191   Drupal.editors = {};
192
193   /**
194    * Enables editors on text_format elements.
195    *
196    * @type {Drupal~behavior}
197    *
198    * @prop {Drupal~behaviorAttach} attach
199    *   Attaches an editor to an input element.
200    * @prop {Drupal~behaviorDetach} detach
201    *   Detaches an editor from an input element.
202    */
203   Drupal.behaviors.editor = {
204     attach(context, settings) {
205       // If there are no editor settings, there are no editors to enable.
206       if (!settings.editor) {
207         return;
208       }
209
210       $(context)
211         .find('[data-editor-for]')
212         .once('editor')
213         .each(function() {
214           const $this = $(this);
215           const field = findFieldForFormatSelector($this);
216
217           // Opt-out if no supported text area was found.
218           if (!field) {
219             return;
220           }
221
222           // Store the current active format.
223           const activeFormatID = $this.val();
224           field.setAttribute('data-editor-active-text-format', activeFormatID);
225
226           // Directly attach this text editor, if the text format is enabled.
227           if (settings.editor.formats[activeFormatID]) {
228             // XSS protection for the current text format/editor is performed on
229             // the server side, so we don't need to do anything special here.
230             Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
231           }
232           // When there is no text editor for this text format, still track
233           // changes, because the user has the ability to switch to some text
234           // editor, otherwise this code would not be executed.
235           $(field).on('change.editor keypress.editor', () => {
236             field.setAttribute('data-editor-value-is-changed', 'true');
237             // Just knowing that the value was changed is enough, stop tracking.
238             $(field).off('.editor');
239           });
240
241           // Attach onChange handler to text format selector element.
242           if ($this.is('select')) {
243             $this.on('change.editorAttach', { field }, onTextFormatChange);
244           }
245           // Detach any editor when the containing form is submitted.
246           $this.parents('form').on('submit', event => {
247             // Do not detach if the event was canceled.
248             if (event.isDefaultPrevented()) {
249               return;
250             }
251             // Detach the current editor (if any).
252             if (settings.editor.formats[activeFormatID]) {
253               Drupal.editorDetach(
254                 field,
255                 settings.editor.formats[activeFormatID],
256                 'serialize',
257               );
258             }
259           });
260         });
261     },
262
263     detach(context, settings, trigger) {
264       let editors;
265       // The 'serialize' trigger indicates that we should simply update the
266       // underlying element with the new text, without destroying the editor.
267       if (trigger === 'serialize') {
268         // Removing the editor-processed class guarantees that the editor will
269         // be reattached. Only do this if we're planning to destroy the editor.
270         editors = $(context)
271           .find('[data-editor-for]')
272           .findOnce('editor');
273       } else {
274         editors = $(context)
275           .find('[data-editor-for]')
276           .removeOnce('editor');
277       }
278
279       editors.each(function() {
280         const $this = $(this);
281         const activeFormatID = $this.val();
282         const field = findFieldForFormatSelector($this);
283         if (field && activeFormatID in settings.editor.formats) {
284           Drupal.editorDetach(
285             field,
286             settings.editor.formats[activeFormatID],
287             trigger,
288           );
289         }
290       });
291     },
292   };
293
294   /**
295    * Attaches editor behaviors to the field.
296    *
297    * @param {HTMLElement} field
298    *   The textarea DOM element.
299    * @param {object} format
300    *   The text format that's being activated, from
301    *   drupalSettings.editor.formats.
302    *
303    * @listens event:change
304    *
305    * @fires event:formUpdated
306    */
307   Drupal.editorAttach = function(field, format) {
308     if (format.editor) {
309       // Attach the text editor.
310       Drupal.editors[format.editor].attach(field, format);
311
312       // Ensures form.js' 'formUpdated' event is triggered even for changes that
313       // happen within the text editor.
314       Drupal.editors[format.editor].onChange(field, () => {
315         $(field).trigger('formUpdated');
316
317         // Keep track of changes, so we know what to do when switching text
318         // formats and guaranteeing XSS protection.
319         field.setAttribute('data-editor-value-is-changed', 'true');
320       });
321     }
322   };
323
324   /**
325    * Detaches editor behaviors from the field.
326    *
327    * @param {HTMLElement} field
328    *   The textarea DOM element.
329    * @param {object} format
330    *   The text format that's being activated, from
331    *   drupalSettings.editor.formats.
332    * @param {string} trigger
333    *   Trigger value from the detach behavior.
334    */
335   Drupal.editorDetach = function(field, format, trigger) {
336     if (format.editor) {
337       Drupal.editors[format.editor].detach(field, format, trigger);
338
339       // Restore the original value if the user didn't make any changes yet.
340       if (field.getAttribute('data-editor-value-is-changed') === 'false') {
341         field.value = field.getAttribute('data-editor-value-original');
342       }
343     }
344   };
345 })(jQuery, Drupal, drupalSettings);