vanilla-jui 1.3.10 → 1.4.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.
Files changed (90) hide show
  1. package/dist/index.d.ts +2927 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.umd.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +4 -3
  6. package/src/components/accordion.ts +522 -0
  7. package/src/components/drop.ts +497 -0
  8. package/src/components/flow.ts +1579 -0
  9. package/src/components/form.ts +953 -0
  10. package/src/components/{icons.js → icons.ts} +24 -11
  11. package/src/components/menu.ts +615 -0
  12. package/src/components/{modal.js → modal.ts} +560 -237
  13. package/src/components/offcanvas.ts +496 -0
  14. package/src/components/{pagination.js → pagination.ts} +251 -76
  15. package/src/components/parabola.ts +335 -0
  16. package/src/components/{sticky.js → sticky.ts} +123 -98
  17. package/src/components/{swiper.js → swiper.ts} +510 -232
  18. package/src/components/tabs.ts +984 -0
  19. package/src/components/theme.ts +437 -0
  20. package/src/components/toast.ts +371 -0
  21. package/src/components/toc.ts +385 -0
  22. package/src/components/tooltip.ts +179 -0
  23. package/src/components/validator.ts +596 -0
  24. package/src/core/{Component.js → Component.ts} +134 -27
  25. package/src/css/accordion.css +2 -2
  26. package/src/css/animation.css +31 -1
  27. package/src/css/breadcrumb.css +1 -1
  28. package/src/css/button.css +5 -5
  29. package/src/css/card.css +7 -7
  30. package/src/css/color.css +865 -1028
  31. package/src/css/color_p3.css +871 -1036
  32. package/src/css/components.css +1 -0
  33. package/src/css/drop.css +1 -1
  34. package/src/css/flow.css +1 -1
  35. package/src/css/form.css +21 -21
  36. package/src/css/gradient.css +17 -4
  37. package/src/css/icon.css +17 -1
  38. package/src/css/index.css +3 -3
  39. package/src/css/layout.css +1 -1
  40. package/src/css/menu.css +27 -27
  41. package/src/css/modal.css +10 -27
  42. package/src/css/offcanvas.css +2 -2
  43. package/src/css/pagination.css +1 -1
  44. package/src/css/popup.css +2 -2
  45. package/src/css/swiper.css +12 -12
  46. package/src/css/tabs.css +3 -2
  47. package/src/css/tag.css +3 -4
  48. package/src/css/theme-palette.css +6 -6
  49. package/src/css/tip.css +1 -1
  50. package/src/css/toast.css +8 -8
  51. package/src/css/tokens.css +197 -226
  52. package/src/css/toolbar.css +3 -3
  53. package/src/css/tooltip.css +1 -1
  54. package/src/index.ts +37 -0
  55. package/src/locales/{index.js → index.ts} +1 -2
  56. package/src/types.d.ts +1 -0
  57. package/src/utilities/browser.ts +131 -0
  58. package/src/utilities/cache.ts +57 -0
  59. package/src/utilities/core.ts +614 -0
  60. package/src/utilities/dom.ts +468 -0
  61. package/src/utilities/events.ts +106 -0
  62. package/src/utilities/http.ts +35 -0
  63. package/src/utilities/storage.ts +46 -0
  64. package/src/components/accordion.js +0 -373
  65. package/src/components/drop.js +0 -437
  66. package/src/components/flow.js +0 -1133
  67. package/src/components/form.js +0 -585
  68. package/src/components/menu.js +0 -422
  69. package/src/components/offcanvas.js +0 -332
  70. package/src/components/parabola.js +0 -295
  71. package/src/components/tabs.js +0 -746
  72. package/src/components/theme.js +0 -353
  73. package/src/components/toast.js +0 -329
  74. package/src/components/toc.js +0 -226
  75. package/src/components/tooltip.js +0 -86
  76. package/src/components/validator.js +0 -586
  77. package/src/css/toast.min.css +0 -1
  78. package/src/index.js +0 -45
  79. package/src/locales/index.min.js +0 -1
  80. package/src/utilities/browser.js +0 -111
  81. package/src/utilities/cache.js +0 -159
  82. package/src/utilities/core.js +0 -502
  83. package/src/utilities/debug.js +0 -103
  84. package/src/utilities/dom.js +0 -313
  85. package/src/utilities/events.js +0 -69
  86. package/src/utilities/http.js +0 -30
  87. package/src/utilities/legacy-dom.js +0 -179
  88. package/src/utilities/storage.js +0 -46
  89. /package/src/{components/components.js → css/themes/index.css} +0 -0
  90. /package/src/icons/{index.js → index.ts} +0 -0
