8
0

bindtwostepcarettoattribute.js 12 KB

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