8
0

position.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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. assertPosition( { element, target, positions: [ attachLeft ] }, {
  56. top: -900,
  57. left: -920,
  58. name: 'left'
  59. } );
  60. } );
  61. } );
  62. describe( 'for multiple positions', () => {
  63. beforeEach( setElementTargetPlayground );
  64. it( 'should return coordinates', () => {
  65. assertPosition( {
  66. element, target,
  67. positions: [ attachLeft, attachRight ]
  68. }, {
  69. top: 100,
  70. left: 80,
  71. name: 'left'
  72. } );
  73. } );
  74. it( 'should return coordinates (position preference order)', () => {
  75. assertPosition( {
  76. element, target,
  77. positions: [ attachRight, attachLeft ]
  78. }, {
  79. top: 100,
  80. left: 110,
  81. name: 'right'
  82. } );
  83. } );
  84. } );
  85. describe( 'with a limiter', () => {
  86. beforeEach( setElementTargetLimiterPlayground );
  87. it( 'should return coordinates (#1)', () => {
  88. assertPosition( {
  89. element, target, limiter,
  90. positions: [ attachLeft, attachRight ]
  91. }, {
  92. top: 100,
  93. left: -20,
  94. name: 'left'
  95. } );
  96. } );
  97. it( 'should return coordinates (#2)', () => {
  98. assertPosition( {
  99. element, target, limiter,
  100. positions: [ attachRight, attachLeft ]
  101. }, {
  102. top: 100,
  103. left: -20,
  104. name: 'left'
  105. } );
  106. } );
  107. } );
  108. describe( 'with fitInViewport on', () => {
  109. beforeEach( setElementTargetLimiterPlayground );
  110. it( 'should return coordinates (#1)', () => {
  111. assertPosition( {
  112. element, target,
  113. positions: [ attachLeft, attachRight ],
  114. fitInViewport: true
  115. }, {
  116. top: 100,
  117. left: 10,
  118. name: 'right'
  119. } );
  120. } );
  121. it( 'should return coordinates (#2)', () => {
  122. assertPosition( {
  123. element, target,
  124. positions: [ attachRight, attachLeft ],
  125. fitInViewport: true
  126. }, {
  127. top: 100,
  128. left: 10,
  129. name: 'right'
  130. } );
  131. } );
  132. it( 'should return coordinates (#3)', () => {
  133. assertPosition( {
  134. element, target,
  135. positions: [ attachLeft, attachBottom, attachRight ],
  136. fitInViewport: true
  137. }, {
  138. top: 110,
  139. left: 0,
  140. name: 'bottom'
  141. } );
  142. } );
  143. } );
  144. describe( 'with limiter and fitInViewport on', () => {
  145. beforeEach( setElementTargetLimiterPlayground );
  146. it( 'should return coordinates (#1)', () => {
  147. assertPosition( {
  148. element, target, limiter,
  149. positions: [ attachLeft, attachRight ],
  150. fitInViewport: true
  151. }, {
  152. top: 100,
  153. left: 10,
  154. name: 'right'
  155. } );
  156. } );
  157. it( 'should return coordinates (#2)', () => {
  158. assertPosition( {
  159. element, target, limiter,
  160. positions: [ attachRight, attachLeft ],
  161. fitInViewport: true
  162. }, {
  163. top: 100,
  164. left: 10,
  165. name: 'right'
  166. } );
  167. } );
  168. it( 'should return coordinates (#3)', () => {
  169. assertPosition( {
  170. element, target, limiter,
  171. positions: [ attachRight, attachLeft, attachBottom ],
  172. fitInViewport: true
  173. }, {
  174. top: 110,
  175. left: 0,
  176. name: 'bottom'
  177. } );
  178. } );
  179. it( 'should return coordinates (#4)', () => {
  180. assertPosition( {
  181. element, target, limiter,
  182. positions: [ attachTop, attachRight ],
  183. fitInViewport: true
  184. }, {
  185. top: 100,
  186. left: 10,
  187. name: 'right'
  188. } );
  189. } );
  190. it( 'should return the very first coordinates if no fitting position with a positive intersection has been found', () => {
  191. assertPosition( {
  192. element, target, limiter,
  193. positions: [
  194. () => ( {
  195. left: -10000,
  196. top: -10000,
  197. name: 'no-intersect-position'
  198. } )
  199. ],
  200. fitInViewport: true
  201. }, {
  202. left: -10000,
  203. top: -10000,
  204. name: 'no-intersect-position'
  205. } );
  206. } );
  207. it( 'should return the very first coordinates if limiter does not fit into the viewport', () => {
  208. stubElementRect( limiter, {
  209. top: -100,
  210. right: -80,
  211. bottom: -80,
  212. left: -100,
  213. width: 20,
  214. height: 20
  215. } );
  216. assertPosition( {
  217. element, target, limiter,
  218. positions: [ attachRight, attachTop ],
  219. fitInViewport: true
  220. }, {
  221. top: 100,
  222. left: 10,
  223. name: 'right'
  224. } );
  225. } );
  226. } );
  227. } );
  228. function assertPosition( options, expected ) {
  229. const position = getOptimalPosition( options );
  230. expect( position ).to.deep.equal( expected );
  231. }
  232. // +--------+-----+
  233. // | E | T |
  234. // +--------+-----+
  235. const attachLeft = ( targetRect, elementRect ) => ( {
  236. top: targetRect.top,
  237. left: targetRect.left - elementRect.width,
  238. name: 'left'
  239. } );
  240. // +-----+--------+
  241. // | T | E |
  242. // +-----+--------+
  243. const attachRight = ( targetRect ) => ( {
  244. top: targetRect.top,
  245. left: targetRect.left + targetRect.width,
  246. name: 'right'
  247. } );
  248. // +-----+
  249. // | T |
  250. // +-----+--+
  251. // | E |
  252. // +--------+
  253. const attachBottom = ( targetRect ) => ( {
  254. top: targetRect.bottom,
  255. left: targetRect.left,
  256. name: 'bottom'
  257. } );
  258. // +--------+
  259. // | E |
  260. // +--+-----+
  261. // | T |
  262. // +-----+
  263. const attachTop = ( targetRect, elementRect ) => ( {
  264. top: targetRect.top - elementRect.height,
  265. left: targetRect.left - ( elementRect.width - targetRect.width ),
  266. name: 'bottom'
  267. } );
  268. function stubWindowScroll( x, y ) {
  269. const { scrollX: savedX, scrollY: savedY } = window;
  270. window.scrollX = x;
  271. window.scrollY = y;
  272. revertWindowScroll = () => {
  273. window.scrollX = savedX;
  274. window.scrollY = savedY;
  275. revertWindowScroll = null;
  276. };
  277. }
  278. function stubElementRect( element, rect ) {
  279. if ( element.getBoundingClientRect.restore ) {
  280. element.getBoundingClientRect.restore();
  281. }
  282. testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( rect );
  283. }
  284. // <-- 100px ->
  285. //
  286. // ^ +--------------[ Viewport ]----------
  287. // | |
  288. // 100px |
  289. // | | <-- 10px -->
  290. // V |
  291. // | ^ +---------+
  292. // | | | |
  293. // | 10px | T |
  294. // | | | |
  295. // | V +---------+
  296. // |
  297. //
  298. function setElementTargetPlayground() {
  299. element = document.createElement( 'div' );
  300. target = document.createElement( 'div' );
  301. stubElementRect( element, {
  302. top: 0,
  303. right: 20,
  304. bottom: 20,
  305. left: 0,
  306. width: 20,
  307. height: 20
  308. } );
  309. stubElementRect( target, {
  310. top: 100,
  311. right: 110,
  312. bottom: 110,
  313. left: 100,
  314. width: 10,
  315. height: 10
  316. } );
  317. }
  318. //
  319. //
  320. // ^ +-----------[ Viewport ]----------------------
  321. // | |
  322. // 100px |
  323. // | <--------- 20px ------->
  324. // | <-- 10px -->
  325. // V |
  326. // +------------+---------+ ^ ^
  327. // | | | | |
  328. // | | T | 10px |
  329. // | | | | |
  330. // | +---------+ V 20px
  331. // | | | |
  332. // | | | |
  333. // | | | |
  334. // +------[ Limiter ]-----+ V
  335. // |
  336. // |
  337. //
  338. //
  339. function setElementTargetLimiterPlayground() {
  340. element = document.createElement( 'div' );
  341. target = document.createElement( 'div' );
  342. limiter = document.createElement( 'div' );
  343. stubElementRect( element, {
  344. top: 0,
  345. right: 20,
  346. bottom: 20,
  347. left: 0,
  348. width: 20,
  349. height: 20
  350. } );
  351. stubElementRect( limiter, {
  352. top: 100,
  353. right: 10,
  354. bottom: 120,
  355. left: -10,
  356. width: 20,
  357. height: 20
  358. } );
  359. stubElementRect( target, {
  360. top: 100,
  361. right: 10,
  362. bottom: 110,
  363. left: 0,
  364. width: 10,
  365. height: 10
  366. } );
  367. }