annotate core/modules/editor/js/editor.es6.js @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
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);