viewconsumable.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/conversion/viewconsumable
  7. */
  8. import isArray from '../../utils/lib/lodash/isArray.js';
  9. import CKEditorError from '../../utils/ckeditorerror.js';
  10. import ViewElement from '../view/element.js';
  11. import ViewText from '../view/text.js';
  12. import ViewDocumentFragment from '../view/documentfragment.js';
  13. /**
  14. * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
  15. * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
  16. * Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
  17. * does not consume its attributes, classes and styles.
  18. * To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
  19. * To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
  20. * To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
  21. * To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
  22. *
  23. * viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
  24. * viewConsumable.add( textNode ); // Adds text node for consumption.
  25. * viewConsumable.add( docFragment ); // Adds document fragment for consumption.
  26. * viewConsumable.test( element, { name: true } ); // Tests if element's name can be consumed.
  27. * viewConsumable.test( textNode ); // Tests if text node can be consumed.
  28. * viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
  29. * viewConsumable.consume( element, { name: true } ); // Consume element's name.
  30. * viewConsumable.consume( textNode ); // Consume text node.
  31. * viewConsumable.consume( docFragment ); // Consume document fragment.
  32. * viewConsumable.revert( element, { name: true } ); // Revert already consumed element's name.
  33. * viewConsumable.revert( textNode ); // Revert already consumed text node.
  34. * viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
  35. */
  36. export default class ViewConsumable {
  37. /**
  38. * Creates new ViewConsumable.
  39. */
  40. constructor() {
  41. /**
  42. * Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
  43. * {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
  44. * For {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}
  45. * boolean value is stored as value.
  46. *
  47. * @protected
  48. * @member {Map.<module:engine/conversion/viewconsumable~ViewElementConsumables|Boolean>}
  49. */
  50. this._consumables = new Map();
  51. }
  52. /**
  53. * Adds {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
  54. * {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
  55. *
  56. * viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
  57. * viewConsumable.add( p, { attribute: 'name' } ); // Adds element's attribute.
  58. * viewConsumable.add( p, { class: 'foobar' } ); // Adds element's class.
  59. * viewConsumable.add( p, { style: 'color' } ); // Adds element's style
  60. * viewConsumable.add( p, { attribute: 'name', style: 'color' } ); // Adds attribute and style.
  61. * viewConsumable.add( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
  62. * viewConsumable.add( textNode ); // Adds text node to consume.
  63. * viewConsumable.add( docFragment ); // Adds document fragment to consume.
  64. *
  65. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
  66. * attribute is provided - it should be handled separately by providing actual style/class.
  67. *
  68. * viewConsumable.add( p, { attribute: 'style' } ); // This call will throw an exception.
  69. * viewConsumable.add( p, { style: 'color' } ); // This is properly handled style.
  70. *
  71. * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
  72. * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
  73. * @param {Boolean} consumables.name If set to true element's name will be included.
  74. * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
  75. * @param {String|Array.<String>} consumables.class Class name or array of class names.
  76. * @param {String|Array.<String>} consumables.style Style name or array of style names.
  77. */
  78. add( element, consumables ) {
  79. let elementConsumables;
  80. // For text nodes and document fragments just mark them as consumable.
  81. if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
  82. this._consumables.set( element, true );
  83. return;
  84. }
  85. // For elements create new ViewElementConsumables or update already existing one.
  86. if ( !this._consumables.has( element ) ) {
  87. elementConsumables = new ViewElementConsumables();
  88. this._consumables.set( element, elementConsumables );
  89. } else {
  90. elementConsumables = this._consumables.get( element );
  91. }
  92. elementConsumables.add( consumables );
  93. }
  94. /**
  95. * Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
  96. * {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
  97. * It returns `true` when all items included in method's call can be consumed. Returns `false` when
  98. * first already consumed item is found and `null` when first non-consumable item is found.
  99. *
  100. * viewConsumable.test( p, { name: true } ); // Tests element's name.
  101. * viewConsumable.test( p, { attribute: 'name' } ); // Tests attribute.
  102. * viewConsumable.test( p, { class: 'foobar' } ); // Tests class.
  103. * viewConsumable.test( p, { style: 'color' } ); // Tests style.
  104. * viewConsumable.test( p, { attribute: 'name', style: 'color' } ); // Tests attribute and style.
  105. * viewConsumable.test( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
  106. * viewConsumable.test( textNode ); // Tests text node.
  107. * viewConsumable.test( docFragment ); // Tests document fragment.
  108. *
  109. * Testing classes and styles as attribute will test if all added classes/styles can be consumed.
  110. *
  111. * viewConsumable.test( p, { attribute: 'class' } ); // Tests if all added classes can be consumed.
  112. * viewConsumable.test( p, { attribute: 'style' } ); // Tests if all added styles can be consumed.
  113. *
  114. * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
  115. * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
  116. * @param {Boolean} consumables.name If set to true element's name will be included.
  117. * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
  118. * @param {String|Array.<String>} consumables.class Class name or array of class names.
  119. * @param {String|Array.<String>} consumables.style Style name or array of style names.
  120. * @returns {Boolean|null} Returns `true` when all items included in method's call can be consumed. Returns `false`
  121. * when first already consumed item is found and `null` when first non-consumable item is found.
  122. */
  123. test( element, consumables ) {
  124. const elementConsumables = this._consumables.get( element );
  125. if ( elementConsumables === undefined ) {
  126. return null;
  127. }
  128. // For text nodes and document fragments return stored boolean value.
  129. if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
  130. return elementConsumables;
  131. }
  132. // For elements test consumables object.
  133. return elementConsumables.test( consumables );
  134. }
  135. /**
  136. * Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
  137. * {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
  138. * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
  139. *
  140. * viewConsumable.consume( p, { name: true } ); // Consumes element's name.
  141. * viewConsumable.consume( p, { attribute: 'name' } ); // Consumes element's attribute.
  142. * viewConsumable.consume( p, { class: 'foobar' } ); // Consumes element's class.
  143. * viewConsumable.consume( p, { style: 'color' } ); // Consumes element's style.
  144. * viewConsumable.consume( p, { attribute: 'name', style: 'color' } ); // Consumes attribute and style.
  145. * viewConsumable.consume( p, { class: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
  146. * viewConsumable.consume( textNode ); // Consumes text node.
  147. * viewConsumable.consume( docFragment ); // Consumes document fragment.
  148. *
  149. * Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
  150. *
  151. * viewConsumable.consume( p, { attribute: 'class' } ); // Consume only if all added classes can be consumed.
  152. * viewConsumable.consume( p, { attribute: 'style' } ); // Consume only if all added styles can be consumed.
  153. *
  154. * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
  155. * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
  156. * @param {Boolean} consumables.name If set to true element's name will be included.
  157. * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
  158. * @param {String|Array.<String>} consumables.class Class name or array of class names.
  159. * @param {String|Array.<String>} consumables.style Style name or array of style names.
  160. * @returns {Boolean} Returns `true` when all items included in method's call can be consumed,
  161. * otherwise returns `false`.
  162. */
  163. consume( element, consumables ) {
  164. if ( this.test( element, consumables ) ) {
  165. if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
  166. // For text nodes and document fragments set value to false.
  167. this._consumables.set( element, false );
  168. } else {
  169. // For elements - consume consumables object.
  170. this._consumables.get( element ).consume( consumables );
  171. }
  172. return true;
  173. }
  174. return false;
  175. }
  176. /**
  177. * Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
  178. * {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
  179. * Method does not revert items that were never previously added for consumption, even if they are included in
  180. * method's call.
  181. *
  182. * viewConsumable.revert( p, { name: true } ); // Reverts element's name.
  183. * viewConsumable.revert( p, { attribute: 'name' } ); // Reverts element's attribute.
  184. * viewConsumable.revert( p, { class: 'foobar' } ); // Reverts element's class.
  185. * viewConsumable.revert( p, { style: 'color' } ); // Reverts element's style.
  186. * viewConsumable.revert( p, { attribute: 'name', style: 'color' } ); // Reverts attribute and style.
  187. * viewConsumable.revert( p, { class: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
  188. * viewConsumable.revert( textNode ); // Reverts text node.
  189. * viewConsumable.revert( docFragment ); // Reverts document fragment.
  190. *
  191. * Reverting classes and styles as attribute will revert all classes/styles that were previously added for
  192. * consumption.
  193. *
  194. * viewConsumable.revert( p, { attribute: 'class' } ); // Reverts all classes added for consumption.
  195. * viewConsumable.revert( p, { attribute: 'style' } ); // Reverts all styles added for consumption.
  196. *
  197. * @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
  198. * @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
  199. * @param {Boolean} consumables.name If set to true element's name will be included.
  200. * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names.
  201. * @param {String|Array.<String>} consumables.class Class name or array of class names.
  202. * @param {String|Array.<String>} consumables.style Style name or array of style names.
  203. */
  204. revert( element, consumables ) {
  205. const elementConsumables = this._consumables.get( element );
  206. if ( elementConsumables !== undefined ) {
  207. if ( element instanceof ViewText || element instanceof ViewDocumentFragment ) {
  208. // For text nodes and document fragments - set consumable to true.
  209. this._consumables.set( element, true );
  210. } else {
  211. // For elements - revert items from consumables object.
  212. elementConsumables.revert( consumables );
  213. }
  214. }
  215. }
  216. /**
  217. * Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
  218. * element's name and all its attributes, classes and styles.
  219. *
  220. * @static
  221. * @param {module:engine/view/element~Element} element
  222. * @returns {Object} consumables
  223. */
  224. static consumablesFromElement( element ) {
  225. const consumables = {
  226. name: true,
  227. attribute: [],
  228. class: [],
  229. style: []
  230. };
  231. const attributes = element.getAttributeKeys();
  232. for ( let attribute of attributes ) {
  233. // Skip classes and styles - will be added separately.
  234. if ( attribute == 'style' || attribute == 'class' ) {
  235. continue;
  236. }
  237. consumables.attribute.push( attribute );
  238. }
  239. const classes = element.getClassNames();
  240. for ( let className of classes ) {
  241. consumables.class.push( className );
  242. }
  243. const styles = element.getStyleNames();
  244. for ( let style of styles ) {
  245. consumables.style.push( style );
  246. }
  247. return consumables;
  248. }
  249. /**
  250. * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
  251. * {@link module:engine/view/element~Element element} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
  252. * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
  253. *
  254. * @static
  255. * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} from View element or document fragment
  256. * from which `ViewConsumable` will be created.
  257. * @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used
  258. * to add all consumables. It will be returned instead of a new instance.
  259. */
  260. static createFrom( from, instance ) {
  261. if ( !instance ) {
  262. instance = new ViewConsumable();
  263. }
  264. if ( from instanceof ViewText ) {
  265. instance.add( from );
  266. return instance;
  267. }
  268. // Add `from` itself, if it is an element.
  269. if ( from instanceof ViewElement ) {
  270. instance.add( from, ViewConsumable.consumablesFromElement( from ) );
  271. }
  272. if ( from instanceof ViewDocumentFragment ) {
  273. instance.add( from );
  274. }
  275. for ( let child of from.getChildren() ) {
  276. instance = ViewConsumable.createFrom( child, instance );
  277. }
  278. return instance;
  279. }
  280. }
  281. /**
  282. * This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
  283. * It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
  284. *
  285. * @private
  286. */
  287. class ViewElementConsumables {
  288. /**
  289. * Creates ViewElementConsumables instance.
  290. */
  291. constructor() {
  292. /**
  293. * Flag indicating if name of the element can be consumed.
  294. *
  295. * @private
  296. * @member {Boolean}
  297. */
  298. this._canConsumeName = null;
  299. /**
  300. * Contains maps of element's consumables: attributes, classes and styles.
  301. *
  302. * @private
  303. * @member {Object}
  304. */
  305. this._consumables = {
  306. attribute: new Map(),
  307. style: new Map(),
  308. class: new Map()
  309. };
  310. }
  311. /**
  312. * Adds consumable parts of the {@link module:engine/view/element~Element view element}.
  313. * Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
  314. * styles still could be consumed):
  315. *
  316. * consumables.add( { name: true } );
  317. *
  318. * Attributes classes and styles:
  319. *
  320. * consumables.add( { attribute: 'title', class: 'foo', style: 'color' } );
  321. * consumables.add( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
  322. *
  323. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
  324. * attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
  325. *
  326. * @param {Object} consumables Object describing which parts of the element can be consumed.
  327. * @param {Boolean} consumables.name If set to `true` element's name will be added as consumable.
  328. * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to add as consumable.
  329. * @param {String|Array.<String>} consumables.class Class name or array of class names to add as consumable.
  330. * @param {String|Array.<String>} consumables.style Style name or array of style names to add as consumable.
  331. */
  332. add( consumables ) {
  333. if ( consumables.name ) {
  334. this._canConsumeName = true;
  335. }
  336. for ( let type in this._consumables ) {
  337. if ( type in consumables ) {
  338. this._add( type, consumables[ type ] );
  339. }
  340. }
  341. }
  342. /**
  343. * Tests if parts of the {@link module:engine/view/node~Node view node} can be consumed.
  344. *
  345. * Element's name can be tested:
  346. *
  347. * consumables.test( { name: true } );
  348. *
  349. * Attributes classes and styles:
  350. *
  351. * consumables.test( { attribute: 'title', class: 'foo', style: 'color' } );
  352. * consumables.test( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
  353. *
  354. * @param {Object} consumables Object describing which parts of the element should be tested.
  355. * @param {Boolean} consumables.name If set to `true` element's name will be tested.
  356. * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to test.
  357. * @param {String|Array.<String>} consumables.class Class name or array of class names to test.
  358. * @param {String|Array.<String>} consumables.style Style name or array of style names to test.
  359. * @returns {Boolean|null} `true` when all tested items can be consumed, `null` when even one of the items
  360. * was never marked for consumption and `false` when even one of the items was already consumed.
  361. */
  362. test( consumables ) {
  363. // Check if name can be consumed.
  364. if ( consumables.name && !this._canConsumeName ) {
  365. return this._canConsumeName;
  366. }
  367. for ( let type in this._consumables ) {
  368. if ( type in consumables ) {
  369. const value = this._test( type, consumables[ type ] );
  370. if ( value !== true ) {
  371. return value;
  372. }
  373. }
  374. }
  375. // Return true only if all can be consumed.
  376. return true;
  377. }
  378. /**
  379. * Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
  380. * is already consumed - it consumes all consumable items provided.
  381. * Element's name can be consumed:
  382. *
  383. * consumables.consume( { name: true } );
  384. *
  385. * Attributes classes and styles:
  386. *
  387. * consumables.consume( { attribute: 'title', class: 'foo', style: 'color' } );
  388. * consumables.consume( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
  389. *
  390. * @param {Object} consumables Object describing which parts of the element should be consumed.
  391. * @param {Boolean} consumables.name If set to `true` element's name will be consumed.
  392. * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to consume.
  393. * @param {String|Array.<String>} consumables.class Class name or array of class names to consume.
  394. * @param {String|Array.<String>} consumables.style Style name or array of style names to consume.
  395. */
  396. consume( consumables ) {
  397. if ( consumables.name ) {
  398. this._canConsumeName = false;
  399. }
  400. for ( let type in this._consumables ) {
  401. if ( type in consumables ) {
  402. this._consume( type, consumables[ type ] );
  403. }
  404. }
  405. }
  406. /**
  407. * Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
  408. * Element's name can be reverted:
  409. *
  410. * consumables.revert( { name: true } );
  411. *
  412. * Attributes classes and styles:
  413. *
  414. * consumables.revert( { attribute: 'title', class: 'foo', style: 'color' } );
  415. * consumables.revert( { attribute: [ 'title', 'name' ], class: [ 'foo', 'bar' ] );
  416. *
  417. * @param {Object} consumables Object describing which parts of the element should be reverted.
  418. * @param {Boolean} consumables.name If set to `true` element's name will be reverted.
  419. * @param {String|Array.<String>} consumables.attribute Attribute name or array of attribute names to revert.
  420. * @param {String|Array.<String>} consumables.class Class name or array of class names to revert.
  421. * @param {String|Array.<String>} consumables.style Style name or array of style names to revert.
  422. */
  423. revert( consumables ) {
  424. if ( consumables.name ) {
  425. this._canConsumeName = true;
  426. }
  427. for ( let type in this._consumables ) {
  428. if ( type in consumables ) {
  429. this._revert( type, consumables[ type ] );
  430. }
  431. }
  432. }
  433. /**
  434. * Helper method that adds consumables of a given type: attribute, class or style.
  435. *
  436. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
  437. * type is provided - it should be handled separately by providing actual style/class type.
  438. *
  439. * @private
  440. * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
  441. * @param {String|Array.<String>} item Consumable item or array of items.
  442. */
  443. _add( type, item ) {
  444. const items = isArray( item ) ? item : [ item ];
  445. const consumables = this._consumables[ type ];
  446. for ( let name of items ) {
  447. if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
  448. /**
  449. * Class and style attributes should be handled separately.
  450. *
  451. * @error viewconsumable-invalid-attribute
  452. */
  453. throw new CKEditorError( 'viewconsumable-invalid-attribute: Classes and styles should be handled separately.' );
  454. }
  455. consumables.set( name, true );
  456. }
  457. }
  458. /**
  459. * Helper method that tests consumables of a given type: attribute, class or style.
  460. *
  461. * @private
  462. * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
  463. * @param {String|Array.<String>} item Consumable item or array of items.
  464. * @returns {Boolean|null} Returns `true` if all items can be consumed, `null` when one of the items cannot be
  465. * consumed and `false` when one of the items is already consumed.
  466. */
  467. _test( type, item ) {
  468. const items = isArray( item ) ? item : [ item ];
  469. const consumables = this._consumables[ type ];
  470. for ( let name of items ) {
  471. if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
  472. // Check all classes/styles if class/style attribute is tested.
  473. const value = this._test( name, [ ...this._consumables[ name ].keys() ] );
  474. if ( value !== true ) {
  475. return value;
  476. }
  477. } else {
  478. const value = consumables.get( name );
  479. // Return null if attribute is not found.
  480. if ( value === undefined ) {
  481. return null;
  482. }
  483. if ( !value ) {
  484. return false;
  485. }
  486. }
  487. }
  488. return true;
  489. }
  490. /**
  491. * Helper method that consumes items of a given type: attribute, class or style.
  492. *
  493. * @private
  494. * @param {String} type Type of the consumable item: `attribute`, `class` or `style`.
  495. * @param {String|Array.<String>} item Consumable item or array of items.
  496. */
  497. _consume( type, item ) {
  498. const items = isArray( item ) ? item : [ item ];
  499. const consumables = this._consumables[ type ];
  500. for ( let name of items ) {
  501. if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
  502. // If class or style is provided for consumption - consume them all.
  503. this._consume( name, [ ...this._consumables[ name ].keys() ] );
  504. } else {
  505. consumables.set( name, false );
  506. }
  507. }
  508. }
  509. /**
  510. * Helper method that reverts items of a given type: attribute, class or style.
  511. *
  512. * @private
  513. * @param {String} type Type of the consumable item: `attribute`, `class` or , `style`.
  514. * @param {String|Array.<String>} item Consumable item or array of items.
  515. */
  516. _revert( type, item ) {
  517. const items = isArray( item ) ? item : [ item ];
  518. const consumables = this._consumables[ type ];
  519. for ( let name of items ) {
  520. if ( type === 'attribute' && ( name === 'class' || name === 'style' ) ) {
  521. // If class or style is provided for reverting - revert them all.
  522. this._revert( name, [ ...this._consumables[ name ].keys() ] );
  523. } else {
  524. const value = consumables.get( name );
  525. if ( value === false ) {
  526. consumables.set( name, true );
  527. }
  528. }
  529. }
  530. }
  531. }