selection.js 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* bender-tags: model */
  6. 'use strict';
  7. import Document from '/ckeditor5/engine/model/document.js';
  8. import Element from '/ckeditor5/engine/model/element.js';
  9. import Text from '/ckeditor5/engine/model/text.js';
  10. import Range from '/ckeditor5/engine/model/range.js';
  11. import Position from '/ckeditor5/engine/model/position.js';
  12. import LiveRange from '/ckeditor5/engine/model/liverange.js';
  13. import Selection from '/ckeditor5/engine/model/selection.js';
  14. import InsertOperation from '/ckeditor5/engine/model/operation/insertoperation.js';
  15. import MoveOperation from '/ckeditor5/engine/model/operation/moveoperation.js';
  16. import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
  17. import testUtils from '/tests/ckeditor5/_utils/utils.js';
  18. import count from '/ckeditor5/utils/count.js';
  19. import { jsonParseStringify, wrapInDelta } from '/tests/engine/model/_utils/utils.js';
  20. testUtils.createSinonSandbox();
  21. describe( 'Selection', () => {
  22. let attrFooBar;
  23. before( () => {
  24. attrFooBar = { foo: 'bar' };
  25. } );
  26. let doc, root, selection, liveRange, range;
  27. beforeEach( () => {
  28. doc = new Document();
  29. root = doc.createRoot();
  30. root.appendChildren( [
  31. new Element( 'p' ),
  32. new Element( 'p' ),
  33. new Element( 'p', [], 'foobar' ),
  34. new Element( 'p' ),
  35. new Element( 'p' ),
  36. new Element( 'p' ),
  37. new Element( 'p', [], 'foobar' )
  38. ] );
  39. selection = doc.selection;
  40. doc.schema.registerItem( 'p', '$block' );
  41. liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
  42. range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 2, 2 ] ) );
  43. } );
  44. afterEach( () => {
  45. doc.destroy();
  46. liveRange.detach();
  47. } );
  48. describe( 'default range', () => {
  49. it( 'should go to the first editable element', () => {
  50. const ranges = Array.from( selection.getRanges() );
  51. expect( ranges.length ).to.equal( 1 );
  52. expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
  53. expect( selection.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
  54. expect( selection ).to.have.property( 'isBackward', false );
  55. expect( selection._attrs ).to.be.instanceof( Map );
  56. expect( selection._attrs.size ).to.equal( 0 );
  57. } );
  58. it( 'should be set to the beginning of the doc if there is no editable element', () => {
  59. doc = new Document();
  60. root = doc.createRoot();
  61. root.insertChildren( 0, 'foobar' );
  62. selection = doc.selection;
  63. const ranges = Array.from( selection.getRanges() );
  64. expect( ranges.length ).to.equal( 1 );
  65. expect( selection.anchor.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true;
  66. expect( selection.focus.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true;
  67. expect( selection ).to.have.property( 'isBackward', false );
  68. expect( selection._attrs ).to.be.instanceof( Map );
  69. expect( selection._attrs.size ).to.equal( 0 );
  70. } );
  71. it( 'should skip element when you can not put selection', () => {
  72. doc = new Document();
  73. root = doc.createRoot();
  74. root.insertChildren( 0, [
  75. new Element( 'img' ),
  76. new Element( 'p', [], 'foobar' )
  77. ] );
  78. doc.schema.registerItem( 'img' );
  79. doc.schema.registerItem( 'p', '$block' );
  80. selection = doc.selection;
  81. const ranges = Array.from( selection.getRanges() );
  82. expect( ranges.length ).to.equal( 1 );
  83. expect( selection.anchor.isEqual( new Position( root, [ 1, 0 ] ) ) ).to.be.true;
  84. expect( selection.focus.isEqual( new Position( root, [ 1, 0 ] ) ) ).to.be.true;
  85. expect( selection ).to.have.property( 'isBackward', false );
  86. expect( selection._attrs ).to.be.instanceof( Map );
  87. expect( selection._attrs.size ).to.equal( 0 );
  88. } );
  89. } );
  90. describe( 'isCollapsed', () => {
  91. it( 'should return true for default range', () => {
  92. expect( selection.isCollapsed ).to.be.true;
  93. } );
  94. it( 'should return true when there is single collapsed ranges', () => {
  95. selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
  96. expect( selection.isCollapsed ).to.be.true;
  97. } );
  98. it( 'should return false when there are multiple ranges', () => {
  99. selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
  100. selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
  101. expect( selection.isCollapsed ).to.be.false;
  102. } );
  103. it( 'should return false when there is not collapsed range', () => {
  104. selection.addRange( range );
  105. expect( selection.isCollapsed ).to.be.false;
  106. } );
  107. } );
  108. describe( 'rangeCount', () => {
  109. it( 'should return proper range count', () => {
  110. expect( selection.rangeCount ).to.equal( 1 );
  111. selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
  112. expect( selection.rangeCount ).to.equal( 1 );
  113. selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
  114. expect( selection.rangeCount ).to.equal( 2 );
  115. } );
  116. } );
  117. describe( 'isBackward', () => {
  118. it( 'is defined by the last added range', () => {
  119. selection.addRange( range, true );
  120. expect( selection ).to.have.property( 'isBackward', true );
  121. selection.addRange( liveRange );
  122. expect( selection ).to.have.property( 'isBackward', false );
  123. } );
  124. it( 'is false when last range is collapsed', () => {
  125. const pos = Position.createAt( root, 0 );
  126. selection.addRange( new Range( pos, pos ), true );
  127. expect( selection.isBackward ).to.be.false;
  128. } );
  129. } );
  130. describe( 'addRange', () => {
  131. it( 'should copy added ranges and store multiple ranges', () => {
  132. selection.addRange( liveRange );
  133. selection.addRange( range );
  134. const ranges = selection._ranges;
  135. expect( ranges.length ).to.equal( 2 );
  136. expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
  137. expect( ranges[ 1 ].isEqual( range ) ).to.be.true;
  138. expect( ranges[ 0 ] ).not.to.be.equal( liveRange );
  139. expect( ranges[ 1 ] ).not.to.be.equal( range );
  140. } );
  141. it( 'should set anchor and focus to the start and end of the most recently added range', () => {
  142. selection.addRange( liveRange );
  143. expect( selection.anchor.path ).to.deep.equal( [ 0 ] );
  144. expect( selection.focus.path ).to.deep.equal( [ 1 ] );
  145. selection.addRange( range );
  146. expect( selection.anchor.path ).to.deep.equal( [ 2 ] );
  147. expect( selection.focus.path ).to.deep.equal( [ 2, 2 ] );
  148. } );
  149. it( 'should set anchor and focus to the end and start of the most recently added range if backward flag was used', () => {
  150. selection.addRange( liveRange, true );
  151. expect( selection.anchor.path ).to.deep.equal( [ 1 ] );
  152. expect( selection.focus.path ).to.deep.equal( [ 0 ] );
  153. selection.addRange( range, true );
  154. expect( selection.anchor.path ).to.deep.equal( [ 2, 2 ] );
  155. expect( selection.focus.path ).to.deep.equal( [ 2 ] );
  156. } );
  157. it( 'should return a copy of (not a reference to) array of stored ranges', () => {
  158. selection.addRange( liveRange );
  159. const ranges = Array.from( selection.getRanges() );
  160. selection.addRange( range );
  161. expect( ranges.length ).to.equal( 1 );
  162. expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
  163. } );
  164. it( 'should convert added Range to LiveRange', () => {
  165. selection.addRange( range );
  166. const ranges = selection._ranges;
  167. expect( ranges[ 0 ] ).to.be.instanceof( LiveRange );
  168. } );
  169. it( 'should fire change:range event when adding a range', () => {
  170. let spy = sinon.spy();
  171. selection.on( 'change:range', spy );
  172. selection.addRange( range );
  173. expect( spy.called ).to.be.true;
  174. } );
  175. it( 'should unbind all events when destroyed', () => {
  176. selection.addRange( liveRange );
  177. selection.addRange( range );
  178. const ranges = selection._ranges;
  179. sinon.spy( ranges[ 0 ], 'detach' );
  180. sinon.spy( ranges[ 1 ], 'detach' );
  181. selection.destroy();
  182. expect( ranges[ 0 ].detach.called ).to.be.true;
  183. expect( ranges[ 1 ].detach.called ).to.be.true;
  184. ranges[ 0 ].detach.restore();
  185. ranges[ 1 ].detach.restore();
  186. } );
  187. it( 'should throw an error if added range intersects with already stored range', () => {
  188. selection.addRange( liveRange );
  189. expect( () => {
  190. selection.addRange(
  191. new Range(
  192. new Position( root, [ 0, 4 ] ),
  193. new Position( root, [ 1, 2 ] )
  194. )
  195. );
  196. } ).to.throw( CKEditorError, /selection-range-intersects/ );
  197. } );
  198. } );
  199. describe( 'collapse', () => {
  200. it( 'detaches all existing ranges', () => {
  201. selection.addRange( range );
  202. selection.addRange( liveRange );
  203. const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
  204. selection.collapse( root );
  205. expect( spy.calledTwice ).to.be.true;
  206. } );
  207. it( 'fires change:range', () => {
  208. const spy = sinon.spy();
  209. selection.on( 'change:range', spy );
  210. selection.collapse( root );
  211. expect( spy.calledOnce ).to.be.true;
  212. } );
  213. it( 'sets selection at the 0 offset if second parameter not passed', () => {
  214. selection.collapse( root );
  215. expect( selection ).to.have.property( 'isCollapsed', true );
  216. const focus = selection.focus;
  217. expect( focus ).to.have.property( 'parent', root );
  218. expect( focus ).to.have.property( 'offset', 0 );
  219. } );
  220. it( 'sets selection at given offset in given parent', () => {
  221. selection.collapse( root, 3 );
  222. expect( selection ).to.have.property( 'isCollapsed', true );
  223. const focus = selection.focus;
  224. expect( focus ).to.have.property( 'parent', root );
  225. expect( focus ).to.have.property( 'offset', 3 );
  226. } );
  227. it( 'sets selection at the end of the given parent', () => {
  228. selection.collapse( root, 'END' );
  229. expect( selection ).to.have.property( 'isCollapsed', true );
  230. const focus = selection.focus;
  231. expect( focus ).to.have.property( 'parent', root );
  232. expect( focus ).to.have.property( 'offset', root.getChildCount() );
  233. } );
  234. it( 'sets selection before the specified element', () => {
  235. selection.collapse( root.getChild( 1 ), 'BEFORE' );
  236. expect( selection ).to.have.property( 'isCollapsed', true );
  237. const focus = selection.focus;
  238. expect( focus ).to.have.property( 'parent', root );
  239. expect( focus ).to.have.property( 'offset', 1 );
  240. } );
  241. it( 'sets selection after the specified element', () => {
  242. selection.collapse( root.getChild( 1 ), 'AFTER' );
  243. expect( selection ).to.have.property( 'isCollapsed', true );
  244. const focus = selection.focus;
  245. expect( focus ).to.have.property( 'parent', root );
  246. expect( focus ).to.have.property( 'offset', 2 );
  247. } );
  248. it( 'sets selection at the specified position', () => {
  249. const pos = Position.createFromParentAndOffset( root, 3 );
  250. selection.collapse( pos );
  251. expect( selection ).to.have.property( 'isCollapsed', true );
  252. const focus = selection.focus;
  253. expect( focus ).to.have.property( 'parent', root );
  254. expect( focus ).to.have.property( 'offset', 3 );
  255. } );
  256. } );
  257. describe( 'setFocus', () => {
  258. it( 'keeps all existing ranges and fires no change:range when no modifications needed', () => {
  259. selection.addRange( range );
  260. selection.addRange( liveRange );
  261. const spy = sinon.spy();
  262. selection.on( 'change:range', spy );
  263. selection.setFocus( selection.focus );
  264. expect( count( selection.getRanges() ) ).to.equal( 2 );
  265. expect( spy.callCount ).to.equal( 0 );
  266. } );
  267. it( 'fires change:range', () => {
  268. selection.addRange( range );
  269. const spy = sinon.spy();
  270. selection.on( 'change:range', spy );
  271. selection.setFocus( Position.createAt( root, 'END' ) );
  272. expect( spy.calledOnce ).to.be.true;
  273. } );
  274. it( 'modifies default range', () => {
  275. const startPos = selection.getFirstPosition();
  276. const endPos = Position.createAt( root, 'END' );
  277. selection.setFocus( endPos );
  278. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
  279. expect( selection.focus.compareWith( endPos ) ).to.equal( 'SAME' );
  280. } );
  281. it( 'modifies existing collapsed selection', () => {
  282. const startPos = Position.createAt( root, 1 );
  283. const endPos = Position.createAt( root, 2 );
  284. selection.collapse( startPos );
  285. selection.setFocus( endPos );
  286. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
  287. expect( selection.focus.compareWith( endPos ) ).to.equal( 'SAME' );
  288. } );
  289. it( 'makes existing collapsed selection a backward selection', () => {
  290. const startPos = Position.createAt( root, 1 );
  291. const endPos = Position.createAt( root, 0 );
  292. selection.collapse( startPos );
  293. selection.setFocus( endPos );
  294. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
  295. expect( selection.focus.compareWith( endPos ) ).to.equal( 'SAME' );
  296. expect( selection.isBackward ).to.be.true;
  297. } );
  298. it( 'modifies existing non-collapsed selection', () => {
  299. const startPos = Position.createAt( root, 1 );
  300. const endPos = Position.createAt( root, 2 );
  301. const newEndPos = Position.createAt( root, 3 );
  302. selection.addRange( new Range( startPos, endPos ) );
  303. selection.setFocus( newEndPos );
  304. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
  305. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
  306. } );
  307. it( 'makes existing non-collapsed selection a backward selection', () => {
  308. const startPos = Position.createAt( root, 1 );
  309. const endPos = Position.createAt( root, 2 );
  310. const newEndPos = Position.createAt( root, 0 );
  311. selection.addRange( new Range( startPos, endPos ) );
  312. selection.setFocus( newEndPos );
  313. expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
  314. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
  315. expect( selection.isBackward ).to.be.true;
  316. } );
  317. it( 'makes existing backward selection a forward selection', () => {
  318. const startPos = Position.createAt( root, 1 );
  319. const endPos = Position.createAt( root, 2 );
  320. const newEndPos = Position.createAt( root, 3 );
  321. selection.addRange( new Range( startPos, endPos ), true );
  322. selection.setFocus( newEndPos );
  323. expect( selection.anchor.compareWith( endPos ) ).to.equal( 'SAME' );
  324. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
  325. expect( selection.isBackward ).to.be.false;
  326. } );
  327. it( 'modifies existing backward selection', () => {
  328. const startPos = Position.createAt( root, 1 );
  329. const endPos = Position.createAt( root, 2 );
  330. const newEndPos = Position.createAt( root, 0 );
  331. selection.addRange( new Range( startPos, endPos ), true );
  332. selection.setFocus( newEndPos );
  333. expect( selection.anchor.compareWith( endPos ) ).to.equal( 'SAME' );
  334. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
  335. expect( selection.isBackward ).to.be.true;
  336. } );
  337. it( 'modifies only the last range', () => {
  338. // Offsets are chosen in this way that the order of adding ranges must count, not their document order.
  339. const startPos1 = Position.createAt( root, 4 );
  340. const endPos1 = Position.createAt( root, 5 );
  341. const startPos2 = Position.createAt( root, 1 );
  342. const endPos2 = Position.createAt( root, 2 );
  343. const newEndPos = Position.createAt( root, 0 );
  344. selection.addRange( new Range( startPos1, endPos1 ) );
  345. selection.addRange( new Range( startPos2, endPos2 ) );
  346. const spy = sinon.spy();
  347. selection.on( 'change:range', spy );
  348. selection.setFocus( newEndPos );
  349. const ranges = Array.from( selection.getRanges() );
  350. expect( ranges ).to.have.lengthOf( 2 );
  351. expect( ranges[ 0 ].start.compareWith( startPos1 ) ).to.equal( 'SAME' );
  352. expect( ranges[ 0 ].end.compareWith( endPos1 ) ).to.equal( 'SAME' );
  353. expect( selection.anchor.compareWith( startPos2 ) ).to.equal( 'SAME' );
  354. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
  355. expect( selection.isBackward ).to.be.true;
  356. expect( spy.calledOnce ).to.be.true;
  357. } );
  358. it( 'collapses the selection when extending to the anchor', () => {
  359. const startPos = Position.createAt( root, 1 );
  360. const endPos = Position.createAt( root, 2 );
  361. selection.addRange( new Range( startPos, endPos ) );
  362. selection.setFocus( startPos );
  363. expect( selection.focus.compareWith( startPos ) ).to.equal( 'SAME' );
  364. expect( selection.isCollapsed ).to.be.true;
  365. } );
  366. it( 'uses Position.createAt', () => {
  367. const startPos = Position.createAt( root, 1 );
  368. const endPos = Position.createAt( root, 2 );
  369. const newEndPos = Position.createAt( root, 4 );
  370. const spy = testUtils.sinon.stub( Position, 'createAt', () => newEndPos );
  371. selection.addRange( new Range( startPos, endPos ) );
  372. selection.setFocus( root, 'END' );
  373. expect( spy.calledOnce ).to.be.true;
  374. expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
  375. } );
  376. it( 'detaches the range it replaces', () => {
  377. const startPos = Position.createAt( root, 1 );
  378. const endPos = Position.createAt( root, 2 );
  379. const newEndPos = Position.createAt( root, 4 );
  380. const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
  381. selection.addRange( new Range( startPos, endPos ) );
  382. selection.setFocus( newEndPos );
  383. expect( spy.calledOnce ).to.be.true;
  384. } );
  385. } );
  386. describe( 'removeAllRanges', () => {
  387. let spy, ranges;
  388. beforeEach( () => {
  389. selection.addRange( liveRange );
  390. selection.addRange( range );
  391. spy = sinon.spy();
  392. selection.on( 'change:range', spy );
  393. ranges = selection._ranges;
  394. sinon.spy( ranges[ 0 ], 'detach' );
  395. sinon.spy( ranges[ 1 ], 'detach' );
  396. selection.removeAllRanges();
  397. } );
  398. afterEach( () => {
  399. ranges[ 0 ].detach.restore();
  400. ranges[ 1 ].detach.restore();
  401. } );
  402. it( 'should remove all stored ranges (and reset to default range)', () => {
  403. expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
  404. expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
  405. expect( selection.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
  406. } );
  407. it( 'should fire exactly one update event', () => {
  408. expect( spy.calledOnce ).to.be.true;
  409. } );
  410. it( 'should detach ranges', () => {
  411. expect( ranges[ 0 ].detach.called ).to.be.true;
  412. expect( ranges[ 1 ].detach.called ).to.be.true;
  413. } );
  414. } );
  415. describe( 'setRanges', () => {
  416. let newRanges, spy, oldRanges;
  417. before( () => {
  418. newRanges = [
  419. new Range( new Position( root, [ 4 ] ), new Position( root, [ 5 ] ) ),
  420. new Range( new Position( root, [ 5, 0 ] ), new Position( root, [ 6, 0 ] ) )
  421. ];
  422. } );
  423. beforeEach( () => {
  424. selection.addRange( liveRange );
  425. selection.addRange( range );
  426. spy = sinon.spy();
  427. selection.on( 'change:range', spy );
  428. oldRanges = selection._ranges;
  429. sinon.spy( oldRanges[ 0 ], 'detach' );
  430. sinon.spy( oldRanges[ 1 ], 'detach' );
  431. } );
  432. afterEach( () => {
  433. oldRanges[ 0 ].detach.restore();
  434. oldRanges[ 1 ].detach.restore();
  435. } );
  436. it( 'should remove all ranges and add given ranges', () => {
  437. selection.setRanges( newRanges );
  438. let ranges = selection._ranges;
  439. expect( ranges.length ).to.equal( 2 );
  440. expect( ranges[ 0 ].isEqual( newRanges[ 0 ] ) ).to.be.true;
  441. expect( ranges[ 1 ].isEqual( newRanges[ 1 ] ) ).to.be.true;
  442. } );
  443. it( 'should use last range from given array to get anchor and focus position', () => {
  444. selection.setRanges( newRanges );
  445. expect( selection.anchor.path ).to.deep.equal( [ 5, 0 ] );
  446. expect( selection.focus.path ).to.deep.equal( [ 6, 0 ] );
  447. } );
  448. it( 'should acknowledge backward flag when setting anchor and focus', () => {
  449. selection.setRanges( newRanges, true );
  450. expect( selection.anchor.path ).to.deep.equal( [ 6, 0 ] );
  451. expect( selection.focus.path ).to.deep.equal( [ 5, 0 ] );
  452. } );
  453. it( 'should fire exactly one update event', () => {
  454. selection.setRanges( newRanges );
  455. expect( spy.calledOnce ).to.be.true;
  456. } );
  457. it( 'should detach removed LiveRanges', () => {
  458. selection.setRanges( newRanges );
  459. expect( oldRanges[ 0 ].detach.called ).to.be.true;
  460. expect( oldRanges[ 1 ].detach.called ).to.be.true;
  461. } );
  462. } );
  463. describe( 'getFirstRange', () => {
  464. it( 'should return a range which start position is before all other ranges\' start positions', () => {
  465. // This will not be the first range despite being added as first
  466. selection.addRange( new Range( new Position( root, [ 4 ] ), new Position( root, [ 5 ] ) ) );
  467. // This should be the first range.
  468. selection.addRange( new Range( new Position( root, [ 1 ] ), new Position( root, [ 4 ] ) ) );
  469. // A random range that is not first.
  470. selection.addRange( new Range( new Position( root, [ 6 ] ), new Position( root, [ 7 ] ) ) );
  471. let range = selection.getFirstRange();
  472. expect( range.start.path ).to.deep.equal( [ 1 ] );
  473. expect( range.end.path ).to.deep.equal( [ 4 ] );
  474. } );
  475. } );
  476. describe( 'getFirstPosition', () => {
  477. it( 'should return a position that is in selection and is before any other position from the selection', () => {
  478. // This will not be a range containing the first position despite being added as first
  479. selection.addRange( new Range( new Position( root, [ 4 ] ), new Position( root, [ 5 ] ) ) );
  480. // This should be the first range.
  481. selection.addRange( new Range( new Position( root, [ 1 ] ), new Position( root, [ 4 ] ) ) );
  482. // A random range that is not first.
  483. selection.addRange( new Range( new Position( root, [ 6 ] ), new Position( root, [ 7 ] ) ) );
  484. let position = selection.getFirstPosition();
  485. expect( position.path ).to.deep.equal( [ 1 ] );
  486. } );
  487. } );
  488. // Selection uses LiveRanges so here are only simple test to see if integration is
  489. // working well, without getting into complicated corner cases.
  490. describe( 'after applying an operation should get updated and not fire update event', () => {
  491. let spy;
  492. beforeEach( () => {
  493. root.insertChildren( 0, [ new Element( 'ul', [], 'abcdef' ), new Element( 'p', [], 'foobar' ), 'xyz' ] );
  494. selection.addRange( new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 4 ] ) ) );
  495. spy = sinon.spy();
  496. selection.on( 'change:range', spy );
  497. } );
  498. describe( 'InsertOperation', () => {
  499. it( 'before selection', () => {
  500. doc.applyOperation( wrapInDelta(
  501. new InsertOperation(
  502. new Position( root, [ 0, 1 ] ),
  503. 'xyz',
  504. doc.version
  505. )
  506. ) );
  507. let range = selection._ranges[ 0 ];
  508. expect( range.start.path ).to.deep.equal( [ 0, 5 ] );
  509. expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
  510. expect( spy.called ).to.be.false;
  511. } );
  512. it( 'inside selection', () => {
  513. doc.applyOperation( wrapInDelta(
  514. new InsertOperation(
  515. new Position( root, [ 1, 0 ] ),
  516. 'xyz',
  517. doc.version
  518. )
  519. ) );
  520. let range = selection._ranges[ 0 ];
  521. expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
  522. expect( range.end.path ).to.deep.equal( [ 1, 7 ] );
  523. expect( spy.called ).to.be.false;
  524. } );
  525. } );
  526. describe( 'MoveOperation', () => {
  527. it( 'move range from before a selection', () => {
  528. doc.applyOperation( wrapInDelta(
  529. new MoveOperation(
  530. new Position( root, [ 0, 0 ] ),
  531. 2,
  532. new Position( root, [ 2 ] ),
  533. doc.version
  534. )
  535. ) );
  536. let range = selection._ranges[ 0 ];
  537. expect( range.start.path ).to.deep.equal( [ 0, 0 ] );
  538. expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
  539. expect( spy.called ).to.be.false;
  540. } );
  541. it( 'moved into before a selection', () => {
  542. doc.applyOperation( wrapInDelta(
  543. new MoveOperation(
  544. new Position( root, [ 2 ] ),
  545. 2,
  546. new Position( root, [ 0, 0 ] ),
  547. doc.version
  548. )
  549. ) );
  550. let range = selection._ranges[ 0 ];
  551. expect( range.start.path ).to.deep.equal( [ 0, 4 ] );
  552. expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
  553. expect( spy.called ).to.be.false;
  554. } );
  555. it( 'move range from inside of selection', () => {
  556. doc.applyOperation( wrapInDelta(
  557. new MoveOperation(
  558. new Position( root, [ 1, 0 ] ),
  559. 2,
  560. new Position( root, [ 2 ] ),
  561. doc.version
  562. )
  563. ) );
  564. let range = selection._ranges[ 0 ];
  565. expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
  566. expect( range.end.path ).to.deep.equal( [ 1, 2 ] );
  567. expect( spy.called ).to.be.false;
  568. } );
  569. it( 'moved range intersects with selection', () => {
  570. doc.applyOperation( wrapInDelta(
  571. new MoveOperation(
  572. new Position( root, [ 1, 3 ] ),
  573. 2,
  574. new Position( root, [ 4 ] ),
  575. doc.version
  576. )
  577. ) );
  578. let range = selection._ranges[ 0 ];
  579. expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
  580. expect( range.end.path ).to.deep.equal( [ 1, 3 ] );
  581. expect( spy.called ).to.be.false;
  582. } );
  583. it( 'split inside selection (do not break selection)', () => {
  584. doc.applyOperation( wrapInDelta(
  585. new InsertOperation(
  586. new Position( root, [ 2 ] ),
  587. new Element( 'p' ),
  588. doc.version
  589. )
  590. ) );
  591. doc.applyOperation( wrapInDelta(
  592. new MoveOperation(
  593. new Position( root, [ 1, 2 ] ),
  594. 4,
  595. new Position( root, [ 2, 0 ] ),
  596. doc.version
  597. )
  598. ) );
  599. let range = selection._ranges[ 0 ];
  600. expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
  601. expect( range.end.path ).to.deep.equal( [ 2, 2 ] );
  602. expect( spy.called ).to.be.false;
  603. } );
  604. } );
  605. } );
  606. describe( 'attributes interface', () => {
  607. let fullP, emptyP, rangeInFullP, rangeInEmptyP;
  608. beforeEach( () => {
  609. root.insertChildren( 0, [
  610. new Element( 'p', [], 'foobar' ),
  611. new Element( 'p', [], [] )
  612. ] );
  613. fullP = root.getChild( 0 );
  614. emptyP = root.getChild( 1 );
  615. rangeInFullP = new Range( new Position( root, [ 0, 4 ] ), new Position( root, [ 0, 4 ] ) );
  616. rangeInEmptyP = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) );
  617. } );
  618. describe( 'setAttribute', () => {
  619. it( 'should set given attribute on the selection', () => {
  620. selection.setRanges( [ rangeInFullP ] );
  621. selection.setAttribute( 'foo', 'bar' );
  622. expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
  623. expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
  624. } );
  625. it( 'should store attribute if the selection is in empty node', () => {
  626. selection.setRanges( [ rangeInEmptyP ] );
  627. selection.setAttribute( 'foo', 'bar' );
  628. expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
  629. expect( emptyP.getAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.equal( 'bar' );
  630. } );
  631. it( 'should fire change:attribute event', () => {
  632. let spy = sinon.spy();
  633. selection.on( 'change:attribute', spy );
  634. selection.setAttribute( 'foo', 'bar' );
  635. expect( spy.called ).to.be.true;
  636. } );
  637. } );
  638. describe( 'hasAttribute', () => {
  639. it( 'should return true if element contains attribute with given key', () => {
  640. selection.setRanges( [ rangeInFullP ] );
  641. selection.setAttribute( 'foo', 'bar' );
  642. expect( selection.hasAttribute( 'foo' ) ).to.be.true;
  643. } );
  644. it( 'should return false if element does not contain attribute with given key', () => {
  645. expect( selection.hasAttribute( 'abc' ) ).to.be.false;
  646. } );
  647. } );
  648. describe( 'getAttribute', () => {
  649. it( 'should return undefined if element does not contain given attribute', () => {
  650. expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
  651. } );
  652. } );
  653. describe( 'getAttributes', () => {
  654. it( 'should return an iterator that iterates over all attributes set on the text fragment', () => {
  655. selection.setRanges( [ rangeInFullP ] );
  656. selection.setAttribute( 'foo', 'bar' );
  657. selection.setAttribute( 'abc', 'xyz' );
  658. let attrs = Array.from( selection.getAttributes() );
  659. expect( attrs ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'abc', 'xyz' ] ] );
  660. } );
  661. } );
  662. describe( 'setAttributesTo', () => {
  663. it( 'should remove all attributes set on element and set the given ones', () => {
  664. selection.setRanges( [ rangeInFullP ] );
  665. selection.setAttribute( 'abc', 'xyz' );
  666. selection.setAttributesTo( { foo: 'bar' } );
  667. expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
  668. expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
  669. expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
  670. expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
  671. } );
  672. it( 'should remove all stored attributes and store the given ones if the selection is in empty node', () => {
  673. selection.setRanges( [ rangeInEmptyP ] );
  674. selection.setAttribute( 'abc', 'xyz' );
  675. selection.setAttributesTo( { foo: 'bar' } );
  676. expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
  677. expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
  678. expect( emptyP.getAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.equal( 'bar' );
  679. expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
  680. } );
  681. it( 'should fire change:attribute event', () => {
  682. let spy = sinon.spy();
  683. selection.on( 'change:attribute', spy );
  684. selection.setAttributesTo( { foo: 'bar' } );
  685. expect( spy.called ).to.be.true;
  686. } );
  687. } );
  688. describe( 'removeAttribute', () => {
  689. it( 'should remove attribute set on the text fragment', () => {
  690. selection.setRanges( [ rangeInFullP ] );
  691. selection.setAttribute( 'foo', 'bar' );
  692. selection.removeAttribute( 'foo' );
  693. expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
  694. expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
  695. } );
  696. it( 'should remove stored attribute if the selection is in empty node', () => {
  697. selection.setRanges( [ rangeInEmptyP ] );
  698. selection.setAttribute( 'foo', 'bar' );
  699. selection.removeAttribute( 'foo' );
  700. expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
  701. expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
  702. } );
  703. it( 'should fire change:attribute event', () => {
  704. let spy = sinon.spy();
  705. selection.on( 'change:attribute', spy );
  706. selection.removeAttribute( 'foo' );
  707. expect( spy.called ).to.be.true;
  708. } );
  709. } );
  710. describe( 'clearAttributes', () => {
  711. it( 'should remove all attributes from the element', () => {
  712. selection.setRanges( [ rangeInFullP ] );
  713. selection.setAttribute( 'foo', 'bar' );
  714. selection.setAttribute( 'abc', 'xyz' );
  715. selection.clearAttributes();
  716. expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
  717. expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
  718. expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
  719. expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
  720. } );
  721. it( 'should remove all stored attributes if the selection is in empty node', () => {
  722. selection.setRanges( [ rangeInEmptyP ] );
  723. selection.setAttribute( 'foo', 'bar' );
  724. selection.setAttribute( 'abc', 'xyz' );
  725. selection.clearAttributes();
  726. expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
  727. expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
  728. expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
  729. expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
  730. } );
  731. it( 'should fire change:attribute event', () => {
  732. let spy = sinon.spy();
  733. selection.on( 'change:attribute', spy );
  734. selection.clearAttributes();
  735. expect( spy.called ).to.be.true;
  736. } );
  737. } );
  738. } );
  739. describe( '_updateAttributes', () => {
  740. beforeEach( () => {
  741. root.insertChildren( 0, [
  742. new Element( 'p', { p: true } ),
  743. new Text( 'a', { a: true } ),
  744. new Element( 'p', { p: true } ),
  745. new Text( 'b', { b: true } ),
  746. new Text( 'c', { c: true } ),
  747. new Element( 'p', [], [
  748. new Text( 'd', { d: true } )
  749. ] ),
  750. new Element( 'p', { p: true } ),
  751. new Text( 'e', { e: true } )
  752. ] );
  753. } );
  754. it( 'if selection is a range, should find first character in it and copy it\'s attributes', () => {
  755. selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
  756. expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
  757. // Step into elements when looking for first character:
  758. selection.setRanges( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
  759. expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
  760. } );
  761. it( 'if selection is collapsed it should seek a character to copy that character\'s attributes', () => {
  762. // Take styles from character before selection.
  763. selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
  764. expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
  765. // If there are none,
  766. // Take styles from character after selection.
  767. selection.setRanges( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] );
  768. expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
  769. // If there are none,
  770. // Look from the selection position to the beginning of node looking for character to take attributes from.
  771. selection.setRanges( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] );
  772. expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'c', true ] ] );
  773. // If there are none,
  774. // Look from the selection position to the end of node looking for character to take attributes from.
  775. selection.setRanges( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] );
  776. expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
  777. // If there are no characters to copy attributes from, use stored attributes.
  778. selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
  779. expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [] );
  780. } );
  781. it( 'should fire change:attribute event', () => {
  782. let spy = sinon.spy();
  783. selection.on( 'change:attribute', spy );
  784. selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
  785. expect( spy.called ).to.be.true;
  786. } );
  787. } );
  788. describe( '_getStoredAttributes', () => {
  789. it( 'should return no values if there are no ranges in selection', () => {
  790. let values = Array.from( selection._getStoredAttributes() );
  791. expect( values ).to.deep.equal( [] );
  792. } );
  793. } );
  794. } );