owl-tiptap 1.1.0 → 1.2.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/package.json +1 -1
- package/es/components/tag-input/GroupExample.d.ts +0 -24
- package/es/components/tag-input/GroupExample.js +0 -54
- package/es/components/tag-input/SuggestionList.d.ts +0 -67
- package/es/components/tag-input/SuggestionList.js +0 -215
- package/es/components/tag-input/TagInput.css +0 -145
- package/es/components/tag-input/TagInput.d.ts +0 -154
- package/es/components/tag-input/TagInput.js +0 -256
- package/es/components/tag-input/TagNode.d.ts +0 -9
- package/es/components/tag-input/TagNode.js +0 -81
- package/es/components/tag-input/suggestion.d.ts +0 -16
- package/es/components/tag-input/suggestion.js +0 -26
- package/es/index.d.ts +0 -2
- package/es/index.js +0 -2
- package/es/utils/classNames.d.ts +0 -6
- package/es/utils/classNames.js +0 -22
package/package.json
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { Component } from '@odoo/owl';
|
|
2
|
-
export interface SuggestionItem {
|
|
3
|
-
value: string;
|
|
4
|
-
label?: string;
|
|
5
|
-
disabled?: boolean;
|
|
6
|
-
[key: string]: any;
|
|
7
|
-
}
|
|
8
|
-
export interface GroupItem {
|
|
9
|
-
group: string;
|
|
10
|
-
items: SuggestionItem[];
|
|
11
|
-
[key: string]: any;
|
|
12
|
-
}
|
|
13
|
-
export type ListItem = SuggestionItem | GroupItem;
|
|
14
|
-
interface SuggestionListProps {
|
|
15
|
-
items: ListItem[];
|
|
16
|
-
onSelect: (event: MouseEvent, item: SuggestionItem) => void;
|
|
17
|
-
slots?: {
|
|
18
|
-
listHeader?: any;
|
|
19
|
-
listItem?: any;
|
|
20
|
-
listGroupHeader?: any;
|
|
21
|
-
listEmpty?: any;
|
|
22
|
-
listFooter?: any;
|
|
23
|
-
};
|
|
24
|
-
className?: string;
|
|
25
|
-
}
|
|
26
|
-
interface SuggestionListState {
|
|
27
|
-
selectedItem: SuggestionItem | null;
|
|
28
|
-
}
|
|
29
|
-
export declare class SuggestionList extends Component<SuggestionListProps> {
|
|
30
|
-
static props: {
|
|
31
|
-
items: {
|
|
32
|
-
type: ArrayConstructor;
|
|
33
|
-
};
|
|
34
|
-
onSelect: {
|
|
35
|
-
type: FunctionConstructor;
|
|
36
|
-
};
|
|
37
|
-
slots: {
|
|
38
|
-
type: ObjectConstructor;
|
|
39
|
-
optional: boolean;
|
|
40
|
-
};
|
|
41
|
-
className: {
|
|
42
|
-
type: StringConstructor;
|
|
43
|
-
optional: boolean;
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* 检查项目是否为分组项
|
|
48
|
-
*/
|
|
49
|
-
private isGroupItem;
|
|
50
|
-
/**
|
|
51
|
-
* 获取处理后的项目列表
|
|
52
|
-
*/
|
|
53
|
-
get processedItems(): ListItem[];
|
|
54
|
-
static template: string;
|
|
55
|
-
state: SuggestionListState;
|
|
56
|
-
listRef: {
|
|
57
|
-
el: HTMLElement;
|
|
58
|
-
};
|
|
59
|
-
onSelect(ev: MouseEvent, item: SuggestionItem): void;
|
|
60
|
-
setup(): void;
|
|
61
|
-
onKeyDown: (event: KeyboardEvent) => void;
|
|
62
|
-
upHandler(): void;
|
|
63
|
-
downHandler(): void;
|
|
64
|
-
enterHandler(event: KeyboardEvent): void;
|
|
65
|
-
scrollIntoView(): void;
|
|
66
|
-
}
|
|
67
|
-
export {};
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { Component, xml, useState, useEffect, useRef } from '@odoo/owl';
|
|
2
|
-
import { classNames } from '../../utils/classNames';
|
|
3
|
-
export class SuggestionList extends Component {
|
|
4
|
-
constructor() {
|
|
5
|
-
super(...arguments);
|
|
6
|
-
this.state = useState({
|
|
7
|
-
selectedItem: null,
|
|
8
|
-
});
|
|
9
|
-
this.listRef = useRef('list');
|
|
10
|
-
this.onKeyDown = (event) => {
|
|
11
|
-
if (event.key === 'ArrowUp') {
|
|
12
|
-
this.upHandler();
|
|
13
|
-
event.preventDefault();
|
|
14
|
-
event.stopImmediatePropagation();
|
|
15
|
-
}
|
|
16
|
-
else if (event.key === 'ArrowDown') {
|
|
17
|
-
this.downHandler();
|
|
18
|
-
event.preventDefault();
|
|
19
|
-
event.stopImmediatePropagation();
|
|
20
|
-
}
|
|
21
|
-
else if (event.key === 'Enter') {
|
|
22
|
-
this.enterHandler(event);
|
|
23
|
-
event.preventDefault();
|
|
24
|
-
event.stopImmediatePropagation();
|
|
25
|
-
}
|
|
26
|
-
};
|
|
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
|
-
}
|
|
47
|
-
onSelect(ev, item) {
|
|
48
|
-
if (!item.disabled) {
|
|
49
|
-
this.props.onSelect?.(ev, item);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
setup() {
|
|
53
|
-
useEffect(() => {
|
|
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
|
-
}
|
|
71
|
-
if (this.state.selectedItem?.value !== selectedItem?.value) {
|
|
72
|
-
this.state.selectedItem = selectedItem;
|
|
73
|
-
}
|
|
74
|
-
}, () => [this.props.items]);
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
window.addEventListener('keydown', this.onKeyDown, true);
|
|
77
|
-
return () => {
|
|
78
|
-
window.removeEventListener('keydown', this.onKeyDown, true);
|
|
79
|
-
};
|
|
80
|
-
}, () => [this.props.items]);
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
this.scrollIntoView();
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
upHandler() {
|
|
86
|
-
const { selectedItem } = this.state;
|
|
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
|
-
}
|
|
99
|
-
if (!selectableItems.length) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const selectedIndex = selectableItems.indexOf(selectedItem);
|
|
103
|
-
const newIndex = (selectedIndex - 1 + selectableItems.length) % selectableItems.length;
|
|
104
|
-
this.state.selectedItem = selectableItems[newIndex];
|
|
105
|
-
}
|
|
106
|
-
downHandler() {
|
|
107
|
-
const { selectedItem } = this.state;
|
|
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
|
-
}
|
|
120
|
-
if (!selectableItems.length) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const selectedIndex = selectableItems.indexOf(selectedItem);
|
|
124
|
-
const newIndex = (selectedIndex + 1) % selectableItems.length;
|
|
125
|
-
this.state.selectedItem = selectableItems[newIndex];
|
|
126
|
-
}
|
|
127
|
-
enterHandler(event) {
|
|
128
|
-
if (this.state.selectedItem) {
|
|
129
|
-
this.props.onSelect(event, this.state.selectedItem);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
scrollIntoView() {
|
|
133
|
-
if (!this.listRef.el || !this.state.selectedItem) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const selectedItemEl = this.listRef.el.querySelector(`[data-value="${this.state.selectedItem.value}"]`);
|
|
137
|
-
if (selectedItemEl) {
|
|
138
|
-
const listRect = this.listRef.el.getBoundingClientRect();
|
|
139
|
-
const itemRect = selectedItemEl.getBoundingClientRect();
|
|
140
|
-
const listStyle = window.getComputedStyle(this.listRef.el);
|
|
141
|
-
const paddingTop = parseFloat(listStyle.paddingTop);
|
|
142
|
-
const paddingBottom = parseFloat(listStyle.paddingBottom);
|
|
143
|
-
if (itemRect.bottom > listRect.bottom - paddingBottom) {
|
|
144
|
-
this.listRef.el.scrollTop += itemRect.bottom - (listRect.bottom - paddingBottom);
|
|
145
|
-
}
|
|
146
|
-
else if (itemRect.top < listRect.top + paddingTop) {
|
|
147
|
-
this.listRef.el.scrollTop -= listRect.top + paddingTop - itemRect.top;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
SuggestionList.props = {
|
|
153
|
-
items: { type: Array },
|
|
154
|
-
onSelect: { type: Function },
|
|
155
|
-
slots: { type: Object, optional: true },
|
|
156
|
-
className: { type: String, optional: true },
|
|
157
|
-
};
|
|
158
|
-
SuggestionList.template = xml `
|
|
159
|
-
<div class="${classNames('&suggestion-list')}" t-att-class="props.className" t-on-mousedown="(event) => event.preventDefault()" t-ref="list">
|
|
160
|
-
<t t-slot="listHeader"/>
|
|
161
|
-
|
|
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>
|
|
185
|
-
</t>
|
|
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>
|
|
202
|
-
</t>
|
|
203
|
-
</t>
|
|
204
|
-
|
|
205
|
-
<t t-else="">
|
|
206
|
-
<div class="is-empty">
|
|
207
|
-
<t t-slot="listEmpty" item="item">
|
|
208
|
-
没有匹配项
|
|
209
|
-
</t>
|
|
210
|
-
</div>
|
|
211
|
-
</t>
|
|
212
|
-
|
|
213
|
-
<t t-slot="listFooter"/>
|
|
214
|
-
</div>
|
|
215
|
-
`;
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
.ott-tag-input {
|
|
2
|
-
--ott-primary-color: rgb(22, 119, 255);
|
|
3
|
-
--ott-border-color: rgb(204, 204, 204);
|
|
4
|
-
--ott-placeholder-color: rgb(173, 181, 189);
|
|
5
|
-
--ott-text-color-secondary: rgb(136, 136, 136);
|
|
6
|
-
--ott-primary-color-08: rgba(22, 119, 255, 0.8);
|
|
7
|
-
--ott-primary-color-02: rgba(22, 119, 255, 0.2);
|
|
8
|
-
--ott-primary-color-04: rgba(22, 119, 255, 0.4);
|
|
9
|
-
--ott-primary-color-01: rgba(22, 119, 255, 0.1);
|
|
10
|
-
position: relative;
|
|
11
|
-
background-color: white;
|
|
12
|
-
}
|
|
13
|
-
.ott-tag-input.disabled {
|
|
14
|
-
cursor: not-allowed;
|
|
15
|
-
background-color: #f5f5f5;
|
|
16
|
-
opacity: 0.7;
|
|
17
|
-
}
|
|
18
|
-
.ott-tag-input.disabled .ProseMirror {
|
|
19
|
-
background-color: #f5f5f5;
|
|
20
|
-
border-color: #e0e0e0;
|
|
21
|
-
color: #999;
|
|
22
|
-
pointer-events: none;
|
|
23
|
-
}
|
|
24
|
-
.ott-tag-input.disabled .tag-wrapper .tag {
|
|
25
|
-
pointer-events: none;
|
|
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
|
-
}
|
|
49
|
-
.ott-tag-input .ProseMirror {
|
|
50
|
-
padding: 8px;
|
|
51
|
-
border: 1px solid var(--ott-border-color);
|
|
52
|
-
border-radius: 4px;
|
|
53
|
-
}
|
|
54
|
-
.ott-tag-input .ProseMirror:focus {
|
|
55
|
-
outline: none;
|
|
56
|
-
border-color: var(--ott-primary-color-08);
|
|
57
|
-
box-shadow: 0 0 0 2px var(--ott-primary-color-02);
|
|
58
|
-
}
|
|
59
|
-
.ott-tag-input .ProseMirror p {
|
|
60
|
-
margin: 0;
|
|
61
|
-
}
|
|
62
|
-
.ott-tag-input .ProseMirror p.is-editor-empty:first-child::before {
|
|
63
|
-
content: attr(data-placeholder);
|
|
64
|
-
float: left;
|
|
65
|
-
color: var(--ott-placeholder-color);
|
|
66
|
-
pointer-events: none;
|
|
67
|
-
height: 0;
|
|
68
|
-
}
|
|
69
|
-
.ott-tag-input .tag-wrapper {
|
|
70
|
-
display: inline-flex;
|
|
71
|
-
padding: 0 2px;
|
|
72
|
-
align-items: center;
|
|
73
|
-
}
|
|
74
|
-
.ott-tag-input .tag-wrapper .tag {
|
|
75
|
-
min-height: 20px;
|
|
76
|
-
display: inline-flex;
|
|
77
|
-
align-items: center;
|
|
78
|
-
background-color: var(--ott-primary-color);
|
|
79
|
-
border: none;
|
|
80
|
-
border-radius: 3px;
|
|
81
|
-
padding: 2px 8px;
|
|
82
|
-
color: white;
|
|
83
|
-
white-space: nowrap;
|
|
84
|
-
transition: all 0.2s;
|
|
85
|
-
font-size: 12px;
|
|
86
|
-
box-sizing: border-box;
|
|
87
|
-
line-height: 1;
|
|
88
|
-
}
|
|
89
|
-
.ott-tag-input .tag-wrapper .tag:hover {
|
|
90
|
-
cursor: pointer;
|
|
91
|
-
}
|
|
92
|
-
.ott-tag-input .tag-wrapper .tag .close-button {
|
|
93
|
-
margin-left: 2px;
|
|
94
|
-
border: none;
|
|
95
|
-
background: none;
|
|
96
|
-
cursor: pointer;
|
|
97
|
-
padding: 0;
|
|
98
|
-
display: inline-flex;
|
|
99
|
-
align-items: center;
|
|
100
|
-
justify-content: center;
|
|
101
|
-
color: white;
|
|
102
|
-
border-radius: 50%;
|
|
103
|
-
width: 14px;
|
|
104
|
-
height: 14px;
|
|
105
|
-
transition: all 0.2s;
|
|
106
|
-
}
|
|
107
|
-
.ott-tag-input .tag-wrapper .tag .close-button:hover {
|
|
108
|
-
background-color: rgba(0, 0, 0, 0.2);
|
|
109
|
-
color: white;
|
|
110
|
-
}
|
|
111
|
-
.ott-tag-input .ott-suggestion-list-wrapper {
|
|
112
|
-
position: absolute;
|
|
113
|
-
z-index: 10;
|
|
114
|
-
left: 0;
|
|
115
|
-
right: 0;
|
|
116
|
-
}
|
|
117
|
-
.ott-tag-input .ott-suggestion-list {
|
|
118
|
-
background: white;
|
|
119
|
-
max-height: 200px;
|
|
120
|
-
border: 1px solid var(--ott-primary-color-04);
|
|
121
|
-
border-radius: 4px;
|
|
122
|
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
123
|
-
overflow: auto;
|
|
124
|
-
padding: 3px;
|
|
125
|
-
}
|
|
126
|
-
.ott-tag-input .ott-suggestion-list div {
|
|
127
|
-
padding: 3px 8px;
|
|
128
|
-
cursor: pointer;
|
|
129
|
-
}
|
|
130
|
-
.ott-tag-input .ott-suggestion-list div.is-selected, .ott-tag-input .ott-suggestion-list div:hover {
|
|
131
|
-
background-color: var(--ott-primary-color-01);
|
|
132
|
-
color: var(--ott-primary-color);
|
|
133
|
-
}
|
|
134
|
-
.ott-tag-input .ott-suggestion-list div.is-empty {
|
|
135
|
-
color: var(--ott-text-color-secondary);
|
|
136
|
-
cursor: default;
|
|
137
|
-
}
|
|
138
|
-
.ott-tag-input .ott-suggestion-list div.is-disabled {
|
|
139
|
-
color: var(--ott-text-color-secondary);
|
|
140
|
-
cursor: not-allowed;
|
|
141
|
-
}
|
|
142
|
-
.ott-tag-input .ott-suggestion-list div.is-disabled.is-selected, .ott-tag-input .ott-suggestion-list div.is-disabled:hover {
|
|
143
|
-
background-color: transparent;
|
|
144
|
-
color: var(--ott-text-color-secondary);
|
|
145
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { Component } from '@odoo/owl';
|
|
2
|
-
import { Editor } from '@tiptap/core';
|
|
3
|
-
import { SuggestionList } from './SuggestionList';
|
|
4
|
-
import type { SuggestionItem, ListItem } from './SuggestionList';
|
|
5
|
-
export type { SuggestionItem, ListItem, GroupItem } from './SuggestionList';
|
|
6
|
-
import './TagInput.css';
|
|
7
|
-
export interface TagAttributes {
|
|
8
|
-
value: string;
|
|
9
|
-
label?: string;
|
|
10
|
-
group?: string;
|
|
11
|
-
[key: string]: any;
|
|
12
|
-
}
|
|
13
|
-
export interface TagNode {
|
|
14
|
-
node: any;
|
|
15
|
-
pos: number;
|
|
16
|
-
}
|
|
17
|
-
interface SuggestionState {
|
|
18
|
-
visible: boolean;
|
|
19
|
-
range: {
|
|
20
|
-
from: number;
|
|
21
|
-
to: number;
|
|
22
|
-
};
|
|
23
|
-
items: SuggestionItem[];
|
|
24
|
-
style: string;
|
|
25
|
-
}
|
|
26
|
-
interface TagInputState {
|
|
27
|
-
editor: Editor | null;
|
|
28
|
-
suggestion: SuggestionState;
|
|
29
|
-
}
|
|
30
|
-
interface TagInputProps {
|
|
31
|
-
className?: string;
|
|
32
|
-
ref?: (component: TagInput) => void;
|
|
33
|
-
placeholder?: string;
|
|
34
|
-
slots?: {
|
|
35
|
-
listHeader?: any;
|
|
36
|
-
listItem?: any;
|
|
37
|
-
listGroupHeader?: any;
|
|
38
|
-
listEmpty?: any;
|
|
39
|
-
listFooter?: any;
|
|
40
|
-
};
|
|
41
|
-
suggestionListClassName?: string;
|
|
42
|
-
onSuggestionSelect?: (event: MouseEvent, item: SuggestionItem) => void;
|
|
43
|
-
items?: (params: {
|
|
44
|
-
query: string;
|
|
45
|
-
}) => ListItem[];
|
|
46
|
-
onChange?: (content: any) => void;
|
|
47
|
-
readonly?: boolean;
|
|
48
|
-
onTagClick?: (index: number, attrs: TagAttributes) => void;
|
|
49
|
-
char?: string;
|
|
50
|
-
}
|
|
51
|
-
export declare class TagInput extends Component<TagInputProps> {
|
|
52
|
-
static components: {
|
|
53
|
-
SuggestionList: typeof SuggestionList;
|
|
54
|
-
};
|
|
55
|
-
static props: {
|
|
56
|
-
className: {
|
|
57
|
-
type: StringConstructor;
|
|
58
|
-
optional: boolean;
|
|
59
|
-
};
|
|
60
|
-
ref: {
|
|
61
|
-
type: FunctionConstructor;
|
|
62
|
-
optional: boolean;
|
|
63
|
-
};
|
|
64
|
-
placeholder: {
|
|
65
|
-
type: StringConstructor;
|
|
66
|
-
optional: boolean;
|
|
67
|
-
};
|
|
68
|
-
slots: {
|
|
69
|
-
type: ObjectConstructor;
|
|
70
|
-
optional: boolean;
|
|
71
|
-
};
|
|
72
|
-
suggestionListClassName: {
|
|
73
|
-
type: StringConstructor;
|
|
74
|
-
optional: boolean;
|
|
75
|
-
};
|
|
76
|
-
onSuggestionSelect: {
|
|
77
|
-
type: FunctionConstructor;
|
|
78
|
-
optional: boolean;
|
|
79
|
-
};
|
|
80
|
-
items: {
|
|
81
|
-
type: FunctionConstructor;
|
|
82
|
-
optional: boolean;
|
|
83
|
-
};
|
|
84
|
-
onChange: {
|
|
85
|
-
type: FunctionConstructor;
|
|
86
|
-
optional: boolean;
|
|
87
|
-
};
|
|
88
|
-
readonly: {
|
|
89
|
-
type: BooleanConstructor;
|
|
90
|
-
optional: boolean;
|
|
91
|
-
};
|
|
92
|
-
onTagClick: {
|
|
93
|
-
type: FunctionConstructor;
|
|
94
|
-
optional: boolean;
|
|
95
|
-
};
|
|
96
|
-
char: {
|
|
97
|
-
type: StringConstructor;
|
|
98
|
-
optional: boolean;
|
|
99
|
-
};
|
|
100
|
-
};
|
|
101
|
-
static template: string;
|
|
102
|
-
static defaultProps: {
|
|
103
|
-
items: () => any[];
|
|
104
|
-
readonly: boolean;
|
|
105
|
-
char: string;
|
|
106
|
-
};
|
|
107
|
-
editorRef: {
|
|
108
|
-
el: HTMLElement;
|
|
109
|
-
};
|
|
110
|
-
state: TagInputState;
|
|
111
|
-
get className(): string;
|
|
112
|
-
onSuggestionSelect(event: MouseEvent, item: SuggestionItem): void;
|
|
113
|
-
updateSuggestion(props: any, options?: Partial<SuggestionState>): void;
|
|
114
|
-
/**
|
|
115
|
-
* Serializes the current content of the editor.
|
|
116
|
-
* @returns {object} The content as a JSON object.
|
|
117
|
-
*/
|
|
118
|
-
getContent(): any;
|
|
119
|
-
/**
|
|
120
|
-
* Deserializes content into the editor.
|
|
121
|
-
* @param {object} content - The content to set, as a JSON object.
|
|
122
|
-
*/
|
|
123
|
-
setContent(content: any): void;
|
|
124
|
-
/**
|
|
125
|
-
* Adds a tag to the editor.
|
|
126
|
-
* @param {object} range - The range of the tag
|
|
127
|
-
* @param {number} range.from - The start position of the tag.
|
|
128
|
-
* @param {number} range.to - The end position of the tag.
|
|
129
|
-
* @param {object} props - The properties of the tag.
|
|
130
|
-
*/
|
|
131
|
-
addTag(range: {
|
|
132
|
-
from: number;
|
|
133
|
-
to: number;
|
|
134
|
-
}, props: TagAttributes): void;
|
|
135
|
-
/**
|
|
136
|
-
* Removes a tag from the editor by its index.
|
|
137
|
-
* @param {number} index - The index of the tag to remove.
|
|
138
|
-
*/
|
|
139
|
-
removeTag(index: number): void;
|
|
140
|
-
/**
|
|
141
|
-
* Replaces a tag at a specific index with a new tag.
|
|
142
|
-
* @param {number} index - The index of the tag to replace.
|
|
143
|
-
* @param {object} props - The properties of the new tag.
|
|
144
|
-
*/
|
|
145
|
-
replaceTag(index: number, props: TagAttributes): void;
|
|
146
|
-
/**
|
|
147
|
-
* Gets all tag nodes from the editor.
|
|
148
|
-
* @returns {Array} An array of tag nodes with their positions.
|
|
149
|
-
*/
|
|
150
|
-
getTags(): TagNode[];
|
|
151
|
-
renderEditor(): void;
|
|
152
|
-
destroyEditor(): void;
|
|
153
|
-
setup(): void;
|
|
154
|
-
}
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { Component, xml, useRef, onMounted, useEffect, useState, markRaw } from '@odoo/owl';
|
|
2
|
-
import { classNames } from '../../utils/classNames';
|
|
3
|
-
import { Editor } from '@tiptap/core';
|
|
4
|
-
import Document from '@tiptap/extension-document';
|
|
5
|
-
import Text from '@tiptap/extension-text';
|
|
6
|
-
import Paragraph from '@tiptap/extension-paragraph';
|
|
7
|
-
import History from '@tiptap/extension-history';
|
|
8
|
-
import Placeholder from '@tiptap/extension-placeholder';
|
|
9
|
-
import { SuggestionList } from './SuggestionList';
|
|
10
|
-
import { TagNode } from './TagNode';
|
|
11
|
-
import { SuggestionPlugin } from './suggestion';
|
|
12
|
-
import './TagInput.css';
|
|
13
|
-
export class TagInput extends Component {
|
|
14
|
-
constructor() {
|
|
15
|
-
super(...arguments);
|
|
16
|
-
this.editorRef = useRef('editor');
|
|
17
|
-
this.state = useState({
|
|
18
|
-
editor: null,
|
|
19
|
-
suggestion: {
|
|
20
|
-
visible: false,
|
|
21
|
-
range: {
|
|
22
|
-
from: 0,
|
|
23
|
-
to: 0,
|
|
24
|
-
},
|
|
25
|
-
items: [],
|
|
26
|
-
style: '',
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
get className() {
|
|
31
|
-
const disabledClass = this.props.readonly ? 'disabled' : '';
|
|
32
|
-
return classNames('&tag-input', this.props.className, disabledClass);
|
|
33
|
-
}
|
|
34
|
-
onSuggestionSelect(event, item) {
|
|
35
|
-
this.props.onSuggestionSelect?.(event, item);
|
|
36
|
-
if (!event.cancelBubble) {
|
|
37
|
-
this.addTag(this.state.suggestion.range, item);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
updateSuggestion(props, options = {}) {
|
|
41
|
-
const clientRect = props.clientRect();
|
|
42
|
-
const editorRect = this.editorRef.el.getBoundingClientRect();
|
|
43
|
-
const { innerHeight } = window;
|
|
44
|
-
let style = '';
|
|
45
|
-
if (clientRect.bottom + 200 > innerHeight) {
|
|
46
|
-
style = `bottom: ${editorRect.height}px`;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
style = `top: ${editorRect.height}`;
|
|
50
|
-
}
|
|
51
|
-
this.state.suggestion = {
|
|
52
|
-
...this.state.suggestion,
|
|
53
|
-
range: props.range,
|
|
54
|
-
items: props.items,
|
|
55
|
-
style,
|
|
56
|
-
...options,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Serializes the current content of the editor.
|
|
61
|
-
* @returns {object} The content as a JSON object.
|
|
62
|
-
*/
|
|
63
|
-
getContent() {
|
|
64
|
-
const json = this.state.editor.getJSON();
|
|
65
|
-
const { content } = json;
|
|
66
|
-
if (content.length === 1 && content[0].type === 'paragraph' && !content[0].content) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
return json;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Deserializes content into the editor.
|
|
73
|
-
* @param {object} content - The content to set, as a JSON object.
|
|
74
|
-
*/
|
|
75
|
-
setContent(content) {
|
|
76
|
-
this.state.editor.commands.setContent(content);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Adds a tag to the editor.
|
|
80
|
-
* @param {object} range - The range of the tag
|
|
81
|
-
* @param {number} range.from - The start position of the tag.
|
|
82
|
-
* @param {number} range.to - The end position of the tag.
|
|
83
|
-
* @param {object} props - The properties of the tag.
|
|
84
|
-
*/
|
|
85
|
-
addTag(range, props) {
|
|
86
|
-
this.state
|
|
87
|
-
.editor.chain()
|
|
88
|
-
.focus()
|
|
89
|
-
.insertContentAt(range, {
|
|
90
|
-
type: 'tag',
|
|
91
|
-
attrs: props,
|
|
92
|
-
})
|
|
93
|
-
.run();
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Removes a tag from the editor by its index.
|
|
97
|
-
* @param {number} index - The index of the tag to remove.
|
|
98
|
-
*/
|
|
99
|
-
removeTag(index) {
|
|
100
|
-
const tags = this.getTags();
|
|
101
|
-
const tagToRemove = tags[index];
|
|
102
|
-
if (tagToRemove) {
|
|
103
|
-
const { node, pos } = tagToRemove;
|
|
104
|
-
const from = pos;
|
|
105
|
-
const to = pos + node.nodeSize;
|
|
106
|
-
this.state.editor.chain().focus().deleteRange({ from, to }).run();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Replaces a tag at a specific index with a new tag.
|
|
111
|
-
* @param {number} index - The index of the tag to replace.
|
|
112
|
-
* @param {object} props - The properties of the new tag.
|
|
113
|
-
*/
|
|
114
|
-
replaceTag(index, props) {
|
|
115
|
-
const tags = this.getTags();
|
|
116
|
-
const tagToReplace = tags[index];
|
|
117
|
-
if (tagToReplace) {
|
|
118
|
-
const { node, pos } = tagToReplace;
|
|
119
|
-
const from = pos;
|
|
120
|
-
const to = pos + node.nodeSize;
|
|
121
|
-
this.state
|
|
122
|
-
.editor.chain()
|
|
123
|
-
.focus()
|
|
124
|
-
.insertContentAt({ from, to }, {
|
|
125
|
-
type: 'tag',
|
|
126
|
-
attrs: props,
|
|
127
|
-
})
|
|
128
|
-
.run();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Gets all tag nodes from the editor.
|
|
133
|
-
* @returns {Array} An array of tag nodes with their positions.
|
|
134
|
-
*/
|
|
135
|
-
getTags() {
|
|
136
|
-
const tags = [];
|
|
137
|
-
this.state.editor.state.doc.descendants((node, pos) => {
|
|
138
|
-
if (node.type.name === 'tag') {
|
|
139
|
-
tags.push({ node, pos });
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
return tags;
|
|
143
|
-
}
|
|
144
|
-
renderEditor() {
|
|
145
|
-
this.destroyEditor();
|
|
146
|
-
this.state.editor = markRaw(new Editor({
|
|
147
|
-
element: this.editorRef.el,
|
|
148
|
-
editable: !this.props.readonly,
|
|
149
|
-
extensions: [
|
|
150
|
-
Document,
|
|
151
|
-
Text,
|
|
152
|
-
Paragraph,
|
|
153
|
-
History,
|
|
154
|
-
Placeholder.configure({
|
|
155
|
-
placeholder: this.props.placeholder || `请输入文本,按${this.props.char}选择标签...`,
|
|
156
|
-
}),
|
|
157
|
-
TagNode.configure({
|
|
158
|
-
readonly: this.props.readonly,
|
|
159
|
-
onTagClick: (attrs, pos) => {
|
|
160
|
-
if (this.props.onTagClick) {
|
|
161
|
-
const tags = this.getTags();
|
|
162
|
-
const index = tags.findIndex((tag) => tag.pos === pos);
|
|
163
|
-
if (index !== -1) {
|
|
164
|
-
this.props.onTagClick(index, attrs);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
}),
|
|
169
|
-
SuggestionPlugin.configure({
|
|
170
|
-
char: this.props.char,
|
|
171
|
-
items: this.props.items,
|
|
172
|
-
render: () => {
|
|
173
|
-
return {
|
|
174
|
-
onStart: (props) => {
|
|
175
|
-
if (props.text !== this.props.char) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
this.updateSuggestion(props, { visible: true });
|
|
179
|
-
},
|
|
180
|
-
onUpdate: (props) => {
|
|
181
|
-
this.updateSuggestion(props);
|
|
182
|
-
},
|
|
183
|
-
onExit: (props) => {
|
|
184
|
-
this.state.suggestion.visible = false;
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
},
|
|
188
|
-
}),
|
|
189
|
-
],
|
|
190
|
-
content: null,
|
|
191
|
-
onBlur: ({ editor }) => {
|
|
192
|
-
// this.state.suggestion.visible = false;
|
|
193
|
-
// exitSuggestionPlugin(editor.view);
|
|
194
|
-
},
|
|
195
|
-
onUpdate: ({ editor, transaction }) => {
|
|
196
|
-
if (transaction.docChanged) {
|
|
197
|
-
this.props.onChange?.(this.getContent());
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
}));
|
|
201
|
-
}
|
|
202
|
-
destroyEditor() {
|
|
203
|
-
this.state.editor?.destroy();
|
|
204
|
-
}
|
|
205
|
-
setup() {
|
|
206
|
-
onMounted(() => {
|
|
207
|
-
this.props.ref?.(this);
|
|
208
|
-
});
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
if (this.state.editor) {
|
|
211
|
-
const content = this.getContent();
|
|
212
|
-
this.destroyEditor();
|
|
213
|
-
this.renderEditor();
|
|
214
|
-
this.setContent(content);
|
|
215
|
-
}
|
|
216
|
-
}, () => [this.props.readonly]);
|
|
217
|
-
useEffect(() => {
|
|
218
|
-
this.renderEditor();
|
|
219
|
-
return () => {
|
|
220
|
-
this.destroyEditor();
|
|
221
|
-
};
|
|
222
|
-
}, () => []);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
TagInput.components = { SuggestionList };
|
|
226
|
-
TagInput.props = {
|
|
227
|
-
className: { type: String, optional: true },
|
|
228
|
-
ref: { type: Function, optional: true },
|
|
229
|
-
placeholder: { type: String, optional: true },
|
|
230
|
-
slots: { type: Object, optional: true },
|
|
231
|
-
suggestionListClassName: { type: String, optional: true },
|
|
232
|
-
onSuggestionSelect: { type: Function, optional: true },
|
|
233
|
-
items: { type: Function, optional: true },
|
|
234
|
-
onChange: { type: Function, optional: true },
|
|
235
|
-
readonly: { type: Boolean, optional: true },
|
|
236
|
-
onTagClick: { type: Function, optional: true },
|
|
237
|
-
char: { type: String, optional: true },
|
|
238
|
-
};
|
|
239
|
-
TagInput.template = xml `
|
|
240
|
-
<div t-att-class="className">
|
|
241
|
-
<div t-ref="editor"/>
|
|
242
|
-
<div t-if="state.suggestion.visible" t-att-style="state.suggestion.style" class="ott-suggestion-list-wrapper">
|
|
243
|
-
<SuggestionList
|
|
244
|
-
className="props.suggestionListClassName"
|
|
245
|
-
items="state.suggestion.items"
|
|
246
|
-
onSelect.bind="onSuggestionSelect"
|
|
247
|
-
slots="props.slots"
|
|
248
|
-
/>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
`;
|
|
252
|
-
TagInput.defaultProps = {
|
|
253
|
-
items: () => [],
|
|
254
|
-
readonly: false,
|
|
255
|
-
char: '@',
|
|
256
|
-
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { Node } from '@tiptap/core';
|
|
2
|
-
import { TagAttributes } from './TagInput';
|
|
3
|
-
interface TagNodeOptions {
|
|
4
|
-
onTagClick: (attrs: TagAttributes, pos: number) => void;
|
|
5
|
-
renderLabel?: (attrs: TagAttributes) => string;
|
|
6
|
-
readonly?: boolean;
|
|
7
|
-
}
|
|
8
|
-
export declare const TagNode: Node<TagNodeOptions, any>;
|
|
9
|
-
export {};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { Node } from '@tiptap/core';
|
|
2
|
-
const closeSVG = `
|
|
3
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="14" height="14">
|
|
4
|
-
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
-
</svg>
|
|
6
|
-
`;
|
|
7
|
-
export const TagNode = Node.create({
|
|
8
|
-
name: 'tag',
|
|
9
|
-
group: 'inline',
|
|
10
|
-
inline: true,
|
|
11
|
-
selectable: false,
|
|
12
|
-
atom: true,
|
|
13
|
-
addOptions() {
|
|
14
|
-
return {
|
|
15
|
-
onTagClick: () => { },
|
|
16
|
-
readonly: false, // 默认不只读
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
addAttributes() {
|
|
20
|
-
return {
|
|
21
|
-
value: {
|
|
22
|
-
default: null,
|
|
23
|
-
},
|
|
24
|
-
label: {
|
|
25
|
-
default: null,
|
|
26
|
-
},
|
|
27
|
-
group: {
|
|
28
|
-
default: null,
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
addNodeView() {
|
|
33
|
-
// We can implement an Owl NodeView here later if complex rendering is needed.
|
|
34
|
-
// For now, renderHTML is sufficient.
|
|
35
|
-
return ({ node, getPos, editor }) => {
|
|
36
|
-
const { readonly } = this.options; // 从 options 中获取当前状态
|
|
37
|
-
console.log(readonly);
|
|
38
|
-
const wrapper = document.createElement('span');
|
|
39
|
-
wrapper.classList.add('tag-wrapper');
|
|
40
|
-
const dom = document.createElement('span');
|
|
41
|
-
// Get attributes directly from the node being rendered
|
|
42
|
-
const { label, ...attrs } = node.attrs;
|
|
43
|
-
// Set data attributes
|
|
44
|
-
Object.entries(attrs).forEach(([key, value]) => {
|
|
45
|
-
if (value !== null) {
|
|
46
|
-
dom.setAttribute(`data-${key}`, value);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
dom.setAttribute('data-type', 'tag');
|
|
50
|
-
dom.classList.add('tag'); // Add a class for styling
|
|
51
|
-
if (!readonly) {
|
|
52
|
-
dom.addEventListener('click', () => {
|
|
53
|
-
this.options.onTagClick(node.attrs, getPos());
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
const labelSpan = document.createElement('span');
|
|
57
|
-
labelSpan.textContent = label; // Set the visible text
|
|
58
|
-
dom.appendChild(labelSpan);
|
|
59
|
-
if (!readonly) {
|
|
60
|
-
const closeButton = document.createElement('button');
|
|
61
|
-
closeButton.classList.add('close-button');
|
|
62
|
-
closeButton.innerHTML = closeSVG;
|
|
63
|
-
closeButton.addEventListener('click', (e) => {
|
|
64
|
-
e.stopPropagation();
|
|
65
|
-
const pos = getPos();
|
|
66
|
-
editor.view.dispatch(editor.view.state.tr.delete(pos, pos + node.nodeSize));
|
|
67
|
-
editor.view.focus();
|
|
68
|
-
});
|
|
69
|
-
dom.appendChild(closeButton);
|
|
70
|
-
}
|
|
71
|
-
wrapper.appendChild(dom);
|
|
72
|
-
return {
|
|
73
|
-
dom: wrapper,
|
|
74
|
-
update: (updatedNode) => {
|
|
75
|
-
debugger;
|
|
76
|
-
return true;
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
},
|
|
81
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
import { ListItem } from './SuggestionList';
|
|
3
|
-
import { PluginKey } from 'prosemirror-state';
|
|
4
|
-
interface SuggestionPluginOptions {
|
|
5
|
-
char?: string;
|
|
6
|
-
allowedPrefixes?: string[] | null;
|
|
7
|
-
items?: (params: {
|
|
8
|
-
query: string;
|
|
9
|
-
}) => ListItem[];
|
|
10
|
-
command?: (props: any) => void;
|
|
11
|
-
render?: () => any;
|
|
12
|
-
}
|
|
13
|
-
export declare const MySuggestionPluginKey: PluginKey<any>;
|
|
14
|
-
export declare const exitSuggestionPlugin: (view: any) => void;
|
|
15
|
-
export declare const SuggestionPlugin: Extension<SuggestionPluginOptions, any>;
|
|
16
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
import { Suggestion } from '@tiptap/suggestion';
|
|
3
|
-
import { PluginKey } from 'prosemirror-state'; // optional, if you need to create a custom key
|
|
4
|
-
import { exitSuggestion } from '@tiptap/suggestion';
|
|
5
|
-
export const MySuggestionPluginKey = new PluginKey('my-suggestions'); // or use the default 'suggestion'
|
|
6
|
-
export const exitSuggestionPlugin = (view) => {
|
|
7
|
-
exitSuggestion(view, MySuggestionPluginKey);
|
|
8
|
-
};
|
|
9
|
-
export const SuggestionPlugin = Extension.create({
|
|
10
|
-
name: 'suggestionPlugin',
|
|
11
|
-
addOptions() {
|
|
12
|
-
return {
|
|
13
|
-
allowedPrefixes: null, // Default Tiptap behavior
|
|
14
|
-
command: () => { },
|
|
15
|
-
};
|
|
16
|
-
},
|
|
17
|
-
addProseMirrorPlugins() {
|
|
18
|
-
return [
|
|
19
|
-
Suggestion({
|
|
20
|
-
pluginKey: MySuggestionPluginKey,
|
|
21
|
-
editor: this.editor,
|
|
22
|
-
...this.options,
|
|
23
|
-
}),
|
|
24
|
-
];
|
|
25
|
-
},
|
|
26
|
-
});
|
package/es/index.d.ts
DELETED
package/es/index.js
DELETED
package/es/utils/classNames.d.ts
DELETED
package/es/utils/classNames.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import _classNames from 'classnames';
|
|
2
|
-
const PREFIX = 'ott';
|
|
3
|
-
/**
|
|
4
|
-
* 自定义className合并函数,用于给带&的className添加前缀
|
|
5
|
-
* @param names - 需要合并的className字符串
|
|
6
|
-
* @returns 合并后的className字符串
|
|
7
|
-
*/
|
|
8
|
-
export const classNames = (...names) => {
|
|
9
|
-
const result = _classNames(...names);
|
|
10
|
-
if (result === '') {
|
|
11
|
-
return '';
|
|
12
|
-
}
|
|
13
|
-
return result
|
|
14
|
-
.split(' ')
|
|
15
|
-
.map((name) => {
|
|
16
|
-
if (name.startsWith('&')) {
|
|
17
|
-
return `${PREFIX}-${name.slice(1)}`;
|
|
18
|
-
}
|
|
19
|
-
return name;
|
|
20
|
-
})
|
|
21
|
-
.join(' ');
|
|
22
|
-
};
|