vanilla-jui 1.3.11 → 1.4.1
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 +2921 -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 +457 -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 +0 -45
- package/src/css/animation.css +31 -1
- package/src/css/avatar.css +7 -33
- package/src/css/badge.css +11 -59
- package/src/css/breadcrumb.css +9 -32
- package/src/css/button.css +23 -224
- package/src/css/card.css +1 -1
- package/src/css/color.css +863 -1013
- package/src/css/color_p3.css +822 -975
- package/src/css/components.css +1 -0
- package/src/css/drop.css +7 -7
- package/src/css/flow.css +1 -1
- package/src/css/form.css +1 -1
- package/src/css/icon.css +7 -12
- package/src/css/index.css +5 -2
- package/src/css/loading.css +36 -20
- package/src/css/menu.css +1 -1
- package/src/css/modal.css +15 -44
- package/src/css/offcanvas.css +14 -40
- package/src/css/popup.css +4 -4
- package/src/css/status.css +0 -2
- package/src/css/swiper.css +35 -50
- package/src/css/tabs.css +9 -31
- package/src/css/tag.css +16 -77
- package/src/css/themes/default/accordion.css +62 -0
- package/src/css/themes/default/avatar.css +39 -0
- package/src/css/themes/default/badge.css +63 -0
- package/src/css/themes/default/breadcrumb.css +28 -0
- package/src/css/themes/default/button.css +204 -0
- package/src/css/themes/default/drop.css +11 -0
- package/src/css/themes/default/index.css +16 -0
- package/src/css/themes/default/loading.css +23 -0
- package/src/css/themes/default/modal.css +40 -0
- package/src/css/themes/default/offcanvas.css +13 -0
- package/src/css/themes/default/popup.css +7 -0
- package/src/css/themes/default/swiper.css +35 -0
- package/src/css/themes/default/tabs.css +37 -0
- package/src/css/themes/default/tag.css +85 -0
- package/src/css/themes/default/tip.css +49 -0
- package/src/css/themes/default/toast.css +70 -0
- package/src/css/themes/default/tooltip.css +14 -0
- package/src/css/themes/index.css +1 -0
- package/src/css/tip.css +20 -39
- package/src/css/toast.css +18 -80
- package/src/css/tokens.css +8 -3
- package/src/css/tooltip.css +10 -10
- 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/components.js +0 -0
- 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/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/icons/{index.js → index.ts} +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { createDeepStore, jsx } from 'vanilla-signal';
|
|
2
|
+
|
|
3
|
+
import Component, {
|
|
4
|
+
type ComponentDOM,
|
|
5
|
+
type ComponentRuntime,
|
|
6
|
+
} from '../core/Component.ts';
|
|
7
|
+
import {
|
|
8
|
+
type ResolveSchema,
|
|
9
|
+
randomId,
|
|
10
|
+
resolveProps,
|
|
11
|
+
} from '../utilities/core.ts';
|
|
12
|
+
import { type DOMReference, all, requireContainer } from '../utilities/dom.ts';
|
|
13
|
+
|
|
14
|
+
export interface TocClassNames {
|
|
15
|
+
toc: string;
|
|
16
|
+
list: string;
|
|
17
|
+
link: string;
|
|
18
|
+
active: string;
|
|
19
|
+
levelPrefix: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TocClassNameConfig = Partial<TocClassNames>;
|
|
23
|
+
|
|
24
|
+
export interface TocItem {
|
|
25
|
+
id: string;
|
|
26
|
+
text: string;
|
|
27
|
+
level: number;
|
|
28
|
+
element: HTMLHeadingElement;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TocCurrent {
|
|
32
|
+
index: number;
|
|
33
|
+
item: TocItem | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TocProps extends Record<string, unknown> {
|
|
37
|
+
container?: DOMReference;
|
|
38
|
+
target?: DOMReference;
|
|
39
|
+
headings?: string;
|
|
40
|
+
offset?: number;
|
|
41
|
+
className?: TocClassNameConfig;
|
|
42
|
+
onUpdate?: ((item: TocItem | null, index: number, toc: Toc) => void) | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ResolvedTocProps extends Record<string, unknown> {
|
|
46
|
+
container: DOMReference;
|
|
47
|
+
target: DOMReference;
|
|
48
|
+
headings: string;
|
|
49
|
+
offset: number;
|
|
50
|
+
className: TocClassNames;
|
|
51
|
+
onUpdate: ((item: TocItem | null, index: number, toc: Toc) => void) | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface TocState extends Record<string, unknown> {
|
|
55
|
+
items: TocItem[];
|
|
56
|
+
current: TocCurrent;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface TocDOM extends ComponentDOM {
|
|
60
|
+
root: HTMLElement | null;
|
|
61
|
+
container: Element | null;
|
|
62
|
+
target: Element | null;
|
|
63
|
+
list: HTMLElement | null;
|
|
64
|
+
headings: HTMLHeadingElement[];
|
|
65
|
+
links: HTMLAnchorElement[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface TocRuntime extends ComponentRuntime {
|
|
69
|
+
built: boolean;
|
|
70
|
+
ticking: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface TocScrollOptions {
|
|
74
|
+
activeIndex?: number;
|
|
75
|
+
updateHash?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const DEFAULT_CLASS_NAMES: TocClassNames = {
|
|
79
|
+
toc: 'j-toc',
|
|
80
|
+
list: 'toc-list',
|
|
81
|
+
link: 'toc-link',
|
|
82
|
+
active: 'is-active',
|
|
83
|
+
levelPrefix: 'is-level-',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const TOC_PROPS_SCHEMA = {
|
|
87
|
+
container: { default: null },
|
|
88
|
+
target: { default: '.j-content' },
|
|
89
|
+
headings: { default: 'h2, h3', type: 'string' },
|
|
90
|
+
offset: {
|
|
91
|
+
default: 80,
|
|
92
|
+
type: 'number',
|
|
93
|
+
validate: (value) => typeof value === 'number' && value >= 0,
|
|
94
|
+
message: 'expects a positive number or 0.',
|
|
95
|
+
},
|
|
96
|
+
className: {
|
|
97
|
+
default: DEFAULT_CLASS_NAMES,
|
|
98
|
+
type: 'object',
|
|
99
|
+
normalize: (value: unknown) => ({
|
|
100
|
+
...DEFAULT_CLASS_NAMES,
|
|
101
|
+
...(value && typeof value === 'object' ? value : {}),
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
onUpdate: { default: null, types: ['function', 'null'] },
|
|
105
|
+
} satisfies ResolveSchema<TocProps>;
|
|
106
|
+
|
|
107
|
+
const ACTIVE_OFFSET_TOLERANCE = 1;
|
|
108
|
+
|
|
109
|
+
function normalizeProps(input: TocProps): ResolvedTocProps {
|
|
110
|
+
const props = resolveProps(input, TOC_PROPS_SCHEMA, 'Toc.props');
|
|
111
|
+
return {
|
|
112
|
+
container: props.container as DOMReference,
|
|
113
|
+
target: props.target as DOMReference,
|
|
114
|
+
headings: props.headings as string,
|
|
115
|
+
offset: props.offset as number,
|
|
116
|
+
className: props.className as TocClassNames,
|
|
117
|
+
onUpdate: props.onUpdate as ResolvedTocProps['onUpdate'],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveHeadingLevel(element: Element): number {
|
|
122
|
+
const match = /^H([1-6])$/.exec(element.tagName);
|
|
123
|
+
return match ? Number(match[1]) : 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeHeading(element: HTMLHeadingElement, index: number): TocItem {
|
|
127
|
+
if (!element.id) element.id = `toc-${randomId()}-${index}`;
|
|
128
|
+
return {
|
|
129
|
+
id: element.id,
|
|
130
|
+
text: element.textContent || '',
|
|
131
|
+
level: resolveHeadingLevel(element),
|
|
132
|
+
element,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 页面目录组件。
|
|
138
|
+
*
|
|
139
|
+
* 扫描内容区域内的标题,生成锚点列表,并随页面滚动更新 active 状态。
|
|
140
|
+
*/
|
|
141
|
+
export class Toc extends Component<ResolvedTocProps, TocState, TocDOM> {
|
|
142
|
+
declare runtime: TocRuntime;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 创建 Toc 实例。
|
|
146
|
+
* @param {object} [input={}] Toc 配置。
|
|
147
|
+
*/
|
|
148
|
+
constructor(input: TocProps = {}) {
|
|
149
|
+
const props = normalizeProps(input);
|
|
150
|
+
super(props);
|
|
151
|
+
|
|
152
|
+
this.dom.container = null;
|
|
153
|
+
this.dom.target = null;
|
|
154
|
+
this.dom.list = null;
|
|
155
|
+
this.dom.headings = [];
|
|
156
|
+
this.dom.links = [];
|
|
157
|
+
|
|
158
|
+
this.runtime.built = false;
|
|
159
|
+
this.runtime.ticking = false;
|
|
160
|
+
|
|
161
|
+
this.state = createDeepStore({
|
|
162
|
+
items: [],
|
|
163
|
+
current: {
|
|
164
|
+
index: -1,
|
|
165
|
+
item: null,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 构建 Toc DOM 和滚动监听。
|
|
172
|
+
* @returns {Toc} 当前实例。
|
|
173
|
+
*/
|
|
174
|
+
build(): this {
|
|
175
|
+
if (this.runtime.destroyed)
|
|
176
|
+
throw new Error('Toc.build: instance destroyed');
|
|
177
|
+
if (this.runtime.built) return this;
|
|
178
|
+
|
|
179
|
+
this.init(this.props);
|
|
180
|
+
this.dom.container = requireContainer(
|
|
181
|
+
this.props.container,
|
|
182
|
+
'Toc.container'
|
|
183
|
+
);
|
|
184
|
+
this.dom.target = requireContainer(this.props.target, 'Toc.target');
|
|
185
|
+
const root = document.createElement('nav');
|
|
186
|
+
root.className = this.props.className.toc;
|
|
187
|
+
root.dataset.toc = 'root';
|
|
188
|
+
this.root = root;
|
|
189
|
+
this.dom.list = document.createElement('div');
|
|
190
|
+
this.dom.list.className = this.props.className.list;
|
|
191
|
+
this.dom.list.dataset.tocList = 'root';
|
|
192
|
+
root.appendChild(this.dom.list);
|
|
193
|
+
this.dom.container.innerHTML = '';
|
|
194
|
+
this.dom.container.appendChild(root);
|
|
195
|
+
|
|
196
|
+
this.runtime.built = true;
|
|
197
|
+
this.refresh();
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private bindEvents(): void {
|
|
202
|
+
if (!this.dom.list) return;
|
|
203
|
+
this.cleanup.events.on('scroll', window, 'scroll', () => this.onScroll(), {
|
|
204
|
+
passive: true,
|
|
205
|
+
});
|
|
206
|
+
this.cleanup.events.on('click', this.dom.list, 'click', (event) =>
|
|
207
|
+
this.onClick(event)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private onScroll(): void {
|
|
212
|
+
if (this.runtime.ticking) return;
|
|
213
|
+
|
|
214
|
+
requestAnimationFrame(() => {
|
|
215
|
+
this.updateActive();
|
|
216
|
+
this.runtime.ticking = false;
|
|
217
|
+
});
|
|
218
|
+
this.runtime.ticking = true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private onClick(event: Event): void {
|
|
222
|
+
if (!(event.target instanceof Element) || !this.dom.list) return;
|
|
223
|
+
const link = event.target.closest<HTMLElement>('[data-toc-index]');
|
|
224
|
+
if (!link || !this.dom.list.contains(link)) return;
|
|
225
|
+
|
|
226
|
+
const index = Number(link.dataset.tocIndex);
|
|
227
|
+
const item = this.state?.items[index];
|
|
228
|
+
if (!item) return;
|
|
229
|
+
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
this.scrollToItem(item, { activeIndex: index, updateHash: true });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private scrollToItem(
|
|
235
|
+
item: TocItem,
|
|
236
|
+
{ activeIndex = -1, updateHash = false }: TocScrollOptions = {}
|
|
237
|
+
): void {
|
|
238
|
+
const scrollY = window.scrollY || window.pageYOffset || 0;
|
|
239
|
+
const top = Math.max(
|
|
240
|
+
0,
|
|
241
|
+
item.element.getBoundingClientRect().top + scrollY - this.props.offset
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
window.scrollTo({
|
|
245
|
+
top,
|
|
246
|
+
behavior: 'smooth',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (activeIndex >= 0) this.setActive(activeIndex);
|
|
250
|
+
|
|
251
|
+
if (updateHash && window.history?.pushState) {
|
|
252
|
+
window.history.pushState(null, '', `#${item.id}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private linkClassName(item: TocItem, active = false): string {
|
|
257
|
+
const classes = [
|
|
258
|
+
this.props.className.link,
|
|
259
|
+
`${this.props.className.levelPrefix}${item.level}`,
|
|
260
|
+
];
|
|
261
|
+
if (active) classes.push(this.props.className.active);
|
|
262
|
+
return classes.filter(Boolean).join(' ');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private buildLink(item: TocItem, index: number): HTMLAnchorElement {
|
|
266
|
+
return jsx('a', {
|
|
267
|
+
className: this.linkClassName(item),
|
|
268
|
+
href: `#${item.id}`,
|
|
269
|
+
'data-toc-index': String(index),
|
|
270
|
+
'data-toc-target': item.id,
|
|
271
|
+
children: item.text,
|
|
272
|
+
}) as HTMLAnchorElement;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private updateActive(): void {
|
|
276
|
+
if (!this.runtime.built) return;
|
|
277
|
+
|
|
278
|
+
let index = -1;
|
|
279
|
+
const activeOffset = this.props.offset + ACTIVE_OFFSET_TOLERANCE;
|
|
280
|
+
for (let i = this.dom.headings.length - 1; i >= 0; i--) {
|
|
281
|
+
if (this.dom.headings[i].getBoundingClientRect().top <= activeOffset) {
|
|
282
|
+
index = i;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.setActive(index);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private setActive(index: number): void {
|
|
291
|
+
if (!this.state || index === this.state.current.index) return;
|
|
292
|
+
|
|
293
|
+
const current = this.state.items[index] || null;
|
|
294
|
+
this.setState({
|
|
295
|
+
current: { index, item: current },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const items = this.state.items;
|
|
299
|
+
this.dom.links.forEach((link, i) => {
|
|
300
|
+
const active = i === index;
|
|
301
|
+
link.dataset.active = active ? '1' : '0';
|
|
302
|
+
link.className = this.linkClassName(items[i], active);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (typeof this.props.onUpdate === 'function') {
|
|
306
|
+
this.props.onUpdate(current, index, this);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 重新扫描标题并重建目录列表。
|
|
312
|
+
* @returns {Toc} 当前实例。
|
|
313
|
+
*/
|
|
314
|
+
refresh(): this {
|
|
315
|
+
if (
|
|
316
|
+
this.runtime.destroyed ||
|
|
317
|
+
!this.runtime.built ||
|
|
318
|
+
!this.dom.target ||
|
|
319
|
+
!this.dom.list
|
|
320
|
+
) {
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.cleanup.events.clear();
|
|
325
|
+
|
|
326
|
+
this.dom.headings = all<HTMLHeadingElement>(
|
|
327
|
+
this.props.headings,
|
|
328
|
+
this.dom.target
|
|
329
|
+
);
|
|
330
|
+
const items = this.dom.headings.map(normalizeHeading);
|
|
331
|
+
this.dom.links = items.map((item, index) => this.buildLink(item, index));
|
|
332
|
+
|
|
333
|
+
this.dom.list.innerHTML = '';
|
|
334
|
+
for (const link of this.dom.links) this.dom.list.appendChild(link);
|
|
335
|
+
|
|
336
|
+
this.setState({
|
|
337
|
+
items,
|
|
338
|
+
current: { index: -1, item: null },
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
this.bindEvents();
|
|
342
|
+
this.updateActive();
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 激活并滚动到指定目录项。
|
|
348
|
+
* @param {number} index 目录项索引。
|
|
349
|
+
* @returns {Toc} 当前实例。
|
|
350
|
+
*/
|
|
351
|
+
activate(index: number): this {
|
|
352
|
+
if (!this.runtime.built || !this.state) return this;
|
|
353
|
+
if (
|
|
354
|
+
!Number.isInteger(index) ||
|
|
355
|
+
index < 0 ||
|
|
356
|
+
index >= this.dom.links.length
|
|
357
|
+
) {
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.scrollToItem(this.state.items[index]);
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 销毁实例并清空渲染内容。
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
protected onDestroy(): void {
|
|
370
|
+
this.cleanup.events.clear();
|
|
371
|
+
if (this.dom.container) this.dom.container.innerHTML = '';
|
|
372
|
+
|
|
373
|
+
this.runtime.built = false;
|
|
374
|
+
this.runtime.ticking = false;
|
|
375
|
+
this.dom.container = null;
|
|
376
|
+
this.dom.target = null;
|
|
377
|
+
this.dom.list = null;
|
|
378
|
+
this.dom.headings = [];
|
|
379
|
+
this.dom.links = [];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function createToc(props: TocProps = {}): Toc {
|
|
384
|
+
return new Toc(props);
|
|
385
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { jsx } from 'vanilla-signal';
|
|
2
|
+
|
|
3
|
+
import { type ResolveSchema, resolveProps } from '../utilities/core.ts';
|
|
4
|
+
import { type DOMReference } from '../utilities/dom.ts';
|
|
5
|
+
import {
|
|
6
|
+
Drop,
|
|
7
|
+
type DropClassNameConfig,
|
|
8
|
+
type DropDelay,
|
|
9
|
+
type DropMode,
|
|
10
|
+
type DropPosition,
|
|
11
|
+
} from './drop.ts';
|
|
12
|
+
|
|
13
|
+
export interface TooltipClassNames {
|
|
14
|
+
root: string;
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TooltipClassNameConfig = Partial<TooltipClassNames>;
|
|
19
|
+
|
|
20
|
+
export interface TooltipProps extends Record<string, unknown> {
|
|
21
|
+
name?: string | null;
|
|
22
|
+
mode?: DropMode;
|
|
23
|
+
position?: DropPosition;
|
|
24
|
+
offset?: number;
|
|
25
|
+
message?: string;
|
|
26
|
+
className?: TooltipClassNameConfig;
|
|
27
|
+
dropClassName?: DropClassNameConfig;
|
|
28
|
+
id?: string | null;
|
|
29
|
+
delay?: number | DropDelay;
|
|
30
|
+
hoverIntent?: boolean;
|
|
31
|
+
onShown?: ((drop: Drop) => void | Promise<void>) | null;
|
|
32
|
+
onHidden?: ((drop: Drop) => void | Promise<void>) | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ResolvedTooltipProps extends Record<string, unknown> {
|
|
36
|
+
name: string | null;
|
|
37
|
+
mode: DropMode;
|
|
38
|
+
position: DropPosition;
|
|
39
|
+
offset: number;
|
|
40
|
+
message: string;
|
|
41
|
+
className: TooltipClassNames;
|
|
42
|
+
dropClassName: DropClassNameConfig;
|
|
43
|
+
id: string | null;
|
|
44
|
+
delay: number | DropDelay;
|
|
45
|
+
hoverIntent: boolean;
|
|
46
|
+
onShown: NonNullable<TooltipProps['onShown']> | null;
|
|
47
|
+
onHidden: NonNullable<TooltipProps['onHidden']> | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_CLASS_NAMES: TooltipClassNames = {
|
|
51
|
+
root: 'j-tooltip',
|
|
52
|
+
message: 'tooltip-message',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const TOOLTIP_OPTIONS_SCHEMA = {
|
|
56
|
+
name: { default: null, types: ['string', 'null'] },
|
|
57
|
+
mode: { default: 'hover', type: 'string', enum: ['hover', 'click'] },
|
|
58
|
+
position: {
|
|
59
|
+
default: 'auto',
|
|
60
|
+
type: 'string',
|
|
61
|
+
enum: [
|
|
62
|
+
'auto',
|
|
63
|
+
'top-left',
|
|
64
|
+
'top-center',
|
|
65
|
+
'top-right',
|
|
66
|
+
'bottom-left',
|
|
67
|
+
'bottom-center',
|
|
68
|
+
'bottom-right',
|
|
69
|
+
'left',
|
|
70
|
+
'right',
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
offset: { default: 8, type: 'number' },
|
|
74
|
+
message: {
|
|
75
|
+
default: '',
|
|
76
|
+
type: 'string',
|
|
77
|
+
normalize: (value: unknown) =>
|
|
78
|
+
typeof value === 'string' ? value.trim() : value,
|
|
79
|
+
validate: (value: unknown) => typeof value === 'string' && value.length > 0,
|
|
80
|
+
message: 'expects a non-empty string.',
|
|
81
|
+
},
|
|
82
|
+
className: {
|
|
83
|
+
default: DEFAULT_CLASS_NAMES,
|
|
84
|
+
type: 'object',
|
|
85
|
+
normalize: (value: unknown) => ({
|
|
86
|
+
...DEFAULT_CLASS_NAMES,
|
|
87
|
+
...(value && typeof value === 'object' ? value : {}),
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
dropClassName: { default: {}, type: 'object' },
|
|
91
|
+
id: { default: null, types: ['string', 'null'] },
|
|
92
|
+
delay: { default: 100, types: ['number', 'object'] },
|
|
93
|
+
hoverIntent: { default: true, type: 'boolean' },
|
|
94
|
+
onShown: { default: null, types: ['function', 'null'] },
|
|
95
|
+
onHidden: { default: null, types: ['function', 'null'] },
|
|
96
|
+
} satisfies ResolveSchema<TooltipProps>;
|
|
97
|
+
|
|
98
|
+
function normalizeProps(input: TooltipProps): ResolvedTooltipProps {
|
|
99
|
+
const props = resolveProps(input, TOOLTIP_OPTIONS_SCHEMA, 'Tooltip.props');
|
|
100
|
+
return {
|
|
101
|
+
name: props.name as string | null,
|
|
102
|
+
mode: props.mode as DropMode,
|
|
103
|
+
position: props.position as DropPosition,
|
|
104
|
+
offset: props.offset as number,
|
|
105
|
+
message: props.message as string,
|
|
106
|
+
className: props.className as TooltipClassNames,
|
|
107
|
+
dropClassName: props.dropClassName as DropClassNameConfig,
|
|
108
|
+
id: props.id as string | null,
|
|
109
|
+
delay: props.delay as number | DropDelay,
|
|
110
|
+
hoverIntent: props.hoverIntent as boolean,
|
|
111
|
+
onShown: props.onShown as ResolvedTooltipProps['onShown'],
|
|
112
|
+
onHidden: props.onHidden as ResolvedTooltipProps['onHidden'],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Tooltip 提示组件。
|
|
118
|
+
*
|
|
119
|
+
* 基于 Drop 实现,提供更轻量的文本提示封装。
|
|
120
|
+
*/
|
|
121
|
+
export class Tooltip {
|
|
122
|
+
drop: Drop | null;
|
|
123
|
+
props: ResolvedTooltipProps | null;
|
|
124
|
+
|
|
125
|
+
constructor(element: DOMReference, props: TooltipProps = {}) {
|
|
126
|
+
const settings = normalizeProps(props);
|
|
127
|
+
this.props = settings;
|
|
128
|
+
this.drop = new Drop(element, {
|
|
129
|
+
name: settings.name,
|
|
130
|
+
mode: settings.mode,
|
|
131
|
+
position: settings.position,
|
|
132
|
+
offset: settings.offset,
|
|
133
|
+
id: settings.id,
|
|
134
|
+
delay: settings.delay,
|
|
135
|
+
hoverIntent: settings.hoverIntent,
|
|
136
|
+
onShown: settings.onShown,
|
|
137
|
+
onHidden: settings.onHidden,
|
|
138
|
+
className: settings.dropClassName,
|
|
139
|
+
content: this.buildContent(settings),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private buildContent(settings: ResolvedTooltipProps): HTMLElement {
|
|
144
|
+
return jsx('div', {
|
|
145
|
+
className: settings.className.root,
|
|
146
|
+
'data-tooltip': settings.name || settings.id || '',
|
|
147
|
+
children: jsx('div', {
|
|
148
|
+
className: settings.className.message,
|
|
149
|
+
'data-tooltip-message': '',
|
|
150
|
+
children: settings.message,
|
|
151
|
+
}),
|
|
152
|
+
}) as HTMLElement;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
show(useDelay = true): void {
|
|
156
|
+
this.drop?.show(useDelay);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
hide(useDelay = true): void {
|
|
160
|
+
this.drop?.hide(useDelay);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
toggle(): void {
|
|
164
|
+
this.drop?.toggle();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
destroy(): void {
|
|
168
|
+
this.drop?.destroy();
|
|
169
|
+
this.drop = null;
|
|
170
|
+
this.props = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function createTooltip(
|
|
175
|
+
element: DOMReference,
|
|
176
|
+
props: TooltipProps = {}
|
|
177
|
+
): Tooltip {
|
|
178
|
+
return new Tooltip(element, props);
|
|
179
|
+
}
|