8
0

modifyselection.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Document from '../../src/model/document';
  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', () => {
  11. let document, dataController;
  12. beforeEach( () => {
  13. document = new Document();
  14. dataController = new DataController( document );
  15. document.schema.registerItem( 'p', '$block' );
  16. document.schema.registerItem( 'x', '$block' );
  17. document.schema.registerItem( 'img', '$inline' );
  18. document.schema.allow( { name: '$text', inside: '$root' } );
  19. document.schema.allow( { name: '$text', inside: 'img' } );
  20. document.schema.allow( { name: '$text', inside: 'obj' } );
  21. document.schema.allow( { name: '$text', inside: 'inlineObj' } );
  22. document.schema.allow( { name: 'x', inside: 'p' } );
  23. document.createRoot();
  24. } );
  25. describe( 'modifySelection', () => {
  26. describe( 'unit=character', () => {
  27. describe( 'within element', () => {
  28. test(
  29. 'does nothing on empty content',
  30. '[]',
  31. '[]'
  32. );
  33. test(
  34. 'does nothing on empty content (with empty element)',
  35. '<p>[]</p>',
  36. '<p>[]</p>'
  37. );
  38. test(
  39. 'does nothing on empty content (backward)',
  40. '[]',
  41. '[]',
  42. { direction: 'backward' }
  43. );
  44. test(
  45. 'does nothing on root boundary',
  46. '<p>foo[]</p>',
  47. '<p>foo[]</p>'
  48. );
  49. test(
  50. 'does nothing on root boundary (backward)',
  51. '<p>[]foo</p>',
  52. '<p>[]foo</p>',
  53. { direction: 'backward' }
  54. );
  55. test(
  56. 'extends one character forward',
  57. '<p>f[]oo</p>',
  58. '<p>f[o]o</p>'
  59. );
  60. it( 'extends one character backward', () => {
  61. setData( document, '<p>fo[]o</p>', { lastRangeBackward: true } );
  62. modifySelection( dataController, document.selection, { direction: 'backward' } );
  63. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>f[o]o</p>' );
  64. expect( document.selection.isBackward ).to.true;
  65. } );
  66. test(
  67. 'extends one character forward (non-collapsed)',
  68. '<p>f[o]obar</p>',
  69. '<p>f[oo]bar</p>'
  70. );
  71. it( 'extends one character backward (non-collapsed)', () => {
  72. setData( document, '<p>foob[a]r</p>', { lastRangeBackward: true } );
  73. modifySelection( dataController, document.selection, { direction: 'backward' } );
  74. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foo[ba]r</p>' );
  75. expect( document.selection.isBackward ).to.true;
  76. } );
  77. test(
  78. 'extends to element boundary',
  79. '<p>fo[]o</p>',
  80. '<p>fo[o]</p>'
  81. );
  82. it( 'extends to element boundary (backward)', () => {
  83. setData( document, '<p>f[]oo</p>' );
  84. modifySelection( dataController, document.selection, { direction: 'backward' } );
  85. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[f]oo</p>' );
  86. expect( document.selection.isBackward ).to.true;
  87. } );
  88. test(
  89. 'shrinks forward selection (to collapsed)',
  90. '<p>foo[b]ar</p>',
  91. '<p>foo[]bar</p>',
  92. { direction: 'backward' }
  93. );
  94. it( 'shrinks backward selection (to collapsed)', () => {
  95. setData( document, '<p>foo[b]ar</p>', { lastRangeBackward: true } );
  96. modifySelection( dataController, document.selection );
  97. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foob[]ar</p>' );
  98. expect( document.selection.isBackward ).to.false;
  99. } );
  100. test(
  101. 'extends one element forward',
  102. '<p>f[]<img></img>oo</p>',
  103. '<p>f[<img></img>]oo</p>'
  104. );
  105. test(
  106. 'extends one non-empty element forward',
  107. '<p>f[]<img>x</img>oo</p>',
  108. '<p>f[<img>x</img>]oo</p>'
  109. );
  110. it( 'extends one element backward', () => {
  111. setData( document, '<p>fo<img></img>[]o</p>' );
  112. modifySelection( dataController, document.selection, { direction: 'backward' } );
  113. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>fo[<img></img>]o</p>' );
  114. expect( document.selection.isBackward ).to.true;
  115. } );
  116. test(
  117. 'unicode support - combining mark forward',
  118. '<p>foo[]b̂ar</p>',
  119. '<p>foo[b̂]ar</p>'
  120. );
  121. it( 'unicode support - combining mark backward', () => {
  122. setData( document, '<p>foob̂[]ar</p>' );
  123. modifySelection( dataController, document.selection, { direction: 'backward' } );
  124. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>foo[b̂]ar</p>' );
  125. expect( document.selection.isBackward ).to.true;
  126. } );
  127. test(
  128. 'unicode support - combining mark multiple',
  129. '<p>fo[]o̻̐ͩbar</p>',
  130. '<p>fo[o̻̐ͩ]bar</p>'
  131. );
  132. it( 'unicode support - combining mark multiple backward', () => {
  133. setData( document, '<p>foo̻̐ͩ[]bar</p>' );
  134. modifySelection( dataController, document.selection, { direction: 'backward' } );
  135. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>fo[o̻̐ͩ]bar</p>' );
  136. expect( document.selection.isBackward ).to.true;
  137. } );
  138. test(
  139. 'unicode support - combining mark to the end',
  140. '<p>fo[]o̻̐ͩ</p>',
  141. '<p>fo[o̻̐ͩ]</p>'
  142. );
  143. test(
  144. 'unicode support - surrogate pairs forward',
  145. '<p>[]\uD83D\uDCA9</p>',
  146. '<p>[\uD83D\uDCA9]</p>'
  147. );
  148. it( 'unicode support - surrogate pairs backward', () => {
  149. setData( document, '<p>\uD83D\uDCA9[]</p>' );
  150. modifySelection( dataController, document.selection, { direction: 'backward' } );
  151. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
  152. expect( document.selection.isBackward ).to.true;
  153. } );
  154. } );
  155. describe( 'beyond element', () => {
  156. test(
  157. 'extends over boundary of empty elements',
  158. '<p>[]</p><p></p><p></p>',
  159. '<p>[</p><p>]</p><p></p>'
  160. );
  161. it( 'extends over boundary of empty elements (backward)', () => {
  162. setData( document, '<p></p><p></p><p>[]</p>' );
  163. modifySelection( dataController, document.selection, { direction: 'backward' } );
  164. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p></p><p>[</p><p>]</p>' );
  165. expect( document.selection.isBackward ).to.true;
  166. } );
  167. test(
  168. 'extends over boundary of non-empty elements',
  169. '<p>a[]</p><p>bcd</p>',
  170. '<p>a[</p><p>]bcd</p>'
  171. );
  172. it( 'extends over boundary of non-empty elements (backward)', () => {
  173. setData( document, '<p>a</p><p>[]bcd</p>' );
  174. modifySelection( dataController, document.selection, { direction: 'backward' } );
  175. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>a[</p><p>]bcd</p>' );
  176. expect( document.selection.isBackward ).to.true;
  177. } );
  178. test(
  179. 'extends over character after boundary',
  180. '<p>a[</p><p>]bcd</p>',
  181. '<p>a[</p><p>b]cd</p>'
  182. );
  183. it( 'extends over character after boundary (backward)', () => {
  184. setData( document, '<p>abc[</p><p>]d</p>', { lastRangeBackward: true } );
  185. modifySelection( dataController, document.selection, { direction: 'backward' } );
  186. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>ab[c</p><p>]d</p>' );
  187. expect( document.selection.isBackward ).to.true;
  188. } );
  189. test(
  190. 'extends over boundary when next element has nested elements',
  191. '<p>a[]</p><p><x>bcd</x></p>',
  192. '<p>a[</p><p>]<x>bcd</x></p>'
  193. );
  194. test(
  195. 'extends over element when next element has nested elements',
  196. '<p>a[</p><p>]<x>bcd</x>ef</p>',
  197. '<p>a[</p><p><x>bcd</x>]ef</p>'
  198. );
  199. test(
  200. 'extends over element when next node is a text',
  201. '<p>a[]</p>bc',
  202. '<p>a[</p>]bc'
  203. );
  204. it( 'extends over element when next node is a text (backward)', () => {
  205. setData( document, 'ab<p>[]c</p>' );
  206. modifySelection( dataController, document.selection, { direction: 'backward' } );
  207. expect( stringify( document.getRoot(), document.selection ) ).to.equal( 'ab[<p>]c</p>' );
  208. expect( document.selection.isBackward ).to.true;
  209. } );
  210. it( 'shrinks over boundary of empty elements', () => {
  211. setData( document, '<p>[</p><p>]</p>', { lastRangeBackward: true } );
  212. modifySelection( dataController, document.selection );
  213. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p></p><p>[]</p>' );
  214. expect( document.selection.isBackward ).to.false;
  215. } );
  216. it( 'shrinks over boundary of empty elements (backward)', () => {
  217. setData( document, '<p>[</p><p>]</p>' );
  218. modifySelection( dataController, document.selection, { direction: 'backward' } );
  219. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[]</p><p></p>' );
  220. expect( document.selection.isBackward ).to.false;
  221. } );
  222. it( 'shrinks over boundary of non-empty elements', () => {
  223. setData( document, '<p>a[</p><p>]b</p>', { lastRangeBackward: true } );
  224. modifySelection( dataController, document.selection );
  225. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>a</p><p>[]b</p>' );
  226. expect( document.selection.isBackward ).to.false;
  227. } );
  228. test(
  229. 'shrinks over boundary of non-empty elements (backward)',
  230. '<p>a[</p><p>]b</p>',
  231. '<p>a[]</p><p>b</p>',
  232. { direction: 'backward' }
  233. );
  234. it( 'updates selection attributes', () => {
  235. setData( document, '<p><$text bold="true">foo</$text>[b]</p>' );
  236. modifySelection( dataController, document.selection, { direction: 'backward' } );
  237. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p><$text bold="true">foo[]</$text>b</p>' );
  238. expect( document.selection.getAttribute( 'bold' ) ).to.equal( true );
  239. } );
  240. } );
  241. } );
  242. describe( 'unit=codePoint', () => {
  243. test(
  244. 'does nothing on empty content',
  245. '[]',
  246. '[]',
  247. { unit: 'codePoint' }
  248. );
  249. test(
  250. 'does nothing on empty content (with empty element)',
  251. '<p>[]</p>',
  252. '<p>[]</p>',
  253. { unit: 'codePoint' }
  254. );
  255. test(
  256. 'extends one user-perceived character forward - latin letters',
  257. '<p>f[]oo</p>',
  258. '<p>f[o]o</p>',
  259. { unit: 'codePoint' }
  260. );
  261. it( 'extends one user-perceived character backward - latin letters', () => {
  262. setData( document, '<p>fo[]o</p>' );
  263. modifySelection( dataController, document.selection, { unit: 'codePoint', direction: 'backward' } );
  264. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>f[o]o</p>' );
  265. expect( document.selection.isBackward ).to.true;
  266. } );
  267. test(
  268. 'unicode support - combining mark forward',
  269. '<p>foo[]b̂ar</p>',
  270. '<p>foo[b]̂ar</p>',
  271. { unit: 'codePoint' }
  272. );
  273. it( 'unicode support - combining mark backward', () => {
  274. setData( document, '<p>foob̂[]ar</p>' );
  275. // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
  276. // Document's selection will throw errors in some test cases (which are correct cases, but only for
  277. // non-document selections).
  278. const testSelection = Selection.createFromSelection( document.selection );
  279. modifySelection( dataController, testSelection, { unit: 'codePoint', direction: 'backward' } );
  280. expect( stringify( document.getRoot(), testSelection ) ).to.equal( '<p>foob[̂]ar</p>' );
  281. expect( testSelection.isBackward ).to.true;
  282. } );
  283. test(
  284. 'unicode support - combining mark multiple',
  285. '<p>fo[]o̻̐ͩbar</p>',
  286. '<p>fo[o]̻̐ͩbar</p>',
  287. { unit: 'codePoint' }
  288. );
  289. test(
  290. 'unicode support - surrogate pairs forward',
  291. '<p>[]\uD83D\uDCA9</p>',
  292. '<p>[\uD83D\uDCA9]</p>',
  293. { unit: 'codePoint' }
  294. );
  295. it( 'unicode support surrogate pairs backward', () => {
  296. setData( document, '<p>\uD83D\uDCA9[]</p>' );
  297. modifySelection( dataController, document.selection, { unit: 'codePoint', direction: 'backward' } );
  298. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '<p>[\uD83D\uDCA9]</p>' );
  299. expect( document.selection.isBackward ).to.true;
  300. } );
  301. } );
  302. describe( 'objects handling', () => {
  303. beforeEach( () => {
  304. document.schema.registerItem( 'obj' );
  305. document.schema.allow( { name: 'obj', inside: '$root' } );
  306. document.schema.objects.add( 'obj' );
  307. document.schema.registerItem( 'inlineObj', '$inline' );
  308. document.schema.objects.add( 'inlineObj' );
  309. } );
  310. test(
  311. 'extends over next object element when at the end of an element',
  312. '<p>foo[]</p><obj>bar</obj>',
  313. '<p>foo[</p><obj>bar</obj>]'
  314. );
  315. test(
  316. 'extends over previous object element when at the beginning of an element ',
  317. '<obj>bar</obj><p>[]foo</p>',
  318. '[<obj>bar</obj><p>]foo</p>',
  319. { direction: 'backward' }
  320. );
  321. test(
  322. 'extends over object elements - forward',
  323. '[<obj></obj>]<obj></obj>',
  324. '[<obj></obj><obj></obj>]'
  325. );
  326. it( 'extends over object elements - backward', () => {
  327. setData( document, '<obj></obj>[<obj></obj>]', { lastRangeBackward: true } );
  328. modifySelection( dataController, document.selection, { direction: 'backward' } );
  329. expect( stringify( document.getRoot(), document.selection ) ).to.equal( '[<obj></obj><obj></obj>]' );
  330. expect( document.selection.isBackward ).to.true;
  331. } );
  332. test(
  333. 'extends over inline objects - forward',
  334. '<p>foo[]<inlineObj>bar</inlineObj></p>',
  335. '<p>foo[<inlineObj>bar</inlineObj>]</p>'
  336. );
  337. test(
  338. 'extends over inline objects - backward',
  339. '<p><inlineObj>bar</inlineObj>[]foo</p>',
  340. '<p>[<inlineObj>bar</inlineObj>]foo</p>',
  341. { direction: 'backward' }
  342. );
  343. } );
  344. } );
  345. function test( title, input, output, options ) {
  346. it( title, () => {
  347. input = input.normalize();
  348. output = output.normalize();
  349. setData( document, input );
  350. // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection.
  351. // Document's selection will throw errors in some test cases (which are correct cases, but only for
  352. // non-document selections).
  353. const testSelection = Selection.createFromSelection( document.selection );
  354. modifySelection( dataController, testSelection, options );
  355. expect( stringify( document.getRoot(), testSelection ) ).to.equal( output );
  356. } );
  357. }
  358. } );