@@ -0,0 +1,335 @@
1
+ import { jsx } from 'vanilla-signal';
2
+
3
+ import { type ResolveSchema, resolveProps } from '../utilities/core.ts';
4
+ import { type DOMReference, isNode, resolveElement } from '../utilities/dom.ts';
5
+
6
+ export type ParabolaDirection =
7
+ | 'center'
8
+ | 'top-right'
9
+ | 'top-left'
10
+ | 'bottom-right'
11
+ | 'bottom-left';
12
+
13
+ export interface ParabolaBallOptions {
14
+ color: string;
15
+ size: string;
16
+ }
17
+
18
+ export interface ParabolaClassNames {
19
+ ball: string;
20
+ }
21
+
22
+ export type ParabolaClassNameConfig = Partial<ParabolaClassNames>;
23
+
24
+ export interface ParabolaOptions extends Record<string, unknown> {
25
+ ball?: ParabolaBallOptions;
26
+ className?: ParabolaClassNameConfig;
27
+ from?: DOMReference;
28
+ to?: DOMReference;
29
+ direction?: ParabolaDirection;
30
+ showDelay?: number;
31
+ hideDelay?: number;
32
+ onShow?: ((parabola: Parabola) => void) | null;
33
+ onHidden?: ((parabola: Parabola) => void) | null;
34
+ }
35
+
36
+ interface ResolvedParabolaOptions extends Record<string, unknown> {
37
+ ball: ParabolaBallOptions;
38
+ className: ParabolaClassNames;
39
+ from: DOMReference;
40
+ to: DOMReference;
41
+ direction: ParabolaDirection;
42
+ showDelay: number;
43
+ hideDelay: number;
44
+ onShow: ((parabola: Parabola) => void) | null;
45
+ onHidden: ((parabola: Parabola) => void) | null;
46
+ }
47
+
48
+ interface ParabolaPath {
49
+ startX: number;
50
+ startY: number;
51
+ endX: number;
52
+ endY: number;
53
+ }
54
+
55
+ const DEFAULT_BALL: ParabolaBallOptions = {
56
+ color: 'var(--primary, #3e63dd)',
57
+ size: '10px',
58
+ };
59
+
60
+ const DEFAULT_CLASS_NAMES: ParabolaClassNames = {
61
+ ball: 'parabola-ball',
62
+ };
63
+
64
+ const ELEMENT_REF_RULE = {
65
+ validate: (value: unknown) =>
66
+ value == null ||
67
+ typeof value === 'string' ||
68
+ Array.isArray(value) ||
69
+ isNode(value),
70
+ message: 'expects Element, Node, selector or JSX node.',
71
+ };
72
+
73
+ const PARABOLA_OPTIONS_SCHEMA = {
74
+ ball: {
75
+ default: DEFAULT_BALL,
76
+ type: 'object',
77
+ normalize: (value: unknown) => ({
78
+ ...DEFAULT_BALL,
79
+ ...(value && typeof value === 'object' ? value : {}),
80
+ }),
81
+ validate: (value: unknown) =>
82
+ !!value &&
83
+ typeof value === 'object' &&
84
+ typeof (value as ParabolaBallOptions).color === 'string' &&
85
+ typeof (value as ParabolaBallOptions).size === 'string',
86
+ message:
87
+ 'expects an object with two string properties: "color" and "size".',
88
+ },
89
+ className: {
90
+ default: DEFAULT_CLASS_NAMES,
91
+ type: 'object',
92
+ normalize: (value: unknown) => ({
93
+ ...DEFAULT_CLASS_NAMES,
94
+ ...(value && typeof value === 'object' ? value : {}),
95
+ }),
96
+ },
97
+ from: { default: null, ...ELEMENT_REF_RULE },
98
+ to: { default: null, ...ELEMENT_REF_RULE },
99
+ direction: {
100
+ default: 'center',
101
+ type: 'string',
102
+ enum: ['center', 'top-right', 'top-left', 'bottom-right', 'bottom-left'],
103
+ },
104
+ showDelay: { default: 0, type: 'number' },
105
+ hideDelay: { default: 0, type: 'number' },
106
+ onShow: { default: null, types: ['function', 'null'] },
107
+ onHidden: { default: null, types: ['function', 'null'] },
108
+ } satisfies ResolveSchema<ParabolaOptions>;
109
+
110
+ function normalizeOptions(input: ParabolaOptions): ResolvedParabolaOptions {
111
+ const options = resolveProps(
112
+ input,
113
+ PARABOLA_OPTIONS_SCHEMA,
114
+ 'Parabola.options'
115
+ );
116
+ return {
117
+ ball: options.ball as ParabolaBallOptions,
118
+ className: options.className as ParabolaClassNames,
119
+ from: options.from as DOMReference,
120
+ to: options.to as DOMReference,
121
+ direction: options.direction as ParabolaDirection,
122
+ showDelay: options.showDelay as number,
123
+ hideDelay: options.hideDelay as number,
124
+ onShow: options.onShow as ResolvedParabolaOptions['onShow'],
125
+ onHidden: options.onHidden as ResolvedParabolaOptions['onHidden'],
126
+ };
127
+ }
128
+
129
+ /**
130
+ * 抛物线动画组件。
131
+ *
132
+ * 用于实现“加入购物车”等从一个元素飞向另一个元素的小球动画。
133
+ */
134
+ export class Parabola {
135
+ options: ResolvedParabolaOptions;
136
+ hidden: boolean;
137
+ _ball: HTMLElement | null;
138
+ _animationId: number | null;
139
+ _fromEl: Element | null;
140
+ _toEl: Element | null;
141
+ private showTimerId: ReturnType<typeof setTimeout> | null;
142
+
143
+ /**
144
+ * 创建抛物线动画实例。
145
+ * @param {ParabolaOptions} options 动画配置。
146
+ */
147
+ constructor(options: ParabolaOptions = {}) {
148
+ this.options = normalizeOptions(options);
149
+ this.hidden = false;
150
+ this._ball = null;
151
+ this._animationId = null;
152
+ this._fromEl = resolveElement(this.options.from);
153
+ this._toEl = resolveElement(this.options.to);
154
+ this.showTimerId = null;
155
+
156
+ this.createBall();
157
+ }
158
+
159
+ private createBall(): void {
160
+ if (this.hidden || this._ball) return;
161
+
162
+ const { color, size } = this.options.ball;
163
+
164
+ this._ball = jsx('div', {
165
+ className: this.options.className.ball,
166
+ 'data-parabola': 'ball',
167
+ style: {
168
+ backgroundColor: color,
169
+ width: size,
170
+ height: size,
171
+ position: 'fixed',
172
+ borderRadius: '50%',
173
+ transform: 'translate(-50%, -50%)',
174
+ pointerEvents: 'none',
175
+ zIndex: '9999',
176
+ opacity: '1',
177
+ },
178
+ }) as HTMLElement;
179
+ document.body.appendChild(this._ball);
180
+ }
181
+
182
+ private calculatePath(): ParabolaPath | null {
183
+ const fromRect = this._fromEl?.getBoundingClientRect();
184
+ const toRect = this._toEl?.getBoundingClientRect();
185
+
186
+ if (!fromRect || !toRect) return null;
187
+
188
+ const { left: fL, top: fT, width: fW, height: fH } = fromRect;
189
+ const { left: tL, top: tT, width: tW, height: tH } = toRect;
190
+
191
+ let startX: number;
192
+ let startY: number;
193
+ switch (this.options.direction) {
194
+ case 'top-left':
195
+ startX = fL + fW * 0.2;
196
+ startY = fT + fH * 0.2;
197
+ break;
198
+ case 'top-right':
199
+ startX = fL + fW * 0.8;
200
+ startY = fT + fH * 0.2;
201
+ break;
202
+ case 'bottom-left':
203
+ startX = fL + fW * 0.2;
204
+ startY = fT + fH * 0.8;
205
+ break;
206
+ case 'bottom-right':
207
+ startX = fL + fW * 0.8;
208
+ startY = fT + fH * 0.8;
209
+ break;
210
+ default:
211
+ startX = fL + fW / 2;
212
+ startY = fT + fH / 2;
213
+ }
214
+
215
+ const endX = tL + tW / 2;
216
+ const endY = tT + tH / 2;
217
+
218
+ return { startX, startY, endX, endY };
219
+ }
220
+
221
+ private easeOutCubic(t: number): number {
222
+ return 1 - (1 - t) ** 3;
223
+ }
224
+
225
+ private animate(
226
+ startX: number,
227
+ startY: number,
228
+ endX: number,
229
+ endY: number,
230
+ duration = 800
231
+ ): void {
232
+ if (this.hidden || !this._ball) return;
233
+
234
+ const startTime = performance.now();
235
+ const deltaX = endX - startX;
236
+ const deltaY = endY - startY;
237
+ const peakOffset = -100;
238
+
239
+ const step = (currentTime: number): void => {
240
+ if (this.hidden || !this._ball) return;
241
+
242
+ const elapsed = currentTime - startTime;
243
+ const progress = Math.min(elapsed / duration, 1);
244
+ const eased = this.easeOutCubic(progress);
245
+
246
+ const currentX = startX + deltaX * eased;
247
+ const currentY =
248
+ startY + peakOffset * Math.sin(Math.PI * eased) + deltaY * eased;
249
+
250
+ const opacity = 1 - progress;
251
+
252
+ this._ball.style.left = `${currentX}px`;
253
+ this._ball.style.top = `${currentY}px`;
254
+ this._ball.style.opacity = `${opacity}`;
255
+ this._ball.style.transform = `translate(-50%, -50%) scale(${1 - eased * 0.3})`;
256
+
257
+ if (progress < 1) {
258
+ this._animationId = requestAnimationFrame(step);
259
+ } else {
260
+ this.destroy();
261
+ }
262
+ };
263
+
264
+ this._animationId = requestAnimationFrame(step);
265
+ }
266
+
267
+ /**
268
+ * 开始播放动画。
269
+ * @returns {Promise<boolean>} 成功开始动画时返回 true;元素缺失或已销毁时返回 false。
270
+ */
271
+ show(): Promise<boolean> {
272
+ if (this.hidden) return Promise.resolve(false);
273
+
274
+ return new Promise((resolve) => {
275
+ this.showTimerId = setTimeout(() => {
276
+ this.showTimerId = null;
277
+ if (this.hidden || !this._ball) {
278
+ resolve(false);
279
+ return;
280
+ }
281
+
282
+ const path = this.calculatePath();
283
+ if (!path) {
284
+ this.destroy();
285
+ resolve(false);
286
+ return;
287
+ }
288
+
289
+ this._ball.style.left = `${path.startX}px`;
290
+ this._ball.style.top = `${path.startY}px`;
291
+
292
+ this.options.onShow?.(this);
293
+
294
+ this.animate(path.startX, path.startY, path.endX, path.endY);
295
+ resolve(true);
296
+ }, this.options.showDelay);
297
+ });
298
+ }
299
+
300
+ /**
301
+ * 销毁动画实例并清理 DOM、定时器和动画帧。
302
+ * @returns {void}
303
+ */
304
+ destroy(): void {
305
+ if (this.hidden) return;
306
+
307
+ this._ball?.remove();
308
+
309
+ if (this.showTimerId) {
310
+ clearTimeout(this.showTimerId);
311
+ this.showTimerId = null;
312
+ }
313
+ if (this._animationId) cancelAnimationFrame(this._animationId);
314
+
315
+ this._ball = null;
316
+ this._fromEl = null;
317
+ this._toEl = null;
318
+ this._animationId = null;
319
+ this.hidden = true;
320
+
321
+ this.options.onHidden?.(this);
322
+ }
323
+
324
+ /**
325
+ * show 的语义化别名。
326
+ * @returns {Promise<boolean>}
327
+ */
328
+ start(): Promise<boolean> {
329
+ return this.show();
330
+ }
331
+ }
332
+
333
+ export function createParabola(options: ParabolaOptions = {}): Parabola {
334
+ return new Parabola(options);
335
+ }
@@ -1,12 +1,65 @@
1
1
  import { createDeepStore } from 'vanilla-signal';
