tiptapify 0.2.5 → 0.3.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 +2 -2
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +23328 -25129
- package/dist/tiptapify.umd.js +70 -72
- package/index.d.ts +19 -0
- package/package.json +1 -1
- package/src/components/Footer.vue +1 -1
- package/src/components/Tiptapify.vue +31 -10
- package/src/components/Toolbar/Index.vue +3 -4
- package/src/components/Toolbar/Items.vue +3 -3
- package/src/components/UI/HotkeyTooltip.test.ts +65 -0
- package/src/components/UI/HotkeyTooltip.vue +54 -0
- package/src/components/UI/Tooltip.vue +20 -0
- package/src/composables/useTiptapifyConfig.test.ts +66 -0
- package/src/composables/useTiptapifyConfig.ts +17 -0
- package/src/constants/defaults.ts +4 -2
- package/src/extensions/components/actions/redo/Button.vue +5 -9
- package/src/extensions/components/actions/undo/Button.vue +5 -9
- package/src/extensions/components/ai/Button.vue +7 -10
- package/src/extensions/components/ai/Dialog.vue +9 -13
- package/src/extensions/components/ai/backend.test.ts +314 -0
- package/src/extensions/components/ai/backend.ts +303 -0
- package/src/extensions/components/alignment/center/Button.vue +5 -9
- package/src/extensions/components/alignment/justify/Button.vue +5 -9
- package/src/extensions/components/alignment/left/Button.vue +5 -9
- package/src/extensions/components/alignment/right/Button.vue +5 -9
- package/src/extensions/components/format/bold/Button.vue +5 -9
- package/src/extensions/components/format/italic/Button.vue +5 -9
- package/src/extensions/components/format/strike/Button.vue +5 -9
- package/src/extensions/components/format/underline/Button.vue +5 -9
- package/src/extensions/components/formatExtra/code/Button.vue +5 -9
- package/src/extensions/components/formatExtra/codeBlock/Button.vue +5 -9
- package/src/extensions/components/formatExtra/quote/Button.vue +5 -9
- package/src/extensions/components/formatExtra/sub/Button.vue +5 -9
- package/src/extensions/components/formatExtra/sup/Button.vue +5 -9
- package/src/extensions/components/list/bullet/Button.vue +8 -13
- package/src/extensions/components/list/indent/Button.vue +5 -9
- package/src/extensions/components/list/numbered/Button.vue +5 -9
- package/src/extensions/components/list/outdent/Button.vue +5 -9
- package/src/extensions/components/list/task/Button.vue +5 -9
- package/src/extensions/components/media/charmap/Button.vue +5 -8
- package/src/extensions/components/media/charmap/Picker.vue +2 -3
- package/src/extensions/components/media/emoji/Button.vue +6 -9
- package/src/extensions/components/media/emoji/Picker.vue +2 -3
- package/src/extensions/components/media/iframe/Button.vue +6 -10
- package/src/extensions/components/media/iframe/IframeDialog.vue +3 -7
- package/src/extensions/components/media/image/Button.vue +6 -10
- package/src/extensions/components/media/image/ImageDialog.vue +5 -10
- package/src/extensions/components/media/link/Button.vue +7 -10
- package/src/extensions/components/media/link/LinkDialog.vue +3 -7
- package/src/extensions/components/media/table/Button.vue +10 -24
- package/src/extensions/components/media/table/ColumnActions.vue +4 -9
- package/src/extensions/components/media/table/RowActions.vue +4 -9
- package/src/extensions/components/media/video/Button.vue +6 -10
- package/src/extensions/components/media/video/VideoDialog.vue +3 -7
- package/src/extensions/components/misc/break/Button.vue +5 -9
- package/src/extensions/components/misc/formatClear/Button.vue +5 -9
- package/src/extensions/components/misc/fullscreen/Button.vue +5 -9
- package/src/extensions/components/misc/invisibleChar/Button.vue +5 -9
- package/src/extensions/components/misc/line/Button.vue +5 -9
- package/src/extensions/components/misc/preview/Button.vue +5 -9
- package/src/extensions/components/misc/source/Button.vue +6 -10
- package/src/extensions/components/misc/source/ShowSourceDialog.vue +4 -5
- package/src/extensions/components/style/color/Button.vue +5 -9
- package/src/extensions/components/style/fontFamily/Button.vue +5 -9
- package/src/extensions/components/style/fontSize/Button.vue +6 -7
- package/src/extensions/components/style/heading/Button.vue +6 -8
- package/src/extensions/components/style/highlight/Button.vue +5 -9
- package/src/extensions/components/style/lineHeight/Button.vue +5 -9
- package/src/index.ts +7 -0
- package/src/types/editor.ts +18 -1
- package/src/utils/hotkeys.test.ts +146 -0
- package/src/utils/hotkeys.ts +65 -0
package/index.d.ts
CHANGED
|
@@ -135,6 +135,23 @@ export type TiptapifyAiProvider = (
|
|
|
135
135
|
|
|
136
136
|
export type TiptapifyAiTokenProvider = () => Promise<string | null> | string | null
|
|
137
137
|
|
|
138
|
+
export interface TiptapifyAiBackendRequest {
|
|
139
|
+
prompt: string
|
|
140
|
+
instruction: string
|
|
141
|
+
stream?: boolean
|
|
142
|
+
thinking?: boolean
|
|
143
|
+
reasoning_effort?: TiptapifyAiReasoningEffort
|
|
144
|
+
model?: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface TiptapifyAiBackendProviderOptions {
|
|
148
|
+
endpoint: string
|
|
149
|
+
headers?: Record<string, string>
|
|
150
|
+
tokenProvider?: TiptapifyAiTokenProvider
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export declare function createAiBackendProvider(options: TiptapifyAiBackendProviderOptions): TiptapifyAiProvider
|
|
154
|
+
|
|
138
155
|
export interface TiptapifyAiStorage {
|
|
139
156
|
getItem: (key: string) => string | null | Promise<string | null>
|
|
140
157
|
setItem: (key: string, value: string) => void | Promise<void>
|
|
@@ -143,6 +160,8 @@ export interface TiptapifyAiStorage {
|
|
|
143
160
|
|
|
144
161
|
export interface TiptapifyAiOptions {
|
|
145
162
|
aiProvider?: TiptapifyAiProvider
|
|
163
|
+
aiEndpoint?: string
|
|
164
|
+
aiHeaders?: Record<string, string>
|
|
146
165
|
model?: string
|
|
147
166
|
promptExamples?: TiptapifyAiPromptExample[]
|
|
148
167
|
mode?: TiptapifyAiMode
|
package/package.json
CHANGED
|
@@ -3,23 +3,26 @@
|
|
|
3
3
|
import defaults from '@tiptapify/constants/defaults'
|
|
4
4
|
import { itemsPropType, toolbarSections } from '@tiptapify/types/toolbarTypes'
|
|
5
5
|
import { SlashCommandsConfig } from '@tiptapify/types/slashCommandsTypes'
|
|
6
|
-
import { computed, onBeforeUnmount, PropType, provide, ref, ShallowRef } from 'vue'
|
|
6
|
+
import { computed, onBeforeUnmount, PropType, provide, ref, ShallowRef, toRef } 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 { TiptapifyAiOptions, TiptapifyEditor, TiptapifyFooterAlignment, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
|
|
9
|
+
import { TiptapifyAiOptions, TiptapifyAiResolvedOptions, TiptapifyEditor, TiptapifyFooterAlignment, 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
|
|
|
13
13
|
import { useI18n } from 'vue-i18n'
|
|
14
14
|
|
|
15
15
|
import { getTiptapEditor } from '@tiptapify/components/index'
|
|
16
|
+
import { TIPTAPIFY_CONFIG_KEY } from '@tiptapify/composables/useTiptapifyConfig'
|
|
17
|
+
import { createAiBackendProvider } from '@tiptapify/extensions/components/ai/backend'
|
|
16
18
|
|
|
17
19
|
import Footer from '@tiptapify/components/Footer.vue'
|
|
18
20
|
import { useTheme } from 'vuetify/framework'
|
|
19
21
|
|
|
20
22
|
const props = defineProps({
|
|
21
23
|
locale: { type: String, default () { return 'en' } },
|
|
22
|
-
content: { type: [String, Object] as PropType<string | Record<string, never>>,
|
|
24
|
+
content: { type: [String, Object] as PropType<string | Record<string, never>>, default () { return '' } },
|
|
25
|
+
modelValue: { type: [String, Object] as PropType<string | Record<string, never>>, default () { return '' } },
|
|
23
26
|
height: { type: [Number, String], default () { return null } },
|
|
24
27
|
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
25
28
|
variantField: { type: String as PropType<variantFieldTypes>, default() { return defaults.variantField } },
|
|
@@ -48,12 +51,14 @@ const props = defineProps({
|
|
|
48
51
|
loadingHeight: { type: String, default() { return '1px' } },
|
|
49
52
|
})
|
|
50
53
|
|
|
54
|
+
const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'])
|
|
55
|
+
|
|
51
56
|
const loadingProgress = ref(0)
|
|
52
57
|
|
|
53
58
|
const { t } = useI18n()
|
|
54
59
|
|
|
55
60
|
const appTheme = useTheme()
|
|
56
|
-
const currentTheme =
|
|
61
|
+
const currentTheme = computed(() => appTheme.name.value)
|
|
57
62
|
|
|
58
63
|
const defaultAiPromptExamples = computed(() => [
|
|
59
64
|
{
|
|
@@ -81,15 +86,30 @@ const tiptapifyAi = computed(() => {
|
|
|
81
86
|
|
|
82
87
|
const options = props.ai === true ? {} : props.ai
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
const resolved: TiptapifyAiResolvedOptions = {
|
|
85
90
|
...options,
|
|
86
91
|
enabled: true,
|
|
87
92
|
promptExamples: options.promptExamples?.length ? options.promptExamples : defaultAiPromptExamples.value,
|
|
88
93
|
}
|
|
94
|
+
|
|
95
|
+
// Backend generation: when no `aiProvider` is given but an `aiEndpoint`
|
|
96
|
+
// is set, synthesize a provider that posts the request to the endpoint.
|
|
97
|
+
// An explicit `aiProvider` always wins.
|
|
98
|
+
if (!resolved.aiProvider && resolved.aiEndpoint) {
|
|
99
|
+
resolved.aiProvider = createAiBackendProvider({
|
|
100
|
+
endpoint: resolved.aiEndpoint,
|
|
101
|
+
headers: resolved.aiHeaders,
|
|
102
|
+
tokenProvider: resolved.tokenProvider,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return resolved
|
|
89
107
|
})
|
|
90
108
|
|
|
91
109
|
function contentChanged() {
|
|
92
|
-
|
|
110
|
+
const html = editor.value?.getHTML() ?? ''
|
|
111
|
+
emit('update:modelValue', html)
|
|
112
|
+
emit('content-changed', { html, json: editor.value?.getJSON() })
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
function onContainerClick(event: MouseEvent) {
|
|
@@ -101,7 +121,7 @@ function onContainerClick(event: MouseEvent) {
|
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
|
|
104
|
-
props.content,
|
|
124
|
+
props.content || props.modelValue,
|
|
105
125
|
computed(() => props.placeholder || t('content.placeholder')).value,
|
|
106
126
|
props.slashCommands,
|
|
107
127
|
props.customExtensions,
|
|
@@ -117,11 +137,13 @@ const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
|
|
|
117
137
|
},
|
|
118
138
|
)
|
|
119
139
|
|
|
120
|
-
const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'])
|
|
121
|
-
|
|
122
140
|
provide('tiptapifyEditor', editor)
|
|
123
141
|
provide('tiptapifyI18n', { t })
|
|
124
142
|
provide('tiptapifyAi', tiptapifyAi)
|
|
143
|
+
provide(TIPTAPIFY_CONFIG_KEY, {
|
|
144
|
+
variantBtn: toRef(props, 'variantBtn'),
|
|
145
|
+
variantField: toRef(props, 'variantField'),
|
|
146
|
+
})
|
|
125
147
|
|
|
126
148
|
editor.value?.chain().setFontFamily(props.defaultFontFamily).run()
|
|
127
149
|
|
|
@@ -145,7 +167,6 @@ onBeforeUnmount(() => {
|
|
|
145
167
|
<template v-if="toolbar">
|
|
146
168
|
<Toolbar
|
|
147
169
|
v-if="editor"
|
|
148
|
-
:variant-btn="variantBtn"
|
|
149
170
|
:variant-field="variantField"
|
|
150
171
|
:font-measure="fontMeasure"
|
|
151
172
|
:items="items"
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
import { Editor } from '@tiptap/vue-3'
|
|
3
3
|
import Items from '@tiptapify/components/Toolbar/Items.vue'
|
|
4
4
|
import defaults from '@tiptapify/constants/defaults'
|
|
5
|
-
import {
|
|
5
|
+
import { variantFieldTypes } from '@tiptapify/types/editor'
|
|
6
6
|
import { computed, inject, PropType, Ref } from 'vue'
|
|
7
7
|
import { itemsPropType, toolbarSections } from '@tiptapify/types/toolbarTypes'
|
|
8
8
|
|
|
9
9
|
import { default as toolbarItemsGroups, availableItems } from '@tiptapify/components/Toolbar/items'
|
|
10
10
|
|
|
11
11
|
const props = defineProps({
|
|
12
|
-
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
13
12
|
variantField: { type: String as PropType<variantFieldTypes>, default() { return defaults.variantField } },
|
|
14
13
|
items: { type: [Array, Object] as PropType<itemsPropType>, default() { return [] } },
|
|
15
14
|
itemsExclude: { type: Boolean, default() { return false } },
|
|
@@ -178,10 +177,10 @@ Object.keys(props.customExtensions).forEach((key: any) => {
|
|
|
178
177
|
<div v-if="editor" class="tiptapify-toolbar">
|
|
179
178
|
<VToolbar elevation="1" :theme="theme" height="auto" :class="`ps-1 pr-1 rounded-t-${rounded}`">
|
|
180
179
|
<VSlideGroup v-if="toolbarScrollable">
|
|
181
|
-
<Items :items="toolbarItems"
|
|
180
|
+
<Items :items="toolbarItems" />
|
|
182
181
|
</VSlideGroup>
|
|
183
182
|
|
|
184
|
-
<Items v-else :items="toolbarItems"
|
|
183
|
+
<Items v-else :items="toolbarItems" />
|
|
185
184
|
</VToolbar>
|
|
186
185
|
</div>
|
|
187
186
|
</template>
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { variantBtnTypes } from '@tiptapify/types/editor'
|
|
3
|
+
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
5
4
|
import { PropType } from 'vue'
|
|
6
5
|
|
|
7
6
|
import { toolbarSections } from '@tiptapify/types/toolbarTypes'
|
|
8
7
|
|
|
9
8
|
defineProps({
|
|
10
|
-
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
11
9
|
items: { type: Array as PropType<toolbarSections>, default() { return {} } },
|
|
12
10
|
})
|
|
13
11
|
|
|
12
|
+
const { variantBtn } = useTiptapifyConfig()
|
|
13
|
+
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createSSRApp, h } from 'vue'
|
|
3
|
+
import { renderToString } from 'vue/server-renderer'
|
|
4
|
+
import { createVuetify } from 'vuetify'
|
|
5
|
+
import HotkeyTooltip from './HotkeyTooltip.vue'
|
|
6
|
+
|
|
7
|
+
const MAC_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
|
8
|
+
const WIN_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
|
9
|
+
|
|
10
|
+
function mockUserAgent (userAgent: string) {
|
|
11
|
+
Object.defineProperty(window.navigator, 'userAgent', {
|
|
12
|
+
value: userAgent,
|
|
13
|
+
configurable: true,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renderTooltip (label: string, hotkey: string): Promise<string> {
|
|
18
|
+
const app = createSSRApp({
|
|
19
|
+
render: () => h(HotkeyTooltip, { label, hotkey })
|
|
20
|
+
})
|
|
21
|
+
app.use(createVuetify())
|
|
22
|
+
|
|
23
|
+
return renderToString(app)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('HotkeyTooltip', () => {
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.unstubAllGlobals()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('renders label and spelled-out hotkey on two lines on Windows', async () => {
|
|
32
|
+
mockUserAgent(WIN_UA)
|
|
33
|
+
const html = await renderTooltip('Bold', 'Mod-Shift-b')
|
|
34
|
+
|
|
35
|
+
expect(html).toContain('t-hotkey-tooltip__label')
|
|
36
|
+
expect(html).toContain('t-hotkey-tooltip__hotkey')
|
|
37
|
+
expect(html).toContain('Bold')
|
|
38
|
+
expect(html).toContain('Ctrl')
|
|
39
|
+
expect(html).toContain('Shift')
|
|
40
|
+
expect(html).toContain('>B</span>')
|
|
41
|
+
expect(html).not.toContain('mdiSvg:')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('renders the hotkey with icons on macOS without spelled-out names', async () => {
|
|
45
|
+
mockUserAgent(MAC_UA)
|
|
46
|
+
const html = await renderTooltip('Heading', 'Mod-Alt-1..6')
|
|
47
|
+
|
|
48
|
+
expect(html).toContain('t-hotkey-tooltip__label')
|
|
49
|
+
expect(html).toContain('t-hotkey-tooltip__hotkey')
|
|
50
|
+
expect(html).toContain('Heading')
|
|
51
|
+
expect(html).toContain('mdiSvg:')
|
|
52
|
+
expect(html).toContain('1–6')
|
|
53
|
+
expect(html).not.toContain('Ctrl')
|
|
54
|
+
expect(html).not.toContain('>Command<')
|
|
55
|
+
expect(html).not.toContain('>Option<')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('renders Enter as an icon on macOS', async () => {
|
|
59
|
+
mockUserAgent(MAC_UA)
|
|
60
|
+
const html = await renderTooltip('Hard break', 'Mod-Enter')
|
|
61
|
+
|
|
62
|
+
expect(html).toContain('mdiSvg:')
|
|
63
|
+
expect(html).not.toContain('>Enter<')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { hotkeyParts } from '@tiptapify/utils/hotkeys'
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
label: { type: String, required: true },
|
|
8
|
+
hotkey: { type: String, required: true },
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const parts = computed(() => hotkeyParts(props.hotkey))
|
|
12
|
+
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<span class="t-hotkey-tooltip">
|
|
17
|
+
<span class="t-hotkey-tooltip__label">{{ label }}</span>
|
|
18
|
+
<span class="t-hotkey-tooltip__hotkey">
|
|
19
|
+
<template v-for="(part, index) in parts" :key="index">
|
|
20
|
+
<span v-if="index" class="t-hotkey-sep">+</span>
|
|
21
|
+
<VIcon v-if="part.icon" :icon="part.icon" size="12" class="t-hotkey-icon" />
|
|
22
|
+
<span v-else>{{ part.label }}</span>
|
|
23
|
+
</template>
|
|
24
|
+
</span>
|
|
25
|
+
</span>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<style scoped lang="scss">
|
|
29
|
+
.t-hotkey-tooltip {
|
|
30
|
+
align-items: center;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: 3px;
|
|
34
|
+
padding: 4px 0;
|
|
35
|
+
white-space: nowrap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.t-hotkey-tooltip__hotkey {
|
|
39
|
+
align-items: center;
|
|
40
|
+
background: rgba(127, 127, 127, .2);
|
|
41
|
+
background: color-mix(in srgb, currentColor 14%, transparent);
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
font-family: 'JetBrainsMono', ui-monospace, Menlo, Consolas, monospace;
|
|
45
|
+
font-size: 11px;
|
|
46
|
+
gap: 4px;
|
|
47
|
+
letter-spacing: .03em;
|
|
48
|
+
padding: 3px 6px;
|
|
49
|
+
|
|
50
|
+
.t-hotkey-icon {
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
|
|
3
|
+
import HotkeyTooltip from '@tiptapify/components/UI/HotkeyTooltip.vue'
|
|
4
|
+
|
|
5
|
+
defineProps({
|
|
6
|
+
label: { type: String, default: '' },
|
|
7
|
+
hotkey: { type: String, default: '' },
|
|
8
|
+
openDelay: { type: Number, default: 500 },
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<VTooltip activator="parent" :open-delay="openDelay">
|
|
15
|
+
<HotkeyTooltip v-if="hotkey" :label="label" :hotkey="hotkey" />
|
|
16
|
+
<slot v-else>
|
|
17
|
+
{{ label }}
|
|
18
|
+
</slot>
|
|
19
|
+
</VTooltip>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createSSRApp, h, provide, ref } from 'vue'
|
|
3
|
+
import { renderToString } from 'vue/server-renderer'
|
|
4
|
+
import { createVuetify } from 'vuetify'
|
|
5
|
+
import BoldButton from '@tiptapify/extensions/components/format/bold/Button.vue'
|
|
6
|
+
import { TIPTAPIFY_CONFIG_KEY, useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
7
|
+
|
|
8
|
+
const editor = {
|
|
9
|
+
isActive: () => false,
|
|
10
|
+
can: () => ({ chain: () => ({ focus: () => ({ toggleBold: () => ({ run: () => false }) }) }) }),
|
|
11
|
+
commands: { toggleBold: () => {} },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const Consumer = {
|
|
15
|
+
setup () {
|
|
16
|
+
const { variantBtn, variantField } = useTiptapifyConfig()
|
|
17
|
+
return () => h('span', [
|
|
18
|
+
h('span', { id: 'variant-btn' }, variantBtn.value),
|
|
19
|
+
h('span', { id: 'variant-field' }, variantField.value),
|
|
20
|
+
])
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function render (root: () => any, withConfig: boolean) {
|
|
25
|
+
const app = createSSRApp({
|
|
26
|
+
setup () {
|
|
27
|
+
if (withConfig) {
|
|
28
|
+
provide(TIPTAPIFY_CONFIG_KEY, {
|
|
29
|
+
variantBtn: ref('tonal'),
|
|
30
|
+
variantField: ref('outlined'),
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
provide('tiptapifyEditor', ref(editor))
|
|
34
|
+
provide('tiptapifyI18n', { t: (key: string) => key })
|
|
35
|
+
return root
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
app.use(createVuetify())
|
|
39
|
+
|
|
40
|
+
return renderToString(app)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('useTiptapifyConfig', () => {
|
|
44
|
+
it('exposes provided config values', async () => {
|
|
45
|
+
const html = await render(() => h(Consumer), true)
|
|
46
|
+
|
|
47
|
+
expect(html).toContain('<span id="variant-btn">tonal</span>')
|
|
48
|
+
expect(html).toContain('<span id="variant-field">outlined</span>')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('falls back to defaults when no provider is present', async () => {
|
|
52
|
+
const html = await render(() => h(Consumer), false)
|
|
53
|
+
|
|
54
|
+
expect(html).toContain('<span id="variant-btn">flat</span>')
|
|
55
|
+
expect(html).toContain('<span id="variant-field">solo</span>')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('is consumed by toolbar extension buttons', async () => {
|
|
59
|
+
const custom = await render(() => h(BoldButton), true)
|
|
60
|
+
const fallback = await render(() => h(BoldButton), false)
|
|
61
|
+
|
|
62
|
+
expect(custom).toContain('tonal')
|
|
63
|
+
expect(fallback).toContain('flat')
|
|
64
|
+
expect(custom).not.toEqual(fallback)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { computed, inject, InjectionKey, Ref } from 'vue'
|
|
2
|
+
import defaults from '@tiptapify/constants/defaults'
|
|
3
|
+
import { variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
|
|
4
|
+
|
|
5
|
+
export interface TiptapifyConfig {
|
|
6
|
+
variantBtn: Ref<variantBtnTypes>
|
|
7
|
+
variantField: Ref<variantFieldTypes>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const TIPTAPIFY_CONFIG_KEY: InjectionKey<TiptapifyConfig> = Symbol('tiptapifyConfig')
|
|
11
|
+
|
|
12
|
+
export function useTiptapifyConfig(): TiptapifyConfig {
|
|
13
|
+
return inject(TIPTAPIFY_CONFIG_KEY, {
|
|
14
|
+
variantBtn: computed(() => defaults.variantBtn),
|
|
15
|
+
variantField: computed(() => defaults.variantField),
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
import * as mdi from '@mdi/js'
|
|
4
4
|
import { Editor } from '@tiptap/vue-3'
|
|
5
5
|
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
7
|
+
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
8
|
+
import { inject, Ref } from 'vue'
|
|
8
9
|
|
|
9
|
-
import defaults from '@tiptapify/constants/defaults'
|
|
10
10
|
import { ComposerTranslation } from 'vue-i18n'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
14
|
-
})
|
|
12
|
+
const { variantBtn } = useTiptapifyConfig()
|
|
15
13
|
|
|
16
14
|
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
17
15
|
|
|
@@ -27,9 +25,7 @@ const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
|
27
25
|
:variant="variantBtn"
|
|
28
26
|
@click="editor.chain().focus().redo().run()"
|
|
29
27
|
>
|
|
30
|
-
<
|
|
31
|
-
{{ t('action.redo') }}
|
|
32
|
-
</VTooltip>
|
|
28
|
+
<Tooltip :label="t('action.redo')" hotkey="Mod-Shift-z" />
|
|
33
29
|
<BtnIcon :icon="`mdiSvg:${mdi.mdiRedo}`" />
|
|
34
30
|
</VBtn>
|
|
35
31
|
</template>
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
import * as mdi from '@mdi/js'
|
|
4
4
|
import { Editor } from '@tiptap/vue-3'
|
|
5
5
|
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
7
|
+
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
8
|
+
import { inject, Ref } from 'vue'
|
|
8
9
|
|
|
9
|
-
import defaults from '@tiptapify/constants/defaults'
|
|
10
10
|
import { ComposerTranslation } from 'vue-i18n'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
14
|
-
})
|
|
12
|
+
const { variantBtn } = useTiptapifyConfig()
|
|
15
13
|
|
|
16
14
|
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
17
15
|
|
|
@@ -27,9 +25,7 @@ const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
|
27
25
|
:variant="variantBtn"
|
|
28
26
|
@click="editor.chain().focus().undo().run()"
|
|
29
27
|
>
|
|
30
|
-
<
|
|
31
|
-
{{ t('action.undo') }}
|
|
32
|
-
</VTooltip>
|
|
28
|
+
<Tooltip :label="t('action.undo')" hotkey="Mod-z" />
|
|
33
29
|
<BtnIcon :icon="`mdiSvg:${mdi.mdiUndo}`" />
|
|
34
30
|
</VBtn>
|
|
35
31
|
</template>
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import * as mdi from '@mdi/js'
|
|
4
4
|
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
5
|
+
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
5
6
|
import AiDialog from '@tiptapify/extensions/components/ai/Dialog.vue'
|
|
6
|
-
import
|
|
7
|
-
import { TiptapifyAiResolvedOptions
|
|
8
|
-
import { computed, ComputedRef, inject,
|
|
7
|
+
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
8
|
+
import { TiptapifyAiResolvedOptions } from '@tiptapify/types/editor'
|
|
9
|
+
import { computed, ComputedRef, inject, useTemplateRef } from 'vue'
|
|
9
10
|
import { ComposerTranslation } from 'vue-i18n'
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
13
|
-
})
|
|
12
|
+
const { variantBtn } = useTiptapifyConfig()
|
|
14
13
|
|
|
15
14
|
const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
16
15
|
const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
|
|
@@ -32,11 +31,9 @@ function showDialog() {
|
|
|
32
31
|
|
|
33
32
|
<template>
|
|
34
33
|
<VBtn color="" :variant="variantBtn" size="32" :disabled="!isAvailable" @click="showDialog">
|
|
35
|
-
<
|
|
36
|
-
{{ isAvailable ? t('ai.title') : t('ai.unavailable') }}
|
|
37
|
-
</VTooltip>
|
|
34
|
+
<Tooltip :label="isAvailable ? t('ai.title') : t('ai.unavailable')" />
|
|
38
35
|
<BtnIcon :icon="icon" />
|
|
39
36
|
</VBtn>
|
|
40
37
|
|
|
41
|
-
<AiDialog ref="dialog"
|
|
38
|
+
<AiDialog ref="dialog" />
|
|
42
39
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
|
|
3
3
|
import TiptapifyDialog from '@tiptapify/components/UI/TiptapifyDialog.vue'
|
|
4
|
-
import
|
|
4
|
+
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
5
5
|
import {
|
|
6
6
|
TiptapifyAiEditorContext,
|
|
7
7
|
TiptapifyAiMode,
|
|
@@ -11,19 +11,15 @@ import {
|
|
|
11
11
|
TiptapifyAiResponse,
|
|
12
12
|
TiptapifyAiResolvedOptions,
|
|
13
13
|
TiptapifyAiStream,
|
|
14
|
-
TiptapifyEditor
|
|
15
|
-
variantBtnTypes,
|
|
16
|
-
variantFieldTypes
|
|
14
|
+
TiptapifyEditor
|
|
17
15
|
} from '@tiptapify/types/editor'
|
|
18
|
-
import { computed, ComputedRef, inject, nextTick,
|
|
16
|
+
import { computed, ComputedRef, inject, nextTick, Ref, ref, useTemplateRef, watch } from 'vue'
|
|
19
17
|
import * as mdi from '@mdi/js'
|
|
20
18
|
import { ComposerTranslation } from 'vue-i18n'
|
|
21
19
|
import { exceedsLimit } from './charLimit'
|
|
20
|
+
import { DEFAULT_AI_INSTRUCTION } from './backend'
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
variantBtn: { type: String as PropType<variantBtnTypes>, default() { return defaults.variantBtn } },
|
|
25
|
-
variantField: { type: String as PropType<variantFieldTypes>, default() { return defaults.variantField } },
|
|
26
|
-
})
|
|
22
|
+
const { variantBtn, variantField } = useTiptapifyConfig()
|
|
27
23
|
|
|
28
24
|
const editor = inject('tiptapifyEditor') as Ref<TiptapifyEditor>
|
|
29
25
|
const ai = inject('tiptapifyAi') as ComputedRef<TiptapifyAiResolvedOptions | false>
|
|
@@ -180,7 +176,7 @@ function getSelectedText(from: number, to: number) {
|
|
|
180
176
|
function buildDefaultMessages(context: TiptapifyAiEditorContext) {
|
|
181
177
|
const systemPrompt = ai.value && ai.value.systemPrompt
|
|
182
178
|
? ai.value.systemPrompt
|
|
183
|
-
:
|
|
179
|
+
: DEFAULT_AI_INSTRUCTION
|
|
184
180
|
const contextText = context.selectedText || context.text
|
|
185
181
|
const contextLabel = context.selectedText ? 'Selected text' : 'Editor text'
|
|
186
182
|
|
|
@@ -532,19 +528,19 @@ function apply() {
|
|
|
532
528
|
|
|
533
529
|
.ai-reasoning {
|
|
534
530
|
:deep(.v-expansion-panel-title) {
|
|
535
|
-
color: var(--
|
|
531
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
536
532
|
font-size: .8125rem;
|
|
537
533
|
font-weight: 500;
|
|
538
534
|
padding: .625rem .75rem;
|
|
539
535
|
}
|
|
540
536
|
|
|
541
537
|
:deep(.v-expansion-panel-title__icon) {
|
|
542
|
-
color: var(--
|
|
538
|
+
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
|
543
539
|
}
|
|
544
540
|
}
|
|
545
541
|
|
|
546
542
|
.ai-reasoning__text {
|
|
547
|
-
color: var(--
|
|
543
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
548
544
|
font-family: 'JetBrainsMono', monospace;
|
|
549
545
|
font-size: .75rem;
|
|
550
546
|
line-height: 1.4;
|