modifyselection.js 14 KB

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