wok-ui-ext 0.1.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/LICENSE +21 -0
- package/README.en.md +75 -0
- package/README.md +75 -0
- package/dist/alert/index.d.ts +10 -0
- package/dist/alert/index.js +48 -0
- package/dist/alert/style.css +116 -0
- package/dist/avatar/index.d.ts +9 -0
- package/dist/avatar/index.js +30 -0
- package/dist/avatar/style.css +32 -0
- package/dist/badge/index.d.ts +32 -0
- package/dist/badge/index.js +61 -0
- package/dist/badge/style.css +68 -0
- package/dist/breadcrumb/index.d.ts +25 -0
- package/dist/breadcrumb/index.js +47 -0
- package/dist/breadcrumb/style.css +35 -0
- package/dist/button/index.d.ts +34 -0
- package/dist/button/index.js +57 -0
- package/dist/button/style.css +133 -0
- package/dist/card/index.d.ts +25 -0
- package/dist/card/index.js +29 -0
- package/dist/card/style.css +23 -0
- package/dist/collapse/index.d.ts +89 -0
- package/dist/collapse/index.js +213 -0
- package/dist/collapse/style.css +76 -0
- package/dist/context-menu/context-menu.d.ts +113 -0
- package/dist/context-menu/context-menu.js +150 -0
- package/dist/context-menu/index.d.ts +30 -0
- package/dist/context-menu/index.js +42 -0
- package/dist/context-menu/style.css +46 -0
- package/dist/date-picker/calendar-panel.d.ts +44 -0
- package/dist/date-picker/calendar-panel.js +180 -0
- package/dist/date-picker/date-picker.d.ts +29 -0
- package/dist/date-picker/date-picker.js +81 -0
- package/dist/date-picker/date-utils.d.ts +63 -0
- package/dist/date-picker/date-utils.js +130 -0
- package/dist/date-picker/index.d.ts +7 -0
- package/dist/date-picker/index.js +5 -0
- package/dist/date-picker/layer.d.ts +19 -0
- package/dist/date-picker/layer.js +36 -0
- package/dist/date-picker/range-picker.d.ts +30 -0
- package/dist/date-picker/range-picker.js +117 -0
- package/dist/date-picker/style.css +161 -0
- package/dist/description-list/index.d.ts +37 -0
- package/dist/description-list/index.js +145 -0
- package/dist/description-list/style.css +63 -0
- package/dist/divider/index.d.ts +29 -0
- package/dist/divider/index.js +29 -0
- package/dist/divider/style.css +50 -0
- package/dist/drawer/index.d.ts +51 -0
- package/dist/drawer/index.js +183 -0
- package/dist/drawer/style.css +100 -0
- package/dist/dropdown/index.d.ts +38 -0
- package/dist/dropdown/index.js +97 -0
- package/dist/dropdown/style.css +39 -0
- package/dist/empty/index.d.ts +11 -0
- package/dist/empty/index.js +20 -0
- package/dist/empty/style.css +19 -0
- package/dist/floating-layer/index.d.ts +109 -0
- package/dist/floating-layer/index.js +337 -0
- package/dist/floating-layer/style.css +5 -0
- package/dist/form/form-item/index.d.ts +26 -0
- package/dist/form/form-item/index.js +21 -0
- package/dist/form/form-item/style.css +33 -0
- package/dist/form/form-modal/index.d.ts +18 -0
- package/dist/form/form-modal/index.js +38 -0
- package/dist/form/form.d.ts +21 -0
- package/dist/form/form.js +28 -0
- package/dist/form/index.d.ts +4 -0
- package/dist/form/index.js +4 -0
- package/dist/form/select/index.d.ts +48 -0
- package/dist/form/select/index.js +94 -0
- package/dist/form/select/style.css +29 -0
- package/dist/global.css +71 -0
- package/dist/i18n/de-DE.d.ts +2 -0
- package/dist/i18n/de-DE.js +36 -0
- package/dist/i18n/en-US.d.ts +2 -0
- package/dist/i18n/en-US.js +36 -0
- package/dist/i18n/es-ES.d.ts +2 -0
- package/dist/i18n/es-ES.js +36 -0
- package/dist/i18n/fr-FR.d.ts +2 -0
- package/dist/i18n/fr-FR.js +36 -0
- package/dist/i18n/index.d.ts +12 -0
- package/dist/i18n/index.js +39 -0
- package/dist/i18n/ja-JP.d.ts +2 -0
- package/dist/i18n/ja-JP.js +36 -0
- package/dist/i18n/ko-KR.d.ts +2 -0
- package/dist/i18n/ko-KR.js +36 -0
- package/dist/i18n/messages.d.ts +40 -0
- package/dist/i18n/messages.js +1 -0
- package/dist/i18n/wok-ui/de-DE.d.ts +2 -0
- package/dist/i18n/wok-ui/de-DE.js +20 -0
- package/dist/i18n/wok-ui/es-ES.d.ts +2 -0
- package/dist/i18n/wok-ui/es-ES.js +20 -0
- package/dist/i18n/wok-ui/fr-FR.d.ts +2 -0
- package/dist/i18n/wok-ui/fr-FR.js +20 -0
- package/dist/i18n/wok-ui/ja-JP.d.ts +2 -0
- package/dist/i18n/wok-ui/ja-JP.js +20 -0
- package/dist/i18n/wok-ui/ko-KR.d.ts +2 -0
- package/dist/i18n/wok-ui/ko-KR.js +20 -0
- package/dist/i18n/wok-ui/zh-TW.d.ts +2 -0
- package/dist/i18n/wok-ui/zh-TW.js +20 -0
- package/dist/i18n/zh-CN.d.ts +2 -0
- package/dist/i18n/zh-CN.js +36 -0
- package/dist/i18n/zh-TW.d.ts +2 -0
- package/dist/i18n/zh-TW.js +36 -0
- package/dist/icons.d.ts +88 -0
- package/dist/icons.js +176 -0
- package/dist/image/index.d.ts +38 -0
- package/dist/image/index.js +97 -0
- package/dist/image/style.css +72 -0
- package/dist/image-preview/index.d.ts +26 -0
- package/dist/image-preview/index.js +134 -0
- package/dist/image-preview/style.css +96 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +45 -0
- package/dist/list/index.d.ts +32 -0
- package/dist/list/index.js +42 -0
- package/dist/list/list-item.d.ts +27 -0
- package/dist/list/list-item.js +51 -0
- package/dist/list/style.css +20 -0
- package/dist/loading/index.d.ts +10 -0
- package/dist/loading/index.js +46 -0
- package/dist/loading/style.css +48 -0
- package/dist/menu/index.d.ts +38 -0
- package/dist/menu/index.js +62 -0
- package/dist/menu/leaf-item.d.ts +12 -0
- package/dist/menu/leaf-item.js +41 -0
- package/dist/menu/style.css +117 -0
- package/dist/menu/sub-menu.d.ts +21 -0
- package/dist/menu/sub-menu.js +85 -0
- package/dist/menu/types.d.ts +36 -0
- package/dist/menu/types.js +1 -0
- package/dist/menu/utils.d.ts +11 -0
- package/dist/menu/utils.js +24 -0
- package/dist/modal/index.d.ts +23 -0
- package/dist/modal/index.js +79 -0
- package/dist/modal/modal.d.ts +76 -0
- package/dist/modal/modal.js +188 -0
- package/dist/modal/style.css +90 -0
- package/dist/pagination/index.d.ts +31 -0
- package/dist/pagination/index.js +215 -0
- package/dist/pagination/style.css +50 -0
- package/dist/popconfirm/api.d.ts +43 -0
- package/dist/popconfirm/api.js +77 -0
- package/dist/popconfirm/component.d.ts +47 -0
- package/dist/popconfirm/component.js +48 -0
- package/dist/popconfirm/index.d.ts +3 -0
- package/dist/popconfirm/index.js +3 -0
- package/dist/popconfirm/style.css +22 -0
- package/dist/popover/api.d.ts +36 -0
- package/dist/popover/api.js +61 -0
- package/dist/popover/component.d.ts +38 -0
- package/dist/popover/component.js +51 -0
- package/dist/popover/index.d.ts +3 -0
- package/dist/popover/index.js +3 -0
- package/dist/popover/style.css +102 -0
- package/dist/progress/index.d.ts +34 -0
- package/dist/progress/index.js +70 -0
- package/dist/progress/style.css +37 -0
- package/dist/quick-input/index.d.ts +2 -0
- package/dist/quick-input/index.js +2 -0
- package/dist/quick-input/number.d.ts +35 -0
- package/dist/quick-input/number.js +68 -0
- package/dist/quick-input/text.d.ts +20 -0
- package/dist/quick-input/text.js +61 -0
- package/dist/result/index.d.ts +39 -0
- package/dist/result/index.js +71 -0
- package/dist/result/style.css +41 -0
- package/dist/search-box/index.d.ts +26 -0
- package/dist/search-box/index.js +60 -0
- package/dist/search-box/style.css +22 -0
- package/dist/segmented/index.d.ts +42 -0
- package/dist/segmented/index.js +61 -0
- package/dist/segmented/style.css +28 -0
- package/dist/skeleton/index.d.ts +22 -0
- package/dist/skeleton/index.js +68 -0
- package/dist/skeleton/style.css +31 -0
- package/dist/steps/index.d.ts +46 -0
- package/dist/steps/index.js +95 -0
- package/dist/steps/style.css +181 -0
- package/dist/switch/index.d.ts +32 -0
- package/dist/switch/index.js +74 -0
- package/dist/switch/style.css +52 -0
- package/dist/table/body.d.ts +21 -0
- package/dist/table/body.js +35 -0
- package/dist/table/column.d.ts +78 -0
- package/dist/table/column.js +113 -0
- package/dist/table/header.d.ts +13 -0
- package/dist/table/header.js +15 -0
- package/dist/table/index.d.ts +41 -0
- package/dist/table/index.js +84 -0
- package/dist/table/row.d.ts +25 -0
- package/dist/table/row.js +74 -0
- package/dist/table/style.css +98 -0
- package/dist/tabs/index.d.ts +49 -0
- package/dist/tabs/index.js +76 -0
- package/dist/tabs/style.css +41 -0
- package/dist/tag/index.d.ts +53 -0
- package/dist/tag/index.js +100 -0
- package/dist/tag/style.css +134 -0
- package/dist/text/index.d.ts +104 -0
- package/dist/text/index.js +143 -0
- package/dist/text/style.css +113 -0
- package/dist/theme/index.d.ts +14 -0
- package/dist/theme/index.js +94 -0
- package/dist/theme/themes.d.ts +3 -0
- package/dist/theme/themes.js +323 -0
- package/dist/theme/types.d.ts +58 -0
- package/dist/theme/types.js +1 -0
- package/dist/timeline/index.d.ts +28 -0
- package/dist/timeline/index.js +35 -0
- package/dist/timeline/style.css +53 -0
- package/dist/toast/index.d.ts +53 -0
- package/dist/toast/index.js +131 -0
- package/dist/toast/style.css +76 -0
- package/dist/tooltip/api.d.ts +44 -0
- package/dist/tooltip/api.js +103 -0
- package/dist/tooltip/component.d.ts +28 -0
- package/dist/tooltip/component.js +35 -0
- package/dist/tooltip/index.d.ts +3 -0
- package/dist/tooltip/index.js +3 -0
- package/dist/tooltip/style.css +46 -0
- package/dist/tree/dragging-img.d.ts +13 -0
- package/dist/tree/dragging-img.js +23 -0
- package/dist/tree/index.d.ts +180 -0
- package/dist/tree/index.js +385 -0
- package/dist/tree/node-info.d.ts +63 -0
- package/dist/tree/node-info.js +128 -0
- package/dist/tree/node.d.ts +132 -0
- package/dist/tree/node.js +264 -0
- package/dist/tree/style.css +99 -0
- package/dist/upload/index.d.ts +48 -0
- package/dist/upload/index.js +141 -0
- package/dist/upload/style.css +41 -0
- package/package.json +51 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DivModule, SubModulesOpt } from 'wok-ui';
|
|
2
|
+
import { IconInput } from '../icons';
|
|
3
|
+
import './style.less';
|
|
4
|
+
interface DropdownItem {
|
|
5
|
+
/**
|
|
6
|
+
* 图标工厂函数。wok-ui 的模块实例不可复用(挂载过的实例销毁后无法再次挂载),
|
|
7
|
+
* 而 Dropdown 每次打开菜单都会重建内容,因此这里传函数,每次执行返回新的图标实例。
|
|
8
|
+
*/
|
|
9
|
+
icon?: () => IconInput;
|
|
10
|
+
text: string;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 下拉菜单组件
|
|
15
|
+
*/
|
|
16
|
+
export declare class Dropdown extends DivModule {
|
|
17
|
+
private readonly __align;
|
|
18
|
+
private readonly __items;
|
|
19
|
+
private readonly __onSelect?;
|
|
20
|
+
private __menu;
|
|
21
|
+
/**
|
|
22
|
+
* 下拉菜单组件
|
|
23
|
+
* @param opts
|
|
24
|
+
* @param opts.align 对齐方式,默认左对齐
|
|
25
|
+
* @param opts.children 子模块配置(触发元素)
|
|
26
|
+
* @param opts.items 下拉菜单项目
|
|
27
|
+
* @param opts.onSelect 选择项目回调,传递项目索引
|
|
28
|
+
*/
|
|
29
|
+
constructor(opts: {
|
|
30
|
+
align?: 'left' | 'right';
|
|
31
|
+
children: SubModulesOpt;
|
|
32
|
+
items: DropdownItem[];
|
|
33
|
+
onSelect?: (index: number) => void;
|
|
34
|
+
});
|
|
35
|
+
private __toggleMenu;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { DivModule } from 'wok-ui';
|
|
2
|
+
import { FloatingLayer } from '../floating-layer';
|
|
3
|
+
import { resolveIcon } from '../icons';
|
|
4
|
+
import './style.css';
|
|
5
|
+
/** 当前打开的下拉菜单,全局同时只存在一个 */
|
|
6
|
+
let activeDropdown = null;
|
|
7
|
+
/**
|
|
8
|
+
* 下拉菜单浮层。挂载到 body,由 FloatingLayer 提供定位、滚动跟随、自动关闭。
|
|
9
|
+
*/
|
|
10
|
+
class DropdownMenuLayer extends FloatingLayer {
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
super({ target: opts.target, placement: opts.placement, offset: 8, onClose: opts.onClose });
|
|
13
|
+
this.el.classList.add('wok-ui-ext-dropdown-menu');
|
|
14
|
+
this.items = opts.items;
|
|
15
|
+
this.onSelect = opts.onSelect;
|
|
16
|
+
}
|
|
17
|
+
buildContent() {
|
|
18
|
+
this.items.forEach((item, index) => {
|
|
19
|
+
this.addChild({
|
|
20
|
+
classNames: ['wok-ui-ext-dropdown-item', item.active ? 'active' : ''],
|
|
21
|
+
children: add => {
|
|
22
|
+
if (item.icon) {
|
|
23
|
+
add(resolveIcon(item.icon()));
|
|
24
|
+
}
|
|
25
|
+
add(` ${item.text}`);
|
|
26
|
+
},
|
|
27
|
+
onClick: () => {
|
|
28
|
+
// 先关闭菜单再回调,保证回调执行时菜单已消失
|
|
29
|
+
this.destroy();
|
|
30
|
+
this.onSelect?.(index);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
destroy() {
|
|
36
|
+
if (activeDropdown === this) {
|
|
37
|
+
activeDropdown = null;
|
|
38
|
+
}
|
|
39
|
+
super.destroy();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 下拉菜单组件
|
|
44
|
+
*/
|
|
45
|
+
export class Dropdown extends DivModule {
|
|
46
|
+
/**
|
|
47
|
+
* 下拉菜单组件
|
|
48
|
+
* @param opts
|
|
49
|
+
* @param opts.align 对齐方式,默认左对齐
|
|
50
|
+
* @param opts.children 子模块配置(触发元素)
|
|
51
|
+
* @param opts.items 下拉菜单项目
|
|
52
|
+
* @param opts.onSelect 选择项目回调,传递项目索引
|
|
53
|
+
*/
|
|
54
|
+
constructor(opts) {
|
|
55
|
+
super();
|
|
56
|
+
this.__menu = null;
|
|
57
|
+
this.el.classList.add('wok-ui-ext-dropdown');
|
|
58
|
+
this.__align = opts.align ?? 'left';
|
|
59
|
+
this.__items = opts.items;
|
|
60
|
+
this.__onSelect = opts.onSelect;
|
|
61
|
+
this.addChild(opts.children);
|
|
62
|
+
// 点击触发元素切换菜单开合
|
|
63
|
+
this.el.addEventListener('click', () => this.__toggleMenu());
|
|
64
|
+
}
|
|
65
|
+
__toggleMenu() {
|
|
66
|
+
// 当前菜单已打开则关闭(点击外部关闭后引用已被清空,无需处理)
|
|
67
|
+
if (this.__menu) {
|
|
68
|
+
this.__menu.destroy();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// 关闭其他 Dropdown 打开的菜单
|
|
72
|
+
if (activeDropdown) {
|
|
73
|
+
activeDropdown.destroy();
|
|
74
|
+
}
|
|
75
|
+
const menu = new DropdownMenuLayer({
|
|
76
|
+
target: this.el,
|
|
77
|
+
placement: this.__align === 'right' ? 'bottomRight' : 'bottomLeft',
|
|
78
|
+
items: this.__items,
|
|
79
|
+
onSelect: this.__onSelect,
|
|
80
|
+
onClose: () => {
|
|
81
|
+
if (this.__menu === menu) {
|
|
82
|
+
this.__menu = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
this.__menu = menu;
|
|
87
|
+
activeDropdown = menu;
|
|
88
|
+
menu.mount(document.body);
|
|
89
|
+
}
|
|
90
|
+
destroy() {
|
|
91
|
+
if (this.__menu) {
|
|
92
|
+
this.__menu.destroy();
|
|
93
|
+
this.__menu = null;
|
|
94
|
+
}
|
|
95
|
+
super.destroy();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* 下拉菜单 */
|
|
2
|
+
.wok-ui-ext-dropdown {
|
|
3
|
+
display: inline-block;
|
|
4
|
+
}
|
|
5
|
+
/* 菜单浮层,挂载到 body,定位与层级由 FloatingLayer 处理 */
|
|
6
|
+
.wok-ui-ext-dropdown-menu {
|
|
7
|
+
background: var(--bg-card);
|
|
8
|
+
border-radius: var(--radius-lg);
|
|
9
|
+
box-shadow: var(--shadow-lg);
|
|
10
|
+
border: 1px solid var(--color-border);
|
|
11
|
+
min-width: 180px;
|
|
12
|
+
padding: 8px 0;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
}
|
|
15
|
+
.wok-ui-ext-dropdown-menu .wok-ui-ext-dropdown-item {
|
|
16
|
+
padding: 10px 20px;
|
|
17
|
+
font-size: var(--font-size-small);
|
|
18
|
+
color: var(--text-secondary);
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
transition: background var(--transition-speed) ease;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 8px;
|
|
24
|
+
}
|
|
25
|
+
.wok-ui-ext-dropdown-menu .wok-ui-ext-dropdown-item:hover {
|
|
26
|
+
background: var(--bg-hover);
|
|
27
|
+
}
|
|
28
|
+
.wok-ui-ext-dropdown-menu .wok-ui-ext-dropdown-item.active {
|
|
29
|
+
background: var(--color-primary);
|
|
30
|
+
color: white;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
}
|
|
33
|
+
.wok-ui-ext-dropdown-menu .wok-ui-ext-dropdown-item.active i {
|
|
34
|
+
color: white;
|
|
35
|
+
}
|
|
36
|
+
.wok-ui-ext-dropdown-menu .wok-ui-ext-dropdown-item i {
|
|
37
|
+
width: 20px;
|
|
38
|
+
color: var(--color-primary);
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DivModule } from 'wok-ui';
|
|
2
|
+
import { getWokUiExtI18n } from '../i18n';
|
|
3
|
+
import { IconInbox } from '../icons';
|
|
4
|
+
import './style.css';
|
|
5
|
+
/**
|
|
6
|
+
* 空状态组件
|
|
7
|
+
*/
|
|
8
|
+
export class Empty extends DivModule {
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
super('wok-ui-ext-empty-state');
|
|
11
|
+
if (opts.noBorder) {
|
|
12
|
+
this.el.classList.add('no-border');
|
|
13
|
+
}
|
|
14
|
+
this.addChild(new IconInbox(), 30, {
|
|
15
|
+
tag: 'p',
|
|
16
|
+
className: 'desc',
|
|
17
|
+
innerText: opts.text || getWokUiExtI18n().buildMsg('empty-text')
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.wok-ui-ext-empty-state {
|
|
2
|
+
text-align: center;
|
|
3
|
+
padding: 36px 20px;
|
|
4
|
+
background: var(--bg-content);
|
|
5
|
+
border-radius: 32px;
|
|
6
|
+
border: 1px dashed var(--border-medium);
|
|
7
|
+
}
|
|
8
|
+
.wok-ui-ext-empty-state.no-border {
|
|
9
|
+
border: none;
|
|
10
|
+
background: none;
|
|
11
|
+
}
|
|
12
|
+
.wok-ui-ext-empty-state .wok-ui-svg-icon {
|
|
13
|
+
font-size: 56px;
|
|
14
|
+
color: var(--text-disabled);
|
|
15
|
+
}
|
|
16
|
+
.wok-ui-ext-empty-state p.desc {
|
|
17
|
+
font-size: var(--font-size-small);
|
|
18
|
+
color: var(--text-tertiary);
|
|
19
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { DivModule } from 'wok-ui';
|
|
2
|
+
import './style.less';
|
|
3
|
+
export type FloatingPlacement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight' | 'left' | 'leftTop' | 'leftBottom' | 'right' | 'rightTop' | 'rightBottom';
|
|
4
|
+
export interface FloatingLayerOptions {
|
|
5
|
+
/**
|
|
6
|
+
* 定位锚点元素。与 anchor 二选一:基于元素定位时传入,
|
|
7
|
+
* 支持滚动跟随、目标移除自动销毁。
|
|
8
|
+
*/
|
|
9
|
+
target?: HTMLElement;
|
|
10
|
+
/**
|
|
11
|
+
* 定位锚点坐标(视口坐标)。与 target 二选一:适用于光标定位等
|
|
12
|
+
* 没有固定锚点元素的场景(如右键菜单),此时以该点为零尺寸锚点定位。
|
|
13
|
+
*/
|
|
14
|
+
anchor?: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
};
|
|
18
|
+
/** 弹出位置,默认 bottom */
|
|
19
|
+
placement?: FloatingPlacement;
|
|
20
|
+
/** 与锚点的间距(px),默认 6 */
|
|
21
|
+
offset?: number;
|
|
22
|
+
/** 视口空间不足时自动翻转方向,默认 true */
|
|
23
|
+
autoFlip?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 页面滚动时的行为,默认 'follow'。
|
|
26
|
+
* - 'follow': 跟随锚点重新定位(target 模式适用)
|
|
27
|
+
* - 'destroy': 直接销毁浮层(菜单等瞬态浮层适用)
|
|
28
|
+
*/
|
|
29
|
+
scrollBehavior?: 'follow' | 'destroy';
|
|
30
|
+
/** 关闭回调,浮层销毁时触发(外部点击/Esc/目标移除/手动 destroy 均会触发) */
|
|
31
|
+
onClose?: () => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 通用浮层基类。
|
|
35
|
+
*
|
|
36
|
+
* 将自身挂载到 body 后即显示,核心能力:
|
|
37
|
+
* - 基于目标元素定位(fixed + 视口坐标),支持 8 个方向
|
|
38
|
+
* - 视口空间不足时自动翻转方向(bottom ↔ top、left ↔ right)
|
|
39
|
+
* - 滚动跟随:目标元素的可滚动祖先 + window 滚动时自动重新定位
|
|
40
|
+
* - 自动销毁:点击浮层外部、按 Esc、目标元素被移除时调用 destroy()
|
|
41
|
+
* - destroy() 内统一解除所有事件绑定
|
|
42
|
+
*
|
|
43
|
+
* 内容默认空白,子类通过覆写 buildContent() 添加内容。
|
|
44
|
+
* 注意:不要在构造或 mount 之外直接 addChild 添加内容,统一在 buildContent() 中构建,
|
|
45
|
+
* 因为 buildContent() 在 mount() 时执行,此时子类字段已完成初始化。
|
|
46
|
+
*
|
|
47
|
+
* 用法:
|
|
48
|
+
* ```ts
|
|
49
|
+
* class MyLayer extends FloatingLayer {
|
|
50
|
+
* protected buildContent(): void {
|
|
51
|
+
* this.addChild('hello')
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* new MyLayer({ target: btn.el }).mount(document.body)
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare abstract class FloatingLayer extends DivModule {
|
|
58
|
+
private readonly target?;
|
|
59
|
+
private readonly anchor?;
|
|
60
|
+
private readonly placement;
|
|
61
|
+
private readonly offset;
|
|
62
|
+
private readonly autoFlip;
|
|
63
|
+
private readonly scrollBehavior;
|
|
64
|
+
private readonly onClose?;
|
|
65
|
+
private __contentBuilt;
|
|
66
|
+
private __layerDestroyed;
|
|
67
|
+
private __scrollParents;
|
|
68
|
+
private readonly __boundScrollHandler;
|
|
69
|
+
private readonly __boundOutsideHandler;
|
|
70
|
+
private readonly __boundKeydownHandler;
|
|
71
|
+
constructor(opts: FloatingLayerOptions);
|
|
72
|
+
mount(parentEl: Element): void;
|
|
73
|
+
/**
|
|
74
|
+
* 构建浮层内容,默认空白,子类覆写。
|
|
75
|
+
*/
|
|
76
|
+
protected abstract buildContent(): void;
|
|
77
|
+
/**
|
|
78
|
+
* 重新计算浮层位置。滚动/窗口变化时自动调用,子类内容变化后也可主动调用。
|
|
79
|
+
*/
|
|
80
|
+
protected reposition(): void;
|
|
81
|
+
/**
|
|
82
|
+
* 获取定位锚点矩形。target 模式返回元素矩形,anchor 模式返回零尺寸矩形。
|
|
83
|
+
*/
|
|
84
|
+
private getAnchorRect;
|
|
85
|
+
/**
|
|
86
|
+
* 判断一个节点是否属于"浮层内部区域",用于外部点击判定。
|
|
87
|
+
* 子类可覆写以扩展内部区域(如上下文菜单的子菜单)。
|
|
88
|
+
*/
|
|
89
|
+
protected containsTarget(target: Node): boolean;
|
|
90
|
+
destroy(): void;
|
|
91
|
+
private __setupListeners;
|
|
92
|
+
private __cleanupListeners;
|
|
93
|
+
private __handleScroll;
|
|
94
|
+
private __handleOutsideClick;
|
|
95
|
+
private __handleKeydown;
|
|
96
|
+
/**
|
|
97
|
+
* 根据锚点周边可用空间决定实际方向:主方向放不下时翻转,例如 bottom 放不下翻到 top。
|
|
98
|
+
* 对向空间同样不足时保持原方向,由 clamp 做最后兜底。
|
|
99
|
+
*/
|
|
100
|
+
private __resolvePlacement;
|
|
101
|
+
/**
|
|
102
|
+
* 将浮层坐标限制在视口内(保留 8px 边距),防止浮层超出视口被截断。
|
|
103
|
+
* 自动翻转方向后仍可能因目标位置极端(如紧贴视口边缘)而溢出,这里做最后兜底。
|
|
104
|
+
* @param pos 计算出的坐标
|
|
105
|
+
* @param pw 浮层宽度
|
|
106
|
+
* @param ph 浮层高度
|
|
107
|
+
*/
|
|
108
|
+
private __clampToViewport;
|
|
109
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { DivModule } from 'wok-ui';
|
|
2
|
+
import './style.css';
|
|
3
|
+
function isScrollable(el) {
|
|
4
|
+
const style = getComputedStyle(el);
|
|
5
|
+
return /(auto|scroll|overlay)/.test(style.overflowX + style.overflowY);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 沿祖先链向上查找所有可滚动容器(用于滚动跟随)。
|
|
9
|
+
* window 由调用方单独监听。
|
|
10
|
+
*/
|
|
11
|
+
function getScrollParents(el) {
|
|
12
|
+
const parents = [];
|
|
13
|
+
let node = el.parentElement;
|
|
14
|
+
while (node) {
|
|
15
|
+
if (isScrollable(node)) {
|
|
16
|
+
parents.push(node);
|
|
17
|
+
}
|
|
18
|
+
node = node.parentElement;
|
|
19
|
+
}
|
|
20
|
+
return parents;
|
|
21
|
+
}
|
|
22
|
+
function calcPosition(placement, trigger, pw, ph, spacing) {
|
|
23
|
+
switch (placement) {
|
|
24
|
+
case 'top':
|
|
25
|
+
return {
|
|
26
|
+
top: trigger.top - ph - spacing,
|
|
27
|
+
left: trigger.left + trigger.width / 2 - pw / 2
|
|
28
|
+
};
|
|
29
|
+
case 'topLeft':
|
|
30
|
+
return {
|
|
31
|
+
top: trigger.top - ph - spacing,
|
|
32
|
+
left: trigger.left
|
|
33
|
+
};
|
|
34
|
+
case 'topRight':
|
|
35
|
+
return {
|
|
36
|
+
top: trigger.top - ph - spacing,
|
|
37
|
+
left: trigger.right - pw
|
|
38
|
+
};
|
|
39
|
+
case 'bottom':
|
|
40
|
+
return {
|
|
41
|
+
top: trigger.bottom + spacing,
|
|
42
|
+
left: trigger.left + trigger.width / 2 - pw / 2
|
|
43
|
+
};
|
|
44
|
+
case 'bottomLeft':
|
|
45
|
+
return {
|
|
46
|
+
top: trigger.bottom + spacing,
|
|
47
|
+
left: trigger.left
|
|
48
|
+
};
|
|
49
|
+
case 'bottomRight':
|
|
50
|
+
return {
|
|
51
|
+
top: trigger.bottom + spacing,
|
|
52
|
+
left: trigger.right - pw
|
|
53
|
+
};
|
|
54
|
+
case 'left':
|
|
55
|
+
return {
|
|
56
|
+
top: trigger.top + trigger.height / 2 - ph / 2,
|
|
57
|
+
left: trigger.left - pw - spacing
|
|
58
|
+
};
|
|
59
|
+
case 'leftTop':
|
|
60
|
+
return {
|
|
61
|
+
top: trigger.top,
|
|
62
|
+
left: trigger.left - pw - spacing
|
|
63
|
+
};
|
|
64
|
+
case 'leftBottom':
|
|
65
|
+
return {
|
|
66
|
+
top: trigger.bottom - ph,
|
|
67
|
+
left: trigger.left - pw - spacing
|
|
68
|
+
};
|
|
69
|
+
case 'right':
|
|
70
|
+
return {
|
|
71
|
+
top: trigger.top + trigger.height / 2 - ph / 2,
|
|
72
|
+
left: trigger.right + spacing
|
|
73
|
+
};
|
|
74
|
+
case 'rightTop':
|
|
75
|
+
return {
|
|
76
|
+
top: trigger.top,
|
|
77
|
+
left: trigger.right + spacing
|
|
78
|
+
};
|
|
79
|
+
case 'rightBottom':
|
|
80
|
+
return {
|
|
81
|
+
top: trigger.bottom - ph,
|
|
82
|
+
left: trigger.right + spacing
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 返回指定方向翻转后的对向方向(bottom ↔ top、left ↔ right),
|
|
88
|
+
* 用于视口空间不足时的自动翻转。
|
|
89
|
+
*/
|
|
90
|
+
function flipPlacement(p) {
|
|
91
|
+
switch (p) {
|
|
92
|
+
case 'top':
|
|
93
|
+
return 'bottom';
|
|
94
|
+
case 'bottom':
|
|
95
|
+
return 'top';
|
|
96
|
+
case 'topLeft':
|
|
97
|
+
return 'bottomLeft';
|
|
98
|
+
case 'topRight':
|
|
99
|
+
return 'bottomRight';
|
|
100
|
+
case 'bottomLeft':
|
|
101
|
+
return 'topLeft';
|
|
102
|
+
case 'bottomRight':
|
|
103
|
+
return 'topRight';
|
|
104
|
+
case 'left':
|
|
105
|
+
return 'right';
|
|
106
|
+
case 'right':
|
|
107
|
+
return 'left';
|
|
108
|
+
case 'leftTop':
|
|
109
|
+
return 'rightTop';
|
|
110
|
+
case 'leftBottom':
|
|
111
|
+
return 'rightBottom';
|
|
112
|
+
case 'rightTop':
|
|
113
|
+
return 'leftTop';
|
|
114
|
+
case 'rightBottom':
|
|
115
|
+
return 'leftBottom';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 通用浮层基类。
|
|
120
|
+
*
|
|
121
|
+
* 将自身挂载到 body 后即显示,核心能力:
|
|
122
|
+
* - 基于目标元素定位(fixed + 视口坐标),支持 8 个方向
|
|
123
|
+
* - 视口空间不足时自动翻转方向(bottom ↔ top、left ↔ right)
|
|
124
|
+
* - 滚动跟随:目标元素的可滚动祖先 + window 滚动时自动重新定位
|
|
125
|
+
* - 自动销毁:点击浮层外部、按 Esc、目标元素被移除时调用 destroy()
|
|
126
|
+
* - destroy() 内统一解除所有事件绑定
|
|
127
|
+
*
|
|
128
|
+
* 内容默认空白,子类通过覆写 buildContent() 添加内容。
|
|
129
|
+
* 注意:不要在构造或 mount 之外直接 addChild 添加内容,统一在 buildContent() 中构建,
|
|
130
|
+
* 因为 buildContent() 在 mount() 时执行,此时子类字段已完成初始化。
|
|
131
|
+
*
|
|
132
|
+
* 用法:
|
|
133
|
+
* ```ts
|
|
134
|
+
* class MyLayer extends FloatingLayer {
|
|
135
|
+
* protected buildContent(): void {
|
|
136
|
+
* this.addChild('hello')
|
|
137
|
+
* }
|
|
138
|
+
* }
|
|
139
|
+
* new MyLayer({ target: btn.el }).mount(document.body)
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export class FloatingLayer extends DivModule {
|
|
143
|
+
constructor(opts) {
|
|
144
|
+
super('wok-ui-ext-floating-layer');
|
|
145
|
+
this.__contentBuilt = false;
|
|
146
|
+
this.__layerDestroyed = false;
|
|
147
|
+
this.__scrollParents = [];
|
|
148
|
+
this.__boundScrollHandler = () => this.__handleScroll();
|
|
149
|
+
this.__boundOutsideHandler = (e) => this.__handleOutsideClick(e);
|
|
150
|
+
this.__boundKeydownHandler = (e) => this.__handleKeydown(e);
|
|
151
|
+
this.target = opts.target;
|
|
152
|
+
this.anchor = opts.anchor;
|
|
153
|
+
this.placement = opts.placement ?? 'bottom';
|
|
154
|
+
this.offset = opts.offset ?? 6;
|
|
155
|
+
this.autoFlip = opts.autoFlip ?? true;
|
|
156
|
+
this.scrollBehavior = opts.scrollBehavior ?? 'follow';
|
|
157
|
+
this.onClose = opts.onClose;
|
|
158
|
+
}
|
|
159
|
+
mount(parentEl) {
|
|
160
|
+
super.mount(parentEl);
|
|
161
|
+
if (!this.__contentBuilt) {
|
|
162
|
+
this.__contentBuilt = true;
|
|
163
|
+
this.buildContent();
|
|
164
|
+
}
|
|
165
|
+
this.__setupListeners();
|
|
166
|
+
// 内容刚挂载,等待浏览器完成布局后再定位
|
|
167
|
+
requestAnimationFrame(() => {
|
|
168
|
+
if (!this.__layerDestroyed && this.el.isConnected) {
|
|
169
|
+
this.reposition();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 重新计算浮层位置。滚动/窗口变化时自动调用,子类内容变化后也可主动调用。
|
|
175
|
+
*/
|
|
176
|
+
reposition() {
|
|
177
|
+
if (this.__layerDestroyed ||
|
|
178
|
+
!this.el.isConnected ||
|
|
179
|
+
(this.target && !this.target.isConnected)) {
|
|
180
|
+
this.destroy();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const rect = this.getAnchorRect();
|
|
184
|
+
// 锚点完全滚出视口(不可见)时销毁浮层,避免其被 clamp 在视口边缘常驻
|
|
185
|
+
if (rect.bottom < 0 ||
|
|
186
|
+
rect.top > window.innerHeight ||
|
|
187
|
+
rect.right < 0 ||
|
|
188
|
+
rect.left > window.innerWidth) {
|
|
189
|
+
this.destroy();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const placement = this.__resolvePlacement();
|
|
193
|
+
const pw = this.el.offsetWidth;
|
|
194
|
+
const ph = this.el.offsetHeight;
|
|
195
|
+
const pos = calcPosition(placement, rect, pw, ph, this.offset);
|
|
196
|
+
const { top, left } = this.__clampToViewport(pos, pw, ph);
|
|
197
|
+
this.el.style.top = `${top}px`;
|
|
198
|
+
this.el.style.left = `${left}px`;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 获取定位锚点矩形。target 模式返回元素矩形,anchor 模式返回零尺寸矩形。
|
|
202
|
+
*/
|
|
203
|
+
getAnchorRect() {
|
|
204
|
+
if (this.target) {
|
|
205
|
+
return this.target.getBoundingClientRect();
|
|
206
|
+
}
|
|
207
|
+
const a = this.anchor;
|
|
208
|
+
return {
|
|
209
|
+
left: a.x,
|
|
210
|
+
top: a.y,
|
|
211
|
+
right: a.x,
|
|
212
|
+
bottom: a.y,
|
|
213
|
+
width: 0,
|
|
214
|
+
height: 0
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 判断一个节点是否属于"浮层内部区域",用于外部点击判定。
|
|
219
|
+
* 子类可覆写以扩展内部区域(如上下文菜单的子菜单)。
|
|
220
|
+
*/
|
|
221
|
+
containsTarget(target) {
|
|
222
|
+
if (this.el.contains(target))
|
|
223
|
+
return true;
|
|
224
|
+
if (this.target && this.target.contains(target))
|
|
225
|
+
return true;
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
destroy() {
|
|
229
|
+
if (this.__layerDestroyed)
|
|
230
|
+
return;
|
|
231
|
+
this.__layerDestroyed = true;
|
|
232
|
+
this.__cleanupListeners();
|
|
233
|
+
this.onClose?.();
|
|
234
|
+
super.destroy();
|
|
235
|
+
}
|
|
236
|
+
// ─── 事件监听 ──────────────────────────────────────
|
|
237
|
+
__setupListeners() {
|
|
238
|
+
// anchor 模式没有锚点元素,无滚动跟随的祖先容器
|
|
239
|
+
if (this.target) {
|
|
240
|
+
this.__scrollParents = getScrollParents(this.target);
|
|
241
|
+
for (const parent of this.__scrollParents) {
|
|
242
|
+
parent.addEventListener('scroll', this.__boundScrollHandler);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
window.addEventListener('scroll', this.__boundScrollHandler);
|
|
246
|
+
window.addEventListener('resize', this.__boundScrollHandler);
|
|
247
|
+
document.addEventListener('mousedown', this.__boundOutsideHandler);
|
|
248
|
+
document.addEventListener('keydown', this.__boundKeydownHandler);
|
|
249
|
+
}
|
|
250
|
+
__cleanupListeners() {
|
|
251
|
+
for (const parent of this.__scrollParents) {
|
|
252
|
+
parent.removeEventListener('scroll', this.__boundScrollHandler);
|
|
253
|
+
}
|
|
254
|
+
this.__scrollParents = [];
|
|
255
|
+
window.removeEventListener('scroll', this.__boundScrollHandler);
|
|
256
|
+
window.removeEventListener('resize', this.__boundScrollHandler);
|
|
257
|
+
document.removeEventListener('mousedown', this.__boundOutsideHandler);
|
|
258
|
+
document.removeEventListener('keydown', this.__boundKeydownHandler);
|
|
259
|
+
}
|
|
260
|
+
__handleScroll() {
|
|
261
|
+
if (this.__layerDestroyed)
|
|
262
|
+
return;
|
|
263
|
+
if (this.scrollBehavior === 'destroy') {
|
|
264
|
+
this.destroy();
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.reposition();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
__handleOutsideClick(e) {
|
|
271
|
+
const target = e.target;
|
|
272
|
+
// 触发点击的元素可能因重新渲染已被移除(如点击浮层/目标自身导致的 DOM 重建),
|
|
273
|
+
// 此时冒泡到 document 的事件不应被当作"点击外部"处理
|
|
274
|
+
if (!target || !target.isConnected)
|
|
275
|
+
return;
|
|
276
|
+
if (this.containsTarget(target))
|
|
277
|
+
return;
|
|
278
|
+
this.destroy();
|
|
279
|
+
}
|
|
280
|
+
__handleKeydown(e) {
|
|
281
|
+
if (e.key === 'Escape') {
|
|
282
|
+
this.destroy();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// ─── 定位计算 ──────────────────────────────────────
|
|
286
|
+
/**
|
|
287
|
+
* 根据锚点周边可用空间决定实际方向:主方向放不下时翻转,例如 bottom 放不下翻到 top。
|
|
288
|
+
* 对向空间同样不足时保持原方向,由 clamp 做最后兜底。
|
|
289
|
+
*/
|
|
290
|
+
__resolvePlacement() {
|
|
291
|
+
if (!this.autoFlip)
|
|
292
|
+
return this.placement;
|
|
293
|
+
const rect = this.getAnchorRect();
|
|
294
|
+
const pw = this.el.offsetWidth;
|
|
295
|
+
const ph = this.el.offsetHeight;
|
|
296
|
+
const margin = 8;
|
|
297
|
+
const p = this.placement;
|
|
298
|
+
const isVertical = p.startsWith('top') || p.startsWith('bottom');
|
|
299
|
+
if (isVertical) {
|
|
300
|
+
const spaceMain = p.startsWith('bottom') ? window.innerHeight - rect.bottom : rect.top;
|
|
301
|
+
const spaceOpposite = p.startsWith('bottom') ? rect.top : window.innerHeight - rect.bottom;
|
|
302
|
+
const need = ph + this.offset + margin;
|
|
303
|
+
if (spaceMain < need && spaceOpposite > need) {
|
|
304
|
+
return flipPlacement(p);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const spaceMain = p.startsWith('right') ? window.innerWidth - rect.right : rect.left;
|
|
309
|
+
const spaceOpposite = p.startsWith('right') ? rect.left : window.innerWidth - rect.right;
|
|
310
|
+
const need = pw + this.offset + margin;
|
|
311
|
+
if (spaceMain < need && spaceOpposite > need) {
|
|
312
|
+
return flipPlacement(p);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return p;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 将浮层坐标限制在视口内(保留 8px 边距),防止浮层超出视口被截断。
|
|
319
|
+
* 自动翻转方向后仍可能因目标位置极端(如紧贴视口边缘)而溢出,这里做最后兜底。
|
|
320
|
+
* @param pos 计算出的坐标
|
|
321
|
+
* @param pw 浮层宽度
|
|
322
|
+
* @param ph 浮层高度
|
|
323
|
+
*/
|
|
324
|
+
__clampToViewport(pos, pw, ph) {
|
|
325
|
+
const margin = 8;
|
|
326
|
+
let { top, left } = pos;
|
|
327
|
+
if (left < margin)
|
|
328
|
+
left = margin;
|
|
329
|
+
else if (left + pw > window.innerWidth - margin)
|
|
330
|
+
left = window.innerWidth - pw - margin;
|
|
331
|
+
if (top < margin)
|
|
332
|
+
top = margin;
|
|
333
|
+
else if (top + ph > window.innerHeight - margin)
|
|
334
|
+
top = window.innerHeight - ph - margin;
|
|
335
|
+
return { top, left };
|
|
336
|
+
}
|
|
337
|
+
}
|