UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

414 lines (413 loc) • 13.6 kB
/** * DevExtreme (esm/common/core/animation/position.js) * Version: 24.2.6 * Build date: Mon Mar 17 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import { getOuterWidth, getOuterHeight, getWidth, getHeight } from "../../../core/utils/size"; import $ from "../../../core/renderer"; import { splitPair, pairToObject } from "../../../core/utils/common"; import { each } from "../../../core/utils/iterator"; import { getWindow } from "../../../core/utils/window"; const window = getWindow(); import domAdapter from "../../../core/dom_adapter"; import { isWindow, isDefined } from "../../../core/utils/type"; import { extend } from "../../../core/utils/extend"; import { getBoundingRect } from "../../../core/utils/position"; import browser from "../../../core/utils/browser"; import { resetPosition, move } from "./translator"; import { touch } from "../../../core/utils/support"; import devices from "../../../core/devices"; import { setStyle } from "../../../core/utils/style"; const horzRe = /left|right/; const vertRe = /top|bottom/; const collisionRe = /fit|flip|none/; const scaleRe = /scale\(.+?\)/; const IS_SAFARI = browser.safari; const normalizeAlign = function(raw) { const result = { h: "center", v: "center" }; const pair = splitPair(raw); if (pair) { each(pair, (function() { const w = String(this).toLowerCase(); if (horzRe.test(w)) { result.h = w } else if (vertRe.test(w)) { result.v = w } })) } return result }; const normalizeOffset = function(raw, preventRound) { return pairToObject(raw, preventRound) }; const normalizeCollision = function(raw) { const pair = splitPair(raw); let h = String(pair && pair[0]).toLowerCase(); let v = String(pair && pair[1]).toLowerCase(); if (!collisionRe.test(h)) { h = "none" } if (!collisionRe.test(v)) { v = h } return { h: h, v: v } }; const getAlignFactor = function(align) { switch (align) { case "center": return .5; case "right": case "bottom": return 1; default: return 0 } }; const inverseAlign = function(align) { switch (align) { case "left": return "right"; case "right": return "left"; case "top": return "bottom"; case "bottom": return "top"; default: return align } }; const calculateOversize = function(data, bounds) { let oversize = 0; if (data.myLocation < bounds.min) { oversize += bounds.min - data.myLocation } if (data.myLocation > bounds.max) { oversize += data.myLocation - bounds.max } return oversize }; const collisionSide = function(direction, data, bounds) { if (data.myLocation < bounds.min) { return "h" === direction ? "left" : "top" } if (data.myLocation > bounds.max) { return "h" === direction ? "right" : "bottom" } return "none" }; const initMyLocation = function(data) { data.myLocation = data.atLocation + getAlignFactor(data.atAlign) * data.atSize - getAlignFactor(data.myAlign) * data.mySize + data.offset }; const collisionResolvers = { fit: function(data, bounds) { let result = false; if (data.myLocation > bounds.max) { data.myLocation = bounds.max; result = true } if (data.myLocation < bounds.min) { data.myLocation = bounds.min; result = true } data.fit = result }, flip: function(data, bounds) { data.flip = false; if ("center" === data.myAlign && "center" === data.atAlign) { return } if (data.myLocation < bounds.min || data.myLocation > bounds.max) { const inverseData = extend({}, data, { myAlign: inverseAlign(data.myAlign), atAlign: inverseAlign(data.atAlign), offset: -data.offset }); initMyLocation(inverseData); inverseData.oversize = calculateOversize(inverseData, bounds); if (inverseData.myLocation >= bounds.min && inverseData.myLocation <= bounds.max || data.oversize > inverseData.oversize) { data.myLocation = inverseData.myLocation; data.oversize = inverseData.oversize; data.flip = true } } }, flipfit: function(data, bounds) { this.flip(data, bounds); this.fit(data, bounds) }, none: function(data) { data.oversize = 0 } }; let scrollbarWidth; const calculateScrollbarWidth = function() { const $scrollDiv = $("<div>").css({ width: 100, height: 100, overflow: "scroll", position: "absolute", top: -9999 }).appendTo($("body")); const result = $scrollDiv.get(0).offsetWidth - $scrollDiv.get(0).clientWidth; $scrollDiv.remove(); scrollbarWidth = result }; const defaultPositionResult = { h: { location: 0, flip: false, fit: false, oversize: 0 }, v: { location: 0, flip: false, fit: false, oversize: 0 } }; const calculatePosition = function(what, options) { const $what = $(what); const currentOffset = $what.offset(); const result = extend(true, {}, defaultPositionResult, { h: { location: currentOffset.left }, v: { location: currentOffset.top } }); if (!options) { return result } const my = normalizeAlign(options.my); const at = normalizeAlign(options.at); let of = $(options.of).length && options.of || window; const offset = normalizeOffset(options.offset, options.precise); const collision = normalizeCollision(options.collision); const boundary = options.boundary; const boundaryOffset = normalizeOffset(options.boundaryOffset, options.precise); const h = { mySize: getOuterWidth($what), myAlign: my.h, atAlign: at.h, offset: offset.h, collision: collision.h, boundaryOffset: boundaryOffset.h }; const v = { mySize: getOuterHeight($what), myAlign: my.v, atAlign: at.v, offset: offset.v, collision: collision.v, boundaryOffset: boundaryOffset.v }; if (of.preventDefault) { h.atLocation = of.pageX; v.atLocation = of.pageY; h.atSize = 0; v.atSize = 0 } else { of = $(of); if (isWindow(of [0])) { h.atLocation = of.scrollLeft(); v.atLocation = of.scrollTop(); if ("phone" === devices.real().deviceType && of [0].visualViewport) { h.atLocation = Math.max(h.atLocation, of [0].visualViewport.offsetLeft); v.atLocation = Math.max(v.atLocation, of [0].visualViewport.offsetTop); h.atSize = of [0].visualViewport.width; v.atSize = of [0].visualViewport.height } else { h.atSize = of [0].innerWidth > of [0].outerWidth ? of [0].innerWidth : getWidth(of); v.atSize = of [0].innerHeight > of [0].outerHeight || IS_SAFARI ? of [0].innerHeight : getHeight(of) } } else if (9 === of [0].nodeType) { h.atLocation = 0; v.atLocation = 0; h.atSize = getWidth(of); v.atSize = getHeight(of) } else { const ofRect = getBoundingRect(of.get(0)); const o = getOffsetWithoutScale(of); h.atLocation = o.left; v.atLocation = o.top; h.atSize = Math.max(ofRect.width, getOuterWidth(of)); v.atSize = Math.max(ofRect.height, getOuterHeight(of)) } } initMyLocation(h); initMyLocation(v); const bounds = function() { const win = $(window); const windowWidth = getWidth(win); const windowHeight = getHeight(win); let left = win.scrollLeft(); let top = win.scrollTop(); const documentElement = domAdapter.getDocumentElement(); const hZoomLevel = touch ? documentElement.clientWidth / windowWidth : 1; const vZoomLevel = touch ? documentElement.clientHeight / windowHeight : 1; if (void 0 === scrollbarWidth) { calculateScrollbarWidth() } let boundaryWidth = windowWidth; let boundaryHeight = windowHeight; if (boundary && !isWindow(boundary)) { const $boundary = $(boundary); const boundaryPosition = $boundary.offset(); left = boundaryPosition.left; top = boundaryPosition.top; boundaryWidth = getWidth($boundary); boundaryHeight = getHeight($boundary) } return { h: { min: left + h.boundaryOffset, max: left + boundaryWidth / hZoomLevel - h.mySize - h.boundaryOffset }, v: { min: top + v.boundaryOffset, max: top + boundaryHeight / vZoomLevel - v.mySize - v.boundaryOffset } } }(); h.oversize = calculateOversize(h, bounds.h); v.oversize = calculateOversize(v, bounds.v); h.collisionSide = collisionSide("h", h, bounds.h); v.collisionSide = collisionSide("v", v, bounds.v); if (collisionResolvers[h.collision]) { collisionResolvers[h.collision](h, bounds.h) } if (collisionResolvers[v.collision]) { collisionResolvers[v.collision](v, bounds.v) } const preciser = function(number) { return options.precise ? number : Math.round(number) }; extend(true, result, { h: { location: preciser(h.myLocation), oversize: preciser(h.oversize), fit: h.fit, flip: h.flip, collisionSide: h.collisionSide }, v: { location: preciser(v.myLocation), oversize: preciser(v.oversize), fit: v.fit, flip: v.flip, collisionSide: v.collisionSide }, precise: options.precise }); return result }; const setScaleProperty = function(element, scale, styleAttr, isEmpty) { const stylePropIsValid = isDefined(element.style) && !domAdapter.isNode(element.style); const newStyleValue = isEmpty ? styleAttr.replace(scale, "") : styleAttr; if (stylePropIsValid) { setStyle(element, newStyleValue, false) } else { const styleAttributeNode = domAdapter.createAttribute("style"); styleAttributeNode.value = newStyleValue; element.setAttributeNode(styleAttributeNode) } }; const getOffsetWithoutScale = function($startElement) { var _currentElement$getAt, _style$match; let $currentElement = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : $startElement; const currentElement = $currentElement.get(0); if (!currentElement) { return $startElement.offset() } const style = (null === (_currentElement$getAt = currentElement.getAttribute) || void 0 === _currentElement$getAt ? void 0 : _currentElement$getAt.call(currentElement, "style")) || ""; const scale = null === (_style$match = style.match(scaleRe)) || void 0 === _style$match ? void 0 : _style$match[0]; let offset; if (scale) { setScaleProperty(currentElement, scale, style, true); offset = getOffsetWithoutScale($startElement, $currentElement.parent()); setScaleProperty(currentElement, scale, style, false) } else { offset = getOffsetWithoutScale($startElement, $currentElement.parent()) } return offset }; const position = function(what, options) { const $what = $(what); if (!options) { return $what.offset() } resetPosition($what, true); const offset = getOffsetWithoutScale($what); const targetPosition = options.h && options.v ? options : calculatePosition($what, options); const preciser = function(number) { return options.precise ? number : Math.round(number) }; move($what, { left: targetPosition.h.location - preciser(offset.left), top: targetPosition.v.location - preciser(offset.top) }); return targetPosition }; const offset = function(element) { element = $(element).get(0); if (isWindow(element)) { return null } else if (element && "pageY" in element && "pageX" in element) { return { top: element.pageY, left: element.pageX } } return $(element).offset() }; if (!position.inverseAlign) { position.inverseAlign = inverseAlign } if (!position.normalizeAlign) { position.normalizeAlign = normalizeAlign } export default { calculateScrollbarWidth: calculateScrollbarWidth, calculate: calculatePosition, setup: position, offset: offset };