position.js 11 KB

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