Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / editor / js / editor.admin.es6.js
1 /**
2  * @file
3  * Provides a JavaScript API to broadcast text editor configuration changes.
4  *
5  * Filter implementations may listen to the drupalEditorFeatureAdded,
6  * drupalEditorFeatureRemoved, and drupalEditorFeatureRemoved events on document
7  * to automatically adjust their settings based on the editor configuration.
8  */
9
10 (function($, _, Drupal, document) {
11   /**
12    * Editor configuration namespace.
13    *
14    * @namespace
15    */
16   Drupal.editorConfiguration = {
17     /**
18      * Must be called by a specific text editor's configuration whenever a
19      * feature is added by the user.
20      *
21      * Triggers the drupalEditorFeatureAdded event on the document, which
22      * receives a {@link Drupal.EditorFeature} object.
23      *
24      * @param {Drupal.EditorFeature} feature
25      *   A text editor feature object.
26      *
27      * @fires event:drupalEditorFeatureAdded
28      */
29     addedFeature(feature) {
30       $(document).trigger('drupalEditorFeatureAdded', feature);
31     },
32
33     /**
34      * Must be called by a specific text editor's configuration whenever a
35      * feature is removed by the user.
36      *
37      * Triggers the drupalEditorFeatureRemoved event on the document, which
38      * receives a {@link Drupal.EditorFeature} object.
39      *
40      * @param {Drupal.EditorFeature} feature
41      *   A text editor feature object.
42      *
43      * @fires event:drupalEditorFeatureRemoved
44      */
45     removedFeature(feature) {
46       $(document).trigger('drupalEditorFeatureRemoved', feature);
47     },
48
49     /**
50      * Must be called by a specific text editor's configuration whenever a
51      * feature is modified, i.e. has different rules.
52      *
53      * For example when the "Bold" button is configured to use the `<b>` tag
54      * instead of the `<strong>` tag.
55      *
56      * Triggers the drupalEditorFeatureModified event on the document, which
57      * receives a {@link Drupal.EditorFeature} object.
58      *
59      * @param {Drupal.EditorFeature} feature
60      *   A text editor feature object.
61      *
62      * @fires event:drupalEditorFeatureModified
63      */
64     modifiedFeature(feature) {
65       $(document).trigger('drupalEditorFeatureModified', feature);
66     },
67
68     /**
69      * May be called by a specific text editor's configuration whenever a
70      * feature is being added, to check whether it would require the filter
71      * settings to be updated.
72      *
73      * The canonical use case is when a text editor is being enabled:
74      * preferably
75      * this would not cause the filter settings to be changed; rather, the
76      * default set of buttons (features) for the text editor should adjust
77      * itself to not cause filter setting changes.
78      *
79      * Note: for filters to integrate with this functionality, it is necessary
80      * that they implement
81      * `Drupal.filterSettingsForEditors[filterID].getRules()`.
82      *
83      * @param {Drupal.EditorFeature} feature
84      *   A text editor feature object.
85      *
86      * @return {bool}
87      *   Whether the given feature is allowed by the current filters.
88      */
89     featureIsAllowedByFilters(feature) {
90       /**
91        * Provided a section of a feature or filter rule, checks if no property
92        * values are defined for all properties: attributes, classes and styles.
93        *
94        * @param {object} section
95        *   The section to check.
96        *
97        * @return {bool}
98        *   Returns true if the section has empty properties, false otherwise.
99        */
100       function emptyProperties(section) {
101         return (
102           section.attributes.length === 0 &&
103           section.classes.length === 0 &&
104           section.styles.length === 0
105         );
106       }
107
108       /**
109        * Generate the universe U of possible values that can result from the
110        * feature's rules' requirements.
111        *
112        * This generates an object of this form:
113        *   var universe = {
114        *     a: {
115        *       'touchedByAllowedPropertyRule': false,
116        *       'tag': false,
117        *       'attributes:href': false,
118        *       'classes:external': false,
119        *     },
120        *     strong: {
121        *       'touchedByAllowedPropertyRule': false,
122        *       'tag': false,
123        *     },
124        *     img: {
125        *       'touchedByAllowedPropertyRule': false,
126        *       'tag': false,
127        *       'attributes:src': false
128        *     }
129        *   };
130        *
131        * In this example, the given text editor feature resulted in the above
132        * universe, which shows that it must be allowed to generate the a,
133        * strong and img tags. For the a tag, it must be able to set the "href"
134        * attribute and the "external" class. For the strong tag, no further
135        * properties are required. For the img tag, the "src" attribute is
136        * required. The "tag" key is used to track whether that tag was
137        * explicitly allowed by one of the filter's rules. The
138        * "touchedByAllowedPropertyRule" key is used for state tracking that is
139        * essential for filterStatusAllowsFeature() to be able to reason: when
140        * all of a filter's rules have been applied, and none of the forbidden
141        * rules matched (which would have resulted in early termination) yet the
142        * universe has not been made empty (which would be the end result if
143        * everything in the universe were explicitly allowed), then this piece
144        * of state data enables us to determine whether a tag whose properties
145        * were not all explicitly allowed are in fact still allowed, because its
146        * tag was explicitly allowed and there were no filter rules applying
147        * "allowed tag property value" restrictions for this particular tag.
148        *
149        * @param {object} feature
150        *   The feature in question.
151        *
152        * @return {object}
153        *   The universe generated.
154        *
155        * @see findPropertyValueOnTag()
156        * @see filterStatusAllowsFeature()
157        */
158       function generateUniverseFromFeatureRequirements(feature) {
159         const properties = ['attributes', 'styles', 'classes'];
160         const universe = {};
161
162         for (let r = 0; r < feature.rules.length; r++) {
163           const featureRule = feature.rules[r];
164
165           // For each tag required by this feature rule, create a basic entry in
166           // the universe.
167           const requiredTags = featureRule.required.tags;
168           for (let t = 0; t < requiredTags.length; t++) {
169             universe[requiredTags[t]] = {
170               // Whether this tag was allowed or not.
171               tag: false,
172               // Whether any filter rule that applies to this tag had an allowed
173               // property rule. i.e. will become true if >=1 filter rule has >=1
174               // allowed property rule.
175               touchedByAllowedPropertyRule: false,
176               // Analogous, but for forbidden property rule.
177               touchedBytouchedByForbiddenPropertyRule: false,
178             };
179           }
180
181           // If no required properties are defined for this rule, we can move on
182           // to the next feature.
183           if (emptyProperties(featureRule.required)) {
184             continue;
185           }
186
187           // Expand the existing universe, assume that each tags' property
188           // value is disallowed. If the filter rules allow everything in the
189           // feature's universe, then the feature is allowed.
190           for (let p = 0; p < properties.length; p++) {
191             const property = properties[p];
192             for (let pv = 0; pv < featureRule.required[property].length; pv++) {
193               const propertyValue = featureRule.required[property];
194               universe[requiredTags][`${property}:${propertyValue}`] = false;
195             }
196           }
197         }
198
199         return universe;
200       }
201
202       /**
203        * Finds out if a specific property value (potentially containing
204        * wildcards) exists on the given tag. When the "allowing" parameter
205        * equals true, the universe will be updated if that specific property
206        * value exists. Returns true if found, false otherwise.
207        *
208        * @param {object} universe
209        *   The universe to check.
210        * @param {string} tag
211        *   The tag to look for.
212        * @param {string} property
213        *   The property to check.
214        * @param {string} propertyValue
215        *   The property value to check.
216        * @param {bool} allowing
217        *   Whether to update the universe or not.
218        *
219        * @return {bool}
220        *   Returns true if found, false otherwise.
221        */
222       function findPropertyValueOnTag(
223         universe,
224         tag,
225         property,
226         propertyValue,
227         allowing,
228       ) {
229         // If the tag does not exist in the universe, then it definitely can't
230         // have this specific property value.
231         if (!_.has(universe, tag)) {
232           return false;
233         }
234
235         const key = `${property}:${propertyValue}`;
236
237         // Track whether a tag was touched by a filter rule that allows specific
238         // property values on this particular tag.
239         // @see generateUniverseFromFeatureRequirements
240         if (allowing) {
241           universe[tag].touchedByAllowedPropertyRule = true;
242         }
243
244         // The simple case: no wildcard in property value.
245         if (_.indexOf(propertyValue, '*') === -1) {
246           if (_.has(universe, tag) && _.has(universe[tag], key)) {
247             if (allowing) {
248               universe[tag][key] = true;
249             }
250             return true;
251           }
252           return false;
253         }
254         // The complex case: wildcard in property value.
255
256         let atLeastOneFound = false;
257         const regex = key.replace(/\*/g, '[^ ]*');
258         _.each(_.keys(universe[tag]), key => {
259           if (key.match(regex)) {
260             atLeastOneFound = true;
261             if (allowing) {
262               universe[tag][key] = true;
263             }
264           }
265         });
266         return atLeastOneFound;
267       }
268
269       /**
270        * Calls findPropertyValuesOnAllTags for all tags in the universe.
271        *
272        * @param {object} universe
273        *   The universe to check.
274        * @param {string} property
275        *   The property to check.
276        * @param {Array} propertyValues
277        *   Values of the property to check.
278        * @param {bool} allowing
279        *   Whether to update the universe or not.
280        *
281        * @return {bool}
282        *   Returns true if found, false otherwise.
283        */
284       function findPropertyValuesOnAllTags(
285         universe,
286         property,
287         propertyValues,
288         allowing,
289       ) {
290         let atLeastOneFound = false;
291         _.each(_.keys(universe), tag => {
292           if (
293             // eslint-disable-next-line no-use-before-define
294             findPropertyValuesOnTag(
295               universe,
296               tag,
297               property,
298               propertyValues,
299               allowing,
300             )
301           ) {
302             atLeastOneFound = true;
303           }
304         });
305         return atLeastOneFound;
306       }
307
308       /**
309        * Calls findPropertyValueOnTag on the given tag for every property value
310        * that is listed in the "propertyValues" parameter. Supports the wildcard
311        * tag.
312        *
313        * @param {object} universe
314        *   The universe to check.
315        * @param {string} tag
316        *   The tag to look for.
317        * @param {string} property
318        *   The property to check.
319        * @param {Array} propertyValues
320        *   Values of the property to check.
321        * @param {bool} allowing
322        *   Whether to update the universe or not.
323        *
324        * @return {bool}
325        *   Returns true if found, false otherwise.
326        */
327       function findPropertyValuesOnTag(
328         universe,
329         tag,
330         property,
331         propertyValues,
332         allowing,
333       ) {
334         // Detect the wildcard case.
335         if (tag === '*') {
336           return findPropertyValuesOnAllTags(
337             universe,
338             property,
339             propertyValues,
340             allowing,
341           );
342         }
343
344         let atLeastOneFound = false;
345         _.each(propertyValues, propertyValue => {
346           if (
347             findPropertyValueOnTag(
348               universe,
349               tag,
350               property,
351               propertyValue,
352               allowing,
353             )
354           ) {
355             atLeastOneFound = true;
356           }
357         });
358         return atLeastOneFound;
359       }
360
361       /**
362        * Calls deleteFromUniverseIfAllowed for all tags in the universe.
363        *
364        * @param {object} universe
365        *   The universe to delete from.
366        *
367        * @return {bool}
368        *   Whether something was deleted from the universe.
369        */
370       function deleteAllTagsFromUniverseIfAllowed(universe) {
371         let atLeastOneDeleted = false;
372         _.each(_.keys(universe), tag => {
373           // eslint-disable-next-line no-use-before-define
374           if (deleteFromUniverseIfAllowed(universe, tag)) {
375             atLeastOneDeleted = true;
376           }
377         });
378         return atLeastOneDeleted;
379       }
380
381       /**
382        * Deletes a tag from the universe if the tag itself and each of its
383        * properties are marked as allowed.
384        *
385        * @param {object} universe
386        *   The universe to delete from.
387        * @param {string} tag
388        *   The tag to check.
389        *
390        * @return {bool}
391        *   Whether something was deleted from the universe.
392        */
393       function deleteFromUniverseIfAllowed(universe, tag) {
394         // Detect the wildcard case.
395         if (tag === '*') {
396           return deleteAllTagsFromUniverseIfAllowed(universe);
397         }
398         if (
399           _.has(universe, tag) &&
400           _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))
401         ) {
402           delete universe[tag];
403           return true;
404         }
405         return false;
406       }
407
408       /**
409        * Checks if any filter rule forbids either a tag or a tag property value
410        * that exists in the universe.
411        *
412        * @param {object} universe
413        *   Universe to check.
414        * @param {object} filterStatus
415        *   Filter status to use for check.
416        *
417        * @return {bool}
418        *   Whether any filter rule forbids something in the universe.
419        */
420       function anyForbiddenFilterRuleMatches(universe, filterStatus) {
421         const properties = ['attributes', 'styles', 'classes'];
422
423         // Check if a tag in the universe is forbidden.
424         const allRequiredTags = _.keys(universe);
425         let filterRule;
426         for (let i = 0; i < filterStatus.rules.length; i++) {
427           filterRule = filterStatus.rules[i];
428           if (filterRule.allow === false) {
429             if (_.intersection(allRequiredTags, filterRule.tags).length > 0) {
430               return true;
431             }
432           }
433         }
434
435         // Check if a property value of a tag in the universe is forbidden.
436         // For all filter rules…
437         for (let n = 0; n < filterStatus.rules.length; n++) {
438           filterRule = filterStatus.rules[n];
439           // â€¦ if there are tags with restricted property values â€¦
440           if (
441             filterRule.restrictedTags.tags.length &&
442             !emptyProperties(filterRule.restrictedTags.forbidden)
443           ) {
444             // â€¦ for all those tags â€¦
445             for (let j = 0; j < filterRule.restrictedTags.tags.length; j++) {
446               const tag = filterRule.restrictedTags.tags[j];
447               // â€¦ then iterate over all properties â€¦
448               for (let k = 0; k < properties.length; k++) {
449                 const property = properties[k];
450                 // â€¦ and return true if just one of the forbidden property
451                 // values for this tag and property is listed in the universe.
452                 if (
453                   findPropertyValuesOnTag(
454                     universe,
455                     tag,
456                     property,
457                     filterRule.restrictedTags.forbidden[property],
458                     false,
459                   )
460                 ) {
461                   return true;
462                 }
463               }
464             }
465           }
466         }
467
468         return false;
469       }
470
471       /**
472        * Applies every filter rule's explicit allowing of a tag or a tag
473        * property value to the universe. Whenever both the tag and all of its
474        * required property values are marked as explicitly allowed, they are
475        * deleted from the universe.
476        *
477        * @param {object} universe
478        *   Universe to delete from.
479        * @param {object} filterStatus
480        *   The filter status in question.
481        */
482       function markAllowedTagsAndPropertyValues(universe, filterStatus) {
483         const properties = ['attributes', 'styles', 'classes'];
484
485         // Check if a tag in the universe is allowed.
486         let filterRule;
487         let tag;
488         for (
489           let l = 0;
490           !_.isEmpty(universe) && l < filterStatus.rules.length;
491           l++
492         ) {
493           filterRule = filterStatus.rules[l];
494           if (filterRule.allow === true) {
495             for (
496               let m = 0;
497               !_.isEmpty(universe) && m < filterRule.tags.length;
498               m++
499             ) {
500               tag = filterRule.tags[m];
501               if (_.has(universe, tag)) {
502                 universe[tag].tag = true;
503                 deleteFromUniverseIfAllowed(universe, tag);
504               }
505             }
506           }
507         }
508
509         // Check if a property value of a tag in the universe is allowed.
510         // For all filter rules…
511         for (
512           let i = 0;
513           !_.isEmpty(universe) && i < filterStatus.rules.length;
514           i++
515         ) {
516           filterRule = filterStatus.rules[i];
517           // â€¦ if there are tags with restricted property values â€¦
518           if (
519             filterRule.restrictedTags.tags.length &&
520             !emptyProperties(filterRule.restrictedTags.allowed)
521           ) {
522             // â€¦ for all those tags â€¦
523             for (
524               let j = 0;
525               !_.isEmpty(universe) && j < filterRule.restrictedTags.tags.length;
526               j++
527             ) {
528               tag = filterRule.restrictedTags.tags[j];
529               // â€¦ then iterate over all properties â€¦
530               for (let k = 0; k < properties.length; k++) {
531                 const property = properties[k];
532                 // â€¦ and try to delete this tag from the universe if just one
533                 // of the allowed property values for this tag and property is
534                 // listed in the universe. (Because everything might be allowed
535                 // now.)
536                 if (
537                   findPropertyValuesOnTag(
538                     universe,
539                     tag,
540                     property,
541                     filterRule.restrictedTags.allowed[property],
542                     true,
543                   )
544                 ) {
545                   deleteFromUniverseIfAllowed(universe, tag);
546                 }
547               }
548             }
549           }
550         }
551       }
552
553       /**
554        * Checks whether the current status of a filter allows a specific feature
555        * by building the universe of potential values from the feature's
556        * requirements and then checking whether anything in the filter prevents
557        * that.
558        *
559        * @param {object} filterStatus
560        *   The filter status in question.
561        * @param {object} feature
562        *   The feature requested.
563        *
564        * @return {bool}
565        *   Whether the current status of the filter allows specified feature.
566        *
567        * @see generateUniverseFromFeatureRequirements()
568        */
569       function filterStatusAllowsFeature(filterStatus, feature) {
570         // An inactive filter by definition allows the feature.
571         if (!filterStatus.active) {
572           return true;
573         }
574
575         // A feature that specifies no rules has no HTML requirements and is
576         // hence allowed by definition.
577         if (feature.rules.length === 0) {
578           return true;
579         }
580
581         // Analogously for a filter that specifies no rules.
582         if (filterStatus.rules.length === 0) {
583           return true;
584         }
585
586         // Generate the universe U of possible values that can result from the
587         // feature's rules' requirements.
588         const universe = generateUniverseFromFeatureRequirements(feature);
589
590         // If anything that is in the universe (and is thus required by the
591         // feature) is forbidden by any of the filter's rules, then this filter
592         // does not allow this feature.
593         if (anyForbiddenFilterRuleMatches(universe, filterStatus)) {
594           return false;
595         }
596
597         // Mark anything in the universe that is allowed by any of the filter's
598         // rules as allowed. If everything is explicitly allowed, then the
599         // universe will become empty.
600         markAllowedTagsAndPropertyValues(universe, filterStatus);
601
602         // If there was at least one filter rule allowing tags, then everything
603         // in the universe must be allowed for this feature to be allowed, and
604         // thus by now it must be empty. However, it is still possible that the
605         // filter allows the feature, due to no rules for allowing tag property
606         // values and/or rules for forbidding tag property values. For details:
607         // see the comments below.
608         // @see generateUniverseFromFeatureRequirements()
609         if (_.some(_.pluck(filterStatus.rules, 'allow'))) {
610           // If the universe is empty, then everything was explicitly allowed
611           // and our job is done: this filter allows this feature!
612           if (_.isEmpty(universe)) {
613             return true;
614           }
615           // Otherwise, it is still possible that this feature is allowed.
616
617           // Every tag must be explicitly allowed if there are filter rules
618           // doing tag whitelisting.
619           if (!_.every(_.pluck(universe, 'tag'))) {
620             return false;
621           }
622           // Every tag was explicitly allowed, but since the universe is not
623           // empty, one or more tag properties are disallowed. However, if
624           // only blacklisting of tag properties was applied to these tags,
625           // and no whitelisting was ever applied, then it's still fine:
626           // since none of the tag properties were blacklisted, we got to
627           // this point, and since no whitelisting was applied, it doesn't
628           // matter that the properties: this could never have happened
629           // anyway. It's only this late that we can know this for certain.
630
631           const tags = _.keys(universe);
632           // Figure out if there was any rule applying whitelisting tag
633           // restrictions to each of the remaining tags.
634           for (let i = 0; i < tags.length; i++) {
635             const tag = tags[i];
636             if (_.has(universe, tag)) {
637               if (universe[tag].touchedByAllowedPropertyRule === false) {
638                 delete universe[tag];
639               }
640             }
641           }
642           return _.isEmpty(universe);
643         }
644         // Otherwise, if all filter rules were doing blacklisting, then the sole
645         // fact that we got to this point indicates that this filter allows for
646         // everything that is required for this feature.
647
648         return true;
649       }
650
651       // If any filter's current status forbids the editor feature, return
652       // false.
653       Drupal.filterConfiguration.update();
654       return Object.keys(Drupal.filterConfiguration.statuses).every(filterID =>
655         filterStatusAllowsFeature(
656           Drupal.filterConfiguration.statuses[filterID],
657           feature,
658         ),
659       );
660     },
661   };
662
663   /**
664    * Constructor for an editor feature HTML rule.
665    *
666    * Intended to be used in combination with {@link Drupal.EditorFeature}.
667    *
668    * A text editor feature rule object describes both:
669    *  - required HTML tags, attributes, styles and classes: without these, the
670    *    text editor feature is unable to function. It's possible that a
671    *  - allowed HTML tags, attributes, styles and classes: these are optional
672    *    in the strictest sense, but it is possible that the feature generates
673    *    them.
674    *
675    * The structure can be very clearly seen below: there's a "required" and an
676    * "allowed" key. For each of those, there are objects with the "tags",
677    * "attributes", "styles" and "classes" keys. For all these keys the values
678    * are initialized to the empty array. List each possible value as an array
679    * value. Besides the "required" and "allowed" keys, there's an optional
680    * "raw" key: it allows text editor implementations to optionally pass in
681    * their raw representation instead of the Drupal-defined representation for
682    * HTML rules.
683    *
684    * @example
685    * tags: ['<a>']
686    * attributes: ['href', 'alt']
687    * styles: ['color', 'text-decoration']
688    * classes: ['external', 'internal']
689    *
690    * @constructor
691    *
692    * @see Drupal.EditorFeature
693    */
694   Drupal.EditorFeatureHTMLRule = function() {
695     /**
696      *
697      * @type {Object}
698      *
699      * @prop {Array} tags
700      * @prop {Array} attributes
701      * @prop {Array} styles
702      * @prop {Array} classes
703      */
704     this.required = {
705       tags: [],
706       attributes: [],
707       styles: [],
708       classes: [],
709     };
710
711     /**
712      *
713      * @type {Object}
714      *
715      * @prop {Array} tags
716      * @prop {Array} attributes
717      * @prop {Array} styles
718      * @prop {Array} classes
719      */
720     this.allowed = {
721       tags: [],
722       attributes: [],
723       styles: [],
724       classes: [],
725     };
726
727     /**
728      *
729      * @type {null}
730      */
731     this.raw = null;
732   };
733
734   /**
735    * A text editor feature object. Initialized with the feature name.
736    *
737    * Contains a set of HTML rules ({@link Drupal.EditorFeatureHTMLRule} objects)
738    * that describe which HTML tags, attributes, styles and classes are required
739    * (i.e. essential for the feature to function at all) and which are allowed
740    * (i.e. the feature may generate this, but they're not essential).
741    *
742    * It is necessary to allow for multiple HTML rules per feature: with just
743    * one HTML rule per feature, there is not enough expressiveness to describe
744    * certain cases. For example: a "table" feature would probably require the
745    * `<table>` tag, and might allow e.g. the "summary" attribute on that tag.
746    * However, the table feature would also require the `<tr>` and `<td>` tags,
747    * but it doesn't make sense to allow for a "summary" attribute on these tags.
748    * Hence these would need to be split in two separate rules.
749    *
750    * HTML rules must be added with the `addHTMLRule()` method. A feature that
751    * has zero HTML rules does not create or modify HTML.
752    *
753    * @constructor
754    *
755    * @param {string} name
756    *   The name of the feature.
757    *
758    * @see Drupal.EditorFeatureHTMLRule
759    */
760   Drupal.EditorFeature = function(name) {
761     this.name = name;
762     this.rules = [];
763   };
764
765   /**
766    * Adds a HTML rule to the list of HTML rules for this feature.
767    *
768    * @param {Drupal.EditorFeatureHTMLRule} rule
769    *   A text editor feature HTML rule.
770    */
771   Drupal.EditorFeature.prototype.addHTMLRule = function(rule) {
772     this.rules.push(rule);
773   };
774
775   /**
776    * Text filter status object. Initialized with the filter ID.
777    *
778    * Indicates whether the text filter is currently active (enabled) or not.
779    *
780    * Contains a set of HTML rules ({@link Drupal.FilterHTMLRule} objects) that
781    * describe which HTML tags are allowed or forbidden. They can also describe
782    * for a set of tags (or all tags) which attributes, styles and classes are
783    * allowed and which are forbidden.
784    *
785    * It is necessary to allow for multiple HTML rules per feature, for
786    * analogous reasons as {@link Drupal.EditorFeature}.
787    *
788    * HTML rules must be added with the `addHTMLRule()` method. A filter that has
789    * zero HTML rules does not disallow any HTML.
790    *
791    * @constructor
792    *
793    * @param {string} name
794    *   The name of the feature.
795    *
796    * @see Drupal.FilterHTMLRule
797    */
798   Drupal.FilterStatus = function(name) {
799     /**
800      *
801      * @type {string}
802      */
803     this.name = name;
804
805     /**
806      *
807      * @type {bool}
808      */
809     this.active = false;
810
811     /**
812      *
813      * @type {Array.<Drupal.FilterHTMLRule>}
814      */
815     this.rules = [];
816   };
817
818   /**
819    * Adds a HTML rule to the list of HTML rules for this filter.
820    *
821    * @param {Drupal.FilterHTMLRule} rule
822    *   A text filter HTML rule.
823    */
824   Drupal.FilterStatus.prototype.addHTMLRule = function(rule) {
825     this.rules.push(rule);
826   };
827
828   /**
829    * A text filter HTML rule object.
830    *
831    * Intended to be used in combination with {@link Drupal.FilterStatus}.
832    *
833    * A text filter rule object describes:
834    *  1. allowed or forbidden tags: (optional) whitelist or blacklist HTML tags
835    *  2. restricted tag properties: (optional) whitelist or blacklist
836    *     attributes, styles and classes on a set of HTML tags.
837    *
838    * Typically, each text filter rule object does either 1 or 2, not both.
839    *
840    * The structure can be very clearly seen below:
841    *  1. use the "tags" key to list HTML tags, and set the "allow" key to
842    *     either true (to allow these HTML tags) or false (to forbid these HTML
843    *     tags). If you leave the "tags" key's default value (the empty array),
844    *     no restrictions are applied.
845    *  2. all nested within the "restrictedTags" key: use the "tags" subkey to
846    *     list HTML tags to which you want to apply property restrictions, then
847    *     use the "allowed" subkey to whitelist specific property values, and
848    *     similarly use the "forbidden" subkey to blacklist specific property
849    *     values.
850    *
851    * @example
852    * <caption>Whitelist the "p", "strong" and "a" HTML tags.</caption>
853    * {
854    *   tags: ['p', 'strong', 'a'],
855    *   allow: true,
856    *   restrictedTags: {
857    *     tags: [],
858    *     allowed: { attributes: [], styles: [], classes: [] },
859    *     forbidden: { attributes: [], styles: [], classes: [] }
860    *   }
861    * }
862    * @example
863    * <caption>For the "a" HTML tag, only allow the "href" attribute
864    * and the "external" class and disallow the "target" attribute.</caption>
865    * {
866    *   tags: [],
867    *   allow: null,
868    *   restrictedTags: {
869    *     tags: ['a'],
870    *     allowed: { attributes: ['href'], styles: [], classes: ['external'] },
871    *     forbidden: { attributes: ['target'], styles: [], classes: [] }
872    *   }
873    * }
874    * @example
875    * <caption>For all tags, allow the "data-*" attribute (that is, any
876    * attribute that begins with "data-").</caption>
877    * {
878    *   tags: [],
879    *   allow: null,
880    *   restrictedTags: {
881    *     tags: ['*'],
882    *     allowed: { attributes: ['data-*'], styles: [], classes: [] },
883    *     forbidden: { attributes: [], styles: [], classes: [] }
884    *   }
885    * }
886    *
887    * @return {object}
888    *   An object with the following structure:
889    * ```
890    * {
891    *   tags: Array,
892    *   allow: null,
893    *   restrictedTags: {
894    *     tags: Array,
895    *     allowed: {attributes: Array, styles: Array, classes: Array},
896    *     forbidden: {attributes: Array, styles: Array, classes: Array}
897    *   }
898    * }
899    * ```
900    *
901    * @see Drupal.FilterStatus
902    */
903   Drupal.FilterHTMLRule = function() {
904     // Allow or forbid tags.
905     this.tags = [];
906     this.allow = null;
907
908     // Apply restrictions to properties set on tags.
909     this.restrictedTags = {
910       tags: [],
911       allowed: { attributes: [], styles: [], classes: [] },
912       forbidden: { attributes: [], styles: [], classes: [] },
913     };
914
915     return this;
916   };
917
918   Drupal.FilterHTMLRule.prototype.clone = function() {
919     const clone = new Drupal.FilterHTMLRule();
920     clone.tags = this.tags.slice(0);
921     clone.allow = this.allow;
922     clone.restrictedTags.tags = this.restrictedTags.tags.slice(0);
923     clone.restrictedTags.allowed.attributes = this.restrictedTags.allowed.attributes.slice(
924       0,
925     );
926     clone.restrictedTags.allowed.styles = this.restrictedTags.allowed.styles.slice(
927       0,
928     );
929     clone.restrictedTags.allowed.classes = this.restrictedTags.allowed.classes.slice(
930       0,
931     );
932     clone.restrictedTags.forbidden.attributes = this.restrictedTags.forbidden.attributes.slice(
933       0,
934     );
935     clone.restrictedTags.forbidden.styles = this.restrictedTags.forbidden.styles.slice(
936       0,
937     );
938     clone.restrictedTags.forbidden.classes = this.restrictedTags.forbidden.classes.slice(
939       0,
940     );
941     return clone;
942   };
943
944   /**
945    * Tracks the configuration of all text filters in {@link Drupal.FilterStatus}
946    * objects for {@link Drupal.editorConfiguration.featureIsAllowedByFilters}.
947    *
948    * @namespace
949    */
950   Drupal.filterConfiguration = {
951     /**
952      * Drupal.FilterStatus objects, keyed by filter ID.
953      *
954      * @type {Object.<string, Drupal.FilterStatus>}
955      */
956     statuses: {},
957
958     /**
959      * Live filter setting parsers.
960      *
961      * Object keyed by filter ID, for those filters that implement it.
962      *
963      * Filters should load the implementing JavaScript on the filter
964      * configuration form and implement
965      * `Drupal.filterSettings[filterID].getRules()`, which should return an
966      * array of {@link Drupal.FilterHTMLRule} objects.
967      *
968      * @namespace
969      */
970     liveSettingParsers: {},
971
972     /**
973      * Updates all {@link Drupal.FilterStatus} objects to reflect current state.
974      *
975      * Automatically checks whether a filter is currently enabled or not. To
976      * support more finegrained.
977      *
978      * If a filter implements a live setting parser, then that will be used to
979      * keep the HTML rules for the {@link Drupal.FilterStatus} object
980      * up-to-date.
981      */
982     update() {
983       Object.keys(Drupal.filterConfiguration.statuses || {}).forEach(
984         filterID => {
985           // Update status.
986           Drupal.filterConfiguration.statuses[filterID].active = $(
987             `[name="filters[${filterID}][status]"]`,
988           ).is(':checked');
989
990           // Update current rules.
991           if (Drupal.filterConfiguration.liveSettingParsers[filterID]) {
992             Drupal.filterConfiguration.statuses[
993               filterID
994             ].rules = Drupal.filterConfiguration.liveSettingParsers[
995               filterID
996             ].getRules();
997           }
998         },
999       );
1000     },
1001   };
1002
1003   /**
1004    * Initializes {@link Drupal.filterConfiguration}.
1005    *
1006    * @type {Drupal~behavior}
1007    *
1008    * @prop {Drupal~behaviorAttach} attach
1009    *   Gets filter configuration from filter form input.
1010    */
1011   Drupal.behaviors.initializeFilterConfiguration = {
1012     attach(context, settings) {
1013       const $context = $(context);
1014
1015       $context
1016         .find('#filters-status-wrapper input.form-checkbox')
1017         .once('filter-editor-status')
1018         .each(function() {
1019           const $checkbox = $(this);
1020           const nameAttribute = $checkbox.attr('name');
1021
1022           // The filter's checkbox has a name attribute of the form
1023           // "filters[<name of filter>][status]", parse "<name of filter>"
1024           // from it.
1025           const filterID = nameAttribute.substring(
1026             8,
1027             nameAttribute.indexOf(']'),
1028           );
1029
1030           // Create a Drupal.FilterStatus object to track the state (whether it's
1031           // active or not and its current settings, if any) of each filter.
1032           Drupal.filterConfiguration.statuses[
1033             filterID
1034           ] = new Drupal.FilterStatus(filterID);
1035         });
1036     },
1037   };
1038 })(jQuery, _, Drupal, document);