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