position.js 9.6 KB

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