--- /dev/null
+/**
+ * @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 field_id = $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 $(`#${field_id}`).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.<br><br>Save your changes before switching the text format to avoid losing data.', {
+ '%text_format': $select.find('option:selected').text(),
+ });
+ var confirmationDialog = Drupal.dialog(`<div>${message}</div>`, {
+ 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));