@finema/core 3.7.1 → 3.8.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.
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "3.7.1",
3
+ "version": "3.8.1",
4
4
  "configKey": "core",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
package/dist/module.mjs CHANGED
@@ -4,7 +4,7 @@ import * as lodash from 'lodash-es';
4
4
  import * as theme from '../dist/runtime/theme/index.js';
5
5
 
6
6
  const name = "@finema/core";
7
- const version = "3.7.1";
7
+ const version = "3.8.1";
8
8
 
9
9
  const nuxtAppOptions = {
10
10
  head: {
@@ -131,7 +131,12 @@ const module = defineNuxtModule({
131
131
  "@vee-validate/valibot",
132
132
  "axios",
133
133
  "tailwind-variants",
134
- "@vueuse/core"
134
+ "@vueuse/core",
135
+ "@nuxt/ui > prosemirror-state",
136
+ "@nuxt/ui > prosemirror-transform",
137
+ "@nuxt/ui > prosemirror-model",
138
+ "@nuxt/ui > prosemirror-view",
139
+ "@nuxt/ui > prosemirror-gapcursor"
135
140
  ]
136
141
  }
137
142
  },
@@ -94,12 +94,18 @@ const theme = computed(
94
94
  disabled: wrapperProps.value.disabled
95
95
  })
96
96
  );
