modifyselection.js 31 KB

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