@veltra/utils 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dom/position.d.ts +3 -2
- package/dist/dom/position.js +5 -4
- package/dist/dom/position.js.map +1 -1
- package/dist/helper/overflow-nav.d.ts +41 -0
- package/dist/helper/overflow-nav.js +45 -0
- package/dist/helper/overflow-nav.js.map +1 -0
- package/dist/helper/tween.js +2 -2
- package/dist/helper/tween.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/package.json +2 -2
- package/src/dom/position.ts +5 -4
- package/src/helper/overflow-nav.ts +71 -0
- package/src/index.ts +2 -0
package/dist/dom/position.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
//#region src/dom/position.d.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 获取可滚动的父级(纵向或横向可滚动均计入:overflow-x 容器如横向滚动条
|
|
4
|
+
* 也是滚动父级,弹层需监听其 scroll 事件)
|
|
4
5
|
* @param el 元素
|
|
5
6
|
* @returns
|
|
6
7
|
*/
|
|
7
8
|
declare function getScrollParents(el: HTMLElement): HTMLElement[];
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* 获取最近的可滚动父级(纵向或横向可滚动均计入)
|
|
10
11
|
* @param el 元素
|
|
11
12
|
* @returns 最近的可滚动父级
|
|
12
13
|
*/
|
package/dist/dom/position.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/dom/position.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 获取可滚动的父级(纵向或横向可滚动均计入:overflow-x 容器如横向滚动条
|
|
4
|
+
* 也是滚动父级,弹层需监听其 scroll 事件)
|
|
4
5
|
* @param el 元素
|
|
5
6
|
* @returns
|
|
6
7
|
*/
|
|
@@ -8,20 +9,20 @@ function getScrollParents(el) {
|
|
|
8
9
|
const parents = [];
|
|
9
10
|
let parent = el.parentElement;
|
|
10
11
|
while (parent) {
|
|
11
|
-
if (parent.scrollHeight > parent.clientHeight) parents.push(parent);
|
|
12
|
+
if (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) parents.push(parent);
|
|
12
13
|
parent = parent.parentElement;
|
|
13
14
|
}
|
|
14
15
|
return parents;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
+
* 获取最近的可滚动父级(纵向或横向可滚动均计入)
|
|
18
19
|
* @param el 元素
|
|
19
20
|
* @returns 最近的可滚动父级
|
|
20
21
|
*/
|
|
21
22
|
function getNearestScrollParent(el) {
|
|
22
23
|
let parent = el.parentElement;
|
|
23
24
|
while (parent) {
|
|
24
|
-
if (parent.scrollHeight > parent.clientHeight) return parent;
|
|
25
|
+
if (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) return parent;
|
|
25
26
|
parent = parent.parentElement;
|
|
26
27
|
}
|
|
27
28
|
return null;
|
package/dist/dom/position.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"position.js","names":[],"sources":["../../src/dom/position.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"position.js","names":[],"sources":["../../src/dom/position.ts"],"sourcesContent":["/**\n * 获取可滚动的父级(纵向或横向可滚动均计入:overflow-x 容器如横向滚动条\n * 也是滚动父级,弹层需监听其 scroll 事件)\n * @param el 元素\n * @returns\n */\nexport function getScrollParents(el: HTMLElement): HTMLElement[] {\n const parents: HTMLElement[] = []\n let parent = el.parentElement\n while (parent) {\n if (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) {\n parents.push(parent)\n }\n\n parent = parent.parentElement\n }\n return parents\n}\n\n/**\n * 获取最近的可滚动父级(纵向或横向可滚动均计入)\n * @param el 元素\n * @returns 最近的可滚动父级\n */\nexport function getNearestScrollParent(el: HTMLElement): HTMLElement | null {\n let parent = el.parentElement\n while (parent) {\n if (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) {\n return parent\n }\n parent = parent.parentElement\n }\n return null\n}\n\ntype ScrollViewPosition = 'center' | 'start' | 'end'\n\n/**\n * 滚动元素到容器视图中\n * @description 用于替代 `el.scrollIntoView` 方法,因为 `el.scrollIntoView` 在某些情况下会导致外部元素滚动\n * @param el 元素\n * @param container 可滚动容器\n * @param options 滚动选项\n */\nexport function scrollIntoContainerView(\n el: HTMLElement,\n container: HTMLElement | null,\n options?: { block?: ScrollViewPosition; inline?: ScrollViewPosition }\n): void {\n container = container || getNearestScrollParent(el)\n if (!container) return\n\n const { block = 'center', inline = 'center' } = options || {}\n\n const {\n offsetTop: eOffsetTop,\n offsetLeft: eOffsetLeft,\n offsetHeight: eOffsetHeight,\n offsetWidth: eOffsetWidth\n } = el\n\n const {\n clientHeight: cClientHeight,\n clientWidth: cClientWidth,\n scrollTop: cScrollTop,\n scrollLeft: cScrollLeft\n } = container\n\n const isVerticalInView =\n cScrollTop + cClientHeight > eOffsetTop + eOffsetHeight && cScrollTop < eOffsetTop\n\n const isHorizontalInView =\n cScrollLeft + cClientWidth > eOffsetLeft + eOffsetWidth && cScrollLeft < eOffsetLeft\n\n // 垂直方向和水平方向都已经在视图中,则不进行滚动\n if (isVerticalInView && isHorizontalInView) return\n\n if (!isVerticalInView) {\n if (block === 'center') {\n container.scrollTop = eOffsetTop - cClientHeight / 2 + eOffsetHeight / 2\n } else if (block === 'start') {\n container.scrollTop = eOffsetTop\n } else if (block === 'end') {\n container.scrollTop = eOffsetTop - cClientHeight + eOffsetHeight\n }\n }\n\n if (!isHorizontalInView) {\n if (inline === 'center') {\n container.scrollLeft = eOffsetLeft - cClientWidth / 2 + eOffsetWidth / 2\n } else if (inline === 'start') {\n container.scrollLeft = eOffsetLeft\n } else if (inline === 'end') {\n container.scrollLeft = eOffsetLeft - cClientWidth + eOffsetWidth\n }\n }\n}\n"],"mappings":";;;;;;;AAMA,SAAgB,iBAAiB,IAAgC;CAC/D,MAAM,UAAyB,CAAC;CAChC,IAAI,SAAS,GAAG;CAChB,OAAO,QAAQ;EACb,IAAI,OAAO,eAAe,OAAO,gBAAgB,OAAO,cAAc,OAAO,aAC3E,QAAQ,KAAK,MAAM;EAGrB,SAAS,OAAO;CAClB;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,uBAAuB,IAAqC;CAC1E,IAAI,SAAS,GAAG;CAChB,OAAO,QAAQ;EACb,IAAI,OAAO,eAAe,OAAO,gBAAgB,OAAO,cAAc,OAAO,aAC3E,OAAO;EAET,SAAS,OAAO;CAClB;CACA,OAAO;AACT;;;;;;;;AAWA,SAAgB,wBACd,IACA,WACA,SACM;CACN,YAAY,aAAa,uBAAuB,EAAE;CAClD,IAAI,CAAC,WAAW;CAEhB,MAAM,EAAE,QAAQ,UAAU,SAAS,aAAa,WAAW,CAAC;CAE5D,MAAM,EACJ,WAAW,YACX,YAAY,aACZ,cAAc,eACd,aAAa,iBACX;CAEJ,MAAM,EACJ,cAAc,eACd,aAAa,cACb,WAAW,YACX,YAAY,gBACV;CAEJ,MAAM,mBACJ,aAAa,gBAAgB,aAAa,iBAAiB,aAAa;CAE1E,MAAM,qBACJ,cAAc,eAAe,cAAc,gBAAgB,cAAc;CAG3E,IAAI,oBAAoB,oBAAoB;CAE5C,IAAI,CAAC,kBACC;MAAA,UAAU,UACZ,UAAU,YAAY,aAAa,gBAAgB,IAAI,gBAAgB;OAClE,IAAI,UAAU,SACnB,UAAU,YAAY;OACjB,IAAI,UAAU,OACnB,UAAU,YAAY,aAAa,gBAAgB;CAAA;CAIvD,IAAI,CAAC,oBACC;MAAA,WAAW,UACb,UAAU,aAAa,cAAc,eAAe,IAAI,eAAe;OAClE,IAAI,WAAW,SACpB,UAAU,aAAa;OAClB,IAAI,WAAW,OACpB,UAAU,aAAa,cAAc,eAAe;CAAA;AAG1D"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/helper/overflow-nav.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 水平溢出滚动导航的共享纯逻辑(tabs / toolbar 三处复制的收敛,见 #17)。
|
|
4
|
+
* 只提供无 Vue 依赖的计算与副作用,事件绑定 / ResizeObserver / watch 由各
|
|
5
|
+
* composable 按需组合(三处的监听源与生命周期有真实差异,不强行统一)。
|
|
6
|
+
*/
|
|
7
|
+
/** 视口滚动几何(scrollTo 兼容 HTMLElement 与 vue 模板 ref) */
|
|
8
|
+
interface OverflowNavViewport {
|
|
9
|
+
scrollLeft: number;
|
|
10
|
+
scrollWidth: number;
|
|
11
|
+
clientWidth: number;
|
|
12
|
+
}
|
|
13
|
+
/** 导航按钮状态 */
|
|
14
|
+
interface OverflowNavState {
|
|
15
|
+
/** 内容是否溢出视口 */
|
|
16
|
+
overflowing: boolean;
|
|
17
|
+
/** 可向前(左/上)滚动 */
|
|
18
|
+
canPrev: boolean;
|
|
19
|
+
/** 可向后(右/下)滚动 */
|
|
20
|
+
canNext: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** 依据视口与内容尺寸推导滚动按钮可用状态 */
|
|
23
|
+
declare function computeOverflowNavState(vp: OverflowNavViewport): OverflowNavState;
|
|
24
|
+
type ScrollableElement = OverflowNavViewport & {
|
|
25
|
+
scrollTo(options: {
|
|
26
|
+
left: number;
|
|
27
|
+
behavior: 'smooth';
|
|
28
|
+
}): void;
|
|
29
|
+
};
|
|
30
|
+
/** 箭头按钮:按视口宽度 80% 的步长平滑滚动 */
|
|
31
|
+
declare function scrollViewportByStep(vp: ScrollableElement, dir: 1 | -1): void;
|
|
32
|
+
/** 目标元素滚入视口(左/右越界时对齐到边缘,留 offset 边距) */
|
|
33
|
+
declare function scrollElementIntoView(vp: HTMLElement, el: HTMLElement, offset?: number): void;
|
|
34
|
+
/**
|
|
35
|
+
* 鼠标滚轮(纵向)驱动水平滚动;触控板横滑(已有 deltaX)不拦截。
|
|
36
|
+
* navActive = 导航按钮是否可见(内容未溢出时不消费滚轮)。
|
|
37
|
+
*/
|
|
38
|
+
declare function applyWheelHorizontalScroll(e: WheelEvent, vp: HTMLElement, navActive: boolean): void;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { OverflowNavState, OverflowNavViewport, applyWheelHorizontalScroll, computeOverflowNavState, scrollElementIntoView, scrollViewportByStep };
|
|
41
|
+
//# sourceMappingURL=overflow-nav.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/helper/overflow-nav.ts
|
|
2
|
+
/** 依据视口与内容尺寸推导滚动按钮可用状态 */
|
|
3
|
+
function computeOverflowNavState(vp) {
|
|
4
|
+
const overflowing = vp.scrollWidth - vp.clientWidth > 1;
|
|
5
|
+
return {
|
|
6
|
+
overflowing,
|
|
7
|
+
canPrev: overflowing && vp.scrollLeft > 0,
|
|
8
|
+
canNext: overflowing && vp.scrollLeft + vp.clientWidth < vp.scrollWidth - 1
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** 箭头按钮:按视口宽度 80% 的步长平滑滚动 */
|
|
12
|
+
function scrollViewportByStep(vp, dir) {
|
|
13
|
+
vp.scrollTo({
|
|
14
|
+
left: vp.scrollLeft + dir * vp.clientWidth * .8,
|
|
15
|
+
behavior: "smooth"
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** 目标元素滚入视口(左/右越界时对齐到边缘,留 offset 边距) */
|
|
19
|
+
function scrollElementIntoView(vp, el, offset = 8) {
|
|
20
|
+
const vpRect = vp.getBoundingClientRect();
|
|
21
|
+
const elRect = el.getBoundingClientRect();
|
|
22
|
+
if (elRect.left < vpRect.left) vp.scrollTo({
|
|
23
|
+
left: vp.scrollLeft + (elRect.left - vpRect.left) - offset,
|
|
24
|
+
behavior: "smooth"
|
|
25
|
+
});
|
|
26
|
+
else if (elRect.right > vpRect.right) vp.scrollTo({
|
|
27
|
+
left: vp.scrollLeft + (elRect.right - vpRect.right) + offset,
|
|
28
|
+
behavior: "smooth"
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 鼠标滚轮(纵向)驱动水平滚动;触控板横滑(已有 deltaX)不拦截。
|
|
33
|
+
* navActive = 导航按钮是否可见(内容未溢出时不消费滚轮)。
|
|
34
|
+
*/
|
|
35
|
+
function applyWheelHorizontalScroll(e, vp, navActive) {
|
|
36
|
+
if (!navActive) return;
|
|
37
|
+
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
|
|
38
|
+
if (e.deltaY === 0) return;
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
vp.scrollLeft += e.deltaY;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { applyWheelHorizontalScroll, computeOverflowNavState, scrollElementIntoView, scrollViewportByStep };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=overflow-nav.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overflow-nav.js","names":[],"sources":["../../src/helper/overflow-nav.ts"],"sourcesContent":["/**\n * 水平溢出滚动导航的共享纯逻辑(tabs / toolbar 三处复制的收敛,见 #17)。\n * 只提供无 Vue 依赖的计算与副作用,事件绑定 / ResizeObserver / watch 由各\n * composable 按需组合(三处的监听源与生命周期有真实差异,不强行统一)。\n */\n\n/** 视口滚动几何(scrollTo 兼容 HTMLElement 与 vue 模板 ref) */\nexport interface OverflowNavViewport {\n scrollLeft: number\n scrollWidth: number\n clientWidth: number\n}\n\n/** 导航按钮状态 */\nexport interface OverflowNavState {\n /** 内容是否溢出视口 */\n overflowing: boolean\n /** 可向前(左/上)滚动 */\n canPrev: boolean\n /** 可向后(右/下)滚动 */\n canNext: boolean\n}\n\n/** 依据视口与内容尺寸推导滚动按钮可用状态 */\nexport function computeOverflowNavState(vp: OverflowNavViewport): OverflowNavState {\n const overflowing = vp.scrollWidth - vp.clientWidth > 1\n return {\n overflowing,\n canPrev: overflowing && vp.scrollLeft > 0,\n canNext: overflowing && vp.scrollLeft + vp.clientWidth < vp.scrollWidth - 1\n }\n}\n\ntype ScrollableElement = OverflowNavViewport & {\n scrollTo(options: { left: number; behavior: 'smooth' }): void\n}\n\n/** 箭头按钮:按视口宽度 80% 的步长平滑滚动 */\nexport function scrollViewportByStep(vp: ScrollableElement, dir: 1 | -1): void {\n vp.scrollTo({ left: vp.scrollLeft + dir * vp.clientWidth * 0.8, behavior: 'smooth' })\n}\n\n/** 目标元素滚入视口(左/右越界时对齐到边缘,留 offset 边距) */\nexport function scrollElementIntoView(vp: HTMLElement, el: HTMLElement, offset = 8): void {\n const vpRect = vp.getBoundingClientRect()\n const elRect = el.getBoundingClientRect()\n if (elRect.left < vpRect.left) {\n vp.scrollTo({ left: vp.scrollLeft + (elRect.left - vpRect.left) - offset, behavior: 'smooth' })\n } else if (elRect.right > vpRect.right) {\n vp.scrollTo({\n left: vp.scrollLeft + (elRect.right - vpRect.right) + offset,\n behavior: 'smooth'\n })\n }\n}\n\n/**\n * 鼠标滚轮(纵向)驱动水平滚动;触控板横滑(已有 deltaX)不拦截。\n * navActive = 导航按钮是否可见(内容未溢出时不消费滚轮)。\n */\nexport function applyWheelHorizontalScroll(\n e: WheelEvent,\n vp: HTMLElement,\n navActive: boolean\n): void {\n if (!navActive) return\n if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return\n if (e.deltaY === 0) return\n e.preventDefault()\n vp.scrollLeft += e.deltaY\n}\n"],"mappings":";;AAwBA,SAAgB,wBAAwB,IAA2C;CACjF,MAAM,cAAc,GAAG,cAAc,GAAG,cAAc;CACtD,OAAO;EACL;EACA,SAAS,eAAe,GAAG,aAAa;EACxC,SAAS,eAAe,GAAG,aAAa,GAAG,cAAc,GAAG,cAAc;CAC5E;AACF;;AAOA,SAAgB,qBAAqB,IAAuB,KAAmB;CAC7E,GAAG,SAAS;EAAE,MAAM,GAAG,aAAa,MAAM,GAAG,cAAc;EAAK,UAAU;CAAS,CAAC;AACtF;;AAGA,SAAgB,sBAAsB,IAAiB,IAAiB,SAAS,GAAS;CACxF,MAAM,SAAS,GAAG,sBAAsB;CACxC,MAAM,SAAS,GAAG,sBAAsB;CACxC,IAAI,OAAO,OAAO,OAAO,MACvB,GAAG,SAAS;EAAE,MAAM,GAAG,cAAc,OAAO,OAAO,OAAO,QAAQ;EAAQ,UAAU;CAAS,CAAC;MACzF,IAAI,OAAO,QAAQ,OAAO,OAC/B,GAAG,SAAS;EACV,MAAM,GAAG,cAAc,OAAO,QAAQ,OAAO,SAAS;EACtD,UAAU;CACZ,CAAC;AAEL;;;;;AAMA,SAAgB,2BACd,GACA,IACA,WACM;CACN,IAAI,CAAC,WAAW;CAChB,IAAI,KAAK,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,EAAE,MAAM,GAAG;CAC7C,IAAI,EAAE,WAAW,GAAG;CACpB,EAAE,eAAe;CACjB,GAAG,cAAc,EAAE;AACrB"}
|
package/dist/helper/tween.js
CHANGED
|
@@ -78,8 +78,8 @@ var Tween = class Tween {
|
|
|
78
78
|
easeInBack: (p) => p * p * (3.70158 * p - 1),
|
|
79
79
|
easeOutBack: (p) => 1 + 2.70158 * Math.pow(p - 1, 3) + 1.70158 * Math.pow(p - 1, 2),
|
|
80
80
|
easeInOutBack: (p) => {
|
|
81
|
-
const c2 =
|
|
82
|
-
return p < .5 ? Math.pow(2 * p, 2) * (
|
|
81
|
+
const c2 = 2.5949095;
|
|
82
|
+
return p < .5 ? Math.pow(2 * p, 2) * (7.189819 * p - c2) / 2 : (Math.pow(2 * p - 2, 2) * (3.5949095 * (p * 2 - 2) + c2) + 2) / 2;
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
};
|
package/dist/helper/tween.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tween.js","names":["pk"],"sources":["../../src/helper/tween.ts"],"sourcesContent":["/** 自 cat-kit 3.x Tween 行为对齐的轻量动画(原 `cat-kit/fe` 导出) */\n\nexport interface AnimeConfig<State extends Record<string, number>> {\n duration?: number\n easingFunction?: (progress: number) => number\n onComplete?(state: State): void\n}\n\nexport interface TweenConfig<State extends Record<string, number>> {\n duration?: number\n onUpdate?(state: State): void\n onComplete?(state: State): void\n easingFunction?: (progress: number) => number\n}\n\nexport class Tween<State extends Record<string, number> = Record<string, number>> {\n readonly state: State\n protected duration = 300\n protected onUpdate?: (state: State) => void\n protected onComplete?: (state: State) => void\n protected frameId?: number\n protected easingFunction: (progress: number) => number\n private defaultState: State\n\n constructor(state: State, config?: TweenConfig<State>) {\n this.state = state\n this.defaultState = { ...state }\n const { duration, onUpdate, onComplete, easingFunction } = config || {}\n if (duration !== undefined) this.duration = duration\n if (onUpdate !== undefined) this.onUpdate = onUpdate\n if (onComplete !== undefined) this.onComplete = onComplete\n this.easingFunction = easingFunction ?? Tween.easing.linear\n }\n\n protected raf(options: {\n onComplete: () => void\n duration: number\n tick: (p: number) => void\n }): void {\n const start = performance.now()\n const { onComplete, tick, duration } = options\n const update = (timestamp: number) => {\n const elapsed = timestamp - start\n const progress = Math.min(elapsed / duration, 1)\n tick(progress)\n if (progress < 1) {\n this.frameId = requestAnimationFrame(update)\n } else {\n tick(progress)\n this.stop()\n onComplete()\n }\n }\n this.frameId = requestAnimationFrame(update)\n }\n\n to(state: Partial<State>, config?: AnimeConfig<State>): void {\n this.stop()\n const prevState = { ...this.state }\n const stateDistance = Object.keys(state).reduce(\n (acc, key) => {\n const k = key as keyof State\n const sv = state[k] as number\n const pv = prevState[k] as number\n if ((sv || sv === 0) && (pv || pv === 0)) {\n acc[key] = sv - pv\n }\n return acc\n },\n {} as Record<string, number>\n )\n const duration = config?.duration || this.duration\n const easingFunction = config?.easingFunction || this.easingFunction\n const onComplete = config?.onComplete || this.onComplete\n this.raf({\n duration,\n onComplete: () => {\n for (const key in state) {\n if (key in this.state) {\n ;(this.state as Record<string, number>)[key] = state[key as keyof State] as number\n }\n }\n this.onUpdate?.(this.state)\n onComplete?.(this.state)\n },\n tick: (progress) => {\n for (const key in stateDistance) {\n const pk = key as keyof State\n const target = (prevState[pk] as number) + easingFunction(progress) * stateDistance[key]!\n ;(this.state as Record<string, number>)[key] = target\n }\n this.onUpdate?.(this.state)\n }\n })\n }\n\n back(config?: AnimeConfig<State>): void {\n this.to(this.defaultState as Partial<State>, config)\n }\n\n private stop(): boolean {\n if (!this.frameId) return false\n cancelAnimationFrame(this.frameId)\n this.frameId = undefined\n return true\n }\n\n static readonly easing = {\n linear: (p: number) => p,\n easeInQuad: (p: number) => p * p,\n easeOutQuad: (p: number) => p * (2 - p),\n easeInOutQuad: (p: number) => (p < 0.5 ? 2 * p * p : -1 + (4 - 2 * p) * p),\n easeInBack: (p: number) => p * p * ((2.70158 + 1) * p - 1),\n easeOutBack: (p: number) => 1 + 2.70158 * Math.pow(p - 1, 3) + 1.70158 * Math.pow(p - 1, 2),\n easeInOutBack: (p: number) => {\n const c1 = 1.70158\n const c2 = c1 * 1.525\n return p < 0.5\n ? (Math.pow(2 * p, 2) * ((c2 + 1) * 2 * p - c2)) / 2\n : (Math.pow(2 * p - 2, 2) * ((c2 + 1) * (p * 2 - 2) + c2) + 2) / 2\n }\n }\n}\n"],"mappings":";AAeA,IAAa,QAAb,MAAa,MAAqE;CAChF;CACA,WAAqB;CACrB;CACA;CACA;CACA;CACA;CAEA,YAAY,OAAc,QAA6B;EACrD,KAAK,QAAQ;EACb,KAAK,eAAe,EAAE,GAAG,MAAM;EAC/B,MAAM,EAAE,UAAU,UAAU,YAAY,mBAAmB,UAAU,CAAC;EACtE,IAAI,aAAa,KAAA,GAAW,KAAK,WAAW;EAC5C,IAAI,aAAa,KAAA,GAAW,KAAK,WAAW;EAC5C,IAAI,eAAe,KAAA,GAAW,KAAK,aAAa;EAChD,KAAK,iBAAiB,kBAAkB,MAAM,OAAO;CACvD;CAEA,IAAc,SAIL;EACP,MAAM,QAAQ,YAAY,IAAI;EAC9B,MAAM,EAAE,YAAY,MAAM,aAAa;EACvC,MAAM,UAAU,cAAsB;GACpC,MAAM,UAAU,YAAY;GAC5B,MAAM,WAAW,KAAK,IAAI,UAAU,UAAU,CAAC;GAC/C,KAAK,QAAQ;GACb,IAAI,WAAW,GACb,KAAK,UAAU,sBAAsB,MAAM;QACtC;IACL,KAAK,QAAQ;IACb,KAAK,KAAK;IACV,WAAW;GACb;EACF;EACA,KAAK,UAAU,sBAAsB,MAAM;CAC7C;CAEA,GAAG,OAAuB,QAAmC;EAC3D,KAAK,KAAK;EACV,MAAM,YAAY,EAAE,GAAG,KAAK,MAAM;EAClC,MAAM,gBAAgB,OAAO,KAAK,KAAK,CAAC,CAAC,QACtC,KAAK,QAAQ;GACZ,MAAM,IAAI;GACV,MAAM,KAAK,MAAM;GACjB,MAAM,KAAK,UAAU;GACrB,KAAK,MAAM,OAAO,OAAO,MAAM,OAAO,IACpC,IAAI,OAAO,KAAK;GAElB,OAAO;EACT,GACA,CAAC,CACH;EACA,MAAM,WAAW,QAAQ,YAAY,KAAK;EAC1C,MAAM,iBAAiB,QAAQ,kBAAkB,KAAK;EACtD,MAAM,aAAa,QAAQ,cAAc,KAAK;EAC9C,KAAK,IAAI;GACP;GACA,kBAAkB;IAChB,KAAK,MAAM,OAAO,OAChB,IAAI,OAAO,KAAK,OACb,KAAM,MAAiC,OAAO,MAAM;IAGzD,KAAK,WAAW,KAAK,KAAK;IAC1B,aAAa,KAAK,KAAK;GACzB;GACA,OAAO,aAAa;IAClB,KAAK,MAAM,OAAO,eAAe;KAE/B,MAAM,SAAU,UAAUA,OAAiB,eAAe,QAAQ,IAAI,cAAc;KACnF,KAAM,MAAiC,OAAO;IACjD;IACA,KAAK,WAAW,KAAK,KAAK;GAC5B;EACF,CAAC;CACH;CAEA,KAAK,QAAmC;EACtC,KAAK,GAAG,KAAK,cAAgC,MAAM;CACrD;CAEA,OAAwB;EACtB,IAAI,CAAC,KAAK,SAAS,OAAO;EAC1B,qBAAqB,KAAK,OAAO;EACjC,KAAK,UAAU,KAAA;EACf,OAAO;CACT;CAEA,OAAgB,SAAS;EACvB,SAAS,MAAc;EACvB,aAAa,MAAc,IAAI;EAC/B,cAAc,MAAc,KAAK,IAAI;EACrC,gBAAgB,MAAe,IAAI,KAAM,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,KAAK;EACxE,aAAa,MAAc,IAAI,KAAM,UAAe,IAAI;EACxD,cAAc,MAAc,IAAI,UAAU,KAAK,IAAI,IAAI,GAAG,CAAC,IAAI,UAAU,KAAK,IAAI,IAAI,GAAG,CAAC;EAC1F,gBAAgB,MAAc;GAE5B,MAAM,KAAK
|
|
1
|
+
{"version":3,"file":"tween.js","names":["pk"],"sources":["../../src/helper/tween.ts"],"sourcesContent":["/** 自 cat-kit 3.x Tween 行为对齐的轻量动画(原 `cat-kit/fe` 导出) */\n\nexport interface AnimeConfig<State extends Record<string, number>> {\n duration?: number\n easingFunction?: (progress: number) => number\n onComplete?(state: State): void\n}\n\nexport interface TweenConfig<State extends Record<string, number>> {\n duration?: number\n onUpdate?(state: State): void\n onComplete?(state: State): void\n easingFunction?: (progress: number) => number\n}\n\nexport class Tween<State extends Record<string, number> = Record<string, number>> {\n readonly state: State\n protected duration = 300\n protected onUpdate?: (state: State) => void\n protected onComplete?: (state: State) => void\n protected frameId?: number\n protected easingFunction: (progress: number) => number\n private defaultState: State\n\n constructor(state: State, config?: TweenConfig<State>) {\n this.state = state\n this.defaultState = { ...state }\n const { duration, onUpdate, onComplete, easingFunction } = config || {}\n if (duration !== undefined) this.duration = duration\n if (onUpdate !== undefined) this.onUpdate = onUpdate\n if (onComplete !== undefined) this.onComplete = onComplete\n this.easingFunction = easingFunction ?? Tween.easing.linear\n }\n\n protected raf(options: {\n onComplete: () => void\n duration: number\n tick: (p: number) => void\n }): void {\n const start = performance.now()\n const { onComplete, tick, duration } = options\n const update = (timestamp: number) => {\n const elapsed = timestamp - start\n const progress = Math.min(elapsed / duration, 1)\n tick(progress)\n if (progress < 1) {\n this.frameId = requestAnimationFrame(update)\n } else {\n tick(progress)\n this.stop()\n onComplete()\n }\n }\n this.frameId = requestAnimationFrame(update)\n }\n\n to(state: Partial<State>, config?: AnimeConfig<State>): void {\n this.stop()\n const prevState = { ...this.state }\n const stateDistance = Object.keys(state).reduce(\n (acc, key) => {\n const k = key as keyof State\n const sv = state[k] as number\n const pv = prevState[k] as number\n if ((sv || sv === 0) && (pv || pv === 0)) {\n acc[key] = sv - pv\n }\n return acc\n },\n {} as Record<string, number>\n )\n const duration = config?.duration || this.duration\n const easingFunction = config?.easingFunction || this.easingFunction\n const onComplete = config?.onComplete || this.onComplete\n this.raf({\n duration,\n onComplete: () => {\n for (const key in state) {\n if (key in this.state) {\n ;(this.state as Record<string, number>)[key] = state[key as keyof State] as number\n }\n }\n this.onUpdate?.(this.state)\n onComplete?.(this.state)\n },\n tick: (progress) => {\n for (const key in stateDistance) {\n const pk = key as keyof State\n const target = (prevState[pk] as number) + easingFunction(progress) * stateDistance[key]!\n ;(this.state as Record<string, number>)[key] = target\n }\n this.onUpdate?.(this.state)\n }\n })\n }\n\n back(config?: AnimeConfig<State>): void {\n this.to(this.defaultState as Partial<State>, config)\n }\n\n private stop(): boolean {\n if (!this.frameId) return false\n cancelAnimationFrame(this.frameId)\n this.frameId = undefined\n return true\n }\n\n static readonly easing = {\n linear: (p: number) => p,\n easeInQuad: (p: number) => p * p,\n easeOutQuad: (p: number) => p * (2 - p),\n easeInOutQuad: (p: number) => (p < 0.5 ? 2 * p * p : -1 + (4 - 2 * p) * p),\n easeInBack: (p: number) => p * p * ((2.70158 + 1) * p - 1),\n easeOutBack: (p: number) => 1 + 2.70158 * Math.pow(p - 1, 3) + 1.70158 * Math.pow(p - 1, 2),\n easeInOutBack: (p: number) => {\n const c1 = 1.70158\n const c2 = c1 * 1.525\n return p < 0.5\n ? (Math.pow(2 * p, 2) * ((c2 + 1) * 2 * p - c2)) / 2\n : (Math.pow(2 * p - 2, 2) * ((c2 + 1) * (p * 2 - 2) + c2) + 2) / 2\n }\n }\n}\n"],"mappings":";AAeA,IAAa,QAAb,MAAa,MAAqE;CAChF;CACA,WAAqB;CACrB;CACA;CACA;CACA;CACA;CAEA,YAAY,OAAc,QAA6B;EACrD,KAAK,QAAQ;EACb,KAAK,eAAe,EAAE,GAAG,MAAM;EAC/B,MAAM,EAAE,UAAU,UAAU,YAAY,mBAAmB,UAAU,CAAC;EACtE,IAAI,aAAa,KAAA,GAAW,KAAK,WAAW;EAC5C,IAAI,aAAa,KAAA,GAAW,KAAK,WAAW;EAC5C,IAAI,eAAe,KAAA,GAAW,KAAK,aAAa;EAChD,KAAK,iBAAiB,kBAAkB,MAAM,OAAO;CACvD;CAEA,IAAc,SAIL;EACP,MAAM,QAAQ,YAAY,IAAI;EAC9B,MAAM,EAAE,YAAY,MAAM,aAAa;EACvC,MAAM,UAAU,cAAsB;GACpC,MAAM,UAAU,YAAY;GAC5B,MAAM,WAAW,KAAK,IAAI,UAAU,UAAU,CAAC;GAC/C,KAAK,QAAQ;GACb,IAAI,WAAW,GACb,KAAK,UAAU,sBAAsB,MAAM;QACtC;IACL,KAAK,QAAQ;IACb,KAAK,KAAK;IACV,WAAW;GACb;EACF;EACA,KAAK,UAAU,sBAAsB,MAAM;CAC7C;CAEA,GAAG,OAAuB,QAAmC;EAC3D,KAAK,KAAK;EACV,MAAM,YAAY,EAAE,GAAG,KAAK,MAAM;EAClC,MAAM,gBAAgB,OAAO,KAAK,KAAK,CAAC,CAAC,QACtC,KAAK,QAAQ;GACZ,MAAM,IAAI;GACV,MAAM,KAAK,MAAM;GACjB,MAAM,KAAK,UAAU;GACrB,KAAK,MAAM,OAAO,OAAO,MAAM,OAAO,IACpC,IAAI,OAAO,KAAK;GAElB,OAAO;EACT,GACA,CAAC,CACH;EACA,MAAM,WAAW,QAAQ,YAAY,KAAK;EAC1C,MAAM,iBAAiB,QAAQ,kBAAkB,KAAK;EACtD,MAAM,aAAa,QAAQ,cAAc,KAAK;EAC9C,KAAK,IAAI;GACP;GACA,kBAAkB;IAChB,KAAK,MAAM,OAAO,OAChB,IAAI,OAAO,KAAK,OACb,KAAM,MAAiC,OAAO,MAAM;IAGzD,KAAK,WAAW,KAAK,KAAK;IAC1B,aAAa,KAAK,KAAK;GACzB;GACA,OAAO,aAAa;IAClB,KAAK,MAAM,OAAO,eAAe;KAE/B,MAAM,SAAU,UAAUA,OAAiB,eAAe,QAAQ,IAAI,cAAc;KACnF,KAAM,MAAiC,OAAO;IACjD;IACA,KAAK,WAAW,KAAK,KAAK;GAC5B;EACF,CAAC;CACH;CAEA,KAAK,QAAmC;EACtC,KAAK,GAAG,KAAK,cAAgC,MAAM;CACrD;CAEA,OAAwB;EACtB,IAAI,CAAC,KAAK,SAAS,OAAO;EAC1B,qBAAqB,KAAK,OAAO;EACjC,KAAK,UAAU,KAAA;EACf,OAAO;CACT;CAEA,OAAgB,SAAS;EACvB,SAAS,MAAc;EACvB,aAAa,MAAc,IAAI;EAC/B,cAAc,MAAc,KAAK,IAAI;EACrC,gBAAgB,MAAe,IAAI,KAAM,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,KAAK;EACxE,aAAa,MAAc,IAAI,KAAM,UAAe,IAAI;EACxD,cAAc,MAAc,IAAI,UAAU,KAAK,IAAI,IAAI,GAAG,CAAC,IAAI,UAAU,KAAK,IAAI,IAAI,GAAG,CAAC;EAC1F,gBAAgB,MAAc;GAE5B,MAAM,KAAK;GACX,OAAO,IAAI,KACN,KAAK,IAAI,IAAI,GAAG,CAAC,KAAK,WAAe,IAAI,MAAO,KAChD,KAAK,IAAI,IAAI,IAAI,GAAG,CAAC,KAAM,aAAW,IAAI,IAAI,KAAK,MAAM,KAAK;EACrE;CACF;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,10 +10,11 @@ import { createIncrease } from "./helper/create-increase.js";
|
|
|
10
10
|
import { createToggle } from "./helper/create-toggle.js";
|
|
11
11
|
import { AnimeConfig, Tween, TweenConfig } from "./helper/tween.js";
|
|
12
12
|
import { nextFrame } from "./helper/frame.js";
|
|
13
|
+
import { OverflowNavState, OverflowNavViewport, applyWheelHorizontalScroll, computeOverflowNavState, scrollElementIntoView, scrollViewportByStep } from "./helper/overflow-nav.js";
|
|
13
14
|
import { extractNormalVNodes, fieldKey, isComment, isFragment, isTemplate, isTextNode, shallowComputed } from "./helper/vue.js";
|
|
14
15
|
import { middleProxy } from "./reactive/proxy.js";
|
|
15
16
|
import { DeconstructValue, DefineEvent, Index, Null, RenderReturn, Undef } from "./types/helper.js";
|
|
16
17
|
import { BreakpointName, ColorType, ComponentProps, ComponentSize, FormComponentProps, PresetRule, PropsWithServerQuery, ValidateRule } from "./types/component-common.js";
|
|
17
18
|
import { FormContextModel, FormContextProps, FormFieldItem, injectFormContext, provideFormContext } from "./types/form-context.js";
|
|
18
19
|
import "./types/index.js";
|
|
19
|
-
export { AnimeConfig, BEM, BEMFactory, BreakpointName, CLS_PREFIX, ColorType, ComponentProps, ComponentSize, DeconstructValue, DefineEvent, ExpandTransition, ExpandTransitionOptions, FORM_EMPTY_CONTENT, FormComponentProps, FormContextModel, FormContextProps, FormFieldItem, Index, NAME_SPACE, Null, PresetRule, PropsWithServerQuery, RenderReturn, Tween, TweenConfig, Undef, ValidateRule, addClass, bem, createIncrease, createToggle, extractNormalVNodes, fieldKey, getHighlightChunks, getNearestScrollParent, getScrollParents, injectFormContext, isComment, isFragment, isTemplate, isTextNode, makeBEM, middleProxy, nextFrame, provideFormContext, removeClass, removeStyles, scrollIntoContainerView, setStyles, shallowComputed, withUnit, zIndex };
|
|
20
|
+
export { AnimeConfig, BEM, BEMFactory, BreakpointName, CLS_PREFIX, ColorType, ComponentProps, ComponentSize, DeconstructValue, DefineEvent, ExpandTransition, ExpandTransitionOptions, FORM_EMPTY_CONTENT, FormComponentProps, FormContextModel, FormContextProps, FormFieldItem, Index, NAME_SPACE, Null, OverflowNavState, OverflowNavViewport, PresetRule, PropsWithServerQuery, RenderReturn, Tween, TweenConfig, Undef, ValidateRule, addClass, applyWheelHorizontalScroll, bem, computeOverflowNavState, createIncrease, createToggle, extractNormalVNodes, fieldKey, getHighlightChunks, getNearestScrollParent, getScrollParents, injectFormContext, isComment, isFragment, isTemplate, isTextNode, makeBEM, middleProxy, nextFrame, provideFormContext, removeClass, removeStyles, scrollElementIntoView, scrollIntoContainerView, scrollViewportByStep, setStyles, shallowComputed, withUnit, zIndex };
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,8 @@ import { zIndex } from "./dom/z-index.js";
|
|
|
10
10
|
import { createToggle } from "./helper/create-toggle.js";
|
|
11
11
|
import { Tween } from "./helper/tween.js";
|
|
12
12
|
import { nextFrame } from "./helper/frame.js";
|
|
13
|
+
import { applyWheelHorizontalScroll, computeOverflowNavState, scrollElementIntoView, scrollViewportByStep } from "./helper/overflow-nav.js";
|
|
13
14
|
import { extractNormalVNodes, fieldKey, isComment, isFragment, isTemplate, isTextNode, shallowComputed } from "./helper/vue.js";
|
|
14
15
|
import { middleProxy } from "./reactive/proxy.js";
|
|
15
16
|
import { injectFormContext, provideFormContext } from "./types/form-context.js";
|
|
16
|
-
export { CLS_PREFIX, ExpandTransition, FORM_EMPTY_CONTENT, NAME_SPACE, Tween, addClass, bem, createIncrease, createToggle, extractNormalVNodes, fieldKey, getHighlightChunks, getNearestScrollParent, getScrollParents, injectFormContext, isComment, isFragment, isTemplate, isTextNode, makeBEM, middleProxy, nextFrame, provideFormContext, removeClass, removeStyles, scrollIntoContainerView, setStyles, shallowComputed, withUnit, zIndex };
|
|
17
|
+
export { CLS_PREFIX, ExpandTransition, FORM_EMPTY_CONTENT, NAME_SPACE, Tween, addClass, applyWheelHorizontalScroll, bem, computeOverflowNavState, createIncrease, createToggle, extractNormalVNodes, fieldKey, getHighlightChunks, getNearestScrollParent, getScrollParents, injectFormContext, isComment, isFragment, isTemplate, isTextNode, makeBEM, middleProxy, nextFrame, provideFormContext, removeClass, removeStyles, scrollElementIntoView, scrollIntoContainerView, scrollViewportByStep, setStyles, shallowComputed, withUnit, zIndex };
|
package/package.json
CHANGED
package/src/dom/position.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 获取可滚动的父级(纵向或横向可滚动均计入:overflow-x 容器如横向滚动条
|
|
3
|
+
* 也是滚动父级,弹层需监听其 scroll 事件)
|
|
3
4
|
* @param el 元素
|
|
4
5
|
* @returns
|
|
5
6
|
*/
|
|
@@ -7,7 +8,7 @@ export function getScrollParents(el: HTMLElement): HTMLElement[] {
|
|
|
7
8
|
const parents: HTMLElement[] = []
|
|
8
9
|
let parent = el.parentElement
|
|
9
10
|
while (parent) {
|
|
10
|
-
if (parent.scrollHeight > parent.clientHeight) {
|
|
11
|
+
if (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) {
|
|
11
12
|
parents.push(parent)
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -17,14 +18,14 @@ export function getScrollParents(el: HTMLElement): HTMLElement[] {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
+
* 获取最近的可滚动父级(纵向或横向可滚动均计入)
|
|
21
22
|
* @param el 元素
|
|
22
23
|
* @returns 最近的可滚动父级
|
|
23
24
|
*/
|
|
24
25
|
export function getNearestScrollParent(el: HTMLElement): HTMLElement | null {
|
|
25
26
|
let parent = el.parentElement
|
|
26
27
|
while (parent) {
|
|
27
|
-
if (parent.scrollHeight > parent.clientHeight) {
|
|
28
|
+
if (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) {
|
|
28
29
|
return parent
|
|
29
30
|
}
|
|
30
31
|
parent = parent.parentElement
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 水平溢出滚动导航的共享纯逻辑(tabs / toolbar 三处复制的收敛,见 #17)。
|
|
3
|
+
* 只提供无 Vue 依赖的计算与副作用,事件绑定 / ResizeObserver / watch 由各
|
|
4
|
+
* composable 按需组合(三处的监听源与生命周期有真实差异,不强行统一)。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 视口滚动几何(scrollTo 兼容 HTMLElement 与 vue 模板 ref) */
|
|
8
|
+
export interface OverflowNavViewport {
|
|
9
|
+
scrollLeft: number
|
|
10
|
+
scrollWidth: number
|
|
11
|
+
clientWidth: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 导航按钮状态 */
|
|
15
|
+
export interface OverflowNavState {
|
|
16
|
+
/** 内容是否溢出视口 */
|
|
17
|
+
overflowing: boolean
|
|
18
|
+
/** 可向前(左/上)滚动 */
|
|
19
|
+
canPrev: boolean
|
|
20
|
+
/** 可向后(右/下)滚动 */
|
|
21
|
+
canNext: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 依据视口与内容尺寸推导滚动按钮可用状态 */
|
|
25
|
+
export function computeOverflowNavState(vp: OverflowNavViewport): OverflowNavState {
|
|
26
|
+
const overflowing = vp.scrollWidth - vp.clientWidth > 1
|
|
27
|
+
return {
|
|
28
|
+
overflowing,
|
|
29
|
+
canPrev: overflowing && vp.scrollLeft > 0,
|
|
30
|
+
canNext: overflowing && vp.scrollLeft + vp.clientWidth < vp.scrollWidth - 1
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ScrollableElement = OverflowNavViewport & {
|
|
35
|
+
scrollTo(options: { left: number; behavior: 'smooth' }): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 箭头按钮:按视口宽度 80% 的步长平滑滚动 */
|
|
39
|
+
export function scrollViewportByStep(vp: ScrollableElement, dir: 1 | -1): void {
|
|
40
|
+
vp.scrollTo({ left: vp.scrollLeft + dir * vp.clientWidth * 0.8, behavior: 'smooth' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 目标元素滚入视口(左/右越界时对齐到边缘,留 offset 边距) */
|
|
44
|
+
export function scrollElementIntoView(vp: HTMLElement, el: HTMLElement, offset = 8): void {
|
|
45
|
+
const vpRect = vp.getBoundingClientRect()
|
|
46
|
+
const elRect = el.getBoundingClientRect()
|
|
47
|
+
if (elRect.left < vpRect.left) {
|
|
48
|
+
vp.scrollTo({ left: vp.scrollLeft + (elRect.left - vpRect.left) - offset, behavior: 'smooth' })
|
|
49
|
+
} else if (elRect.right > vpRect.right) {
|
|
50
|
+
vp.scrollTo({
|
|
51
|
+
left: vp.scrollLeft + (elRect.right - vpRect.right) + offset,
|
|
52
|
+
behavior: 'smooth'
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 鼠标滚轮(纵向)驱动水平滚动;触控板横滑(已有 deltaX)不拦截。
|
|
59
|
+
* navActive = 导航按钮是否可见(内容未溢出时不消费滚轮)。
|
|
60
|
+
*/
|
|
61
|
+
export function applyWheelHorizontalScroll(
|
|
62
|
+
e: WheelEvent,
|
|
63
|
+
vp: HTMLElement,
|
|
64
|
+
navActive: boolean
|
|
65
|
+
): void {
|
|
66
|
+
if (!navActive) return
|
|
67
|
+
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return
|
|
68
|
+
if (e.deltaY === 0) return
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
vp.scrollLeft += e.deltaY
|
|
71
|
+
}
|