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.
- package/dist/index.d.ts +2927 -0
- package/dist/index.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -3
- package/src/components/accordion.ts +522 -0
- package/src/components/drop.ts +497 -0
- package/src/components/flow.ts +1579 -0
- package/src/components/form.ts +953 -0
- package/src/components/{icons.js → icons.ts} +24 -11
- package/src/components/menu.ts +615 -0
- package/src/components/{modal.js → modal.ts} +560 -237
- package/src/components/offcanvas.ts +496 -0
- package/src/components/{pagination.js → pagination.ts} +251 -76
- package/src/components/parabola.ts +335 -0
- package/src/components/{sticky.js → sticky.ts} +123 -98
- package/src/components/{swiper.js → swiper.ts} +510 -232
- package/src/components/tabs.ts +984 -0
- package/src/components/theme.ts +437 -0
- package/src/components/toast.ts +371 -0
- package/src/components/toc.ts +385 -0
- package/src/components/tooltip.ts +179 -0
- package/src/components/validator.ts +596 -0
- package/src/core/{Component.js → Component.ts} +134 -27
- package/src/css/accordion.css +2 -2
- package/src/css/animation.css +31 -1
- package/src/css/breadcrumb.css +1 -1
- package/src/css/button.css +5 -5
- package/src/css/card.css +7 -7
- package/src/css/color.css +865 -1028
- package/src/css/color_p3.css +871 -1036
- package/src/css/components.css +1 -0
- package/src/css/drop.css +1 -1
- package/src/css/flow.css +1 -1
- package/src/css/form.css +21 -21
- package/src/css/gradient.css +17 -4
- package/src/css/icon.css +17 -1
- package/src/css/index.css +3 -3
- package/src/css/layout.css +1 -1
- package/src/css/menu.css +27 -27
- package/src/css/modal.css +10 -27
- package/src/css/offcanvas.css +2 -2
- package/src/css/pagination.css +1 -1
- package/src/css/popup.css +2 -2
- package/src/css/swiper.css +12 -12
- package/src/css/tabs.css +3 -2
- package/src/css/tag.css +3 -4
- package/src/css/theme-palette.css +6 -6
- package/src/css/tip.css +1 -1
- package/src/css/toast.css +8 -8
- package/src/css/tokens.css +197 -226
- package/src/css/toolbar.css +3 -3
- package/src/css/tooltip.css +1 -1
- package/src/index.ts +37 -0
- package/src/locales/{index.js → index.ts} +1 -2
- package/src/types.d.ts +1 -0
- package/src/utilities/browser.ts +131 -0
- package/src/utilities/cache.ts +57 -0
- package/src/utilities/core.ts +614 -0
- package/src/utilities/dom.ts +468 -0
- package/src/utilities/events.ts +106 -0
- package/src/utilities/http.ts +35 -0
- package/src/utilities/storage.ts +46 -0
- package/src/components/accordion.js +0 -373
- package/src/components/drop.js +0 -437
- package/src/components/flow.js +0 -1133
- package/src/components/form.js +0 -585
- package/src/components/menu.js +0 -422
- package/src/components/offcanvas.js +0 -332
- package/src/components/parabola.js +0 -295
- package/src/components/tabs.js +0 -746
- package/src/components/theme.js +0 -353
- package/src/components/toast.js +0 -329
- package/src/components/toc.js +0 -226
- package/src/components/tooltip.js +0 -86
- package/src/components/validator.js +0 -586
- package/src/css/toast.min.css +0 -1
- package/src/index.js +0 -45
- package/src/locales/index.min.js +0 -1
- package/src/utilities/browser.js +0 -111
- package/src/utilities/cache.js +0 -159
- package/src/utilities/core.js +0 -502
- package/src/utilities/debug.js +0 -103
- package/src/utilities/dom.js +0 -313
- package/src/utilities/events.js +0 -69
- package/src/utilities/http.js +0 -30
- package/src/utilities/legacy-dom.js +0 -179
- package/src/utilities/storage.js +0 -46
- /package/src/{components/components.js → css/themes/index.css} +0 -0
- /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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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) =>
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
137
|
+
const elements = uniqueElements(
|
|
138
|
+
requireContainer(target, 'Sticky.target', 'array')
|
|
139
|
+
);
|
|
95
140
|
if (!parent) return elements;
|
|
96
|
-
|
|
97
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
168
|
-
this.
|
|
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 +=
|
|
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.
|
|
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
|
}
|