97
- const handleDownloadFile = () => {
98
- if (value.value?.url && value.value?.name) {
97
+ const handleDownloadFile = async () => {
98
+ const fileValue = value.value;
99
+ if (fileValue instanceof File) {
100
+ const blobUrl = URL.createObjectURL(fileValue);
99
101
  const a = document.createElement("a");
100
- a.href = value.value.url;
101
- a.download = value.value.name;
102
+ a.href = blobUrl;
103
+ a.download = fileValue.name;
104
+ document.body.appendChild(a);
102
105
  a.click();
106
+ a.remove();
107
+ URL.revokeObjectURL(blobUrl);
108
+ return;
103
109
  }
104
110
  };
105
111
  </script>
@@ -0,0 +1,9 @@
1
+ import { Node } from '@tiptap/core';
2
+ declare module '@tiptap/core' {
3
+ interface Commands<ReturnType> {
4
+ imageUpload: {
5
+ insertImageUpload: () => ReturnType;
6
+ };
7
+ }
8
+ }
9
+ export declare const ImageUpload: Node<any, any>;
@@ -0,0 +1,38 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core";
2
+ import { VueNodeViewRenderer } from "@tiptap/vue-3";
3
+ import ImageUploadNodeComponent from "./EditorImageUploadNode.vue";
4
+ export const ImageUpload = Node.create({
5
+ name: "imageUpload",
6
+ group: "block",
7
+ atom: true,
8
+ draggable: true,
9
+ addAttributes() {
10
+ return {};
11
+ },
12
+ parseHTML() {
13
+ return [{
14
+ tag: 'div[data-type="image-upload"]'
15
+ }];
16
+ },
17
+ renderHTML({
18
+ HTMLAttributes
19
+ }) {
20
+ return ["div", mergeAttributes(HTMLAttributes, {
21
+ "data-type": "image-upload"
22
+ })];
23
+ },
24
+ addNodeView() {
25
+ return VueNodeViewRenderer(ImageUploadNodeComponent);
26
+ },
27
+ addCommands() {
28
+ return {
29
+ insertImageUpload: () => ({
30
+ commands
31
+ }) => {
32
+ return commands.insertContent({
33
+ type: this.name
34
+ });
35
+ }
36
+ };
37
+ }
38
+ });
@@ -0,0 +1,4 @@
1
+ import type { NodeViewProps } from '@tiptap/vue-3';
2
+ declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
3
+ declare const _default: typeof __VLS_export;
4
+ export default _default;
@@ -0,0 +1,104 @@
1
+ <template>
2
+ <NodeViewWrapper>
3
+ <FileUpload
4
+ v-model="file"
5
+ accept="image/*"
6
+ label="Upload an image"
7
+ description="SVG, PNG, JPG or GIF"
8
+ :preview="false"
9
+ class="min-h-48"
10
+ >
11
+ <template #leading>
12
+ <Avatar
13
+ :icon="loading ? 'i-lucide-loader-circle' : 'i-lucide-image'"
14
+ size="xl"
15
+ :ui="{ icon: [loading && 'animate-spin'] }"
16
+ />
17
+ </template>
18
+ </FileUpload>
19
+ </NodeViewWrapper>
20
+ </template>
21
+
22
+ <script setup>
23
+ import { NodeViewWrapper } from "@tiptap/vue-3";
24
+ import { computed, ref, watch } from "vue";
25
+ import { useUploadLoader } from "#core/composables/useUpload";
26
+ import { _get } from "#core/utils/lodash";
27
+ import { StringHelper } from "#core/utils/StringHelper";
28
+ const props = defineProps({
29
+ decorations: { type: Array, required: true },
30
+ selected: { type: Boolean, required: true },
31
+ updateAttributes: { type: Function, required: true },
32
+ deleteNode: { type: Function, required: true },
33
+ node: { type: null, required: true },
34
+ view: { type: null, required: true },
35
+ getPos: { type: null, required: true },
36
+ innerDecorations: { type: null, required: true },
37
+ editor: { type: Object, required: true },
38
+ extension: { type: Object, required: true },
39
+ HTMLAttributes: { type: Object, required: true }
40
+ });
41
+ const options = computed(() => props.extension.options);
42
+ const file = ref(null);
43
+ const loading = ref(false);
44
+ watch(file, async (newFile) => {
45
+ if (!newFile) return;
46
+ loading.value = true;
47
+ if (!options.value.requestOptions) {
48
+ console.warn("ImageUpload: requestOptions is not configured");
49
+ loading.value = false;
50
+ return;
51
+ }
52
+ const request = {
53
+ requestOptions: options.value.requestOptions,
54
+ pathURL: options.value.uploadPathURL
55
+ };
56
+ const uploadLoader = useUploadLoader(request);
57
+ const formData = new FormData();
58
+ const bodyKey = options.value.bodyKey || "file";
59
+ formData.append(bodyKey, newFile);
60
+ uploadLoader.run({
61
+ data: formData
62
+ });
63
+ const stopSuccessWatch = watch(
64
+ () => uploadLoader.status.value.isSuccess,
65
+ (isSuccess) => {
66
+ if (isSuccess && uploadLoader.data.value) {
67
+ const responseURL = options.value.responseURL || "url";
68
+ const url = _get(uploadLoader.data.value, responseURL);
69
+ if (!url) {
70
+ console.error("ImageUpload: Could not find URL in response", uploadLoader.data.value);
71
+ loading.value = false;
72
+ return;
73
+ }
74
+ const pos = props.getPos();
75
+ if (typeof pos !== "number") {
76
+ loading.value = false;
77
+ return;
78
+ }
79
+ props.editor.chain().focus().deleteRange({
80
+ from: pos,
81
+ to: pos + 1
82
+ }).setImage({
83
+ src: url
84
+ }).run();
85
+ loading.value = false;
86
+ stopSuccessWatch();
87
+ stopErrorWatch();
88
+ }
89
+ }
90
+ );
91
+ const stopErrorWatch = watch(
92
+ () => uploadLoader.status.value.isError,
93
+ (isError) => {
94
+ if (isError) {
95
+ const errorMsg = StringHelper.getError(uploadLoader.status.value.errorData);
96
+ console.error("ImageUpload error:", errorMsg);
97
+ loading.value = false;
98
+ stopSuccessWatch();
99
+ stopErrorWatch();
100
+ }
101
+ }
102
+ );
103
+ });
104
+ </script>
@@ -0,0 +1,4 @@
1
+ import type { NodeViewProps } from '@tiptap/vue-3';
2
+ declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
3
+ declare const _default: typeof __VLS_export;
4
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import type { Editor } from '@tiptap/vue-3';
2
+ type __VLS_Props = {
3
+ editor: Editor;
4
+ autoOpen?: boolean;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -0,0 +1,147 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ const props = defineProps({
4
+ editor: { type: Object, required: true },
5
+ autoOpen: { type: Boolean, required: false }
6
+ });
7
+ const open = ref(false);
8
+ const url = ref("");
9
+ const active = computed(() => props.editor.isActive("link"));
10
+ const disabled = computed(() => {
11
+ if (!props.editor.isEditable) return true;
12
+ const {
13
+ selection
14
+ } = props.editor.state;
15
+ return selection.empty && !props.editor.isActive("link");
16
+ });
17
+ watch(() => props.editor, (editor, _, onCleanup) => {
18
+ if (!editor) return;
19
+ const updateUrl = () => {
20
+ const {
21
+ href
22
+ } = editor.getAttributes("link");
23
+ url.value = href || "";
24
+ };
25
+ updateUrl();
26
+ editor.on("selectionUpdate", updateUrl);
27
+ onCleanup(() => {
28
+ editor.off("selectionUpdate", updateUrl);
29
+ });
30
+ }, {
31
+ immediate: true
32
+ });
33
+ watch(active, (isActive) => {
34
+ if (isActive && props.autoOpen) {
35
+ open.value = true;
36
+ }
37
+ });
38
+ const setLink = () => {
39
+ if (!url.value) return;
40
+ const {
41
+ selection
42
+ } = props.editor.state;
43
+ const isEmpty = selection.empty;
44
+ const hasCode = props.editor.isActive("code");
45
+ let chain = props.editor.chain().focus();
46
+ if (hasCode && !isEmpty) {
47
+ chain = chain.extendMarkRange("code").setLink({
48
+ href: url.value
49
+ });
50
+ } else {
51
+ chain = chain.extendMarkRange("link").setLink({
52
+ href: url.value
53
+ });
54
+ if (isEmpty) {
55
+ chain = chain.insertContent({
56
+ type: "text",
57
+ text: url.value
58
+ });
59
+ }
60
+ }
61
+ chain.run();
62
+ open.value = false;
63
+ };
64
+ const removeLink = () => {
65
+ props.editor.chain().focus().extendMarkRange("link").unsetLink().setMeta("preventAutolink", true).run();
66
+ url.value = "";
67
+ open.value = false;
68
+ };
69
+ const openLink = () => {
70
+ if (!url.value) return;
71
+ window.open(url.value, "_blank", "noopener,noreferrer");
72
+ };
73
+ const handleKeyDown = (event) => {
74
+ if (event.key === "Enter") {
75
+ event.preventDefault();
76
+ setLink();
77
+ }
78
+ };
79
+ </script>
80
+
81
+ <template>
82
+ <Popover
83
+ v-model:open="open"
84
+ :ui="{ content: 'p-0.5' }"
85
+ >
86
+ <Tooltip text="Link">
87
+ <Button
88
+ icon="i-lucide-link"
89
+ color="neutral"
90
+ active-color="primary"
91
+ variant="ghost"
92
+ active-variant="soft"
93
+ size="sm"
94
+ :active="active"
95
+ :disabled="disabled"
96
+ />
97
+ </Tooltip>
98
+
99
+ <template #content>
100
+ <Input
101
+ v-model="url"
102
+ autofocus
103
+ name="url"
104
+ type="url"
105
+ variant="none"
106
+ placeholder="Paste a link..."
107
+ @keydown="handleKeyDown"
108
+ >
109
+ <div class="mr-0.5 flex items-center">
110
+ <Button
111
+ icon="i-lucide-corner-down-left"
112
+ variant="ghost"
113
+ size="sm"
114
+ :disabled="!url && !active"
115
+ title="Apply link"
116
+ @click="setLink"
117
+ />
118
+
119
+ <Separator
120
+ orientation="vertical"
121
+ class="mx-1 h-6"
122
+ />
123
+
124
+ <Button
125
+ icon="i-lucide-external-link"
126
+ color="neutral"
127
+ variant="ghost"
128
+ size="sm"
129
+ :disabled="!url && !active"
130
+ title="Open in new window"
131
+ @click="openLink"
132
+ />
133
+
134
+ <Button
135
+ icon="i-lucide-trash"
136
+ color="neutral"
137
+ variant="ghost"
138
+ size="sm"
139
+ :disabled="!url && !active"
140
+ title="Remove link"
141
+ @click="removeLink"
142
+ />
143
+ </div>
144
+ </Input>
145
+ </template>
146
+ </Popover>
147
+ </template>
@@ -0,0 +1,8 @@
1
+ import type { Editor } from '@tiptap/vue-3';
2
+ type __VLS_Props = {
3
+ editor: Editor;
4
+ autoOpen?: boolean;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -1,7 +1,19 @@
1
1
  import type { IWYSIWYGFieldProps } from '#core/components/Form/InputWYSIWYG/types';
2
2
  declare const __VLS_export: import("vue").DefineComponent<IWYSIWYGFieldProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<IWYSIWYGFieldProps> & Readonly<{}>, {
3
- size: "xs" | "sm" | "md" | "lg" | "xl";
4
- color: "primary" | "gray";
3
+ image: {
4
+ requestOptions?: Omit<import("axios").AxiosRequestConfig, "baseURL"> & {
5
+ baseURL: string;
6
+ };
7
+ uploadPathURL?: string;
8
+ bodyKey?: string;
9
+ responseURL?: string;
10
+ responsePath?: string;
11
+ responseName?: string;
12
+ responseSize?: string;
13
+ responseID?: string;
14
+ maxSize?: number;
15
+ };
16
+ editable: boolean;
5
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
18
  declare const _default: typeof __VLS_export;
7
19
  export default _default;
@@ -1,72 +1,78 @@
1
1
  <template>
2
2
  <FieldWrapper v-bind="wrapperProps">
3
3
  <div :class="ui.container()">
4
- <div
5
- v-if="showToolbar"
6
- :class="ui.toolbar()"
4
+ <Editor
5
+ v-slot="{ editor }"
6
+ v-model="value"
7
+ content-type="html"
8
+ :editable="editable"
9
+ :placeholder="placeholder"
10
+ :autofocus="autoFocus"
11
+ class="min-h-[200px]"
12
+ :extensions="[
13
+ TextAlign.configure({
14
+ types: ['heading', 'paragraph'],
15
+ alignments: ['left', 'center', 'right', 'justify']
16
+ }),
17
+ ImageUpload.configure(image)
18
+ ]"
19
+ :ui="{
20
+ content: '',
21
+ base: ['min-h-[200px] w-full sm:px-3 py-1']
22
+ }"
23
+ :handlers="customHandlers"
7
24
  >
8
- <div
9
- v-for="(items, index) in menuItems"
10
- :key="index"
11
- :class="ui.toolbarGroup()"
25
+ <EditorToolbar
26
+ :editor="editor"
27
+ :items="toolbarItems"
28
+ :class="ui.toolbar()"
12
29
  >
13
- <button
14
- v-for="item in items"
15
- :key="item.name"
16
- :class="[ui.menuItem(), { [ui.menuItemActive()]: item.isActive?.() }]"
17
- type="button"
18
- :title="item.title"
19
- @click="item.action"
20
- >
21
- <Icon
22
- :name="item.icon"
23
- :class="ui.icon()"
30
+ <template #link>
31
+ <EditorLinkPopover
32
+ :editor="editor"
33
+ auto-open
24
34
  />
25
- </button>
26
- </div>
27
- </div>
28
- <ClientOnly>
29
- <EditorContent
35
+ </template>
36
+ </EditorToolbar>
37
+ <EditorToolbar
38
+ :editor="editor"
39
+ :items="imageToolbarItems(editor)"
40
+ layout="bubble"
41
+ :should-show="({ editor: editor2, view }) => {
42
+ return editor2.isActive('image') && view.hasFocus();
43
+ }"
44
+ />
45
+
46
+ <EditorSuggestionMenu
30
47
  :editor="editor"
31
- :class="ui.editorContent()"
48
+ :items="suggestionItems"
49
+ :append-to="appendToBody"
32
50
  />
33
- <template #fallback>
34
- <div
35
- class="prose min-h-[200px] px-4 py-2"
36
- v-html="value"
37
- />
38
- </template>
39
- </ClientOnly>
51
+ </Editor>
40
52
  </div>
41
53
  </FieldWrapper>
42
54
  </template>
43
55
 
44
56
  <script setup>
45
- import { computed, watch } from "vue";
46
- import { EditorContent, useEditor } from "@tiptap/vue-3";
47
- import StarterKit from "@tiptap/starter-kit";
48
- import Underline from "@tiptap/extension-underline";
49
- import TextAlign from "@tiptap/extension-text-align";
50
- import Link from "@tiptap/extension-link";
51
- import Image from "@tiptap/extension-image";
52
- import Youtube from "@tiptap/extension-youtube";
57
+ import { computed } from "vue";
53
58
  import { useFieldHOC } from "#core/composables/useForm";
54
59
  import FieldWrapper from "#core/components/Form/FieldWrapper.vue";
55
60
  import { wysiwygTheme } from "#core/theme/wysiwyg";
56
61
  import { useUiConfig } from "#core/composables/useConfig";
62
+ import { TextAlign } from "@tiptap/extension-text-align";
63
+ import { ImageUpload } from "./EditorImageUploadExtension";
64
+ import EditorLinkPopover from "./EditorLinkPopover.vue";
57
65
  const props = defineProps({
58
- editable: { type: Boolean, required: false },
59
- autofocus: { type: Boolean, required: false },
60
- content: { type: String, required: false },
66
+ editable: { type: Boolean, required: false, default: () => true },
67
+ image: { type: Object, required: false, default: () => ({
68
+ bodyKey: "file",
69
+ responseURL: "url",
70
+ responsePath: "path",
71
+ responseName: "name",
72
+ responseSize: "size",
73
+ responseID: "id"
74
+ }) },
61
75
  toolbar: { type: Object, required: false },
62
- minHeight: { type: [String, Number], required: false },
63
- maxHeight: { type: [String, Number], required: false },
64
- size: { type: String, required: false, default: "md" },
65
- color: { type: String, required: false, default: "gray" },
66
- imageUpload: { type: Object, required: false },
67
- linkOptions: { type: Object, required: false },
68
- containerUi: { type: null, required: false },
69
- image: { type: Object, required: false },
70
76
  form: { type: Object, required: false },
71
77
  name: { type: String, required: true },
72
78
  errorMessage: { type: String, required: false },
@@ -86,196 +92,289 @@ const {
86
92
  value,
87
93
  wrapperProps
88
94
  } = useFieldHOC(props);
89
- const ui = computed(() => useUiConfig(wysiwygTheme, "wysiwyg")({
90
- size: props.size,
91
- color: props.color
92
- }));
93
- const showToolbar = computed(() => {
94
- if (!props.toolbar) return true;
95
- return Object.values(props.toolbar).some(Boolean);
96
- });
97
- const editor = useEditor({
98
- content: value.value,
99
- extensions: [
100
- StarterKit,
101
- Underline,
102
- TextAlign.configure({
103
- types: ["heading", "paragraph"]
95
+ const ui = computed(() => useUiConfig(wysiwygTheme, "wysiwyg")({}));
96
+ const appendToBody = false ? () => document.body : void 0;
97
+ const customHandlers = {
98
+ imageUpload: {
99
+ canExecute: (editor) => editor.can().insertContent({
100
+ type: "imageUpload"
104
101
  }),
105
- Link.configure({
106
- openOnClick: false
102
+ execute: (editor) => editor.chain().focus().insertContent({
103
+ type: "imageUpload"
107
104
  }),
108
- Image,
109
- Youtube
110
- ],
111
- editorProps: {
112
- attributes: {
113
- class: "prose px-4 py-2 focus:outline-none min-h-[200px]"
114
- }
115
- },
116
- onUpdate: ({
117
- editor: editor2
118
- }) => {
119
- value.value = editor2.getHTML();
105
+ isActive: (editor) => editor.isActive("imageUpload"),
106
+ isDisabled: void 0
120
107
  }
121
- });
122
- watch(value, (newValue) => {
123
- if (editor.value && newValue !== editor.value.getHTML()) {
124
- editor.value.commands.setContent(newValue);
125
- }
126
- });
127
- const toolbarConfig = {
128
- format: [
108
+ };
109
+ const imageToolbarItems = (editor) => {
110
+ const node = editor.state.doc.nodeAt(editor.state.selection.from);
111
+ return [[{
112
+ icon: "i-lucide-download",
113
+ to: node?.attrs?.src,
114
+ download: true,
115
+ tooltip: {
116
+ text: "Download"
117
+ }
118
+ }], [{
119
+ icon: "i-lucide-trash",
120
+ tooltip: {
121
+ text: "Delete"
122
+ },
123
+ onClick: () => {
124
+ const {
125
+ state
126
+ } = editor;
127
+ const {
128
+ selection
129
+ } = state;
130
+ const pos = selection.from;
131
+ const node2 = state.doc.nodeAt(pos);
132
+ if (node2 && node2.type.name === "image") {
133
+ editor.chain().focus().deleteRange({
134
+ from: pos,
135
+ to: pos + node2.nodeSize
136
+ }).run();
137
+ }
138
+ }
139
+ }]];
140
+ };
141
+ const toolbarItems = [
142
+ // Block types
143
+ [
129
144
  {
130
- key: "bold",
131
- name: "bold",
132
- icon: "ph:text-b-bold",
133
- action: () => editor.value?.chain().focus().toggleBold().run(),
134
- isActive: () => editor.value?.isActive("bold") || false,
135
- title: "\u0E15\u0E31\u0E27\u0E2B\u0E19\u0E32"
145
+ icon: "i-lucide-heading",
146
+ tooltip: {
147
+ text: "Headings"
148
+ },
149
+ content: {
150
+ align: "start"
151
+ },
152
+ items: [{
153
+ kind: "heading",
154
+ level: 1,
155
+ icon: "i-lucide-heading-1",
156
+ label: "Heading 1"
157
+ }, {
158
+ kind: "heading",
159
+ level: 2,
160
+ icon: "i-lucide-heading-2",
161
+ label: "Heading 2"
162
+ }, {
163
+ kind: "heading",
164
+ level: 3,
165
+ icon: "i-lucide-heading-3",
166
+ label: "Heading 3"
167
+ }, {
168
+ kind: "heading",
169
+ level: 4,
170
+ icon: "i-lucide-heading-4",
171
+ label: "Heading 4"
172
+ }]
136
173
  },
174
+ // Text alignment
137
175
  {
138
- key: "italic",
139
- name: "italic",
140
- icon: "ph:text-italic",
141
- action: () => editor.value?.chain().focus().toggleItalic().run(),
142
- isActive: () => editor.value?.isActive("italic") || false,
143
- title: "\u0E15\u0E31\u0E27\u0E40\u0E2D\u0E35\u0E22\u0E07"
176
+ icon: "i-lucide-align-justify",
177
+ tooltip: {
178
+ text: "Text Align"
179
+ },
180
+ content: {
181
+ align: "end"
182
+ },
183
+ items: [
184
+ {
185
+ kind: "textAlign",
186
+ align: "left",
187
+ icon: "i-lucide-align-left",
188
+ label: "Align Left"
189
+ },
190
+ {
191
+ kind: "textAlign",
192
+ align: "center",
193
+ icon: "i-lucide-align-center",
194
+ label: "Align Center"
195
+ },
196
+ {
197
+ kind: "textAlign",
198
+ align: "right",
199
+ icon: "i-lucide-align-right",
200
+ label: "Align Right"
201
+ },
202
+ {
203
+ kind: "textAlign",
204
+ align: "justify",
205
+ icon: "i-lucide-align-justify",
206
+ label: "Align Justify"
207
+ }
208
+ ]
144
209
  },
145
210
  {
146
- key: "underline",
147
- name: "underline",
148
- icon: "ph:text-underline",
149
- action: () => editor.value?.chain().focus().toggleUnderline().run(),
150
- isActive: () => editor.value?.isActive("underline") || false,
151
- title: "\u0E02\u0E35\u0E14\u0E40\u0E2A\u0E49\u0E19\u0E43\u0E15\u0E49"
152
- }
153
- ],
154
- list: [
211
+ icon: "i-lucide-list",
212
+ tooltip: {
213
+ text: "Lists"
214
+ },
215
+ content: {
216
+ align: "start"
217
+ },
218
+ items: [{
219
+ kind: "bulletList",
220
+ icon: "i-lucide-list",
221
+ label: "Bullet List"
222
+ }, {
223
+ kind: "orderedList",
224
+ icon: "i-lucide-list-ordered",
225
+ label: "Ordered List"
226
+ }]
227
+ },
155
228
  {
156
- key: "bulletList",
157
- name: "bullet-list",
158
- icon: "ph:list-bullets",
159
- action: () => editor.value?.chain().focus().toggleBulletList().run(),
160
- isActive: () => editor.value?.isActive("bulletList") || false,
161
- title: "\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E2A\u0E31\u0E0D\u0E25\u0E31\u0E01\u0E29\u0E13\u0E4C"
229
+ kind: "blockquote",
230
+ icon: "i-lucide-text-quote",
231
+ tooltip: {
232
+ text: "Blockquote"
233
+ }
234
+ },
235
+ {
236
+ kind: "codeBlock",
237
+ icon: "i-lucide-square-code",
238
+ tooltip: {
239
+ text: "Code Block"
240
+ }
162
241
  },
163
242
  {
164
- key: "orderedList",
165
- name: "ordered-list",
166
- icon: "ph:list-numbers",
167
- action: () => editor.value?.chain().focus().toggleOrderedList().run(),
168
- isActive: () => editor.value?.isActive("orderedList") || false,
169
- title: "\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E25\u0E33\u0E14\u0E31\u0E1A"
243
+ kind: "horizontalRule",
244
+ icon: "i-lucide-separator-horizontal",
245
+ tooltip: {
246
+ text: "Horizontal Rule"
247
+ }
170
248
  }
171
249
  ],
172
- textAlign: [
250
+ // Text formatting
251
+ [{
252
+ kind: "mark",
253
+ mark: "bold",
254
+ icon: "i-lucide-bold",
255
+ tooltip: {
256
+ text: "Bold"
257
+ }
258
+ }, {
259
+ kind: "mark",
260
+ mark: "italic",
261
+ icon: "i-lucide-italic",
262
+ tooltip: {
263
+ text: "Italic"
264
+ }
265
+ }, {
266
+ kind: "mark",
267
+ mark: "underline",
268
+ icon: "i-lucide-underline",
269
+ tooltip: {
270
+ text: "Underline"
271
+ }
272
+ }, {
273
+ kind: "mark",
274
+ mark: "strike",
275
+ icon: "i-lucide-strikethrough",
276
+ tooltip: {
277
+ text: "Strikethrough"
278
+ }
279
+ }, {
280
+ kind: "mark",
281
+ mark: "code",
282
+ icon: "i-lucide-code",
283
+ tooltip: {
284
+ text: "Code"
285
+ }
286
+ }],
287
+ // Link & image
288
+ [
289
+ {
290
+ slot: "link"
291
+ },
292
+ ...props.image?.requestOptions ? [{
293
+ kind: "imageUpload",
294
+ icon: "i-lucide-image"
295
+ }] : []
296
+ ],
297
+ // History controls
298
+ [{
299
+ kind: "undo",
300
+ icon: "i-lucide-undo",
301
+ tooltip: {
302
+ text: "Undo"
303
+ }
304
+ }, {
305
+ kind: "redo",
306
+ icon: "i-lucide-redo",
307
+ tooltip: {
308
+ text: "Redo"
309
+ }
310
+ }]
311
+ ];
312
+ const suggestionItems = [
313
+ [
314
+ {
315
+ type: "label",
316
+ label: "Text"
317
+ },
318
+ {
319
+ kind: "paragraph",
320
+ label: "Paragraph",
321
+ icon: "i-lucide-type"
322
+ },
173
323
  {
174
- key: "textAlign",
175
- name: "align-left",
176
- icon: "ph:text-align-left",
177
- action: () => editor.value?.chain().focus().setTextAlign("left").run(),
178
- isActive: () => editor.value?.isActive({
179
- textAlign: "left"
180
- }) || false,
181
- title: "\u0E08\u0E31\u0E14\u0E0A\u0E34\u0E14\u0E0B\u0E49\u0E32\u0E22"
324
+ kind: "heading",
325
+ level: 1,
326
+ label: "Heading 1",
327
+ icon: "i-lucide-heading-1"
182
328
  },
183
329
  {
184
- key: "textAlign",
185
- name: "align-center",
186
- icon: "ph:text-align-center",
187
- action: () => editor.value?.chain().focus().setTextAlign("center").run(),
188
- isActive: () => editor.value?.isActive({
189
- textAlign: "center"
190
- }) || false,
191
- title: "\u0E08\u0E31\u0E14\u0E01\u0E36\u0E48\u0E07\u0E01\u0E25\u0E32\u0E07"
330
+ kind: "heading",
331
+ level: 2,
332
+ label: "Heading 2",
333
+ icon: "i-lucide-heading-2"
192
334
  },
193
335
  {
194
- key: "textAlign",
195
- name: "align-right",
196
- icon: "ph:text-align-right",
197
- action: () => editor.value?.chain().focus().setTextAlign("right").run(),
198
- isActive: () => editor.value?.isActive({
199
- textAlign: "right"
200
- }) || false,
201
- title: "\u0E08\u0E31\u0E14\u0E0A\u0E34\u0E14\u0E02\u0E27\u0E32"
336
+ kind: "heading",
337
+ level: 3,
338
+ label: "Heading 3",
339
+ icon: "i-lucide-heading-3"
202
340
  }
203
341
  ],
204
- media: [
342
+ [
205
343
  {
206
- key: "link",
207
- name: "link",
208
- icon: "ph:link-simple",
209
- action: () => {
210
- const url = window.prompt("URL");
211
- if (url) {
212
- editor.value?.chain().focus().setLink({
213
- href: url
214
- }).run();
215
- }
216
- },
217
- isActive: () => editor.value?.isActive("link") || false,
218
- title: "\u0E41\u0E17\u0E23\u0E01\u0E25\u0E34\u0E07\u0E01\u0E4C"
344
+ type: "label",
345
+ label: "Lists"
219
346
  },
220
347
  {
221
- key: "image",
222
- name: "image",
223
- icon: "ph:image",
224
- action: () => {
225
- const url = window.prompt("URL \u0E23\u0E39\u0E1B\u0E20\u0E32\u0E1E");
226
- if (url) {
227
- editor.value?.chain().focus().setImage({
228
- src: url
229
- }).run();
230
- }
231
- },
232
- title: "\u0E41\u0E17\u0E23\u0E01\u0E23\u0E39\u0E1B\u0E20\u0E32\u0E1E"
348
+ kind: "bulletList",
349
+ label: "Bullet List",
350
+ icon: "i-lucide-list"
233
351
  },
234
352
  {
235
- key: "youtube",
236
- name: "video",
237
- icon: "ph:video-camera",
238
- action: () => {
239
- const url = window.prompt("URL \u0E27\u0E34\u0E14\u0E35\u0E42\u0E2D");
240
- if (url) {
241
- editor.value?.chain().focus().setYoutubeVideo({
242
- src: url
243
- }).run();
244
- }
245
- },
246
- title: "\u0E41\u0E17\u0E23\u0E01\u0E27\u0E34\u0E14\u0E35\u0E42\u0E2D"
353
+ kind: "orderedList",
354
+ label: "Numbered List",
355
+ icon: "i-lucide-list-ordered"
247
356
  }
248
357
  ],
249
- history: [
358
+ [
359
+ {
360
+ type: "label",
361
+ label: "Insert"
362
+ },
363
+ {
364
+ kind: "blockquote",
365
+ label: "Blockquote",
366
+ icon: "i-lucide-text-quote"
367
+ },
250
368
  {
251
- key: "undo",
252
- name: "undo",
253
- icon: "ph:arrow-counter-clockwise",
254
- action: () => editor.value?.chain().focus().undo().run(),
255
- title: "\u0E40\u0E25\u0E34\u0E01\u0E17\u0E33"
369
+ kind: "codeBlock",
370
+ label: "Code Block",
371
+ icon: "i-lucide-square-code"
256
372
  },
257
373
  {
258
- key: "redo",
259
- name: "redo",
260
- icon: "ph:arrow-clockwise",
261
- action: () => editor.value?.chain().focus().redo().run(),
262
- title: "\u0E17\u0E33\u0E43\u0E2B\u0E21\u0E48"
374
+ kind: "horizontalRule",
375
+ label: "Divider",
376
+ icon: "i-lucide-separator-horizontal"
263
377
  }
264
378
  ]
265
- };
266
- const menuItems = computed(() => {
267
- const items = [];
268
- Object.entries(toolbarConfig).forEach(([groupKey, groupItems]) => {
269
- const enabledItems = groupItems.filter((item) => {
270
- if (groupKey === "textAlign") {
271
- return props.toolbar?.textAlign;
272
- }
273
- return props.toolbar?.[item.key];
274
- });
275
- if (enabledItems.length > 0) {
276
- items.push(enabledItems);
277
- }
278
- });
279
- return items;
280
- });
379
+ ];
281
380
  </script>
@@ -1,7 +1,19 @@
1
1
  import type { IWYSIWYGFieldProps } from '#core/components/Form/InputWYSIWYG/types';
2
2
  declare const __VLS_export: import("vue").DefineComponent<IWYSIWYGFieldProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<IWYSIWYGFieldProps> & Readonly<{}>, {
3
- size: "xs" | "sm" | "md" | "lg" | "xl";
4
- color: "primary" | "gray";
3
+ image: {
4
+ requestOptions?: Omit<import("axios").AxiosRequestConfig, "baseURL"> & {
5
+ baseURL: string;
6
+ };
7
+ uploadPathURL?: string;
8
+ bodyKey?: string;
9
+ responseURL?: string;
10
+ responsePath?: string;
11
+ responseName?: string;
12
+ responseSize?: string;
13
+ responseID?: string;
14
+ maxSize?: number;
15
+ };
16
+ editable: boolean;
5
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
18
  declare const _default: typeof __VLS_export;
7
19
  export default _default;
@@ -3,60 +3,20 @@ import type { IFieldProps, IFormFieldBase, INPUT_TYPES } from '#core/components/
3
3
  import type { AxiosRequestConfig } from 'axios';
4
4
  export interface IWYSIWYGFieldProps extends IFieldProps {
5
5
  editable?: boolean;
6
- autofocus?: boolean;
7
- content?: string;
8
- toolbar?: {
9
- bold?: boolean;
10
- italic?: boolean;
11
- underline?: boolean;
12
- strike?: boolean;
13
- code?: boolean;
14
- heading?: boolean | number[];
15
- paragraph?: boolean;
16
- bulletList?: boolean;
17
- orderedList?: boolean;
18
- blockquote?: boolean;
19
- codeBlock?: boolean;
20
- horizontalRule?: boolean;
21
- link?: boolean;
22
- image?: {
23
- requestOptions?: Omit<AxiosRequestConfig, 'baseURL'> & {
24
- baseURL: string;
25
- };
26
- uploadPathURL?: string;
27
- bodyKey?: string;
28
- responseURL?: string;
29
- responsePath?: string;
30
- responseName?: string;
31
- responseSize?: string;
32
- responseID?: string;
33
- accept?: string[] | string;
34
- maxSize?: number;
35
- } | boolean;
36
- youtube?: boolean;
37
- textAlign?: boolean;
38
- undo?: boolean;
39
- redo?: boolean;
40
- };
41
- minHeight?: string | number;
42
- maxHeight?: string | number;
43
- size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
44
- color?: 'primary' | 'gray';
45
- imageUpload?: {
46
- enabled?: boolean;
47
- uploadUrl?: string;
48
- maxSize?: number;
49
- accept?: string[];
50
- headers?: Record<string, string>;
51
- };
52
- linkOptions?: {
53
- openOnClick?: boolean;
54
- HTMLAttributes?: Record<string, any>;
55
- };
56
- containerUi?: any;
57
6
  image?: {
58
- requestOptions?: any;
7
+ requestOptions?: Omit<AxiosRequestConfig, 'baseURL'> & {
8
+ baseURL: string;
9
+ };
10
+ uploadPathURL?: string;
11
+ bodyKey?: string;
12
+ responseURL?: string;
13
+ responsePath?: string;
14
+ responseName?: string;
15
+ responseSize?: string;
16
+ responseID?: string;
17
+ maxSize?: number;
59
18
  };
19
+ toolbar?: {};
60
20
  }
61
21
  export type IWYSIWYGField = IFormFieldBase<INPUT_TYPES.WYSIWYG, IWYSIWYGFieldProps, {
62
22
  change?: (content: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finema/core",
3
- "version": "3.7.1",
3
+ "version": "3.8.1",
4
4
  "repository": "https://gitlab.finema.co/finema/ui-kit",
5
5
  "license": "MIT",
6
6
  "author": "Finema Dev Core Team",
@@ -47,15 +47,7 @@
47
47
  "@nuxt/ui": "^4.4.0",
48
48
  "@pinia/nuxt": "^0.11.0",
49
49
  "@tailwindcss/typography": "^0.5.0-alpha.3",
50
- "@tiptap/extension-image": "^3.0.7",
51
- "@tiptap/extension-link": "^3.0.7",
52
- "@tiptap/extension-text-align": "^3.0.7",
53
- "@tiptap/extension-text-style": "^3.0.7",
54
- "@tiptap/extension-underline": "^3.0.7",
55
- "@tiptap/extension-youtube": "^3.0.7",
56
- "@tiptap/pm": "^3.0.7",
57
- "@tiptap/starter-kit": "^3.0.7",
58
- "@tiptap/vue-3": "^3.0.7",
50
+ "@tiptap/extension-text-align": "^3.18.0",
59
51
  "@vee-validate/nuxt": "^4.15.1",
60
52
  "@vee-validate/valibot": "^4.15.1",
61
53
  "@vuepic/vue-datepicker": "^11.0.2",
@@ -91,4 +83,4 @@
91
83
  "lint-staged": {
92
84
  "*": "eslint"
93
85
  }
94
- }
86
+ }
@@ -1,12 +0,0 @@
1
- interface Props {
2
- options?: {
3
- requestOptions?: any;
4
- };
5
- }
6
- declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
- submit: (url: string) => any;
8
- }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
9
- onSubmit?: ((url: string) => any) | undefined;
10
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
- declare const _default: typeof __VLS_export;
12
- export default _default;
@@ -1,38 +0,0 @@
1
- <template>
2
- <div class="upload-image-form">
3
- <input
4
- ref="fileInput"
5
- type="file"
6
- accept="image/*"
7
- @change="handleFileSelect"
8
- />
9
- <button
10
- type="button"
11
- @click="handleUpload"
12
- >
13
- Upload
14
- </button>
15
- </div>
16
- </template>
17
-
18
- <script setup>
19
- import { ref } from "vue";
20
- const emit = defineEmits(["submit"]);
21
- const props = defineProps({
22
- options: { type: Object, required: false }
23
- });
24
- const fileInput = ref();
25
- const selectedFile = ref(null);
26
- const handleFileSelect = (event) => {
27
- const target = event.target;
28
- const file = target.files?.[0];
29
- if (file) {
30
- selectedFile.value = file;
31
- }
32
- };
33
- const handleUpload = async () => {
34
- if (!selectedFile.value) return;
35
- const url = URL.createObjectURL(selectedFile.value);
36
- emit("submit", url);
37
- };
38
- </script>
@@ -1,12 +0,0 @@
1
- interface Props {
2
- options?: {
3
- requestOptions?: any;
4
- };
5
- }
6
- declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
- submit: (url: string) => any;
8
- }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
9
- onSubmit?: ((url: string) => any) | undefined;
10
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
- declare const _default: typeof __VLS_export;
12
- export default _default;