8
0

modifyselection.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Model from '../../src/model/model';
  6. import DataController from '../../src/controller/datacontroller';
  7. import Selection from '../../src/model/selection';
  8. import modifySelection from '../../src/controller/modifyselection';
  9. import { setData, stringify } from '../../src/dev-utils/model';
  10. describe( 'DataController utils', () => {
  11. let model, doc, data;
  12. beforeEach( () => {
  13. model = new Model();
  14. doc = model.document;
  15. data = new DataController( model );
  16. model.schema.registerItem( 'p', '$block' );
  17. model.schema.registerItem( 'x', '$block' );
  18. model.schema.allow( { name: 'x', inside: 'p' } );
  19. doc.createRoot();
  20. } );
  21. describe( 'modifySelection', () => {
  22. describe( 'unit=character', () => {
  23. describe( 'within element', () => {
  24. test(
  25. 'does nothing on empty content',
  26. '[]',
  27. '[]'
  28. );
  29. test(
  30. 'does nothing on empty content (with empty element)',
  31. '<p>[]</p>',
  32. '<p>[]</p>'
  33. );
  34. test(
  35. 'does nothing on empty content (backward)',
  36. '[]',
  37. '[]',
  38. { direction: 'backward' }
  39. );
  40. test(
  41. 'does nothing on root boundary',
  42. '<p>foo[]</p>',
  43. '<p>foo[]</p>'
  44. );
  45. test(
  46. 'does nothing on root boundary (backward)',
  47. '<p>[]foo</p>',
  48. '<p>[]foo</p>',
  49. { direction: 'backward' }
  50. );
  51. test(
  52. 'extends one character forward',
  53. '<p>f[]oo</p>',
  54. '<p>f[o]o</p>'
  55. );
  56. it( 'extends one character backward', () => {
  57. setData( model, '<p>fo[]o</p>', { lastRangeBackward: true } );
  58. modifySelection( data, doc.selection, { direction: 'backward' } );
  59. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>f[o]o</p>' );
  60. expect( doc.selection.isBackward ).to.true;
  61. } );
  62. test(
  63. 'extends one character forward (non-collapsed)',
  64. '<p>f[o]obar</p>',
  65. '<p>f[oo]bar</p>'
  66. );
  67. it( 'extends one character backward (non-collapsed)', () => {
  68. setData( model, '<p>foob[a]r</p>', { lastRangeBackward: true } );
  69. modifySelection( data, doc.selection, { direction: 'backward' } );
  70. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foo[ba]r</p>' );
  71. expect( doc.selection.isBackward ).to.true;
  72. } );
  73. test(
  74. 'extends to element boundary',
  75. '<p>fo[]o</p>',
  76. '<p>fo[o]</p>'
  77. );
  78. it( 'extends to element boundary (backward)', () => {
  79. setData( model, '<p>f[]oo</p>' );
  80. modifySelection( data, doc.selection, { direction: 'backward' } );
  81. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[f]oo</p>' );
  82. expect( doc.selection.isBackward ).to.true;
  83. } );
  84. test(
  85. 'shrinks forward selection (to collapsed)',
  86. '<p>foo[b]ar</p>',
  87. '<p>foo[]bar</p>',
  88. { direction: 'backward' }
  89. );
  90. it( 'shrinks backward selection (to collapsed)', () => {
  91. setData( model, '<p>foo[b]ar</p>', { lastRangeBackward: true } );
  92. modifySelection( data, doc.selection );
  93. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foob[]ar</p>' );
  94. expect( doc.selection.isBackward ).to.false;
  95. } );
  96. test(
  97. 'unicode support - combining mark forward',
  98. '<p>foo[]b̂ar</p>',
  99. '<p>foo[b̂]ar</p>'
  100. );
  101. it( 'unicode support - combining mark backward', () => {
  102. setData( model, '<p>foob̂[]ar</p>' );
  103. modifySelection( data, doc.selection, { direction: 'backward' } );
  104. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foo[b̂]ar</p>' );
  105. expect( doc.selection.isBackward ).to.true;
  106. } );
  107. test(
  108. 'unicode support - combining mark multiple',
  109. '<p>fo[]o̻̐ͩbar</p>',
  110. '<p>fo[o̻̐ͩ]bar</p>'
  111. );
  112. it( 'unicode support - combining mark multiple backward', () => {
  113. setData( model, '<p>foo̻̐ͩ[]bar</p>' );
  114. modifySelection( data, doc.selection, { direction: 'backward' } );
  115. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>fo[o̻̐ͩ]bar</p>' );
  116. expect( doc.selection.isBackward ).to.true;
  117. } );
  118. test(
  119. 'unicode support - combining mark to the end',
  120. '<p>fo[]o̻̐ͩ</p>',
  121. '<p>fo[o̻̐ͩ]</p>'
  122. );
  123. test(
  124. 'unicode support - surrogate pairs forward',
  125. '<p>[]\uD83D\uDCA9</p>',
  126. '<p>[\uD83D\uDCA9]</p>'
  127. );
  128. it( 'unicode support - surrogate pairs backward', () => {
  129. setData( model, '<p>\uD83D\uDCA9[]</p>' );
  130. modifySelection( data, doc.selection, { direction: 'backward' } );
  131. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
  132. expect( doc.selection.isBackward ).to.true;
  133. } );
  134. } );
  135. describe( 'beyond element', () => {
  136. test(
  137. 'extends over boundary of empty elements',
  138. '<p>[]</p><p></p><p></p>',
  139. '<p>[</p><p>]</p><p></p>'
  140. );
  141. it( 'extends over boundary of empty elements (backward)', () => {
  142. setData( model, '<p></p><p></p><p>[]</p>' );
  143. modifySelection( data, doc.selection, { direction: 'backward' } );
  144. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[</p><p>]</p>' );
  145. expect( doc.selection.isBackward ).to.true;
  146. } );
  147. test(
  148. 'extends over boundary of non-empty elements',
  149. '<p>a[]</p><p>bcd</p>',
  150. '<p>a[</p><p>]bcd</p>'
  151. );
  152. it( 'extends over boundary of non-empty elements (backward)', () => {
  153. setData( model, '<p>a</p><p>[]bcd</p>' );
  154. modifySelection( data, doc.selection, { direction: 'backward' } );
  155. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a[</p><p>]bcd</p>' );
  156. expect( doc.selection.isBackward ).to.true;
  157. } );
  158. test(
  159. 'extends over character after boundary',
  160. '<p>a[</p><p>]bcd</p>',
  161. '<p>a[</p><p>b]cd</p>'
  162. );
  163. it( 'extends over character after boundary (backward)', () => {
  164. setData( model, '<p>abc[</p><p>]d</p>', { lastRangeBackward: true } );
  165. modifySelection( data, doc.selection, { direction: 'backward' } );
  166. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>ab[c</p><p>]d</p>' );
  167. expect( doc.selection.isBackward ).to.true;
  168. } );
  169. test(
  170. 'stops on the first position where text is allowed - inside block',
  171. '<p>a[]</p><p><x>bcd</x></p>',
  172. '<p>a[</p><p>]<x>bcd</x></p>'
  173. );
  174. test(
  175. 'stops on the first position where text is allowed - inside inline element',
  176. '<p>a[</p><p>]<x>bcd</x>ef</p>',
  177. '<p>a[</p><p><x>]bcd</x>ef</p>'
  178. );
  179. test(
  180. 'extends over element when next node is a text',
  181. '<p><x>a[]</x>bc</p>',
  182. '<p><x>a[</x>]bc</p>'
  183. );
  184. test(
  185. 'extends over element when next node is a text - backward',
  186. '<p>ab<x>[]c</x></p>',
  187. '<p>ab[<x>]c</x></p>',
  188. { direction: 'backward' }
  189. );
  190. it( 'shrinks over boundary of empty elements', () => {
  191. setData( model, '<p>[</p><p>]</p>', { lastRangeBackward: true } );
  192. modifySelection( data, doc.selection );
  193. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[]</p>' );
  194. expect( doc.selection.isBackward ).to.false;
  195. } );
  196. it( 'shrinks over boundary of empty elements (backward)', () => {
  197. setData( model, '<p>[</p><p>]</p>' );
  198. modifySelection( data, doc.selection, { direction: 'backward' } );
  199. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[]</p><p></p>' );
  200. expect( doc.selection.isBackward ).to.false;
  201. } );
  202. it( 'shrinks over boundary of non-empty elements', () => {
  203. setData( model, '<p>a[</p><p>]b</p>', { lastRangeBackward: true } );
  204. modifySelection( data, doc.selection );
  205. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a</p><p>[]b</p>' );
  206. expect( doc.selection.isBackward ).to.false;
  207. } );
  208. test(
  209. 'shrinks over boundary of non-empty elements (backward)',
  210. '<p>a[</p><p>]b</p>',
  211. '<p>a[]</p><p>b</p>',
  212. { direction: 'backward' }
  213. );
  214. it( 'updates selection attributes', () => {
  215. setData( model, '<p><$text bold="true">foo</$text>[b]</p>' );
  216. modifySelection( data, doc.selection, { direction: 'backward' } );
  217. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p><$text bold="true">foo[]</$text>b</p>' );
  218. expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
  219. } );
  220. } );
  221. describe( 'beyond element – skipping incorrect positions', () => {
  222. beforeEach( () => {
  223. model.schema.registerItem( 'quote' );
  224. model.schema.allow( { name: 'quote', inside: '$root' } );
  225. model.schema.allow( { name: '$block', inside: 'quote' } );
  226. } );
  227. test(
  228. 'skips position at the beginning of an element which does not allow text',
  229. '<p>x[]</p><quote><p>y</p></quote><p>z</p>',
  230. '<p>x[</p><quote><p>]y</p></quote><p>z</p>'
  231. );
  232. test(
  233. 'skips position at the end of an element which does not allow text - backward',
  234. '<p>x</p><quote><p>y</p></quote><p>[]z</p>',
  235. '<p>x</p><quote><p>y[</p></quote><p>]z</p>',
  236. { direction: 'backward' }
  237. );
  238. test(
  239. 'skips position at the end of an element which does not allow text',
  240. '<p>x[</p><quote><p>y]</p></quote><p>z</p>',
  241. '<p>x[</p><quote><p>y</p></quote><p>]z</p>'
  242. );
  243. test(
  244. 'skips position at the beginning of an element which does not allow text - backward',
  245. '<p>x</p><quote><p>[]y</p></quote><p>z</p>',
  246. '<p>x[</p><quote><p>]y</p></quote><p>z</p>',
  247. { direction: 'backward' }
  248. );
  249. test(
  250. 'extends to an empty block after skipping incorrect position',
  251. '<p>x[]</p><quote><p></p></quote><p>z</p>',
  252. '<p>x[</p><quote><p>]</p></quote><p>z</p>'
  253. );
  254. test(
  255. 'extends to an empty block after skipping incorrect position - backward',
  256. '<p>x</p><quote><p></p></quote><p>[]z</p>',
  257. '<p>x</p><quote><p>[</p></quote><p>]z</p>',
  258. { direction: 'backward' }
  259. );
  260. } );
  261. } );
  262. describe( 'unit=codePoint', () => {
  263. it( 'does nothing on empty content', () => {
  264. model.schema.allow( { name: '$text', inside: '$root' } );
  265. setData( model, '' );
  266. modifySelection( data, doc.selection, { unit: 'codePoint' } );
  267. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[]' );
  268. } );
  269. test(
  270. 'does nothing on empty content (with empty element)',
  271. '<p>[]</p>',
  272. '<p>[]</p>',
  273. { unit: 'codePoint' }
  274. );
  275. test(
  276. 'extends one user-perceived character forward - latin letters',
  277. '<p>f[]oo</p>',
  278. '<p>f[o]o</p>',
  279. { unit: 'codePoint' }
  280. );
  281. it( 'extends one user-perceived character backward - latin letters', () => {
  282. setData( model, '<p>fo[]o</p>' );
  283. modifySelection( data, doc.selection, { unit: 'codePoint', direction: 'backward' } );
  284. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>f[o]o</p>' );
  285. expect( doc.selection.isBackward ).to.true;
  286. } );
  287. test(
  288. 'unicode support - combining mark forward',
  289. '<p>foo[]b̂ar</p>',
  290. '<p>foo[b]̂ar</p>',
  291. { unit: 'codePoint' }
  292. );
  293. it( 'unicode support - combining mark backward', () => {
  294. setData( model, '<p>foob̂[]ar</p>' );
  295. // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
  296. // Document's selection will throw errors in some test cases (which are correct cases, but only for
  297. // non-document selections).
  298. const testSelection = Selection.createFromSelection( doc.selection );
  299. modifySelection( data, testSelection, { unit: 'codePoint', direction: 'backward' } );
  300. expect( stringify( doc.getRoot(), testSelection ) ).to.equal( '<p>foob[̂]ar</p>' );
  301. expect( testSelection.isBackward ).to.true;
  302. } );
  303. test(
  304. 'unicode support - combining mark multiple',
  305. '<p>fo[]o̻̐ͩbar</p>',
  306. '<p>fo[o]̻̐ͩbar</p>',
  307. { unit: 'codePoint' }
  308. );
  309. test(
  310. 'unicode support - surrogate pairs forward',
  311. '<p>[]\uD83D\uDCA9</p>',
  312. '<p>[\uD83D\uDCA9]</p>',
  313. { unit: 'codePoint' }
  314. );
  315. it( 'unicode support surrogate pairs backward', () => {
  316. setData( model, '<p>\uD83D\uDCA9[]</p>' );
  317. modifySelection( data, doc.selection, { unit: 'codePoint', direction: 'backward' } );
  318. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
  319. expect( doc.selection.isBackward ).to.true;
  320. } );
  321. } );
  322. describe( 'objects handling', () => {
  323. beforeEach( () => {
  324. model.schema.registerItem( 'obj' );
  325. model.schema.allow( { name: 'obj', inside: '$root' } );
  326. model.schema.allow( { name: '$text', inside: 'obj' } );
  327. model.schema.objects.add( 'obj' );
  328. model.schema.registerItem( 'inlineObj', '$inline' );
  329. model.schema.allow( { name: '$text', inside: 'inlineObj' } );
  330. model.schema.objects.add( 'inlineObj' );
  331. } );
  332. test(
  333. 'extends over next object element when at the end of an element',
  334. '<p>foo[]</p><obj>bar</obj>',
  335. '<p>foo[</p><obj>bar</obj>]'
  336. );
  337. test(
  338. 'extends over previous object element when at the beginning of an element ',
  339. '<obj>bar</obj><p>[]foo</p>',
  340. '[<obj>bar</obj><p>]foo</p>',
  341. { direction: 'backward' }
  342. );
  343. test(
  344. 'extends over object elements - forward',
  345. '[<obj></obj>]<obj></obj>',
  346. '[<obj></obj><obj></obj>]'
  347. );
  348. it( 'extends over object elements - backward', () => {
  349. setData( model, '<obj></obj>[<obj></obj>]', { lastRangeBackward: true } );
  350. modifySelection( data, doc.selection, { direction: 'backward' } );
  351. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[<obj></obj><obj></obj>]' );
  352. expect( doc.selection.isBackward ).to.true;
  353. } );
  354. test(
  355. 'extends over inline objects - forward',
  356. '<p>foo[]<inlineObj>bar</inlineObj></p>',
  357. '<p>foo[<inlineObj>bar</inlineObj>]</p>'
  358. );
  359. test(
  360. 'extends over inline objects - backward',
  361. '<p><inlineObj>bar</inlineObj>[]foo</p>',
  362. '<p>[<inlineObj>bar</inlineObj>]foo</p>',
  363. { direction: 'backward' }
  364. );
  365. test(
  366. 'extends over empty inline objects - forward',
  367. '<p>foo[]<inlineObj></inlineObj></p>',
  368. '<p>foo[<inlineObj></inlineObj>]</p>'
  369. );
  370. test(
  371. 'extends over empty inline objects - backward',
  372. '<p><inlineObj></inlineObj>[]foo</p>',
  373. '<p>[<inlineObj></inlineObj>]foo</p>',
  374. { direction: 'backward' }
  375. );
  376. } );
  377. describe( 'limits handling', () => {
  378. beforeEach( () => {
  379. model.schema.registerItem( 'inlineLimit' );
  380. model.schema.allow( { name: 'inlineLimit', inside: '$block' } );
  381. model.schema.allow( { name: '$text', inside: 'inlineLimit' } );
  382. model.schema.registerItem( 'blockLimit' );
  383. model.schema.allow( { name: 'blockLimit', inside: '$root' } );
  384. model.schema.allow( { name: 'p', inside: 'blockLimit' } );
  385. model.schema.limits.add( 'inlineLimit' );
  386. model.schema.limits.add( 'blockLimit' );
  387. } );
  388. test(
  389. 'should not extend to outside of inline limit element',
  390. '<p>x<inlineLimit>foo[]</inlineLimit>x</p>',
  391. '<p>x<inlineLimit>foo[]</inlineLimit>x</p>'
  392. );
  393. test(
  394. 'should not extend to outside of inline limit element - backward',
  395. '<p>x<inlineLimit>[]foo</inlineLimit>x</p>',
  396. '<p>x<inlineLimit>[]foo</inlineLimit>x</p>',
  397. { direction: 'backward' }
  398. );
  399. test(
  400. 'should not extend to outside of block limit element',
  401. '<p>x</p><blockLimit><p>foo[]</p></blockLimit><p>x</p>',
  402. '<p>x</p><blockLimit><p>foo[]</p></blockLimit><p>x</p>'
  403. );
  404. test(
  405. 'should not extend to outside of block limit element - backward',
  406. '<p>x</p><blockLimit><p>[]foo</p></blockLimit><p>x</p>',
  407. '<p>x</p><blockLimit><p>[]foo</p></blockLimit><p>x</p>',
  408. { direction: 'backward' }
  409. );
  410. // This may seem counterintuitive but it makes sense. The limit element means
  411. // that it can't be left or modified from inside. If you want the same behavior from outside
  412. // register it as an object.
  413. test(
  414. 'should enter a limit element',
  415. '<p>foo[]</p><blockLimit><p>x</p></blockLimit>',
  416. '<p>foo[</p><blockLimit><p>]x</p></blockLimit>'
  417. );
  418. test(
  419. 'should enter a limit element - backward',
  420. '<blockLimit><p>x</p></blockLimit><p>[]foo</p>',
  421. '<blockLimit><p>x[</p></blockLimit><p>]foo</p>',
  422. { direction: 'backward' }
  423. );
  424. } );
  425. } );
  426. function test( title, input, output, options ) {
  427. it( title, () => {
  428. input = input.normalize();
  429. output = output.normalize();
  430. setData( model, input );
  431. // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
  432. // Document's selection will throw errors in some test cases (which are correct cases, but only for
  433. // non-document selections).
  434. const testSelection = Selection.createFromSelection( doc.selection );
  435. modifySelection( data, testSelection, options );
  436. expect( stringify( doc.getRoot(), testSelection ) ).to.equal( output );
  437. } );
  438. }
  439. } );