UNPKG

19.9 kBJavaScriptView Raw
1import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
2import _typeof from 'babel-runtime/helpers/typeof';
3
4var _class, _temp, _initialiseProps;
5
6import { dom } from '../../util';
7import findNode from './find-node';
8
9var VIEWPORT = 'viewport';
10
11// IE8 not support pageXOffset
12var getPageX = function getPageX() {
13 return window.pageXOffset || document.documentElement.scrollLeft;
14};
15var getPageY = function getPageY() {
16 return window.pageYOffset || document.documentElement.scrollTop;
17};
18
19/**
20 * @private get element size
21 * @param {Element} element
22 * @return {Object}
23 */
24function _getSize(element) {
25 // element like `svg` do not have offsetWidth and offsetHeight prop
26 // then getBoundingClientRect
27 if ('offsetWidth' in element && 'offsetHeight' in element) {
28 return {
29 width: element.offsetWidth,
30 height: element.offsetHeight
31 };
32 } else {
33 var _element$getBoundingC = element.getBoundingClientRect(),
34 width = _element$getBoundingC.width,
35 height = _element$getBoundingC.height;
36
37 return {
38 width: width,
39 height: height
40 };
41 }
42}
43
44/**
45 * @private get element rect
46 * @param {Element} elem
47 * @return {Object}
48 */
49function _getElementRect(elem, container) {
50 var offsetTop = 0,
51 offsetLeft = 0,
52 scrollTop = 0,
53 scrollLeft = 0;
54
55 var _getSize2 = _getSize(elem),
56 width = _getSize2.width,
57 height = _getSize2.height;
58
59 do {
60 if (!isNaN(elem.offsetTop)) {
61 offsetTop += elem.offsetTop;
62 }
63 if (!isNaN(elem.offsetLeft)) {
64 offsetLeft += elem.offsetLeft;
65 }
66 if (elem && elem.offsetParent) {
67 if (!isNaN(elem.offsetParent.scrollLeft) && elem.offsetParent !== document.body) {
68 scrollLeft += elem.offsetParent.scrollLeft;
69 }
70
71 if (!isNaN(elem.offsetParent.scrollTop) && elem.offsetParent !== document.body) {
72 scrollTop += elem.offsetParent.scrollTop;
73 }
74 }
75
76 elem = elem.offsetParent;
77 } while (elem !== null && elem !== container);
78
79 // if container is body or invalid, treat as window, use client width & height
80 var treatAsWindow = !container || container === document.body;
81
82 return {
83 top: offsetTop - scrollTop - (treatAsWindow ? document.documentElement.scrollTop || document.body.scrollTop : 0),
84 left: offsetLeft - scrollLeft - (treatAsWindow ? document.documentElement.scrollLeft || document.body.scrollLeft : 0),
85 width: width,
86 height: height
87 };
88}
89
90/**
91 * @private get viewport size
92 * @return {Object}
93 */
94function _getViewportSize(container) {
95 if (!container || container === document.body) {
96 return {
97 width: document.documentElement.clientWidth,
98 height: document.documentElement.clientHeight
99 };
100 }
101
102 var _container$getBoundin = container.getBoundingClientRect(),
103 width = _container$getBoundin.width,
104 height = _container$getBoundin.height;
105
106 return {
107 width: width,
108 height: height
109 };
110}
111
112var getContainer = function getContainer(_ref) {
113 var container = _ref.container,
114 baseElement = _ref.baseElement;
115
116 // SSR下会有副作用
117 if ((typeof document === 'undefined' ? 'undefined' : _typeof(document)) === undefined) {
118 return container;
119 }
120
121 var calcContainer = findNode(container, baseElement);
122
123 if (!calcContainer) {
124 calcContainer = document.body;
125 }
126
127 while (dom.getStyle(calcContainer, 'position') === 'static') {
128 if (!calcContainer || calcContainer === document.body) {
129 return document.body;
130 }
131 calcContainer = calcContainer.parentNode;
132 }
133
134 return calcContainer;
135};
136
137var Position = (_temp = _class = function () {
138 function Position(props) {
139 _classCallCheck(this, Position);
140
141 _initialiseProps.call(this);
142
143 this.pinElement = props.pinElement;
144 this.baseElement = props.baseElement;
145 this.pinFollowBaseElementWhenFixed = props.pinFollowBaseElementWhenFixed;
146 this.container = getContainer(props);
147 this.autoFit = props.autoFit || false;
148 this.align = props.align || 'tl tl';
149 this.offset = props.offset || [0, 0];
150 this.needAdjust = props.needAdjust || false;
151 this.isRtl = props.isRtl || false;
152 }
153
154 /**
155 * @public static place method
156 * @param {Object} props
157 * @param {DOM} props.pinElement
158 * @param {DOM} props.baseElement
159 * @param {String} props.align
160 * @param {Number} props.offset
161 * @param {Boolean} props.needAdjust
162 * @param {Boolean} props.isRtl
163 * @return {Position}
164 */
165
166
167 Position.prototype.setPosition = function setPosition() {
168 var pinElement = this.pinElement;
169 var baseElement = this.baseElement;
170 var pinFollowBaseElementWhenFixed = this.pinFollowBaseElementWhenFixed;
171 var expectedAlign = this._getExpectedAlign();
172 var isPinFixed = void 0,
173 isBaseFixed = void 0,
174 firstPositionResult = void 0;
175 if (pinElement === VIEWPORT) {
176 return;
177 }
178 if (dom.getStyle(pinElement, 'position') !== 'fixed') {
179 dom.setStyle(pinElement, 'position', 'absolute');
180 isPinFixed = false;
181 } else {
182 isPinFixed = true;
183 }
184 if (baseElement === VIEWPORT || dom.getStyle(baseElement, 'position') !== 'fixed') {
185 isBaseFixed = false;
186 } else {
187 isBaseFixed = true;
188 }
189
190 // 根据期望的定位
191 for (var i = 0; i < expectedAlign.length; i++) {
192 var align = expectedAlign[i];
193 var pinElementPoints = this._normalizePosition(pinElement, align.split(' ')[0], isPinFixed);
194 var baseElementPoints = this._normalizePosition(baseElement, align.split(' ')[1],
195 // 忽略元素位置,发生在类似dialog的场景下
196 isPinFixed && !pinFollowBaseElementWhenFixed);
197
198 var pinElementParentOffset = this._getParentOffset(pinElement);
199 var pinElementParentScrollOffset = this._getParentScrollOffset(pinElement);
200
201 var baseElementOffset = isPinFixed && isBaseFixed ? this._getLeftTop(baseElement) : // 在 pin 是 fixed 布局,并且又需要根据 base 计算位置时,计算 base 的 offset 需要忽略页面滚动
202 baseElementPoints.offset(isPinFixed && pinFollowBaseElementWhenFixed);
203 var top = baseElementOffset.top + baseElementPoints.y - pinElementParentOffset.top - pinElementPoints.y + pinElementParentScrollOffset.top;
204 var left = baseElementOffset.left + baseElementPoints.x - pinElementParentOffset.left - pinElementPoints.x + pinElementParentScrollOffset.left;
205
206 this._setPinElementPostion(pinElement, { left: left, top: top }, this.offset);
207
208 if (this._isInViewport(pinElement, align)) {
209 return align;
210 } else if (!firstPositionResult) {
211 if (this.needAdjust && !this.autoFit) {
212 var _getViewportOffset2 = this._getViewportOffset(pinElement, align),
213 right = _getViewportOffset2.right;
214
215 firstPositionResult = {
216 left: right < 0 ? left + right : left,
217 top: top
218 };
219 } else {
220 firstPositionResult = { left: left, top: top };
221 }
222 }
223 }
224
225 // This will only execute if `pinElement` could not be placed entirely in the Viewport
226 var inViewportLeft = this._makeElementInViewport(pinElement, firstPositionResult.left, 'Left', isPinFixed);
227 var inViewportTop = this._makeElementInViewport(pinElement, firstPositionResult.top, 'Top', isPinFixed);
228
229 this._setPinElementPostion(pinElement, { left: inViewportLeft, top: inViewportTop }, this._calPinOffset(expectedAlign[0]));
230
231 return expectedAlign[0];
232 };
233
234 Position.prototype._getParentOffset = function _getParentOffset(element) {
235 var parent = element.offsetParent || document.documentElement;
236 var offset = void 0;
237 if (parent === document.body && dom.getStyle(parent, 'position') === 'static') {
238 offset = {
239 top: 0,
240 left: 0
241 };
242 } else {
243 offset = this._getElementOffset(parent);
244 }
245
246 offset.top += parseFloat(dom.getStyle(parent, 'border-top-width'), 10);
247 offset.left += parseFloat(dom.getStyle(parent, 'border-left-width'), 10);
248 offset.offsetParent = parent;
249 return offset;
250 };
251
252 Position.prototype._makeElementInViewport = function _makeElementInViewport(pinElement, number, type, isPinFixed) {
253 // pinElement.offsetParent is never body because wrapper has position: absolute
254 // refactored to make code clearer. Revert if wrapper style changes.
255 var result = number;
256 var docElement = document.documentElement;
257 var offsetParent = pinElement.offsetParent || document.documentElement;
258
259 if (result < 0) {
260 if (isPinFixed) {
261 result = 0;
262 } else if (offsetParent === document.body && dom.getStyle(offsetParent, 'position') === 'static') {
263 // Only when div's offsetParent is document.body, we set new position result.
264 result = Math.max(docElement['scroll' + type], document.body['scroll' + type]);
265 }
266 }
267 return result;
268 };
269
270 // 这里的第三个参数真实含义为:是否为fixed布局,并且像dialog一样,不跟随trigger元素
271
272
273 Position.prototype._normalizePosition = function _normalizePosition(element, align, ignoreElementOffset) {
274 var points = this._normalizeElement(element, ignoreElementOffset);
275 this._normalizeXY(points, align);
276
277 return points;
278 };
279
280 Position.prototype._normalizeXY = function _normalizeXY(points, align) {
281 var x = align.split('')[1];
282 var y = align.split('')[0];
283
284 points.x = this._xyConverter(x, points, 'width');
285 points.y = this._xyConverter(y, points, 'height');
286
287 return points;
288 };
289
290 Position.prototype._xyConverter = function _xyConverter(align, points, type) {
291 var res = align.replace(/t|l/gi, '0%').replace(/c/gi, '50%').replace(/b|r/gi, '100%').replace(/(\d+)%/gi, function (m, d) {
292 return points.size()[type] * (d / 100);
293 });
294
295 return parseFloat(res, 10) || 0;
296 };
297
298 Position.prototype._getLeftTop = function _getLeftTop(element) {
299 return {
300 left: parseFloat(dom.getStyle(element, 'left')) || 0,
301 top: parseFloat(dom.getStyle(element, 'top')) || 0
302 };
303 };
304
305 Position.prototype._normalizeElement = function _normalizeElement(element, ignoreElementOffset) {
306 var _this = this;
307
308 var result = {
309 element: element,
310 x: 0,
311 y: 0
312 },
313 isViewport = element === VIEWPORT,
314 docElement = document.documentElement;
315
316 result.offset = function (ignoreScroll) {
317 // 这里是关键,第二个参数的含义以ing该是:是否为 fixed 布局,并且像 dialog 一样,不跟随 trigger 元素
318 if (ignoreElementOffset) {
319 return {
320 left: 0,
321 top: 0
322 };
323 } else if (isViewport) {
324 return {
325 left: getPageX(),
326 top: getPageY()
327 };
328 } else {
329 return _this._getElementOffset(element, ignoreScroll);
330 }
331 };
332
333 result.size = function () {
334 if (isViewport) {
335 return {
336 width: docElement.clientWidth,
337 height: docElement.clientHeight
338 };
339 } else {
340 return _getSize(element);
341 }
342 };
343
344 return result;
345 };
346
347 // ignoreScroll 在 pin 元素为 fixed 的时候生效,此时需要忽略页面滚动
348 // 对 fixed 模式下 subNav 弹层的计算很重要,只有在这种情况下,才同时需要元素的相对位置,又不关心页面滚动
349
350
351 Position.prototype._getElementOffset = function _getElementOffset(element, ignoreScroll) {
352 var rect = element.getBoundingClientRect();
353 var docElement = document.documentElement;
354 var body = document.body;
355 var docClientLeft = docElement.clientLeft || body.clientLeft || 0;
356 var docClientTop = docElement.clientTop || body.clientTop || 0;
357
358 return {
359 left: rect.left + (ignoreScroll ? 0 : getPageX()) - docClientLeft,
360 top: rect.top + (ignoreScroll ? 0 : getPageY()) - docClientTop
361 };
362 };
363
364 // According to the location of the overflow to calculate the desired positioning
365
366
367 Position.prototype._getExpectedAlign = function _getExpectedAlign() {
368 var align = this.isRtl ? this._replaceAlignDir(this.align, /l|r/g, { l: 'r', r: 'l' }) : this.align;
369 var expectedAlign = [align];
370 if (this.needAdjust) {
371 if (/t|b/g.test(align)) {
372 expectedAlign.push(this._replaceAlignDir(align, /t|b/g, { t: 'b', b: 't' }));
373 }
374 if (/l|r/g.test(align)) {
375 expectedAlign.push(this._replaceAlignDir(align, /l|r/g, { l: 'r', r: 'l' }));
376 }
377 if (/c/g.test(align)) {
378 expectedAlign.push(this._replaceAlignDir(align, /c(?= |$)/g, { c: 'l' }));
379 expectedAlign.push(this._replaceAlignDir(align, /c(?= |$)/g, { c: 'r' }));
380 }
381 expectedAlign.push(this._replaceAlignDir(align, /l|r|t|b/g, {
382 l: 'r',
383 r: 'l',
384 t: 'b',
385 b: 't'
386 }));
387 }
388 return expectedAlign;
389 };
390
391 // Transform align order.
392
393
394 Position.prototype._replaceAlignDir = function _replaceAlignDir(align, regExp, map) {
395 return align.replace(regExp, function (res) {
396 return map[res];
397 });
398 };
399
400 // Are the right sides of the pin and base aligned?
401
402
403 Position.prototype._isRightAligned = function _isRightAligned(align) {
404 var _align$split = align.split(' '),
405 pinAlign = _align$split[0],
406 baseAlign = _align$split[1];
407
408 return pinAlign[1] === 'r' && pinAlign[1] === baseAlign[1];
409 };
410
411 // Are the bottoms of the pin and base aligned?
412
413
414 Position.prototype._isBottomAligned = function _isBottomAligned(align) {
415 var _align$split2 = align.split(' '),
416 pinAlign = _align$split2[0],
417 baseAlign = _align$split2[1];
418
419 return pinAlign[0] === 'b' && pinAlign[0] === baseAlign[0];
420 };
421
422 // Detecting element is in the window, we want to adjust position later.
423
424
425 Position.prototype._isInViewport = function _isInViewport(element, align) {
426 var viewportSize = _getViewportSize(this.container);
427 var elementRect = _getElementRect(element, this.container);
428 var elementSize = _getSize(element);
429
430 // https://github.com/alibaba-fusion/next/issues/853
431 // Equality causes issues in Chrome when pin element is off screen to right or bottom.
432 // If it is not supposed to align with the bottom or right, then subtract 1 to use strict less than.
433 var viewportWidth = this._isRightAligned(align) ? viewportSize.width : viewportSize.width - 1;
434 var viewportHeight = this._isBottomAligned(align) ? viewportSize.height : viewportSize.height - 1;
435
436 // 临时方案,在 select + table 的场景下,不需要关注横向上是否在可视区域内
437 // 在 balloon 场景下需要关注
438 if (this.autoFit) {
439 return elementRect.top >= 0 && elementRect.top + element.offsetHeight <= viewportHeight;
440 }
441
442 // Avoid animate problem that use offsetWidth instead of getBoundingClientRect.
443 return elementRect.left >= 0 && elementRect.left + elementSize.width <= viewportWidth && elementRect.top >= 0 && elementRect.top + elementSize.height <= viewportHeight;
444 };
445
446 Position.prototype._getViewportOffset = function _getViewportOffset(element, align) {
447 var viewportSize = _getViewportSize(this.container);
448 var elementRect = _getElementRect(element, this.container);
449 var elementSize = _getSize(element);
450
451 var viewportWidth = this._isRightAligned(align) ? viewportSize.width : viewportSize.width - 1;
452 var viewportHeight = this._isBottomAligned(align) ? viewportSize.height : viewportSize.height - 1;
453
454 return {
455 top: elementRect.top,
456 right: viewportWidth - (elementRect.left + elementSize.width),
457 bottom: viewportHeight - (elementRect.top + elementSize.height),
458 left: elementRect.left
459 };
460 };
461
462 // 在这里做RTL判断 top-left 定位转化为等效的 top-right定位
463
464
465 Position.prototype._setPinElementPostion = function _setPinElementPostion(pinElement, postion) {
466 var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0];
467 var top = postion.top,
468 left = postion.left;
469
470 if (!this.isRtl) {
471 dom.setStyle(pinElement, {
472 left: left + offset[0] + 'px',
473 top: top + offset[1] + 'px'
474 });
475 return;
476 }
477
478 // transfer {left,top} equaly to {right,top}
479 var pinElementParentOffset = this._getParentOffset(pinElement);
480
481 var _getElementRect2 = _getElementRect(pinElementParentOffset.offsetParent),
482 offsetParentWidth = _getElementRect2.width;
483
484 var _getElementRect3 = _getElementRect(pinElement),
485 width = _getElementRect3.width;
486
487 var right = offsetParentWidth - (left + width);
488 dom.setStyle(pinElement, {
489 left: 'auto',
490 right: right + offset[0] + 'px',
491 top: top + offset[1] + 'px'
492 });
493 };
494
495 return Position;
496}(), _class.VIEWPORT = VIEWPORT, _class.place = function (props) {
497 return new Position(props).setPosition();
498}, _initialiseProps = function _initialiseProps() {
499 var _this2 = this;
500
501 this._calPinOffset = function (align) {
502 var offset = [].concat(_this2.offset);
503
504 if (_this2.autoFit && align && _this2.container && _this2.container !== document.body) {
505 var baseElementRect = _getElementRect(_this2.baseElement, _this2.container);
506 var pinElementRect = _getElementRect(_this2.pinElement, _this2.container);
507 var viewportSize = _getViewportSize(_this2.container);
508 var pinAlign = align.split(' ')[0];
509 var x = pinAlign.charAt(1);
510 var y = pinAlign.charAt(0);
511
512 if (pinElementRect.top < 0 || pinElementRect.top + pinElementRect.height > viewportSize.height) {
513 offset[1] = -baseElementRect.top - (y === 't' ? baseElementRect.height : 0);
514 }
515 }
516 return offset;
517 };
518
519 this._getParentScrollOffset = function (elem) {
520 var top = 0;
521 var left = 0;
522
523 if (elem && elem.offsetParent && elem.offsetParent !== document.body) {
524 if (!isNaN(elem.offsetParent.scrollTop)) {
525 top += elem.offsetParent.scrollTop;
526 }
527 if (!isNaN(elem.offsetParent.scrollLeft)) {
528 left += elem.offsetParent.scrollLeft;
529 }
530 }
531
532 return {
533 top: top,
534 left: left
535 };
536 };
537}, _temp);
538export { Position as default };
\No newline at end of file