/** * @file * Attaches behavior for the Editor module. */ (function ($, Drupal, drupalSettings) { /** * Finds the text area field associated with the given text format selector. * * @param {jQuery} $formatSelector * A text format selector DOM element. * * @return {HTMLElement} * The text area DOM element, if it was found. */ function findFieldForFormatSelector($formatSelector) { const fieldId = $formatSelector.attr('data-editor-for'); // This selector will only find text areas in the top-level document. We do // not support attaching editors on text areas within iframes. return $(`#${fieldId}`).get(0); } /** * Changes the text editor on a text area. * * @param {HTMLElement} field * The text area DOM element. * @param {string} newFormatID * The text format we're changing to; the text editor for the currently * active text format will be detached, and the text editor for the new text * format will be attached. */ function changeTextEditor(field, newFormatID) { const previousFormatID = field.getAttribute('data-editor-active-text-format'); // Detach the current editor (if any) and attach a new editor. if (drupalSettings.editor.formats[previousFormatID]) { Drupal.editorDetach(field, drupalSettings.editor.formats[previousFormatID]); } // When no text editor is currently active, stop tracking changes. else { $(field).off('.editor'); } // Attach the new text editor (if any). if (drupalSettings.editor.formats[newFormatID]) { const format = drupalSettings.editor.formats[newFormatID]; filterXssWhenSwitching(field, format, previousFormatID, Drupal.editorAttach); } // Store the new active format. field.setAttribute('data-editor-active-text-format', newFormatID); } /** * Handles changes in text format. * * @param {jQuery.Event} event * The text format change event. */ function onTextFormatChange(event) { const $select = $(event.target); const field = event.data.field; const activeFormatID = field.getAttribute('data-editor-active-text-format'); const newFormatID = $select.val(); // Prevent double-attaching if the change event is triggered manually. if (newFormatID === activeFormatID) { return; } // When changing to a text format that has a text editor associated // with it that supports content filtering, then first ask for // confirmation, because switching text formats might cause certain // markup to be stripped away. const supportContentFiltering = drupalSettings.editor.formats[newFormatID] && drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering; // If there is no content yet, it's always safe to change the text format. const hasContent = field.value !== ''; if (hasContent && supportContentFiltering) { const message = Drupal.t('Changing the text format to %text_format will permanently remove content that is not allowed in that text format.

Save your changes before switching the text format to avoid losing data.', { '%text_format': $select.find('option:selected').text(), }); const confirmationDialog = Drupal.dialog(`
${message}
`, { title: Drupal.t('Change text format?'), dialogClass: 'editor-change-text-format-modal', resizable: false, buttons: [ { text: Drupal.t('Continue'), class: 'button button--primary', click() { changeTextEditor(field, newFormatID); confirmationDialog.close(); }, }, { text: Drupal.t('Cancel'), class: 'button', click() { // Restore the active format ID: cancel changing text format. We // cannot simply call event.preventDefault() because jQuery's // change event is only triggered after the change has already // been accepted. $select.val(activeFormatID); confirmationDialog.close(); }, }, ], // Prevent this modal from being closed without the user making a choice // as per http://stackoverflow.com/a/5438771. closeOnEscape: false, create() { $(this).parent().find('.ui-dialog-titlebar-close').remove(); }, beforeClose: false, close(event) { // Automatically destroy the DOM element that was used for the dialog. $(event.target).remove(); }, }); confirmationDialog.showModal(); } else { changeTextEditor(field, newFormatID); } } /** * Initialize an empty object for editors to place their attachment code. * * @namespace */ Drupal.editors = {}; /** * Enables editors on text_format elements. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches an editor to an input element. * @prop {Drupal~behaviorDetach} detach * Detaches an editor from an input element. */ Drupal.behaviors.editor = { attach(context, settings) { // If there are no editor settings, there are no editors to enable. if (!settings.editor) { return; } $(context).find('[data-editor-for]').once('editor').each(function () { const $this = $(this); const field = findFieldForFormatSelector($this); // Opt-out if no supported text area was found. if (!field) { return; } // Store the current active format. const activeFormatID = $this.val(); field.setAttribute('data-editor-active-text-format', activeFormatID); // Directly attach this text editor, if the text format is enabled. if (settings.editor.formats[activeFormatID]) { // XSS protection for the current text format/editor is performed on // the server side, so we don't need to do anything special here. Drupal.editorAttach(field, settings.editor.formats[activeFormatID]); } // When there is no text editor for this text format, still track // changes, because the user has the ability to switch to some text // editor, otherwise this code would not be executed. $(field).on('change.editor keypress.editor', () => { field.setAttribute('data-editor-value-is-changed', 'true'); // Just knowing that the value was changed is enough, stop tracking. $(field).off('.editor'); }); // Attach onChange handler to text format selector element. if ($this.is('select')) { $this.on('change.editorAttach', { field }, onTextFormatChange); } // Detach any editor when the containing form is submitted. $this.parents('form').on('submit', (event) => { // Do not detach if the event was canceled. if (event.isDefaultPrevented()) { return; } // Detach the current editor (if any). if (settings.editor.formats[activeFormatID]) { Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize'); } }); }); }, detach(context, settings, trigger) { let editors; // The 'serialize' trigger indicates that we should simply update the // underlying element with the new text, without destroying the editor. if (trigger === 'serialize') { // Removing the editor-processed class guarantees that the editor will // be reattached. Only do this if we're planning to destroy the editor. editors = $(context).find('[data-editor-for]').findOnce('editor'); } else { editors = $(context).find('[data-editor-for]').removeOnce('editor'); } editors.each(function () { const $this = $(this); const activeFormatID = $this.val(); const field = findFieldForFormatSelector($this); if (field && activeFormatID in settings.editor.formats) { Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger); } }); }, }; /** * Attaches editor behaviors to the field. * * @param {HTMLElement} field * The textarea DOM element. * @param {object} format * The text format that's being activated, from * drupalSettings.editor.formats. * * @listens event:change * * @fires event:formUpdated */ Drupal.editorAttach = function (field, format) { if (format.editor) { // Attach the text editor. Drupal.editors[format.editor].attach(field, format); // Ensures form.js' 'formUpdated' event is triggered even for changes that // happen within the text editor. Drupal.editors[format.editor].onChange(field, () => { $(field).trigger('formUpdated'); // Keep track of changes, so we know what to do when switching text // formats and guaranteeing XSS protection. field.setAttribute('data-editor-value-is-changed', 'true'); }); } }; /** * Detaches editor behaviors from the field. * * @param {HTMLElement} field * The textarea DOM element. * @param {object} format * The text format that's being activated, from * drupalSettings.editor.formats. * @param {string} trigger * Trigger value from the detach behavior. */ Drupal.editorDetach = function (field, format, trigger) { if (format.editor) { Drupal.editors[format.editor].detach(field, format, trigger); // Restore the original value if the user didn't make any changes yet. if (field.getAttribute('data-editor-value-is-changed') === 'false') { field.value = field.getAttribute('data-editor-value-original'); } } }; /** * Filter away XSS attack vectors when switching text formats. * * @param {HTMLElement} field * The textarea DOM element. * @param {object} format * The text format that's being activated, from * drupalSettings.editor.formats. * @param {string} originalFormatID * The text format ID of the original text format. * @param {function} callback * A callback to be called (with no parameters) after the field's value has * been XSS filtered. */ function filterXssWhenSwitching(field, format, originalFormatID, callback) { // A text editor that already is XSS-safe needs no additional measures. if (format.editor.isXssSafe) { callback(field, format); } // Otherwise, ensure XSS safety: let the server XSS filter this value. else { $.ajax({ url: Drupal.url(`editor/filter_xss/${format.format}`), type: 'POST', data: { value: field.value, original_format_id: originalFormatID, }, dataType: 'json', success(xssFilteredValue) { // If the server returns false, then no XSS filtering is needed. if (xssFilteredValue !== false) { field.value = xssFilteredValue; } callback(field, format); }, }); } } }(jQuery, Drupal, drupalSettings));