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