vanilla-jui 1.3.8 → 1.3.10
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.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +3 -2
- package/src/components/accordion.js +373 -0
- package/src/components/components.js +0 -0
- package/src/components/drop.js +437 -0
- package/src/components/flow.js +1133 -0
- package/src/components/form.js +585 -0
- package/src/components/icons.js +83 -0
- package/src/components/menu.js +422 -0
- package/src/components/modal.js +1174 -0
- package/src/components/offcanvas.js +332 -0
- package/src/components/pagination.js +536 -0
- package/src/components/parabola.js +295 -0
- package/src/components/sticky.js +271 -0
- package/src/components/swiper.js +1123 -0
- package/src/components/tabs.js +746 -0
- package/src/components/theme.js +353 -0
- package/src/components/toast.js +329 -0
- package/src/components/toc.js +226 -0
- package/src/components/tooltip.js +86 -0
- package/src/components/validator.js +586 -0
- package/src/core/Component.js +248 -0
- package/src/css/accordion.css +72 -0
- package/src/css/animation.css +177 -0
- package/src/css/avatar.css +62 -0
- package/src/css/badge.css +66 -0
- package/src/css/breadcrumb.css +106 -0
- package/src/css/button.css +296 -0
- package/src/css/card.css +151 -0
- package/src/css/color.css +1176 -0
- package/src/css/color_p3.css +1184 -0
- package/src/css/components.css +26 -0
- package/src/css/content.css +716 -0
- package/src/css/divider.css +9 -0
- package/src/css/drop.css +28 -0
- package/src/css/flow.css +86 -0
- package/src/css/font.css +23 -0
- package/src/css/form.css +919 -0
- package/src/css/gradient.css +211 -0
- package/src/css/icon.css +63 -0
- package/src/css/index.css +12 -0
- package/src/css/layout.css +782 -0
- package/src/css/loading.css +38 -0
- package/src/css/menu.css +564 -0
- package/src/css/modal.css +111 -0
- package/src/css/offcanvas.css +104 -0
- package/src/css/pagination.css +25 -0
- package/src/css/popup.css +69 -0
- package/src/css/skeleton.css +76 -0
- package/src/css/status.css +56 -0
- package/src/css/swiper.css +160 -0
- package/src/css/table.css +52 -0
- package/src/css/tabs.css +95 -0
- package/src/css/tag.css +98 -0
- package/src/css/theme-palette.css +59 -0
- package/src/css/tip.css +84 -0
- package/src/css/toast.css +154 -0
- package/src/css/toast.min.css +1 -0
- package/src/css/toc.css +38 -0
- package/src/css/tokens.css +1324 -0
- package/src/css/toolbar.css +115 -0
- package/src/css/tooltip.css +19 -0
- package/src/icons/index.js +30 -0
- package/src/index.js +45 -0
- package/src/locales/index.js +45 -0
- package/src/locales/index.min.js +1 -0
- package/src/utilities/browser.js +111 -0
- package/src/utilities/cache.js +159 -0
- package/src/utilities/core.js +502 -0
- package/src/utilities/debug.js +103 -0
- package/src/utilities/dom.js +313 -0
- package/src/utilities/events.js +69 -0
- package/src/utilities/http.js +30 -0
- package/src/utilities/legacy-dom.js +179 -0
- package/src/utilities/storage.js +46 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { jsx } from 'vanilla-signal';
|
|
2
|
+
|
|
3
|
+
import { resolveProps, timer } from '../utilities/core.js';
|
|
4
|
+
import { isNode, requireRenderDOM, resolveElement } from '../utilities/dom.js';
|
|
5
|
+
|
|
6
|
+
const ELEMENT_REF_RULE = {
|
|
7
|
+
validate: (value) =>
|
|
8
|
+
value == null ||
|
|
9
|
+
typeof value === 'string' ||
|
|
10
|
+
Array.isArray(value) ||
|
|
11
|
+
isNode(value),
|
|
12
|
+
message: 'expects Element, Node, selector or JSX node.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PARABOLA_OPTIONS_SCHEMA = {
|
|
16
|
+
ball: {
|
|
17
|
+
default: {
|
|
18
|
+
color: 'var(--primary, #3e63dd)',
|
|
19
|
+
size: '10px',
|
|
20
|
+
},
|
|
21
|
+
type: 'object',
|
|
22
|
+
validate: (value) =>
|
|
23
|
+
value &&
|
|
24
|
+
Object.keys(value).length === 2 &&
|
|
25
|
+
typeof value.color === 'string' &&
|
|
26
|
+
typeof value.size === 'string',
|
|
27
|
+
message:
|
|
28
|
+
'expects an object with two string properties: "color" and "size".',
|
|
29
|
+
},
|
|
30
|
+
from: { default: null, ...ELEMENT_REF_RULE },
|
|
31
|
+
to: { default: null, ...ELEMENT_REF_RULE },
|
|
32
|
+
direction: {
|
|
33
|
+
default: 'center',
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['center', 'top-right', 'top-left', 'bottom-right', 'bottom-left'],
|
|
36
|
+
},
|
|
37
|
+
showDelay: { default: 0, type: 'number' },
|
|
38
|
+
hideDelay: { default: 0, type: 'number' },
|
|
39
|
+
onShow: { default: null, types: ['function', 'null'] },
|
|
40
|
+
onHidden: { default: null, types: ['function', 'null'] },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} ParabolaBallOptions
|
|
45
|
+
* @property {string} color 小球颜色。
|
|
46
|
+
* @property {string} size 小球尺寸,例如 `10px`。
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {object} ParabolaOptions
|
|
51
|
+
* @property {ParabolaBallOptions} [ball] 小球样式配置。
|
|
52
|
+
* @property {Element|Node|string|Array} from 起点元素、选择器或 JSX/h 返回节点。
|
|
53
|
+
* @property {Element|Node|string|Array} to 终点元素、选择器或 JSX/h 返回节点。
|
|
54
|
+
* @property {"center"|"top-right"|"top-left"|"bottom-right"|"bottom-left"} [direction="center"] 起点取样位置。
|
|
55
|
+
* @property {number} [showDelay=0] 开始动画前的延迟,单位毫秒。
|
|
56
|
+
* @property {number} [hideDelay=0] 预留隐藏延迟配置。
|
|
57
|
+
* @property {Function|null} [onShow] 动画开始前回调。
|
|
58
|
+
* @property {Function|null} [onHidden] 动画结束或销毁后回调。
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 抛物线动画组件。
|
|
63
|
+
*
|
|
64
|
+
* 用于实现“加入购物车”等从一个元素飞向另一个元素的小球动画。
|
|
65
|
+
*/
|
|
66
|
+
export class Parabola {
|
|
67
|
+
/**
|
|
68
|
+
* 创建抛物线动画实例。
|
|
69
|
+
* @param {ParabolaOptions} options 动画配置。
|
|
70
|
+
*/
|
|
71
|
+
constructor(options) {
|
|
72
|
+
requireRenderDOM('Parabola');
|
|
73
|
+
|
|
74
|
+
this.options = resolveProps(
|
|
75
|
+
options,
|
|
76
|
+
PARABOLA_OPTIONS_SCHEMA,
|
|
77
|
+
'Parabola.options'
|
|
78
|
+
);
|
|
79
|
+
this._init(this.options);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 初始化动画状态并创建小球。
|
|
84
|
+
* @private
|
|
85
|
+
* @param {ParabolaOptions} options 已归一化配置。
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
_init(options) {
|
|
89
|
+
this.hidden = false;
|
|
90
|
+
|
|
91
|
+
this._ball = null;
|
|
92
|
+
this._animationId = null;
|
|
93
|
+
|
|
94
|
+
const fromEl = resolveElement(options.from, 'Parabola.from');
|
|
95
|
+
const toEl = resolveElement(options.to, 'Parabola.to');
|
|
96
|
+
|
|
97
|
+
this._fromEl = fromEl;
|
|
98
|
+
this._toEl = toEl;
|
|
99
|
+
|
|
100
|
+
this._createBall(options);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 创建动画小球节点。
|
|
105
|
+
* @private
|
|
106
|
+
* @param {ParabolaOptions} options 已归一化配置。
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
_createBall(options) {
|
|
110
|
+
if (this.hidden || this._ball) return;
|
|
111
|
+
|
|
112
|
+
const { color, size } = options.ball;
|
|
113
|
+
|
|
114
|
+
this._ball = jsx('div', {
|
|
115
|
+
className: 'parabola-ball',
|
|
116
|
+
style: {
|
|
117
|
+
backgroundColor: color,
|
|
118
|
+
width: size,
|
|
119
|
+
height: size,
|
|
120
|
+
position: 'fixed',
|
|
121
|
+
borderRadius: '50%',
|
|
122
|
+
transform: 'translate(-50%, -50%)',
|
|
123
|
+
pointerEvents: 'none',
|
|
124
|
+
zIndex: '9999',
|
|
125
|
+
opacity: '1',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
document.body.appendChild(this._ball);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 计算动画起点和终点坐标。
|
|
133
|
+
* @private
|
|
134
|
+
* @param {ParabolaOptions} options 已归一化配置。
|
|
135
|
+
* @returns {{startX:number,startY:number,endX:number,endY:number}|null}
|
|
136
|
+
*/
|
|
137
|
+
_calculatePath(options) {
|
|
138
|
+
const fromRect = this._fromEl?.getBoundingClientRect();
|
|
139
|
+
const toRect = this._toEl?.getBoundingClientRect();
|
|
140
|
+
|
|
141
|
+
if (!fromRect || !toRect) return null;
|
|
142
|
+
|
|
143
|
+
const { left: fL, top: fT, width: fW, height: fH } = fromRect;
|
|
144
|
+
const { left: tL, top: tT, width: tW, height: tH } = toRect;
|
|
145
|
+
|
|
146
|
+
let startX, startY;
|
|
147
|
+
switch (options.direction) {
|
|
148
|
+
case 'top-left':
|
|
149
|
+
startX = fL + fW * 0.2;
|
|
150
|
+
startY = fT + fH * 0.2;
|
|
151
|
+
break;
|
|
152
|
+
case 'top-right':
|
|
153
|
+
startX = fL + fW * 0.8;
|
|
154
|
+
startY = fT + fH * 0.2;
|
|
155
|
+
break;
|
|
156
|
+
case 'bottom-left':
|
|
157
|
+
startX = fL + fW * 0.2;
|
|
158
|
+
startY = fT + fH * 0.8;
|
|
159
|
+
break;
|
|
160
|
+
case 'bottom-right':
|
|
161
|
+
startX = fL + fW * 0.8;
|
|
162
|
+
startY = fT + fH * 0.8;
|
|
163
|
+
break;
|
|
164
|
+
default: // 'center'
|
|
165
|
+
startX = fL + fW / 2;
|
|
166
|
+
startY = fT + fH / 2;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const endX = tL + tW / 2;
|
|
170
|
+
const endY = tT + tH / 2;
|
|
171
|
+
|
|
172
|
+
return { startX, startY, endX, endY };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 三次缓出曲线。
|
|
177
|
+
* @private
|
|
178
|
+
* @param {number} t 动画进度,范围 0 到 1。
|
|
179
|
+
* @returns {number}
|
|
180
|
+
*/
|
|
181
|
+
_easeOutCubic(t) {
|
|
182
|
+
return 1 - (1 - t) ** 3;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 执行抛物线动画。
|
|
187
|
+
* @private
|
|
188
|
+
* @param {number} startX 起点 x 坐标。
|
|
189
|
+
* @param {number} startY 起点 y 坐标。
|
|
190
|
+
* @param {number} endX 终点 x 坐标。
|
|
191
|
+
* @param {number} endY 终点 y 坐标。
|
|
192
|
+
* @param {number} [duration=800] 动画时长,单位毫秒。
|
|
193
|
+
* @returns {void}
|
|
194
|
+
*/
|
|
195
|
+
_animate(startX, startY, endX, endY, duration = 800) {
|
|
196
|
+
if (this.hidden) return;
|
|
197
|
+
|
|
198
|
+
const startTime = performance.now();
|
|
199
|
+
const deltaX = endX - startX;
|
|
200
|
+
const deltaY = endY - startY;
|
|
201
|
+
const peakOffset = -100;
|
|
202
|
+
|
|
203
|
+
const step = (currentTime) => {
|
|
204
|
+
if (this.hidden) return;
|
|
205
|
+
|
|
206
|
+
const elapsed = currentTime - startTime;
|
|
207
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
208
|
+
const eased = this._easeOutCubic(progress);
|
|
209
|
+
|
|
210
|
+
const currentX = startX + deltaX * eased;
|
|
211
|
+
const currentY =
|
|
212
|
+
startY + peakOffset * Math.sin(Math.PI * eased) + deltaY * eased;
|
|
213
|
+
|
|
214
|
+
const opacity = 1 - progress;
|
|
215
|
+
|
|
216
|
+
this._ball.style.left = `${currentX}px`;
|
|
217
|
+
this._ball.style.top = `${currentY}px`;
|
|
218
|
+
this._ball.style.opacity = `${opacity}`;
|
|
219
|
+
this._ball.style.transform = `translate(-50%, -50%) scale(${1 - eased * 0.3})`;
|
|
220
|
+
|
|
221
|
+
if (progress < 1) {
|
|
222
|
+
this._animationId = requestAnimationFrame(step);
|
|
223
|
+
} else {
|
|
224
|
+
this.destroy(); // 动画结束,直接销毁
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
this._animationId = requestAnimationFrame(step);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 开始播放动画。
|
|
233
|
+
* @returns {Promise<boolean>} 成功开始动画时返回 true;元素缺失或已销毁时返回 false。
|
|
234
|
+
*/
|
|
235
|
+
async show() {
|
|
236
|
+
if (this.hidden) return false;
|
|
237
|
+
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
timer.start('show', this.options.showDelay, () => {
|
|
240
|
+
if (this.hidden) {
|
|
241
|
+
resolve(false);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const path = this._calculatePath(this.options);
|
|
246
|
+
if (!path) {
|
|
247
|
+
this.destroy();
|
|
248
|
+
resolve(false);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this._ball.style.left = `${path.startX}px`;
|
|
253
|
+
this._ball.style.top = `${path.startY}px`;
|
|
254
|
+
|
|
255
|
+
if (this.options.onShow) this.options.onShow();
|
|
256
|
+
|
|
257
|
+
this._animate(path.startX, path.startY, path.endX, path.endY);
|
|
258
|
+
resolve(true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 销毁动画实例并清理 DOM、定时器和动画帧。
|
|
265
|
+
* @returns {void}
|
|
266
|
+
*/
|
|
267
|
+
destroy() {
|
|
268
|
+
if (this.hidden) return;
|
|
269
|
+
|
|
270
|
+
this._ball?.remove();
|
|
271
|
+
|
|
272
|
+
timer.cancel('show');
|
|
273
|
+
if (this._animationId) cancelAnimationFrame(this._animationId);
|
|
274
|
+
|
|
275
|
+
this._ball = null;
|
|
276
|
+
this._fromEl = null;
|
|
277
|
+
this._toEl = null;
|
|
278
|
+
|
|
279
|
+
this.hidden = true;
|
|
280
|
+
|
|
281
|
+
if (this.options.onHidden) this.options.onHidden();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* show 的语义化别名。
|
|
286
|
+
* @returns {Promise<boolean>}
|
|
287
|
+
*/
|
|
288
|
+
start() {
|
|
289
|
+
return this.show();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function createParabola(options = {}) {
|
|
294
|
+
return new Parabola(options);
|
|
295
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { createDeepStore } from 'vanilla-signal';
|
|
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';
|
|
10
|
+
|
|
11
|
+
const STICKY_PROPS_SCHEMA = {
|
|
12
|
+
target: { default: null },
|
|
13
|
+
parent: { default: null },
|
|
14
|
+
max: {
|
|
15
|
+
default: 10,
|
|
16
|
+
type: 'number',
|
|
17
|
+
validate: (value) => Number.isInteger(value) && value > 0,
|
|
18
|
+
message: 'expects a positive integer.',
|
|
19
|
+
},
|
|
20
|
+
top: {
|
|
21
|
+
default: 16,
|
|
22
|
+
type: 'number',
|
|
23
|
+
validate: (value) => value >= 0,
|
|
24
|
+
message: 'expects a positive number or 0.',
|
|
25
|
+
},
|
|
26
|
+
gap: {
|
|
27
|
+
default: 16,
|
|
28
|
+
type: 'number',
|
|
29
|
+
validate: (value) => value >= 0,
|
|
30
|
+
message: 'expects a positive number or 0.',
|
|
31
|
+
},
|
|
32
|
+
overflow: {
|
|
33
|
+
default: 'destroy',
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['destroy', 'ignore'],
|
|
36
|
+
},
|
|
37
|
+
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)));
|
|
47
|
+
}
|
|
48
|
+
|
|
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);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 归一化 parent 输入。
|
|
61
|
+
* @param {Element|Node|string|null|false} parent 父级输入。
|
|
62
|
+
* @returns {Element|null}
|
|
63
|
+
*/
|
|
64
|
+
function resolveParent(parent) {
|
|
65
|
+
if (parent === false || parent == null) return null;
|
|
66
|
+
return requireContainer(parent, 'Sticky.parent', 'element');
|
|
67
|
+
}
|
|
68
|
+
|
|
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) {
|
|
76
|
+
if (target === false || target == null) return [];
|
|
77
|
+
|
|
78
|
+
if (typeof target === 'string') {
|
|
79
|
+
if (!parent) {
|
|
80
|
+
return uniqueElements(requireContainer(target, 'Sticky.target', 'array'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const elements = uniqueElements(
|
|
84
|
+
Array.from(parent.querySelectorAll(target))
|
|
85
|
+
);
|
|
86
|
+
if (elements.length === 0) {
|
|
87
|
+
throw new Error('Sticky.target: target not found.');
|
|
88
|
+
}
|
|
89
|
+
return elements;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nodes = requireContainer(target, 'Sticky.target', 'array');
|
|
93
|
+
const elements = uniqueElements(nodes);
|
|
94
|
+
|
|
95
|
+
if (!parent) return elements;
|
|
96
|
+
const scopedElements = elements.filter((element) =>
|
|
97
|
+
isWithinParent(element, parent)
|
|
98
|
+
);
|
|
99
|
+
if (scopedElements.length === 0) {
|
|
100
|
+
throw new Error('Sticky.target: target not found in parent.');
|
|
101
|
+
}
|
|
102
|
+
return scopedElements;
|
|
103
|
+
}
|
|
104
|
+
|
|
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
|
+
/**
|
|
116
|
+
* Sticky 吸附组件。
|
|
117
|
+
*
|
|
118
|
+
* 用于给一个或多个元素应用 `position: sticky`,并按顺序计算 `top`
|
|
119
|
+
* 偏移,适合页面侧边栏中多个 widget 的堆叠吸附场景。
|
|
120
|
+
*/
|
|
121
|
+
export class Sticky extends Component {
|
|
122
|
+
/**
|
|
123
|
+
* 创建 Sticky 实例。
|
|
124
|
+
* @param {object} [input={}] Sticky 配置。
|
|
125
|
+
*/
|
|
126
|
+
constructor(input = {}) {
|
|
127
|
+
const props = resolveProps(input, STICKY_PROPS_SCHEMA, 'Sticky.props');
|
|
128
|
+
super(props);
|
|
129
|
+
|
|
130
|
+
this.dom.parent = null;
|
|
131
|
+
this.dom.targets = [];
|
|
132
|
+
|
|
133
|
+
this.runtime.active = false;
|
|
134
|
+
this.runtime.built = false;
|
|
135
|
+
this.runtime.ignored = false;
|
|
136
|
+
this.runtime.items = [];
|
|
137
|
+
|
|
138
|
+
this.state = createDeepStore({
|
|
139
|
+
count: 0,
|
|
140
|
+
top: props.top,
|
|
141
|
+
items: [],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 构建 Sticky 行为并应用样式。
|
|
147
|
+
* @returns {Sticky} 当前实例。
|
|
148
|
+
*/
|
|
149
|
+
build() {
|
|
150
|
+
if (this.runtime.destroyed)
|
|
151
|
+
throw new Error('Sticky.build: instance destroyed');
|
|
152
|
+
if (this.runtime.built) return this;
|
|
153
|
+
|
|
154
|
+
requireRenderDOM('Sticky');
|
|
155
|
+
|
|
156
|
+
this.init(this.props);
|
|
157
|
+
|
|
158
|
+
this.dom.parent = resolveParent(this.props.parent);
|
|
159
|
+
this.dom.targets = resolveTarget(this.props.target, this.dom.parent);
|
|
160
|
+
this.dom.targets = this._resolveOverflow(this.dom.targets);
|
|
161
|
+
|
|
162
|
+
this.runtime.built = true;
|
|
163
|
+
|
|
164
|
+
if (this.dom.targets.length === 0) return this;
|
|
165
|
+
|
|
166
|
+
this.runtime.active = true;
|
|
167
|
+
this._captureItems();
|
|
168
|
+
this._apply();
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 按当前实例的 targets 创建运行时条目。
|
|
174
|
+
* @private
|
|
175
|
+
*/
|
|
176
|
+
_captureItems() {
|
|
177
|
+
this.runtime.items = this.dom.targets.map((element) => ({
|
|
178
|
+
element,
|
|
179
|
+
top: this.props.top,
|
|
180
|
+
originalPosition: element.style.position,
|
|
181
|
+
originalTop: element.style.top,
|
|
182
|
+
originalZIndex: element.style.zIndex,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 按 max/overflow 处理当前实例的目标集合。
|
|
188
|
+
* @param {Element[]} targets 目标元素集合。
|
|
189
|
+
* @returns {Element[]}
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
_resolveOverflow(targets) {
|
|
193
|
+
const { max, overflow } = this.props;
|
|
194
|
+
if (targets.length <= max) return targets;
|
|
195
|
+
|
|
196
|
+
this.runtime.ignored = overflow === 'ignore';
|
|
197
|
+
return overflow === 'ignore' ? [] : targets.slice(-max);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 对本实例重新计算 sticky 偏移。
|
|
202
|
+
* @param {number} [startTop=this.props.top] 起始 top。
|
|
203
|
+
* @returns {number} 当前实例处理后的下一个 top。
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
_apply(startTop = this.props.top) {
|
|
207
|
+
let nextTop = startTop;
|
|
208
|
+
const stateItems = [];
|
|
209
|
+
|
|
210
|
+
for (const item of this.runtime.items) {
|
|
211
|
+
item.top = nextTop;
|
|
212
|
+
item.element.style.position = 'sticky';
|
|
213
|
+
item.element.style.top = `${nextTop}px`;
|
|
214
|
+
|
|
215
|
+
stateItems.push({ element: item.element, top: nextTop });
|
|
216
|
+
nextTop += getElementHeight(item.element) + this.props.gap;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.setState({
|
|
220
|
+
count: this.runtime.items.length,
|
|
221
|
+
top: this.runtime.items[0]?.top ?? this.props.top,
|
|
222
|
+
items: stateItems,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (typeof this.props.onUpdate === 'function') {
|
|
226
|
+
this.props.onUpdate(this);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return nextTop;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 恢复本实例管理元素的原始内联样式。
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_restore() {
|
|
237
|
+
for (const item of this.runtime.items) {
|
|
238
|
+
item.element.style.position = item.originalPosition;
|
|
239
|
+
item.element.style.top = item.originalTop;
|
|
240
|
+
item.element.style.zIndex = item.originalZIndex;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 重新计算当前实例内所有 sticky 元素的 top。
|
|
246
|
+
* @returns {Sticky} 当前实例。
|
|
247
|
+
*/
|
|
248
|
+
refresh() {
|
|
249
|
+
if (this.runtime.destroyed || !this.runtime.built || !this.runtime.active) {
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
this._apply();
|
|
253
|
+
return this;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 销毁实例并恢复被管理元素的原始样式。
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
onDestroy() {
|
|
261
|
+
this._restore();
|
|
262
|
+
this.runtime.active = false;
|
|
263
|
+
this.runtime.built = false;
|
|
264
|
+
this.runtime.items = [];
|
|
265
|
+
this.dom.targets = [];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createSticky(props = {}) {
|
|
270
|
+
return new Sticky(props);
|
|
271
|
+
}
|