twostepcaretmovement.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  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 DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
  9. import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
  10. import TwoStepCaretMovement from '../src/twostepcaretmovement';
  11. import Position from '@ckeditor/ckeditor5-engine/src/model/position';
  12. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  13. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  14. import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  15. import '@ckeditor/ckeditor5-core/tests/_utils/assertions/attribute';
  16. describe( 'TwoStepCaretMovement()', () => {
  17. let editor, model, emitter, selection, view, plugin;
  18. let preventDefaultSpy, evtStopSpy;
  19. testUtils.createSinonSandbox();
  20. beforeEach( () => {
  21. emitter = Object.create( DomEmitterMixin );
  22. return VirtualTestEditor.create( { plugins: [ TwoStepCaretMovement ] } ).then( newEditor => {
  23. editor = newEditor;
  24. model = editor.model;
  25. selection = model.document.selection;
  26. view = editor.editing.view;
  27. plugin = editor.plugins.get( TwoStepCaretMovement );
  28. preventDefaultSpy = sinon.spy().named( 'preventDefault' );
  29. evtStopSpy = sinon.spy().named( 'evt.stop' );
  30. editor.model.schema.extend( '$text', {
  31. allowAttributes: [ 'a', 'b', 'c' ],
  32. allowIn: '$root'
  33. } );
  34. model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  35. editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: 'a' } );
  36. editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'b', model: 'b' } );
  37. editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
  38. editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  39. plugin.registerAttribute( 'a' );
  40. } );
  41. } );
  42. afterEach( () => {
  43. return editor.destroy();
  44. } );
  45. describe( 'moving right', () => {
  46. it( 'should do nothing for unrelated attribute (at the beginning)', () => {
  47. setData( model, '[]<$text c="true">foo</$text>' );
  48. testTwoStepCaretMovement( [
  49. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 0 },
  50. '→',
  51. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 1 }
  52. ] );
  53. } );
  54. it( 'should do nothing for unrelated attribute (at the end)', () => {
  55. setData( model, '<$text c="true">foo[]</$text>' );
  56. testTwoStepCaretMovement( [
  57. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  58. '→',
  59. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 }
  60. ] );
  61. } );
  62. it( 'should "enter" the text with attribute in two steps', () => {
  63. setData( model, '<$text c="true">foo[]</$text><$text a="true" b="true">bar</$text>' );
  64. testTwoStepCaretMovement( [
  65. // Gravity is not overridden, caret is at the beginning of the text but is "outside" of the text.
  66. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  67. '→',
  68. // Gravity is overridden, caret movement is blocked, selection at the beginning but "inside" the text.
  69. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  70. '→',
  71. // Caret movement was not blocked this time (still once) so everything works normally.
  72. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 }
  73. ] );
  74. } );
  75. it( 'should "leave" the text with attribute in two steps', () => {
  76. setData( model, '<$text a="true" b="true">bar[]</$text><$text c="true">foo</$text>' );
  77. testTwoStepCaretMovement( [
  78. // Gravity is not overridden, caret is at the end of the text but is "inside" of the text.
  79. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  80. '→',
  81. // Gravity is overridden, caret movement is blocked, selection at the end but "outside" the text.
  82. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  83. '→',
  84. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 }
  85. ] );
  86. } );
  87. it( 'should use two-steps movement when between nodes with the same attribute but different value', () => {
  88. setData( model, '<$text a="1">bar[]</$text><$text a="2">foo</$text>' );
  89. testTwoStepCaretMovement( [
  90. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 3 },
  91. '→',
  92. // <$text a="1">bar</$text><$text a="2">[]foo</$text>
  93. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1, caretPosition: 3 },
  94. '→',
  95. // <$text a="1">bar</$text><$text a="2">f[]oo</$text>
  96. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 4 }
  97. ] );
  98. } );
  99. // https://github.com/ckeditor/ckeditor5/issues/937
  100. it( 'should not require two-steps between unrelated attributes inside the initial attribute', () => {
  101. setData( model, '<$text a="1">fo[]o</$text><$text a="1" b="2">bar</$text><$text a="1">baz</$text>' );
  102. testTwoStepCaretMovement( [
  103. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  104. '→',
  105. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  106. '→',
  107. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  108. '→',
  109. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  110. '→',
  111. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  112. '→',
  113. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 }
  114. ] );
  115. } );
  116. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  117. it( 'should handle passing through the only character in the block', () => {
  118. setData( model, '<$text a="1">[]x</$text>' );
  119. testTwoStepCaretMovement( [
  120. // <$text a="1">[]x</$text>
  121. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 0 },
  122. '→',
  123. // <$text a="1">x[]</$text>
  124. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 1 },
  125. '→',
  126. // <$text a="1">x</$text>[]
  127. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1, caretPosition: 1 },
  128. '→',
  129. // Stays at <$text a="1">x</$text>[]
  130. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1, caretPosition: 1 }
  131. ] );
  132. } );
  133. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  134. it( 'should handle passing through the only character in the block (no attribute in the initial selection)', () => {
  135. setData( model, '[]<$text a="1">x</$text>' );
  136. model.change( writer => writer.removeSelectionAttribute( 'a' ) );
  137. testTwoStepCaretMovement( [
  138. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  139. '→',
  140. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  141. '→',
  142. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  143. '→',
  144. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 2, evtStop: 2 },
  145. '→',
  146. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 2, evtStop: 2 }
  147. ] );
  148. } );
  149. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  150. it( 'should handle passing through the only-child with an attribute (multiple characters)', () => {
  151. setData( model, '[]<$text a="1">xyz</$text>' );
  152. testTwoStepCaretMovement( [
  153. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  154. '→',
  155. // <$text a="1">x{}yz</$text>
  156. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  157. '→',
  158. // <$text a="1">xy{}z</$text>
  159. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  160. '→',
  161. // <$text a="1">xyz{}</$text>
  162. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  163. '→',
  164. // <$text a="1">xyz</$text>{}
  165. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  166. '→',
  167. // <$text a="1">xyz</$text>{}
  168. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1 }
  169. ] );
  170. } );
  171. it( 'should handle leaving an attribute followed by another block', () => {
  172. setData( model, '<paragraph><$text a="1">foo[]</$text></paragraph><paragraph><$text b="1">bar</$text></paragraph>' );
  173. testTwoStepCaretMovement( [
  174. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  175. '→',
  176. // <paragraph><$text a="1">bar</$text>[]</paragraph><paragraph>foo</paragraph>
  177. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  178. '→',
  179. // <paragraph><$text a="1">bar</$text></paragraph><paragraph>f[]oo</paragraph>
  180. { selectionAttributes: [ 'b' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  181. '→',
  182. // <paragraph><$text a="1">bar</$text></paragraph><paragraph>fo[]o</paragraph>
  183. { selectionAttributes: [ 'b' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 }
  184. ] );
  185. } );
  186. } );
  187. describe( 'moving left', () => {
  188. it( 'should "enter" the text with attribute in two steps', () => {
  189. setData( model, '<$text>foo</$text><$text a="true" b="true">bar</$text><$text c="true">b[]iz</$text>' );
  190. testTwoStepCaretMovement( [
  191. // Gravity is not overridden, caret is a one character after the and of the text.
  192. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  193. '←',
  194. // Caret movement was not blocked but the gravity is overridden.
  195. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 0, evtStop: 0 },
  196. '←',
  197. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  198. '←',
  199. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 }
  200. ] );
  201. } );
  202. it( 'should "leave" the text with attribute in two steps', () => {
  203. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  204. testTwoStepCaretMovement( [
  205. // Gravity is not overridden, caret is a one character after the beginning of the text.
  206. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  207. '←',
  208. // Caret movement was not blocked.
  209. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: true, preventDefault: 0, evtStop: 0 },
  210. '←',
  211. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  212. '←',
  213. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 }
  214. ] );
  215. } );
  216. it( 'should do nothing for unrelated attribute (at the beginning)', () => {
  217. setData( model, '<$text c="true">[]foo</$text>' );
  218. testTwoStepCaretMovement( [
  219. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  220. '←',
  221. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  222. '←',
  223. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 }
  224. ] );
  225. } );
  226. it( 'should do nothing for unrelated attribute (at the end)', () => {
  227. setData( model, '<$text c="true">foo</$text>[]' );
  228. testTwoStepCaretMovement( [
  229. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  230. '←',
  231. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  232. '←',
  233. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 }
  234. ] );
  235. } );
  236. it( 'should do nothing when caret is at the beginning of block element', () => {
  237. setData( model, '[]foo', { lastRangeBackward: true } );
  238. testTwoStepCaretMovement( [
  239. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  240. '←',
  241. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 }
  242. ] );
  243. } );
  244. it( 'should require two-steps movement when caret goes between text node with the same attribute but different value', () => {
  245. setData( model, '<$text a="2">foo</$text><$text a="1">b[]ar</$text>' );
  246. testTwoStepCaretMovement( [
  247. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 4 },
  248. '←',
  249. // <$text a="2">foo</$text><$text a="1">[]bar</$text>
  250. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStop: 0, caretPosition: 3 },
  251. '←',
  252. // <$text a="2">foo[]</$text><$text a="1">bar</$text>
  253. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 3 }
  254. ] );
  255. expect( selection ).to.have.attribute( 'a', 2 );
  256. testTwoStepCaretMovement( [
  257. '←',
  258. // <$text a="2">fo[]o</$text><$text a="1">bar</$text>
  259. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 2 }
  260. ] );
  261. } );
  262. // https://github.com/ckeditor/ckeditor5/issues/937
  263. it( 'should not require two-steps between unrelated attributes inside the initial attribute', () => {
  264. setData( model, '<$text a="1">foo</$text><$text a="1" b="2">bar</$text><$text a="1">b[]az</$text>' );
  265. testTwoStepCaretMovement( [
  266. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  267. '←',
  268. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  269. '←',
  270. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  271. '←',
  272. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  273. '←',
  274. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 }
  275. ] );
  276. } );
  277. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  278. it( 'should handle passing through the only-child with an attribute (single character)', () => {
  279. setData( model, '<$text a="1">x</$text>[]' );
  280. testTwoStepCaretMovement( [
  281. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 1 },
  282. '←',
  283. // <$text a="1">{}x</$text>
  284. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStop: 0, caretPosition: 0 },
  285. '←',
  286. // {}<$text a="1">x</$text> (because it's a first-child)
  287. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 0 },
  288. '←',
  289. // {}<$text a="1">x</$text>
  290. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 0 }
  291. ] );
  292. } );
  293. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  294. it( 'should handle passing through the only character in the block (no attribute in the initial selection)', () => {
  295. setData( model, '<$text a="1">x</$text>[]' );
  296. model.change( writer => writer.removeSelectionAttribute( 'a' ) );
  297. testTwoStepCaretMovement( [
  298. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  299. '←',
  300. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  301. '←',
  302. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  303. '←',
  304. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStop: 2 },
  305. '←',
  306. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStop: 2 }
  307. ] );
  308. } );
  309. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  310. it( 'should handle passing through the only-child with an attribute (single character, text before)', () => {
  311. setData( model, 'abc<$text a="1">x</$text>[]' );
  312. testTwoStepCaretMovement( [
  313. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  314. '←',
  315. // abc<$text a="1">{}x</$text>
  316. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStop: 0 },
  317. '←',
  318. // abc{}<$text a="1">x</$text> (because it's a first-child)
  319. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  320. '←',
  321. // abc{}<$text a="1">x</$text>
  322. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1 }
  323. ] );
  324. } );
  325. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  326. it( 'should handle passing through the only-child with an attribute (multiple characters)', () => {
  327. setData( model, '<$text a="1">xyz</$text>[]' );
  328. testTwoStepCaretMovement( [
  329. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  330. '←',
  331. // <$text a="1">xy{}z</$text>
  332. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  333. '←',
  334. // <$text a="1">x{}yz</$text>
  335. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  336. '←',
  337. // <$text a="1">{}xyz</$text>
  338. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStop: 0 },
  339. '←',
  340. // {}<$text a="1">xyz</$text>
  341. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  342. '←',
  343. // {}<$text a="1">xyz</$text>
  344. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1 }
  345. ] );
  346. } );
  347. it( 'should handle leaving an attribute preceded by another block', () => {
  348. setData( model, '<paragraph><$text b="1">foo</$text></paragraph><paragraph><$text a="1">[]bar</$text></paragraph>' );
  349. testTwoStepCaretMovement( [
  350. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: [ 1, 0 ] },
  351. '←',
  352. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: [ 1, 0 ] },
  353. '←',
  354. { selectionAttributes: [ 'b' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: [ 0, 3 ] }
  355. ] );
  356. } );
  357. } );
  358. describe( 'moving and typing around the attribute', () => {
  359. it( 'should handle typing after the attribute', () => {
  360. setData( model, '<$text a="1">x[]</$text>' );
  361. testTwoStepCaretMovement( [
  362. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 1 },
  363. 'y',
  364. // <$text a="1">xy[]</$text>
  365. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 2 },
  366. '→',
  367. // <$text a="1">xy</$text>[]
  368. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1, caretPosition: 2 },
  369. 'z',
  370. // <$text a="1">xy</$text>z[]
  371. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 3 },
  372. '←',
  373. // <$text a="1">xy</$text>[]z
  374. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1, caretPosition: 2 },
  375. '←',
  376. // <$text a="1">xy[]</$text>z
  377. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStop: 2, caretPosition: 2 },
  378. 'w',
  379. // <$text a="1">xyw[]</$text>
  380. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStop: 2, caretPosition: 3 }
  381. ] );
  382. } );
  383. it( 'should handle typing before the attribute', () => {
  384. setData( model, '<$text a="1">[]x</$text>' );
  385. testTwoStepCaretMovement( [
  386. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  387. '←',
  388. // []<$text a="1">x</$text>
  389. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  390. 'z',
  391. // z[]<$text a="1">x</$text>
  392. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  393. 'x',
  394. // zx[]<$text a="1">x</$text>
  395. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  396. '→',
  397. // zx<$text a="1">[]x</$text>
  398. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 2, evtStop: 2 },
  399. 'a',
  400. // zx<$text a="1">a[]x</$text>
  401. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStop: 2 }
  402. ] );
  403. } );
  404. // https://github.com/ckeditor/ckeditor5-engine/issues/1346
  405. // https://github.com/ckeditor/ckeditor5/issues/946
  406. it( 'should correctly re-renter the attribute', () => {
  407. setData( model, 'fo[]o <$text a="1">bar</$text>' );
  408. testTwoStepCaretMovement( [
  409. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  410. '→',
  411. // foo[] <$text a="1">bar</$text>
  412. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  413. '→',
  414. // foo []<$text a="1">bar</$text>
  415. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  416. '→',
  417. // foo <$text a="1">[]bar</$text>
  418. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  419. '→',
  420. // foo <$text a="1">b[]ar</$text>
  421. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  422. '←',
  423. // foo <$text a="1">[]bar</$text>
  424. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  425. '←',
  426. // foo []<$text a="1">bar</$text>
  427. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStop: 2 },
  428. '←',
  429. // foo[] <$text a="1">bar</$text>
  430. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStop: 2 },
  431. '←',
  432. // fo[]o <$text a="1">bar</$text>
  433. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStop: 2 }
  434. ] );
  435. } );
  436. // https://github.com/ckeditor/ckeditor5/issues/922
  437. it( 'should not lose the new attribute when typing (after)', () => {
  438. setData( model, '<$text a="1">x[]</$text>' );
  439. testTwoStepCaretMovement( [
  440. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  441. '→',
  442. // <$text a="1">x</$text>[]
  443. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1 }
  444. ] );
  445. model.change( writer => {
  446. writer.setSelectionAttribute( 'b', 1 );
  447. } );
  448. // <$text a="1">x</$text><$text b="1">[]</$text>
  449. expect( selection ).to.have.property( 'isGravityOverridden', true );
  450. expect( getSelectionAttributesArray( selection ) ).to.have.members( [ 'b' ] );
  451. model.change( writer => {
  452. writer.insertText( 'yz', selection.getAttributes(), selection.getFirstPosition() );
  453. } );
  454. // <$text a="1">x</$text><$text b="1">yz[]</$text>
  455. expect( selection ).to.have.property( 'isGravityOverridden', false );
  456. expect( getSelectionAttributesArray( selection ) ).to.have.members( [ 'b' ] );
  457. } );
  458. // https://github.com/ckeditor/ckeditor5/issues/922
  459. it( 'should not lose the new attribute when typing (before)', () => {
  460. setData( model, '<$text a="1">[]x</$text>' );
  461. testTwoStepCaretMovement( [
  462. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  463. '←',
  464. // []<$text a="1">x</$text>
  465. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1 }
  466. ] );
  467. model.change( writer => {
  468. writer.setSelectionAttribute( 'b', 1 );
  469. } );
  470. // <$text b="1">[]</$text><$text a="1">x</$text>
  471. expect( selection ).to.have.property( 'isGravityOverridden', false );
  472. expect( getSelectionAttributesArray( selection ) ).to.have.members( [ 'b' ] );
  473. model.change( writer => {
  474. writer.insertText( 'yz', selection.getAttributes(), selection.getFirstPosition() );
  475. } );
  476. // <$text b="1">yz[]</$text><$text a="1">x</$text>
  477. expect( selection ).to.have.property( 'isGravityOverridden', false );
  478. expect( getSelectionAttributesArray( selection ) ).to.have.members( [ 'b' ] );
  479. } );
  480. } );
  481. describe( 'multiple attributes', () => {
  482. beforeEach( () => {
  483. plugin.registerAttribute( 'c' );
  484. } );
  485. it( 'should work with the two-step caret movement (moving right)', () => {
  486. setData( model, 'fo[]o<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux' );
  487. testTwoStepCaretMovement( [
  488. // fo[]o<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  489. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  490. '→',
  491. // foo[]<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  492. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  493. '→',
  494. // foo<$text a="true">[]foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  495. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  496. '→',
  497. '→',
  498. '→',
  499. // foo<$text a="true">foo[]</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  500. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  501. '→',
  502. // foo<$text a="true">foo</$text><$text a="true" c="true">[]bar</$text><$text c="true">baz</$text>qux
  503. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: true, preventDefault: 2, evtStop: 2 },
  504. '→',
  505. '→',
  506. '→',
  507. // foo<$text a="true">foo</$text><$text a="true" c="true">bar[]</$text><$text c="true">baz</$text>qux
  508. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 2, evtStop: 2 },
  509. '→',
  510. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">[]baz</$text>qux
  511. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 3, evtStop: 3 },
  512. '→',
  513. '→',
  514. '→',
  515. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz[]</$text>qux
  516. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 3, evtStop: 3 },
  517. '→',
  518. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>[]qux
  519. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 4, evtStop: 4 },
  520. '→',
  521. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>q[]ux
  522. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 4, evtStop: 4 }
  523. ] );
  524. } );
  525. it( 'should work with the two-step caret movement (moving left)', () => {
  526. setData( model, 'foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>q[]ux' );
  527. testTwoStepCaretMovement( [
  528. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>q[]ux
  529. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  530. '←',
  531. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>[]qux
  532. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 0, evtStop: 0 },
  533. '←',
  534. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz[]</$text>qux
  535. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1 },
  536. '←',
  537. '←',
  538. '←',
  539. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">[]baz</$text>qux
  540. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1 },
  541. '←',
  542. // foo<$text a="true">foo</$text><$text a="true" c="true">bar[]</$text><$text c="true">baz</$text>qux
  543. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 2, evtStop: 2 },
  544. '←',
  545. '←',
  546. '←',
  547. // foo<$text a="true">foo</$text><$text a="true" c="true">[]bar</$text><$text c="true">baz</$text>qux
  548. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: true, preventDefault: 2, evtStop: 2 },
  549. '←',
  550. // foo<$text a="true">foo[]</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  551. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 3, evtStop: 3 },
  552. '←',
  553. '←',
  554. '←',
  555. // foo<$text a="true">[]foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  556. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 3, evtStop: 3 },
  557. '←',
  558. // foo[]<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  559. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 4, evtStop: 4 },
  560. '←',
  561. // fo[]o<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  562. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 4, evtStop: 4 }
  563. ] );
  564. } );
  565. describe( 'when two elements ends at the same position', () => {
  566. it( 'moving the caret in should take 2 steps', () => {
  567. setData( model, 'foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text>q[]ux' );
  568. testTwoStepCaretMovement( [
  569. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text>q[]ux
  570. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 10 },
  571. '←',
  572. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text>[]qux
  573. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 0, evtStop: 0, caretPosition: 9 },
  574. '←',
  575. // foo<$text a="true">foo</$text><$text a="true" c="true">bar[]</$text>qux
  576. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 9 },
  577. '←',
  578. // foo<$text a="true">foo</$text><$text a="true" c="true">ba[]r</$text>qux
  579. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 8 }
  580. ] );
  581. } );
  582. it( 'moving the caret out should take 2 steps', () => {
  583. setData( model, 'foo<$text a="true">foo</$text><$text a="true" c="true">ba[]r</$text>qux' );
  584. testTwoStepCaretMovement( [
  585. // foo<$text a="true">foo</$text><$text a="true" c="true">ba[]r</$text>qux
  586. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 8 },
  587. '→',
  588. // foo<$text a="true">foo</$text><$text a="true" c="true">bar[]</$text>qux
  589. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 9 },
  590. '→',
  591. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text>[]qux
  592. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1, caretPosition: 9 },
  593. '→',
  594. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text>q[]ux
  595. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 10 }
  596. ] );
  597. } );
  598. } );
  599. describe( 'when two elements starts at the same position', () => {
  600. it( 'moving the caret in should take 2 steps', () => {
  601. setData( model, 'fo[]o<$text a="true" c="true">bar</$text><$text a="true">baz</$text>qux' );
  602. testTwoStepCaretMovement( [
  603. // fo[]o<$text a="true" c="true">bar</$text><$text a="true">baz</$text>qux
  604. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 2 },
  605. '→',
  606. // foo[]<$text a="true" c="true">bar</$text><$text a="true">baz</$text>qux
  607. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 3 },
  608. '→',
  609. // foo<$text a="true" c="true">[]bar</$text><$text a="true">baz</$text>qux
  610. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1, caretPosition: 3 },
  611. '→',
  612. // foo<$text a="true" c="true">b[]ar</$text><$text a="true">baz</$text>qux
  613. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 4 }
  614. ] );
  615. } );
  616. it( 'moving the caret out should take 2 steps', () => {
  617. setData( model, 'foo<$text a="true" c="true">b[]ar</$text><$text a="true">baz</$text>qux' );
  618. testTwoStepCaretMovement( [
  619. // foo<$text a="true" c="true">b[]ar</$text><$text a="true">baz</$text>qux
  620. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 4 },
  621. '←',
  622. // foo<$text a="true" c="true">[]bar</$text><$text a="true">baz</$text>qux
  623. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: true, preventDefault: 0, evtStop: 0, caretPosition: 3 },
  624. '←',
  625. // foo[]<$text a="true" c="true">bar</$text><$text a="true">baz</$text>qux
  626. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 3 },
  627. '←',
  628. // fo[]o<$text a="true" c="true">bar</$text><$text a="true">baz</$text>qux
  629. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 2 }
  630. ] );
  631. } );
  632. } );
  633. } );
  634. describe( 'mouse', () => {
  635. it( 'should not override gravity when selection is placed at the beginning of text', () => {
  636. setData( model, '<$text a="true">[]foo</$text>' );
  637. expect( selection ).to.have.property( 'isGravityOverridden', false );
  638. } );
  639. it( 'should not override gravity when selection is placed at the end of text', () => {
  640. setData( model, '<$text a="true">foo[]</$text>' );
  641. expect( selection ).to.have.property( 'isGravityOverridden', false );
  642. } );
  643. } );
  644. it( 'should listen with the high+1 priority on view.document#keydown', () => {
  645. const highestPrioritySpy = sinon.spy().named( 'highestPrioritySpy' );
  646. const highPrioritySpy = sinon.spy().named( 'highPrioritySpy' );
  647. const normalPrioritySpy = sinon.spy().named( 'normalPrioritySpy' );
  648. setData( model, '<$text c="true">foo[]</$text><$text a="true" b="true">bar</$text>' );
  649. emitter.listenTo( view.document, 'keydown', highestPrioritySpy, { priority: 'highest' } );
  650. emitter.listenTo( view.document, 'keydown', highPrioritySpy, { priority: 'high' } );
  651. emitter.listenTo( view.document, 'keydown', normalPrioritySpy, { priority: 'normal' } );
  652. fireKeyDownEvent( {
  653. keyCode: keyCodes.arrowright,
  654. preventDefault: preventDefaultSpy
  655. } );
  656. expect( highestPrioritySpy ).to.be.calledOnce;
  657. expect( preventDefaultSpy ).to.be.calledImmediatelyAfter( highestPrioritySpy );
  658. expect( highPrioritySpy ).not.to.be.called;
  659. expect( normalPrioritySpy ).not.to.be.called;
  660. } );
  661. it( 'should do nothing when key other then arrow left and right is pressed', () => {
  662. setData( model, '<$text a="true">foo[]</$text>' );
  663. expect( () => {
  664. fireKeyDownEvent( { keyCode: keyCodes.arrowup } );
  665. } ).to.not.throw();
  666. } );
  667. it( 'should do nothing for non-collapsed selection', () => {
  668. setData( model, '<$text c="true">fo[o]</$text><$text a="true" b="true">bar</$text>' );
  669. fireKeyDownEvent( { keyCode: keyCodes.arrowright } );
  670. expect( selection ).to.have.property( 'isGravityOverridden', false );
  671. } );
  672. it( 'should do nothing when shift key is pressed', () => {
  673. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  674. fireKeyDownEvent( {
  675. keyCode: keyCodes.arrowleft,
  676. shiftKey: true
  677. } );
  678. expect( selection ).to.have.property( 'isGravityOverridden', false );
  679. } );
  680. it( 'should do nothing when alt key is pressed', () => {
  681. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  682. fireKeyDownEvent( {
  683. keyCode: keyCodes.arrowleft,
  684. altKey: true
  685. } );
  686. expect( selection ).to.have.property( 'isGravityOverridden', false );
  687. } );
  688. it( 'should do nothing when ctrl key is pressed', () => {
  689. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  690. fireKeyDownEvent( {
  691. keyCode: keyCodes.arrowleft,
  692. ctrlKey: true
  693. } );
  694. expect( selection ).to.have.property( 'isGravityOverridden', false );
  695. } );
  696. it( 'should do nothing when the not a direct selection change but at the attribute boundary', () => {
  697. setData( model, '<$text a="true">foo[]</$text>bar' );
  698. testTwoStepCaretMovement( [
  699. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0 },
  700. '→',
  701. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStop: 1 }
  702. ] );
  703. // Simulate an external text insertion BEFORE the user selection to trigger #change:range.
  704. model.enqueueChange( 'transparent', writer => {
  705. writer.insertText( 'x', selection.getFirstPosition().getShiftedBy( -2 ) );
  706. } );
  707. expect( selection ).to.have.property( 'isGravityOverridden', true );
  708. expect( getSelectionAttributesArray( selection ) ).to.have.members( [] );
  709. } );
  710. describe( 'right–to–left content', () => {
  711. it( 'should use the opposite helper methods (RTL content direction)', () => {
  712. // let model;
  713. return VirtualTestEditor
  714. .create( {
  715. plugins: [ TwoStepCaretMovement ],
  716. language: {
  717. content: 'he'
  718. }
  719. } )
  720. .then( newEditor => {
  721. model = newEditor.model;
  722. selection = model.document.selection;
  723. view = newEditor.editing.view;
  724. newEditor.model.schema.extend( '$text', {
  725. allowAttributes: [ 'a', 'b', 'c' ],
  726. allowIn: '$root'
  727. } );
  728. model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  729. newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: 'a' } );
  730. newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'b', model: 'b' } );
  731. newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
  732. newEditor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  733. newEditor.plugins.get( TwoStepCaretMovement ).registerAttribute( 'a' );
  734. return newEditor;
  735. } )
  736. .then( newEditor => {
  737. setData( model, '<$text>לזה[]</$text><$text a="true">שיוצג</$text>' );
  738. testTwoStepCaretMovement( [
  739. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 3 },
  740. '←',
  741. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStop: 1, caretPosition: 3 }
  742. ], 'rtl' );
  743. preventDefaultSpy.resetHistory();
  744. evtStopSpy.resetHistory();
  745. setData( model, '<$text>לזה</$text><$text a="true">ש[]יוצג</$text>' );
  746. testTwoStepCaretMovement( [
  747. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStop: 0, caretPosition: 4 },
  748. '→',
  749. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStop: 0, caretPosition: 3 },
  750. '→',
  751. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 3 },
  752. '→',
  753. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStop: 1, caretPosition: 2 }
  754. ], 'rtl' );
  755. return newEditor.destroy();
  756. } );
  757. } );
  758. } );
  759. const keyMap = {
  760. '→': 'arrowright',
  761. '←': 'arrowleft'
  762. };
  763. function fireKeyDownEvent( options ) {
  764. const eventInfo = new EventInfo( view.document, 'keydown' );
  765. const eventData = new DomEventData( view.document, {
  766. target: document.body
  767. }, options );
  768. sinon.stub( eventInfo, 'stop' ).callsFake( evtStopSpy );
  769. view.document.fire( eventInfo, eventData );
  770. }
  771. function getSelectionAttributesArray( selection ) {
  772. return Array.from( selection.getAttributeKeys() );
  773. }
  774. function testTwoStepCaretMovement( scenario, rtl ) {
  775. for ( const step of scenario ) {
  776. if ( typeof step == 'string' ) {
  777. // An arrow key pressed. Fire the view event and update the model selection.
  778. if ( keyMap[ step ] ) {
  779. let preventDefaultCalled;
  780. fireKeyDownEvent( {
  781. keyCode: keyCodes[ keyMap[ step ] ],
  782. preventDefault: () => {
  783. preventDefaultSpy();
  784. preventDefaultCalled = true;
  785. }
  786. } );
  787. const position = selection.getFirstPosition();
  788. if ( !preventDefaultCalled ) {
  789. if ( step == '→' && !rtl || step == '←' && rtl ) {
  790. model.change( writer => {
  791. if ( position.isAtEnd ) {
  792. const nextBlock = position.parent.nextSibling;
  793. if ( nextBlock ) {
  794. writer.setSelection( Position._createAt( nextBlock, 0 ) );
  795. }
  796. } else {
  797. writer.setSelection( selection.getFirstPosition().getShiftedBy( 1 ) );
  798. }
  799. } );
  800. } else if ( step == '←' && !rtl || step == '→' && rtl ) {
  801. model.change( writer => {
  802. if ( position.isAtStart ) {
  803. const previousBlock = position.parent.previousSibling;
  804. if ( previousBlock ) {
  805. writer.setSelection( Position._createAt( previousBlock, 'end' ) );
  806. }
  807. } else {
  808. writer.setSelection( selection.getFirstPosition().getShiftedBy( -1 ) );
  809. }
  810. } );
  811. }
  812. }
  813. }
  814. // A regular key pressed. Type some text in the model.
  815. else {
  816. model.change( writer => {
  817. writer.insertText( step, selection.getAttributes(), selection.getFirstPosition() );
  818. } );
  819. }
  820. }
  821. // If not a key, then it's an assertion.
  822. else {
  823. const stepIndex = scenario.indexOf( step );
  824. const stepString = `in step #${ stepIndex }`;
  825. let caretPosition = step.caretPosition;
  826. if ( caretPosition !== undefined ) {
  827. // Normalize position
  828. caretPosition = Array.isArray( step.caretPosition ) ? caretPosition : [ caretPosition ];
  829. expect( selection.getFirstPosition(), `in step #${ stepIndex }, selection's first position` )
  830. .to.have.deep.property( 'path', caretPosition );
  831. }
  832. expect( getSelectionAttributesArray( selection ), `${ stepString }, selection's attributes` )
  833. .to.have.members( step.selectionAttributes );
  834. expect( selection, `${ stepString }, selection's gravity` )
  835. .to.have.property( 'isGravityOverridden', step.isGravityOverridden );
  836. expect( preventDefaultSpy, stepString ).to.have.callCount( step.preventDefault );
  837. expect( evtStopSpy, stepString ).to.have.callCount( step.evtStop );
  838. }
  839. }
  840. }
  841. } );