8
0

bindtwostepcarettoattribute.js 11 KB

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