model-selection-to-view-converters.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ViewElement from '../view/element';
  6. import ViewRange from '../view/range';
  7. import viewWriter from '../view/writer';
  8. /**
  9. * Contains {@link module:engine/model/selection~Selection model selection} to
  10. * {@link module:engine/view/selection~Selection view selection} converters for
  11. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
  12. *
  13. * @module engine/conversion/model-selection-to-view-converters
  14. */
  15. /**
  16. * Function factory, creates a converter that converts non-collapsed {@link module:engine/model/selection~Selection model selection} to
  17. * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object
  18. * and maps model positions from selection to view positions.
  19. *
  20. * modelDispatcher.on( 'selection', convertRangeSelection() );
  21. *
  22. * @returns {Function} Selection converter.
  23. */
  24. export function convertRangeSelection() {
  25. return ( evt, data, consumable, conversionApi ) => {
  26. const selection = data.selection;
  27. if ( selection.isCollapsed ) {
  28. return;
  29. }
  30. if ( !consumable.consume( selection, 'selection' ) ) {
  31. return;
  32. }
  33. conversionApi.viewSelection.removeAllRanges();
  34. for ( let range of selection.getRanges() ) {
  35. const viewRange = conversionApi.mapper.toViewRange( range );
  36. conversionApi.viewSelection.addRange( viewRange, selection.isBackward );
  37. }
  38. };
  39. }
  40. /**
  41. * Function factory, creates a converter that converts collapsed {@link module:engine/model/selection~Selection model selection} to
  42. * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object,
  43. * maps model selection position to view position and breaks {@link module:engine/view/attributeelement~AttributeElement attribute elements}
  44. * at the selection position.
  45. *
  46. * modelDispatcher.on( 'selection', convertCollapsedSelection() );
  47. *
  48. * Example of view state before and after converting collapsed selection:
  49. *
  50. * <p><strong>f^oo<strong>bar</p>
  51. * -> <p><strong>f</strong>^<strong>oo</strong>bar</p>
  52. *
  53. * By breaking attribute elements like `<strong>`, selection is in correct element. See also complementary
  54. * {@link module:engine/conversion/model-selection-to-view-converters~convertSelectionAttribute attribute converter}
  55. * for selection attributes,
  56. * which wraps collapsed selection into view elements. Those converters together ensure, that selection ends up in
  57. * appropriate attribute elements.
  58. *
  59. * See also {@link module:engine/conversion/model-selection-to-view-converters~clearAttributes} which does a clean-up
  60. * by merging attributes.
  61. *
  62. * @returns {Function} Selection converter.
  63. */
  64. export function convertCollapsedSelection() {
  65. return ( evt, data, consumable, conversionApi ) => {
  66. const selection = data.selection;
  67. if ( !selection.isCollapsed ) {
  68. return;
  69. }
  70. if ( !consumable.consume( selection, 'selection' ) ) {
  71. return;
  72. }
  73. const modelPosition = selection.getFirstPosition();
  74. const viewPosition = conversionApi.mapper.toViewPosition( modelPosition );
  75. const brokenPosition = viewWriter.breakAttributes( viewPosition );
  76. conversionApi.viewSelection.removeAllRanges();
  77. conversionApi.viewSelection.addRange( new ViewRange( brokenPosition, brokenPosition ) );
  78. };
  79. }
  80. /**
  81. * Function factory, creates a converter that converts {@link module:engine/model/selection~Selection model selection} attributes to
  82. * {@link module:engine/view/attributeelement~AttributeElement view attribute elements}. The converter works only for collapsed selection.
  83. * The converter consumes appropriate value from `consumable` object, maps model selection position to view position and
  84. * wraps that position into a view attribute element.
  85. *
  86. * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
  87. * the copy will become the wrapping element. If `Function` is provided, it is passed all the parameters of the
  88. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:selectionAttribute selectionAttribute event}.
  89. * It's expected that the function returns a {@link module:engine/view/attributeelement~AttributeElement}.
  90. * The result of the function will be the wrapping element.
  91. *
  92. * modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
  93. *
  94. * function styleElementCreator( styleValue ) {
  95. * if ( styleValue == 'important' ) {
  96. * return new ViewAttributeElement( 'strong', { style: 'text-transform:uppercase;' } );
  97. * } else if ( styleValue == 'gold' ) {
  98. * return new ViewAttributeElement( 'span', { style: 'color:yellow;' } );
  99. * }
  100. * }
  101. * modelDispatcher.on( 'selectionAttribute:style', convertSelectionAttribute( styleCreator ) );
  102. *
  103. * **Note:** You can use the same `elementCreator` function for this converter factory
  104. * and {@link module:engine/conversion/model-to-view-converters~wrap}
  105. * model to view converter, as long as the `elementCreator` function uses only the first parameter (attribute value).
  106. *
  107. * modelDispatcher.on( 'selection', convertCollapsedSelection() );
  108. * modelDispatcher.on( 'selectionAttribute:italic', convertSelectionAttribute( new ViewAttributeElement( 'em' ) ) );
  109. * modelDispatcher.on( 'selectionAttribute:bold', convertSelectionAttribute( new ViewAttributeElement( 'strong' ) ) );
  110. *
  111. * Example of view states before and after converting collapsed selection:
  112. *
  113. * <p><em>f^oo</em>bar</p>
  114. * -> <p><em>f</em>^<em>oo</em>bar</p>
  115. * -> <p><em>f^oo</em>bar</p>
  116. *
  117. * Example of view state after converting collapsed selection. The scenario is: selection is inside bold text (`<strong>` element)
  118. * but it does not have bold attribute itself and has italic attribute instead (let's assume that user turned off bold and turned
  119. * on italic with selection collapsed):
  120. *
  121. * <p><strong>f^oo<strong>bar</p>
  122. * -> <p><strong>f</strong>^<strong>oo<strong>bar</p>
  123. * -> <p><strong>f</strong><em>^</em><strong>oo</strong>bar</p>
  124. *
  125. * In first example, nothing has changed, because first `<em>` element got broken by `convertCollapsedSelection()` converter,
  126. * but then it got wrapped-back by `convertSelectionAttribute()` converter. In second example, notice how `<strong>` element
  127. * is broken to prevent putting selection in it, since selection has no `bold` attribute.
  128. *
  129. * @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View element,
  130. * or function returning a view element, which will be used for wrapping.
  131. * @returns {Function} Selection converter.
  132. */
  133. export function convertSelectionAttribute( elementCreator ) {
  134. return ( evt, data, consumable, conversionApi ) => {
  135. const viewElement = elementCreator instanceof ViewElement ?
  136. elementCreator.clone( true ) :
  137. elementCreator( data.value, data, data.selection, consumable, conversionApi );
  138. const consumableName = 'selectionAttribute:' + data.key;
  139. wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
  140. };
  141. }
  142. /**
  143. * Performs similar conversion as {@link ~convertSelectionAttribute}, but depends on a marker name of a marker in which
  144. * collapsed selection is placed.
  145. *
  146. * modelDispatcher.on( 'selectionMarker:searchResult', wrapRange( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
  147. *
  148. * **Note:** You can use the same `elementCreator` function for this converter factory
  149. * and {@link module:engine/conversion/model-to-view-converters~wrapRange}.
  150. *
  151. * @see {~convertSelectionAttribute}
  152. * @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View element,
  153. * or function returning a view element, which will be used for wrapping.
  154. * @returns {Function} Selection converter.
  155. */
  156. export function convertSelectionMarker( elementCreator ) {
  157. return ( evt, data, consumable, conversionApi ) => {
  158. const viewElement = elementCreator instanceof ViewElement ?
  159. elementCreator.clone( true ) :
  160. elementCreator( data, consumable, conversionApi );
  161. const consumableName = 'selectionMarker:' + data.name;
  162. wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName );
  163. };
  164. }
  165. // Helper function for `convertSelectionAttribute` and `convertSelectionMarker`, which perform similar task.
  166. function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElement, consumable, consumableName ) {
  167. if ( !modelSelection.isCollapsed ) {
  168. return;
  169. }
  170. if ( !consumable.consume( modelSelection, consumableName ) ) {
  171. return;
  172. }
  173. let viewPosition = viewSelection.getFirstPosition();
  174. viewPosition = viewWriter.wrapPosition( viewPosition, viewElement );
  175. viewSelection.removeAllRanges();
  176. viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) );
  177. }
  178. /**
  179. * Function factory, creates a converter that clears artifacts after the previous
  180. * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty
  181. * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merge sibling attributes at all start and end
  182. * positions of all ranges.
  183. *
  184. * <p><strong>^</strong></p>
  185. * -> <p>^</p>
  186. *
  187. * <p><strong>foo</strong>^<strong>bar</strong>bar</p>
  188. * -> <p><strong>foo^bar<strong>bar</p>
  189. *
  190. * <p><strong>foo</strong><em>^</em><strong>bar</strong>bar</p>
  191. * -> <p><strong>foo^bar<strong>bar</p>
  192. *
  193. * This listener should be assigned before any converter for the new selection:
  194. *
  195. * modelDispatcher.on( 'selection', clearAttributes() );
  196. *
  197. * See {@link module:engine/conversion/model-selection-to-view-converters~convertCollapsedSelection}
  198. * which do the opposite by breaking attributes in the selection position.
  199. *
  200. * @returns {Function} Selection converter.
  201. */
  202. export function clearAttributes() {
  203. return ( evt, data, consumable, conversionApi ) => {
  204. for ( let range of conversionApi.viewSelection.getRanges() ) {
  205. // Not collapsed selection should not have artifacts.
  206. if ( range.isCollapsed ) {
  207. // Position might be in the node removed by the view writer.
  208. if ( range.end.parent.document ) {
  209. viewWriter.mergeAttributes( range.start );
  210. }
  211. }
  212. }
  213. conversionApi.viewSelection.removeAllRanges();
  214. };
  215. }
  216. /**
  217. * Function factory, creates a converter that clears fake selection marking after the previous
  218. * {@link module:engine/model/selection~Selection model selection} conversion.
  219. */
  220. export function clearFakeSelection() {
  221. return ( evt, data, consumable, conversionApi ) => conversionApi.viewSelection.setFake( false );
  222. }