8
0

selection.js 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018
  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 Selection from '../../src/view/selection';
  6. import DocumentSelection from '../../src/view/documentselection';
  7. import Range from '../../src/view/range';
  8. import Document from '../../src/view/document';
  9. import Element from '../../src/view/element';
  10. import Text from '../../src/view/text';
  11. import Position from '../../src/view/position';
  12. import count from '@ckeditor/ckeditor5-utils/src/count';
  13. import createViewRoot from './_utils/createroot';
  14. import { parse } from '../../src/dev-utils/view';
  15. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  16. import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  17. import { StylesProcessor } from '../../src/view/stylesmap';
  18. describe( 'Selection', () => {
  19. let selection, el, range1, range2, range3, viewDocument;
  20. testUtils.createSinonSandbox();
  21. beforeEach( () => {
  22. viewDocument = new Document( new StylesProcessor() );
  23. const text = new Text( viewDocument, 'xxxxxxxxxxxxxxxxxxxx' );
  24. el = new Element( viewDocument, 'p', null, text );
  25. selection = new Selection();
  26. range1 = Range._createFromParentsAndOffsets( text, 5, text, 10 );
  27. range2 = Range._createFromParentsAndOffsets( text, 1, text, 2 );
  28. range3 = Range._createFromParentsAndOffsets( text, 12, text, 14 );
  29. } );
  30. describe( 'constructor()', () => {
  31. it( 'should be able to create an empty selection', () => {
  32. const selection = new Selection();
  33. expect( Array.from( selection.getRanges() ) ).to.deep.equal( [] );
  34. } );
  35. it( 'should be able to create a selection from the given ranges', () => {
  36. const ranges = [ range1, range2, range3 ];
  37. const selection = new Selection( ranges );
  38. expect( Array.from( selection.getRanges() ) ).to.deep.equal( ranges );
  39. } );
  40. it( 'should be able to create a selection from the given ranges and isLastBackward flag', () => {
  41. const ranges = [ range1, range2, range3 ];
  42. const selection = new Selection( ranges, { backward: true } );
  43. expect( selection.isBackward ).to.be.true;
  44. } );
  45. it( 'should be able to create a selection from the given range and isLastBackward flag', () => {
  46. const selection = new Selection( range1, { backward: true } );
  47. expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] );
  48. expect( selection.isBackward ).to.be.true;
  49. } );
  50. it( 'should be able to create a selection from the given iterable of ranges and isLastBackward flag', () => {
  51. const ranges = new Set( [ range1, range2, range3 ] );
  52. const selection = new Selection( ranges, { backward: false } );
  53. expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2, range3 ] );
  54. expect( selection.isBackward ).to.be.false;
  55. } );
  56. it( 'should be able to create a collapsed selection at the given position', () => {
  57. const position = range1.start;
  58. const selection = new Selection( position );
  59. expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
  60. expect( selection.getFirstRange().start ).to.deep.equal( position );
  61. expect( selection.getFirstRange().end ).to.deep.equal( position );
  62. expect( selection.isBackward ).to.be.false;
  63. } );
  64. it( 'should be able to create a collapsed selection at the given position', () => {
  65. const position = range1.start;
  66. const selection = new Selection( position );
  67. expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
  68. expect( selection.getFirstRange().start ).to.deep.equal( position );
  69. expect( selection.getFirstRange().end ).to.deep.equal( position );
  70. expect( selection.isBackward ).to.be.false;
  71. } );
  72. it( 'should be able to create a selection from the other selection', () => {
  73. const otherSelection = new Selection( [ range2, range3 ], { backward: true } );
  74. const selection = new Selection( otherSelection );
  75. expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range2, range3 ] );
  76. expect( selection.isBackward ).to.be.true;
  77. } );
  78. it( 'should be able to create a selection from the other document selection', () => {
  79. const otherSelection = new DocumentSelection( [ range2, range3 ], { backward: true } );
  80. const selection = new Selection( otherSelection );
  81. expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range2, range3 ] );
  82. expect( selection.isBackward ).to.be.true;
  83. } );
  84. it( 'should be able to create a fake selection from the other fake selection', () => {
  85. const otherSelection = new Selection( [ range2, range3 ], { fake: true, label: 'foo bar baz' } );
  86. const selection = new Selection( otherSelection );
  87. expect( selection.isFake ).to.be.true;
  88. expect( selection.fakeSelectionLabel ).to.equal( 'foo bar baz' );
  89. } );
  90. it( 'should throw an error when range is invalid', () => {
  91. expectToThrowCKEditorError( () => {
  92. // eslint-disable-next-line no-new
  93. new Selection( [ { invalid: 'range' } ] );
  94. }, /view-selection-add-range-not-range/ );
  95. } );
  96. it( 'should throw an error when ranges intersects', () => {
  97. const text = el.getChild( 0 );
  98. const range2 = Range._createFromParentsAndOffsets( text, 7, text, 15 );
  99. expectToThrowCKEditorError( () => {
  100. // eslint-disable-next-line no-new
  101. new Selection( [ range1, range2 ] );
  102. }, 'view-selection-range-intersects' );
  103. } );
  104. it( 'should throw an error when trying to set to not selectable', () => {
  105. expectToThrowCKEditorError( () => {
  106. // eslint-disable-next-line no-new
  107. new Selection( {} );
  108. }, /view-selection-setTo-not-selectable/ );
  109. } );
  110. } );
  111. describe( 'anchor', () => {
  112. it( 'should return null if no ranges in selection', () => {
  113. expect( selection.anchor ).to.be.null;
  114. } );
  115. it( 'should return start of single range in selection', () => {
  116. selection.setTo( range1 );
  117. const anchor = selection.anchor;
  118. expect( anchor.isEqual( range1.start ) ).to.be.true;
  119. expect( anchor ).to.not.equal( range1.start );
  120. } );
  121. it( 'should return end of single range in selection when added as backward', () => {
  122. selection.setTo( range1, { backward: true } );
  123. const anchor = selection.anchor;
  124. expect( anchor.isEqual( range1.end ) ).to.be.true;
  125. expect( anchor ).to.not.equal( range1.end );
  126. } );
  127. it( 'should get anchor from last inserted range', () => {
  128. selection.setTo( [ range1, range2 ] );
  129. expect( selection.anchor.isEqual( range2.start ) ).to.be.true;
  130. } );
  131. } );
  132. describe( 'focus', () => {
  133. it( 'should return null if no ranges in selection', () => {
  134. expect( selection.focus ).to.be.null;
  135. } );
  136. it( 'should return end of single range in selection', () => {
  137. selection.setTo( range1 );
  138. const focus = selection.focus;
  139. expect( focus.isEqual( range1.end ) ).to.be.true;
  140. } );
  141. it( 'should return start of single range in selection when added as backward', () => {
  142. selection.setTo( range1, { backward: true } );
  143. const focus = selection.focus;
  144. expect( focus.isEqual( range1.start ) ).to.be.true;
  145. expect( focus ).to.not.equal( range1.start );
  146. } );
  147. it( 'should get focus from last inserted range', () => {
  148. selection.setTo( [ range1, range2 ] );
  149. expect( selection.focus.isEqual( range2.end ) ).to.be.true;
  150. } );
  151. } );
  152. describe( 'setFocus()', () => {
  153. it( 'keeps all existing ranges when no modifications needed', () => {
  154. selection.setTo( range1 );
  155. selection.setFocus( selection.focus );
  156. expect( count( selection.getRanges() ) ).to.equal( 1 );
  157. } );
  158. it( 'throws if there are no ranges in selection', () => {
  159. const endPos = Position._createAt( el, 'end' );
  160. expectToThrowCKEditorError( () => {
  161. selection.setFocus( endPos );
  162. }, /view-selection-setFocus-no-ranges/ );
  163. } );
  164. it( 'modifies existing collapsed selection', () => {
  165. const startPos = Position._createAt( el, 1 );
  166. const endPos = Position._createAt( el, 2 );
  167. selection.setTo( startPos );
  168. selection.setFocus( endPos );
  169. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
  170. expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
  171. } );
  172. it( 'makes existing collapsed selection a backward selection', () => {
  173. const startPos = Position._createAt( el, 1 );
  174. const endPos = Position._createAt( el, 0 );
  175. selection.setTo( startPos );
  176. selection.setFocus( endPos );
  177. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
  178. expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
  179. expect( selection.isBackward ).to.be.true;
  180. } );
  181. it( 'modifies existing non-collapsed selection', () => {
  182. const startPos = Position._createAt( el, 1 );
  183. const endPos = Position._createAt( el, 2 );
  184. const newEndPos = Position._createAt( el, 3 );
  185. selection.setTo( new Range( startPos, endPos ) );
  186. selection.setFocus( newEndPos );
  187. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
  188. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
  189. } );
  190. it( 'makes existing non-collapsed selection a backward selection', () => {
  191. const startPos = Position._createAt( el, 1 );
  192. const endPos = Position._createAt( el, 2 );
  193. const newEndPos = Position._createAt( el, 0 );
  194. selection.setTo( new Range( startPos, endPos ) );
  195. selection.setFocus( newEndPos );
  196. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
  197. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
  198. expect( selection.isBackward ).to.be.true;
  199. } );
  200. it( 'makes existing backward selection a forward selection', () => {
  201. const startPos = Position._createAt( el, 1 );
  202. const endPos = Position._createAt( el, 2 );
  203. const newEndPos = Position._createAt( el, 3 );
  204. selection.setTo( new Range( startPos, endPos ), { backward: true } );
  205. selection.setFocus( newEndPos );
  206. expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
  207. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
  208. expect( selection.isBackward ).to.be.false;
  209. } );
  210. it( 'modifies existing backward selection', () => {
  211. const startPos = Position._createAt( el, 1 );
  212. const endPos = Position._createAt( el, 2 );
  213. const newEndPos = Position._createAt( el, 0 );
  214. selection.setTo( new Range( startPos, endPos ), { backward: true } );
  215. selection.setFocus( newEndPos );
  216. expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
  217. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
  218. expect( selection.isBackward ).to.be.true;
  219. } );
  220. it( 'modifies only the last range', () => {
  221. // Offsets are chosen in this way that the order of adding ranges must count, not their document order.
  222. const startPos1 = Position._createAt( el, 4 );
  223. const endPos1 = Position._createAt( el, 5 );
  224. const startPos2 = Position._createAt( el, 1 );
  225. const endPos2 = Position._createAt( el, 2 );
  226. const newEndPos = Position._createAt( el, 0 );
  227. selection.setTo( [
  228. new Range( startPos1, endPos1 ),
  229. new Range( startPos2, endPos2 )
  230. ] );
  231. selection.setFocus( newEndPos );
  232. const ranges = Array.from( selection.getRanges() );
  233. expect( ranges ).to.have.lengthOf( 2 );
  234. expect( ranges[ 0 ].start.compareWith( startPos1 ) ).to.equal( 'same' );
  235. expect( ranges[ 0 ].end.compareWith( endPos1 ) ).to.equal( 'same' );
  236. expect( selection.anchor.compareWith( startPos2 ) ).to.equal( 'same' );
  237. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
  238. expect( selection.isBackward ).to.be.true;
  239. } );
  240. it( 'collapses the selection when extending to the anchor', () => {
  241. const startPos = Position._createAt( el, 1 );
  242. const endPos = Position._createAt( el, 2 );
  243. selection.setTo( new Range( startPos, endPos ) );
  244. selection.setFocus( startPos );
  245. expect( selection.focus.compareWith( startPos ) ).to.equal( 'same' );
  246. expect( selection.isCollapsed ).to.be.true;
  247. } );
  248. } );
  249. describe( 'isCollapsed', () => {
  250. it( 'should return true when there is single collapsed range', () => {
  251. const range = Range._createFromParentsAndOffsets( el, 5, el, 5 );
  252. selection.setTo( range );
  253. expect( selection.isCollapsed ).to.be.true;
  254. } );
  255. it( 'should return false when there are multiple ranges', () => {
  256. const range1 = Range._createFromParentsAndOffsets( el, 5, el, 5 );
  257. const range2 = Range._createFromParentsAndOffsets( el, 15, el, 15 );
  258. selection.setTo( [ range1, range2 ] );
  259. expect( selection.isCollapsed ).to.be.false;
  260. } );
  261. it( 'should return false when there is not collapsed range', () => {
  262. const range = Range._createFromParentsAndOffsets( el, 15, el, 16 );
  263. selection.setTo( range );
  264. expect( selection.isCollapsed ).to.be.false;
  265. } );
  266. } );
  267. describe( 'rangeCount', () => {
  268. it( 'should return proper range count', () => {
  269. expect( selection.rangeCount ).to.equal( 0 );
  270. selection.setTo( range1 );
  271. expect( selection.rangeCount ).to.equal( 1 );
  272. selection.setTo( [ range1, range2 ] );
  273. expect( selection.rangeCount ).to.equal( 2 );
  274. } );
  275. } );
  276. describe( 'isBackward', () => {
  277. it( 'is defined by the last added range', () => {
  278. const range1 = Range._createFromParentsAndOffsets( el, 5, el, 10 );
  279. const range2 = Range._createFromParentsAndOffsets( el, 15, el, 16 );
  280. selection.setTo( range1, { backward: true } );
  281. expect( selection ).to.have.property( 'isBackward', true );
  282. selection.setTo( [ range1, range2 ] );
  283. expect( selection ).to.have.property( 'isBackward', false );
  284. } );
  285. it( 'is false when last range is collapsed', () => {
  286. const range = Range._createFromParentsAndOffsets( el, 5, el, 5 );
  287. selection.setTo( range, { backward: true } );
  288. expect( selection.isBackward ).to.be.false;
  289. } );
  290. } );
  291. describe( 'getRanges', () => {
  292. it( 'should return iterator with copies of all ranges', () => {
  293. selection.setTo( [ range1, range2 ] );
  294. const iterable = selection.getRanges();
  295. const ranges = Array.from( iterable );
  296. expect( ranges.length ).to.equal( 2 );
  297. expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true;
  298. expect( ranges[ 0 ] ).to.not.equal( range1 );
  299. expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true;
  300. expect( ranges[ 1 ] ).to.not.equal( range2 );
  301. } );
  302. } );
  303. describe( 'getFirstRange', () => {
  304. it( 'should return copy of range with first position', () => {
  305. selection.setTo( [ range1, range2, range3 ] );
  306. const range = selection.getFirstRange();
  307. expect( range.isEqual( range2 ) ).to.be.true;
  308. expect( range ).to.not.equal( range2 );
  309. } );
  310. it( 'should return null if no ranges are present', () => {
  311. expect( selection.getFirstRange() ).to.be.null;
  312. } );
  313. } );
  314. describe( 'getLastRange', () => {
  315. it( 'should return copy of range with last position', () => {
  316. selection.setTo( [ range1, range2, range3 ] );
  317. const range = selection.getLastRange();
  318. expect( range.isEqual( range3 ) ).to.be.true;
  319. expect( range ).to.not.equal( range3 );
  320. } );
  321. it( 'should return null if no ranges are present', () => {
  322. expect( selection.getLastRange() ).to.be.null;
  323. } );
  324. } );
  325. describe( 'getFirstPosition', () => {
  326. it( 'should return copy of first position', () => {
  327. selection.setTo( [ range1, range2, range3 ] );
  328. const position = selection.getFirstPosition();
  329. expect( position.isEqual( range2.start ) ).to.be.true;
  330. expect( position ).to.not.equal( range2.start );
  331. } );
  332. it( 'should return null if no ranges are present', () => {
  333. expect( selection.getFirstPosition() ).to.be.null;
  334. } );
  335. } );
  336. describe( 'getLastPosition', () => {
  337. it( 'should return copy of range with last position', () => {
  338. selection.setTo( [ range1, range2, range3 ] );
  339. const position = selection.getLastPosition();
  340. expect( position.isEqual( range3.end ) ).to.be.true;
  341. expect( position ).to.not.equal( range3.end );
  342. } );
  343. it( 'should return null if no ranges are present', () => {
  344. expect( selection.getLastPosition() ).to.be.null;
  345. } );
  346. } );
  347. describe( 'isEqual', () => {
  348. it( 'should return true if selections equal', () => {
  349. selection.setTo( [ range1, range2 ] );
  350. const otherSelection = new Selection();
  351. otherSelection.setTo( [ range1, range2 ] );
  352. expect( selection.isEqual( otherSelection ) ).to.be.true;
  353. } );
  354. it( 'should return true if backward selections equal', () => {
  355. selection.setTo( range1, { backward: true } );
  356. const otherSelection = new Selection( [ range1 ], { backward: true } );
  357. expect( selection.isEqual( otherSelection ) ).to.be.true;
  358. } );
  359. it( 'should return false if ranges count does not equal', () => {
  360. selection.setTo( [ range1, range2 ] );
  361. const otherSelection = new Selection( [ range1 ] );
  362. expect( selection.isEqual( otherSelection ) ).to.be.false;
  363. } );
  364. it( 'should return false if ranges (other than the last added one) do not equal', () => {
  365. selection.setTo( [ range1, range3 ] );
  366. const otherSelection = new Selection( [ range2, range3 ] );
  367. expect( selection.isEqual( otherSelection ) ).to.be.false;
  368. } );
  369. it( 'should return false if directions do not equal', () => {
  370. selection.setTo( range1 );
  371. const otherSelection = new Selection( [ range1 ], { backward: true } );
  372. expect( selection.isEqual( otherSelection ) ).to.be.false;
  373. } );
  374. it( 'should return false if one selection is fake', () => {
  375. const otherSelection = new Selection( null, { fake: true } );
  376. expect( selection.isEqual( otherSelection ) ).to.be.false;
  377. } );
  378. it( 'should return true if both selection are fake', () => {
  379. const otherSelection = new Selection( range1, { fake: true } );
  380. selection.setTo( range1, { fake: true } );
  381. expect( selection.isEqual( otherSelection ) ).to.be.true;
  382. } );
  383. it( 'should return false if both selection are fake but have different label', () => {
  384. const otherSelection = new Selection( [ range1 ], { fake: true, label: 'foo bar baz' } );
  385. selection.setTo( range1, { fake: true, label: 'foo' } );
  386. expect( selection.isEqual( otherSelection ) ).to.be.false;
  387. } );
  388. it( 'should return true if both selections are empty', () => {
  389. const otherSelection = new Selection();
  390. expect( selection.isEqual( otherSelection ) ).to.be.true;
  391. } );
  392. } );
  393. describe( 'isSimilar', () => {
  394. it( 'should return true if selections equal', () => {
  395. selection.setTo( [ range1, range2 ] );
  396. const otherSelection = new Selection( [ range1, range2 ] );
  397. expect( selection.isSimilar( otherSelection ) ).to.be.true;
  398. } );
  399. it( 'should return false if ranges count does not equal', () => {
  400. selection.setTo( [ range1, range2 ] );
  401. const otherSelection = new Selection( [ range1 ] );
  402. expect( selection.isSimilar( otherSelection ) ).to.be.false;
  403. } );
  404. it( 'should return false if trimmed ranges (other than the last added one) are not equal', () => {
  405. selection.setTo( [ range1, range3 ] );
  406. const otherSelection = new Selection( [ range2, range3 ] );
  407. expect( selection.isSimilar( otherSelection ) ).to.be.false;
  408. } );
  409. it( 'should return false if directions are not equal', () => {
  410. selection.setTo( range1 );
  411. const otherSelection = new Selection( [ range1 ], { backward: true } );
  412. expect( selection.isSimilar( otherSelection ) ).to.be.false;
  413. } );
  414. it( 'should return true if both selections are empty', () => {
  415. const otherSelection = new Selection();
  416. expect( selection.isSimilar( otherSelection ) ).to.be.true;
  417. } );
  418. it( 'should return true if all ranges trimmed from both selections are equal', () => {
  419. const view = parse(
  420. '<container:p><attribute:span></attribute:span></container:p>' +
  421. '<container:p><attribute:span>xx</attribute:span></container:p>'
  422. );
  423. const p1 = view.getChild( 0 );
  424. const p2 = view.getChild( 1 );
  425. const span1 = p1.getChild( 0 );
  426. const span2 = p2.getChild( 0 );
  427. // <p>[<span>{]</span>}</p><p>[<span>{xx}</span>]</p>
  428. const rangeA1 = Range._createFromParentsAndOffsets( p1, 0, span1, 0 );
  429. const rangeB1 = Range._createFromParentsAndOffsets( span1, 0, p1, 1 );
  430. const rangeA2 = Range._createFromParentsAndOffsets( p2, 0, p2, 1 );
  431. const rangeB2 = Range._createFromParentsAndOffsets( span2, 0, span2, 1 );
  432. selection.setTo( [ rangeA1, rangeA2 ] );
  433. const otherSelection = new Selection( [ rangeB2, rangeB1 ] );
  434. expect( selection.isSimilar( otherSelection ) ).to.be.true;
  435. expect( otherSelection.isSimilar( selection ) ).to.be.true;
  436. expect( selection.isEqual( otherSelection ) ).to.be.false;
  437. expect( otherSelection.isEqual( selection ) ).to.be.false;
  438. } );
  439. } );
  440. describe( 'is()', () => {
  441. it( 'should return true for selection', () => {
  442. expect( selection.is( 'selection' ) ).to.be.true;
  443. expect( selection.is( 'view:selection' ) ).to.be.true;
  444. } );
  445. it( 'should return false for other values', () => {
  446. expect( selection.is( 'documentSelection' ) ).to.be.false;
  447. expect( selection.is( 'view:documentSelection' ) ).to.be.false;
  448. expect( selection.is( 'node' ) ).to.be.false;
  449. expect( selection.is( 'text' ) ).to.be.false;
  450. expect( selection.is( 'textProxy' ) ).to.be.false;
  451. expect( selection.is( 'element' ) ).to.be.false;
  452. expect( selection.is( 'rootElement' ) ).to.be.false;
  453. expect( selection.is( 'model:selection' ) ).to.be.false;
  454. } );
  455. } );
  456. describe( 'setTo()', () => {
  457. describe( 'simple scenarios', () => {
  458. it( 'should set selection ranges from the given selection', () => {
  459. selection.setTo( range1 );
  460. const otherSelection = new Selection( [ range2, range3 ], { backward: true } );
  461. selection.setTo( otherSelection );
  462. expect( selection.rangeCount ).to.equal( 2 );
  463. expect( selection._ranges[ 0 ].isEqual( range2 ) ).to.be.true;
  464. expect( selection._ranges[ 0 ] ).is.not.equal( range2 );
  465. expect( selection._ranges[ 1 ].isEqual( range3 ) ).to.be.true;
  466. expect( selection._ranges[ 1 ] ).is.not.equal( range3 );
  467. expect( selection.anchor.isEqual( range3.end ) ).to.be.true;
  468. } );
  469. it( 'should set selection on the given Range', () => {
  470. selection.setTo( range1 );
  471. expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] );
  472. expect( selection.isBackward ).to.be.false;
  473. } );
  474. it( 'should set selection on the given iterable of Ranges', () => {
  475. selection.setTo( new Set( [ range1, range2 ] ) );
  476. expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2 ] );
  477. expect( selection.isBackward ).to.be.false;
  478. } );
  479. it( 'should set collapsed selection on the given Position', () => {
  480. selection.setTo( range1.start );
  481. expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
  482. expect( Array.from( selection.getRanges() )[ 0 ].start ).to.deep.equal( range1.start );
  483. expect( selection.isBackward ).to.be.false;
  484. expect( selection.isCollapsed ).to.be.true;
  485. } );
  486. it( 'should fire change event', done => {
  487. selection.on( 'change', () => {
  488. expect( selection.rangeCount ).to.equal( 1 );
  489. expect( selection.getFirstRange().isEqual( range1 ) ).to.be.true;
  490. done();
  491. } );
  492. const otherSelection = new Selection( [ range1 ] );
  493. selection.setTo( otherSelection );
  494. } );
  495. it( 'should set fake state and label', () => {
  496. const label = 'foo bar baz';
  497. const otherSelection = new Selection( null, { fake: true, label } );
  498. selection.setTo( otherSelection );
  499. expect( selection.isFake ).to.be.true;
  500. expect( selection.fakeSelectionLabel ).to.equal( label );
  501. } );
  502. it( 'should throw an error when trying to set to not selectable', () => {
  503. const otherSelection = new Selection();
  504. expectToThrowCKEditorError( () => {
  505. otherSelection.setTo( {} );
  506. }, /view-selection-setTo-not-selectable/ );
  507. } );
  508. it( 'should throw an error when trying to set to not selectable #2', () => {
  509. const otherSelection = new Selection();
  510. expectToThrowCKEditorError( () => {
  511. otherSelection.setTo();
  512. }, /view-selection-setTo-not-selectable/ );
  513. } );
  514. } );
  515. describe( 'setting collapsed selection', () => {
  516. beforeEach( () => {
  517. selection.setTo( [ range1, range2 ] );
  518. } );
  519. it( 'should collapse selection at position', () => {
  520. const position = new Position( el, 4 );
  521. selection.setTo( position );
  522. const range = selection.getFirstRange();
  523. expect( range.start.parent ).to.equal( el );
  524. expect( range.start.offset ).to.equal( 4 );
  525. expect( range.start.isEqual( range.end ) ).to.be.true;
  526. } );
  527. it( 'should collapse selection at node and offset', () => {
  528. const foo = new Text( viewDocument, 'foo' );
  529. const p = new Element( viewDocument, 'p', null, foo );
  530. selection.setTo( foo, 0 );
  531. let range = selection.getFirstRange();
  532. expect( range.start.parent ).to.equal( foo );
  533. expect( range.start.offset ).to.equal( 0 );
  534. expect( range.start.isEqual( range.end ) ).to.be.true;
  535. selection.setTo( p, 1 );
  536. range = selection.getFirstRange();
  537. expect( range.start.parent ).to.equal( p );
  538. expect( range.start.offset ).to.equal( 1 );
  539. expect( range.start.isEqual( range.end ) ).to.be.true;
  540. } );
  541. it( 'should throw an error when the second parameter is not passed and first is an item', () => {
  542. const foo = new Text( viewDocument, 'foo' );
  543. expectToThrowCKEditorError( () => {
  544. selection.setTo( foo );
  545. }, /view-selection-setTo-required-second-parameter/ );
  546. } );
  547. it( 'should collapse selection at node and flag', () => {
  548. const foo = new Text( viewDocument, 'foo' );
  549. const p = new Element( viewDocument, 'p', null, foo );
  550. selection.setTo( foo, 'end' );
  551. let range = selection.getFirstRange();
  552. expect( range.start.parent ).to.equal( foo );
  553. expect( range.start.offset ).to.equal( 3 );
  554. expect( range.start.isEqual( range.end ) ).to.be.true;
  555. selection.setTo( foo, 'before' );
  556. range = selection.getFirstRange();
  557. expect( range.start.parent ).to.equal( p );
  558. expect( range.start.offset ).to.equal( 0 );
  559. expect( range.start.isEqual( range.end ) ).to.be.true;
  560. selection.setTo( foo, 'after' );
  561. range = selection.getFirstRange();
  562. expect( range.start.parent ).to.equal( p );
  563. expect( range.start.offset ).to.equal( 1 );
  564. expect( range.start.isEqual( range.end ) ).to.be.true;
  565. } );
  566. } );
  567. describe( 'setting collapsed selection at start', () => {
  568. it( 'should collapse to start position and fire change event', done => {
  569. selection.setTo( [ range1, range2, range3 ] );
  570. selection.once( 'change', () => {
  571. expect( selection.rangeCount ).to.equal( 1 );
  572. expect( selection.isCollapsed ).to.be.true;
  573. expect( selection._ranges[ 0 ].start.isEqual( range2.start ) ).to.be.true;
  574. done();
  575. } );
  576. selection.setTo( selection.getFirstPosition() );
  577. } );
  578. } );
  579. describe( 'setting collapsed selection to end', () => {
  580. it( 'should collapse to end position and fire change event', done => {
  581. selection.setTo( [ range1, range2, range3 ] );
  582. selection.once( 'change', () => {
  583. expect( selection.rangeCount ).to.equal( 1 );
  584. expect( selection.isCollapsed ).to.be.true;
  585. expect( selection._ranges[ 0 ].end.isEqual( range3.end ) ).to.be.true;
  586. done();
  587. } );
  588. selection.setTo( selection.getLastPosition() );
  589. } );
  590. } );
  591. describe( 'removing all ranges', () => {
  592. it( 'should remove all ranges and fire change event', done => {
  593. selection.setTo( [ range1, range2 ] );
  594. selection.once( 'change', () => {
  595. expect( selection.rangeCount ).to.equal( 0 );
  596. done();
  597. } );
  598. selection.setTo( null );
  599. } );
  600. } );
  601. describe( 'setting fake selection', () => {
  602. it( 'should allow to set selection to fake', () => {
  603. selection.setTo( range1, { fake: true } );
  604. expect( selection.isFake ).to.be.true;
  605. } );
  606. it( 'should allow to set fake selection label', () => {
  607. const label = 'foo bar baz';
  608. selection.setTo( range1, { fake: true, label } );
  609. expect( selection.fakeSelectionLabel ).to.equal( label );
  610. } );
  611. it( 'should not set label when set to false', () => {
  612. const label = 'foo bar baz';
  613. selection.setTo( range1, { fake: false, label } );
  614. expect( selection.fakeSelectionLabel ).to.equal( '' );
  615. } );
  616. it( 'should reset label when set to false', () => {
  617. const label = 'foo bar baz';
  618. selection.setTo( range1, { fake: true, label } );
  619. selection.setTo( range1 );
  620. expect( selection.fakeSelectionLabel ).to.equal( '' );
  621. } );
  622. it( 'should fire change event', done => {
  623. selection.once( 'change', () => {
  624. expect( selection.isFake ).to.be.true;
  625. expect( selection.fakeSelectionLabel ).to.equal( 'foo bar baz' );
  626. done();
  627. } );
  628. selection.setTo( range1, { fake: true, label: 'foo bar baz' } );
  629. } );
  630. it( 'should be possible to create an empty fake selection', () => {
  631. selection.setTo( null, { fake: true, label: 'foo bar baz' } );
  632. expect( selection.fakeSelectionLabel ).to.equal( 'foo bar baz' );
  633. expect( selection.isFake ).to.be.true;
  634. } );
  635. } );
  636. describe( 'setting selection to itself', () => {
  637. it( 'should correctly set ranges when setting to the same selection', () => {
  638. selection.setTo( [ range1, range2 ] );
  639. selection.setTo( selection );
  640. const ranges = Array.from( selection.getRanges() );
  641. expect( ranges.length ).to.equal( 2 );
  642. expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true;
  643. expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true;
  644. } );
  645. it( 'should correctly set ranges when setting to the same selection\'s ranges', () => {
  646. selection.setTo( [ range1, range2 ] );
  647. selection.setTo( selection.getRanges() );
  648. const ranges = Array.from( selection.getRanges() );
  649. expect( ranges.length ).to.equal( 2 );
  650. expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true;
  651. expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true;
  652. } );
  653. } );
  654. describe( 'throwing errors', () => {
  655. it( 'should throw an error when range is invalid', () => {
  656. expectToThrowCKEditorError( () => {
  657. selection.setTo( [ { invalid: 'range' } ] );
  658. }, /view-selection-add-range-not-range/ );
  659. } );
  660. it( 'should throw when range is intersecting with already added range', () => {
  661. const text = el.getChild( 0 );
  662. const range2 = Range._createFromParentsAndOffsets( text, 7, text, 15 );
  663. expectToThrowCKEditorError( () => {
  664. selection.setTo( [ range1, range2 ] );
  665. }, 'view-selection-range-intersects' );
  666. } );
  667. } );
  668. it( 'should allow setting selection on an item', () => {
  669. const textNode1 = new Text( viewDocument, 'foo' );
  670. const textNode2 = new Text( viewDocument, 'bar' );
  671. const textNode3 = new Text( viewDocument, 'baz' );
  672. const element = new Element( viewDocument, 'p', null, [ textNode1, textNode2, textNode3 ] );
  673. selection.setTo( textNode2, 'on' );
  674. const ranges = Array.from( selection.getRanges() );
  675. expect( ranges.length ).to.equal( 1 );
  676. expect( ranges[ 0 ].start.parent ).to.equal( element );
  677. expect( ranges[ 0 ].start.offset ).to.deep.equal( 1 );
  678. expect( ranges[ 0 ].end.parent ).to.equal( element );
  679. expect( ranges[ 0 ].end.offset ).to.deep.equal( 2 );
  680. } );
  681. it( 'should allow setting selection inside an element', () => {
  682. const element = new Element( viewDocument, 'p', null, [ new Text( viewDocument, 'foo' ), new Text( viewDocument, 'bar' ) ] );
  683. selection.setTo( element, 'in' );
  684. const ranges = Array.from( selection.getRanges() );
  685. expect( ranges.length ).to.equal( 1 );
  686. expect( ranges[ 0 ].start.parent ).to.equal( element );
  687. expect( ranges[ 0 ].start.offset ).to.deep.equal( 0 );
  688. expect( ranges[ 0 ].end.parent ).to.equal( element );
  689. expect( ranges[ 0 ].end.offset ).to.deep.equal( 2 );
  690. } );
  691. it( 'should allow setting backward selection inside an element', () => {
  692. const element = new Element( viewDocument, 'p', null, [ new Text( viewDocument, 'foo' ), new Text( viewDocument, 'bar' ) ] );
  693. selection.setTo( element, 'in', { backward: true } );
  694. const ranges = Array.from( selection.getRanges() );
  695. expect( ranges.length ).to.equal( 1 );
  696. expect( ranges[ 0 ].start.parent ).to.equal( element );
  697. expect( ranges[ 0 ].start.offset ).to.deep.equal( 0 );
  698. expect( ranges[ 0 ].end.parent ).to.equal( element );
  699. expect( ranges[ 0 ].end.offset ).to.deep.equal( 2 );
  700. expect( selection.isBackward ).to.be.true;
  701. } );
  702. } );
  703. describe( 'getEditableElement()', () => {
  704. it( 'should return null if no ranges in selection', () => {
  705. expect( selection.editableElement ).to.be.null;
  706. } );
  707. it( 'should return null if selection is placed in container that is not EditableElement', () => {
  708. selection.setTo( range1 );
  709. expect( selection.editableElement ).to.be.null;
  710. } );
  711. it( 'should return EditableElement when selection is placed inside', () => {
  712. selection.setTo( viewDocument.selection );
  713. const root = createViewRoot( viewDocument, 'div', 'main' );
  714. const element = new Element( viewDocument, 'p' );
  715. root._appendChild( element );
  716. selection.setTo( Range._createFromParentsAndOffsets( element, 0, element, 0 ) );
  717. expect( selection.editableElement ).to.equal( root );
  718. } );
  719. } );
  720. describe( 'isFake', () => {
  721. it( 'should be false for newly created instance', () => {
  722. expect( selection.isFake ).to.be.false;
  723. } );
  724. } );
  725. describe( 'getSelectedElement()', () => {
  726. it( 'should return selected element', () => {
  727. const { selection: docSelection, view } = parse( 'foo [<b>bar</b>] baz' );
  728. const b = view.getChild( 1 );
  729. const selection = new Selection( docSelection );
  730. expect( selection.getSelectedElement() ).to.equal( b );
  731. } );
  732. it( 'should return selected element if the selection is anchored at the end/at the beginning of a text node', () => {
  733. const { selection: docSelection, view } = parse( 'foo {<b>bar</b>} baz' );
  734. const b = view.getChild( 1 );
  735. const selection = new Selection( docSelection );
  736. expect( selection.getSelectedElement() ).to.equal( b );
  737. } );
  738. it( 'should return null if there is more than one range', () => {
  739. const { selection: docSelection } = parse( 'foo [<b>bar</b>] [<i>baz</i>]' );
  740. const selection = new Selection( docSelection );
  741. expect( selection.getSelectedElement() ).to.be.null;
  742. } );
  743. it( 'should return null if there is no selection', () => {
  744. expect( selection.getSelectedElement() ).to.be.null;
  745. } );
  746. it( 'should return null if selection is not over single element #1', () => {
  747. const { selection: docSelection } = parse( 'foo [<b>bar</b> ba}z' );
  748. const selection = new Selection( docSelection );
  749. expect( selection.getSelectedElement() ).to.be.null;
  750. } );
  751. it( 'should return null if selection is not over single element #2', () => {
  752. const { selection: docSelection } = parse( 'foo <b>{bar}</b> baz' );
  753. const selection = new Selection( docSelection );
  754. expect( selection.getSelectedElement() ).to.be.null;
  755. } );
  756. } );
  757. } );