8
0

modelconsumable.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import TextProxy from '../model/textproxy.js';
  6. /**
  7. * Manages a list of consumable values for {@link engine.model.Item model items}.
  8. *
  9. * Consumables are various aspects of the model. A model item can be broken down into singular properties that might be
  10. * taken into consideration when converting that item.
  11. *
  12. * `ModelConsumable` is used by {@link engine.conversion.ModelConversionDispatcher} while analyzing changed
  13. * parts of {@link engine.model.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. * For model items, `ModelConsumable` stores consumable values of one of following types: `insert`, `addAttribute:<attributeKey>`,
  19. * `changeAttribute:<attributeKey>`, `removeAttribute:<attributeKey>`.
  20. *
  21. * In most cases, it is enough to let {@link engine.conversion.ModelConversionDispatcher} gather consumable values, so
  22. * there is no need to use {@link engine.conversion.ModelConsumable#add add method} directly. However, it is important to
  23. * understand how consumable values can be {@link engine.conversion.ModelConsumable#consume consumed}. See
  24. * {@link engine.conversion.modelToView default model to view converters} for more information.
  25. *
  26. * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is
  27. * able to convert one or more parts of the model. However, when one of those callbacks actually converts
  28. * something, other should not, because they would duplicate the results. Using `ModelConsumable` helps avoiding
  29. * this situation, because callbacks should only convert those values, which were not yet consumed from `ModelConsumable`.
  30. *
  31. * Consuming multiple values in a single callback:
  32. *
  33. * // Converter for custom `image` element that might have a `caption` element inside which changes
  34. * // how the image is displayed in the view:
  35. * //
  36. * // Model:
  37. * //
  38. * // [image]
  39. * // └─ [caption]
  40. * // └─ foo
  41. * //
  42. * // View:
  43. * //
  44. * // <figure>
  45. * // ├─ <img />
  46. * // └─ <caption>
  47. * // └─ foo
  48. * modelConversionDispatcher.on( 'insert:image', ( evt, data, consumable, conversionApi ) => {
  49. * // First, consume the `image` element.
  50. * consumable.consume( data.item, 'insert' );
  51. *
  52. * // Just create normal image element for the view.
  53. * // Maybe it will be "decorated" later.
  54. * const viewImage = new ViewElement( 'img' );
  55. * const insertPosition = conversionApi.mapper.toViewPosition( data.range.start );
  56. *
  57. * // Check if the `image` element has children.
  58. * if ( data.item.getChildCount() > 0 ) {
  59. * const modelCaption = data.item.getChild( 0 );
  60. *
  61. * // `modelCaption` insertion change is consumed from consumable values.
  62. * // It will not be converted by other converters, but it's children (probably some text) will be.
  63. * // Through mapping, converters for text will know where to insert contents of `modelCaption`.
  64. * if ( consumable.consume( modelCaption, 'insert' ) ) {
  65. * const viewCaption = new ViewElement( 'figcaption' );
  66. *
  67. * const viewImageHolder = new ViewElement( 'figure', null, [ viewImage, viewCaption ] );
  68. *
  69. * conversionApi.mapper.bindElements( modelCaption, viewCaption );
  70. * conversionApi.mapper.bindElements( data.item, viewImageHolder );
  71. * viewWriter.insert( insertPosition, viewImageHolder );
  72. * }
  73. * } else {
  74. * conversionApi.mapper.bindElements( data.item, viewImage );
  75. * viewWriter.insert( insertPosition, viewImage );
  76. * }
  77. *
  78. * evt.stop();
  79. * } );
  80. *
  81. * @memberOf engine.conversion
  82. */
  83. export default class ModelConsumable {
  84. /**
  85. * Creates an empty consumables list.
  86. */
  87. constructor() {
  88. /**
  89. * Contains list of consumable values.
  90. *
  91. * @private
  92. * @member {Map} engine.conversion.ModelConsumable#_consumable
  93. */
  94. this._consumable = new Map();
  95. /**
  96. * For each {@link engine.model.TextProxy} added to `ModelConsumable`, this registry holds parent
  97. * of that `TextProxy` and start and end indices of that `TextProxy`. This allows identification of `TextProxy`
  98. * instances that points to the same part of the model but are different instances. Each distinct `TextProxy`
  99. * is given unique `Symbol` which is then registered as consumable. This process is transparent for `ModelConsumable`
  100. * API user because whenever `TextProxy` is added, tested, consumed or reverted, internal mechanisms of
  101. * `ModelConsumable` translates `TextProxy` to that unique `Symbol`.
  102. *
  103. * @private
  104. * @member {Map} engine.conversion.ModelConsumable#_textProxyRegistry
  105. */
  106. this._textProxyRegistry = new Map();
  107. }
  108. /**
  109. * Adds a consumable value to the consumables list and links it with given model item.
  110. *
  111. * modelConsumable.add( modelElement, 'insert' ); // Add `modelElement` insertion change to consumable values.
  112. * modelConsumable.add( modelElement, 'addAttribute:bold' ); // Add `bold` attribute insertion on `modelElement` change.
  113. * modelConsumable.add( modelElement, 'removeAttribute:bold' ); // Add `bold` attribute removal on `modelElement` change.
  114. * modelConsumable.add( modelSelection, 'selection' ); // Add `modelSelection` to consumable values.
  115. * modelConsumable.add( modelSelection, 'selectionAttribute:bold' ); // Add `bold` attribute on `modelSelection` to consumables.
  116. *
  117. * @param {engine.model.Item|engine.model.Selection} item Model item or selection that has the consumable.
  118. * @param {String} type Consumable type.
  119. */
  120. add( item, type ) {
  121. if ( item instanceof TextProxy ) {
  122. item = this._getSymbolForTextProxy( item );
  123. }
  124. if ( !this._consumable.has( item ) ) {
  125. this._consumable.set( item, new Map() );
  126. }
  127. this._consumable.get( item ).set( type, true );
  128. }
  129. /**
  130. * Removes given consumable value from given model item.
  131. *
  132. * modelConsumable.consume( modelElement, 'insert' ); // Remove `modelElement` insertion change from consumable values.
  133. * modelConsumable.consume( modelElement, 'addAttribute:bold' ); // Remove `bold` attribute insertion on `modelElement` change.
  134. * modelConsumable.consume( modelElement, 'removeAttribute:bold' ); // Remove `bold` attribute removal on `modelElement` change.
  135. * modelConsumable.consume( modelSelection, 'selection' ); // Remove `modelSelection` from consumable values.
  136. * modelConsumable.consume( modelSelection, 'selectionAttribute:bold' ); // Remove `bold` on `modelSelection` from consumables.
  137. *
  138. * @param {engine.model.Item|engine.model.Selection} item Model item or selection from which consumable will be consumed.
  139. * @param {String} type Consumable type.
  140. * @returns {Boolean} `true` if consumable value was available and was consumed, `false` otherwise.
  141. */
  142. consume( item, type ) {
  143. if ( item instanceof TextProxy ) {
  144. item = this._getSymbolForTextProxy( item );
  145. }
  146. if ( this.test( item, type ) ) {
  147. this._consumable.get( item ).set( type, false );
  148. return true;
  149. } else {
  150. return false;
  151. }
  152. }
  153. /**
  154. * Tests whether there is a consumable value of given type connected with given model item.
  155. *
  156. * modelConsumable.test( modelElement, 'insert' ); // Check for `modelElement` insertion change.
  157. * modelConsumable.test( modelElement, 'addAttribute:bold' ); // Check for `bold` attribute insertion on `modelElement` change.
  158. * modelConsumable.test( modelElement, 'removeAttribute:bold' ); // Check for `bold` attribute removal on `modelElement` change.
  159. * modelConsumable.test( modelSelection, 'selection' ); // Check if `modelSelection` is consumable.
  160. * modelConsumable.test( modelSelection, 'selectionAttribute:bold' ); // Check if `bold` on `modelSelection` is consumable.
  161. *
  162. * @param {engine.model.Item|engine.model.Selection} item Model item or selection that will be tested.
  163. * @param {String} type Consumable type.
  164. * @returns {null|Boolean} `null` if such consumable was never added, `false` if the consumable values was
  165. * already consumed or `true` if it was added and not consumed yet.
  166. */
  167. test( item, type ) {
  168. if ( item instanceof TextProxy ) {
  169. item = this._getSymbolForTextProxy( item );
  170. }
  171. const itemConsumables = this._consumable.get( item );
  172. if ( itemConsumables === undefined ) {
  173. return null;
  174. }
  175. const value = itemConsumables.get( type );
  176. if ( value === undefined ) {
  177. return null;
  178. }
  179. return value;
  180. }
  181. /**
  182. * Reverts consuming of consumable value.
  183. *
  184. * modelConsumable.revert( modelElement, 'insert' ); // Revert consuming `modelElement` insertion change.
  185. * modelConsumable.revert( modelElement, 'addAttribute:bold' ); // Revert consuming `bold` attribute insert from `modelElement`.
  186. * modelConsumable.revert( modelElement, 'removeAttribute:bold' ); // Revert consuming `bold` attribute remove from `modelElement`.
  187. * modelConsumable.revert( modelSelection, 'selection' ); // Revert consuming `modelSelection`.
  188. * modelConsumable.revert( modelSelection, 'selectionAttribute:bold' ); // Revert consuming `bold` from `modelSelection`.
  189. *
  190. * @param {engine.model.Item|engine.model.Selection} item Model item or selection that will be reverted.
  191. * @param {String} type Consumable type.
  192. * @returns {null|Boolean} `true` if consumable has been reversed, `false` otherwise. `null` if the consumable has
  193. * never been added.
  194. */
  195. revert( item, type ) {
  196. if ( item instanceof TextProxy ) {
  197. item = this._getSymbolForTextProxy( item );
  198. }
  199. const test = this.test( item, type );
  200. if ( test === false ) {
  201. this._consumable.get( item ).set( type, true );
  202. return true;
  203. } else if ( test === true ) {
  204. return false;
  205. }
  206. return null;
  207. }
  208. /**
  209. * Gets a unique symbol for passed {@link engine.model.TextProxy} instance. All `TextProxy` instances that
  210. * have same parent, same start index and same end index will get the same symbol.
  211. *
  212. * Used internally to correctly consume `TextProxy` instances.
  213. *
  214. * @private
  215. * @param {engine.model.TextProxy} textProxy `TextProxy` instance to get a symbol for.
  216. * @returns {Symbol} Symbol representing all equal instances of `TextProxy`.
  217. */
  218. _getSymbolForTextProxy( textProxy ) {
  219. let symbol = null;
  220. const startMap = this._textProxyRegistry.get( textProxy.startOffset );
  221. if ( startMap ) {
  222. const endMap = startMap.get( textProxy.endOffset );
  223. if ( endMap ) {
  224. symbol = endMap.get( textProxy.parent );
  225. }
  226. }
  227. if ( !symbol ) {
  228. symbol = this._addSymbolForTextProxy( textProxy.startOffset, textProxy.endOffset, textProxy.parent );
  229. }
  230. return symbol;
  231. }
  232. /**
  233. * Adds a symbol for given properties that characterizes a {@link engine.model.TextProxy} instance.
  234. *
  235. * Used internally to correctly consume `TextProxy` instances.
  236. *
  237. * @private
  238. * @param {Number} startIndex Text proxy start index in it's parent.
  239. * @param {Number} endIndex Text proxy end index in it's parent.
  240. * @param {engine.model.Element} parent Text proxy parent.
  241. * @returns {Symbol} Symbol generated for given properties.
  242. */
  243. _addSymbolForTextProxy( start, end, parent ) {
  244. const symbol = Symbol();
  245. let startMap, endMap;
  246. startMap = this._textProxyRegistry.get( start );
  247. if ( !startMap ) {
  248. startMap = new Map();
  249. this._textProxyRegistry.set( start, startMap );
  250. }
  251. endMap = startMap.get( end );
  252. if ( !endMap ) {
  253. endMap = new Map();
  254. startMap.set( end, endMap );
  255. }
  256. endMap.set( parent, symbol );
  257. return symbol;
  258. }
  259. }