Version 1
[yaffs-website] / web / core / modules / editor / js / editor.formattedTextEditor.js
1 /**
2  * @file
3  * Text editor-based in-place editor for formatted text content in Drupal.
4  *
5  * Depends on editor.module. Works with any (WYSIWYG) editor that implements the
6  * editor.js API, including the optional attachInlineEditor() and onChange()
7  * methods.
8  * For example, assuming that a hypothetical editor's name was "Magical Editor"
9  * and its editor.js API implementation lived at Drupal.editors.magical, this
10  * JavaScript would use:
11  *  - Drupal.editors.magical.attachInlineEditor()
12  */
13
14 (function ($, Drupal, drupalSettings, _) {
15
16   'use strict';
17
18   Drupal.quickedit.editors.editor = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.editor# */{
19
20     /**
21      * The text format for this field.
22      *
23      * @type {string}
24      */
25     textFormat: null,
26
27     /**
28      * Indicates whether this text format has transformations.
29      *
30      * @type {bool}
31      */
32     textFormatHasTransformations: null,
33
34     /**
35      * Stores a reference to the text editor object for this field.
36      *
37      * @type {Drupal.quickedit.EditorModel}
38      */
39     textEditor: null,
40
41     /**
42      * Stores the textual DOM element that is being in-place edited.
43      *
44      * @type {jQuery}
45      */
46     $textElement: null,
47
48     /**
49      * @constructs
50      *
51      * @augments Drupal.quickedit.EditorView
52      *
53      * @param {object} options
54      *   Options for the editor view.
55      */
56     initialize: function (options) {
57       Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
58
59       var metadata = Drupal.quickedit.metadata.get(this.fieldModel.get('fieldID'), 'custom');
60       this.textFormat = drupalSettings.editor.formats[metadata.format];
61       this.textFormatHasTransformations = metadata.formatHasTransformations;
62       this.textEditor = Drupal.editors[this.textFormat.editor];
63
64       // Store the actual value of this field. We'll need this to restore the
65       // original value when the user discards his modifications.
66       var $fieldItems = this.$el.find('.quickedit-field');
67       if ($fieldItems.length) {
68         this.$textElement = $fieldItems.eq(0);
69       }
70       else {
71         this.$textElement = this.$el;
72       }
73       this.model.set('originalValue', this.$textElement.html());
74     },
75
76     /**
77      * @inheritdoc
78      *
79      * @return {jQuery}
80      *   The text element edited.
81      */
82     getEditedElement: function () {
83       return this.$textElement;
84     },
85
86     /**
87      * @inheritdoc
88      *
89      * @param {object} fieldModel
90      *   The field model.
91      * @param {string} state
92      *   The current state.
93      */
94     stateChange: function (fieldModel, state) {
95       var editorModel = this.model;
96       var from = fieldModel.previous('state');
97       var to = state;
98       switch (to) {
99         case 'inactive':
100           break;
101
102         case 'candidate':
103           // Detach the text editor when entering the 'candidate' state from one
104           // of the states where it could have been attached.
105           if (from !== 'inactive' && from !== 'highlighted') {
106             this.textEditor.detach(this.$textElement.get(0), this.textFormat);
107           }
108           // A field model's editor view revert() method is invoked when an
109           // 'active' field becomes a 'candidate' field. But, in the case of
110           // this in-place editor, the content will have been *replaced* if the
111           // text format has transformation filters. Therefore, if we stop
112           // in-place editing this entity, revert explicitly.
113           if (from === 'active' && this.textFormatHasTransformations) {
114             this.revert();
115           }
116           if (from === 'invalid') {
117             this.removeValidationErrors();
118           }
119           break;
120
121         case 'highlighted':
122           break;
123
124         case 'activating':
125           // When transformation filters have been applied to the formatted text
126           // of this field, then we'll need to load a re-formatted version of it
127           // without the transformation filters.
128           if (this.textFormatHasTransformations) {
129             var $textElement = this.$textElement;
130             this._getUntransformedText(function (untransformedText) {
131               $textElement.html(untransformedText);
132               fieldModel.set('state', 'active');
133             });
134           }
135           // When no transformation filters have been applied: start WYSIWYG
136           // editing immediately!
137           else {
138             // Defer updating the model until the current state change has
139             // propagated, to not trigger a nested state change event.
140             _.defer(function () {
141               fieldModel.set('state', 'active');
142             });
143           }
144           break;
145
146         case 'active':
147           var textElement = this.$textElement.get(0);
148           var toolbarView = fieldModel.toolbarView;
149           this.textEditor.attachInlineEditor(
150             textElement,
151             this.textFormat,
152             toolbarView.getMainWysiwygToolgroupId(),
153             toolbarView.getFloatedWysiwygToolgroupId()
154           );
155           // Set the state to 'changed' whenever the content has changed.
156           this.textEditor.onChange(textElement, function (htmlText) {
157             editorModel.set('currentValue', htmlText);
158             fieldModel.set('state', 'changed');
159           });
160           break;
161
162         case 'changed':
163           break;
164
165         case 'saving':
166           if (from === 'invalid') {
167             this.removeValidationErrors();
168           }
169           this.save();
170           break;
171
172         case 'saved':
173           break;
174
175         case 'invalid':
176           this.showValidationErrors();
177           break;
178       }
179     },
180
181     /**
182      * @inheritdoc
183      *
184      * @return {object}
185      *   The sttings for the quick edit UI.
186      */
187     getQuickEditUISettings: function () {
188       return {padding: true, unifiedToolbar: true, fullWidthToolbar: true, popup: false};
189     },
190
191     /**
192      * @inheritdoc
193      */
194     revert: function () {
195       this.$textElement.html(this.model.get('originalValue'));
196     },
197
198     /**
199      * Loads untransformed text for this field.
200      *
201      * More accurately: it re-filters formatted text to exclude transformation
202      * filters used by the text format.
203      *
204      * @param {function} callback
205      *   A callback function that will receive the untransformed text.
206      *
207      * @see \Drupal\editor\Ajax\GetUntransformedTextCommand
208      */
209     _getUntransformedText: function (callback) {
210       var fieldID = this.fieldModel.get('fieldID');
211
212       // Create a Drupal.ajax instance to load the form.
213       var textLoaderAjax = Drupal.ajax({
214         url: Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('editor/!entity_type/!id/!field_name/!langcode/!view_mode')),
215         submit: {nocssjs: true}
216       });
217
218       // Implement a scoped editorGetUntransformedText AJAX command: calls the
219       // callback.
220       textLoaderAjax.commands.editorGetUntransformedText = function (ajax, response, status) {
221         callback(response.data);
222       };
223
224       // This will ensure our scoped editorGetUntransformedText AJAX command
225       // gets called.
226       textLoaderAjax.execute();
227     }
228
229   });
230
231 })(jQuery, Drupal, drupalSettings, _);