position.js 8.8 KB

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