position.js 8.6 KB

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