annotate core/modules/editor/js/editor.admin.es6.js @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
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);