position.js 9.8 KB

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