owl-tiptap 1.0.1 → 1.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/es/components/tag-input/GroupExample.d.ts +24 -0
- package/es/components/tag-input/GroupExample.js +54 -0
- package/es/components/tag-input/SuggestionList.d.ts +16 -1
- package/es/components/tag-input/SuggestionList.js +102 -19
- package/es/components/tag-input/TagInput.css +22 -0
- package/es/components/tag-input/TagInput.d.ts +5 -3
- package/es/components/tag-input/TagInput.js +3 -3
- package/es/components/tag-input/suggestion.d.ts +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type SuggestionItem, type GroupItem } from './TagInput';
|
|
2
|
+
export declare function ExampleUsage(): {
|
|
3
|
+
normalTagInput: {
|
|
4
|
+
items: ({ query }: {
|
|
5
|
+
query: string;
|
|
6
|
+
}) => SuggestionItem[];
|
|
7
|
+
};
|
|
8
|
+
structuredGroupedTagInput: {
|
|
9
|
+
items: ({ query }: {
|
|
10
|
+
query: string;
|
|
11
|
+
}) => ({
|
|
12
|
+
items: any;
|
|
13
|
+
value: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
} | {
|
|
17
|
+
items: any;
|
|
18
|
+
group: string;
|
|
19
|
+
})[];
|
|
20
|
+
slots: {
|
|
21
|
+
listGroupHeader: (item: GroupItem) => string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// 示例1: 普通列表(保持原有结构)
|
|
2
|
+
const normalItems = [
|
|
3
|
+
{ value: 'apple', label: '苹果' },
|
|
4
|
+
{ value: 'banana', label: '香蕉' },
|
|
5
|
+
{ value: 'orange', label: '橙子' },
|
|
6
|
+
];
|
|
7
|
+
// 示例2: 分组列表(通过GroupItem结构)
|
|
8
|
+
const structuredGroupedItems = [
|
|
9
|
+
{
|
|
10
|
+
group: '水果',
|
|
11
|
+
items: [
|
|
12
|
+
{ value: 'apple', label: '苹果' },
|
|
13
|
+
{ value: 'banana', label: '香蕉' },
|
|
14
|
+
{ value: 'orange', label: '橙子' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
group: '蔬菜',
|
|
19
|
+
items: [
|
|
20
|
+
{ value: 'carrot', label: '胡萝卜' },
|
|
21
|
+
{ value: 'broccoli', label: '西兰花' },
|
|
22
|
+
{ value: 'tomato', label: '西红柿' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
// 使用示例
|
|
27
|
+
export function ExampleUsage() {
|
|
28
|
+
return {
|
|
29
|
+
// 普通列表用法(与之前完全相同)
|
|
30
|
+
normalTagInput: {
|
|
31
|
+
items: ({ query }) => {
|
|
32
|
+
return normalItems.filter((item) => item.label?.toLowerCase().includes(query.toLowerCase()));
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
// 分组列表用法(通过GroupItem结构)
|
|
36
|
+
structuredGroupedTagInput: {
|
|
37
|
+
items: ({ query }) => {
|
|
38
|
+
// 对于结构化分组,需要过滤每个分组内的项目
|
|
39
|
+
return structuredGroupedItems
|
|
40
|
+
.map((group) => ({
|
|
41
|
+
...group,
|
|
42
|
+
items: group.items.filter((item) => item.label?.toLowerCase().includes(query.toLowerCase())),
|
|
43
|
+
}))
|
|
44
|
+
.filter((group) => group.items.length > 0); // 只返回有匹配项的分组
|
|
45
|
+
},
|
|
46
|
+
// 自定义分组标题样式
|
|
47
|
+
slots: {
|
|
48
|
+
listGroupHeader: (item) => {
|
|
49
|
+
return `<div style="color: #666; font-weight: bold; padding: 6px 8px; background: #f5f5f5;">${item.group}</div>`;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -5,12 +5,19 @@ export interface SuggestionItem {
|
|
|
5
5
|
disabled?: boolean;
|
|
6
6
|
[key: string]: any;
|
|
7
7
|
}
|
|
8
|
-
interface
|
|
8
|
+
export interface GroupItem {
|
|
9
|
+
group: string;
|
|
9
10
|
items: SuggestionItem[];
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
export type ListItem = SuggestionItem | GroupItem;
|
|
14
|
+
interface SuggestionListProps {
|
|
15
|
+
items: ListItem[];
|
|
10
16
|
onSelect: (event: MouseEvent, item: SuggestionItem) => void;
|
|
11
17
|
slots?: {
|
|
12
18
|
listHeader?: any;
|
|
13
19
|
listItem?: any;
|
|
20
|
+
listGroupHeader?: any;
|
|
14
21
|
listEmpty?: any;
|
|
15
22
|
listFooter?: any;
|
|
16
23
|
};
|
|
@@ -36,6 +43,14 @@ export declare class SuggestionList extends Component<SuggestionListProps> {
|
|
|
36
43
|
optional: boolean;
|
|
37
44
|
};
|
|
38
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* 检查项目是否为分组项
|
|
48
|
+
*/
|
|
49
|
+
private isGroupItem;
|
|
50
|
+
/**
|
|
51
|
+
* 获取处理后的项目列表
|
|
52
|
+
*/
|
|
53
|
+
get processedItems(): ListItem[];
|
|
39
54
|
static template: string;
|
|
40
55
|
state: SuggestionListState;
|
|
41
56
|
listRef: {
|
|
@@ -25,12 +25,49 @@ export class SuggestionList extends Component {
|
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* 检查项目是否为分组项
|
|
30
|
+
*/
|
|
31
|
+
isGroupItem(item) {
|
|
32
|
+
return item && typeof item === 'object' && 'group' in item;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 获取处理后的项目列表
|
|
36
|
+
*/
|
|
37
|
+
get processedItems() {
|
|
38
|
+
// 检查是否有分组项结构
|
|
39
|
+
const hasGroupItems = this.props.items.some((item) => this.isGroupItem(item));
|
|
40
|
+
// 如果有分组项,则直接返回
|
|
41
|
+
if (hasGroupItems) {
|
|
42
|
+
return this.props.items;
|
|
43
|
+
}
|
|
44
|
+
// 否则返回原始项目(普通列表)
|
|
45
|
+
return this.props.items;
|
|
46
|
+
}
|
|
28
47
|
onSelect(ev, item) {
|
|
29
|
-
!item.disabled
|
|
48
|
+
if (!item.disabled) {
|
|
49
|
+
this.props.onSelect?.(ev, item);
|
|
50
|
+
}
|
|
30
51
|
}
|
|
31
52
|
setup() {
|
|
32
53
|
useEffect(() => {
|
|
33
|
-
|
|
54
|
+
// 找到第一个非禁用且非分组标题的项
|
|
55
|
+
let selectedItem = null;
|
|
56
|
+
for (const item of this.processedItems) {
|
|
57
|
+
if (this.isGroupItem(item)) {
|
|
58
|
+
// 如果是分组项,查找分组内的第一个非禁用项
|
|
59
|
+
const firstSelectable = item.items.find((subItem) => !subItem.disabled);
|
|
60
|
+
if (firstSelectable) {
|
|
61
|
+
selectedItem = firstSelectable;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// 如果是普通项且非禁用
|
|
67
|
+
selectedItem = item;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
34
71
|
if (this.state.selectedItem?.value !== selectedItem?.value) {
|
|
35
72
|
this.state.selectedItem = selectedItem;
|
|
36
73
|
}
|
|
@@ -46,9 +83,19 @@ export class SuggestionList extends Component {
|
|
|
46
83
|
});
|
|
47
84
|
}
|
|
48
85
|
upHandler() {
|
|
49
|
-
const { items } = this.props;
|
|
50
86
|
const { selectedItem } = this.state;
|
|
51
|
-
|
|
87
|
+
// 收集所有可选择的项目
|
|
88
|
+
const selectableItems = [];
|
|
89
|
+
for (const item of this.processedItems) {
|
|
90
|
+
if (this.isGroupItem(item)) {
|
|
91
|
+
// 如果是分组项,添加分组内的非禁用项
|
|
92
|
+
selectableItems.push(...item.items.filter((subItem) => !subItem.disabled));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// 如果是普通项且非禁用
|
|
96
|
+
selectableItems.push(item);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
52
99
|
if (!selectableItems.length) {
|
|
53
100
|
return;
|
|
54
101
|
}
|
|
@@ -57,9 +104,19 @@ export class SuggestionList extends Component {
|
|
|
57
104
|
this.state.selectedItem = selectableItems[newIndex];
|
|
58
105
|
}
|
|
59
106
|
downHandler() {
|
|
60
|
-
const { items } = this.props;
|
|
61
107
|
const { selectedItem } = this.state;
|
|
62
|
-
|
|
108
|
+
// 收集所有可选择的项目
|
|
109
|
+
const selectableItems = [];
|
|
110
|
+
for (const item of this.processedItems) {
|
|
111
|
+
if (this.isGroupItem(item)) {
|
|
112
|
+
// 如果是分组项,添加分组内的非禁用项
|
|
113
|
+
selectableItems.push(...item.items.filter((subItem) => !subItem.disabled));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// 如果是普通项且非禁用
|
|
117
|
+
selectableItems.push(item);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
63
120
|
if (!selectableItems.length) {
|
|
64
121
|
return;
|
|
65
122
|
}
|
|
@@ -102,20 +159,46 @@ SuggestionList.template = xml `
|
|
|
102
159
|
<div class="${classNames('&suggestion-list')}" t-att-class="props.className" t-on-mousedown="(event) => event.preventDefault()" t-ref="list">
|
|
103
160
|
<t t-slot="listHeader"/>
|
|
104
161
|
|
|
105
|
-
<t t-if="
|
|
106
|
-
<t t-foreach="
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<t t-
|
|
116
|
-
<
|
|
162
|
+
<t t-if="processedItems.length">
|
|
163
|
+
<t t-foreach="processedItems" t-as="item" t-key="item.value || item.group">
|
|
164
|
+
<!-- 分组项 -->
|
|
165
|
+
<t t-if="this.isGroupItem(item)">
|
|
166
|
+
<div class="${classNames('&suggestion-header')}">
|
|
167
|
+
<t t-slot="listGroupHeader" item="item">
|
|
168
|
+
<span t-esc="item.group" />
|
|
169
|
+
<svg class="svg-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15717" width="200" height="200"><path d="M320 928c-9.6 0-16-3.2-22.4-9.6-12.8-12.8-12.8-32 0-44.8L659.2 512 297.6 150.4c-12.8-12.8-12.8-32 0-44.8s32-12.8 44.8 0l384 384c12.8 12.8 12.8 32 0 44.8l-384 384c-6.4 6.4-12.8 9.6-22.4 9.6z" fill="#333333" p-id="15718"></path></svg>
|
|
170
|
+
</t>
|
|
171
|
+
</div>
|
|
172
|
+
<t t-foreach="item.items" t-as="subItem" t-key="subItem.value">
|
|
173
|
+
<div
|
|
174
|
+
t-att-class="{
|
|
175
|
+
'is-selected': subItem.value === state.selectedItem?.value,
|
|
176
|
+
'is-disabled': subItem.disabled,
|
|
177
|
+
}"
|
|
178
|
+
t-on-click="(ev) => this.onSelect(ev, subItem)"
|
|
179
|
+
t-att-data-value="subItem.value"
|
|
180
|
+
>
|
|
181
|
+
<t t-slot="listItem" item="subItem">
|
|
182
|
+
<t t-esc="subItem.label" />
|
|
183
|
+
</t>
|
|
184
|
+
</div>
|
|
117
185
|
</t>
|
|
118
|
-
</
|
|
186
|
+
</t>
|
|
187
|
+
<!-- 普通项 -->
|
|
188
|
+
<t t-else="">
|
|
189
|
+
<div
|
|
190
|
+
t-att-class="{
|
|
191
|
+
'is-selected': item.value === state.selectedItem?.value,
|
|
192
|
+
'is-disabled': item.disabled,
|
|
193
|
+
}"
|
|
194
|
+
t-on-click="(ev) => this.onSelect(ev, item)"
|
|
195
|
+
t-att-data-value="item.value"
|
|
196
|
+
>
|
|
197
|
+
<t t-slot="listItem" item="item">
|
|
198
|
+
<t t-esc="item.label" />
|
|
199
|
+
</t>
|
|
200
|
+
</div>
|
|
201
|
+
</t>
|
|
119
202
|
</t>
|
|
120
203
|
</t>
|
|
121
204
|
|
|
@@ -24,6 +24,28 @@
|
|
|
24
24
|
.ott-tag-input.disabled .tag-wrapper .tag {
|
|
25
25
|
pointer-events: none;
|
|
26
26
|
}
|
|
27
|
+
.ott-tag-input .ott-suggestion-header {
|
|
28
|
+
font-weight: normal;
|
|
29
|
+
color: var(--ott-text-color-secondary);
|
|
30
|
+
background-color: #f5f5f5;
|
|
31
|
+
cursor: default !important;
|
|
32
|
+
padding: 6px 8px;
|
|
33
|
+
border-bottom: 1px solid #eee;
|
|
34
|
+
margin-top: 3px;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
}
|
|
38
|
+
.ott-tag-input .ott-suggestion-header:first-child {
|
|
39
|
+
margin-top: 0;
|
|
40
|
+
}
|
|
41
|
+
.ott-tag-input .ott-suggestion-header .svg-icon {
|
|
42
|
+
width: 14px;
|
|
43
|
+
height: 14px;
|
|
44
|
+
}
|
|
45
|
+
.ott-tag-input .ott-suggestion-header:hover {
|
|
46
|
+
background-color: #f5f5f5 !important;
|
|
47
|
+
color: var(--ott-text-color-secondary) !important;
|
|
48
|
+
}
|
|
27
49
|
.ott-tag-input .ProseMirror {
|
|
28
50
|
padding: 8px;
|
|
29
51
|
border: 1px solid var(--ott-border-color);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Component } from '@odoo/owl';
|
|
2
2
|
import { Editor } from '@tiptap/core';
|
|
3
|
-
import { SuggestionList
|
|
3
|
+
import { SuggestionList } from './SuggestionList';
|
|
4
|
+
import type { SuggestionItem, ListItem } from './SuggestionList';
|
|
5
|
+
export type { SuggestionItem, ListItem, GroupItem } from './SuggestionList';
|
|
4
6
|
import './TagInput.css';
|
|
5
7
|
export interface TagAttributes {
|
|
6
8
|
value: string;
|
|
@@ -32,6 +34,7 @@ interface TagInputProps {
|
|
|
32
34
|
slots?: {
|
|
33
35
|
listHeader?: any;
|
|
34
36
|
listItem?: any;
|
|
37
|
+
listGroupHeader?: any;
|
|
35
38
|
listEmpty?: any;
|
|
36
39
|
listFooter?: any;
|
|
37
40
|
};
|
|
@@ -39,7 +42,7 @@ interface TagInputProps {
|
|
|
39
42
|
onSuggestionSelect?: (event: MouseEvent, item: SuggestionItem) => void;
|
|
40
43
|
items?: (params: {
|
|
41
44
|
query: string;
|
|
42
|
-
}) =>
|
|
45
|
+
}) => ListItem[];
|
|
43
46
|
onChange?: (content: any) => void;
|
|
44
47
|
readonly?: boolean;
|
|
45
48
|
onTagClick?: (index: number, attrs: TagAttributes) => void;
|
|
@@ -149,4 +152,3 @@ export declare class TagInput extends Component<TagInputProps> {
|
|
|
149
152
|
destroyEditor(): void;
|
|
150
153
|
setup(): void;
|
|
151
154
|
}
|
|
152
|
-
export {};
|
|
@@ -8,7 +8,7 @@ import History from '@tiptap/extension-history';
|
|
|
8
8
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
9
9
|
import { SuggestionList } from './SuggestionList';
|
|
10
10
|
import { TagNode } from './TagNode';
|
|
11
|
-
import { SuggestionPlugin
|
|
11
|
+
import { SuggestionPlugin } from './suggestion';
|
|
12
12
|
import './TagInput.css';
|
|
13
13
|
export class TagInput extends Component {
|
|
14
14
|
constructor() {
|
|
@@ -189,8 +189,8 @@ export class TagInput extends Component {
|
|
|
189
189
|
],
|
|
190
190
|
content: null,
|
|
191
191
|
onBlur: ({ editor }) => {
|
|
192
|
-
this.state.suggestion.visible = false;
|
|
193
|
-
exitSuggestionPlugin(editor.view);
|
|
192
|
+
// this.state.suggestion.visible = false;
|
|
193
|
+
// exitSuggestionPlugin(editor.view);
|
|
194
194
|
},
|
|
195
195
|
onUpdate: ({ editor, transaction }) => {
|
|
196
196
|
if (transaction.docChanged) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Extension } from '@tiptap/core';
|
|
2
|
-
import {
|
|
2
|
+
import { ListItem } from './SuggestionList';
|
|
3
3
|
import { PluginKey } from 'prosemirror-state';
|
|
4
4
|
interface SuggestionPluginOptions {
|
|
5
5
|
char?: string;
|
|
6
6
|
allowedPrefixes?: string[] | null;
|
|
7
7
|
items?: (params: {
|
|
8
8
|
query: string;
|
|
9
|
-
}) =>
|
|
9
|
+
}) => ListItem[];
|
|
10
10
|
command?: (props: any) => void;
|
|
11
11
|
render?: () => any;
|
|
12
12
|
}
|