2
2
 
3
- import Component from '../core/Component.js';
4
- import { resolveProps } from '../utilities/core.js';
5
- import {
6
- isElement,
7
- requireContainer,
8
- requireRenderDOM,
9
- } from '../utilities/dom.js';
3
+ import Component, {
4
+ type ComponentDOM,
5
+ type ComponentRuntime,
6
+ } from '../core/Component.ts';
7
+ import { type ResolveSchema, resolveProps } from '../utilities/core.ts';
8
+ import { type DOMReference, all, requireContainer } from '../utilities/dom.ts';
9
+
10
+ export type StickyOverflow = 'destroy' | 'ignore';
11
+
12
+ export interface StickyProps extends Record<string, unknown> {
13
+ target?: DOMReference;
14
+ parent?: DOMReference;
15
+ max?: number;
16
+ top?: number;
17
+ gap?: number;
18
+ overflow?: StickyOverflow;
19
+ onUpdate?: ((sticky: Sticky) => void) | null;
20
+ }
21
+
22
+ interface ResolvedStickyProps extends Record<string, unknown> {
23
+ target: DOMReference;
24
+ parent: DOMReference;
25
+ max: number;
26
+ top: number;
27
+ gap: number;
28
+ overflow: StickyOverflow;
29
+ onUpdate: ((sticky: Sticky) => void) | null;
30
+ }
31
+
32
+ export interface StickyStateItem {
33
+ element: HTMLElement;
34
+ top: number;
35
+ }
36
+
37
+ interface StickyState extends Record<string, unknown> {
38
+ count: number;
39
+ top: number;
40
+ items: StickyStateItem[];
41
+ }
42
+
43
+ interface StickyDOM extends ComponentDOM {
44
+ root: Element | null;
45
+ parent: Element | null;
46
+ targets: HTMLElement[];
47
+ }
48
+
49
+ interface StickyRuntimeItem {
50
+ element: HTMLElement;
51
+ top: number;
52
+ originalPosition: string;
53
+ originalTop: string;
54
+ originalZIndex: string;
55
+ }
56
+
57
+ interface StickyRuntime extends ComponentRuntime {
58
+ active: boolean;
59
+ built: boolean;
60
+ ignored: boolean;
61
+ items: StickyRuntimeItem[];
62
+ }
10
63
 
