tiptapify 0.1.5 → 0.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/README.md +1 -1
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +30351 -26471
- package/dist/tiptapify.umd.js +57 -52
- package/index.d.ts +71 -1
- package/package.json +52 -45
- package/src/components/Tiptapify.vue +38 -1
- package/src/components/Toolbar/ai.ts +13 -0
- package/src/components/Toolbar/items.ts +2 -0
- package/src/components/editorExtensions.ts +9 -3
- package/src/components/index.ts +4 -3
- package/src/constants/style.test.ts +49 -0
- package/src/extensions/PickerEventBus.test.ts +38 -0
- package/src/extensions/components/ai/Button.vue +42 -0
- package/src/extensions/components/ai/Dialog.vue +262 -0
- package/src/extensions/components/media/link/LinkDialog.vue +5 -5
- package/src/i18n/locales/ar.json +29 -0
- package/src/i18n/locales/ch.json +29 -0
- package/src/i18n/locales/cz.json +29 -0
- package/src/i18n/locales/de.json +29 -0
- package/src/i18n/locales/en.json +29 -0
- package/src/i18n/locales/es.json +29 -0
- package/src/i18n/locales/fi.json +29 -0
- package/src/i18n/locales/fr.json +29 -0
- package/src/i18n/locales/hu.json +29 -0
- package/src/i18n/locales/it.json +29 -0
- package/src/i18n/locales/ja.json +29 -0
- package/src/i18n/locales/ko.json +29 -0
- package/src/i18n/locales/la.json +29 -0
- package/src/i18n/locales/lt.json +29 -0
- package/src/i18n/locales/nl.json +29 -0
- package/src/i18n/locales/pl.json +29 -0
- package/src/i18n/locales/pt.json +29 -0
- package/src/i18n/locales/ru.json +29 -0
- package/src/i18n/locales/se.json +29 -0
- package/src/i18n/locales/th.json +29 -0
- package/src/i18n/locales/tr.json +29 -0
- package/src/i18n/locales/uk.json +29 -0
- package/src/i18n/locales/vi.json +29 -0
- package/src/index.ts +15 -1
- package/src/types/editor.ts +74 -1
- package/src/types/toolbarTypes.ts +2 -1
- package/src/utils/helpers.test.ts +32 -0
package/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
SingleCommands,
|
|
33
33
|
} from '@tiptap/core'
|
|
34
34
|
|
|
35
|
+
import '@tiptap/extensions'
|
|
35
36
|
import '@tiptap/extension-text-style'
|
|
36
37
|
import '@tiptap/extension-list'
|
|
37
38
|
import '@tiptap/extensions'
|
|
@@ -42,6 +43,7 @@ import '@tiptap/extension-heading'
|
|
|
42
43
|
import '@tiptap/extension-bold'
|
|
43
44
|
import '@tiptap/extension-italic'
|
|
44
45
|
import '@tiptap/extension-strike'
|
|
46
|
+
import '@tiptap/extension-link'
|
|
45
47
|
import '@tiptap/extension-code'
|
|
46
48
|
import '@tiptap/extension-blockquote'
|
|
47
49
|
import '@tiptap/extension-hard-break'
|
|
@@ -66,6 +68,74 @@ export interface TiptapifyOptions {
|
|
|
66
68
|
i18n?: string
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
export type TiptapifyAiMode = 'insert' | 'replace' | 'append'
|
|
72
|
+
|
|
73
|
+
export interface TiptapifyAiPromptExample {
|
|
74
|
+
title: string
|
|
75
|
+
prompt: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type TiptapifyAiChatRole = 'system' | 'user' | 'assistant' | 'developer' | 'tool'
|
|
79
|
+
|
|
80
|
+
export interface TiptapifyAiChatMessage {
|
|
81
|
+
role: TiptapifyAiChatRole
|
|
82
|
+
content: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface TiptapifyAiEditorContext {
|
|
86
|
+
prompt: string
|
|
87
|
+
selectedText: string
|
|
88
|
+
text: string
|
|
89
|
+
html: string
|
|
90
|
+
json: object
|
|
91
|
+
mode: TiptapifyAiMode
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface TiptapifyAiRequest {
|
|
95
|
+
model?: string
|
|
96
|
+
messages: TiptapifyAiChatMessage[]
|
|
97
|
+
temperature?: number
|
|
98
|
+
stream?: false
|
|
99
|
+
[key: string]: unknown
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TiptapifyAiResponse {
|
|
103
|
+
content: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface TiptapifyAiOpenAiResponse {
|
|
107
|
+
choices?: Array<{
|
|
108
|
+
message?: {
|
|
109
|
+
content?: string
|
|
110
|
+
}
|
|
111
|
+
text?: string
|
|
112
|
+
}>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type TiptapifyAiProvider = (request: TiptapifyAiRequest, context: TiptapifyAiEditorContext) => Promise<TiptapifyAiResponse | TiptapifyAiOpenAiResponse | string>
|
|
116
|
+
|
|
117
|
+
export type TiptapifyAiTokenProvider = () => Promise<string | null> | string | null
|
|
118
|
+
|
|
119
|
+
export interface TiptapifyAiStorage {
|
|
120
|
+
getItem: (key: string) => string | null | Promise<string | null>
|
|
121
|
+
setItem: (key: string, value: string) => void | Promise<void>
|
|
122
|
+
removeItem: (key: string) => void | Promise<void>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface TiptapifyAiOptions {
|
|
126
|
+
aiProvider?: TiptapifyAiProvider
|
|
127
|
+
model?: string
|
|
128
|
+
promptExamples?: TiptapifyAiPromptExample[]
|
|
129
|
+
mode?: TiptapifyAiMode
|
|
130
|
+
defaultPrompt?: string
|
|
131
|
+
systemPrompt?: string
|
|
132
|
+
temperature?: number
|
|
133
|
+
chatCompletionOptions?: Record<string, unknown>
|
|
134
|
+
createMessages?: (context: TiptapifyAiEditorContext) => TiptapifyAiChatMessage[]
|
|
135
|
+
tokenProvider?: TiptapifyAiTokenProvider
|
|
136
|
+
storage?: TiptapifyAiStorage
|
|
137
|
+
}
|
|
138
|
+
|
|
69
139
|
declare const TiptapifyPlugin: {
|
|
70
140
|
install: (app: never, options?: TiptapifyOptions) => void
|
|
71
141
|
}
|
|
@@ -114,4 +184,4 @@ declare module '@vue/runtime-core' {
|
|
|
114
184
|
interface GlobalComponents {
|
|
115
185
|
Tiptapify: typeof Tiptapify
|
|
116
186
|
}
|
|
117
|
-
}
|
|
187
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tiptapify",
|
|
3
3
|
"types": "./index.d.ts",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Tiptap3 editor with Vuetify3 menu implementation",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"dev": "vite",
|
|
31
31
|
"build": "vite build",
|
|
32
32
|
"lint": "eslint resources/ts --fix --ext .ts,.js,.cjs,.vue,.tsx,.jsx",
|
|
33
|
-
"test": "
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"test:watch": "vitest",
|
|
34
35
|
"build:charmap": "tsx build-charmap.ts",
|
|
35
36
|
"build:emojis": "tsx build-emojis.ts",
|
|
36
37
|
"docs:dev": "pnpm --filter tiptapify-docs docs:dev",
|
|
@@ -55,6 +56,10 @@
|
|
|
55
56
|
"material design icons",
|
|
56
57
|
"wysiwyg",
|
|
57
58
|
"wysiwyg toolbar",
|
|
59
|
+
"rich text editor",
|
|
60
|
+
"vue editor",
|
|
61
|
+
"vue rich text",
|
|
62
|
+
"vue wysiwyg",
|
|
58
63
|
"markdown",
|
|
59
64
|
"emoji",
|
|
60
65
|
"emoticons"
|
|
@@ -65,45 +70,45 @@
|
|
|
65
70
|
"packageManager": "pnpm@11.8.0",
|
|
66
71
|
"dependencies": {
|
|
67
72
|
"@floating-ui/dom": "^1.7.6",
|
|
68
|
-
"@tiptap/core": "^3.
|
|
69
|
-
"@tiptap/extension-blockquote": "^3.
|
|
70
|
-
"@tiptap/extension-bold": "^3.
|
|
71
|
-
"@tiptap/extension-bubble-menu": "^3.
|
|
72
|
-
"@tiptap/extension-code": "^3.
|
|
73
|
-
"@tiptap/extension-code-block": "^3.
|
|
74
|
-
"@tiptap/extension-code-block-lowlight": "^3.
|
|
75
|
-
"@tiptap/extension-color": "^3.
|
|
76
|
-
"@tiptap/extension-document": "^3.
|
|
77
|
-
"@tiptap/extension-floating-menu": "^3.
|
|
78
|
-
"@tiptap/extension-font-family": "^3.
|
|
79
|
-
"@tiptap/extension-hard-break": "^3.
|
|
80
|
-
"@tiptap/extension-heading": "^3.
|
|
81
|
-
"@tiptap/extension-highlight": "^3.
|
|
82
|
-
"@tiptap/extension-horizontal-rule": "^3.
|
|
83
|
-
"@tiptap/extension-image": "^3.
|
|
84
|
-
"@tiptap/extension-invisible-characters": "^3.
|
|
85
|
-
"@tiptap/extension-italic": "^3.
|
|
86
|
-
"@tiptap/extension-link": "^3.
|
|
87
|
-
"@tiptap/extension-list": "^3.
|
|
88
|
-
"@tiptap/extension-list-item": "^3.
|
|
89
|
-
"@tiptap/extension-paragraph": "^3.
|
|
90
|
-
"@tiptap/extension-placeholder": "^3.
|
|
91
|
-
"@tiptap/extension-strike": "^3.
|
|
92
|
-
"@tiptap/extension-subscript": "^3.
|
|
93
|
-
"@tiptap/extension-superscript": "^3.
|
|
94
|
-
"@tiptap/extension-table": "^3.
|
|
95
|
-
"@tiptap/extension-task-item": "^3.
|
|
96
|
-
"@tiptap/extension-task-list": "^3.
|
|
97
|
-
"@tiptap/extension-text": "^3.
|
|
98
|
-
"@tiptap/extension-text-align": "^3.
|
|
99
|
-
"@tiptap/extension-text-style": "^3.
|
|
100
|
-
"@tiptap/extension-typography": "^3.
|
|
101
|
-
"@tiptap/extension-underline": "^3.
|
|
102
|
-
"@tiptap/extension-youtube": "^3.
|
|
103
|
-
"@tiptap/extensions": "^3.
|
|
104
|
-
"@tiptap/pm": "^3.
|
|
105
|
-
"@tiptap/suggestion": "^3.
|
|
106
|
-
"@tiptap/vue-3": "^3.
|
|
73
|
+
"@tiptap/core": "^3.27.1",
|
|
74
|
+
"@tiptap/extension-blockquote": "^3.27.1",
|
|
75
|
+
"@tiptap/extension-bold": "^3.27.1",
|
|
76
|
+
"@tiptap/extension-bubble-menu": "^3.27.1",
|
|
77
|
+
"@tiptap/extension-code": "^3.27.1",
|
|
78
|
+
"@tiptap/extension-code-block": "^3.27.1",
|
|
79
|
+
"@tiptap/extension-code-block-lowlight": "^3.27.1",
|
|
80
|
+
"@tiptap/extension-color": "^3.27.1",
|
|
81
|
+
"@tiptap/extension-document": "^3.27.1",
|
|
82
|
+
"@tiptap/extension-floating-menu": "^3.27.1",
|
|
83
|
+
"@tiptap/extension-font-family": "^3.27.1",
|
|
84
|
+
"@tiptap/extension-hard-break": "^3.27.1",
|
|
85
|
+
"@tiptap/extension-heading": "^3.27.1",
|
|
86
|
+
"@tiptap/extension-highlight": "^3.27.1",
|
|
87
|
+
"@tiptap/extension-horizontal-rule": "^3.27.1",
|
|
88
|
+
"@tiptap/extension-image": "^3.27.1",
|
|
89
|
+
"@tiptap/extension-invisible-characters": "^3.27.1",
|
|
90
|
+
"@tiptap/extension-italic": "^3.27.1",
|
|
91
|
+
"@tiptap/extension-link": "^3.27.1",
|
|
92
|
+
"@tiptap/extension-list": "^3.27.1",
|
|
93
|
+
"@tiptap/extension-list-item": "^3.27.1",
|
|
94
|
+
"@tiptap/extension-paragraph": "^3.27.1",
|
|
95
|
+
"@tiptap/extension-placeholder": "^3.27.1",
|
|
96
|
+
"@tiptap/extension-strike": "^3.27.1",
|
|
97
|
+
"@tiptap/extension-subscript": "^3.27.1",
|
|
98
|
+
"@tiptap/extension-superscript": "^3.27.1",
|
|
99
|
+
"@tiptap/extension-table": "^3.27.1",
|
|
100
|
+
"@tiptap/extension-task-item": "^3.27.1",
|
|
101
|
+
"@tiptap/extension-task-list": "^3.27.1",
|
|
102
|
+
"@tiptap/extension-text": "^3.27.1",
|
|
103
|
+
"@tiptap/extension-text-align": "^3.27.1",
|
|
104
|
+
"@tiptap/extension-text-style": "^3.27.1",
|
|
105
|
+
"@tiptap/extension-typography": "^3.27.1",
|
|
106
|
+
"@tiptap/extension-underline": "^3.27.1",
|
|
107
|
+
"@tiptap/extension-youtube": "^3.27.1",
|
|
108
|
+
"@tiptap/extensions": "^3.27.1",
|
|
109
|
+
"@tiptap/pm": "^3.27.1",
|
|
110
|
+
"@tiptap/suggestion": "^3.27.1",
|
|
111
|
+
"@tiptap/vue-3": "^3.27.1",
|
|
107
112
|
"emoji.json": "^16.0.0",
|
|
108
113
|
"highlight.js": "^11.11.1",
|
|
109
114
|
"linkifyjs": "^4.3.3",
|
|
@@ -116,8 +121,8 @@
|
|
|
116
121
|
"vuetify": "^3.8.5 || ^4.0.0"
|
|
117
122
|
},
|
|
118
123
|
"devDependencies": {
|
|
119
|
-
"@intlify/unplugin-vue-i18n": "^11.2.
|
|
120
|
-
"@types/node": "^
|
|
124
|
+
"@intlify/unplugin-vue-i18n": "^11.2.4",
|
|
125
|
+
"@types/node": "^26.0.0",
|
|
121
126
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
122
127
|
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
|
123
128
|
"eslint": "^10.5.0",
|
|
@@ -127,8 +132,9 @@
|
|
|
127
132
|
"eslint-plugin-promise": "^7.3.0",
|
|
128
133
|
"eslint-plugin-regexp": "3.1.0",
|
|
129
134
|
"eslint-plugin-sonarjs": "2.0.4",
|
|
130
|
-
"eslint-plugin-unicorn": "^
|
|
135
|
+
"eslint-plugin-unicorn": "^68.0.0",
|
|
131
136
|
"eslint-plugin-vue": "^10.9.2",
|
|
137
|
+
"jsdom": "^29.1.1",
|
|
132
138
|
"sass-embedded": "^1.100.0",
|
|
133
139
|
"stylelint": "^17.13.0",
|
|
134
140
|
"stylelint-config-standard-scss": "17.0.0",
|
|
@@ -139,7 +145,8 @@
|
|
|
139
145
|
"vite": "^8.0.16",
|
|
140
146
|
"vite-plugin-vuetify": "^2.1.3",
|
|
141
147
|
"vite-svg-loader": "^5.1.1",
|
|
148
|
+
"vitest": "^4.1.9",
|
|
142
149
|
"vue-eslint-parser": "^10.4.1",
|
|
143
150
|
"vue-tsc": "^3.3.5"
|
|
144
151
|
}
|
|
145
|
-
}
|
|
152
|
+
}
|
|
@@ -6,7 +6,7 @@ import { SlashCommandsConfig } from '@tiptapify/types/slashCommandsTypes'
|
|
|
6
6
|
import { computed, onBeforeUnmount, PropType, provide, ref, ShallowRef } from 'vue'
|
|
7
7
|
import { default as Toolbar } from '@tiptapify/components/Toolbar/Index.vue'
|
|
8
8
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
|
9
|
-
import { TiptapifyEditor, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
|
|
9
|
+
import { TiptapifyAiOptions, TiptapifyEditor, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
|
|
10
10
|
import MenuBubble from '@tiptapify/components/MenuBubble.vue'
|
|
11
11
|
import MenuFloating from '@tiptapify/components/MenuFloating.vue'
|
|
12
12
|
|
|
@@ -32,10 +32,12 @@ const props = defineProps({
|
|
|
32
32
|
placeholder: { type: String, default () { return '' } },
|
|
33
33
|
showWordsCount: { type: Boolean, default () { return true } },
|
|
34
34
|
showCharactersCount: { type: Boolean, default () { return true } },
|
|
35
|
+
limit: { type: Number as PropType<number | null>, default () { return null } },
|
|
35
36
|
defaultFontFamily: { type: String, default () { return 'Inter' } },
|
|
36
37
|
fontMeasure: { type: String, default () { return 'px' } },
|
|
37
38
|
rounded: { type: String, default () { return '0' } },
|
|
38
39
|
customExtensions: { type: Array as PropType<toolbarSections>, default() { return [] } },
|
|
40
|
+
ai: { type: [Boolean, Object] as PropType<boolean | TiptapifyAiOptions>, default() { return false } },
|
|
39
41
|
interactiveStyles: { type: Boolean, default() { return true } },
|
|
40
42
|
loading: { type: Boolean, default() { return false } },
|
|
41
43
|
loadingColor: { type: String, default() { return 'default' } },
|
|
@@ -49,6 +51,39 @@ const { t } = useI18n()
|
|
|
49
51
|
const appTheme = useTheme()
|
|
50
52
|
const currentTheme = ref(appTheme.global.name)
|
|
51
53
|
|
|
54
|
+
const defaultAiPromptExamples = computed(() => [
|
|
55
|
+
{
|
|
56
|
+
title: t('ai.defaultExamples.improve.title'),
|
|
57
|
+
prompt: t('ai.defaultExamples.improve.prompt'),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: t('ai.defaultExamples.proofReading.title'),
|
|
61
|
+
prompt: t('ai.defaultExamples.proofReading.prompt'),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: t('ai.defaultExamples.summarize.title'),
|
|
65
|
+
prompt: t('ai.defaultExamples.summarize.prompt'),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: t('ai.defaultExamples.expand.title'),
|
|
69
|
+
prompt: t('ai.defaultExamples.expand.prompt'),
|
|
70
|
+
},
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
const tiptapifyAi = computed(() => {
|
|
74
|
+
if (props.ai === false) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const options = props.ai === true ? {} : props.ai
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...options,
|
|
82
|
+
enabled: true,
|
|
83
|
+
promptExamples: options.promptExamples?.length ? options.promptExamples : defaultAiPromptExamples.value,
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
52
87
|
function contentChanged() {
|
|
53
88
|
emit('content-changed', { html: editor.value?.getHTML(), json: editor.value?.getJSON() })
|
|
54
89
|
}
|
|
@@ -58,6 +93,7 @@ const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
|
|
|
58
93
|
computed(() => props.placeholder || t('content.placeholder')).value,
|
|
59
94
|
props.slashCommands,
|
|
60
95
|
props.customExtensions,
|
|
96
|
+
props.limit,
|
|
61
97
|
contentChanged,
|
|
62
98
|
(editorInstance: Editor) => {
|
|
63
99
|
(<TiptapifyEditor>editorInstance).interactiveStyles = props.interactiveStyles
|
|
@@ -73,6 +109,7 @@ const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'
|
|
|
73
109
|
|
|
74
110
|
provide('tiptapifyEditor', editor)
|
|
75
111
|
provide('tiptapifyI18n', { t })
|
|
112
|
+
provide('tiptapifyAi', tiptapifyAi)
|
|
76
113
|
|
|
77
114
|
editor.value?.chain().setFontFamily(props.defaultFontFamily).run()
|
|
78
115
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import actions from '@tiptapify/components/Toolbar/actions'
|
|
2
|
+
import ai from '@tiptapify/components/Toolbar/ai'
|
|
2
3
|
import alignment from '@tiptapify/components/Toolbar/alignment'
|
|
3
4
|
import format from '@tiptapify/components/Toolbar/format'
|
|
4
5
|
import formatExtra from '@tiptapify/components/Toolbar/formatExtra'
|
|
@@ -15,6 +16,7 @@ const items = {
|
|
|
15
16
|
alignment,
|
|
16
17
|
list,
|
|
17
18
|
actions,
|
|
19
|
+
ai,
|
|
18
20
|
misc,
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
|
2
|
+
import { Gapcursor } from '@tiptap/extensions'
|
|
2
3
|
import { TextStyleKit } from '@tiptap/extension-text-style'
|
|
3
4
|
import { BulletList, OrderedList, ListItem, ListKeymap, TaskList, TaskItem } from '@tiptap/extension-list'
|
|
4
5
|
import { Selection, Focus, Placeholder, UndoRedo, Dropcursor, CharacterCount } from '@tiptap/extensions'
|
|
@@ -9,6 +10,7 @@ import { Heading } from '@tiptap/extension-heading'
|
|
|
9
10
|
import { Bold } from '@tiptap/extension-bold'
|
|
10
11
|
import { Italic } from '@tiptap/extension-italic'
|
|
11
12
|
import { Strike } from '@tiptap/extension-strike'
|
|
13
|
+
import { Link } from '@tiptap/extension-link'
|
|
12
14
|
import { Code } from '@tiptap/extension-code'
|
|
13
15
|
import { Blockquote } from '@tiptap/extension-blockquote'
|
|
14
16
|
import { HardBreak } from '@tiptap/extension-hard-break'
|
|
@@ -51,8 +53,11 @@ const lowlight = createLowlight(common)
|
|
|
51
53
|
// register language example
|
|
52
54
|
// lowlight.register('ts', ts)
|
|
53
55
|
|
|
54
|
-
export function editorExtensions (placeholder: string, slashCommands: SlashCommandsConfig, customExtensions: toolbarSections) {
|
|
56
|
+
export function editorExtensions (placeholder: string, slashCommands: SlashCommandsConfig, customExtensions: toolbarSections, limit: number | null = null) {
|
|
57
|
+
const characterCount = limit && limit > 0 ? CharacterCount.configure({ limit }) : CharacterCount
|
|
58
|
+
|
|
55
59
|
const extensions = [
|
|
60
|
+
Gapcursor,
|
|
56
61
|
TextStyleKit,
|
|
57
62
|
Document,
|
|
58
63
|
Text,
|
|
@@ -61,6 +66,7 @@ export function editorExtensions (placeholder: string, slashCommands: SlashComma
|
|
|
61
66
|
Bold,
|
|
62
67
|
Italic,
|
|
63
68
|
Strike,
|
|
69
|
+
Link,
|
|
64
70
|
Blockquote,
|
|
65
71
|
OrderedList,
|
|
66
72
|
BulletList,
|
|
@@ -107,7 +113,7 @@ export function editorExtensions (placeholder: string, slashCommands: SlashComma
|
|
|
107
113
|
Selection.configure({ className: 'selection' }),
|
|
108
114
|
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
|
109
115
|
Placeholder.configure({ placeholder }),
|
|
110
|
-
|
|
116
|
+
characterCount,
|
|
111
117
|
InvisibleCharacters.configure({
|
|
112
118
|
visible: false,
|
|
113
119
|
}),
|
|
@@ -135,4 +141,4 @@ export function editorExtensions (placeholder: string, slashCommands: SlashComma
|
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
return extensions
|
|
138
|
-
}
|
|
144
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -9,15 +9,16 @@ export function getTiptapEditor (
|
|
|
9
9
|
placeholder: string,
|
|
10
10
|
slashCommands: SlashCommandsConfig = true,
|
|
11
11
|
customExtensions: toolbarSections,
|
|
12
|
+
limit: number | null = null,
|
|
12
13
|
onUpdate: Function = () => {},
|
|
13
14
|
onCreate?: (editor: Editor) => void,
|
|
14
15
|
): ShallowRef<Editor | undefined> {
|
|
15
|
-
const extensions = editorExtensions(placeholder, slashCommands, customExtensions)
|
|
16
|
+
const extensions = editorExtensions(placeholder, slashCommands, customExtensions, limit)
|
|
16
17
|
|
|
17
18
|
return useEditor({
|
|
18
19
|
content,
|
|
19
20
|
extensions,
|
|
20
21
|
onUpdate: ({ editor }) => onUpdate(),
|
|
21
|
-
onCreate: onCreate ? ({ editor }) => onCreate(editor) : undefined,
|
|
22
|
+
onCreate: onCreate ? ({ editor }) => onCreate(editor as Editor) : undefined,
|
|
22
23
|
})
|
|
23
|
-
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { setHeadingLevels, headingLevels, fontSizes, defaultFontSize, lineHeights, defaultLineHeight } from './style'
|
|
3
|
+
|
|
4
|
+
describe('style constants', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
headingLevels.value = [1, 2, 3, 4, 5, 6]
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('sets custom heading levels', () => {
|
|
10
|
+
setHeadingLevels([2, 3, 4])
|
|
11
|
+
expect(headingLevels.value).toEqual([2, 3, 4])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('does not change heading levels on empty array', () => {
|
|
15
|
+
setHeadingLevels([])
|
|
16
|
+
expect(headingLevels.value).toEqual([1, 2, 3, 4, 5, 6])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('rejects levels below 1', () => {
|
|
20
|
+
expect(() => setHeadingLevels([0])).toThrow('customHeadingLevels must be between 1 and 6')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('rejects levels above 6', () => {
|
|
24
|
+
expect(() => setHeadingLevels([7])).toThrow('customHeadingLevels must be between 1 and 6')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('rejects any invalid level in a mixed array', () => {
|
|
28
|
+
expect(() => setHeadingLevels([1, 2, 7])).toThrow('customHeadingLevels must be between 1 and 6')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('has valid fontSizes constant', () => {
|
|
32
|
+
expect(fontSizes).toContain(12)
|
|
33
|
+
expect(fontSizes).toContain(6)
|
|
34
|
+
expect(fontSizes).toContain(96)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('has valid defaultFontSize', () => {
|
|
38
|
+
expect(defaultFontSize).toBe(12)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('has valid lineHeights constant', () => {
|
|
42
|
+
expect(lineHeights).toContain(1)
|
|
43
|
+
expect(lineHeights).toContain(4)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('has valid defaultLineHeight', () => {
|
|
47
|
+
expect(defaultLineHeight).toBe(1)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { PickerEventBus } from './PickerEventBus'
|
|
3
|
+
|
|
4
|
+
describe('PickerEventBus', () => {
|
|
5
|
+
it('calls registered callback on emit', () => {
|
|
6
|
+
const callback = vi.fn()
|
|
7
|
+
PickerEventBus.on('close', callback)
|
|
8
|
+
PickerEventBus.emit('close', { type: 'emoji' })
|
|
9
|
+
expect(callback).toHaveBeenCalledWith({ type: 'emoji' })
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('does not call callback after off', () => {
|
|
13
|
+
const callback = vi.fn()
|
|
14
|
+
PickerEventBus.on('close', callback)
|
|
15
|
+
PickerEventBus.off('close', callback)
|
|
16
|
+
PickerEventBus.emit('close', { type: 'charmap' })
|
|
17
|
+
expect(callback).not.toHaveBeenCalled()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('supports multiple listeners', () => {
|
|
21
|
+
const callback1 = vi.fn()
|
|
22
|
+
const callback2 = vi.fn()
|
|
23
|
+
PickerEventBus.on('close', callback1)
|
|
24
|
+
PickerEventBus.on('close', callback2)
|
|
25
|
+
PickerEventBus.emit('close', { type: 'emoji' })
|
|
26
|
+
expect(callback1).toHaveBeenCalledTimes(1)
|
|
27
|
+
expect(callback2).toHaveBeenCalledTimes(1)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('does not throw when emitting with no listeners', () => {
|
|
31
|
+
expect(() => PickerEventBus.emit('close', { type: 'emoji' })).not.toThrow()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('does not throw when removing unregistered listener', () => {
|
|
35
|
+
const callback = vi.fn()
|
|
36
|
+
expect(() => PickerEventBus.off('close', callback)).not.toThrow()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
|
|
3
|
+
import * as mdi from '@mdi/js'
|
|
4
|
+
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
5
|
+
import AiDialog from '@tiptapify/extensions/components/ai/Dialog.vue'
|
|
6
|
+
import defaults from '@tiptapify/constants/defaults'
|
|
7
|
+
import { TiptapifyAiResolvedOptions, variantBtnTypes } from '@tiptapify/types/editor'
|
|
8
|
+
import { computed, ComputedRef, inject, PropType, useTemplateRef } from 'vue'
|
|
9
|
+
import { ComposerTranslation } from 'vue-i18n'
|
|
10
|
+
|
|
11
|
+
defineProps({
|
|
12
|
+
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
16
|
+
const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
|
|
17
|
+
const dialog = useTemplateRef('dialog')
|
|
18
|
+
|
|
19
|
+
const isAvailable = computed(() => Boolean(ai?.value && ai.value.aiProvider))
|
|
20
|
+
const mdiIcons = mdi as Record<string, string>
|
|
21
|
+
const icon = computed(() => `mdiSvg:${mdiIcons.mdiSparklesOutline ?? mdiIcons.mdiSparkles ?? mdi.mdiCreationOutline}`)
|
|
22
|
+
|
|
23
|
+
function showDialog() {
|
|
24
|
+
if (!isAvailable.value) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
dialog.value?.showDialog()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<VBtn color="" :variant="variantBtn" size="32" :disabled="!isAvailable" @click="showDialog">
|
|
35
|
+
<VTooltip activator="parent">
|
|
36
|
+
{{ isAvailable ? t('ai.title') : t('ai.unavailable') }}
|
|
37
|
+
</VTooltip>
|
|
38
|
+
<BtnIcon :icon="icon" />
|
|
39
|
+
</VBtn>
|
|
40
|
+
|
|
41
|
+
<AiDialog ref="dialog" />
|
|
42
|
+
</template>
|