bindtwostepcarettoattribute.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document */
  6. import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
  7. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  8. import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
  9. import Position from '@ckeditor/ckeditor5-engine/src/model/position';
  10. import Range from '@ckeditor/ckeditor5-engine/src/model/range';
  11. import { upcastElementToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
  12. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  13. import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
  14. import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  15. describe( 'bindTwoStepCaretToAttribute()', () => {
  16. let editor, model, emitter, selection, viewDoc, preventDefaultSpy;
  17. beforeEach( () => {
  18. emitter = Object.create( DomEmitterMixin );
  19. return VirtualTestEditor.create().then( newEditor => {
  20. editor = newEditor;
  21. model = editor.model;
  22. selection = model.document.selection;
  23. viewDoc = editor.editing.view;
  24. preventDefaultSpy = sinon.spy();
  25. editor.model.schema.extend( '$text', {
  26. allowAttributes: [ 'a', 'b', 'c' ],
  27. allowIn: '$root'
  28. } );
  29. editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'a', model: 'a' } ) );
  30. editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'b', model: 'b' } ) );
  31. editor.conversion.for( 'upcast' ).add( upcastElementToAttribute( { view: 'c', model: 'c' } ) );
  32. bindTwoStepCaretToAttribute( editor, emitter, 'a' );
  33. } );
  34. } );
  35. afterEach( () => {
  36. return editor.destroy();
  37. } );
  38. describe( 'moving right', () => {
  39. it( 'should "enter" the text with attribute in two steps', () => {
  40. setData( model, '<$text c="true">foo[]</$text><$text a="true" b="true">bar</$text>' );
  41. // Firing keyup event will simulate that caret is here as a result of right arrow key press.
  42. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowright } ) );
  43. // Gravity is not overridden, caret is at the beginning of the text but is "outside" of the text.
  44. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  45. expect( selection.isGravityOverridden ).to.false;
  46. // Press right key.
  47. viewDoc.fire( 'keydown', getEventData( {
  48. keyCode: keyCodes.arrowright,
  49. preventDefault: preventDefaultSpy
  50. } ) );
  51. // Gravity is overridden, caret movement is blocked, selection at the beginning but "inside" the text.
  52. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  53. expect( selection.isGravityOverridden ).to.true;
  54. sinon.assert.calledOnce( preventDefaultSpy );
  55. // Press right key.
  56. viewDoc.fire( 'keydown', getEventData( {
  57. keyCode: keyCodes.arrowright,
  58. preventDefault: preventDefaultSpy
  59. } ) );
  60. // Caret movement was not blocked this time (still once) so everything works normally.
  61. sinon.assert.calledOnce( preventDefaultSpy );
  62. } );
  63. it( 'should "leave" the text with attribute in two steps', () => {
  64. setData( model, '<$text a="true" b="true">bar[]</$text><$text c="true">foo</$text>' );
  65. // Firing keyup event will simulate that caret is here as a result of right arrow key press.
  66. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowright } ) );
  67. // Gravity is not overridden, caret is at the end of the text but is "inside" of the text.
  68. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  69. expect( selection.isGravityOverridden ).to.false;
  70. // Press right key.
  71. viewDoc.fire( 'keydown', getEventData( {
  72. keyCode: keyCodes.arrowright,
  73. preventDefault: preventDefaultSpy
  74. } ) );
  75. // Gravity is overridden, caret movement is blocked, selection at the beginning but "outside" the text.
  76. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  77. expect( selection.isGravityOverridden ).to.true;
  78. sinon.assert.calledOnce( preventDefaultSpy );
  79. // Press right key.
  80. viewDoc.fire( 'keydown', getEventData( {
  81. keyCode: keyCodes.arrowright,
  82. preventDefault: preventDefaultSpy
  83. } ) );
  84. // Caret movement was not blocked this time (still once) so everything works normally.
  85. sinon.assert.calledOnce( preventDefaultSpy );
  86. } );
  87. it( 'should do nothing for not bound attribute (at the beginning)', () => {
  88. setData( model, '[]<$text c="true">foo</$text>' );
  89. // Firing keyup event will simulate that caret is here as a result of right arrow key press.
  90. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowright } ) );
  91. viewDoc.fire( 'keydown', getEventData( {
  92. keyCode: keyCodes.arrowright,
  93. preventDefault: preventDefaultSpy
  94. } ) );
  95. sinon.assert.notCalled( preventDefaultSpy );
  96. expect( selection.isGravityOverridden ).to.false;
  97. } );
  98. it( 'should do nothing for not bound attribute (at the end)', () => {
  99. setData( model, '<$text c="true">foo[]</$text>' );
  100. // Firing keyup event will simulate that caret is here as a result of right arrow key press.
  101. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowright } ) );
  102. viewDoc.fire( 'keydown', getEventData( {
  103. keyCode: keyCodes.arrowright,
  104. preventDefault: preventDefaultSpy
  105. } ) );
  106. sinon.assert.notCalled( preventDefaultSpy );
  107. expect( selection.isGravityOverridden ).to.false;
  108. } );
  109. it( 'should do nothing for non-collapsed selection', () => {
  110. setData( model, '<$text c="true">fo[o]</$text><$text a="true" b="true">bar</$text>' );
  111. // Firing keyup event will simulate that caret is here as a result of left arrow key press.
  112. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  113. viewDoc.fire( 'keydown', getEventData( { keyCode: keyCodes.arrowright } ) );
  114. expect( selection.isGravityOverridden ).to.false;
  115. } );
  116. } );
  117. describe( 'moving left', () => {
  118. it( 'should "enter" the text with attribute in two steps', () => {
  119. setData( model, '<$text>foo</$text><$text a="true" b="true">bar[]</$text><$text c="true">biz</$text>' );
  120. // Firing keyup event will simulate that caret is here as a result of left arrow key press.
  121. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  122. // Gravity is overridden, caret is at the end of the text but is "outside" of the text.
  123. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  124. expect( selection.isGravityOverridden ).to.true;
  125. // Press left key.
  126. viewDoc.fire( 'keydown', getEventData( {
  127. keyCode: keyCodes.arrowleft,
  128. preventDefault: preventDefaultSpy
  129. } ) );
  130. // Moving left needs additional keyup event to check that everything is right.
  131. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  132. // Caret movement was blocked but now is "inside" the text.
  133. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  134. expect( selection.isGravityOverridden ).to.false;
  135. sinon.assert.calledOnce( preventDefaultSpy );
  136. // Press left key.
  137. viewDoc.fire( 'keydown', getEventData( {
  138. keyCode: keyCodes.arrowleft,
  139. preventDefault: preventDefaultSpy
  140. } ) );
  141. // Moving left needs additional keyup event to check that everything is right.
  142. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  143. // Caret movement was not blocked this time (still once) so everything works normally.
  144. sinon.assert.calledOnce( preventDefaultSpy );
  145. } );
  146. it( 'should "leave" the text with attribute in two steps', () => {
  147. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">[]bar</$text>' );
  148. // Firing keyup event will simulate that caret is here as a result of left arrow key press.
  149. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  150. // Gravity is overridden, caret is at the beginning of the text and is "inside" of the text.
  151. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  152. expect( selection.isGravityOverridden ).to.true;
  153. // Press left key.
  154. viewDoc.fire( 'keydown', getEventData( {
  155. keyCode: keyCodes.arrowleft,
  156. preventDefault: preventDefaultSpy
  157. } ) );
  158. // Moving left needs additional keyup event to check that everything is right.
  159. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  160. // Gravity is not overridden, caret movement was blocked but now is "outside" the text.
  161. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  162. expect( selection.isGravityOverridden ).to.false;
  163. sinon.assert.calledOnce( preventDefaultSpy );
  164. // Press left key.
  165. viewDoc.fire( 'keydown', getEventData( {
  166. keyCode: keyCodes.arrowleft,
  167. preventDefault: preventDefaultSpy
  168. } ) );
  169. // Moving left needs additional keyup event to check that everything is right.
  170. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  171. // Caret movement was not blocked this time (still once) so everything works normally.
  172. sinon.assert.calledOnce( preventDefaultSpy );
  173. } );
  174. it( 'should do nothing for not bound attribute (at the beginning)', () => {
  175. setData( model, '<$text c="true">[]foo</$text>' );
  176. // Firing keyup event will simulate that caret is here as a result of left arrow key press.
  177. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  178. viewDoc.fire( 'keydown', getEventData( {
  179. keyCode: keyCodes.arrowright,
  180. preventDefault: preventDefaultSpy
  181. } ) );
  182. // Moving left needs additional keyup event to check that everything is right.
  183. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  184. sinon.assert.notCalled( preventDefaultSpy );
  185. expect( selection.isGravityOverridden ).to.false;
  186. } );
  187. it( 'should do nothing for not bound attribute (at the end)', () => {
  188. setData( model, '<$text c="true">foo</$text>[]' );
  189. // Firing keyup event will simulate that caret is here as a result of left arrow key press.
  190. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  191. viewDoc.fire( 'keydown', getEventData( {
  192. keyCode: keyCodes.arrowright,
  193. preventDefault: preventDefaultSpy
  194. } ) );
  195. // Moving left needs additional keyup event to check that everything is right.
  196. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  197. sinon.assert.notCalled( preventDefaultSpy );
  198. expect( selection.isGravityOverridden ).to.false;
  199. } );
  200. it( 'should do nothing for non-collapsed selection', () => {
  201. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">[b]ar</$text>', { lastRangeBackward: true } );
  202. // Firing keyup event will simulate that caret is here as a result of left arrow key press.
  203. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  204. expect( selection.isGravityOverridden ).to.false;
  205. } );
  206. // There is no need to test it while moving right, because moving right does not use additional state.
  207. it( 'should work when external changes are made meanwhile', () => {
  208. setData( model, '<$text>foo</$text><$text a="true" b="true">bar[]</$text><$text c="true">biz</$text>' );
  209. // Firing keyup event will simulate that caret is here as a result of left arrow key press.
  210. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  211. // Gravity is overridden, caret is at the end of the text but is "outside" of the text.
  212. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  213. expect( selection.isGravityOverridden ).to.true;
  214. // External changes.
  215. model.change( writer => {
  216. writer.insertText( 'abc', Position.createAt( editor.model.document.getRoot() ) );
  217. } );
  218. // Press left key.
  219. viewDoc.fire( 'keydown', getEventData( {
  220. keyCode: keyCodes.arrowleft,
  221. preventDefault: preventDefaultSpy
  222. } ) );
  223. // Moving left needs additional keyup event to check that everything is right.
  224. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  225. // Caret movement was blocked but now is "inside" the text.
  226. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  227. expect( selection.isGravityOverridden ).to.false;
  228. sinon.assert.calledOnce( preventDefaultSpy );
  229. } );
  230. // There is no need to test it while moving right, because moving right does not use additional state.
  231. it( 'should not block caret when while doing two steps movement and text is removed by external change', () => {
  232. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">[]bar</$text>biz' );
  233. // Firing keyup event will simulate that caret is here as a result of left arrow key press.
  234. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  235. // Gravity is overridden, caret is at the beginning of the text and is "inside" of the text.
  236. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  237. expect( selection.isGravityOverridden ).to.true;
  238. // External changes.
  239. model.change( writer => {
  240. writer.remove( Range.createFromPositionAndShift( new Position( editor.model.document.getRoot(), [ 2 ] ), 5 ) );
  241. } );
  242. // Press left key.
  243. viewDoc.fire( 'keydown', getEventData( {
  244. keyCode: keyCodes.arrowleft,
  245. preventDefault: preventDefaultSpy
  246. } ) );
  247. // Moving left needs additional keyup event to check that everything is right.
  248. viewDoc.fire( 'keyup', getEventData( { keyCode: keyCodes.arrowleft } ) );
  249. sinon.assert.notCalled( preventDefaultSpy );
  250. } );
  251. } );
  252. describe( 'mouse', () => {
  253. it( 'should not override gravity when selection is placed at the beginning of text', () => {
  254. setData( model, '<$text a="true">[]foo</$text>' );
  255. expect( selection.isGravityOverridden ).to.false;
  256. } );
  257. it( 'should not override gravity when selection is placed at the end of text', () => {
  258. setData( model, '<$text a="true">foo[]</$text>' );
  259. expect( selection.isGravityOverridden ).to.false;
  260. } );
  261. } );
  262. it( 'should do nothing when key other then arrow left and right is pressed', () => {
  263. setData( model, '<$text a="true">foo[]</$text>' );
  264. expect( () => {
  265. viewDoc.fire( 'keydown', getEventData( { keyCode: keyCodes.arrowup } ) );
  266. } ).to.not.throw();
  267. } );
  268. function getEventData( data ) {
  269. data.target = document.body;
  270. return new DomEventData( viewDoc, data, { keyCode: data.keyCode } );
  271. }
  272. } );