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