position.js 8.6 KB

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