8
0

bindtwostepcarettoattribute.js 32 KB

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