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