8
0

modelconsumable.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. /**
  7. * Manages a list of consumable values for {@link engine.treeModel.Item model items}.
  8. *
  9. * Consumables are various aspects of the model items. A model item can be broken down into singular properties
  10. * that might be taken into consideration when converting that item.
  11. *
  12. * `ModelConsumable` is used by {@link engine.treeController.ModelConversionDispatcher} while analyzing changed
  13. * parts of {@link engine.treeModel.Document the document}. The added / changed / removed model items are broken down
  14. * into singular properties (the item itself and it's attributes). All those parts are saved in `ModelConsumable`. Then,
  15. * during conversion, when given part of model item is converted (i.e. the view element has been inserted into the view,
  16. * but without attributes), consumable value is removed from `ModelConsumable`.
  17. *
  18. * Although `ModelConsumable` might store consumable values of any type (see {@link engine.treeController.ModelConsumable#add}),
  19. * the commonly used ones are similar to events names fired by {@link engine.treeController.ModelConversionDispatcher}:
  20. * `insert`, `addAttribute:<attributeKey>`, `changeAttribute:<attributeKey>`, `removeAttribute:<attributeKey>`.
  21. *
  22. * Most commonly, {@link engine.treeController.ModelConsumable#add add method} will be used only by
  23. * {@link engine.treeController.ModelConversionDispatcher} to gather consumable values. However, it is important to
  24. * understand how consumable values can be {@link engine.treeController.ModelConsumable#consume consumed}. See also:
  25. * {@link engine.treeController.modelToView}.
  26. *
  27. * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
  28. * able to convert one or more parts of the model. However, when one of those callbacks actually converts
  29. * something, other should not, because they would duplicate the results. Using `ModelConsumable` helps avoiding
  30. * this situation, because callbacks should only convert those values, which were not yet consumed from `ModelConsumable`.
  31. *
  32. * Consuming multiple values in a single callback:
  33. *
  34. * // Converter for custom `image` element that might have a `caption` element inside which changes
  35. * // how the image is displayed in the view:
  36. * //
  37. * // Model:
  38. * //
  39. * // [image]
  40. * // └─ [caption]
  41. * // ├─ f
  42. * // ├─ o
  43. * // └─ o
  44. * //
  45. * // View:
  46. * //
  47. * // <figure>
  48. * // ├─ <img />
  49. * // └─ <caption>
  50. * // └─ foo
  51. * modelConversionDispatcher.on( 'insert:element:image', ( evt, data, consumable, conversionApi ) => {
  52. * // First, consume the `image` element.
  53. * consumable.consume( data.item, 'insert' );
  54. *
  55. * // Just create normal image element for the view.
  56. * // Maybe it will be "decorated" later.
  57. * const viewImage = new ViewElement( 'img' );
  58. * const insertPosition = conversionApi.mapper.toViewPosition( data.range.start );
  59. *
  60. * // Check if the `image` element has children.
  61. * if ( data.item.getChildCount() > 0 ) {
  62. * const modelCaption = data.item.getChild( 0 );
  63. *
  64. * // `modelCaption` insertion change is consumed from consumable values.
  65. * // It will not be converted by other converters, but it's children (probably some text) will be.
  66. * // Through mapping, converters for text will know where to insert contents of `modelCaption`.
  67. * if ( consumable.consume( modelCaption, 'insert' ) ) {
  68. * const viewCaption = new ViewElement( 'figuaption' );
  69. *
  70. * const viewImageHolder = new ViewElement( 'figure', null, [ viewImage, viewCaption ] );
  71. *
  72. * conversionApi.mapper.bindElements( modelCaption, viewCaption );
  73. * conversionApi.mapper.bindElements( data.item, viewImageHolder );
  74. * conversionApi.writer.insert( insertPosition, viewImageHolder );
  75. * }
  76. * } else {
  77. * conversionApi.mapper.bindElements( data.item, viewImage );
  78. * conversionApi.writer.insert( insertPosition, viewImage );
  79. * }
  80. *
  81. * evt.stop();
  82. * } );
  83. *
  84. * @memberOf engine.treeController
  85. */
  86. export default class ModelConsumable {
  87. /**
  88. * Creates an empty consumables list.
  89. */
  90. constructor() {
  91. this.consumable = new Map();
  92. }
  93. /**
  94. * Adds a consumable value to the consumables list and links it with given model item.
  95. *
  96. * modelConsumable.add( modelElement, 'insert' ); // Add `modelElement` insertion change to consumable values.
  97. * modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change.
  98. * modelConsumable.add( modelElement, 'removeAttribute:bold' ); // Add `bold` attribute removal on `modelElement` change.
  99. *
  100. * @param {engine.treeModel.Item} item Model item that has the consumable.
  101. * @param {String} type Consumable type.
  102. */
  103. add( item, type ) {
  104. if ( !this.consumable.has( item ) ) {
  105. this.consumable.set( item, new Map() );
  106. }
  107. this.consumable.get( item ).set( type, true );
  108. }
  109. /**
  110. * Removes given consumable value from given model item.
  111. *
  112. * modelConsumable.consume( modelElement, 'insert' ); // Remove `modelElement` insertion change from consumable values.
  113. * modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change.
  114. * modelConsumable.consume( modelElement, 'removeAttribute:bold' ); // Remove `bold` attribute removal on `modelElement` change.
  115. *
  116. * @param {engine.treeModel.Item} item Model item from which consumable will be consumed.
  117. * @param {String} type Consumable type.
  118. * @returns {Boolean} `true` if consumable value was available and was consumed, `false` otherwise.
  119. */
  120. consume( item, type ) {
  121. if ( this.test( item, type ) ) {
  122. this.consumable.get( item ).set( type, false );
  123. return true;
  124. } else {
  125. return false;
  126. }
  127. }
  128. /**
  129. * Tests whether there is a consumable value of given type connected with given model item.
  130. *
  131. * modelConsumable.test( modelElement, 'insert' ); // Check for `modelElement` insertion change.
  132. * modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change.
  133. * modelConsumable.test( modelElement, 'removeAttribute:bold' ); // Check for `bold` attribute removal on `modelElement` change.
  134. *
  135. * @param {engine.treeModel.Item} item Model item that will be tested.
  136. * @param {String} type Consumable type.
  137. * @returns {null|Boolean} `null` if such consumable was never added, `false` if the consumable values was
  138. * already consumed or `true` if it was added and not consumed yet.
  139. */
  140. test( item, type ) {
  141. const itemConsumables = this.consumable.get( item );
  142. if ( itemConsumables === undefined ) {
  143. return null;
  144. }
  145. const value = itemConsumables.get( type );
  146. if ( value === undefined ) {
  147. return null;
  148. }
  149. return value;
  150. }
  151. /**
  152. * Reverts consuming of consumable value.
  153. *
  154. * modelConsumable.revert( modelElement, 'insert' ); // Revert consuming `modelElement` insertion change.
  155. * modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`.
  156. * modelConsumable.revert( modelElement, 'removeAttribute:bold' ); // Revert consuming `bold` attribute remove from `modelElement`.
  157. *
  158. * @param {engine.treeModel.Item} item Model item that will be tested.
  159. * @param {String} type Consumable type.
  160. * @returns {null|Boolean} `true` if consumable has been reversed, `false` otherwise. `null` if the consumable has
  161. * never been added.
  162. */
  163. revert( item, type ) {
  164. const test = this.test( item, type );
  165. if ( test === false ) {
  166. this.consumable.get( item ).set( type, true );
  167. return true;
  168. } else if ( test === true ) {
  169. return false;
  170. }
  171. return null;
  172. }
  173. }