@neatui/vue 5.0.0 → 5.1.1

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.
@@ -1,6 +1,6 @@
1
1
  import FormLoader from './FormLoader/FormLoader.vue';
2
2
  export { registerField } from './FormLoader/FormRender.vue';
3
- export type { FormValue, FormItem, FormArray, FormRule, FormContext, FormArrayContext, FormLinkage, FormError, FormValidateResult, FormLoaderExpose, FieldEntry } from './FormLoader/FormRender.vue';
3
+ export type { FormValue, FormItem, FormArray, FormRule, FormContext, FormArrayContext, FormLinkage, FormError, FormValidateResult, FormLoaderExpose, FormSlotScope, FieldEntry } from './FormLoader/FormRender.vue';
4
4
  import LimitLoader from './LimitLoader';
5
5
  export * from './TableLoader/index';
6
6
  import TableLoader from './TableLoader/TableLoader.vue';
@@ -1,99 +0,0 @@
1
- <template>
2
- <div class="neat-rich-text b-solid b-xs b-line r-sm" :aria-busy="!editor">
3
- <template v-if="editor">
4
- <div v-if="!readonly" role="toolbar" aria-label="文本格式" class="n-sm" ui-flex="row lm">
5
- <button v-for="tool in tools" :key="tool.label" type="button" ui-btn="@a s" :disabled="disabled"
6
- :aria-pressed="tool.active ? editor.isActive(tool.active) : undefined" @mousedown.prevent @click="tool.run()">{{ tool.label }}</button>
7
- <button type="button" ui-btn="@a s" :disabled="disabled" @mousedown.prevent @click="openLink">链接</button>
8
- <button type="button" ui-btn="@a s" :disabled="disabled || !editor.can().undo()" @mousedown.prevent @click="editor.chain().focus().undo().run()">撤销</button>
9
- <button type="button" ui-btn="@a s" :disabled="disabled || !editor.can().redo()" @mousedown.prevent @click="editor.chain().focus().redo().run()">重做</button>
10
- </div>
11
- <div v-if="linkOpen && !disabled && !readonly" class="n-sm" ui-flex="row lm">
12
- <input v-model="link" aria-label="链接地址" placeholder="https://" @keydown.enter.prevent="applyLink" />
13
- <button type="button" @click="applyLink">应用链接</button>
14
- <button type="button" @click="linkOpen = false">取消</button>
15
- <span v-if="linkError" role="alert">请输入 http、https 或 mailto 地址</span>
16
- </div>
17
- <EditorContent :editor="editor" />
18
- </template>
19
- </div>
20
- </template>
21
- <script setup lang="ts">
22
- import { onMounted, onBeforeUnmount, shallowRef, watch, ref } from 'vue';
23
- import { Editor, EditorContent } from '@tiptap/vue-3';
24
- import StarterKit from '@tiptap/starter-kit';
25
- const props = withDefaults(defineProps<{
26
- modelValue?: string; disabled?: boolean; readonly?: boolean; label?: string;
27
- }>(), { modelValue: '', disabled: false, readonly: false, label: '富文本内容' });
28
- const emit = defineEmits(['update:modelValue', 'change', 'focus', 'blur']);
29
- const editor = shallowRef<Editor>();
30
- const linkOpen = ref(false);
31
- const link = ref('');
32
- const linkError = ref(false);
33
- let linkSelection = { from: 1, to: 1 };
34
- onMounted(() => {
35
- editor.value = new Editor({
36
- extensions: [StarterKit.configure({ link: { openOnClick: false } })],
37
- content: props.modelValue,
38
- editable: !props.disabled && !props.readonly,
39
- editorProps: { attributes: { role: 'textbox', 'aria-multiline': 'true', 'aria-label': props.label } },
40
- onUpdate: ({ editor }) => {
41
- const value = editor.isEmpty ? '' : editor.getHTML();
42
- emit('update:modelValue', value);
43
- emit('change', value);
44
- },
45
- onFocus: ({ event }) => emit('focus', event),
46
- onBlur: ({ event }) => emit('blur', event),
47
- });
48
- });
49
- onBeforeUnmount(() => editor.value?.destroy());
50
- watch(() => props.modelValue, value => {
51
- const instance = editor.value;
52
- if (instance && value !== (instance.isEmpty ? '' : instance.getHTML())) instance.commands.setContent(value || '', { emitUpdate: false });
53
- });
54
- watch(() => [props.disabled, props.readonly], () => {
55
- editor.value?.setEditable(!props.disabled && !props.readonly, false);
56
- linkOpen.value = false;
57
- });
58
- const tools = [
59
- { label: '加粗', active: 'bold', run: () => editor.value?.chain().focus().toggleBold().run() },
60
- { label: '斜体', active: 'italic', run: () => editor.value?.chain().focus().toggleItalic().run() },
61
- { label: '下划线', active: 'underline', run: () => editor.value?.chain().focus().toggleUnderline().run() },
62
- { label: '标题', active: 'heading', run: () => editor.value?.chain().focus().toggleHeading({ level: 2 }).run() },
63
- { label: '无序列表', active: 'bulletList', run: () => editor.value?.chain().focus().toggleBulletList().run() },
64
- { label: '有序列表', active: 'orderedList', run: () => editor.value?.chain().focus().toggleOrderedList().run() },
65
- { label: '引用', active: 'blockquote', run: () => editor.value?.chain().focus().toggleBlockquote().run() },
66
- { label: '清除格式', run: () => editor.value?.chain().focus().unsetAllMarks().clearNodes().run() },
67
- ];
68
- const openLink = () => {
69
- if (!editor.value) return;
70
- const instance = editor.value;
71
- const selection = instance.view.dom.ownerDocument.getSelection();
72
- const { from, to } = instance.state.selection;
73
- linkSelection = { from, to };
74
- // DOM 选区更新可能先于编辑器的 selectionchange 事务。
75
- if (selection?.anchorNode && selection.focusNode && instance.view.dom.contains(selection.anchorNode) && instance.view.dom.contains(selection.focusNode)) {
76
- const anchor = instance.view.posAtDOM(selection.anchorNode, selection.anchorOffset);
77
- const focus = instance.view.posAtDOM(selection.focusNode, selection.focusOffset);
78
- linkSelection = { from: Math.min(anchor, focus), to: Math.max(anchor, focus) };
79
- }
80
- link.value = editor.value?.getAttributes('link').href || '';
81
- linkError.value = false;
82
- linkOpen.value = true;
83
- };
84
- const applyLink = () => {
85
- if (props.disabled || props.readonly) return;
86
- const href = link.value.trim();
87
- if (href && !/^(https?:\/\/|mailto:)/i.test(href)) { linkError.value = true; return; }
88
- const chain = editor.value?.chain().focus().setTextSelection(linkSelection).extendMarkRange('link');
89
- if (href) chain?.setLink({ href }).run(); else chain?.unsetLink().run();
90
- linkOpen.value = false;
91
- };
92
- </script>
93
- <style scoped>
94
- .neat-rich-text :deep(.tiptap) { min-height: 10rem; padding: .75rem; outline-offset: -2px; overflow-wrap: anywhere; }
95
- .neat-rich-text :deep(ul) { list-style: disc; padding-left: 1.5rem; }
96
- .neat-rich-text :deep(ol) { list-style: decimal; padding-left: 1.5rem; }
97
- .neat-rich-text :deep(blockquote) { border-left: 3px solid currentColor; padding-left: .75rem; }
98
- .neat-rich-text :deep(a) { text-decoration: underline; }
99
- </style>