Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / filter / filter.filter_html.admin.es6.js
1 /**
2  * @file
3  * Attaches behavior for updating filter_html's settings automatically.
4  */
5
6 (function($, Drupal, _, document) {
7   if (Drupal.filterConfiguration) {
8     /**
9      * Implement a live setting parser to prevent text editors from automatically
10      * enabling buttons that are not allowed by this filter's configuration.
11      *
12      * @namespace
13      */
14     Drupal.filterConfiguration.liveSettingParsers.filter_html = {
15       /**
16        * @return {Array}
17        *   An array of filter rules.
18        */
19       getRules() {
20         const currentValue = $(
21           '#edit-filters-filter-html-settings-allowed-html',
22         ).val();
23         const rules = Drupal.behaviors.filterFilterHtmlUpdating._parseSetting(
24           currentValue,
25         );
26
27         // Build a FilterHTMLRule that reflects the hard-coded behavior that
28         // strips all "style" attribute and all "on*" attributes.
29         const rule = new Drupal.FilterHTMLRule();
30         rule.restrictedTags.tags = ['*'];
31         rule.restrictedTags.forbidden.attributes = ['style', 'on*'];
32         rules.push(rule);
33
34         return rules;
35       },
36     };
37   }
38
39   /**
40    * Displays and updates what HTML tags are allowed to use in a filter.
41    *
42    * @type {Drupal~behavior}
43    *
44    * @todo Remove everything but 'attach' and 'detach' and make a proper object.
45    *
46    * @prop {Drupal~behaviorAttach} attach
47    *   Attaches behavior for updating allowed HTML tags.
48    */
49   Drupal.behaviors.filterFilterHtmlUpdating = {
50     // The form item contains the "Allowed HTML tags" setting.
51     $allowedHTMLFormItem: null,
52
53     // The description for the "Allowed HTML tags" field.
54     $allowedHTMLDescription: null,
55
56     /**
57      * The parsed, user-entered tag list of $allowedHTMLFormItem
58      *
59      * @var {Object.<string, Drupal.FilterHTMLRule>}
60      */
61     userTags: {},
62
63     // The auto-created tag list thus far added.
64     autoTags: null,
65
66     // Track which new features have been added to the text editor.
67     newFeatures: {},
68
69     attach(context, settings) {
70       const that = this;
71       $(context)
72         .find('[name="filters[filter_html][settings][allowed_html]"]')
73         .once('filter-filter_html-updating')
74         .each(function() {
75           that.$allowedHTMLFormItem = $(this);
76           that.$allowedHTMLDescription = that.$allowedHTMLFormItem
77             .closest('.js-form-item')
78             .find('.description');
79           that.userTags = that._parseSetting(this.value);
80
81           // Update the new allowed tags based on added text editor features.
82           $(document)
83             .on('drupalEditorFeatureAdded', (e, feature) => {
84               that.newFeatures[feature.name] = feature.rules;
85               that._updateAllowedTags();
86             })
87             .on('drupalEditorFeatureModified', (e, feature) => {
88               if (that.newFeatures.hasOwnProperty(feature.name)) {
89                 that.newFeatures[feature.name] = feature.rules;
90                 that._updateAllowedTags();
91               }
92             })
93             .on('drupalEditorFeatureRemoved', (e, feature) => {
94               if (that.newFeatures.hasOwnProperty(feature.name)) {
95                 delete that.newFeatures[feature.name];
96                 that._updateAllowedTags();
97               }
98             });
99
100           // When the allowed tags list is manually changed, update userTags.
101           that.$allowedHTMLFormItem.on('change.updateUserTags', function() {
102             that.userTags = _.difference(
103               that._parseSetting(this.value),
104               that.autoTags,
105             );
106           });
107         });
108     },
109
110     /**
111      * Updates the "Allowed HTML tags" setting and shows an informative message.
112      */
113     _updateAllowedTags() {
114       // Update the list of auto-created tags.
115       this.autoTags = this._calculateAutoAllowedTags(
116         this.userTags,
117         this.newFeatures,
118       );
119
120       // Remove any previous auto-created tag message.
121       this.$allowedHTMLDescription.find('.editor-update-message').remove();
122
123       // If any auto-created tags: insert message and update form item.
124       if (!_.isEmpty(this.autoTags)) {
125         this.$allowedHTMLDescription.append(
126           Drupal.theme('filterFilterHTMLUpdateMessage', this.autoTags),
127         );
128         const userTagsWithoutOverrides = _.omit(
129           this.userTags,
130           _.keys(this.autoTags),
131         );
132         this.$allowedHTMLFormItem.val(
133           `${this._generateSetting(
134             userTagsWithoutOverrides,
135           )} ${this._generateSetting(this.autoTags)}`,
136         );
137       }
138       // Restore to original state.
139       else {
140         this.$allowedHTMLFormItem.val(this._generateSetting(this.userTags));
141       }
142     },
143
144     /**
145      * Calculates which HTML tags the added text editor buttons need to work.
146      *
147      * The filter_html filter is only concerned with the required tags, not with
148      * any properties, nor with each feature's "allowed" tags.
149      *
150      * @param {Array} userAllowedTags
151      *   The list of user-defined allowed tags.
152      * @param {object} newFeatures
153      *   A list of {@link Drupal.EditorFeature} objects' rules, keyed by
154      *   their name.
155      *
156      * @return {Array}
157      *   A list of new allowed tags.
158      */
159     _calculateAutoAllowedTags(userAllowedTags, newFeatures) {
160       const editorRequiredTags = {};
161
162       // Map the newly added Text Editor features to Drupal.FilterHtmlRule
163       // objects (to allow comparing userTags with autoTags).
164       Object.keys(newFeatures || {}).forEach(featureName => {
165         const feature = newFeatures[featureName];
166         let featureRule;
167         let filterRule;
168         let tag;
169
170         for (let f = 0; f < feature.length; f++) {
171           featureRule = feature[f];
172           for (let t = 0; t < featureRule.required.tags.length; t++) {
173             tag = featureRule.required.tags[t];
174             if (!_.has(editorRequiredTags, tag)) {
175               filterRule = new Drupal.FilterHTMLRule();
176               filterRule.restrictedTags.tags = [tag];
177               // @todo Neither Drupal.FilterHtmlRule nor
178               //   Drupal.EditorFeatureHTMLRule allow for generic attribute
179               //   value restrictions, only for the "class" and "style"
180               //   attribute's values to be restricted. The filter_html filter
181               //   always disallows the "style" attribute, so we only need to
182               //   support "class" attribute value restrictions. Fix once
183               //   https://www.drupal.org/node/2567801 lands.
184               filterRule.restrictedTags.allowed.attributes = featureRule.required.attributes.slice(
185                 0,
186               );
187               filterRule.restrictedTags.allowed.classes = featureRule.required.classes.slice(
188                 0,
189               );
190               editorRequiredTags[tag] = filterRule;
191             }
192             // The tag is already allowed, add any additionally allowed
193             // attributes.
194             else {
195               filterRule = editorRequiredTags[tag];
196               filterRule.restrictedTags.allowed.attributes = _.union(
197                 filterRule.restrictedTags.allowed.attributes,
198                 featureRule.required.attributes,
199               );
200               filterRule.restrictedTags.allowed.classes = _.union(
201                 filterRule.restrictedTags.allowed.classes,
202                 featureRule.required.classes,
203               );
204             }
205           }
206         }
207       });
208
209       // Now compare userAllowedTags with editorRequiredTags, and build
210       // autoAllowedTags, which contains:
211       // - any tags in editorRequiredTags but not in userAllowedTags (i.e. tags
212       //   that are additionally going to be allowed)
213       // - any tags in editorRequiredTags that already exists in userAllowedTags
214       //   but does not allow all attributes or attribute values
215       const autoAllowedTags = {};
216       Object.keys(editorRequiredTags).forEach(tag => {
217         // If userAllowedTags does not contain a rule for this editor-required
218         // tag, then add it to the list of automatically allowed tags.
219         if (!_.has(userAllowedTags, tag)) {
220           autoAllowedTags[tag] = editorRequiredTags[tag];
221         }
222         // Otherwise, if userAllowedTags already allows this tag, then check if
223         // additional attributes and classes on this tag are required by the
224         // editor.
225         else {
226           const requiredAttributes =
227             editorRequiredTags[tag].restrictedTags.allowed.attributes;
228           const allowedAttributes =
229             userAllowedTags[tag].restrictedTags.allowed.attributes;
230           const needsAdditionalAttributes =
231             requiredAttributes.length &&
232             _.difference(requiredAttributes, allowedAttributes).length;
233           const requiredClasses =
234             editorRequiredTags[tag].restrictedTags.allowed.classes;
235           const allowedClasses =
236             userAllowedTags[tag].restrictedTags.allowed.classes;
237           const needsAdditionalClasses =
238             requiredClasses.length &&
239             _.difference(requiredClasses, allowedClasses).length;
240           if (needsAdditionalAttributes || needsAdditionalClasses) {
241             autoAllowedTags[tag] = userAllowedTags[tag].clone();
242           }
243           if (needsAdditionalAttributes) {
244             autoAllowedTags[tag].restrictedTags.allowed.attributes = _.union(
245               allowedAttributes,
246               requiredAttributes,
247             );
248           }
249           if (needsAdditionalClasses) {
250             autoAllowedTags[tag].restrictedTags.allowed.classes = _.union(
251               allowedClasses,
252               requiredClasses,
253             );
254           }
255         }
256       });
257
258       return autoAllowedTags;
259     },
260
261     /**
262      * Parses the value of this.$allowedHTMLFormItem.
263      *
264      * @param {string} setting
265      *   The string representation of the setting. For example:
266      *     <p class="callout"> <br> <a href hreflang>
267      *
268      * @return {Object.<string, Drupal.FilterHTMLRule>}
269      *   The corresponding text filter HTML rule objects, one per tag, keyed by
270      *   tag name.
271      */
272     _parseSetting(setting) {
273       let node;
274       let tag;
275       let rule;
276       let attributes;
277       let attribute;
278       const allowedTags = setting.match(/(<[^>]+>)/g);
279       const sandbox = document.createElement('div');
280       const rules = {};
281       for (let t = 0; t < allowedTags.length; t++) {
282         // Let the browser do the parsing work for us.
283         sandbox.innerHTML = allowedTags[t];
284         node = sandbox.firstChild;
285         tag = node.tagName.toLowerCase();
286
287         // Build the Drupal.FilterHtmlRule object.
288         rule = new Drupal.FilterHTMLRule();
289         // We create one rule per allowed tag, so always one tag.
290         rule.restrictedTags.tags = [tag];
291         // Add the attribute restrictions.
292         attributes = node.attributes;
293         for (let i = 0; i < attributes.length; i++) {
294           attribute = attributes.item(i);
295           const attributeName = attribute.nodeName;
296           // @todo Drupal.FilterHtmlRule does not allow for generic attribute
297           //   value restrictions, only for the "class" and "style" attribute's
298           //   values. The filter_html filter always disallows the "style"
299           //   attribute, so we only need to support "class" attribute value
300           //   restrictions. Fix once https://www.drupal.org/node/2567801 lands.
301           if (attributeName === 'class') {
302             const attributeValue = attribute.textContent;
303             rule.restrictedTags.allowed.classes = attributeValue.split(' ');
304           } else {
305             rule.restrictedTags.allowed.attributes.push(attributeName);
306           }
307         }
308
309         rules[tag] = rule;
310       }
311       return rules;
312     },
313
314     /**
315      * Generates the value of this.$allowedHTMLFormItem.
316      *
317      * @param {Object.<string, Drupal.FilterHTMLRule>} tags
318      *   The parsed representation of the setting.
319      *
320      * @return {Array}
321      *   The string representation of the setting. e.g. "<p> <br> <a>"
322      */
323     _generateSetting(tags) {
324       return _.reduce(
325         tags,
326         (setting, rule, tag) => {
327           if (setting.length) {
328             setting += ' ';
329           }
330
331           setting += `<${tag}`;
332           if (rule.restrictedTags.allowed.attributes.length) {
333             setting += ` ${rule.restrictedTags.allowed.attributes.join(' ')}`;
334           }
335           // @todo Drupal.FilterHtmlRule does not allow for generic attribute
336           //   value restrictions, only for the "class" and "style" attribute's
337           //   values. The filter_html filter always disallows the "style"
338           //   attribute, so we only need to support "class" attribute value
339           //   restrictions. Fix once https://www.drupal.org/node/2567801 lands.
340           if (rule.restrictedTags.allowed.classes.length) {
341             setting += ` class="${rule.restrictedTags.allowed.classes.join(
342               ' ',
343             )}"`;
344           }
345
346           setting += '>';
347           return setting;
348         },
349         '',
350       );
351     },
352   };
353
354   /**
355    * Theme function for the filter_html update message.
356    *
357    * @param {Array} tags
358    *   An array of the new tags that are to be allowed.
359    *
360    * @return {string}
361    *   The corresponding HTML.
362    */
363   Drupal.theme.filterFilterHTMLUpdateMessage = function(tags) {
364     let html = '';
365     const tagList = Drupal.behaviors.filterFilterHtmlUpdating._generateSetting(
366       tags,
367     );
368     html += '<p class="editor-update-message">';
369     html += Drupal.t(
370       'Based on the text editor configuration, these tags have automatically been added: <strong>@tag-list</strong>.',
371       { '@tag-list': tagList },
372     );
373     html += '</p>';
374     return html;
375   };
376 })(jQuery, Drupal, _, document);