modifyselection.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Model from '../../../src/model/model';
  6. import Selection from '../../../src/model/selection';
  7. import modifySelection from '../../../src/model/utils/modifyselection';
  8. import { setData, stringify } from '../../../src/dev-utils/model';
  9. describe( 'DataController utils', () => {
  10. let model, doc;
  11. beforeEach( () => {
  12. model = new Model();
  13. doc = model.document;
  14. model.schema.register( 'p', { inheritAllFrom: '$block' } );
  15. model.schema.register( 'x', { inheritAllFrom: '$block' } );
  16. model.schema.extend( 'x', { allowIn: 'p' } );
  17. model.schema.register( 'br', { allowWhere: '$text' } );
  18. doc.createRoot();
  19. } );
  20. describe( 'modifySelection', () => {
  21. describe( 'unit=character', () => {
  22. describe( 'within element', () => {
  23. test(
  24. 'does nothing on empty content',
  25. '[]',
  26. '[]'
  27. );
  28. test(
  29. 'does nothing on empty content (with empty element)',
  30. '<p>[]</p>',
  31. '<p>[]</p>'
  32. );
  33. test(
  34. 'does nothing on empty content (backward)',
  35. '[]',
  36. '[]',
  37. { direction: 'backward' }
  38. );
  39. test(
  40. 'does nothing on root boundary',
  41. '<p>foo[]</p>',
  42. '<p>foo[]</p>'
  43. );
  44. test(
  45. 'does nothing on root boundary (backward)',
  46. '<p>[]foo</p>',
  47. '<p>[]foo</p>',
  48. { direction: 'backward' }
  49. );
  50. test(
  51. 'extends one character forward',
  52. '<p>f[]oo</p>',
  53. '<p>f[o]o</p>'
  54. );
  55. it( 'extends one character backward', () => {
  56. setData( model, '<p>fo[]o</p>', { lastRangeBackward: true } );
  57. modifySelection( model, doc.selection, { direction: 'backward' } );
  58. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>f[o]o</p>' );
  59. expect( doc.selection.isBackward ).to.true;
  60. } );
  61. test(
  62. 'extends one character forward (non-collapsed)',
  63. '<p>f[o]obar</p>',
  64. '<p>f[oo]bar</p>'
  65. );
  66. it( 'extends one character backward (non-collapsed)', () => {
  67. setData( model, '<p>foob[a]r</p>', { lastRangeBackward: true } );
  68. modifySelection( model, doc.selection, { direction: 'backward' } );
  69. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foo[ba]r</p>' );
  70. expect( doc.selection.isBackward ).to.true;
  71. } );
  72. test(
  73. 'extends to element boundary',
  74. '<p>fo[]o</p>',
  75. '<p>fo[o]</p>'
  76. );
  77. it( 'extends to element boundary (backward)', () => {
  78. setData( model, '<p>f[]oo</p>' );
  79. modifySelection( model, doc.selection, { direction: 'backward' } );
  80. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[f]oo</p>' );
  81. expect( doc.selection.isBackward ).to.true;
  82. } );
  83. test(
  84. 'shrinks forward selection (to collapsed)',
  85. '<p>foo[b]ar</p>',
  86. '<p>foo[]bar</p>',
  87. { direction: 'backward' }
  88. );
  89. it( 'shrinks backward selection (to collapsed)', () => {
  90. setData( model, '<p>foo[b]ar</p>', { lastRangeBackward: true } );
  91. modifySelection( model, doc.selection );
  92. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foob[]ar</p>' );
  93. expect( doc.selection.isBackward ).to.false;
  94. } );
  95. test(
  96. 'unicode support - combining mark forward',
  97. '<p>foo[]b̂ar</p>',
  98. '<p>foo[b̂]ar</p>'
  99. );
  100. it( 'unicode support - combining mark backward', () => {
  101. setData( model, '<p>foob̂[]ar</p>' );
  102. modifySelection( model, doc.selection, { direction: 'backward' } );
  103. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foo[b̂]ar</p>' );
  104. expect( doc.selection.isBackward ).to.true;
  105. } );
  106. test(
  107. 'unicode support - combining mark multiple',
  108. '<p>fo[]o̻̐ͩbar</p>',
  109. '<p>fo[o̻̐ͩ]bar</p>'
  110. );
  111. it( 'unicode support - combining mark multiple backward', () => {
  112. setData( model, '<p>foo̻̐ͩ[]bar</p>' );
  113. modifySelection( model, doc.selection, { direction: 'backward' } );
  114. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>fo[o̻̐ͩ]bar</p>' );
  115. expect( doc.selection.isBackward ).to.true;
  116. } );
  117. test(
  118. 'unicode support - combining mark to the end',
  119. '<p>fo[]o̻̐ͩ</p>',
  120. '<p>fo[o̻̐ͩ]</p>'
  121. );
  122. test(
  123. 'unicode support - surrogate pairs forward',
  124. '<p>[]\uD83D\uDCA9</p>',
  125. '<p>[\uD83D\uDCA9]</p>'
  126. );
  127. it( 'unicode support - surrogate pairs backward', () => {
  128. setData( model, '<p>\uD83D\uDCA9[]</p>' );
  129. modifySelection( model, doc.selection, { direction: 'backward' } );
  130. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
  131. expect( doc.selection.isBackward ).to.true;
  132. } );
  133. } );
  134. describe( 'beyond element', () => {
  135. test(
  136. 'extends over boundary of empty elements',
  137. '<p>[]</p><p></p><p></p>',
  138. '<p>[</p><p>]</p><p></p>'
  139. );
  140. it( 'extends over boundary of empty elements (backward)', () => {
  141. setData( model, '<p></p><p></p><p>[]</p>' );
  142. modifySelection( model, doc.selection, { direction: 'backward' } );
  143. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[</p><p>]</p>' );
  144. expect( doc.selection.isBackward ).to.true;
  145. } );
  146. test(
  147. 'extends over boundary of non-empty elements',
  148. '<p>a[]</p><p>bcd</p>',
  149. '<p>a[</p><p>]bcd</p>'
  150. );
  151. it( 'extends over boundary of non-empty elements (backward)', () => {
  152. setData( model, '<p>a</p><p>[]bcd</p>' );
  153. modifySelection( model, doc.selection, { direction: 'backward' } );
  154. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a[</p><p>]bcd</p>' );
  155. expect( doc.selection.isBackward ).to.true;
  156. } );
  157. test(
  158. 'extends over character after boundary',
  159. '<p>a[</p><p>]bcd</p>',
  160. '<p>a[</p><p>b]cd</p>'
  161. );
  162. it( 'extends over character after boundary (backward)', () => {
  163. setData( model, '<p>abc[</p><p>]d</p>', { lastRangeBackward: true } );
  164. modifySelection( model, doc.selection, { direction: 'backward' } );
  165. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>ab[c</p><p>]d</p>' );
  166. expect( doc.selection.isBackward ).to.true;
  167. } );
  168. test(
  169. 'stops on the first position where text is allowed - inside block',
  170. '<p>a[]</p><p><x>bcd</x></p>',
  171. '<p>a[</p><p>]<x>bcd</x></p>'
  172. );
  173. test(
  174. 'stops on the first position where text is allowed - inside inline element',
  175. '<p>a[</p><p>]<x>bcd</x>ef</p>',
  176. '<p>a[</p><p><x>]bcd</x>ef</p>'
  177. );
  178. test(
  179. 'extends over element when next node is a text',
  180. '<p><x>a[]</x>bc</p>',
  181. '<p><x>a[</x>]bc</p>'
  182. );
  183. test(
  184. 'extends over element when next node is a text - backward',
  185. '<p>ab<x>[]c</x></p>',
  186. '<p>ab[<x>]c</x></p>',
  187. { direction: 'backward' }
  188. );
  189. it( 'shrinks over boundary of empty elements', () => {
  190. setData( model, '<p>[</p><p>]</p>', { lastRangeBackward: true } );
  191. modifySelection( model, doc.selection );
  192. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[]</p>' );
  193. expect( doc.selection.isBackward ).to.false;
  194. } );
  195. it( 'shrinks over boundary of empty elements (backward)', () => {
  196. setData( model, '<p>[</p><p>]</p>' );
  197. modifySelection( model, doc.selection, { direction: 'backward' } );
  198. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[]</p><p></p>' );
  199. expect( doc.selection.isBackward ).to.false;
  200. } );
  201. it( 'shrinks over boundary of non-empty elements', () => {
  202. setData( model, '<p>a[</p><p>]b</p>', { lastRangeBackward: true } );
  203. modifySelection( model, doc.selection );
  204. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a</p><p>[]b</p>' );
  205. expect( doc.selection.isBackward ).to.false;
  206. } );
  207. test(
  208. 'shrinks over boundary of non-empty elements (backward)',
  209. '<p>a[</p><p>]b</p>',
  210. '<p>a[]</p><p>b</p>',
  211. { direction: 'backward' }
  212. );
  213. it( 'updates selection attributes', () => {
  214. setData( model, '<p><$text bold="true">foo</$text>[b]</p>' );
  215. modifySelection( model, doc.selection, { direction: 'backward' } );
  216. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p><$text bold="true">foo[]</$text>b</p>' );
  217. expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
  218. } );
  219. } );
  220. describe( 'beyond element – skipping incorrect positions', () => {
  221. beforeEach( () => {
  222. model.schema.register( 'quote' );
  223. model.schema.extend( 'quote', { allowIn: '$root' } );
  224. model.schema.extend( '$block', { allowIn: 'quote' } );
  225. } );
  226. test(
  227. 'skips position at the beginning of an element which does not allow text',
  228. '<p>x[]</p><quote><p>y</p></quote><p>z</p>',
  229. '<p>x[</p><quote><p>]y</p></quote><p>z</p>'
  230. );
  231. test(
  232. 'skips position at the end of an element which does not allow text - backward',
  233. '<p>x</p><quote><p>y</p></quote><p>[]z</p>',
  234. '<p>x</p><quote><p>y[</p></quote><p>]z</p>',
  235. { direction: 'backward' }
  236. );
  237. test(
  238. 'skips position at the end of an element which does not allow text',
  239. '<p>x[</p><quote><p>y]</p></quote><p>z</p>',
  240. '<p>x[</p><quote><p>y</p></quote><p>]z</p>'
  241. );
  242. test(
  243. 'skips position at the beginning of an element which does not allow text - backward',
  244. '<p>x</p><quote><p>[]y</p></quote><p>z</p>',
  245. '<p>x[</p><quote><p>]y</p></quote><p>z</p>',
  246. { direction: 'backward' }
  247. );
  248. test(
  249. 'extends to an empty block after skipping incorrect position',
  250. '<p>x[]</p><quote><p></p></quote><p>z</p>',
  251. '<p>x[</p><quote><p>]</p></quote><p>z</p>'
  252. );
  253. test(
  254. 'extends to an empty block after skipping incorrect position - backward',
  255. '<p>x</p><quote><p></p></quote><p>[]z</p>',
  256. '<p>x</p><quote><p>[</p></quote><p>]z</p>',
  257. { direction: 'backward' }
  258. );
  259. } );
  260. } );
  261. describe( 'unit=codePoint', () => {
  262. it( 'does nothing on empty content', () => {
  263. model.schema.extend( '$text', { allowIn: '$root' } );
  264. setData( model, '' );
  265. modifySelection( model, doc.selection, { unit: 'codePoint' } );
  266. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[]' );
  267. } );
  268. test(
  269. 'does nothing on empty content (with empty element)',
  270. '<p>[]</p>',
  271. '<p>[]</p>',
  272. { unit: 'codePoint' }
  273. );
  274. test(
  275. 'extends one user-perceived character forward - latin letters',
  276. '<p>f[]oo</p>',
  277. '<p>f[o]o</p>',
  278. { unit: 'codePoint' }
  279. );
  280. it( 'extends one user-perceived character backward - latin letters', () => {
  281. setData( model, '<p>fo[]o</p>' );
  282. modifySelection( model, doc.selection, { unit: 'codePoint', direction: 'backward' } );
  283. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>f[o]o</p>' );
  284. expect( doc.selection.isBackward ).to.true;
  285. } );
  286. test(
  287. 'unicode support - combining mark forward',
  288. '<p>foo[]b̂ar</p>',
  289. '<p>foo[b]̂ar</p>',
  290. { unit: 'codePoint' }
  291. );
  292. it( 'unicode support - combining mark backward', () => {
  293. setData( model, '<p>foob̂[]ar</p>' );
  294. // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
  295. // Document's selection will throw errors in some test cases (which are correct cases, but only for
  296. // non-document selections).
  297. const testSelection = new Selection( doc.selection );
  298. modifySelection( model, testSelection, { unit: 'codePoint', direction: 'backward' } );
  299. expect( stringify( doc.getRoot(), testSelection ) ).to.equal( '<p>foob[̂]ar</p>' );
  300. expect( testSelection.isBackward ).to.true;
  301. } );
  302. test(
  303. 'unicode support - combining mark multiple',
  304. '<p>fo[]o̻̐ͩbar</p>',
  305. '<p>fo[o]̻̐ͩbar</p>',
  306. { unit: 'codePoint' }
  307. );
  308. test(
  309. 'unicode support - surrogate pairs forward',
  310. '<p>[]\uD83D\uDCA9</p>',
  311. '<p>[\uD83D\uDCA9]</p>',
  312. { unit: 'codePoint' }
  313. );
  314. it( 'unicode support surrogate pairs backward', () => {
  315. setData( model, '<p>\uD83D\uDCA9[]</p>' );
  316. modifySelection( model, doc.selection, { unit: 'codePoint', direction: 'backward' } );
  317. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
  318. expect( doc.selection.isBackward ).to.true;
  319. } );
  320. } );
  321. describe( 'unit=word', () => {
  322. describe( 'within element', () => {
  323. test(
  324. 'does nothing on empty content',
  325. '[]',
  326. '[]',
  327. { unit: 'word' }
  328. );
  329. test(
  330. 'does nothing on empty content (with empty element)',
  331. '<p>[]</p>',
  332. '<p>[]</p>'
  333. );
  334. test(
  335. 'does nothing on empty content (backward)',
  336. '[]',
  337. '[]',
  338. { unit: 'word', direction: 'backward' }
  339. );
  340. test(
  341. 'does nothing on root boundary',
  342. '<p>foo[]</p>',
  343. '<p>foo[]</p>',
  344. { unit: 'word' }
  345. );
  346. test(
  347. 'does nothing on root boundary (backward)',
  348. '<p>[]foo</p>',
  349. '<p>[]foo</p>',
  350. { unit: 'word', direction: 'backward' }
  351. );
  352. for ( const char of ' ,.?!:;"-()'.split( '' ) ) {
  353. testStopCharacter( char );
  354. }
  355. test(
  356. 'extends whole word forward (non-collapsed)',
  357. '<p>f[o]obar</p>',
  358. '<p>f[oobar]</p>',
  359. { unit: 'word' }
  360. );
  361. it( 'extends whole word backward (non-collapsed)', () => {
  362. setData( model, '<p>foo ba[a]r</p>', { lastRangeBackward: true } );
  363. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  364. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foo [baa]r</p>' );
  365. expect( doc.selection.isBackward ).to.true;
  366. } );
  367. test(
  368. 'extends to element boundary',
  369. '<p>fo[]oo</p>',
  370. '<p>fo[oo]</p>',
  371. { unit: 'word' }
  372. );
  373. it( 'extends to element boundary (backward)', () => {
  374. setData( model, '<p>ff[]oo</p>' );
  375. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  376. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[ff]oo</p>' );
  377. expect( doc.selection.isBackward ).to.true;
  378. } );
  379. test(
  380. 'expands forward selection to the word start',
  381. '<p>foo bar[b]az</p>',
  382. '<p>foo [bar]baz</p>',
  383. { unit: 'word', direction: 'backward' }
  384. );
  385. it( 'expands backward selection to the word end', () => {
  386. setData( model, '<p>foo[b]ar baz</p>', { lastRangeBackward: true } );
  387. modifySelection( model, doc.selection, { unit: 'word' } );
  388. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>foob[ar] baz</p>' );
  389. expect( doc.selection.isBackward ).to.false;
  390. } );
  391. test(
  392. 'unicode support - combining mark forward',
  393. '<p>foo[]b̂ar</p>',
  394. '<p>foo[b̂ar]</p>',
  395. { unit: 'word' }
  396. );
  397. it( 'unicode support - combining mark backward', () => {
  398. setData( model, '<p>foob̂[]ar</p>' );
  399. modifySelection( model, doc.selection, { direction: 'backward', unit: 'word' } );
  400. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[foob̂]ar</p>' );
  401. expect( doc.selection.isBackward ).to.true;
  402. } );
  403. test(
  404. 'unicode support - combining mark multiple',
  405. '<p>fo[]o̻̐ͩbar</p>',
  406. '<p>fo[o̻̐ͩbar]</p>',
  407. { unit: 'word' }
  408. );
  409. it( 'unicode support - combining mark multiple backward', () => {
  410. setData( model, '<p>foo̻̐ͩ[]bar</p>' );
  411. modifySelection( model, doc.selection, { direction: 'backward' } );
  412. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>fo[o̻̐ͩ]bar</p>' );
  413. expect( doc.selection.isBackward ).to.true;
  414. } );
  415. test(
  416. 'unicode support - combining mark to the end',
  417. '<p>f[o]o̻̐ͩ</p>',
  418. '<p>f[oo̻̐ͩ]</p>',
  419. { unit: 'word' }
  420. );
  421. test(
  422. 'unicode support - surrogate pairs forward',
  423. '<p>[]foo\uD83D\uDCA9</p>',
  424. '<p>[foo\uD83D\uDCA9]</p>',
  425. { unit: 'word' }
  426. );
  427. it( 'unicode support - surrogate pairs backward', () => {
  428. setData( model, '<p>foo\uD83D\uDCA9[]</p>' );
  429. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  430. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[foo\uD83D\uDCA9]</p>' );
  431. expect( doc.selection.isBackward ).to.true;
  432. } );
  433. it( 'expands backward selection to the word begin in the paragraph with soft break', () => {
  434. setData( model, '<p>Foo<br></br>Bar[]</p>' );
  435. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  436. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>Foo<br></br>[Bar]</p>' );
  437. } );
  438. it( 'expands backward selection to the whole paragraph in the paragraph with soft break', () => {
  439. setData( model, '<p>Foo<br></br>Bar[]</p>' );
  440. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  441. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  442. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>Foo[<br></br>Bar]</p>' );
  443. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  444. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[Foo<br></br>Bar]</p>' );
  445. } );
  446. it( 'expands forward selection to the word end in the paragraph with soft break', () => {
  447. setData( model, '<p>[]Foo<br></br>Bar</p>' );
  448. modifySelection( model, doc.selection, { unit: 'word', direction: 'forward' } );
  449. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[Foo]<br></br>Bar</p>' );
  450. } );
  451. it( 'expands forward selection to the whole paragraph in the paragraph with soft break', () => {
  452. setData( model, '<p>[]Foo<br></br>Bar</p>' );
  453. modifySelection( model, doc.selection, { unit: 'word', direction: 'forward' } );
  454. modifySelection( model, doc.selection, { unit: 'word', direction: 'forward' } );
  455. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[Foo<br></br>]Bar</p>' );
  456. modifySelection( model, doc.selection, { unit: 'word', direction: 'forward' } );
  457. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[Foo<br></br>Bar]</p>' );
  458. } );
  459. function testStopCharacter( stopCharacter ) {
  460. describe( `stop character: "${ stopCharacter }"`, () => {
  461. test(
  462. 'extends whole word forward',
  463. `<p>f[]oo${ stopCharacter }bar</p>`,
  464. `<p>f[oo]${ stopCharacter }bar</p>`,
  465. { unit: 'word' }
  466. );
  467. it( 'extends whole word backward to the previous word', () => {
  468. setData( model, `<p>foo${ stopCharacter }ba[]r</p>`, { lastRangeBackward: true } );
  469. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  470. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `<p>foo${ stopCharacter }[ba]r</p>` );
  471. expect( doc.selection.isBackward ).to.true;
  472. } );
  473. it( 'extends whole word backward', () => {
  474. setData( model, `<p>fo[]o${ stopCharacter }bar</p>`, { lastRangeBackward: true } );
  475. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  476. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `<p>[fo]o${ stopCharacter }bar</p>` );
  477. expect( doc.selection.isBackward ).to.true;
  478. } );
  479. test(
  480. 'ignores attributes when in one word - case 1',
  481. `<p>foo[]<$text bold="true">bar</$text>baz${ stopCharacter }foobarbaz</p>`,
  482. `<p>foo[<$text bold="true">bar</$text>baz]${ stopCharacter }foobarbaz</p>`,
  483. { unit: 'word' }
  484. );
  485. test(
  486. 'ignores attributes when in one word - case 2',
  487. `<p>foo[]<$text bold="true">bar</$text>${ stopCharacter }foobarbaz</p>`,
  488. `<p>foo[<$text bold="true">bar</$text>]${ stopCharacter }foobarbaz</p>`,
  489. { unit: 'word' }
  490. );
  491. test(
  492. 'ignores attributes when in one word - case 3',
  493. `<p>foo[]<$text bold="true">bar</$text><$text italic="true">baz</$text>baz${ stopCharacter }foobarbaz</p>`,
  494. `<p>foo[<$text bold="true">bar</$text><$text italic="true">baz</$text>baz]${ stopCharacter }foobarbaz</p>`,
  495. { unit: 'word' }
  496. );
  497. it( 'extends whole word backward to the previous word ignoring attributes - case 1', () => {
  498. setData(
  499. model,
  500. `<p>foobarbaz${ stopCharacter }foo<$text bold="true">bar</$text>baz[]</p>`
  501. );
  502. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  503. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal(
  504. `<p>foobarbaz${ stopCharacter }[foo<$text bold="true">bar</$text>baz]</p>`
  505. );
  506. expect( doc.selection.isBackward ).to.true;
  507. } );
  508. it( 'extends whole word backward to the previous word ignoring attributes - case 2', () => {
  509. setData(
  510. model,
  511. `<p>foobarbaz${ stopCharacter }<$text bold="true">bar</$text>baz[]</p>`
  512. );
  513. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  514. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal(
  515. `<p>foobarbaz${ stopCharacter }[<$text bold="true">bar</$text>baz]</p>`
  516. );
  517. expect( doc.selection.isBackward ).to.true;
  518. } );
  519. } );
  520. }
  521. } );
  522. describe( 'beyond element', () => {
  523. test(
  524. 'extends over boundary of empty elements',
  525. '<p>[]</p><p></p><p></p>',
  526. '<p>[</p><p>]</p><p></p>',
  527. { unit: 'word' }
  528. );
  529. it( 'extends over boundary of empty elements (backward)', () => {
  530. setData( model, '<p></p><p></p><p>[]</p>' );
  531. modifySelection( model, doc.selection, { direction: 'backward' } );
  532. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[</p><p>]</p>' );
  533. expect( doc.selection.isBackward ).to.true;
  534. } );
  535. test(
  536. 'extends over boundary of non-empty elements',
  537. '<p>a[]</p><p>bcd</p>',
  538. '<p>a[</p><p>]bcd</p>',
  539. { unit: 'word' }
  540. );
  541. it( 'extends over boundary of non-empty elements (backward)', () => {
  542. setData( model, '<p>a</p><p>[]bcd</p>' );
  543. modifySelection( model, doc.selection, { direction: 'backward' } );
  544. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a[</p><p>]bcd</p>' );
  545. expect( doc.selection.isBackward ).to.true;
  546. } );
  547. test(
  548. 'extends over character after boundary',
  549. '<p>a[</p><p>]bcd</p>',
  550. '<p>a[</p><p>bcd]</p>',
  551. { unit: 'word' }
  552. );
  553. it( 'extends over character after boundary (backward)', () => {
  554. setData( model, '<p>abc[</p><p>]d</p>', { lastRangeBackward: true } );
  555. modifySelection( model, doc.selection, { direction: 'backward' } );
  556. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>ab[c</p><p>]d</p>' );
  557. expect( doc.selection.isBackward ).to.true;
  558. } );
  559. test(
  560. 'stops on the first position where text is allowed - inside block',
  561. '<p>a[]</p><p><x>bcd</x></p>',
  562. '<p>a[</p><p>]<x>bcd</x></p>',
  563. { unit: 'word' }
  564. );
  565. test(
  566. 'stops on the first position where text is allowed - inside inline element',
  567. '<p>a[</p><p>]<x>bcd</x>ef</p>',
  568. '<p>a[</p><p><x>]bcd</x>ef</p>',
  569. { unit: 'word' }
  570. );
  571. test(
  572. 'extends over element when next node is a text',
  573. '<p><x>a[]</x>bc</p>',
  574. '<p><x>a[</x>]bc</p>',
  575. { unit: 'word' }
  576. );
  577. test(
  578. 'extends over element when next node is a text - backward',
  579. '<p>ab<x>[]c</x></p>',
  580. '<p>ab[<x>]c</x></p>',
  581. { unit: 'word', direction: 'backward' }
  582. );
  583. it( 'shrinks over boundary of empty elements', () => {
  584. setData( model, '<p>[</p><p>]</p>', { lastRangeBackward: true } );
  585. modifySelection( model, doc.selection, { unit: 'word' } );
  586. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p></p><p>[]</p>' );
  587. expect( doc.selection.isBackward ).to.false;
  588. } );
  589. it( 'shrinks over boundary of empty elements (backward)', () => {
  590. setData( model, '<p>[</p><p>]</p>' );
  591. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  592. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>[]</p><p></p>' );
  593. expect( doc.selection.isBackward ).to.false;
  594. } );
  595. it( 'shrinks over boundary of non-empty elements', () => {
  596. setData( model, '<p>a[</p><p>]b</p>', { lastRangeBackward: true } );
  597. modifySelection( model, doc.selection, { unit: 'word' } );
  598. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p>a</p><p>[]b</p>' );
  599. expect( doc.selection.isBackward ).to.false;
  600. } );
  601. test(
  602. 'shrinks over boundary of non-empty elements (backward)',
  603. '<p>a[</p><p>]b</p>',
  604. '<p>a[]</p><p>b</p>',
  605. { unit: 'word', direction: 'backward' }
  606. );
  607. it( 'updates selection attributes', () => {
  608. setData( model, '<p><$text bold="true">foo</$text>[b]</p>' );
  609. modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } );
  610. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<p><$text bold="true">foo[]</$text>b</p>' );
  611. expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
  612. } );
  613. } );
  614. describe( 'beyond element – skipping incorrect positions', () => {
  615. beforeEach( () => {
  616. model.schema.register( 'quote' );
  617. model.schema.extend( 'quote', { allowIn: '$root' } );
  618. model.schema.extend( '$block', { allowIn: 'quote' } );
  619. } );
  620. test(
  621. 'skips position at the beginning of an element which does not allow text',
  622. '<p>x[]</p><quote><p>y</p></quote><p>z</p>',
  623. '<p>x[</p><quote><p>]y</p></quote><p>z</p>',
  624. { unit: 'word' }
  625. );
  626. test(
  627. 'skips position at the end of an element which does not allow text - backward',
  628. '<p>x</p><quote><p>y</p></quote><p>[]z</p>',
  629. '<p>x</p><quote><p>y[</p></quote><p>]z</p>',
  630. { unit: 'word', direction: 'backward' }
  631. );
  632. test(
  633. 'skips position at the end of an element which does not allow text',
  634. '<p>x[</p><quote><p>y]</p></quote><p>z</p>',
  635. '<p>x[</p><quote><p>y</p></quote><p>]z</p>',
  636. { unit: 'word' }
  637. );
  638. test(
  639. 'skips position at the beginning of an element which does not allow text - backward',
  640. '<p>x</p><quote><p>[]y</p></quote><p>z</p>',
  641. '<p>x[</p><quote><p>]y</p></quote><p>z</p>',
  642. { unit: 'word', direction: 'backward' }
  643. );
  644. test(
  645. 'extends to an empty block after skipping incorrect position',
  646. '<p>x[]</p><quote><p></p></quote><p>z</p>',
  647. '<p>x[</p><quote><p>]</p></quote><p>z</p>',
  648. { unit: 'word' }
  649. );
  650. test(
  651. 'extends to an empty block after skipping incorrect position - backward',
  652. '<p>x</p><quote><p></p></quote><p>[]z</p>',
  653. '<p>x</p><quote><p>[</p></quote><p>]z</p>',
  654. { unit: 'word', direction: 'backward' }
  655. );
  656. } );
  657. } );
  658. describe( 'objects handling', () => {
  659. beforeEach( () => {
  660. model.schema.register( 'obj', {
  661. isObject: true
  662. } );
  663. model.schema.extend( 'obj', { allowIn: '$root' } );
  664. model.schema.extend( '$text', { allowIn: 'obj' } );
  665. model.schema.register( 'inlineObj', {
  666. allowIn: 'p',
  667. isObject: true
  668. } );
  669. model.schema.extend( '$text', { allowIn: 'inlineObj' } );
  670. } );
  671. test(
  672. 'extends over next object element when at the end of an element',
  673. '<p>foo[]</p><obj>bar</obj>',
  674. '<p>foo[</p><obj>bar</obj>]'
  675. );
  676. test(
  677. 'extends over previous object element when at the beginning of an element ',
  678. '<obj>bar</obj><p>[]foo</p>',
  679. '[<obj>bar</obj><p>]foo</p>',
  680. { direction: 'backward' }
  681. );
  682. test(
  683. 'extends over object elements - forward',
  684. '[<obj></obj>]<obj></obj>',
  685. '[<obj></obj><obj></obj>]'
  686. );
  687. it( 'extends over object elements - backward', () => {
  688. setData( model, '<obj></obj>[<obj></obj>]', { lastRangeBackward: true } );
  689. modifySelection( model, doc.selection, { direction: 'backward' } );
  690. expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[<obj></obj><obj></obj>]' );
  691. expect( doc.selection.isBackward ).to.true;
  692. } );
  693. test(
  694. 'extends over inline objects - forward',
  695. '<p>foo[]<inlineObj>bar</inlineObj></p>',
  696. '<p>foo[<inlineObj>bar</inlineObj>]</p>'
  697. );
  698. test(
  699. 'extends over inline objects - backward',
  700. '<p><inlineObj>bar</inlineObj>[]foo</p>',
  701. '<p>[<inlineObj>bar</inlineObj>]foo</p>',
  702. { direction: 'backward' }
  703. );
  704. test(
  705. 'extends over empty inline objects - forward',
  706. '<p>foo[]<inlineObj></inlineObj></p>',
  707. '<p>foo[<inlineObj></inlineObj>]</p>'
  708. );
  709. test(
  710. 'extends over empty inline objects - backward',
  711. '<p><inlineObj></inlineObj>[]foo</p>',
  712. '<p>[<inlineObj></inlineObj>]foo</p>',
  713. { direction: 'backward' }
  714. );
  715. } );
  716. describe( 'limits handling', () => {
  717. beforeEach( () => {
  718. model.schema.register( 'inlineLimit', {
  719. isLimit: true
  720. } );
  721. model.schema.extend( 'inlineLimit', { allowIn: '$block' } );
  722. model.schema.extend( '$text', { allowIn: 'inlineLimit' } );
  723. model.schema.register( 'blockLimit', {
  724. isLimit: true
  725. } );
  726. model.schema.extend( 'blockLimit', { allowIn: '$root' } );
  727. model.schema.extend( 'p', { allowIn: 'blockLimit' } );
  728. } );
  729. test(
  730. 'should not extend to outside of inline limit element',
  731. '<p>x<inlineLimit>foo[]</inlineLimit>x</p>',
  732. '<p>x<inlineLimit>foo[]</inlineLimit>x</p>'
  733. );
  734. test(
  735. 'should not extend to outside of inline limit element - backward',
  736. '<p>x<inlineLimit>[]foo</inlineLimit>x</p>',
  737. '<p>x<inlineLimit>[]foo</inlineLimit>x</p>',
  738. { direction: 'backward' }
  739. );
  740. test(
  741. 'should not extend to outside of block limit element',
  742. '<p>x</p><blockLimit><p>foo[]</p></blockLimit><p>x</p>',
  743. '<p>x</p><blockLimit><p>foo[]</p></blockLimit><p>x</p>'
  744. );
  745. test(
  746. 'should not extend to outside of block limit element - backward',
  747. '<p>x</p><blockLimit><p>[]foo</p></blockLimit><p>x</p>',
  748. '<p>x</p><blockLimit><p>[]foo</p></blockLimit><p>x</p>',
  749. { direction: 'backward' }
  750. );
  751. // This may seem counterintuitive but it makes sense. The limit element means
  752. // that it can't be left or modified from inside. If you want the same behavior from outside
  753. // register it as an object.
  754. test(
  755. 'should enter a limit element',
  756. '<p>foo[]</p><blockLimit><p>x</p></blockLimit>',
  757. '<p>foo[</p><blockLimit><p>]x</p></blockLimit>'
  758. );
  759. test(
  760. 'should enter a limit element - backward',
  761. '<blockLimit><p>x</p></blockLimit><p>[]foo</p>',
  762. '<blockLimit><p>x[</p></blockLimit><p>]foo</p>',
  763. { direction: 'backward' }
  764. );
  765. } );
  766. } );
  767. function test( title, input, output, options ) {
  768. it( title, () => {
  769. input = input.normalize();
  770. output = output.normalize();
  771. setData( model, input );
  772. // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
  773. // Document's selection will throw errors in some test cases (which are correct cases, but only for
  774. // non-document selections).
  775. const testSelection = new Selection( doc.selection );
  776. modifySelection( model, testSelection, options );
  777. expect( stringify( doc.getRoot(), testSelection ) ).to.equal( output );
  778. } );
  779. }
  780. } );