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.
@@ -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 SuggestionListProps {
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 && this.props.onSelect?.(ev, item);
48
+ if (!item.disabled) {
49
+ this.props.onSelect?.(ev, item);
50
+ }
30
51
  }
31
52
  setup() {
32
53
  useEffect(() => {
33
- const selectedItem = this.props.items.find((item) => !item.disabled) ?? null;
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
- const selectableItems = items.filter((item) => !item.disabled);
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
- const selectableItems = items.filter((item) => !item.disabled);
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="props.items.length">
106
- <t t-foreach="props.items" t-as="item" t-key="item.value">
107
- <div
108
- t-att-class="{
109
- 'is-selected': item.value === state.selectedItem?.value,
110
- 'is-disabled': item.disabled,
111
- }"
112
- t-on-click="(ev) => this.onSelect(ev, item)"
113
- t-att-data-value="item.value"
114
- >
115
- <t t-slot="listItem" item="item">
116
- <t t-esc="item.label" />
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
- </div>
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, SuggestionItem } from './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
- }) => SuggestionItem[];
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, exitSuggestionPlugin } from './suggestion';
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 { SuggestionItem } from './SuggestionList';
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
- }) => SuggestionItem[];
9
+ }) => ListItem[];
10
10
  command?: (props: any) => void;
11
11
  render?: () => any;
12
12
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "owl-tiptap",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "tiptap used in odoo owl2",
5
- "main": "index.js",
5
+ "main": "es/index.js",
6
6
  "scripts": {
7
7
  "start": "vite",
8
8
  "clean": "gulp clean",