rect.js 24 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  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. /* global window, document, console */
  6. import Rect from '../../src/dom/rect';
  7. describe( 'Rect', () => {
  8. let geometry;
  9. beforeEach( () => {
  10. geometry = {
  11. top: 10,
  12. right: 40,
  13. bottom: 30,
  14. left: 20,
  15. width: 20,
  16. height: 20
  17. };
  18. sinon.stub( console, 'warn' );
  19. } );
  20. afterEach( () => {
  21. sinon.restore();
  22. } );
  23. describe( 'constructor()', () => {
  24. it( 'should store passed object in #_source property', () => {
  25. const obj = {};
  26. const rect = new Rect( obj );
  27. expect( rect._source ).to.equal( obj );
  28. } );
  29. it( 'should accept HTMLElement', () => {
  30. const element = document.createElement( 'div' );
  31. sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
  32. assertRect( new Rect( element ), geometry );
  33. } );
  34. it( 'should accept Range (non–collapsed)', () => {
  35. const range = document.createRange();
  36. range.selectNode( document.body );
  37. sinon.stub( range, 'getClientRects' ).returns( [ geometry ] );
  38. assertRect( new Rect( range ), geometry );
  39. } );
  40. // https://github.com/ckeditor/ckeditor5-utils/issues/153
  41. it( 'should accept Range (collapsed)', () => {
  42. const range = document.createRange();
  43. range.collapse();
  44. sinon.stub( range, 'getClientRects' ).returns( [ geometry ] );
  45. assertRect( new Rect( range ), geometry );
  46. } );
  47. // https://github.com/ckeditor/ckeditor5-utils/issues/153
  48. it( 'should accept Range (collapsed, no Range rects available)', () => {
  49. const range = document.createRange();
  50. const element = document.createElement( 'div' );
  51. range.setStart( element, 0 );
  52. range.collapse();
  53. sinon.stub( range, 'getClientRects' ).returns( [] );
  54. sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
  55. const expectedGeometry = Object.assign( {}, geometry );
  56. expectedGeometry.right = expectedGeometry.left;
  57. expectedGeometry.width = 0;
  58. assertRect( new Rect( range ), expectedGeometry );
  59. } );
  60. it( 'should accept the window (viewport)', () => {
  61. sinon.stub( window, 'innerWidth' ).value( 1000 );
  62. sinon.stub( window, 'innerHeight' ).value( 500 );
  63. sinon.stub( window, 'scrollX' ).value( 100 );
  64. sinon.stub( window, 'scrollY' ).value( 200 );
  65. assertRect( new Rect( window ), {
  66. top: 0,
  67. right: 1000,
  68. bottom: 500,
  69. left: 0,
  70. width: 1000,
  71. height: 500
  72. } );
  73. } );
  74. it( 'should accept Rect', () => {
  75. const sourceRect = new Rect( geometry );
  76. const rect = new Rect( sourceRect );
  77. expect( rect ).to.not.equal( sourceRect );
  78. assertRect( rect, geometry );
  79. } );
  80. it( 'should accept ClientRect', () => {
  81. const clientRect = document.body.getBoundingClientRect();
  82. const { top, right, bottom, left, width, height } = clientRect;
  83. const rect = new Rect( clientRect );
  84. assertRect( rect, { top, right, bottom, left, width, height } );
  85. } );
  86. it( 'should accept geometry object', () => {
  87. assertRect( new Rect( geometry ), geometry );
  88. } );
  89. it( 'should accept objects from another window\'s scope', done => {
  90. const iframe = document.createElement( 'iframe' );
  91. iframe.addEventListener( 'load', () => {
  92. const iframeWindow = iframe.contentWindow;
  93. const element = iframeWindow.document.createElement( 'p' );
  94. const range = document.createRange();
  95. range.selectNode( iframeWindow.document.body );
  96. sinon.stub( range, 'getClientRects' ).returns( [ geometry ] );
  97. assertRect( new Rect( range ), geometry );
  98. sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
  99. assertRect( new Rect( element ), geometry );
  100. iframe.remove();
  101. done();
  102. } );
  103. document.body.appendChild( iframe );
  104. } );
  105. it( 'should copy the properties (Rect)', () => {
  106. const sourceGeometry = Object.assign( {}, geometry );
  107. const sourceRect = new Rect( geometry );
  108. const rect = new Rect( sourceRect );
  109. assertRect( rect, geometry );
  110. rect.top = 100;
  111. rect.width = 200;
  112. assertRect( sourceRect, sourceGeometry );
  113. } );
  114. it( 'should copy the properties (geomerty object)', () => {
  115. const sourceGeometry = Object.assign( {}, geometry );
  116. const rect = new Rect( geometry );
  117. assertRect( rect, geometry );
  118. rect.top = 100;
  119. rect.width = 200;
  120. assertRect( geometry, sourceGeometry );
  121. } );
  122. } );
  123. describe( 'clone()', () => {
  124. it( 'should clone the source rect', () => {
  125. const rect = new Rect( geometry );
  126. const clone = rect.clone();
  127. expect( clone ).to.be.instanceOf( Rect );
  128. expect( clone ).not.equal( rect );
  129. assertRect( clone, rect );
  130. } );
  131. it( 'should preserve #_source', () => {
  132. const rect = new Rect( geometry );
  133. const clone = rect.clone();
  134. expect( clone._source ).to.equal( rect._source );
  135. assertRect( clone, rect );
  136. } );
  137. } );
  138. describe( 'moveTo()', () => {
  139. it( 'should return the source rect', () => {
  140. const rect = new Rect( geometry );
  141. const returned = rect.moveTo( 100, 200 );
  142. expect( returned ).to.equal( rect );
  143. } );
  144. it( 'should move the rect', () => {
  145. const rect = new Rect( geometry );
  146. rect.moveTo( 100, 200 );
  147. assertRect( rect, {
  148. top: 200,
  149. right: 120,
  150. bottom: 220,
  151. left: 100,
  152. width: 20,
  153. height: 20
  154. } );
  155. } );
  156. } );
  157. describe( 'moveBy()', () => {
  158. it( 'should return the source rect', () => {
  159. const rect = new Rect( geometry );
  160. const returned = rect.moveBy( 100, 200 );
  161. expect( returned ).to.equal( rect );
  162. } );
  163. it( 'should move the rect', () => {
  164. const rect = new Rect( geometry );
  165. rect.moveBy( 100, 200 );
  166. assertRect( rect, {
  167. top: 210,
  168. right: 140,
  169. bottom: 230,
  170. left: 120,
  171. width: 20,
  172. height: 20
  173. } );
  174. } );
  175. } );
  176. describe( 'getIntersection()', () => {
  177. it( 'should return a new rect', () => {
  178. const rect = new Rect( geometry );
  179. const insersect = rect.getIntersection( new Rect( geometry ) );
  180. expect( insersect ).to.be.instanceOf( Rect );
  181. expect( insersect ).to.not.equal( rect );
  182. } );
  183. it( 'should calculate the geometry (#1)', () => {
  184. const rectA = new Rect( {
  185. top: 0,
  186. right: 100,
  187. bottom: 100,
  188. left: 0,
  189. width: 100,
  190. height: 100
  191. } );
  192. const rectB = new Rect( {
  193. top: 50,
  194. right: 150,
  195. bottom: 150,
  196. left: 50,
  197. width: 100,
  198. height: 100
  199. } );
  200. const insersect = rectA.getIntersection( rectB );
  201. assertRect( insersect, {
  202. top: 50,
  203. right: 100,
  204. bottom: 100,
  205. left: 50,
  206. width: 50,
  207. height: 50
  208. } );
  209. } );
  210. it( 'should calculate the geometry (#2)', () => {
  211. const rectA = new Rect( {
  212. top: 0,
  213. right: 100,
  214. bottom: 100,
  215. left: 0,
  216. width: 100,
  217. height: 100
  218. } );
  219. const rectB = new Rect( {
  220. top: 0,
  221. right: 200,
  222. bottom: 100,
  223. left: 100,
  224. width: 100,
  225. height: 100
  226. } );
  227. const insersect = rectA.getIntersection( rectB );
  228. assertRect( insersect, {
  229. top: 0,
  230. right: 100,
  231. bottom: 100,
  232. left: 100,
  233. width: 0,
  234. height: 100
  235. } );
  236. } );
  237. it( 'should calculate the geometry (#3)', () => {
  238. const rectA = new Rect( {
  239. top: 0,
  240. right: 100,
  241. bottom: 100,
  242. left: 0,
  243. width: 100,
  244. height: 100
  245. } );
  246. const rectB = new Rect( {
  247. top: 100,
  248. right: 300,
  249. bottom: 200,
  250. left: 200,
  251. width: 100,
  252. height: 100
  253. } );
  254. expect( rectA.getIntersection( rectB ) ).to.be.null;
  255. } );
  256. } );
  257. describe( 'getIntersectionArea()', () => {
  258. it( 'should calculate the area (#1)', () => {
  259. const rectA = new Rect( {
  260. top: 0,
  261. right: 100,
  262. bottom: 100,
  263. left: 0,
  264. width: 100,
  265. height: 100
  266. } );
  267. const rectB = new Rect( {
  268. top: 50,
  269. right: 150,
  270. bottom: 150,
  271. left: 50,
  272. width: 100,
  273. height: 100
  274. } );
  275. expect( rectA.getIntersectionArea( rectB ) ).to.equal( 2500 );
  276. } );
  277. it( 'should calculate the area (#2)', () => {
  278. const rectA = new Rect( {
  279. top: 0,
  280. right: 100,
  281. bottom: 100,
  282. left: 0,
  283. width: 100,
  284. height: 100
  285. } );
  286. const rectB = new Rect( {
  287. top: 0,
  288. right: 200,
  289. bottom: 100,
  290. left: 100,
  291. width: 100,
  292. height: 100
  293. } );
  294. expect( rectA.getIntersectionArea( rectB ) ).to.equal( 0 );
  295. } );
  296. it( 'should calculate the area (#3)', () => {
  297. const rectA = new Rect( {
  298. top: 0,
  299. right: 100,
  300. bottom: 100,
  301. left: 0,
  302. width: 100,
  303. height: 100
  304. } );
  305. const rectB = new Rect( {
  306. top: 100,
  307. right: 300,
  308. bottom: 200,
  309. left: 200,
  310. width: 100,
  311. height: 100
  312. } );
  313. expect( rectA.getIntersectionArea( rectB ) ).to.equal( 0 );
  314. } );
  315. } );
  316. describe( 'getArea()', () => {
  317. it( 'should calculate the area', () => {
  318. const rect = new Rect( {
  319. width: 100,
  320. height: 50
  321. } );
  322. expect( rect.getArea() ).to.equal( 5000 );
  323. } );
  324. } );
  325. describe( 'getVisible()', () => {
  326. let element, range, ancestorA, ancestorB;
  327. beforeEach( () => {
  328. element = document.createElement( 'div' );
  329. range = document.createRange();
  330. ancestorA = document.createElement( 'div' );
  331. ancestorB = document.createElement( 'div' );
  332. ancestorA.appendChild( element );
  333. document.body.appendChild( ancestorA );
  334. } );
  335. afterEach( () => {
  336. ancestorA.remove();
  337. ancestorB.remove();
  338. } );
  339. it( 'should return a new rect', () => {
  340. const rect = new Rect( element );
  341. const visible = rect.getVisible();
  342. expect( visible ).to.not.equal( rect );
  343. } );
  344. it( 'should not fail when the rect is for document#body', () => {
  345. sinon.stub( document.body, 'getBoundingClientRect' ).returns( {
  346. top: 0,
  347. right: 100,
  348. bottom: 100,
  349. left: 0,
  350. width: 100,
  351. height: 100
  352. } );
  353. assertRect( new Rect( document.body ).getVisible(), {
  354. top: 0,
  355. right: 100,
  356. bottom: 100,
  357. left: 0,
  358. width: 100,
  359. height: 100
  360. } );
  361. } );
  362. it( 'should not fail when the rect is for an object in another window\'s scope', done => {
  363. const iframe = document.createElement( 'iframe' );
  364. iframe.addEventListener( 'load', () => {
  365. const iframeWindow = iframe.contentWindow;
  366. const element = iframeWindow.document.createElement( 'p' );
  367. const ancestor = iframeWindow.document.createElement( 'p' );
  368. ancestor.appendChild( element );
  369. iframeWindow.document.body.appendChild( ancestor );
  370. sinon.stub( ancestor, 'getBoundingClientRect' ).returns( {
  371. top: 0,
  372. right: 50,
  373. bottom: 50,
  374. left: 0,
  375. width: 50,
  376. height: 50
  377. } );
  378. sinon.stub( element, 'getBoundingClientRect' ).returns( {
  379. top: 0,
  380. right: 100,
  381. bottom: 100,
  382. left: 0,
  383. width: 100,
  384. height: 100
  385. } );
  386. assertRect( new Rect( element ).getVisible(), {
  387. top: 0,
  388. right: 50,
  389. bottom: 50,
  390. left: 0,
  391. width: 50,
  392. height: 50
  393. } );
  394. iframe.remove();
  395. done();
  396. } );
  397. document.body.appendChild( iframe );
  398. } );
  399. it( 'should return the visible rect (HTMLElement), partially cropped', () => {
  400. sinon.stub( element, 'getBoundingClientRect' ).returns( {
  401. top: 0,
  402. right: 100,
  403. bottom: 100,
  404. left: 0,
  405. width: 100,
  406. height: 100
  407. } );
  408. sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
  409. top: 50,
  410. right: 150,
  411. bottom: 150,
  412. left: 50,
  413. width: 100,
  414. height: 100
  415. } );
  416. assertRect( new Rect( element ).getVisible(), {
  417. top: 50,
  418. right: 100,
  419. bottom: 100,
  420. left: 50,
  421. width: 50,
  422. height: 50
  423. } );
  424. } );
  425. it( 'should return the visible rect (HTMLElement), fully visible', () => {
  426. sinon.stub( element, 'getBoundingClientRect' ).returns( {
  427. top: 0,
  428. right: 100,
  429. bottom: 100,
  430. left: 0,
  431. width: 100,
  432. height: 100
  433. } );
  434. sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
  435. top: 0,
  436. right: 150,
  437. bottom: 150,
  438. left: 0,
  439. width: 150,
  440. height: 150
  441. } );
  442. assertRect( new Rect( element ).getVisible(), {
  443. top: 0,
  444. right: 100,
  445. bottom: 100,
  446. left: 0,
  447. width: 100,
  448. height: 100
  449. } );
  450. } );
  451. it( 'should return the visible rect (HTMLElement), partially cropped, deep ancestor overflow', () => {
  452. ancestorB.appendChild( ancestorA );
  453. document.body.appendChild( ancestorB );
  454. sinon.stub( element, 'getBoundingClientRect' ).returns( {
  455. top: 0,
  456. right: 100,
  457. bottom: 100,
  458. left: 0,
  459. width: 100,
  460. height: 100
  461. } );
  462. sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
  463. top: 50,
  464. right: 100,
  465. bottom: 100,
  466. left: 0,
  467. width: 50,
  468. height: 50
  469. } );
  470. sinon.stub( ancestorB, 'getBoundingClientRect' ).returns( {
  471. top: 0,
  472. right: 150,
  473. bottom: 100,
  474. left: 50,
  475. width: 100,
  476. height: 100
  477. } );
  478. assertRect( new Rect( element ).getVisible(), {
  479. top: 50,
  480. right: 100,
  481. bottom: 100,
  482. left: 50,
  483. width: 50,
  484. height: 50
  485. } );
  486. } );
  487. it( 'should return the visible rect (Range), partially cropped', () => {
  488. range.setStart( ancestorA, 0 );
  489. range.setEnd( ancestorA, 1 );
  490. sinon.stub( range, 'getClientRects' ).returns( [ {
  491. top: 0,
  492. right: 100,
  493. bottom: 100,
  494. left: 0,
  495. width: 100,
  496. height: 100
  497. } ] );
  498. sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
  499. top: 50,
  500. right: 150,
  501. bottom: 150,
  502. left: 50,
  503. width: 100,
  504. height: 100
  505. } );
  506. assertRect( new Rect( range ).getVisible(), {
  507. top: 50,
  508. right: 100,
  509. bottom: 100,
  510. left: 50,
  511. width: 50,
  512. height: 50
  513. } );
  514. } );
  515. it( 'should return null if there\'s no visible rect', () => {
  516. sinon.stub( element, 'getBoundingClientRect' ).returns( {
  517. top: 0,
  518. right: 100,
  519. bottom: 100,
  520. left: 0,
  521. width: 100,
  522. height: 100
  523. } );
  524. sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
  525. top: 150,
  526. right: 200,
  527. bottom: 200,
  528. left: 150,
  529. width: 50,
  530. height: 50
  531. } );
  532. expect( new Rect( element ).getVisible() ).to.equal( null );
  533. } );
  534. } );
  535. describe( 'isEqual()', () => {
  536. it( 'returns `true` when rects are equal', () => {
  537. const rectA = new Rect( {
  538. top: 10,
  539. right: 20,
  540. bottom: 30,
  541. left: 40,
  542. width: 10,
  543. height: 20
  544. } );
  545. const rectB = new Rect( {
  546. top: 10,
  547. right: 20,
  548. bottom: 30,
  549. left: 40,
  550. width: 10,
  551. height: 20
  552. } );
  553. expect( rectA.isEqual( rectB ) ).to.be.true;
  554. expect( rectB.isEqual( rectA ) ).to.be.true;
  555. expect( rectA.isEqual( rectA ) ).to.be.true;
  556. } );
  557. it( 'returns `false` when rects are different', () => {
  558. const rectA = new Rect( {
  559. top: 10,
  560. right: 20,
  561. bottom: 30,
  562. left: 40,
  563. width: 10,
  564. height: 20
  565. } );
  566. const rectB = new Rect( {
  567. top: 10,
  568. right: 20,
  569. bottom: 30,
  570. left: 40,
  571. width: 10,
  572. height: 30 // !
  573. } );
  574. expect( rectA.isEqual( rectB ) ).to.be.false;
  575. expect( rectB.isEqual( rectA ) ).to.be.false;
  576. } );
  577. } );
  578. describe( 'contains()', () => {
  579. it( 'should return true if rects are the same', () => {
  580. const rectA = new Rect( {
  581. top: 0,
  582. right: 100,
  583. bottom: 100,
  584. left: 0,
  585. width: 100,
  586. height: 100
  587. } );
  588. const rectB = new Rect( {
  589. top: 0,
  590. right: 100,
  591. bottom: 100,
  592. left: 0,
  593. width: 100,
  594. height: 100
  595. } );
  596. expect( rectA.isEqual( rectB ) ).to.be.true;
  597. expect( rectA.contains( rectB ) ).to.be.true;
  598. expect( rectB.contains( rectA ) ).to.be.true;
  599. } );
  600. it( 'should return true if rect is within', () => {
  601. const rectA = new Rect( {
  602. top: 0,
  603. right: 100,
  604. bottom: 100,
  605. left: 0,
  606. width: 100,
  607. height: 100
  608. } );
  609. const rectB = new Rect( {
  610. top: 10,
  611. right: 90,
  612. bottom: 90,
  613. left: 10,
  614. width: 80,
  615. height: 80
  616. } );
  617. expect( rectA.contains( rectB ) ).to.be.true;
  618. expect( rectB.contains( rectA ) ).to.be.false;
  619. } );
  620. it( 'should return false if rect extends beyond the boundaries', () => {
  621. const rectA = new Rect( {
  622. top: 0,
  623. right: 100,
  624. bottom: 100,
  625. left: 0,
  626. width: 100,
  627. height: 100
  628. } );
  629. const rectB = new Rect( {
  630. top: 10,
  631. right: 100,
  632. bottom: 110,
  633. left: 0,
  634. width: 100,
  635. height: 100
  636. } );
  637. expect( rectA.contains( rectB ) ).to.be.false;
  638. expect( rectB.contains( rectA ) ).to.be.false;
  639. } );
  640. it( 'should return false if rect is completely beyond the boundaries', () => {
  641. const rectA = new Rect( {
  642. top: 0,
  643. right: 100,
  644. bottom: 100,
  645. left: 0,
  646. width: 100,
  647. height: 100
  648. } );
  649. const rectB = new Rect( {
  650. top: 110,
  651. right: 100,
  652. bottom: 210,
  653. left: 0,
  654. width: 100,
  655. height: 100
  656. } );
  657. expect( rectA.contains( rectB ) ).to.be.false;
  658. expect( rectB.contains( rectA ) ).to.be.false;
  659. } );
  660. } );
  661. describe( 'excludeScrollbarsAndBorders()', () => {
  662. it( 'should exclude scrollbars and borders of a HTMLElement (dir="ltr")', () => {
  663. const element = document.createElement( 'div' );
  664. sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
  665. sinon.stub( window, 'getComputedStyle' )
  666. .withArgs( element )
  667. .returns( {
  668. borderTopWidth: '5px',
  669. borderRightWidth: '10px',
  670. borderLeftWidth: '5px',
  671. borderBottomWidth: '10px',
  672. direction: 'ltr'
  673. } );
  674. // Simulate 5px scrollbars.
  675. Object.defineProperties( element, {
  676. offsetWidth: {
  677. value: 20
  678. },
  679. offsetHeight: {
  680. value: 20
  681. },
  682. clientWidth: {
  683. value: 0
  684. },
  685. clientHeight: {
  686. value: 0
  687. }
  688. } );
  689. assertRect( new Rect( element ).excludeScrollbarsAndBorders(), {
  690. top: 15,
  691. right: 25,
  692. bottom: 15,
  693. left: 25,
  694. width: 0,
  695. height: 0
  696. } );
  697. } );
  698. it( 'should exclude scrollbars and borders of a HTMLElement (dir="rtl")', () => {
  699. const element = document.createElement( 'div' );
  700. element.setAttribute( 'dir', 'rtl' );
  701. sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
  702. sinon.stub( window, 'getComputedStyle' )
  703. .withArgs( element )
  704. .returns( {
  705. borderTopWidth: '5px',
  706. borderRightWidth: '10px',
  707. borderLeftWidth: '5px',
  708. borderBottomWidth: '10px',
  709. direction: 'rtl'
  710. } );
  711. // Simulate 5px scrollbars.
  712. Object.defineProperties( element, {
  713. offsetWidth: {
  714. value: 20
  715. },
  716. offsetHeight: {
  717. value: 20
  718. },
  719. clientWidth: {
  720. value: 0
  721. },
  722. clientHeight: {
  723. value: 0
  724. }
  725. } );
  726. assertRect( new Rect( element ).excludeScrollbarsAndBorders(), {
  727. top: 15,
  728. right: 30,
  729. bottom: 15,
  730. left: 30,
  731. width: 0,
  732. height: 0
  733. } );
  734. } );
  735. it( 'should exclude scrollbars from viewport\'s rect (dir="ltr")', () => {
  736. sinon.stub( window, 'innerWidth' ).value( 1000 );
  737. sinon.stub( window, 'innerHeight' ).value( 500 );
  738. sinon.stub( window, 'scrollX' ).value( 100 );
  739. sinon.stub( window, 'scrollY' ).value( 200 );
  740. sinon.stub( document, 'documentElement' ).value( {
  741. clientWidth: 990,
  742. clientHeight: 490
  743. } );
  744. sinon.stub( window, 'getComputedStyle' )
  745. .withArgs( document.documentElement )
  746. .returns( {
  747. direction: 'ltr'
  748. } );
  749. assertRect( new Rect( window ).excludeScrollbarsAndBorders(), {
  750. top: 0,
  751. right: 990,
  752. bottom: 490,
  753. left: 0,
  754. width: 990,
  755. height: 490
  756. } );
  757. } );
  758. it( 'should exclude scrollbars from viewport\'s rect (dir="rtl")', () => {
  759. sinon.stub( window, 'innerWidth' ).value( 1000 );
  760. sinon.stub( window, 'innerHeight' ).value( 500 );
  761. sinon.stub( window, 'scrollX' ).value( 100 );
  762. sinon.stub( window, 'scrollY' ).value( 200 );
  763. sinon.stub( document, 'documentElement' ).value( {
  764. clientWidth: 990,
  765. clientHeight: 490
  766. } );
  767. sinon.stub( window, 'getComputedStyle' )
  768. .withArgs( document.documentElement )
  769. .returns( {
  770. direction: 'rtl'
  771. } );
  772. assertRect( new Rect( window ).excludeScrollbarsAndBorders(), {
  773. top: 0,
  774. right: 1000,
  775. bottom: 490,
  776. left: 10,
  777. width: 990,
  778. height: 490
  779. } );
  780. } );
  781. it( 'should work for a window in an iframe', done => {
  782. const iframe = document.createElement( 'iframe' );
  783. // Mock the properties of the top window. Then make sure the ones
  784. // from the child are used.
  785. sinon.stub( window, 'innerWidth' ).value( 1000 );
  786. sinon.stub( window, 'innerHeight' ).value( 500 );
  787. sinon.stub( window, 'scrollX' ).value( 100 );
  788. sinon.stub( window, 'scrollY' ).value( 200 );
  789. sinon.stub( document, 'documentElement' ).value( {
  790. clientWidth: 990,
  791. clientHeight: 490
  792. } );
  793. sinon.stub( window, 'getComputedStyle' )
  794. .withArgs( document.documentElement )
  795. .returns( {
  796. direction: 'ltr'
  797. } );
  798. iframe.addEventListener( 'load', () => {
  799. const iframeWindow = iframe.contentWindow;
  800. sinon.stub( iframeWindow, 'innerWidth' ).value( 500 );
  801. sinon.stub( iframeWindow, 'innerHeight' ).value( 250 );
  802. sinon.stub( iframeWindow, 'scrollX' ).value( 50 );
  803. sinon.stub( iframeWindow, 'scrollY' ).value( 100 );
  804. sinon.stub( iframeWindow.document, 'documentElement' ).value( {
  805. clientWidth: 480,
  806. clientHeight: 230
  807. } );
  808. sinon.stub( iframeWindow, 'getComputedStyle' )
  809. .withArgs( iframeWindow.document.documentElement )
  810. .returns( {
  811. direction: 'ltr'
  812. } );
  813. assertRect( new Rect( iframeWindow ).excludeScrollbarsAndBorders(), {
  814. top: 0,
  815. right: 480,
  816. bottom: 230,
  817. left: 0,
  818. width: 480,
  819. height: 230
  820. } );
  821. // Safari fails because of "afterEach()" hook tries to restore values from removed element.
  822. // We need to restore these values manually.
  823. sinon.restore();
  824. iframe.remove();
  825. done();
  826. } );
  827. document.body.appendChild( iframe );
  828. } );
  829. } );
  830. describe( 'getDomRangeRects() ', () => {
  831. it( 'should return rects for a Range (non–collapsed)', () => {
  832. const range = document.createRange();
  833. range.selectNode( document.body );
  834. sinon.stub( range, 'getClientRects' ).returns( [ geometry ] );
  835. const rects = Rect.getDomRangeRects( range );
  836. expect( rects ).to.have.length( 1 );
  837. assertRect( rects[ 0 ], geometry );
  838. } );
  839. // https://github.com/ckeditor/ckeditor5-utils/issues/153
  840. it( 'should return rects for a Range (collapsed)', () => {
  841. const range = document.createRange();
  842. const secondGeometry = { top: 20, right: 80, bottom: 60, left: 40, width: 40, height: 40 };
  843. range.collapse();
  844. sinon.stub( range, 'getClientRects' ).returns( [ geometry, secondGeometry ] );
  845. const rects = Rect.getDomRangeRects( range );
  846. expect( rects ).to.have.length( 2 );
  847. assertRect( rects[ 0 ], geometry );
  848. assertRect( rects[ 1 ], secondGeometry );
  849. } );
  850. // https://github.com/ckeditor/ckeditor5-utils/issues/153
  851. it( 'should return rects for a Range (collapsed, no Range rects available)', () => {
  852. const range = document.createRange();
  853. const element = document.createElement( 'div' );
  854. range.setStart( element, 0 );
  855. range.collapse();
  856. sinon.stub( range, 'getClientRects' ).returns( [] );
  857. sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
  858. const expectedGeometry = Object.assign( {}, geometry );
  859. expectedGeometry.right = expectedGeometry.left;
  860. expectedGeometry.width = 0;
  861. const rects = Rect.getDomRangeRects( range );
  862. expect( rects ).to.have.length( 1 );
  863. assertRect( rects[ 0 ], expectedGeometry );
  864. } );
  865. // https://github.com/ckeditor/ckeditor5-ui/issues/317
  866. it( 'should return rects for a text node\'s parent (collapsed, no Range rects available)', () => {
  867. const range = document.createRange();
  868. const element = document.createElement( 'div' );
  869. const textNode = document.createTextNode( 'abc' );
  870. element.appendChild( textNode );
  871. range.setStart( textNode, 3 );
  872. range.collapse();
  873. sinon.stub( range, 'getClientRects' ).returns( [] );
  874. sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
  875. const expectedGeometry = Object.assign( {}, geometry );
  876. expectedGeometry.right = expectedGeometry.left;
  877. expectedGeometry.width = 0;
  878. const rects = Rect.getDomRangeRects( range );
  879. expect( rects ).to.have.length( 1 );
  880. assertRect( rects[ 0 ], expectedGeometry );
  881. } );
  882. } );
  883. } );
  884. function assertRect( rect, expected ) {
  885. expect( rect ).to.deep.equal( expected );
  886. }