3 * Attaches behavior for the Editor module.
6 (function ($, Drupal, drupalSettings) {
11 * Finds the text area field associated with the given text format selector.
13 * @param {jQuery} $formatSelector
14 * A text format selector DOM element.
16 * @return {HTMLElement}
17 * The text area DOM element, if it was found.
19 function findFieldForFormatSelector($formatSelector) {
20 var field_id = $formatSelector.attr('data-editor-for');
21 // This selector will only find text areas in the top-level document. We do
22 // not support attaching editors on text areas within iframes.
23 return $('#' + field_id).get(0);
27 * Changes the text editor on a text area.
29 * @param {HTMLElement} field
30 * The text area DOM element.
31 * @param {string} newFormatID
32 * The text format we're changing to; the text editor for the currently
33 * active text format will be detached, and the text editor for the new text
34 * format will be attached.
36 function changeTextEditor(field, newFormatID) {
37 var previousFormatID = field.getAttribute('data-editor-active-text-format');
39 // Detach the current editor (if any) and attach a new editor.
40 if (drupalSettings.editor.formats[previousFormatID]) {
41 Drupal.editorDetach(field, drupalSettings.editor.formats[previousFormatID]);
43 // When no text editor is currently active, stop tracking changes.
45 $(field).off('.editor');
48 // Attach the new text editor (if any).
49 if (drupalSettings.editor.formats[newFormatID]) {
50 var format = drupalSettings.editor.formats[newFormatID];
51 filterXssWhenSwitching(field, format, previousFormatID, Drupal.editorAttach);
54 // Store the new active format.
55 field.setAttribute('data-editor-active-text-format', newFormatID);
59 * Handles changes in text format.
61 * @param {jQuery.Event} event
62 * The text format change event.
64 function onTextFormatChange(event) {
65 var $select = $(event.target);
66 var field = event.data.field;
67 var activeFormatID = field.getAttribute('data-editor-active-text-format');
68 var newFormatID = $select.val();
70 // Prevent double-attaching if the change event is triggered manually.
71 if (newFormatID === activeFormatID) {
75 // When changing to a text format that has a text editor associated
76 // with it that supports content filtering, then first ask for
77 // confirmation, because switching text formats might cause certain
78 // markup to be stripped away.
79 var supportContentFiltering = drupalSettings.editor.formats[newFormatID] && drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering;
80 // If there is no content yet, it's always safe to change the text format.
81 var hasContent = field.value !== '';
82 if (hasContent && supportContentFiltering) {
83 var 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.', {
84 '%text_format': $select.find('option:selected').text()
86 var confirmationDialog = Drupal.dialog('<div>' + message + '</div>', {
87 title: Drupal.t('Change text format?'),
88 dialogClass: 'editor-change-text-format-modal',
92 text: Drupal.t('Continue'),
93 class: 'button button--primary',
95 changeTextEditor(field, newFormatID);
96 confirmationDialog.close();
100 text: Drupal.t('Cancel'),
103 // Restore the active format ID: cancel changing text format. We
104 // cannot simply call event.preventDefault() because jQuery's
105 // change event is only triggered after the change has already
107 $select.val(activeFormatID);
108 confirmationDialog.close();
112 // Prevent this modal from being closed without the user making a choice
113 // as per http://stackoverflow.com/a/5438771.
114 closeOnEscape: false,
115 create: function () {
116 $(this).parent().find('.ui-dialog-titlebar-close').remove();
119 close: function (event) {
120 // Automatically destroy the DOM element that was used for the dialog.
121 $(event.target).remove();
125 confirmationDialog.showModal();
128 changeTextEditor(field, newFormatID);
133 * Initialize an empty object for editors to place their attachment code.
140 * Enables editors on text_format elements.
142 * @type {Drupal~behavior}
144 * @prop {Drupal~behaviorAttach} attach
145 * Attaches an editor to an input element.
146 * @prop {Drupal~behaviorDetach} detach
147 * Detaches an editor from an input element.
149 Drupal.behaviors.editor = {
150 attach: function (context, settings) {
151 // If there are no editor settings, there are no editors to enable.
152 if (!settings.editor) {
156 $(context).find('[data-editor-for]').once('editor').each(function () {
158 var field = findFieldForFormatSelector($this);
160 // Opt-out if no supported text area was found.
165 // Store the current active format.
166 var activeFormatID = $this.val();
167 field.setAttribute('data-editor-active-text-format', activeFormatID);
169 // Directly attach this text editor, if the text format is enabled.
170 if (settings.editor.formats[activeFormatID]) {
171 // XSS protection for the current text format/editor is performed on
172 // the server side, so we don't need to do anything special here.
173 Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
175 // When there is no text editor for this text format, still track
176 // changes, because the user has the ability to switch to some text
177 // editor, otherwise this code would not be executed.
178 $(field).on('change.editor keypress.editor', function () {
179 field.setAttribute('data-editor-value-is-changed', 'true');
180 // Just knowing that the value was changed is enough, stop tracking.
181 $(field).off('.editor');
184 // Attach onChange handler to text format selector element.
185 if ($this.is('select')) {
186 $this.on('change.editorAttach', {field: field}, onTextFormatChange);
188 // Detach any editor when the containing form is submitted.
189 $this.parents('form').on('submit', function (event) {
190 // Do not detach if the event was canceled.
191 if (event.isDefaultPrevented()) {
194 // Detach the current editor (if any).
195 if (settings.editor.formats[activeFormatID]) {
196 Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize');
202 detach: function (context, settings, trigger) {
204 // The 'serialize' trigger indicates that we should simply update the
205 // underlying element with the new text, without destroying the editor.
206 if (trigger === 'serialize') {
207 // Removing the editor-processed class guarantees that the editor will
208 // be reattached. Only do this if we're planning to destroy the editor.
209 editors = $(context).find('[data-editor-for]').findOnce('editor');
212 editors = $(context).find('[data-editor-for]').removeOnce('editor');
215 editors.each(function () {
217 var activeFormatID = $this.val();
218 var field = findFieldForFormatSelector($this);
219 if (field && activeFormatID in settings.editor.formats) {
220 Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger);
227 * Attaches editor behaviors to the field.
229 * @param {HTMLElement} field
230 * The textarea DOM element.
231 * @param {object} format
232 * The text format that's being activated, from
233 * drupalSettings.editor.formats.
235 * @listens event:change
237 * @fires event:formUpdated
239 Drupal.editorAttach = function (field, format) {
241 // Attach the text editor.
242 Drupal.editors[format.editor].attach(field, format);
244 // Ensures form.js' 'formUpdated' event is triggered even for changes that
245 // happen within the text editor.
246 Drupal.editors[format.editor].onChange(field, function () {
247 $(field).trigger('formUpdated');
249 // Keep track of changes, so we know what to do when switching text
250 // formats and guaranteeing XSS protection.
251 field.setAttribute('data-editor-value-is-changed', 'true');
257 * Detaches editor behaviors from the field.
259 * @param {HTMLElement} field
260 * The textarea DOM element.
261 * @param {object} format
262 * The text format that's being activated, from
263 * drupalSettings.editor.formats.
264 * @param {string} trigger
265 * Trigger value from the detach behavior.
267 Drupal.editorDetach = function (field, format, trigger) {
269 Drupal.editors[format.editor].detach(field, format, trigger);
271 // Restore the original value if the user didn't make any changes yet.
272 if (field.getAttribute('data-editor-value-is-changed') === 'false') {
273 field.value = field.getAttribute('data-editor-value-original');
279 * Filter away XSS attack vectors when switching text formats.
281 * @param {HTMLElement} field
282 * The textarea DOM element.
283 * @param {object} format
284 * The text format that's being activated, from
285 * drupalSettings.editor.formats.
286 * @param {string} originalFormatID
287 * The text format ID of the original text format.
288 * @param {function} callback
289 * A callback to be called (with no parameters) after the field's value has
292 function filterXssWhenSwitching(field, format, originalFormatID, callback) {
293 // A text editor that already is XSS-safe needs no additional measures.
294 if (format.editor.isXssSafe) {
295 callback(field, format);
297 // Otherwise, ensure XSS safety: let the server XSS filter this value.
300 url: Drupal.url('editor/filter_xss/' + format.format),
304 original_format_id: originalFormatID
307 success: function (xssFilteredValue) {
308 // If the server returns false, then no XSS filtering is needed.
309 if (xssFilteredValue !== false) {
310 field.value = xssFilteredValue;
312 callback(field, format);
318 })(jQuery, Drupal, drupalSettings);