8
0

bindtwostepcarettoattribute.js 30 KB

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