@yonailab/levit-vue 0.0.0-stage → 0.4.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/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @yonailab/levit-vue
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [Levit](https://github.com/azusawa/levit) 에디터의 Vue 3 컴포넌트입니다.
4
+
5
+ ```bash
6
+ npm install @yonailab/levit @yonailab/levit-vue
7
+ ```
8
+
9
+ ## 컴포넌트
10
+
11
+ ```vue
12
+ <script setup lang="ts">
13
+ import { ref } from 'vue';
14
+ import { LevitEditor } from '@yonailab/levit-vue';
15
+ import { ko, type DocumentData } from '@yonailab/levit';
16
+ import '@yonailab/levit/levit.css';
17
+
18
+ const doc = ref<DocumentData>();
19
+ </script>
20
+
21
+ <template>
22
+ <LevitEditor v-model="doc" placeholder="메모를 입력하세요" :messages="ko" />
23
+ </template>
24
+ ```
25
+
26
+ - `v-model`로 문서 JSON을 주고받습니다. 돌아온 값을 다시 넣어도 커서는 그대로입니다.
27
+ - 에디터 옵션(`plugins`, `messages`, `uploadImage`, `highlightCode`)을 props로 넘깁니다.
28
+ - `:bubble-menu="false"`, `:slash-menu="false"`로 메뉴를 끌 수 있습니다.
29
+ - `@ready`로 에디터 인스턴스를 받고, `@error`로 업로드 실패 같은 오류를 받습니다.
30
+ - 컴포넌트 ref의 `editor`로도 인스턴스에 접근할 수 있습니다.
31
+
32
+ ## 컴포저블
33
+
34
+ ```vue
35
+ <script setup lang="ts">
36
+ import { useLevitEditor } from '@yonailab/levit-vue';
37
+
38
+ const { element, editor } = useLevitEditor({ placeholder: '입력하세요' });
39
+ </script>
40
+
41
+ <template>
42
+ <button @click="editor?.chain().toggleBold().run()">굵게</button>
43
+ <div ref="element" />
44
+ </template>
45
+ ```
@@ -0,0 +1,161 @@
1
+ import { type PropType, type Ref, type ShallowRef } from 'vue';
2
+ import { type DocumentData, type EditorOptions, type LevitEditor as Editor, type LevitMessages, type LevitPlugin } from '@yonailab/levit';
3
+ export type { Editor };
4
+ /** Editor options, plus whether to add the bubble and slash menus. */
5
+ export interface UseLevitEditorOptions extends Omit<EditorOptions, 'element'> {
6
+ /** Show the formatting toolbar on selected text. Default: true. */
7
+ bubbleMenu?: boolean;
8
+ /** Open the block menu with "/". Default: true. */
9
+ slashMenu?: boolean;
10
+ /** Called once the editor exists. */
11
+ onReady?: (editor: Editor) => void;
12
+ }
13
+ /**
14
+ * Creates a Levit editor on the element you bind to `element` (with
15
+ * `ref="element"` or `h('div', { ref: element })`). The editor is made on
16
+ * mount and destroyed before unmount.
17
+ */
18
+ export declare function useLevitEditor(options?: UseLevitEditorOptions): {
19
+ element: Ref<HTMLElement | null>;
20
+ editor: ShallowRef<Editor | null>;
21
+ };
22
+ /**
23
+ * A Levit editor with its menus. Bind the document with v-model; listen to
24
+ * `ready` for the editor instance, or read it from the component ref.
25
+ */
26
+ export declare const LevitEditor: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
27
+ modelValue: {
28
+ type: PropType<DocumentData>;
29
+ default: undefined;
30
+ };
31
+ placeholder: {
32
+ type: StringConstructor;
33
+ default: undefined;
34
+ };
35
+ plugins: {
36
+ type: PropType<LevitPlugin[]>;
37
+ default: undefined;
38
+ };
39
+ messages: {
40
+ type: PropType<Partial<LevitMessages>>;
41
+ default: undefined;
42
+ };
43
+ uploadImage: {
44
+ type: PropType<EditorOptions["uploadImage"]>;
45
+ default: undefined;
46
+ };
47
+ highlightCode: {
48
+ type: PropType<EditorOptions["highlightCode"]>;
49
+ default: undefined;
50
+ };
51
+ bubbleMenu: {
52
+ type: BooleanConstructor;
53
+ default: boolean;
54
+ };
55
+ slashMenu: {
56
+ type: BooleanConstructor;
57
+ default: boolean;
58
+ };
59
+ onError: {
60
+ type: PropType<(error: unknown) => void>;
61
+ default: undefined;
62
+ };
63
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
64
+ [key: string]: any;
65
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
66
+ 'update:modelValue': (_value: DocumentData) => true;
67
+ ready: (_editor: Editor) => true;
68
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
69
+ modelValue: {
70
+ type: PropType<DocumentData>;
71
+ default: undefined;
72
+ };
73
+ placeholder: {
74
+ type: StringConstructor;
75
+ default: undefined;
76
+ };
77
+ plugins: {
78
+ type: PropType<LevitPlugin[]>;
79
+ default: undefined;
80
+ };
81
+ messages: {
82
+ type: PropType<Partial<LevitMessages>>;
83
+ default: undefined;
84
+ };
85
+ uploadImage: {
86
+ type: PropType<EditorOptions["uploadImage"]>;
87
+ default: undefined;
88
+ };
89
+ highlightCode: {
90
+ type: PropType<EditorOptions["highlightCode"]>;
91
+ default: undefined;
92
+ };
93
+ bubbleMenu: {
94
+ type: BooleanConstructor;
95
+ default: boolean;
96
+ };
97
+ slashMenu: {
98
+ type: BooleanConstructor;
99
+ default: boolean;
100
+ };
101
+ onError: {
102
+ type: PropType<(error: unknown) => void>;
103
+ default: undefined;
104
+ };
105
+ }>> & Readonly<{
106
+ "onUpdate:modelValue"?: ((_value: DocumentData) => any) | undefined;
107
+ onReady?: ((_editor: Editor) => any) | undefined;
108
+ }>, {
109
+ placeholder: string;
110
+ uploadImage: ((file: File) => Promise<string>) | undefined;
111
+ onError: (error: unknown) => void;
112
+ highlightCode: ((code: string, language: string) => string) | undefined;
113
+ plugins: LevitPlugin[];
114
+ messages: Partial<{
115
+ editorLabel: string;
116
+ placeholder: string;
117
+ formattingToolbar: string;
118
+ bold: string;
119
+ italic: string;
120
+ underline: string;
121
+ strikethrough: string;
122
+ highlight: string;
123
+ code: string;
124
+ link: string;
125
+ linkUrl: string;
126
+ linkPlaceholder: string;
127
+ quote: string;
128
+ slashMenuLabel: string;
129
+ paragraph: string;
130
+ paragraphDescription: string;
131
+ heading1: string;
132
+ heading1Description: string;
133
+ heading2: string;
134
+ heading2Description: string;
135
+ heading3: string;
136
+ heading3Description: string;
137
+ quoteDescription: string;
138
+ codeBlock: string;
139
+ codeBlockDescription: string;
140
+ bulletList: string;
141
+ bulletListDescription: string;
142
+ orderedList: string;
143
+ orderedListDescription: string;
144
+ taskList: string;
145
+ taskListDescription: string;
146
+ divider: string;
147
+ dividerDescription: string;
148
+ image: string;
149
+ imageDescription: string;
150
+ imageUrlPrompt: string;
151
+ markDone: string;
152
+ markNotDone: string;
153
+ codeLanguagePrompt: string;
154
+ codeLanguageLabel: string;
155
+ setCodeLanguage: string;
156
+ plainText: string;
157
+ }>;
158
+ bubbleMenu: boolean;
159
+ slashMenu: boolean;
160
+ modelValue: DocumentData;
161
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ import { defineComponent, h, onBeforeUnmount, onMounted, ref, shallowRef, watch, } from 'vue';
2
+ import { createBubbleMenu, createLevitEditor, createSlashMenu, sameContent, } from '@yonailab/levit';
3
+ /**
4
+ * Creates a Levit editor on the element you bind to `element` (with
5
+ * `ref="element"` or `h('div', { ref: element })`). The editor is made on
6
+ * mount and destroyed before unmount.
7
+ */
8
+ export function useLevitEditor(options = {}) {
9
+ const element = ref(null);
10
+ // shallowRef: Vue must not wrap the editor in a reactive proxy.
11
+ const editor = shallowRef(null);
12
+ let cleanup = () => { };
13
+ onMounted(() => {
14
+ if (!element.value)
15
+ return;
16
+ const { bubbleMenu = true, slashMenu = true, ...editorOptions } = options;
17
+ const instance = createLevitEditor(element.value, editorOptions);
18
+ const menus = [bubbleMenu && createBubbleMenu(instance), slashMenu && createSlashMenu(instance)];
19
+ editor.value = instance;
20
+ options.onReady?.(instance);
21
+ cleanup = () => {
22
+ for (const menu of menus)
23
+ if (menu)
24
+ menu.destroy();
25
+ instance.destroy();
26
+ editor.value = null;
27
+ };
28
+ });
29
+ onBeforeUnmount(() => cleanup());
30
+ return { element, editor };
31
+ }
32
+ /**
33
+ * A Levit editor with its menus. Bind the document with v-model; listen to
34
+ * `ready` for the editor instance, or read it from the component ref.
35
+ */
36
+ export const LevitEditor = defineComponent({
37
+ name: 'LevitEditor',
38
+ props: {
39
+ modelValue: { type: Object, default: undefined },
40
+ placeholder: { type: String, default: undefined },
41
+ plugins: { type: Array, default: undefined },
42
+ messages: { type: Object, default: undefined },
43
+ uploadImage: { type: Function, default: undefined },
44
+ highlightCode: { type: Function, default: undefined },
45
+ bubbleMenu: { type: Boolean, default: true },
46
+ slashMenu: { type: Boolean, default: true },
47
+ // A prop rather than an emit, so without an @error listener the editor
48
+ // falls back to logging errors to the console.
49
+ onError: { type: Function, default: undefined },
50
+ },
51
+ emits: {
52
+ 'update:modelValue': (_value) => true,
53
+ ready: (_editor) => true,
54
+ },
55
+ setup(props, { emit, expose }) {
56
+ const { element, editor } = useLevitEditor({
57
+ initialContent: props.modelValue,
58
+ placeholder: props.placeholder,
59
+ plugins: props.plugins,
60
+ messages: props.messages,
61
+ uploadImage: props.uploadImage,
62
+ highlightCode: props.highlightCode,
63
+ bubbleMenu: props.bubbleMenu,
64
+ slashMenu: props.slashMenu,
65
+ onChange: (ed) => emit('update:modelValue', ed.getJSON()),
66
+ onError: props.onError,
67
+ onReady: (ed) => emit('ready', ed),
68
+ });
69
+ // Replace the content only when the bound value differs, so the value
70
+ // that comes back from update:modelValue does not reset the caret.
71
+ watch(() => props.modelValue, (value) => {
72
+ if (!editor.value || value === undefined)
73
+ return;
74
+ if (!sameContent(value, editor.value.getJSON()))
75
+ editor.value.setJSON(value);
76
+ });
77
+ expose({
78
+ get editor() {
79
+ return editor.value;
80
+ },
81
+ });
82
+ return () => h('div', { ref: element });
83
+ },
84
+ });
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@yonailab/levit-vue",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "Vue components for the Levit editor.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "sideEffects": false,
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc --noEmit",
23
+ "check:package": "node ../levit/scripts/check-package.mjs --wrapper",
24
+ "prepublishOnly": "npm run typecheck && npm test && npm run build && npm run check:package"
25
+ },
26
+ "peerDependencies": {
27
+ "@yonailab/levit": "^0.4.0",
28
+ "vue": "^3.3"
29
+ },
30
+ "keywords": [
31
+ "editor",
32
+ "wysiwyg",
33
+ "rich-text",
34
+ "levit",
35
+ "vue"
36
+ ],
37
+ "author": "yonailab",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/azusawa/levit.git",
41
+ "directory": "packages/vue"
42
+ },
43
+ "homepage": "https://github.com/azusawa/levit/tree/main/packages/vue#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/azusawa/levit/issues"
46
+ },
47
+ "license": "MIT",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "devDependencies": {
52
+ "vue": "^3.5.45"
53
+ }
54
+ }