position.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document, window */
  6. import { getOptimalPosition } from '../../src/dom/position';
  7. import Rect from '../../src/dom/rect';
  8. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  9. let element, target, limiter;
  10. // +--------+-----+
  11. // | E | T |
  12. // +--------+-----+
  13. const attachLeft = ( targetRect, elementRect ) => ( {
  14. top: targetRect.top,
  15. left: targetRect.left - elementRect.width,
  16. name: 'left'
  17. } );
  18. // +-----+--------+
  19. // | T | E |
  20. // +-----+--------+
  21. const attachRight = targetRect => ( {
  22. top: targetRect.top,
  23. left: targetRect.left + targetRect.width,
  24. name: 'right'
  25. } );
  26. // +-----+
  27. // | T |
  28. // +-----+--+
  29. // | E |
  30. // +--------+
  31. const attachBottom = targetRect => ( {
  32. top: targetRect.bottom,
  33. left: targetRect.left,
  34. name: 'bottom'
  35. } );
  36. // +--------+
  37. // | E |
  38. // +--+-----+
  39. // | T |
  40. // +-----+
  41. const attachTop = ( targetRect, elementRect ) => ( {
  42. top: targetRect.top - elementRect.height,
  43. left: targetRect.left - ( elementRect.width - targetRect.width ),
  44. name: 'bottom'
  45. } );
  46. describe( 'getOptimalPosition()', () => {
  47. testUtils.createSinonSandbox();
  48. beforeEach( () => {
  49. testUtils.sinon.stub( window, 'getComputedStyle' );
  50. stubWindow( {
  51. innerWidth: 10000,
  52. innerHeight: 10000,
  53. scrollX: 0,
  54. scrollY: 0
  55. } );
  56. } );
  57. it( 'should work when the target is a Function', () => {
  58. setElementTargetPlayground();
  59. assertPosition( {
  60. element,
  61. target: () => target,
  62. positions: [ attachLeft ]
  63. }, {
  64. top: 100,
  65. left: 80,
  66. name: 'left'
  67. } );
  68. } );
  69. it( 'should work when the target is a Rect', () => {
  70. setElementTargetPlayground();
  71. assertPosition( {
  72. element,
  73. target: new Rect( target ),
  74. positions: [ attachLeft ]
  75. }, {
  76. top: 100,
  77. left: 80,
  78. name: 'left'
  79. } );
  80. } );
  81. describe( 'for single position', () => {
  82. beforeEach( setElementTargetPlayground );
  83. it( 'should return coordinates', () => {
  84. assertPosition( { element, target, positions: [ attachLeft ] }, {
  85. top: 100,
  86. left: 80,
  87. name: 'left'
  88. } );
  89. } );
  90. it( 'should return coordinates (window scroll)', () => {
  91. stubWindow( {
  92. innerWidth: 10000,
  93. innerHeight: 10000,
  94. scrollX: 100,
  95. scrollY: 100,
  96. } );
  97. assertPosition( { element, target, positions: [ attachLeft ] }, {
  98. top: 200,
  99. left: 180,
  100. name: 'left'
  101. } );
  102. } );
  103. describe( 'positioned element parent', () => {
  104. let parent;
  105. it( 'should return coordinates', () => {
  106. stubWindow( {
  107. innerWidth: 10000,
  108. innerHeight: 10000,
  109. scrollX: 1000,
  110. scrollY: 1000
  111. } );
  112. parent = getElement( {
  113. top: 1000,
  114. right: 1010,
  115. bottom: 1010,
  116. left: 1000,
  117. width: 10,
  118. height: 10
  119. }, {
  120. position: 'absolute'
  121. } );
  122. element.parentElement = parent;
  123. assertPosition( { element, target, positions: [ attachLeft ] }, {
  124. top: -900,
  125. left: -920,
  126. name: 'left'
  127. } );
  128. } );
  129. it( 'should return coordinates (scroll and border)', () => {
  130. stubWindow( {
  131. innerWidth: 10000,
  132. innerHeight: 10000,
  133. scrollX: 1000,
  134. scrollY: 1000
  135. } );
  136. parent = getElement( {
  137. top: 0,
  138. right: 10,
  139. bottom: 10,
  140. left: 0,
  141. width: 10,
  142. height: 10,
  143. scrollTop: 100,
  144. scrollLeft: 200
  145. }, {
  146. position: 'absolute',
  147. borderLeftWidth: '20px',
  148. borderTopWidth: '40px',
  149. } );
  150. element.parentElement = parent;
  151. assertPosition( { element, target, positions: [ attachLeft ] }, {
  152. top: 160,
  153. left: 260,
  154. name: 'left'
  155. } );
  156. } );
  157. } );
  158. } );
  159. describe( 'for multiple positions', () => {
  160. beforeEach( setElementTargetPlayground );
  161. it( 'should return coordinates', () => {
  162. assertPosition( {
  163. element, target,
  164. positions: [ attachLeft, attachRight ]
  165. }, {
  166. top: 100,
  167. left: 80,
  168. name: 'left'
  169. } );
  170. } );
  171. it( 'should return coordinates (position preference order)', () => {
  172. assertPosition( {
  173. element, target,
  174. positions: [ attachRight, attachLeft ]
  175. }, {
  176. top: 100,
  177. left: 110,
  178. name: 'right'
  179. } );
  180. } );
  181. } );
  182. describe( 'with a limiter', () => {
  183. beforeEach( setElementTargetLimiterPlayground );
  184. it( 'should work when the limiter is a Function', () => {
  185. assertPosition( {
  186. element, target,
  187. limiter: () => limiter,
  188. positions: [ attachLeft, attachRight ]
  189. }, {
  190. top: 100,
  191. left: -20,
  192. name: 'left'
  193. } );
  194. } );
  195. it( 'should work when the limiter is a Rect', () => {
  196. assertPosition( {
  197. element, target,
  198. limiter: new Rect( limiter ),
  199. positions: [ attachLeft, attachRight ]
  200. }, {
  201. top: 100,
  202. left: -20,
  203. name: 'left'
  204. } );
  205. } );
  206. it( 'should return coordinates (#1)', () => {
  207. assertPosition( {
  208. element, target, limiter,
  209. positions: [ attachLeft, attachRight ]
  210. }, {
  211. top: 100,
  212. left: -20,
  213. name: 'left'
  214. } );
  215. } );
  216. it( 'should return coordinates (#2)', () => {
  217. assertPosition( {
  218. element, target, limiter,
  219. positions: [ attachRight, attachLeft ]
  220. }, {
  221. top: 100,
  222. left: -20,
  223. name: 'left'
  224. } );
  225. } );
  226. // https://github.com/ckeditor/ckeditor5-utils/issues/148
  227. it( 'should return coordinates (#3)', () => {
  228. limiter.parentNode = getElement( {
  229. top: 100,
  230. left: 0,
  231. bottom: 110,
  232. right: 10,
  233. width: 10,
  234. height: 10
  235. } );
  236. assertPosition( {
  237. element, target, limiter,
  238. positions: [ attachRight, attachLeft ]
  239. }, {
  240. top: 100,
  241. left: 10,
  242. name: 'right'
  243. } );
  244. } );
  245. } );
  246. describe( 'with fitInViewport on', () => {
  247. beforeEach( setElementTargetLimiterPlayground );
  248. it( 'should return coordinates (#1)', () => {
  249. assertPosition( {
  250. element, target,
  251. positions: [ attachLeft, attachRight ],
  252. fitInViewport: true
  253. }, {
  254. top: 100,
  255. left: 10,
  256. name: 'right'
  257. } );
  258. } );
  259. it( 'should return coordinates (#2)', () => {
  260. assertPosition( {
  261. element, target,
  262. positions: [ attachRight, attachLeft ],
  263. fitInViewport: true
  264. }, {
  265. top: 100,
  266. left: 10,
  267. name: 'right'
  268. } );
  269. } );
  270. it( 'should return coordinates (#3)', () => {
  271. assertPosition( {
  272. element, target,
  273. positions: [ attachLeft, attachBottom, attachRight ],
  274. fitInViewport: true
  275. }, {
  276. top: 110,
  277. left: 0,
  278. name: 'bottom'
  279. } );
  280. } );
  281. } );
  282. describe( 'with limiter and fitInViewport on', () => {
  283. beforeEach( setElementTargetLimiterPlayground );
  284. it( 'should return coordinates (#1)', () => {
  285. assertPosition( {
  286. element, target, limiter,
  287. positions: [ attachLeft, attachRight ],
  288. fitInViewport: true
  289. }, {
  290. top: 100,
  291. left: 10,
  292. name: 'right'
  293. } );
  294. } );
  295. it( 'should return coordinates (#2)', () => {
  296. assertPosition( {
  297. element, target, limiter,
  298. positions: [ attachRight, attachLeft ],
  299. fitInViewport: true
  300. }, {
  301. top: 100,
  302. left: 10,
  303. name: 'right'
  304. } );
  305. } );
  306. it( 'should return coordinates (#3)', () => {
  307. assertPosition( {
  308. element, target, limiter,
  309. positions: [ attachRight, attachLeft, attachBottom ],
  310. fitInViewport: true
  311. }, {
  312. top: 110,
  313. left: 0,
  314. name: 'bottom'
  315. } );
  316. } );
  317. it( 'should return coordinates (#4)', () => {
  318. assertPosition( {
  319. element, target, limiter,
  320. positions: [ attachTop, attachRight ],
  321. fitInViewport: true
  322. }, {
  323. top: 100,
  324. left: 10,
  325. name: 'right'
  326. } );
  327. } );
  328. it( 'should return the very first coordinates if no fitting position with a positive intersection has been found', () => {
  329. assertPosition( {
  330. element, target, limiter,
  331. positions: [
  332. () => ( {
  333. left: -10000,
  334. top: -10000,
  335. name: 'no-intersect-position'
  336. } )
  337. ],
  338. fitInViewport: true
  339. }, {
  340. left: -10000,
  341. top: -10000,
  342. name: 'no-intersect-position'
  343. } );
  344. } );
  345. it( 'should return the very first coordinates if limiter does not fit into the viewport', () => {
  346. limiter = getElement( {
  347. top: -100,
  348. right: -80,
  349. bottom: -80,
  350. left: -100,
  351. width: 20,
  352. height: 20
  353. } );
  354. assertPosition( {
  355. element, target, limiter,
  356. positions: [ attachRight, attachTop ],
  357. fitInViewport: true
  358. }, {
  359. top: 100,
  360. left: 10,
  361. name: 'right'
  362. } );
  363. } );
  364. } );
  365. } );
  366. function assertPosition( options, expected ) {
  367. const position = getOptimalPosition( options );
  368. expect( position ).to.deep.equal( expected );
  369. }
  370. // Returns a synthetic element.
  371. //
  372. // @private
  373. // @param {Object} properties A set of properties for the element.
  374. // @param {Object} styles A set of styles in `window.getComputedStyle()` format.
  375. function getElement( properties = {}, styles = {} ) {
  376. const element = {
  377. tagName: 'div',
  378. scrollLeft: 0,
  379. scrollTop: 0,
  380. ownerDocument: document
  381. };
  382. Object.assign( element, properties );
  383. if ( !styles.borderLeftWidth ) {
  384. styles.borderLeftWidth = '0px';
  385. }
  386. if ( !styles.borderTopWidth ) {
  387. styles.borderTopWidth = '0px';
  388. }
  389. window.getComputedStyle.withArgs( element ).returns( styles );
  390. return element;
  391. }
  392. // Stubs the window.
  393. //
  394. // @private
  395. // @param {Object} properties A set of properties the window should have.
  396. function stubWindow( properties ) {
  397. for ( const p in properties ) {
  398. testUtils.sinon.stub( window, p ).value( properties[ p ] );
  399. }
  400. }
  401. // <-- 100px ->
  402. //
  403. // ^ +--------------[ Viewport ]----------
  404. // | |
  405. // 100px |
  406. // | | <-- 10px -->
  407. // V |
  408. // | ^ +---------+
  409. // | | | |
  410. // | 10px | T |
  411. // | | | |
  412. // | V +---------+
  413. // |
  414. //
  415. function setElementTargetPlayground() {
  416. element = getElement( {
  417. top: 0,
  418. right: 20,
  419. bottom: 20,
  420. left: 0,
  421. width: 20,
  422. height: 20
  423. } );
  424. target = getElement( {
  425. top: 100,
  426. right: 110,
  427. bottom: 110,
  428. left: 100,
  429. width: 10,
  430. height: 10
  431. } );
  432. }
  433. //
  434. //
  435. // ^ +-----------[ Viewport ]----------------------
  436. // | |
  437. // 100px |
  438. // | <--------- 20px ------->
  439. // | <-- 10px -->
  440. // V |
  441. // +------------+---------+ ^ ^
  442. // | | | | |
  443. // | | T | 10px |
  444. // | | | | |
  445. // | +---------+ V 20px
  446. // | | | |
  447. // | | | |
  448. // | | | |
  449. // +------[ Limiter ]-----+ V
  450. // |
  451. // |
  452. //
  453. //
  454. function setElementTargetLimiterPlayground() {
  455. element = getElement( {
  456. top: 0,
  457. right: 20,
  458. bottom: 20,
  459. left: 0,
  460. width: 20,
  461. height: 20
  462. } );
  463. limiter = getElement( {
  464. top: 100,
  465. right: 10,
  466. bottom: 120,
  467. left: -10,
  468. width: 20,
  469. height: 20
  470. } );
  471. target = getElement( {
  472. top: 100,
  473. right: 10,
  474. bottom: 110,
  475. left: 0,
  476. width: 10,
  477. height: 10
  478. } );
  479. }