8
0

bindtwostepcarettoattribute.js 34 KB

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