cdui-js 1.0.22 → 1.0.24
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/css/all.css +1 -2
- package/css/datewidget.css +9 -0
- package/css/{datepicker.css → dropdown.css} +6 -10
- package/css/popup.css +38 -19
- package/demo/css/css.md +6 -10
- package/demo/src/App.tsx +22 -2
- package/demo/src/css/atomic.css +6 -8
- package/demo/src/pages/Canlendar.tsx +4 -4
- package/demo/src/pages/ComboBox.tsx +17 -14
- package/demo/src/pages/DatePicker.tsx +6 -1
- package/demo/src/pages/Form.tsx +70 -45
- package/package.json +1 -1
- package/src/components/Canlendar.tsx +91 -48
- package/src/components/ComboBox.tsx +57 -51
- package/src/components/DatePicker.tsx +146 -60
- package/src/components/Form.tsx +1 -0
- package/src/components/MonthPicker.tsx +1 -1
- package/src/components/MonthWidget.tsx +65 -36
- package/src/components/YearPicker.tsx +1 -1
- package/src/components/YearWidget.tsx +62 -138
- package/src/{components/Dialog.ts → dialog.ts} +13 -6
- package/src/i18n/index.ts +2 -2
- package/src/index.ts +4 -2
- package/src/popup.ts +341 -0
- package/src/reactive.ts +4 -1
- package/src/ssr/render.ts +5 -5
- package/css/combobox.css +0 -38
- package/demo/src/pages/MobileDatePicker.tsx +0 -5
- package/src/components/MobileDatePicker.tsx +0 -444
- package/src/components/Popup.tsx +0 -228
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
import { JSX } from '../jsx';
|
|
2
|
-
import { animateScrollTo } from '../animate-scroll-to';
|
|
3
|
-
import {
|
|
4
|
-
batch,
|
|
5
|
-
combineClass,
|
|
6
|
-
createEffect,
|
|
7
|
-
createMemo,
|
|
8
|
-
createSignal,
|
|
9
|
-
omitProps,
|
|
10
|
-
onCleanup,
|
|
11
|
-
onMount,
|
|
12
|
-
untrack,
|
|
13
|
-
} from '../reactive';
|
|
14
|
-
|
|
15
|
-
import { For } from './For';
|
|
16
|
-
import { parseDate } from './Canlendar';
|
|
17
|
-
|
|
18
|
-
const ITEM_HEIGHT = 44;
|
|
19
|
-
const VIEWPORT_ROWS = 5;
|
|
20
|
-
/** 上下各多渲染的行数,保证快速滑动时不断档 */
|
|
21
|
-
const ROW_OVERSCAN = 5;
|
|
22
|
-
const REPEAT_BLOCKS = 99;
|
|
23
|
-
const TELEPORT_SHIFT = 35;
|
|
24
|
-
|
|
25
|
-
const PAD = ((VIEWPORT_ROWS * ITEM_HEIGHT - ITEM_HEIGHT) / 2) | 0;
|
|
26
|
-
|
|
27
|
-
const daysInMonth = (year: number, month: number) => new Date(year, month, 0).getDate();
|
|
28
|
-
|
|
29
|
-
type LoopColumnProps = {
|
|
30
|
-
class?: string;
|
|
31
|
-
values: readonly number[];
|
|
32
|
-
value: () => number;
|
|
33
|
-
onChange: (v: number) => void;
|
|
34
|
-
format: (v: number) => string;
|
|
35
|
-
/**
|
|
36
|
-
* 额外依赖,用于在同长度列表变化时(如换月)重置滚动位置
|
|
37
|
-
*/
|
|
38
|
-
scrollToken?: () => void;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type VisibleRow = { r: number; v: number };
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 单列循环滚动(年 / 月 / 日),可视区域虚拟化,仅挂载少量行节点
|
|
45
|
-
*/
|
|
46
|
-
const MobileDateLoopColumn = (props: LoopColumnProps) => {
|
|
47
|
-
let wheelEl: HTMLDivElement;
|
|
48
|
-
let teleporting = false;
|
|
49
|
-
let settleTimer: ReturnType<typeof setTimeout>;
|
|
50
|
-
let dragging = false;
|
|
51
|
-
let scrollRaf = 0;
|
|
52
|
-
|
|
53
|
-
const [scrollTop, setScrollTop] = createSignal(0);
|
|
54
|
-
|
|
55
|
-
const blockHeight = () => props.values.length * ITEM_HEIGHT;
|
|
56
|
-
|
|
57
|
-
const rowCount = () => {
|
|
58
|
-
const n = props.values.length;
|
|
59
|
-
return n ? REPEAT_BLOCKS * n : 0;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const innerHeightPx = () => {
|
|
63
|
-
const rc = rowCount();
|
|
64
|
-
return rc ? PAD * 2 + rc * ITEM_HEIGHT : PAD * 2;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const visibleRows = createMemo((): VisibleRow[] => {
|
|
68
|
-
const vals = props.values;
|
|
69
|
-
const n = vals.length;
|
|
70
|
-
const rc = rowCount();
|
|
71
|
-
if (!n || !rc) {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const st = scrollTop();
|
|
76
|
-
const ch = VIEWPORT_ROWS * ITEM_HEIGHT;
|
|
77
|
-
const y0 = st;
|
|
78
|
-
const y1 = st + ch;
|
|
79
|
-
let r0 = Math.floor((y0 - PAD) / ITEM_HEIGHT) - ROW_OVERSCAN;
|
|
80
|
-
let r1 = Math.ceil((y1 - PAD) / ITEM_HEIGHT) + ROW_OVERSCAN;
|
|
81
|
-
if (r0 < 0) {
|
|
82
|
-
r0 = 0;
|
|
83
|
-
}
|
|
84
|
-
if (r1 > rc - 1) {
|
|
85
|
-
r1 = rc - 1;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const out: VisibleRow[] = [];
|
|
89
|
-
for (let r = r0; r <= r1; r++) {
|
|
90
|
-
out.push({ r, v: vals[r % n] });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return out;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const flushScrollTop = () => {
|
|
97
|
-
if (wheelEl) {
|
|
98
|
-
setScrollTop(wheelEl.scrollTop);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const scheduleScrollTop = () => {
|
|
103
|
-
if (scrollRaf) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
scrollRaf = requestAnimationFrame(() => {
|
|
108
|
-
scrollRaf = 0;
|
|
109
|
-
flushScrollTop();
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const syncScroll = () => {
|
|
114
|
-
if (!wheelEl || !props.values.length) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let idx = props.values.indexOf(props.value());
|
|
119
|
-
if (idx < 0) {
|
|
120
|
-
idx = 0;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const n = props.values.length;
|
|
124
|
-
const mid = (REPEAT_BLOCKS / 2) | 0;
|
|
125
|
-
const R = mid * n + idx;
|
|
126
|
-
teleporting = true;
|
|
127
|
-
wheelEl.scrollTop = R * ITEM_HEIGHT;
|
|
128
|
-
setScrollTop(wheelEl.scrollTop);
|
|
129
|
-
requestAnimationFrame(() => {
|
|
130
|
-
teleporting = false;
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const maybeTeleport = () => {
|
|
135
|
-
if (teleporting || !wheelEl) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const block = blockHeight();
|
|
140
|
-
const n = props.values.length;
|
|
141
|
-
if (!n) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const st = wheelEl.scrollTop;
|
|
146
|
-
const low = TELEPORT_SHIFT * block;
|
|
147
|
-
const high = (REPEAT_BLOCKS - TELEPORT_SHIFT) * block;
|
|
148
|
-
|
|
149
|
-
if (st < low) {
|
|
150
|
-
teleporting = true;
|
|
151
|
-
wheelEl.scrollTop = st + TELEPORT_SHIFT * block;
|
|
152
|
-
requestAnimationFrame(() => {
|
|
153
|
-
teleporting = false;
|
|
154
|
-
flushScrollTop();
|
|
155
|
-
});
|
|
156
|
-
} else if (st > high) {
|
|
157
|
-
teleporting = true;
|
|
158
|
-
wheelEl.scrollTop = st - TELEPORT_SHIFT * block;
|
|
159
|
-
requestAnimationFrame(() => {
|
|
160
|
-
teleporting = false;
|
|
161
|
-
flushScrollTop();
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const snapAndCommit = () => {
|
|
167
|
-
if (!wheelEl || !props.values.length) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const n = props.values.length;
|
|
172
|
-
const rc = rowCount();
|
|
173
|
-
const st = wheelEl.scrollTop;
|
|
174
|
-
const ch = wheelEl.clientHeight;
|
|
175
|
-
const center = st + ch / 2;
|
|
176
|
-
let R = Math.round((center - PAD - ITEM_HEIGHT / 2) / ITEM_HEIGHT);
|
|
177
|
-
if (R < 0) {
|
|
178
|
-
R = 0;
|
|
179
|
-
}
|
|
180
|
-
if (R > rc - 1) {
|
|
181
|
-
R = rc - 1;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const idx = R % n;
|
|
185
|
-
const next = props.values[idx];
|
|
186
|
-
const target = R * ITEM_HEIGHT;
|
|
187
|
-
|
|
188
|
-
animateScrollTo(wheelEl, undefined, target, 120).then(() => {
|
|
189
|
-
setScrollTop(wheelEl.scrollTop);
|
|
190
|
-
if (next !== props.value()) {
|
|
191
|
-
props.onChange(next);
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const scheduleSettle = () => {
|
|
197
|
-
clearTimeout(settleTimer);
|
|
198
|
-
settleTimer = setTimeout(() => {
|
|
199
|
-
if (!dragging) {
|
|
200
|
-
snapAndCommit();
|
|
201
|
-
}
|
|
202
|
-
}, 80);
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
onMount(() => {
|
|
206
|
-
syncScroll();
|
|
207
|
-
|
|
208
|
-
const onScroll = () => {
|
|
209
|
-
if (!teleporting) {
|
|
210
|
-
maybeTeleport();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
scheduleScrollTop();
|
|
214
|
-
scheduleSettle();
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
wheelEl.addEventListener('scroll', onScroll, { passive: true });
|
|
218
|
-
|
|
219
|
-
const onTouchStart = () => {
|
|
220
|
-
dragging = true;
|
|
221
|
-
clearTimeout(settleTimer);
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const onTouchEnd = () => {
|
|
225
|
-
dragging = false;
|
|
226
|
-
scheduleSettle();
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
wheelEl.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
230
|
-
wheelEl.addEventListener('touchend', onTouchEnd);
|
|
231
|
-
wheelEl.addEventListener('touchcancel', onTouchEnd);
|
|
232
|
-
|
|
233
|
-
const onScrollEnd = () => {
|
|
234
|
-
dragging = false;
|
|
235
|
-
flushScrollTop();
|
|
236
|
-
snapAndCommit();
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
wheelEl.addEventListener('scrollend', onScrollEnd);
|
|
240
|
-
|
|
241
|
-
onCleanup(() => {
|
|
242
|
-
clearTimeout(settleTimer);
|
|
243
|
-
cancelAnimationFrame(scrollRaf);
|
|
244
|
-
scrollRaf = 0;
|
|
245
|
-
wheelEl.removeEventListener('scroll', onScroll);
|
|
246
|
-
wheelEl.removeEventListener('touchstart', onTouchStart);
|
|
247
|
-
wheelEl.removeEventListener('touchend', onTouchEnd);
|
|
248
|
-
wheelEl.removeEventListener('touchcancel', onTouchEnd);
|
|
249
|
-
wheelEl.removeEventListener('scrollend', onScrollEnd);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
createEffect(() => {
|
|
254
|
-
props.scrollToken?.();
|
|
255
|
-
props.value();
|
|
256
|
-
props.values.length;
|
|
257
|
-
|
|
258
|
-
untrack(() => {
|
|
259
|
-
queueMicrotask(syncScroll);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<div class={combineClass('mobile-date-picker-column', props.class)}>
|
|
265
|
-
<div ref={wheelEl as any} class="mobile-date-picker-wheel scrollbar-hidden">
|
|
266
|
-
<div class="mobile-date-picker-wheel-inner" style={{ height: `${innerHeightPx()}px` }}>
|
|
267
|
-
<For each={visibleRows()}>
|
|
268
|
-
{(row) => (
|
|
269
|
-
<div class="mobile-date-picker-item" style={{ top: `${PAD + row.r * ITEM_HEIGHT}px` }} data-value={row.v}>
|
|
270
|
-
{props.format(row.v)}
|
|
271
|
-
</div>
|
|
272
|
-
)}
|
|
273
|
-
</For>
|
|
274
|
-
</div>
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
);
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const pad2 = (n: number) => (n > 9 ? String(n) : '0' + n);
|
|
281
|
-
|
|
282
|
-
const defaultYearRange = (min: number, max: number) => {
|
|
283
|
-
const a: number[] = [];
|
|
284
|
-
|
|
285
|
-
for (let y = min; y <= max; y++) {
|
|
286
|
-
a.push(y);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return a;
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
const monthValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* 移动端日期触控选择:年、月、日三栏循环滚动
|
|
296
|
-
*/
|
|
297
|
-
export const MobileDatePicker = (
|
|
298
|
-
props?: JSX.HTMLAttributes<never> & {
|
|
299
|
-
/**
|
|
300
|
-
* 当前日期
|
|
301
|
-
*/
|
|
302
|
-
value?: Date | string | number;
|
|
303
|
-
/**
|
|
304
|
-
* 值变化回调
|
|
305
|
-
*/
|
|
306
|
-
onValueChange?: (value: Date) => void;
|
|
307
|
-
/**
|
|
308
|
-
* 最小年份
|
|
309
|
-
*/
|
|
310
|
-
minYear?: number;
|
|
311
|
-
/**
|
|
312
|
-
* 最大年份
|
|
313
|
-
*/
|
|
314
|
-
maxYear?: number;
|
|
315
|
-
},
|
|
316
|
-
) => {
|
|
317
|
-
const minY = () => props.minYear ?? 1970;
|
|
318
|
-
const maxY = () => props.maxYear ?? 2100;
|
|
319
|
-
|
|
320
|
-
const init = () => {
|
|
321
|
-
let d = props.value != null ? parseDate(props.value) : null;
|
|
322
|
-
d = d || new Date();
|
|
323
|
-
let y = d.getFullYear();
|
|
324
|
-
let m = d.getMonth() + 1;
|
|
325
|
-
let day = d.getDate();
|
|
326
|
-
y = Math.min(maxY(), Math.max(minY(), y));
|
|
327
|
-
const dim = daysInMonth(y, m);
|
|
328
|
-
day = Math.min(dim, Math.max(1, day));
|
|
329
|
-
return { y, m, day };
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const [year, setYear] = createSignal(init().y);
|
|
333
|
-
const [month, setMonth] = createSignal(init().m);
|
|
334
|
-
const [day, setDay] = createSignal(init().day);
|
|
335
|
-
|
|
336
|
-
const years = createMemo(() => defaultYearRange(minY(), maxY()));
|
|
337
|
-
|
|
338
|
-
const days = createMemo(() => {
|
|
339
|
-
const dim = daysInMonth(year(), month());
|
|
340
|
-
const a: number[] = [];
|
|
341
|
-
|
|
342
|
-
for (let i = 1; i <= dim; i++) {
|
|
343
|
-
a.push(i);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return a;
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
createEffect(() => {
|
|
350
|
-
const v = props.value;
|
|
351
|
-
|
|
352
|
-
if (v == null) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
let d = parseDate(v);
|
|
357
|
-
if (!d) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
let y = d.getFullYear();
|
|
362
|
-
let m = d.getMonth() + 1;
|
|
363
|
-
let dd = d.getDate();
|
|
364
|
-
y = Math.min(maxY(), Math.max(minY(), y));
|
|
365
|
-
const dim = daysInMonth(y, m);
|
|
366
|
-
dd = Math.min(dim, Math.max(1, dd));
|
|
367
|
-
|
|
368
|
-
batch(() => {
|
|
369
|
-
setYear(y);
|
|
370
|
-
setMonth(m);
|
|
371
|
-
setDay(dd);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
createEffect(() => {
|
|
376
|
-
minY();
|
|
377
|
-
maxY();
|
|
378
|
-
const y = year();
|
|
379
|
-
const c = Math.min(maxY(), Math.max(minY(), y));
|
|
380
|
-
if (c !== y) {
|
|
381
|
-
setYear(c);
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
createEffect(() => {
|
|
386
|
-
const dim = daysInMonth(year(), month());
|
|
387
|
-
if (day() > dim) {
|
|
388
|
-
setDay(dim);
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
const emit = (y: number, m: number, d: number) => {
|
|
393
|
-
const dim = daysInMonth(y, m);
|
|
394
|
-
const dd = Math.min(dim, Math.max(1, d));
|
|
395
|
-
props.onValueChange?.(new Date(y, m - 1, dd));
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const onYear = (y: number) => {
|
|
399
|
-
setYear(y);
|
|
400
|
-
const dim = daysInMonth(y, month());
|
|
401
|
-
const dd = Math.min(dim, day());
|
|
402
|
-
if (dd !== day()) {
|
|
403
|
-
setDay(dd);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
emit(y, month(), dd);
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
const onMonth = (m: number) => {
|
|
410
|
-
setMonth(m);
|
|
411
|
-
const dim = daysInMonth(year(), m);
|
|
412
|
-
const dd = Math.min(dim, day());
|
|
413
|
-
if (dd !== day()) {
|
|
414
|
-
setDay(dd);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
emit(year(), m, dd);
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
const onDay = (d: number) => {
|
|
421
|
-
setDay(d);
|
|
422
|
-
emit(year(), month(), d);
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
return (
|
|
426
|
-
<div
|
|
427
|
-
class={combineClass('mobile-date-picker', props.class)}
|
|
428
|
-
{...omitProps(props, ['class', 'value', 'onValueChange', 'minYear', 'maxYear'])}
|
|
429
|
-
>
|
|
430
|
-
<MobileDateLoopColumn values={years()} value={year} onChange={onYear} format={(y) => String(y)} />
|
|
431
|
-
<MobileDateLoopColumn values={monthValues} value={month} onChange={onMonth} format={(m) => pad2(m)} />
|
|
432
|
-
<MobileDateLoopColumn
|
|
433
|
-
values={days()}
|
|
434
|
-
value={day}
|
|
435
|
-
onChange={onDay}
|
|
436
|
-
format={(d) => pad2(d)}
|
|
437
|
-
scrollToken={() => {
|
|
438
|
-
year();
|
|
439
|
-
month();
|
|
440
|
-
}}
|
|
441
|
-
/>
|
|
442
|
-
</div>
|
|
443
|
-
);
|
|
444
|
-
};
|
package/src/components/Popup.tsx
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { JSX } from '../jsx';
|
|
2
|
-
import { layout } from '../layout';
|
|
3
|
-
import { omitProps } from '../reactive';
|
|
4
|
-
import { disableAutoCloseEvent, hideMaskLayer, registerAutoClose, showMaskLayer } from '../dom';
|
|
5
|
-
|
|
6
|
-
const POPUP_TOP_CLASS = 'popup-top';
|
|
7
|
-
const POPUP_RIGHT_CLASS = 'popup-right';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 当前弹出层
|
|
11
|
-
*/
|
|
12
|
-
const currentPopup = {
|
|
13
|
-
/**
|
|
14
|
-
* 当前 DOM 对象
|
|
15
|
-
*/
|
|
16
|
-
dom: null as HTMLElement,
|
|
17
|
-
/**
|
|
18
|
-
* 是否显示了遮罩层
|
|
19
|
-
*/
|
|
20
|
-
mask: false,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 是否使用下拉动画
|
|
25
|
-
*/
|
|
26
|
-
let useDropdownTransition = true;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 显示遮罩层方法
|
|
30
|
-
*/
|
|
31
|
-
let showMaskLayerFn = () => layout['le-480'];
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 设置是否使用下拉动画
|
|
35
|
-
*
|
|
36
|
-
* @param use 是否使用
|
|
37
|
-
*/
|
|
38
|
-
export const setUseDropdownTransition = (use: boolean) => {
|
|
39
|
-
useDropdownTransition = use;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 设置显示遮罩层的方法
|
|
44
|
-
*
|
|
45
|
-
* @param fn 显示遮罩层的方法
|
|
46
|
-
*/
|
|
47
|
-
export const setShowMaskLayerFn = (fn: () => boolean) => {
|
|
48
|
-
showMaskLayerFn = fn || showMaskLayerFn;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const showPopup = (dom: HTMLElement, onPopup?: (dom: HTMLElement) => void | false) => {
|
|
52
|
-
// 下拉钩子
|
|
53
|
-
if (onPopup && onPopup(dom) === false) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let windowWidth = window.innerWidth;
|
|
58
|
-
let windowHeight = window.innerHeight;
|
|
59
|
-
let classList = dom.classList;
|
|
60
|
-
let style = dom.style;
|
|
61
|
-
|
|
62
|
-
// 有下拉
|
|
63
|
-
if (currentPopup.dom) {
|
|
64
|
-
// 先关闭
|
|
65
|
-
hidePopup();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 记录当前弹出层
|
|
69
|
-
currentPopup.dom = dom;
|
|
70
|
-
|
|
71
|
-
if ((currentPopup.mask = showMaskLayerFn())) {
|
|
72
|
-
showMaskLayer(closePopup);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let host = dom.parentNode as HTMLElement;
|
|
76
|
-
|
|
77
|
-
// 先显示
|
|
78
|
-
style.width = host.offsetWidth + 'px';
|
|
79
|
-
style.height = 'auto';
|
|
80
|
-
style.display = 'block';
|
|
81
|
-
|
|
82
|
-
// 获取宽高及计算位置
|
|
83
|
-
let width = (dom.firstElementChild as HTMLElement).offsetWidth;
|
|
84
|
-
let height = dom.offsetHeight;
|
|
85
|
-
let rect = host.getBoundingClientRect();
|
|
86
|
-
|
|
87
|
-
// 同步子组件宽度
|
|
88
|
-
style.width = width + 'px';
|
|
89
|
-
|
|
90
|
-
if (windowHeight - rect.top - rect.height < height + 4 && rect.top >= height) {
|
|
91
|
-
classList.add(POPUP_TOP_CLASS);
|
|
92
|
-
} else {
|
|
93
|
-
classList.remove(POPUP_TOP_CLASS);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (windowWidth - rect.left - rect.width > width + 4 && rect.left >= width) {
|
|
97
|
-
classList.add(POPUP_RIGHT_CLASS);
|
|
98
|
-
} else {
|
|
99
|
-
classList.remove(POPUP_RIGHT_CLASS);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 过渡动画
|
|
103
|
-
if (useDropdownTransition) {
|
|
104
|
-
style.height = '0';
|
|
105
|
-
|
|
106
|
-
setTimeout(() => {
|
|
107
|
-
style.height = height + 'px';
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const hidePopup = () => {
|
|
113
|
-
let popup = currentPopup;
|
|
114
|
-
let dom = popup.dom;
|
|
115
|
-
let style = dom.style;
|
|
116
|
-
|
|
117
|
-
currentPopup.dom = null;
|
|
118
|
-
|
|
119
|
-
if (popup.mask) {
|
|
120
|
-
hideMaskLayer();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 过渡动画
|
|
124
|
-
if (useDropdownTransition) {
|
|
125
|
-
style.height = dom.offsetHeight + 'px';
|
|
126
|
-
|
|
127
|
-
setTimeout(() => {
|
|
128
|
-
style.height = '0';
|
|
129
|
-
});
|
|
130
|
-
} else {
|
|
131
|
-
style.display = 'none';
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const togglePopup = (dom: HTMLElement, onPopup?: (dom: HTMLElement) => void | false, event?: Event) => {
|
|
136
|
-
// 有显示下拉
|
|
137
|
-
if (currentPopup.dom) {
|
|
138
|
-
// 不是当前下拉框
|
|
139
|
-
if (currentPopup.dom !== dom) {
|
|
140
|
-
// 先关闭
|
|
141
|
-
closePopup();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
showPopup(dom, onPopup);
|
|
145
|
-
} else {
|
|
146
|
-
showPopup(dom, onPopup);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
event && event.stopPropagation();
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
const OMIT_PROPS = ['onPopup', 'api', 'children'] as const;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 弹出层外部访问接口
|
|
156
|
-
*/
|
|
157
|
-
export interface PopupApi {
|
|
158
|
-
/**
|
|
159
|
-
* 是否已经弹出
|
|
160
|
-
*/
|
|
161
|
-
popup: boolean;
|
|
162
|
-
/**
|
|
163
|
-
* 打开弹出框
|
|
164
|
-
*/
|
|
165
|
-
openPopup(): void;
|
|
166
|
-
/**
|
|
167
|
-
* 关闭弹出框
|
|
168
|
-
*/
|
|
169
|
-
closePopup(): void;
|
|
170
|
-
/**
|
|
171
|
-
* 显示或关闭弹出层
|
|
172
|
-
*/
|
|
173
|
-
togglePopup(): void;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 弹出层属性
|
|
178
|
-
*/
|
|
179
|
-
export interface PopupProps {
|
|
180
|
-
/**
|
|
181
|
-
* 弹出对话框钩子
|
|
182
|
-
*/
|
|
183
|
-
onPopup?: (dom: HTMLElement) => void | false;
|
|
184
|
-
/**
|
|
185
|
-
* 外部调用接口
|
|
186
|
-
*/
|
|
187
|
-
api?: (api: PopupApi) => void;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* 弹出层组件
|
|
192
|
-
*/
|
|
193
|
-
export const Popup = (props?: JSX.HTMLAttributes<never> & PopupProps) => {
|
|
194
|
-
let popup: HTMLElement;
|
|
195
|
-
|
|
196
|
-
// 初始化外部调用接口
|
|
197
|
-
props.api &&
|
|
198
|
-
props.api({
|
|
199
|
-
get popup() {
|
|
200
|
-
return currentPopup.dom === popup;
|
|
201
|
-
},
|
|
202
|
-
openPopup: () => showPopup(popup, props.onPopup),
|
|
203
|
-
closePopup: () => currentPopup.dom === popup && hidePopup(),
|
|
204
|
-
togglePopup: () => togglePopup(popup, props.onPopup),
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<div
|
|
209
|
-
ref={popup as any}
|
|
210
|
-
class="popup"
|
|
211
|
-
style={{ display: 'none' }}
|
|
212
|
-
{...disableAutoCloseEvent}
|
|
213
|
-
ontransitionend={() => currentPopup.dom === popup || (popup.style.display = 'none')}
|
|
214
|
-
>
|
|
215
|
-
<div {...omitProps(props, OMIT_PROPS)}>{props.children}</div>
|
|
216
|
-
</div>
|
|
217
|
-
);
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* 关闭当前弹出层
|
|
222
|
-
*/
|
|
223
|
-
export const closePopup = () => {
|
|
224
|
-
currentPopup.dom && hidePopup();
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
// 注册点击关闭弹出层的方法
|
|
228
|
-
registerAutoClose(closePopup);
|