position.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  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. limiter.parentNode = getElement( {
  143. top: 100,
  144. left: 0,
  145. bottom: 110,
  146. right: 10,
  147. width: 10,
  148. height: 10
  149. } );
  150. assertPosition( {
  151. element, target, limiter,
  152. positions: [ attachRight, attachLeft ]
  153. }, {
  154. top: 100,
  155. left: 10,
  156. name: 'right'
  157. } );
  158. } );
  159. } );
  160. describe( 'with fitInViewport on', () => {
  161. beforeEach( setElementTargetLimiterPlayground );
  162. it( 'should return coordinates (#1)', () => {
  163. assertPosition( {
  164. element, target,
  165. positions: [ attachLeft, attachRight ],
  166. fitInViewport: true
  167. }, {
  168. top: 100,
  169. left: 10,
  170. name: 'right'
  171. } );
  172. } );
  173. it( 'should return coordinates (#2)', () => {
  174. assertPosition( {
  175. element, target,
  176. positions: [ attachRight, attachLeft ],
  177. fitInViewport: true
  178. }, {
  179. top: 100,
  180. left: 10,
  181. name: 'right'
  182. } );
  183. } );
  184. it( 'should return coordinates (#3)', () => {
  185. assertPosition( {
  186. element, target,
  187. positions: [ attachLeft, attachBottom, attachRight ],
  188. fitInViewport: true
  189. }, {
  190. top: 110,
  191. left: 0,
  192. name: 'bottom'
  193. } );
  194. } );
  195. } );
  196. describe( 'with limiter and fitInViewport on', () => {
  197. beforeEach( setElementTargetLimiterPlayground );
  198. it( 'should return coordinates (#1)', () => {
  199. assertPosition( {
  200. element, target, limiter,
  201. positions: [ attachLeft, attachRight ],
  202. fitInViewport: true
  203. }, {
  204. top: 100,
  205. left: 10,
  206. name: 'right'
  207. } );
  208. } );
  209. it( 'should return coordinates (#2)', () => {
  210. assertPosition( {
  211. element, target, limiter,
  212. positions: [ attachRight, attachLeft ],
  213. fitInViewport: true
  214. }, {
  215. top: 100,
  216. left: 10,
  217. name: 'right'
  218. } );
  219. } );
  220. it( 'should return coordinates (#3)', () => {
  221. assertPosition( {
  222. element, target, limiter,
  223. positions: [ attachRight, attachLeft, attachBottom ],
  224. fitInViewport: true
  225. }, {
  226. top: 110,
  227. left: 0,
  228. name: 'bottom'
  229. } );
  230. } );
  231. it( 'should return coordinates (#4)', () => {
  232. assertPosition( {
  233. element, target, limiter,
  234. positions: [ attachTop, attachRight ],
  235. fitInViewport: true
  236. }, {
  237. top: 100,
  238. left: 10,
  239. name: 'right'
  240. } );
  241. } );
  242. it( 'should return the very first coordinates if no fitting position with a positive intersection has been found', () => {
  243. assertPosition( {
  244. element, target, limiter,
  245. positions: [
  246. () => ( {
  247. left: -10000,
  248. top: -10000,
  249. name: 'no-intersect-position'
  250. } )
  251. ],
  252. fitInViewport: true
  253. }, {
  254. left: -10000,
  255. top: -10000,
  256. name: 'no-intersect-position'
  257. } );
  258. } );
  259. it( 'should return the very first coordinates if limiter does not fit into the viewport', () => {
  260. limiter = getElement( {
  261. top: -100,
  262. right: -80,
  263. bottom: -80,
  264. left: -100,
  265. width: 20,
  266. height: 20
  267. } );
  268. assertPosition( {
  269. element, target, limiter,
  270. positions: [ attachRight, attachTop ],
  271. fitInViewport: true
  272. }, {
  273. top: 100,
  274. left: 10,
  275. name: 'right'
  276. } );
  277. } );
  278. } );
  279. } );
  280. function assertPosition( options, expected ) {
  281. const position = getOptimalPosition( options );
  282. expect( position ).to.deep.equal( expected );
  283. }
  284. // +--------+-----+
  285. // | E | T |
  286. // +--------+-----+
  287. const attachLeft = ( targetRect, elementRect ) => ( {
  288. top: targetRect.top,
  289. left: targetRect.left - elementRect.width,
  290. name: 'left'
  291. } );
  292. // +-----+--------+
  293. // | T | E |
  294. // +-----+--------+
  295. const attachRight = ( targetRect ) => ( {
  296. top: targetRect.top,
  297. left: targetRect.left + targetRect.width,
  298. name: 'right'
  299. } );
  300. // +-----+
  301. // | T |
  302. // +-----+--+
  303. // | E |
  304. // +--------+
  305. const attachBottom = ( targetRect ) => ( {
  306. top: targetRect.bottom,
  307. left: targetRect.left,
  308. name: 'bottom'
  309. } );
  310. // +--------+
  311. // | E |
  312. // +--+-----+
  313. // | T |
  314. // +-----+
  315. const attachTop = ( targetRect, elementRect ) => ( {
  316. top: targetRect.top - elementRect.height,
  317. left: targetRect.left - ( elementRect.width - targetRect.width ),
  318. name: 'bottom'
  319. } );
  320. // Returns a synthetic element.
  321. //
  322. // @private
  323. // @param {Object} properties A set of properties for the element.
  324. // @param {Object} styles A set of styles in `window.getComputedStyle()` format.
  325. function getElement( properties = {}, styles = {} ) {
  326. const element = {
  327. tagName: 'div',
  328. scrollLeft: 0,
  329. scrollTop: 0
  330. };
  331. Object.assign( element, properties );
  332. if ( !styles.borderLeftWidth ) {
  333. styles.borderLeftWidth = '0px';
  334. }
  335. if ( !styles.borderTopWidth ) {
  336. styles.borderTopWidth = '0px';
  337. }
  338. global.window.getComputedStyle.withArgs( element ).returns( styles );
  339. return element;
  340. }
  341. // Stubs the window.
  342. //
  343. // @private
  344. // @param {Object} properties A set of properties the window should have.
  345. function stubWindow( properties ) {
  346. global.window = Object.assign( {
  347. getComputedStyle: sinon.stub()
  348. }, properties );
  349. }
  350. // <-- 100px ->
  351. //
  352. // ^ +--------------[ Viewport ]----------
  353. // | |
  354. // 100px |
  355. // | | <-- 10px -->
  356. // V |
  357. // | ^ +---------+
  358. // | | | |
  359. // | 10px | T |
  360. // | | | |
  361. // | V +---------+
  362. // |
  363. //
  364. function setElementTargetPlayground() {
  365. element = getElement( {
  366. top: 0,
  367. right: 20,
  368. bottom: 20,
  369. left: 0,
  370. width: 20,
  371. height: 20
  372. } );
  373. target = getElement( {
  374. top: 100,
  375. right: 110,
  376. bottom: 110,
  377. left: 100,
  378. width: 10,
  379. height: 10
  380. } );
  381. }
  382. //
  383. //
  384. // ^ +-----------[ Viewport ]----------------------
  385. // | |
  386. // 100px |
  387. // | <--------- 20px ------->
  388. // | <-- 10px -->
  389. // V |
  390. // +------------+---------+ ^ ^
  391. // | | | | |
  392. // | | T | 10px |
  393. // | | | | |
  394. // | +---------+ V 20px
  395. // | | | |
  396. // | | | |
  397. // | | | |
  398. // +------[ Limiter ]-----+ V
  399. // |
  400. // |
  401. //
  402. //
  403. function setElementTargetLimiterPlayground() {
  404. element = getElement( {
  405. top: 0,
  406. right: 20,
  407. bottom: 20,
  408. left: 0,
  409. width: 20,
  410. height: 20
  411. } );
  412. limiter = getElement( {
  413. top: 100,
  414. right: 10,
  415. bottom: 120,
  416. left: -10,
  417. width: 20,
  418. height: 20
  419. } );
  420. target = getElement( {
  421. top: 100,
  422. right: 10,
  423. bottom: 110,
  424. left: 0,
  425. width: 10,
  426. height: 10
  427. } );
  428. }