3 * Attaches behavior for the Editor module.
6 (function ($, Drupal, drupalSettings) {
8 * Finds the text area field associated with the given text format selector.
10 * @param {jQuery} $formatSelector
11 * A text format selector DOM element.
13 * @return {HTMLElement}
14 * The text area DOM element, if it was found.
16 function findFieldForFormatSelector($formatSelector) {
17 const field_id = $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 $(`#${field_id}`).get(0);
24 * Changes the text editor on a text area.
26 * @param {HTMLElement} field
27 * The text area DOM element.
28 * @param {string} newFormatID
29 * The text format we're changing to; the text editor for the currently
30 * active text format will be detached, and the text editor for the new text
31 * format will be attached.
33 function changeTextEditor(field, newFormatID) {
34 const previousFormatID = field.getAttribute('data-editor-active-text-format');
36 // Detach the current editor (if any) and attach a new editor.
37 if (drupalSettings.editor.formats[previousFormatID]) {
38 Drupal.editorDetach(field, drupalSettings.editor.formats[previousFormatID]);
40 // When no text editor is currently active, stop tracking changes.
42 $(field).off('.editor');
45 // Attach the new text editor (if any).
46 if (drupalSettings.editor.formats[newFormatID]) {
47 const format = drupalSettings.editor.formats[newFormatID];
48 filterXssWhenSwitching(field, format, previousFormatID, Drupal.editorAttach);
51 // Store the new active format.
52 field.setAttribute('data-editor-active-text-format', newFormatID);
56 * Handles changes in text format.
58 * @param {jQuery.Event} event
59 * The text format change event.
61 function onTextFormatChange(event) {
62 const $select = $(event.target);
63 const field = event.data.field;
64 const activeFormatID = field.getAttribute('data-editor-active-text-format');
65 const newFormatID = $select.val();
67 // Prevent double-attaching if the change event is triggered manually.
68 if (newFormatID === activeFormatID) {
72 // When changing to a text format that has a text editor associated
73 // with it that supports content filtering, then first ask for
74 // confirmation, because switching text formats might cause certain
75 // markup to be stripped away.
76 const supportContentFiltering = drupalSettings.editor.formats[newFormatID] && drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering;
77 // If there is no content yet, it's always safe to change the text format.
78 const hasContent = field.value !== '';
79 if (hasContent && supportContentFiltering) {
80 const 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.', {
81 '%text_format': $select.find('option:selected').text(),
83 var confirmationDialog = Drupal.dialog(`<div>${message}</div>`, {
84 title: Drupal.t('Change text format?'),
85 dialogClass: 'editor-change-text-format-modal',
89 text: Drupal.t('Continue'),
90 class: 'button button--primary',
92 changeTextEditor(field, newFormatID);
93 confirmationDialog.close();
97 text: Drupal.t('Cancel'),
100 // Restore the active format ID: cancel changing text format. We
101 // cannot simply call event.preventDefault() because jQuery's
102 // change event is only triggered after the change has already
104 $select.val(activeFormatID);
105 confirmationDialog.close();
109 // Prevent this modal from being closed without the user making a choice
110 // as per http://stackoverflow.com/a/5438771.
111 closeOnEscape: false,
113 $(this).parent().find('.ui-dialog-titlebar-close').remove();
117 // Automatically destroy the DOM element that was used for the dialog.
118 $(event.target).remove();
122 confirmationDialog.showModal();
125 changeTextEditor(field, newFormatID);
130 * Initialize an empty object for editors to place their attachment code.
137 * Enables editors on text_format elements.
139 * @type {Drupal~behavior}
141 * @prop {Drupal~behaviorAttach} attach
142 * Attaches an editor to an input element.
143 * @prop {Drupal~behaviorDetach} detach
144 * Detaches an editor from an input element.
146 Drupal.behaviors.editor = {
147 attach(context, settings) {
148 // If there are no editor settings, there are no editors to enable.
149 if (!settings.editor) {
153 $(context).find('[data-editor-for]').once('editor').each(function () {
154 const $this = $(this);
155 const field = findFieldForFormatSelector($this);
157 // Opt-out if no supported text area was found.
162 // Store the current active format.
163 const activeFormatID = $this.val();
164 field.setAttribute('data-editor-active-text-format', activeFormatID);
166 // Directly attach this text editor, if the text format is enabled.
167 if (settings.editor.formats[activeFormatID]) {
168 // XSS protection for the current text format/editor is performed on
169 // the server side, so we don't need to do anything special here.
170 Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
172 // When there is no text editor for this text format, still track
173 // changes, because the user has the ability to switch to some text
174 // editor, otherwise this code would not be executed.
175 $(field).on('change.editor keypress.editor', () => {
176 field.setAttribute('data-editor-value-is-changed', 'true');
177 // Just knowing that the value was changed is enough, stop tracking.
178 $(field).off('.editor');
181 // Attach onChange handler to text format selector element.
182 if ($this.is('select')) {
183 $this.on('change.editorAttach', { field }, onTextFormatChange);
185 // Detach any editor when the containing form is submitted.
186 $this.parents('form').on('submit', (event) => {
187 // Do not detach if the event was canceled.
188 if (event.isDefaultPrevented()) {
191 // Detach the current editor (if any).
192 if (settings.editor.formats[activeFormatID]) {
193 Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize');
199 detach(context, settings, trigger) {
201 // The 'serialize' trigger indicates that we should simply update the
202 // underlying element with the new text, without destroying the editor.
203 if (trigger === 'serialize') {
204 // Removing the editor-processed class guarantees that the editor will
205 // be reattached. Only do this if we're planning to destroy the editor.
206 editors = $(context).find('[data-editor-for]').findOnce('editor');
209 editors = $(context).find('[data-editor-for]').removeOnce('editor');
212 editors.each(function () {
213 const $this = $(this);
214 const activeFormatID = $this.val();
215 const field = findFieldForFormatSelector($this);
216 if (field && activeFormatID in settings.editor.formats) {
217 Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger);
224 * Attaches editor behaviors to the field.
226 * @param {HTMLElement} field
227 * The textarea DOM element.
228 * @param {object} format
229 * The text format that's being activated, from
230 * drupalSettings.editor.formats.
232 * @listens event:change
234 * @fires event:formUpdated
236 Drupal.editorAttach = function (field, format) {
238 // Attach the text editor.
239 Drupal.editors[format.editor].attach(field, format);
241 // Ensures form.js' 'formUpdated' event is triggered even for changes that
242 // happen within the text editor.
243 Drupal.editors[format.editor].onChange(field, () => {
244 $(field).trigger('formUpdated');
246 // Keep track of changes, so we know what to do when switching text
247 // formats and guaranteeing XSS protection.
248 field.setAttribute('data-editor-value-is-changed', 'true');
254 * Detaches editor behaviors from the field.
256 * @param {HTMLElement} field
257 * The textarea DOM element.
258 * @param {object} format
259 * The text format that's being activated, from
260 * drupalSettings.editor.formats.
261 * @param {string} trigger
262 * Trigger value from the detach behavior.
264 Drupal.editorDetach = function (field, format, trigger) {
266 Drupal.editors[format.editor].detach(field, format, trigger);
268 // Restore the original value if the user didn't make any changes yet.
269 if (field.getAttribute('data-editor-value-is-changed') === 'false') {
270 field.value = field.getAttribute('data-editor-value-original');
276 * Filter away XSS attack vectors when switching text formats.
278 * @param {HTMLElement} field
279 * The textarea DOM element.
280 * @param {object} format
281 * The text format that's being activated, from
282 * drupalSettings.editor.formats.
283 * @param {string} originalFormatID
284 * The text format ID of the original text format.
285 * @param {function} callback
286 * A callback to be called (with no parameters) after the field's value has
289 function filterXssWhenSwitching(field, format, originalFormatID, callback) {
290 // A text editor that already is XSS-safe needs no additional measures.
291 if (format.editor.isXssSafe) {
292 callback(field, format);
294 // Otherwise, ensure XSS safety: let the server XSS filter this value.
297 url: Drupal.url(`editor/filter_xss/${format.format}`),
301 original_format_id: originalFormatID,
304 success(xssFilteredValue) {
305 // If the server returns false, then no XSS filtering is needed.
306 if (xssFilteredValue !== false) {
307 field.value = xssFilteredValue;
309 callback(field, format);
314 }(jQuery, Drupal, drupalSettings));