8
0

position.js 10.0 KB

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