bindtwostepcarettoattribute.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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 DomEventData from '../../src/view/observer/domeventdata';
  12. import { upcastElementToAttribute } from '../../src/conversion/upcast-converters';
  13. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  14. import { setData } from '../../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.document;
  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.editing.view, editor.model, 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. // Gravity is not overridden, caret is at the beginning of the text but is "outside" of the text.
  42. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  43. expect( selection.isGravityOverridden ).to.false;
  44. // Press right key.
  45. viewDoc.fire( 'keydown', getEventData( {
  46. keyCode: keyCodes.arrowright,
  47. preventDefault: preventDefaultSpy
  48. } ) );
  49. // Gravity is overridden, caret movement is blocked, selection at the beginning but "inside" the text.
  50. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  51. expect( selection.isGravityOverridden ).to.true;
  52. sinon.assert.calledOnce( preventDefaultSpy );
  53. // Press right key.
  54. viewDoc.fire( 'keydown', getEventData( {
  55. keyCode: keyCodes.arrowright,
  56. preventDefault: preventDefaultSpy
  57. } ) );
  58. // Caret movement was not blocked this time (still once) so everything works normally.
  59. sinon.assert.calledOnce( preventDefaultSpy );
  60. } );
  61. it( 'should "leave" the text with attribute in two steps', () => {
  62. setData( model, '<$text a="true" b="true">bar[]</$text><$text c="true">foo</$text>' );
  63. // Gravity is not overridden, caret is at the end of the text but is "inside" of the text.
  64. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  65. expect( selection.isGravityOverridden ).to.false;
  66. // Press right key.
  67. viewDoc.fire( 'keydown', getEventData( {
  68. keyCode: keyCodes.arrowright,
  69. preventDefault: preventDefaultSpy
  70. } ) );
  71. // Gravity is overridden, caret movement is blocked, selection at the end but "outside" the text.
  72. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  73. expect( selection.isGravityOverridden ).to.true;
  74. sinon.assert.calledOnce( preventDefaultSpy );
  75. // Press right key.
  76. viewDoc.fire( 'keydown', getEventData( {
  77. keyCode: keyCodes.arrowright,
  78. preventDefault: preventDefaultSpy
  79. } ) );
  80. // Caret movement was not blocked this time (still once) so everything works normally.
  81. sinon.assert.calledOnce( preventDefaultSpy );
  82. } );
  83. it( 'should do nothing for not bound attribute (at the beginning)', () => {
  84. setData( model, '[]<$text c="true">foo</$text>' );
  85. viewDoc.fire( 'keydown', getEventData( {
  86. keyCode: keyCodes.arrowright,
  87. preventDefault: preventDefaultSpy
  88. } ) );
  89. sinon.assert.notCalled( preventDefaultSpy );
  90. expect( selection.isGravityOverridden ).to.false;
  91. } );
  92. it( 'should do nothing for not bound attribute (at the end)', () => {
  93. setData( model, '<$text c="true">foo[]</$text>' );
  94. viewDoc.fire( 'keydown', getEventData( {
  95. keyCode: keyCodes.arrowright,
  96. preventDefault: preventDefaultSpy
  97. } ) );
  98. sinon.assert.notCalled( preventDefaultSpy );
  99. expect( selection.isGravityOverridden ).to.false;
  100. } );
  101. it( 'should require two-steps movement when caret goes between text node with the same attribute but different value', () => {
  102. setData( model, '<$text a="1">bar[]</$text><$text a="2">foo</$text>' );
  103. // Gravity is not overridden.
  104. expect( selection.isGravityOverridden ).to.false;
  105. // Press right key.
  106. viewDoc.fire( 'keydown', getEventData( {
  107. keyCode: keyCodes.arrowright,
  108. preventDefault: preventDefaultSpy
  109. } ) );
  110. // Gravity is overridden, caret movement is blocked.
  111. expect( selection.isGravityOverridden ).to.true;
  112. sinon.assert.calledOnce( preventDefaultSpy );
  113. } );
  114. } );
  115. describe( 'moving left', () => {
  116. it( 'should "enter" the text with attribute in two steps', () => {
  117. setData( model, '<$text>foo</$text><$text a="true" b="true">bar</$text><$text c="true">b[]iz</$text>' );
  118. // Gravity is not overridden, caret is a one character after the and of the text.
  119. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  120. expect( selection.isGravityOverridden ).to.false;
  121. // Press left key.
  122. viewDoc.fire( 'keydown', getEventData( {
  123. keyCode: keyCodes.arrowleft,
  124. preventDefault: preventDefaultSpy
  125. } ) );
  126. // Caret movement was not blocked.
  127. sinon.assert.notCalled( preventDefaultSpy );
  128. // So we need to move caret one character left like it should be done in the real world.
  129. // Caret should ends up at the end of text with attribute but still outside of it.
  130. model.change( writer => writer.setSelection( new Range( new Position( model.document.getRoot(), [ 6 ] ) ) ) );
  131. // Gravity is overridden.
  132. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  133. expect( selection.isGravityOverridden ).to.true;
  134. // Press left key.
  135. viewDoc.fire( 'keydown', getEventData( {
  136. keyCode: keyCodes.arrowleft,
  137. preventDefault: preventDefaultSpy
  138. } ) );
  139. // Caret movement was blocked but now is "inside" the text.
  140. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  141. expect( selection.isGravityOverridden ).to.false;
  142. sinon.assert.calledOnce( preventDefaultSpy );
  143. // Press left key.
  144. viewDoc.fire( 'keydown', getEventData( {
  145. keyCode: keyCodes.arrowleft,
  146. preventDefault: preventDefaultSpy
  147. } ) );
  148. // Caret movement was not blocked this time (still once) so everything works normally.
  149. sinon.assert.calledOnce( preventDefaultSpy );
  150. // And again we need to move the caret like it should be done in the real world to be shure that everything is
  151. // like it should to be.
  152. model.change( writer => writer.setSelection( new Range( new Position( model.document.getRoot(), [ 5 ] ) ) ) );
  153. } );
  154. it( 'should "leave" the text with attribute in two steps', () => {
  155. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  156. // Gravity is not overridden, caret is a one character after the beginning of the text.
  157. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  158. expect( selection.isGravityOverridden ).to.false;
  159. // Press left key.
  160. viewDoc.fire( 'keydown', getEventData( {
  161. keyCode: keyCodes.arrowleft,
  162. preventDefault: preventDefaultSpy
  163. } ) );
  164. // Caret movement was not blocked.
  165. sinon.assert.notCalled( preventDefaultSpy );
  166. // So we need to move caret one character left like it should be done in the real world.
  167. // Caret should ends up at the beginning of text with attribute but still inside of it.
  168. model.change( writer => writer.setSelection( new Range( new Position( model.document.getRoot(), [ 3 ] ) ) ) );
  169. // Gravity is overridden, caret is at the beginning of the text and is "inside" of the text.
  170. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'a', 'b' ] );
  171. expect( selection.isGravityOverridden ).to.true;
  172. // Press left key.
  173. viewDoc.fire( 'keydown', getEventData( {
  174. keyCode: keyCodes.arrowleft,
  175. preventDefault: preventDefaultSpy
  176. } ) );
  177. // Gravity is not overridden, caret movement was blocked but now is "outside" the text.
  178. expect( Array.from( selection.getAttributeKeys() ) ).to.have.members( [ 'c' ] );
  179. expect( selection.isGravityOverridden ).to.false;
  180. sinon.assert.calledOnce( preventDefaultSpy );
  181. // Press left key.
  182. viewDoc.fire( 'keydown', getEventData( {
  183. keyCode: keyCodes.arrowleft,
  184. preventDefault: preventDefaultSpy
  185. } ) );
  186. // Caret movement was not blocked this time (still once) so everything works normally.
  187. sinon.assert.calledOnce( preventDefaultSpy );
  188. } );
  189. it( 'should do nothing for not bound attribute (at the beginning)', () => {
  190. setData( model, '<$text c="true">[]foo</$text>' );
  191. viewDoc.fire( 'keydown', getEventData( {
  192. keyCode: keyCodes.arrowright,
  193. preventDefault: preventDefaultSpy
  194. } ) );
  195. sinon.assert.notCalled( preventDefaultSpy );
  196. expect( selection.isGravityOverridden ).to.false;
  197. } );
  198. it( 'should do nothing for not bound attribute (at the end)', () => {
  199. setData( model, '<$text c="true">foo</$text>[]' );
  200. viewDoc.fire( 'keydown', getEventData( {
  201. keyCode: keyCodes.arrowright,
  202. preventDefault: preventDefaultSpy
  203. } ) );
  204. sinon.assert.notCalled( preventDefaultSpy );
  205. expect( selection.isGravityOverridden ).to.false;
  206. } );
  207. it( 'should do nothing when caret is at the beginning of block element', () => {
  208. setData( model, '[]foo', { lastRangeBackward: true } );
  209. expect( () => {
  210. viewDoc.fire( 'keydown', getEventData( { keyCode: keyCodes.arrowleft } ) );
  211. } ).to.not.throw();
  212. } );
  213. it( 'should require two-steps movement when caret goes between text node with the same attribute but different value', () => {
  214. setData( model, '<$text a="2">foo</$text><$text a="1">b[]ar</$text>' );
  215. // Gravity is not overridden.
  216. expect( selection.isGravityOverridden ).to.false;
  217. // Press left key.
  218. viewDoc.fire( 'keydown', getEventData( {
  219. keyCode: keyCodes.arrowleft,
  220. preventDefault: preventDefaultSpy
  221. } ) );
  222. // Gravity is overridden, caret movement was not blocked.
  223. sinon.assert.notCalled( preventDefaultSpy );
  224. expect( selection.isGravityOverridden ).to.true;
  225. } );
  226. } );
  227. describe( 'mouse', () => {
  228. it( 'should not override gravity when selection is placed at the beginning of text', () => {
  229. setData( model, '<$text a="true">[]foo</$text>' );
  230. expect( selection.isGravityOverridden ).to.false;
  231. } );
  232. it( 'should not override gravity when selection is placed at the end of text', () => {
  233. setData( model, '<$text a="true">foo[]</$text>' );
  234. expect( selection.isGravityOverridden ).to.false;
  235. } );
  236. } );
  237. it( 'should do nothing when key other then arrow left and right is pressed', () => {
  238. setData( model, '<$text a="true">foo[]</$text>' );
  239. expect( () => {
  240. viewDoc.fire( 'keydown', getEventData( { keyCode: keyCodes.arrowup } ) );
  241. } ).to.not.throw();
  242. } );
  243. it( 'should do nothing for non-collapsed selection', () => {
  244. setData( model, '<$text c="true">fo[o]</$text><$text a="true" b="true">bar</$text>' );
  245. viewDoc.fire( 'keydown', getEventData( { keyCode: keyCodes.arrowright } ) );
  246. expect( selection.isGravityOverridden ).to.false;
  247. } );
  248. function getEventData( data ) {
  249. data.target = document.body;
  250. return new DomEventData( viewDoc, data, { keyCode: data.keyCode } );
  251. }
  252. } );