@veltra/utils 1.4.0 → 1.6.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.
@@ -15,8 +15,13 @@ declare class ExpandTransition {
15
15
  beforeLeave(el: HTMLElement): void;
16
16
  leave(el: HTMLElement): void;
17
17
  afterLeave(el: HTMLElement): void;
18
- expand(el: HTMLElement): void;
19
- collapse(el: HTMLElement): void;
18
+ /**
19
+ * 展开动画;`onEnd` 在动画落定(含无需动画直接落定)后调用,
20
+ * 被新的动画打断时不调用
21
+ */
22
+ expand(el: HTMLElement, onEnd?: () => void): void;
23
+ /** 收起动画;`onEnd` 语义同 `expand` */
24
+ collapse(el: HTMLElement, onEnd?: () => void): void;
20
25
  setExpanded(el: HTMLElement, expanded: boolean): void;
21
26
  private animate;
22
27
  private resetTransitionStyles;
@@ -69,11 +69,16 @@ var ExpandTransition = class {
69
69
  afterLeave(el) {
70
70
  this.resetTransitionStyles(el);
71
71
  }
72
- expand(el) {
73
- this.animate(el, true);
72
+ /**
73
+ * 展开动画;`onEnd` 在动画落定(含无需动画直接落定)后调用,
74
+ * 被新的动画打断时不调用
75
+ */
76
+ expand(el, onEnd) {
77
+ this.animate(el, true, onEnd);
74
78
  }
75
- collapse(el) {
76
- this.animate(el, false);
79
+ /** 收起动画;`onEnd` 语义同 `expand` */
80
+ collapse(el, onEnd) {
81
+ this.animate(el, false, onEnd);
77
82
  }
78
83
  setExpanded(el, expanded) {
79
84
  this.cancel(el);
@@ -81,13 +86,14 @@ var ExpandTransition = class {
81
86
  el.style.overflow = "hidden";
82
87
  el.style.height = expanded ? "auto" : "0px";
83
88
  }
84
- animate(el, expanded) {
89
+ animate(el, expanded, onEnd) {
85
90
  this.cancel(el);
86
91
  const startHeight = el.offsetHeight;
87
92
  const endHeight = expanded ? el.scrollHeight : 0;
88
93
  const { paddingTop, paddingBottom } = readVerticalPadding(el);
89
94
  if (startHeight === endHeight) {
90
95
  this.setExpanded(el, expanded);
96
+ onEnd?.();
91
97
  return;
92
98
  }
93
99
  setStyles(el, {
@@ -106,19 +112,20 @@ var ExpandTransition = class {
106
112
  paddingBottom: expanded ? paddingBottom : 0
107
113
  });
108
114
  const cleanup = () => {
109
- el.removeEventListener("transitionend", onEnd);
110
- el.removeEventListener("transitioncancel", onEnd);
115
+ el.removeEventListener("transitionend", onEndHandler);
116
+ el.removeEventListener("transitioncancel", onEndHandler);
111
117
  this.cleanupMap.delete(el);
112
118
  };
113
- const onEnd = (e) => {
119
+ const onEndHandler = (e) => {
114
120
  if (e.target !== el || e.propertyName !== "height") return;
115
121
  cleanup();
116
122
  this.resetTemporaryStyles(el);
117
123
  el.style.overflow = "hidden";
118
124
  el.style.height = expanded ? "auto" : "0px";
125
+ onEnd?.();
119
126
  };
120
- el.addEventListener("transitionend", onEnd);
121
- el.addEventListener("transitioncancel", onEnd);
127
+ el.addEventListener("transitionend", onEndHandler);
128
+ el.addEventListener("transitioncancel", onEndHandler);
122
129
  this.cleanupMap.set(el, cleanup);
123
130
  }
124
131
  resetTransitionStyles(el) {
@@ -1 +1 @@
1
- {"version":3,"file":"expand-transition.js","names":[],"sources":["../../src/dom/expand-transition.ts"],"sourcesContent":["import { removeStyles, setStyles } from './style'\n\nexport interface ExpandTransitionOptions {\n transition?: string\n enterTransition?: string\n leaveTransition?: string\n opacity?: boolean\n}\n\nfunction readVerticalPadding(el: HTMLElement) {\n const { paddingTop, paddingBottom } = getComputedStyle(el)\n return { paddingTop, paddingBottom }\n}\n\nfunction getTransition(options: ExpandTransitionOptions, type: 'enter' | 'leave') {\n return (\n (type === 'enter' ? options.enterTransition : options.leaveTransition) ??\n options.transition ??\n 'height 0.25s cubic-bezier(0.4, 0, 0.2, 1)'\n )\n}\n\nexport class ExpandTransition {\n private cleanupMap = new Map<HTMLElement, () => void>()\n\n constructor(private options: ExpandTransitionOptions = {}) {}\n\n cancel(el: HTMLElement) {\n this.cleanupMap.get(el)?.()\n this.cleanupMap.delete(el)\n }\n\n enter(el: HTMLElement) {\n const height = el.scrollHeight\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: 0,\n paddingTop: 0,\n paddingBottom: 0,\n overflow: 'hidden',\n transition: getTransition(this.options, 'enter'),\n willChange: this.options.opacity ? 'height, opacity' : 'height'\n })\n\n if (this.options.opacity) {\n el.style.opacity = '0'\n }\n\n void el.offsetHeight\n\n setStyles(el, { height: `${height}px`, paddingTop, paddingBottom })\n\n if (this.options.opacity) {\n el.style.opacity = '1'\n }\n }\n\n afterEnter(el: HTMLElement) {\n this.resetTransitionStyles(el)\n }\n\n beforeLeave(el: HTMLElement) {\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: `${el.scrollHeight}px`,\n paddingTop,\n paddingBottom,\n overflow: 'hidden',\n transition: getTransition(this.options, 'leave'),\n willChange: this.options.opacity ? 'height, opacity' : 'height'\n })\n\n if (this.options.opacity) {\n el.style.opacity = '1'\n }\n }\n\n leave(el: HTMLElement) {\n void el.offsetHeight\n\n setStyles(el, { height: 0, paddingTop: 0, paddingBottom: 0 })\n\n if (this.options.opacity) {\n el.style.opacity = '0'\n }\n }\n\n afterLeave(el: HTMLElement) {\n this.resetTransitionStyles(el)\n }\n\n expand(el: HTMLElement) {\n this.animate(el, true)\n }\n\n collapse(el: HTMLElement) {\n this.animate(el, false)\n }\n\n setExpanded(el: HTMLElement, expanded: boolean) {\n this.cancel(el)\n this.resetTemporaryStyles(el)\n el.style.overflow = 'hidden'\n el.style.height = expanded ? 'auto' : '0px'\n }\n\n private animate(el: HTMLElement, expanded: boolean) {\n this.cancel(el)\n\n const startHeight = el.offsetHeight\n const endHeight = expanded ? el.scrollHeight : 0\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n if (startHeight === endHeight) {\n this.setExpanded(el, expanded)\n return\n }\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: `${startHeight}px`,\n paddingTop,\n paddingBottom,\n overflow: 'hidden',\n transition: getTransition(this.options, expanded ? 'enter' : 'leave'),\n willChange: 'height'\n })\n\n void el.offsetHeight\n\n setStyles(el, {\n height: `${endHeight}px`,\n paddingTop: expanded ? paddingTop : 0,\n paddingBottom: expanded ? paddingBottom : 0\n })\n\n const cleanup = () => {\n el.removeEventListener('transitionend', onEnd)\n el.removeEventListener('transitioncancel', onEnd)\n this.cleanupMap.delete(el)\n }\n\n const onEnd = (e: TransitionEvent) => {\n if (e.target !== el || e.propertyName !== 'height') return\n cleanup()\n this.resetTemporaryStyles(el)\n el.style.overflow = 'hidden'\n el.style.height = expanded ? 'auto' : '0px'\n }\n\n el.addEventListener('transitionend', onEnd)\n el.addEventListener('transitioncancel', onEnd)\n this.cleanupMap.set(el, cleanup)\n }\n\n private resetTransitionStyles(el: HTMLElement) {\n removeStyles(el, [\n 'box-sizing',\n 'height',\n 'padding-top',\n 'padding-bottom',\n 'overflow',\n 'transition',\n 'opacity',\n 'will-change'\n ])\n }\n\n private resetTemporaryStyles(el: HTMLElement) {\n removeStyles(el, [\n 'box-sizing',\n 'padding-top',\n 'padding-bottom',\n 'transition',\n 'opacity',\n 'will-change'\n ])\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"expand-transition.js","names":[],"sources":["../../src/dom/expand-transition.ts"],"sourcesContent":["import { removeStyles, setStyles } from './style'\n\nexport interface ExpandTransitionOptions {\n transition?: string\n enterTransition?: string\n leaveTransition?: string\n opacity?: boolean\n}\n\nfunction readVerticalPadding(el: HTMLElement) {\n const { paddingTop, paddingBottom } = getComputedStyle(el)\n return { paddingTop, paddingBottom }\n}\n\nfunction getTransition(options: ExpandTransitionOptions, type: 'enter' | 'leave') {\n return (\n (type === 'enter' ? options.enterTransition : options.leaveTransition) ??\n options.transition ??\n 'height 0.25s cubic-bezier(0.4, 0, 0.2, 1)'\n )\n}\n\nexport class ExpandTransition {\n private cleanupMap = new Map<HTMLElement, () => void>()\n\n constructor(private options: ExpandTransitionOptions = {}) {}\n\n cancel(el: HTMLElement) {\n this.cleanupMap.get(el)?.()\n this.cleanupMap.delete(el)\n }\n\n enter(el: HTMLElement) {\n const height = el.scrollHeight\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: 0,\n paddingTop: 0,\n paddingBottom: 0,\n overflow: 'hidden',\n transition: getTransition(this.options, 'enter'),\n willChange: this.options.opacity ? 'height, opacity' : 'height'\n })\n\n if (this.options.opacity) {\n el.style.opacity = '0'\n }\n\n void el.offsetHeight\n\n setStyles(el, { height: `${height}px`, paddingTop, paddingBottom })\n\n if (this.options.opacity) {\n el.style.opacity = '1'\n }\n }\n\n afterEnter(el: HTMLElement) {\n this.resetTransitionStyles(el)\n }\n\n beforeLeave(el: HTMLElement) {\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: `${el.scrollHeight}px`,\n paddingTop,\n paddingBottom,\n overflow: 'hidden',\n transition: getTransition(this.options, 'leave'),\n willChange: this.options.opacity ? 'height, opacity' : 'height'\n })\n\n if (this.options.opacity) {\n el.style.opacity = '1'\n }\n }\n\n leave(el: HTMLElement) {\n void el.offsetHeight\n\n setStyles(el, { height: 0, paddingTop: 0, paddingBottom: 0 })\n\n if (this.options.opacity) {\n el.style.opacity = '0'\n }\n }\n\n afterLeave(el: HTMLElement) {\n this.resetTransitionStyles(el)\n }\n\n /**\n * 展开动画;`onEnd` 在动画落定(含无需动画直接落定)后调用,\n * 被新的动画打断时不调用\n */\n expand(el: HTMLElement, onEnd?: () => void) {\n this.animate(el, true, onEnd)\n }\n\n /** 收起动画;`onEnd` 语义同 `expand` */\n collapse(el: HTMLElement, onEnd?: () => void) {\n this.animate(el, false, onEnd)\n }\n\n setExpanded(el: HTMLElement, expanded: boolean) {\n this.cancel(el)\n this.resetTemporaryStyles(el)\n el.style.overflow = 'hidden'\n el.style.height = expanded ? 'auto' : '0px'\n }\n\n private animate(el: HTMLElement, expanded: boolean, onEnd?: () => void) {\n this.cancel(el)\n\n const startHeight = el.offsetHeight\n const endHeight = expanded ? el.scrollHeight : 0\n const { paddingTop, paddingBottom } = readVerticalPadding(el)\n\n if (startHeight === endHeight) {\n this.setExpanded(el, expanded)\n onEnd?.()\n return\n }\n\n setStyles(el, {\n boxSizing: 'border-box',\n height: `${startHeight}px`,\n paddingTop,\n paddingBottom,\n overflow: 'hidden',\n transition: getTransition(this.options, expanded ? 'enter' : 'leave'),\n willChange: 'height'\n })\n\n void el.offsetHeight\n\n setStyles(el, {\n height: `${endHeight}px`,\n paddingTop: expanded ? paddingTop : 0,\n paddingBottom: expanded ? paddingBottom : 0\n })\n\n const cleanup = () => {\n el.removeEventListener('transitionend', onEndHandler)\n el.removeEventListener('transitioncancel', onEndHandler)\n this.cleanupMap.delete(el)\n }\n\n const onEndHandler = (e: TransitionEvent) => {\n if (e.target !== el || e.propertyName !== 'height') return\n cleanup()\n this.resetTemporaryStyles(el)\n el.style.overflow = 'hidden'\n el.style.height = expanded ? 'auto' : '0px'\n onEnd?.()\n }\n\n el.addEventListener('transitionend', onEndHandler)\n el.addEventListener('transitioncancel', onEndHandler)\n this.cleanupMap.set(el, cleanup)\n }\n\n private resetTransitionStyles(el: HTMLElement) {\n removeStyles(el, [\n 'box-sizing',\n 'height',\n 'padding-top',\n 'padding-bottom',\n 'overflow',\n 'transition',\n 'opacity',\n 'will-change'\n ])\n }\n\n private resetTemporaryStyles(el: HTMLElement) {\n removeStyles(el, [\n 'box-sizing',\n 'padding-top',\n 'padding-bottom',\n 'transition',\n 'opacity',\n 'will-change'\n ])\n }\n}\n"],"mappings":"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"}
@@ -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
  */
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"position.js","names":[],"sources":["../../src/dom/position.ts"],"sourcesContent":["/**\n * 获取可滚动的父级\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) {\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) {\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":";;;;;;AAKA,SAAgB,iBAAiB,IAAgC;CAC/D,MAAM,UAAyB,CAAC;CAChC,IAAI,SAAS,GAAG;CAChB,OAAO,QAAQ;EACb,IAAI,OAAO,eAAe,OAAO,cAC/B,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,cAC/B,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;MACC,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;MACC,WAAW,UACb,UAAU,aAAa,cAAc,eAAe,IAAI,eAAe;OAClE,IAAI,WAAW,SACpB,UAAU,aAAa;OAClB,IAAI,WAAW,OACpB,UAAU,aAAa,cAAc,eAAe;CAAA;AAG1D"}
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"}
@@ -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 = 1.70158 * 1.525;
82
- return p < .5 ? Math.pow(2 * p, 2) * (3.5949095 * 2 * p - c2) / 2 : (Math.pow(2 * p - 2, 2) * (3.5949095 * (p * 2 - 2) + c2) + 2) / 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
  };
@@ -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,UAAK;GAChB,OAAO,IAAI,KACN,KAAK,IAAI,IAAI,GAAG,CAAC,KAAM,YAAU,IAAI,IAAI,MAAO,KAChD,KAAK,IAAI,IAAI,IAAI,GAAG,CAAC,KAAM,aAAW,IAAI,IAAI,KAAK,MAAM,KAAK;EACrE;CACF;AACF"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/utils",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -21,6 +21,6 @@
21
21
  },
22
22
  "peerDependencies": {
23
23
  "@cat-kit/core": ">=1.1.8",
24
- "vue": ">=3.5.40"
24
+ "vue": ">=3.5.41"
25
25
  }
26
26
  }
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  import { ExpandTransition } from '../expand-transition'
4
4
 
@@ -96,4 +96,54 @@ describe('ExpandTransition', () => {
96
96
  transition.cancel(el)
97
97
  }
98
98
  })
99
+
100
+ it('invokes onEnd after the transition settles', () => {
101
+ const el = createPanel()
102
+ const transition = new ExpandTransition({ transition: 'height 0.25s ease' })
103
+ const onEnd = vi.fn()
104
+
105
+ try {
106
+ transition.collapse(el, onEnd)
107
+ expect(onEnd).not.toHaveBeenCalled()
108
+
109
+ fireHeightTransitionEnd(el)
110
+ expect(onEnd).toHaveBeenCalledTimes(1)
111
+ } finally {
112
+ el.remove()
113
+ transition.cancel(el)
114
+ }
115
+ })
116
+
117
+ it('does not invoke onEnd when interrupted by a new animation', () => {
118
+ const el = createPanel()
119
+ const transition = new ExpandTransition({ transition: 'height 0.25s ease' })
120
+ const onEnd = vi.fn()
121
+
122
+ try {
123
+ transition.collapse(el, onEnd)
124
+ // 新动画打断旧动画:旧 onEnd 不应再触发
125
+ transition.expand(el)
126
+ fireHeightTransitionEnd(el)
127
+ expect(onEnd).not.toHaveBeenCalled()
128
+ } finally {
129
+ el.remove()
130
+ transition.cancel(el)
131
+ }
132
+ })
133
+
134
+ it('invokes onEnd immediately when no animation is needed', () => {
135
+ const el = document.createElement('div')
136
+ document.body.appendChild(el)
137
+ const transition = new ExpandTransition({ transition: 'height 0.25s ease' })
138
+ const onEnd = vi.fn()
139
+
140
+ try {
141
+ // 高度已为 0(happy-dom 无布局),收起无需动画,onEnd 同步调用
142
+ transition.collapse(el, onEnd)
143
+ expect(onEnd).toHaveBeenCalledTimes(1)
144
+ } finally {
145
+ el.remove()
146
+ transition.cancel(el)
147
+ }
148
+ })
99
149
  })
@@ -93,12 +93,17 @@ export class ExpandTransition {
93
93
  this.resetTransitionStyles(el)
94
94
  }
95
95
 
96
- expand(el: HTMLElement) {
97
- this.animate(el, true)
96
+ /**
97
+ * 展开动画;`onEnd` 在动画落定(含无需动画直接落定)后调用,
98
+ * 被新的动画打断时不调用
99
+ */
100
+ expand(el: HTMLElement, onEnd?: () => void) {
101
+ this.animate(el, true, onEnd)
98
102
  }
99
103
 
100
- collapse(el: HTMLElement) {
101
- this.animate(el, false)
104
+ /** 收起动画;`onEnd` 语义同 `expand` */
105
+ collapse(el: HTMLElement, onEnd?: () => void) {
106
+ this.animate(el, false, onEnd)
102
107
  }
103
108
 
104
109
  setExpanded(el: HTMLElement, expanded: boolean) {
@@ -108,7 +113,7 @@ export class ExpandTransition {
108
113
  el.style.height = expanded ? 'auto' : '0px'
109
114
  }
110
115
 
111
- private animate(el: HTMLElement, expanded: boolean) {
116
+ private animate(el: HTMLElement, expanded: boolean, onEnd?: () => void) {
112
117
  this.cancel(el)
113
118
 
114
119
  const startHeight = el.offsetHeight
@@ -117,6 +122,7 @@ export class ExpandTransition {
117
122
 
118
123
  if (startHeight === endHeight) {
119
124
  this.setExpanded(el, expanded)
125
+ onEnd?.()
120
126
  return
121
127
  }
122
128
 
@@ -139,21 +145,22 @@ export class ExpandTransition {
139
145
  })
140
146
 
141
147
  const cleanup = () => {
142
- el.removeEventListener('transitionend', onEnd)
143
- el.removeEventListener('transitioncancel', onEnd)
148
+ el.removeEventListener('transitionend', onEndHandler)
149
+ el.removeEventListener('transitioncancel', onEndHandler)
144
150
  this.cleanupMap.delete(el)
145
151
  }
146
152
 
147
- const onEnd = (e: TransitionEvent) => {
153
+ const onEndHandler = (e: TransitionEvent) => {
148
154
  if (e.target !== el || e.propertyName !== 'height') return
149
155
  cleanup()
150
156
  this.resetTemporaryStyles(el)
151
157
  el.style.overflow = 'hidden'
152
158
  el.style.height = expanded ? 'auto' : '0px'
159
+ onEnd?.()
153
160
  }
154
161
 
155
- el.addEventListener('transitionend', onEnd)
156
- el.addEventListener('transitioncancel', onEnd)
162
+ el.addEventListener('transitionend', onEndHandler)
163
+ el.addEventListener('transitioncancel', onEndHandler)
157
164
  this.cleanupMap.set(el, cleanup)
158
165
  }
159
166
 
@@ -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
+ }
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ export * from './helper/frame'
20
20
 
21
21
  export * from './helper/make-bem'
22
22
 
23
+ export * from './helper/overflow-nav'
24
+
23
25
  export * from './helper/vue'
24
26
 
25
27
  export * from './reactive/proxy'