11
64
  const STICKY_PROPS_SCHEMA = {
12
65
  target: { default: null },
@@ -14,19 +67,20 @@ const STICKY_PROPS_SCHEMA = {
14
67
  max: {
15
68
  default: 10,
16
69
  type: 'number',
17
- validate: (value) => Number.isInteger(value) && value > 0,
70
+ validate: (value) =>
71
+ typeof value === 'number' && Number.isInteger(value) && value > 0,
18
72
  message: 'expects a positive integer.',
19
73
  },
20
74
  top: {
21
75
  default: 16,
22
76
  type: 'number',
23
- validate: (value) => value >= 0,
77
+ validate: (value) => typeof value === 'number' && value >= 0,
24
78
  message: 'expects a positive number or 0.',
25
79
  },
26
80
  gap: {
27
81
  default: 16,
28
82
  type: 'number',
29
- validate: (value) => value >= 0,
83
+ validate: (value) => typeof value === 'number' && value >= 0,
30
84
  message: 'expects a positive number or 0.',
31
85
  },
32
86
  overflow: {
@@ -35,44 +89,37 @@ const STICKY_PROPS_SCHEMA = {
35
89
  enum: ['destroy', 'ignore'],
36
90
  },
37
91
  onUpdate: { default: null, types: ['function', 'null'] },
38
- };
39
-
40
- /**
41
- * 对元素数组去重,并保持原始顺序。
42
- * @param {Element[]} elements 元素数组。
43
- * @returns {Element[]}
44
- */
45
- function uniqueElements(elements) {
46
- return Array.from(new Set(elements.filter(isElement)));
92
+ } satisfies ResolveSchema<StickyProps>;
93
+
94
+ function normalizeProps(input: StickyProps): ResolvedStickyProps {
95
+ const props = resolveProps(input, STICKY_PROPS_SCHEMA, 'Sticky.props');
96
+ return {
97
+ target: props.target as DOMReference,
98
+ parent: props.parent as DOMReference,
99
+ max: props.max as number,
100
+ top: props.top as number,
101
+ gap: props.gap as number,
102
+ overflow: props.overflow as StickyOverflow,
103
+ onUpdate: props.onUpdate as ResolvedStickyProps['onUpdate'],
104
+ };
47
105
  }
48
106
 
49
- /**
50
- * 判断元素是否在指定父级中。
51
- * @param {Element} element 目标元素。
52
- * @param {Element|null} parent 父级。
53
- * @returns {boolean}
54
- */
55
- function isWithinParent(element, parent) {
56
- return !parent || element === parent || parent.contains(element);
107
+ function uniqueElements(elements: readonly Node[]): HTMLElement[] {
108
+ const htmlElements = elements.filter(
109
+ (element): element is HTMLElement => element instanceof HTMLElement
110
+ );
111
+ return Array.from(new Set(htmlElements));
57
112
  }
58
113
 
59
- /**
60
- * 归一化 parent 输入。
61
- * @param {Element|Node|string|null|false} parent 父级输入。
62
- * @returns {Element|null}
63
- */
64
- function resolveParent(parent) {
114
+ function resolveParent(parent: DOMReference): Element | null {
65
115
  if (parent === false || parent == null) return null;
66
116
  return requireContainer(parent, 'Sticky.parent', 'element');
67
117
  }
68
118
 
69
- /**
70
- * 在 parent 作用域内解析 sticky 目标。
71
- * @param {Element|Node|string|Array|null|false} target 目标输入。
72
- * @param {Element|null} parent 父级。
73
- * @returns {Element[]}
74
- */
75
- function resolveTarget(target, parent) {
119
+ function resolveTarget(
120
+ target: DOMReference,
121
+ parent: Element | null
122
+ ): HTMLElement[] {
76
123
  if (target === false || target == null) return [];
77
124
 
78
125
  if (typeof target === 'string') {
@@ -80,21 +127,20 @@ function resolveTarget(target, parent) {
80
127
  return uniqueElements(requireContainer(target, 'Sticky.target', 'array'));
81
128
  }
82
129
 
83
- const elements = uniqueElements(
84
- Array.from(parent.querySelectorAll(target))
85
- );
130
+ const elements = uniqueElements(all<HTMLElement>(target, parent));
86
131
  if (elements.length === 0) {
87
132
  throw new Error('Sticky.target: target not found.');
88
133
  }
89
134
  return elements;
90
135
  }
91
136
 
92
- const nodes = requireContainer(target, 'Sticky.target', 'array');
93
- const elements = uniqueElements(nodes);
94
-
137
+ const elements = uniqueElements(
138
+ requireContainer(target, 'Sticky.target', 'array')
139
+ );
95
140
  if (!parent) return elements;
96
- const scopedElements = elements.filter((element) =>
97
- isWithinParent(element, parent)
141
+
142
+ const scopedElements = elements.filter(
143
+ (element) => element === parent || parent.contains(element)
98
144
  );
99
145
  if (scopedElements.length === 0) {
100
146
  throw new Error('Sticky.target: target not found in parent.');
@@ -102,29 +148,25 @@ function resolveTarget(target, parent) {
102
148
  return scopedElements;
103
149
  }
104
150
 
105
- /**
106
- * 读取元素 offsetHeight。
107
- * @param {Element} element 目标元素。
108
- * @returns {number}
109
- */
110
- function getElementHeight(element) {
111
- const height = element.offsetHeight;
112
- return Number.isFinite(height) ? height : 0;
113
- }
114
-
115
151
  /**
116
152
  * Sticky 吸附组件。
117
153
  *
118
154
  * 用于给一个或多个元素应用 `position: sticky`,并按顺序计算 `top`
119
155
  * 偏移,适合页面侧边栏中多个 widget 的堆叠吸附场景。
120
156
  */
121
- export class Sticky extends Component {
157
+ export class Sticky extends Component<
158
+ ResolvedStickyProps,
159
+ StickyState,
160
+ StickyDOM
161
+ > {
162
+ declare runtime: StickyRuntime;
163
+
122
164
  /**
123
165
  * 创建 Sticky 实例。
124
166
  * @param {object} [input={}] Sticky 配置。
125
167
  */
126
- constructor(input = {}) {
127
- const props = resolveProps(input, STICKY_PROPS_SCHEMA, 'Sticky.props');
168
+ constructor(input: StickyProps = {}) {
169
+ const props = normalizeProps(input);
128
170
  super(props);
129
171
 
130
172
  this.dom.parent = null;
@@ -146,34 +188,28 @@ export class Sticky extends Component {
146
188
  * 构建 Sticky 行为并应用样式。
147
189
  * @returns {Sticky} 当前实例。
148
190
  */
149
- build() {
191
+ build(): this {
150
192
  if (this.runtime.destroyed)
151
193
  throw new Error('Sticky.build: instance destroyed');
152
194
  if (this.runtime.built) return this;
153
195
 
154
- requireRenderDOM('Sticky');
155
-
156
196
  this.init(this.props);
157
197
 
158
198
  this.dom.parent = resolveParent(this.props.parent);
159
199
  this.dom.targets = resolveTarget(this.props.target, this.dom.parent);
160
- this.dom.targets = this._resolveOverflow(this.dom.targets);
200
+ this.dom.targets = this.resolveOverflow(this.dom.targets);
161
201
 
162
202
  this.runtime.built = true;
163
203
 
164
204
  if (this.dom.targets.length === 0) return this;
165
205
 
166
206
  this.runtime.active = true;
167
- this._captureItems();
168
- this._apply();
207
+ this.captureItems();
208
+ this.apply();
169
209
  return this;
170
210
  }
171
211
 
172
- /**
173
- * 按当前实例的 targets 创建运行时条目。
174
- * @private
175
- */
176
- _captureItems() {
212
+ private captureItems(): void {
177
213
  this.runtime.items = this.dom.targets.map((element) => ({
178
214
  element,
179
215
  top: this.props.top,
@@ -183,13 +219,7 @@ export class Sticky extends Component {
183
219
  }));
184
220
  }
185
221
 
186
- /**
187
- * 按 max/overflow 处理当前实例的目标集合。
188
- * @param {Element[]} targets 目标元素集合。
189
- * @returns {Element[]}
190
- * @private
191
- */
192
- _resolveOverflow(targets) {
222
+ private resolveOverflow(targets: HTMLElement[]): HTMLElement[] {
193
223
  const { max, overflow } = this.props;
194
224
  if (targets.length <= max) return targets;
195
225
 
@@ -197,15 +227,9 @@ export class Sticky extends Component {
197
227
  return overflow === 'ignore' ? [] : targets.slice(-max);
198
228
  }
199
229
 
200
- /**
201
- * 对本实例重新计算 sticky 偏移。
202
- * @param {number} [startTop=this.props.top] 起始 top。
203
- * @returns {number} 当前实例处理后的下一个 top。
204
- * @private
205
- */
206
- _apply(startTop = this.props.top) {
230
+ private apply(startTop = this.props.top): number {
207
231
  let nextTop = startTop;
208
- const stateItems = [];
232
+ const stateItems: StickyStateItem[] = [];
209
233
 
210
234
  for (const item of this.runtime.items) {
211
235
  item.top = nextTop;
@@ -213,7 +237,7 @@ export class Sticky extends Component {
213
237
  item.element.style.top = `${nextTop}px`;
214
238
 
215
239
  stateItems.push({ element: item.element, top: nextTop });
216
- nextTop += getElementHeight(item.element) + this.props.gap;
240
+ nextTop += item.element.offsetHeight + this.props.gap;
217
241
  }
218
242
 
219
243
  this.setState({
@@ -229,11 +253,7 @@ export class Sticky extends Component {
229
253
  return nextTop;
230
254
  }
231
255
 
232
- /**
233
- * 恢复本实例管理元素的原始内联样式。
234
- * @private
235
- */
236
- _restore() {
256
+ private restore(): void {
237
257
  for (const item of this.runtime.items) {
238
258
  item.element.style.position = item.originalPosition;
239
259
  item.element.style.top = item.originalTop;
@@ -245,20 +265,25 @@ export class Sticky extends Component {
245
265
  * 重新计算当前实例内所有 sticky 元素的 top。
246
266
  * @returns {Sticky} 当前实例。
247
267
  */
248
- refresh() {
268
+ refresh(): this {
249
269
  if (this.runtime.destroyed || !this.runtime.built || !this.runtime.active) {
250
270
  return this;
251
271
  }
252
- this._apply();
272
+ this.apply();
253
273
  return this;
254
274
  }
255
275
 
276
+ destroy(): void {
277
+ if (this.runtime.destroyed) return;
278
+ this.restore();
279
+ super.destroy();
280
+ }
281
+
256
282
  /**
257
283
  * 销毁实例并恢复被管理元素的原始样式。
258
284
  * @private
259
285
  */
260
- onDestroy() {
261
- this._restore();
286
+ protected onDestroy(): void {
262
287
  this.runtime.active = false;
263
288
  this.runtime.built = false;
264
289
  this.runtime.items = [];
@@ -266,6 +291,6 @@ export class Sticky extends Component {
266
291
  }
267
292
  }
268
293
 
269
- export function createSticky(props = {}) {
294
+ export function createSticky(props: StickyProps = {}): Sticky {
270
295
  return new Sticky(props);
271
296
  }