8
0

viewconsumable.js 26 KB

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