bindtwostepcarettoattribute.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  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 DomEventData from '../../src/view/observer/domeventdata';
  9. import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
  10. import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
  11. import Position from '../../src/model/position';
  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, view;
  16. let preventDefaultSpy, evtStopSpy;
  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. view = editor.editing.view;
  24. preventDefaultSpy = sinon.spy();
  25. evtStopSpy = sinon.spy();
  26. editor.model.schema.extend( '$text', {
  27. allowAttributes: [ 'a', 'b', 'c' ],
  28. allowIn: '$root'
  29. } );
  30. model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
  31. editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'a', model: 'a' } );
  32. editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'b', model: 'b' } );
  33. editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
  34. editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
  35. bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'a' );
  36. } );
  37. } );
  38. afterEach( () => {
  39. return editor.destroy();
  40. } );
  41. describe( 'moving right', () => {
  42. it( 'should do nothing for unrelated attribute (at the beginning)', () => {
  43. setData( model, '[]<$text c="true">foo</$text>' );
  44. testTwoStepCaretMovement( [
  45. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  46. '→',
  47. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  48. ] );
  49. } );
  50. it( 'should do nothing for unrelated attribute (at the end)', () => {
  51. setData( model, '<$text c="true">foo[]</$text>' );
  52. testTwoStepCaretMovement( [
  53. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  54. '→',
  55. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  56. ] );
  57. } );
  58. it( 'should "enter" the text with attribute in two steps', () => {
  59. setData( model, '<$text c="true">foo[]</$text><$text a="true" b="true">bar</$text>' );
  60. testTwoStepCaretMovement( [
  61. // Gravity is not overridden, caret is at the beginning of the text but is "outside" of the text.
  62. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  63. '→',
  64. // Gravity is overridden, caret movement is blocked, selection at the beginning but "inside" the text.
  65. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  66. '→',
  67. // Caret movement was not blocked this time (still once) so everything works normally.
  68. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  69. ] );
  70. } );
  71. it( 'should "leave" the text with attribute in two steps', () => {
  72. setData( model, '<$text a="true" b="true">bar[]</$text><$text c="true">foo</$text>' );
  73. testTwoStepCaretMovement( [
  74. // Gravity is not overridden, caret is at the end of the text but is "inside" of the text.
  75. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  76. '→',
  77. // Gravity is overridden, caret movement is blocked, selection at the end but "outside" the text.
  78. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  79. '→',
  80. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  81. ] );
  82. } );
  83. it( 'should use two-steps movement when between nodes with the same attribute but different value', () => {
  84. setData( model, '<$text a="1">bar[]</$text><$text a="2">foo</$text>' );
  85. testTwoStepCaretMovement( [
  86. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  87. '→',
  88. // <$text a="1">bar</$text>[]<$text a="2">foo</$text>
  89. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  90. '→',
  91. // <$text a="1">bar</$text><$text a="2">[]foo</$text>
  92. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 2, evtStopCalled: 2 },
  93. '→',
  94. // <$text a="1">bar</$text><$text a="2">f[]oo</$text>
  95. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 }
  96. ] );
  97. } );
  98. // https://github.com/ckeditor/ckeditor5/issues/937
  99. it( 'should not require two-steps between unrelated attributes inside the initial attribute', () => {
  100. setData( model, '<$text a="1">fo[]o</$text><$text a="1" b="2">bar</$text><$text a="1">baz</$text>' );
  101. testTwoStepCaretMovement( [
  102. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  103. '→',
  104. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  105. '→',
  106. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  107. '→',
  108. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  109. '→',
  110. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  111. '→',
  112. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 }
  113. ] );
  114. } );
  115. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  116. it( 'should handle passing through the only character in the block', () => {
  117. setData( model, '[]<$text a="1">x</$text>' );
  118. testTwoStepCaretMovement( [
  119. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  120. '→',
  121. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  122. '→',
  123. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  124. '→',
  125. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 }
  126. ] );
  127. } );
  128. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  129. it( 'should handle passing through the only character in the block (no attribute in the initial selection)', () => {
  130. setData( model, '[]<$text a="1">x</$text>' );
  131. model.change( writer => writer.removeSelectionAttribute( 'a' ) );
  132. testTwoStepCaretMovement( [
  133. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  134. '→',
  135. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  136. '→',
  137. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  138. '→',
  139. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 2, evtStopCalled: 2 },
  140. '→',
  141. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 2, evtStopCalled: 2 }
  142. ] );
  143. } );
  144. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  145. it( 'should handle passing through the only-child with an attribute (multiple characters)', () => {
  146. setData( model, '[]<$text a="1">xyz</$text>' );
  147. testTwoStepCaretMovement( [
  148. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  149. '→',
  150. // <$text a="1">x{}yz</$text>
  151. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  152. '→',
  153. // <$text a="1">xy{}z</$text>
  154. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  155. '→',
  156. // <$text a="1">xyz{}</$text>
  157. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  158. '→',
  159. // <$text a="1">xyz</$text>{}
  160. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  161. '→',
  162. // <$text a="1">xyz</$text>{}
  163. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 }
  164. ] );
  165. } );
  166. it( 'should handle leaving an attribute followed by another block', () => {
  167. setData( model, '<paragraph><$text a="1">foo[]</$text></paragraph><paragraph><$text b="1">bar</$text></paragraph>' );
  168. testTwoStepCaretMovement( [
  169. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  170. '→',
  171. // <paragraph><$text a="1">bar</$text>[]</paragraph><paragraph>foo</paragraph>
  172. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  173. '→',
  174. // <paragraph><$text a="1">bar</$text></paragraph><paragraph>f[]oo</paragraph>
  175. { selectionAttributes: [ 'b' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  176. '→',
  177. // <paragraph><$text a="1">bar</$text></paragraph><paragraph>fo[]o</paragraph>
  178. { selectionAttributes: [ 'b' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  179. ] );
  180. } );
  181. } );
  182. describe( 'moving left', () => {
  183. it( 'should "enter" the text with attribute in two steps', () => {
  184. setData( model, '<$text>foo</$text><$text a="true" b="true">bar</$text><$text c="true">b[]iz</$text>' );
  185. testTwoStepCaretMovement( [
  186. // Gravity is not overridden, caret is a one character after the and of the text.
  187. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  188. '←',
  189. // Caret movement was not blocked but the gravity is overridden.
  190. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 0, evtStopCalled: 0 },
  191. '←',
  192. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  193. '←',
  194. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  195. ] );
  196. } );
  197. it( 'should "leave" the text with attribute in two steps', () => {
  198. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  199. testTwoStepCaretMovement( [
  200. // Gravity is not overridden, caret is a one character after the beginning of the text.
  201. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  202. '←',
  203. // Caret movement was not blocked.
  204. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: true, preventDefault: 0, evtStopCalled: 0 },
  205. '←',
  206. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  207. '←',
  208. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 }
  209. ] );
  210. } );
  211. it( 'should do nothing for unrelated attribute (at the beginning)', () => {
  212. setData( model, '<$text c="true">[]foo</$text>' );
  213. testTwoStepCaretMovement( [
  214. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  215. '←',
  216. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  217. '←',
  218. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 }
  219. ] );
  220. } );
  221. it( 'should do nothing for unrelated attribute (at the end)', () => {
  222. setData( model, '<$text c="true">foo</$text>[]' );
  223. testTwoStepCaretMovement( [
  224. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  225. '←',
  226. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  227. '←',
  228. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 }
  229. ] );
  230. } );
  231. it( 'should do nothing when caret is at the beginning of block element', () => {
  232. setData( model, '[]foo', { lastRangeBackward: true } );
  233. testTwoStepCaretMovement( [
  234. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  235. '←',
  236. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  237. ] );
  238. } );
  239. it( 'should require two-steps movement when caret goes between text node with the same attribute but different value', () => {
  240. setData( model, '<$text a="2">foo</$text><$text a="1">b[]ar</$text>' );
  241. testTwoStepCaretMovement( [
  242. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  243. '←',
  244. // <$text a="2">foo</$text><$text a="1">[]bar</$text>
  245. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStopCalled: 0 },
  246. '←',
  247. // <$text a="2">foo</$text>[]<$text a="1">bar</$text>
  248. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  249. '←',
  250. // <$text a="2">foo[]</$text><$text a="1">bar</$text>
  251. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  252. '←',
  253. // <$text a="2">fo[]o</$text><$text a="1">bar</$text>
  254. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 }
  255. ] );
  256. } );
  257. // https://github.com/ckeditor/ckeditor5/issues/937
  258. it( 'should not require two-steps between unrelated attributes inside the initial attribute', () => {
  259. setData( model, '<$text a="1">foo</$text><$text a="1" b="2">bar</$text><$text a="1">b[]az</$text>' );
  260. testTwoStepCaretMovement( [
  261. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  262. '←',
  263. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  264. '←',
  265. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  266. '←',
  267. { selectionAttributes: [ 'a', 'b' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  268. '←',
  269. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 }
  270. ] );
  271. } );
  272. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  273. it( 'should handle passing through the only-child with an attribute (single character)', () => {
  274. setData( model, '<$text a="1">x</$text>[]' );
  275. testTwoStepCaretMovement( [
  276. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  277. '←',
  278. // <$text a="1">{}x</$text>
  279. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStopCalled: 0 },
  280. '←',
  281. // {}<$text a="1">x</$text> (because it's a first-child)
  282. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  283. '←',
  284. // {}<$text a="1">x</$text>
  285. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 }
  286. ] );
  287. } );
  288. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  289. it( 'should handle passing through the only character in the block (no attribute in the initial selection)', () => {
  290. setData( model, '<$text a="1">x</$text>[]' );
  291. model.change( writer => writer.removeSelectionAttribute( 'a' ) );
  292. testTwoStepCaretMovement( [
  293. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  294. '←',
  295. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  296. '←',
  297. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  298. '←',
  299. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  300. '←',
  301. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 }
  302. ] );
  303. } );
  304. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  305. it( 'should handle passing through the only-child with an attribute (single character, text before)', () => {
  306. setData( model, 'abc<$text a="1">x</$text>[]' );
  307. testTwoStepCaretMovement( [
  308. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  309. '←',
  310. // abc<$text a="1">{}x</$text>
  311. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStopCalled: 0 },
  312. '←',
  313. // abc{}<$text a="1">x</$text> (because it's a first-child)
  314. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  315. '←',
  316. // abc{}<$text a="1">x</$text>
  317. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 }
  318. ] );
  319. } );
  320. // https://github.com/ckeditor/ckeditor5-engine/issues/1301
  321. it( 'should handle passing through the only-child with an attribute (multiple characters)', () => {
  322. setData( model, '<$text a="1">xyz</$text>[]' );
  323. testTwoStepCaretMovement( [
  324. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  325. '←',
  326. // <$text a="1">xy{}z</$text>
  327. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  328. '←',
  329. // <$text a="1">x{}yz</$text>
  330. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  331. '←',
  332. // <$text a="1">{}xyz</$text>
  333. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 0, evtStopCalled: 0 },
  334. '←',
  335. // {}<$text a="1">xyz</$text>
  336. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  337. '←',
  338. // {}<$text a="1">xyz</$text>
  339. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 }
  340. ] );
  341. } );
  342. it( 'should handle leaving an attribute preceded by another block', () => {
  343. setData( model, '<paragraph><$text b="1">foo</$text></paragraph><paragraph><$text a="1">[]bar</$text></paragraph>' );
  344. testTwoStepCaretMovement( [
  345. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  346. '←',
  347. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  348. '←',
  349. { selectionAttributes: [ 'b' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  350. ] );
  351. } );
  352. } );
  353. describe( 'moving and typing around the attribute', () => {
  354. it( 'should handle typing after the attribute', () => {
  355. setData( model, '<$text a="1">x[]</$text>' );
  356. testTwoStepCaretMovement( [
  357. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  358. 'y',
  359. // <$text a="1">xy[]</$text>
  360. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  361. '→',
  362. // <$text a="1">xy</$text>[]
  363. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  364. 'z',
  365. // <$text a="1">xy</$text>z[]
  366. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  367. '←',
  368. // <$text a="1">xy</$text>[]z
  369. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  370. '←',
  371. // <$text a="1">xy[]</$text>z
  372. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  373. 'w',
  374. // <$text a="1">xyw[]</$text>
  375. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  376. ] );
  377. } );
  378. it( 'should handle typing before the attribute', () => {
  379. setData( model, '<$text a="1">[]x</$text>' );
  380. testTwoStepCaretMovement( [
  381. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  382. '←',
  383. // []<$text a="1">x</$text>
  384. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  385. 'z',
  386. // z[]<$text a="1">x</$text>
  387. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  388. 'x',
  389. // zx[]<$text a="1">x</$text>
  390. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  391. '→',
  392. // zx<$text a="1">[]x</$text>
  393. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 2, evtStopCalled: 2 },
  394. 'a',
  395. // zx<$text a="1">a[]x</$text>
  396. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  397. ] );
  398. } );
  399. // https://github.com/ckeditor/ckeditor5-engine/issues/1346
  400. // https://github.com/ckeditor/ckeditor5/issues/946
  401. it( 'should correctly re-renter the attribute', () => {
  402. setData( model, 'fo[]o <$text a="1">bar</$text>' );
  403. testTwoStepCaretMovement( [
  404. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  405. '→',
  406. // foo[] <$text a="1">bar</$text>
  407. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  408. '→',
  409. // foo []<$text a="1">bar</$text>
  410. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  411. '→',
  412. // foo <$text a="1">[]bar</$text>
  413. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  414. '→',
  415. // foo <$text a="1">b[]ar</$text>
  416. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  417. '←',
  418. // foo <$text a="1">[]bar</$text>
  419. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  420. '←',
  421. // foo []<$text a="1">bar</$text>
  422. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  423. '←',
  424. // foo[] <$text a="1">bar</$text>
  425. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  426. '←',
  427. // fo[]o <$text a="1">bar</$text>
  428. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  429. ] );
  430. } );
  431. // https://github.com/ckeditor/ckeditor5/issues/922
  432. it( 'should not lose the new attribute when typing (after)', () => {
  433. setData( model, '<$text a="1">x[]</$text>' );
  434. testTwoStepCaretMovement( [
  435. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  436. '→',
  437. // <$text a="1">x</$text>[]
  438. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 }
  439. ] );
  440. model.change( writer => {
  441. writer.setSelectionAttribute( 'b', 1 );
  442. } );
  443. // <$text a="1">x</$text><$text b="1">[]</$text>
  444. expect( selection.isGravityOverridden ).to.be.true;
  445. expect( getSelectionAttributesArray( selection ) ).to.have.members( [ 'b' ] );
  446. model.change( writer => {
  447. writer.insertText( 'yz', selection.getAttributes(), selection.getFirstPosition() );
  448. } );
  449. // <$text a="1">x</$text><$text b="1">yz[]</$text>
  450. expect( selection.isGravityOverridden ).to.be.false;
  451. expect( getSelectionAttributesArray( selection ) ).to.have.members( [ 'b' ] );
  452. } );
  453. // https://github.com/ckeditor/ckeditor5/issues/922
  454. it( 'should not lose the new attribute when typing (before)', () => {
  455. setData( model, '<$text a="1">[]x</$text>' );
  456. testTwoStepCaretMovement( [
  457. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  458. '←',
  459. // []<$text a="1">x</$text>
  460. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 }
  461. ] );
  462. model.change( writer => {
  463. writer.setSelectionAttribute( 'b', 1 );
  464. } );
  465. // <$text b="1">[]</$text><$text a="1">x</$text>
  466. expect( selection.isGravityOverridden ).to.be.false;
  467. expect( getSelectionAttributesArray( selection ) ).to.have.members( [ 'b' ] );
  468. model.change( writer => {
  469. writer.insertText( 'yz', selection.getAttributes(), selection.getFirstPosition() );
  470. } );
  471. // <$text b="1">yz[]</$text><$text a="1">x</$text>
  472. expect( selection.isGravityOverridden ).to.be.false;
  473. expect( getSelectionAttributesArray( selection ) ).to.have.members( [ 'b' ] );
  474. } );
  475. } );
  476. describe( 'multiple attributes', () => {
  477. beforeEach( () => {
  478. bindTwoStepCaretToAttribute( editor.editing.view, editor.model, emitter, 'c' );
  479. } );
  480. it( 'should work with the two-step caret movement (moving right)', () => {
  481. setData( model, 'fo[]o<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux' );
  482. testTwoStepCaretMovement( [
  483. // fo[]o<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  484. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  485. '→',
  486. // foo[]<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  487. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  488. '→',
  489. // foo<$text a="true">[]foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  490. { selectionAttributes: [ 'a' ], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  491. '→',
  492. '→',
  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: false, preventDefault: 1, evtStopCalled: 1 },
  496. '→',
  497. // foo<$text a="true">foo</$text><$text a="true" c="true">[]bar</$text><$text c="true">baz</$text>qux
  498. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: true, preventDefault: 2, evtStopCalled: 2 },
  499. '→',
  500. '→',
  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: false, preventDefault: 2, evtStopCalled: 2 },
  504. '→',
  505. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">[]baz</$text>qux
  506. { selectionAttributes: [ 'c' ], isGravityOverridden: true, preventDefault: 3, evtStopCalled: 3 },
  507. '→',
  508. '→',
  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: false, preventDefault: 3, evtStopCalled: 3 },
  512. '→',
  513. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>[]qux
  514. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 4, evtStopCalled: 4 },
  515. '→',
  516. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>q[]ux
  517. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 4, evtStopCalled: 4 },
  518. ] );
  519. } );
  520. it( 'should work with the two-step caret movement (moving left)', () => {
  521. setData( model, 'foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>q[]ux' );
  522. testTwoStepCaretMovement( [
  523. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>q[]ux
  524. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  525. '←',
  526. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>[]qux
  527. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 0, evtStopCalled: 0 },
  528. '←',
  529. // foo<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz[]</$text>qux
  530. { selectionAttributes: [ 'c' ], isGravityOverridden: false, preventDefault: 1, evtStopCalled: 1 },
  531. '←',
  532. '←',
  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: true, preventDefault: 1, evtStopCalled: 1 },
  536. '←',
  537. // foo<$text a="true">foo</$text><$text a="true" c="true">bar[]</$text><$text c="true">baz</$text>qux
  538. { selectionAttributes: [ 'a', 'c' ], isGravityOverridden: false, preventDefault: 2, evtStopCalled: 2 },
  539. '←',
  540. '←',
  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: true, preventDefault: 2, evtStopCalled: 2 },
  544. '←',
  545. // foo<$text a="true">foo[]</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  546. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 3, evtStopCalled: 3 },
  547. '←',
  548. '←',
  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: true, preventDefault: 3, evtStopCalled: 3 },
  552. '←',
  553. // foo[]<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  554. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 4, evtStopCalled: 4 },
  555. '←',
  556. // fo[]o<$text a="true">foo</$text><$text a="true" c="true">bar</$text><$text c="true">baz</$text>qux
  557. { selectionAttributes: [], isGravityOverridden: false, preventDefault: 4, evtStopCalled: 4 },
  558. ] );
  559. } );
  560. } );
  561. describe( 'mouse', () => {
  562. it( 'should not override gravity when selection is placed at the beginning of text', () => {
  563. setData( model, '<$text a="true">[]foo</$text>' );
  564. expect( selection.isGravityOverridden ).to.be.false;
  565. } );
  566. it( 'should not override gravity when selection is placed at the end of text', () => {
  567. setData( model, '<$text a="true">foo[]</$text>' );
  568. expect( selection.isGravityOverridden ).to.be.false;
  569. } );
  570. } );
  571. it( 'should listen with the high+1 priority on view.document#keydown', () => {
  572. const highestPrioritySpy = sinon.spy();
  573. const highPrioritySpy = sinon.spy();
  574. const normalPrioritySpy = sinon.spy();
  575. setData( model, '<$text c="true">foo[]</$text><$text a="true" b="true">bar</$text>' );
  576. emitter.listenTo( view.document, 'keydown', highestPrioritySpy, { priority: 'highest' } );
  577. emitter.listenTo( view.document, 'keydown', highPrioritySpy, { priority: 'high' } );
  578. emitter.listenTo( view.document, 'keydown', normalPrioritySpy, { priority: 'normal' } );
  579. fireKeyDownEvent( {
  580. keyCode: keyCodes.arrowright,
  581. preventDefault: preventDefaultSpy
  582. } );
  583. sinon.assert.callOrder(
  584. highestPrioritySpy,
  585. preventDefaultSpy );
  586. sinon.assert.notCalled( highPrioritySpy );
  587. sinon.assert.notCalled( normalPrioritySpy );
  588. } );
  589. it( 'should do nothing when key other then arrow left and right is pressed', () => {
  590. setData( model, '<$text a="true">foo[]</$text>' );
  591. expect( () => {
  592. fireKeyDownEvent( { keyCode: keyCodes.arrowup } );
  593. } ).to.not.throw();
  594. } );
  595. it( 'should do nothing for non-collapsed selection', () => {
  596. setData( model, '<$text c="true">fo[o]</$text><$text a="true" b="true">bar</$text>' );
  597. fireKeyDownEvent( { keyCode: keyCodes.arrowright } );
  598. expect( selection.isGravityOverridden ).to.be.false;
  599. } );
  600. it( 'should do nothing when shift key is pressed', () => {
  601. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  602. fireKeyDownEvent( {
  603. keyCode: keyCodes.arrowleft,
  604. shiftKey: true
  605. } );
  606. expect( selection.isGravityOverridden ).to.be.false;
  607. } );
  608. it( 'should do nothing when alt key is pressed', () => {
  609. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  610. fireKeyDownEvent( {
  611. keyCode: keyCodes.arrowleft,
  612. altKey: true
  613. } );
  614. expect( selection.isGravityOverridden ).to.be.false;
  615. } );
  616. it( 'should do nothing when ctrl key is pressed', () => {
  617. setData( model, '<$text c="true">foo</$text><$text a="true" b="true">b[]ar</$text>' );
  618. fireKeyDownEvent( {
  619. keyCode: keyCodes.arrowleft,
  620. ctrlKey: true
  621. } );
  622. expect( selection.isGravityOverridden ).to.be.false;
  623. } );
  624. it( 'should do nothing when the not a direct selection change but at the attribute boundary', () => {
  625. setData( model, '<$text a="true">foo[]</$text>bar' );
  626. testTwoStepCaretMovement( [
  627. { selectionAttributes: [ 'a' ], isGravityOverridden: false, preventDefault: 0, evtStopCalled: 0 },
  628. '→',
  629. { selectionAttributes: [], isGravityOverridden: true, preventDefault: 1, evtStopCalled: 1 },
  630. ] );
  631. // Simulate an external text insertion BEFORE the user selection to trigger #change:range.
  632. model.enqueueChange( 'transparent', writer => {
  633. writer.insertText( 'x', selection.getFirstPosition().getShiftedBy( -2 ) );
  634. } );
  635. expect( selection.isGravityOverridden ).to.be.true;
  636. expect( getSelectionAttributesArray( selection ) ).to.have.members( [] );
  637. } );
  638. const keyMap = {
  639. '→': 'arrowright',
  640. '←': 'arrowleft'
  641. };
  642. function fireKeyDownEvent( options ) {
  643. const eventInfo = new EventInfo( view.document, 'keydown' );
  644. const eventData = new DomEventData( view.document, {
  645. target: document.body,
  646. }, options );
  647. sinon.stub( eventInfo, 'stop' ).callsFake( evtStopSpy );
  648. view.document.fire( eventInfo, eventData );
  649. }
  650. function getSelectionAttributesArray( selection ) {
  651. return Array.from( selection.getAttributeKeys() );
  652. }
  653. function testTwoStepCaretMovement( scenario ) {
  654. for ( const step of scenario ) {
  655. if ( typeof step == 'string' ) {
  656. // An arrow key pressed. Fire the view event and update the model selection.
  657. if ( keyMap[ step ] ) {
  658. let preventDefaultCalled;
  659. fireKeyDownEvent( {
  660. keyCode: keyCodes[ keyMap[ step ] ],
  661. preventDefault: () => {
  662. preventDefaultSpy();
  663. preventDefaultCalled = true;
  664. }
  665. } );
  666. const position = selection.getFirstPosition();
  667. if ( !preventDefaultCalled ) {
  668. if ( step == '→' ) {
  669. model.change( writer => {
  670. if ( position.isAtEnd ) {
  671. const nextBlock = position.parent.nextSibling;
  672. if ( nextBlock ) {
  673. writer.setSelection( Position._createAt( nextBlock, 0 ) );
  674. }
  675. } else {
  676. writer.setSelection( selection.getFirstPosition().getShiftedBy( 1 ) );
  677. }
  678. } );
  679. } else if ( step == '←' ) {
  680. model.change( writer => {
  681. if ( position.isAtStart ) {
  682. const previousBlock = position.parent.previousSibling;
  683. if ( previousBlock ) {
  684. writer.setSelection( Position._createAt( previousBlock, 'end' ) );
  685. }
  686. } else {
  687. writer.setSelection( selection.getFirstPosition().getShiftedBy( -1 ) );
  688. }
  689. } );
  690. }
  691. }
  692. }
  693. // A regular key pressed. Type some text in the model.
  694. else {
  695. model.change( writer => {
  696. writer.insertText( step, selection.getAttributes(), selection.getFirstPosition() );
  697. } );
  698. }
  699. }
  700. // If not a key, then it's an assertion.
  701. else {
  702. const stepIndex = scenario.indexOf( step );
  703. const stepString = `in step #${ stepIndex } ${ JSON.stringify( step ) }`;
  704. expect( getSelectionAttributesArray( selection ) ).to.have.members(
  705. step.selectionAttributes, `#attributes ${ stepString }` );
  706. expect( selection.isGravityOverridden ).to.equal( step.isGravityOverridden, `#isGravityOverridden ${ stepString }` );
  707. expect( preventDefaultSpy.callCount ).to.equal( step.preventDefault, `#preventDefault ${ stepString }` );
  708. expect( evtStopSpy.callCount ).to.equal( step.evtStopCalled, `#evtStopCalled ${ stepString }` );
  709. }
  710. }
  711. }
  712. } );