@blockslides/vue-3 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.
@@ -0,0 +1,84 @@
1
+ import type { FloatingMenuPluginProps } from '@blockslides/extension-floating-menu'
2
+ import { FloatingMenuPlugin } from '@blockslides/extension-floating-menu'
3
+ import type { PropType } from 'vue'
4
+ import { defineComponent, h, onBeforeUnmount, onMounted, ref } from 'vue'
5
+
6
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
7
+
8
+ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element'>
9
+
10
+ export const FloatingMenu = defineComponent({
11
+ name: 'FloatingMenu',
12
+
13
+ inheritAttrs: false,
14
+
15
+ props: {
16
+ pluginKey: {
17
+ // TODO: TypeScript breaks
18
+ // type: [String, Object as PropType<Exclude<FloatingMenuPluginProps['pluginKey'], string>>],
19
+ type: null,
20
+ default: 'floatingMenu',
21
+ },
22
+
23
+ editor: {
24
+ type: Object as PropType<FloatingMenuPluginProps['editor']>,
25
+ required: true,
26
+ },
27
+
28
+ options: {
29
+ type: Object as PropType<FloatingMenuPluginProps['options']>,
30
+ default: () => ({}),
31
+ },
32
+
33
+ appendTo: {
34
+ type: [Object, Function] as PropType<FloatingMenuPluginProps['appendTo']>,
35
+ default: undefined,
36
+ },
37
+
38
+ shouldShow: {
39
+ type: Function as PropType<Exclude<Required<FloatingMenuPluginProps>['shouldShow'], null>>,
40
+ default: null,
41
+ },
42
+ },
43
+
44
+ setup(props, { slots, attrs }) {
45
+ const root = ref<HTMLElement | null>(null)
46
+
47
+ onMounted(() => {
48
+ const { pluginKey, editor, options, appendTo, shouldShow } = props
49
+
50
+ const el = root.value
51
+
52
+ if (!el) {
53
+ return
54
+ }
55
+
56
+ el.style.visibility = 'hidden'
57
+ el.style.position = 'absolute'
58
+
59
+ // Remove element from DOM; plugin will re-parent it when shown
60
+ el.remove()
61
+
62
+ editor.registerPlugin(
63
+ FloatingMenuPlugin({
64
+ pluginKey,
65
+ editor,
66
+ element: el,
67
+ options,
68
+ appendTo,
69
+ shouldShow,
70
+ }),
71
+ )
72
+ })
73
+
74
+ onBeforeUnmount(() => {
75
+ const { pluginKey, editor } = props
76
+
77
+ editor.unregisterPlugin(pluginKey)
78
+ })
79
+
80
+ // Vue owns this element; attrs are applied reactively by Vue
81
+ // Plugin re-parents it when showing the menu
82
+ return () => h('div', { ref: root, ...attrs }, slots.default?.())
83
+ },
84
+ }) as any
@@ -0,0 +1,3 @@
1
+ export * from './BubbleMenu.js'
2
+ export * from './FloatingMenu.js'
3
+ export { default as BubbleMenuPreset } from './BubbleMenuPreset.vue'
@@ -0,0 +1,24 @@
1
+ import type { EditorOptions } from '@blockslides/core'
2
+ import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
3
+
4
+ import { Editor } from './Editor.js'
5
+
6
+ export const useEditor = (options: Partial<EditorOptions> = {}) => {
7
+ const editor = shallowRef<Editor>()
8
+
9
+ onMounted(() => {
10
+ editor.value = new Editor(options)
11
+ })
12
+
13
+ onBeforeUnmount(() => {
14
+ // Cloning root node (and its children) to avoid content being lost by destroy
15
+ const nodes = editor.value?.view.dom?.parentNode
16
+ const newEl = nodes?.cloneNode(true) as HTMLElement
17
+
18
+ nodes?.parentNode?.replaceChild(newEl, nodes)
19
+
20
+ editor.value?.destroy()
21
+ })
22
+
23
+ return editor
24
+ }
@@ -0,0 +1,255 @@
1
+ import { computed, watch, toRefs } from 'vue'
2
+ import { templatesV1 } from '@blockslides/ai-context'
3
+ import type { AnyExtension, Editor, JSONContent } from '@blockslides/core'
4
+ import type { SlideOptions } from '@blockslides/extension-slide'
5
+ import { ExtensionKit, type ExtensionKitOptions } from '@blockslides/extension-kit'
6
+
7
+ import { useEditor } from './useEditor.js'
8
+ import type { EditorOptions } from '@blockslides/core'
9
+
10
+ type PresetTemplates = ReturnType<typeof templatesV1.listPresetTemplates>
11
+
12
+ export interface UseSlideEditorProps {
13
+ /**
14
+ * Initial content for the editor. If omitted, a single preset slide is used.
15
+ */
16
+ content?: EditorOptions['content']
17
+ /**
18
+ * Called on every update with the current JSON document.
19
+ */
20
+ onChange?: (doc: JSONContent, editor: Editor) => void
21
+ /**
22
+ * Additional extensions to append after the ExtensionKit bundle.
23
+ */
24
+ extensions?: AnyExtension[]
25
+ /**
26
+ * Customize or disable pieces of ExtensionKit (e.g., bubbleMenu: false).
27
+ */
28
+ extensionKitOptions?: ExtensionKitOptions
29
+ /**
30
+ * Optional preset list to power the add-slide button.
31
+ */
32
+ presetTemplates?: PresetTemplates
33
+ /**
34
+ * Called once when an editor instance is ready.
35
+ */
36
+ onEditorReady?: (editor: Editor) => void
37
+ /**
38
+ * Editor theme for UI styling
39
+ */
40
+ theme?: EditorOptions['theme']
41
+ /**
42
+ * The editor's props
43
+ */
44
+ editorProps?: EditorOptions['editorProps']
45
+ /**
46
+ * Called on every update
47
+ */
48
+ onUpdate?: EditorOptions['onUpdate']
49
+ /**
50
+ * Additional editor options to pass through to the core editor.
51
+ * This allows passing any EditorOptions without Vue auto-initializing them.
52
+ */
53
+ editorOptions?: Partial<EditorOptions>
54
+ }
55
+
56
+ const defaultSlide = () => ({
57
+ /**
58
+ * Placeholder slide if no content is provided.
59
+ */
60
+ type: 'doc',
61
+ content: [
62
+ {
63
+ type: 'slide',
64
+ attrs: {
65
+ size: '16x9',
66
+ className: '',
67
+ id: 'slide-1',
68
+ backgroundMode: 'none',
69
+ backgroundColor: null,
70
+ backgroundImage: null,
71
+ backgroundOverlayColor: null,
72
+ backgroundOverlayOpacity: null,
73
+ },
74
+ content: [
75
+ {
76
+ type: 'column',
77
+ attrs: {
78
+ align: 'center',
79
+ padding: 'lg',
80
+ margin: null,
81
+ gap: 'md',
82
+ backgroundColor: '#ffffff',
83
+ backgroundImage: null,
84
+ borderRadius: null,
85
+ border: null,
86
+ fill: true,
87
+ width: null,
88
+ height: null,
89
+ justify: 'center',
90
+ },
91
+ content: [
92
+ {
93
+ type: 'heading',
94
+ attrs: {
95
+ align: null,
96
+ padding: null,
97
+ margin: null,
98
+ gap: null,
99
+ backgroundColor: null,
100
+ backgroundImage: null,
101
+ borderRadius: null,
102
+ border: null,
103
+ fill: null,
104
+ width: null,
105
+ height: null,
106
+ justify: null,
107
+ id: '1fc4664c-333d-4203-a3f1-3ad27a54c535',
108
+ 'data-toc-id': '1fc4664c-333d-4203-a3f1-3ad27a54c535',
109
+ level: 1,
110
+ },
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: 'Lorem ipsum dolor sit amet',
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ type: 'paragraph',
120
+ attrs: {
121
+ align: null,
122
+ padding: null,
123
+ margin: null,
124
+ gap: null,
125
+ backgroundColor: null,
126
+ backgroundImage: null,
127
+ borderRadius: null,
128
+ border: null,
129
+ fill: null,
130
+ width: null,
131
+ height: null,
132
+ justify: null,
133
+ },
134
+ content: [
135
+ {
136
+ type: 'text',
137
+ text: 'Consectetur adipiscing elit. Sed do eiusmod tempor incididunt. ',
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ },
143
+ ],
144
+ },
145
+ ],
146
+ })
147
+
148
+ const defaultAddSlideButton = (presets: PresetTemplates) => ({
149
+ showPresets: true,
150
+ presets,
151
+ presetBackground: '#0f172a',
152
+ presetForeground: '#e5e7eb',
153
+ })
154
+
155
+ const defaultSlideOptions: Partial<SlideOptions> = {
156
+ renderMode: 'dynamic',
157
+ hoverOutline: { color: '#3b82f6', width: '1.5px', offset: '4px' },
158
+ hoverOutlineCascade: false,
159
+ }
160
+
161
+ export const useSlideEditor = (props: UseSlideEditorProps = {}) => {
162
+ const {
163
+ content,
164
+ onChange,
165
+ extensions,
166
+ extensionKitOptions,
167
+ presetTemplates,
168
+ onEditorReady,
169
+ theme = 'light',
170
+ editorProps,
171
+ onUpdate,
172
+ editorOptions = {}
173
+ } = props
174
+ /**
175
+ * Presets for add slide button.
176
+ */
177
+ const presets = computed<PresetTemplates>(
178
+ () => presetTemplates ?? templatesV1.listPresetTemplates()
179
+ )
180
+
181
+ const mergedExtensionKitOptions = computed<ExtensionKitOptions>(() => {
182
+ const addSlideButton =
183
+ extensionKitOptions?.addSlideButton === false
184
+ ? false
185
+ : {
186
+ ...defaultAddSlideButton(presets.value),
187
+ ...(extensionKitOptions?.addSlideButton ?? {}),
188
+ }
189
+
190
+ const slide =
191
+ extensionKitOptions?.slide === false
192
+ ? false
193
+ : {
194
+ ...defaultSlideOptions,
195
+ ...(extensionKitOptions?.slide ?? {}),
196
+ }
197
+
198
+ return {
199
+ ...extensionKitOptions,
200
+ addSlideButton,
201
+ slide,
202
+ }
203
+ })
204
+
205
+ const resolvedExtensions = computed<AnyExtension[]>(() => {
206
+ const kit = ExtensionKit.configure(mergedExtensionKitOptions.value)
207
+ return extensions ? [kit, ...extensions] : [kit]
208
+ })
209
+
210
+ /**
211
+ * Initial content for the editor.
212
+ */
213
+ const initialContent = content ?? defaultSlide()
214
+
215
+ console.log('resolvedExtensions', resolvedExtensions.value, resolvedExtensions);
216
+ console.log('editorOptions (as single prop):', editorOptions);
217
+
218
+ const editor = useEditor(
219
+ {
220
+ content: initialContent,
221
+ extensions: resolvedExtensions.value,
222
+ theme,
223
+ editorProps: {
224
+ attributes: {
225
+ autocomplete: 'off',
226
+ autocorrect: 'off',
227
+ autocapitalize: 'off',
228
+ class: 'min-h-full min-w-full',
229
+ ...(editorProps?.attributes ?? {}),
230
+ },
231
+ ...editorProps,
232
+ },
233
+ ...editorOptions,
234
+ onUpdate: (ctx: any) => {
235
+ const json = ctx.editor.getJSON()
236
+ onChange?.(json, ctx.editor)
237
+ onUpdate?.(ctx)
238
+ },
239
+ }
240
+ );
241
+
242
+ watch(
243
+ editor,
244
+ newEditor => {
245
+ if (newEditor && !newEditor.isDestroyed) {
246
+ onEditorReady?.(newEditor)
247
+ } else {
248
+ console.log('[useSlideEditor] ❌ Editor not ready or destroyed')
249
+ }
250
+ },
251
+ { immediate: true }
252
+ )
253
+
254
+ return { editor, presets }
255
+ }
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<{}, {}, any>
4
+ export default component
5
+ }