@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,6 @@
1
+ import { BubbleMenuPluginProps } from '../../../extension-bubble-menu/src';
2
+
3
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
4
+ export type BubbleMenuProps = Omit<Optional<BubbleMenuPluginProps, 'pluginKey'>, 'element'>;
5
+ export declare const BubbleMenu: any;
6
+ export {};
@@ -0,0 +1,6 @@
1
+ import { FloatingMenuPluginProps } from '../../../extension-floating-menu/src';
2
+
3
+ type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
4
+ export type FloatingMenuProps = Omit<Optional<FloatingMenuPluginProps, 'pluginKey'>, 'element'>;
5
+ export declare const FloatingMenu: any;
6
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './BubbleMenu.js';
2
+ export * from './FloatingMenu.js';
3
+ export { default as BubbleMenuPreset } from './BubbleMenuPreset.vue';
package/dist/menus.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./FloatingMenu-By8Qi7tW.cjs");exports.BubbleMenu=e.BubbleMenu;exports.BubbleMenuPreset=e._sfc_main;exports.FloatingMenu=e.FloatingMenu;
@@ -0,0 +1 @@
1
+ export * from './menus/index'
package/dist/menus.js ADDED
@@ -0,0 +1,6 @@
1
+ import { B as a, _ as b, F as n } from "./FloatingMenu-BKkixozS.js";
2
+ export {
3
+ a as BubbleMenu,
4
+ b as BubbleMenuPreset,
5
+ n as FloatingMenu
6
+ };
@@ -0,0 +1,4 @@
1
+ import { EditorOptions } from '../../core/src';
2
+ import { Editor } from './Editor.js';
3
+
4
+ export declare const useEditor: (options?: Partial<EditorOptions>) => import('vue').ShallowRef<Editor | undefined, Editor | undefined>;
@@ -0,0 +1,36 @@
1
+ import { templatesV1 } from '../../ai-context/src';
2
+ import { AnyExtension, Editor, JSONContent, EditorOptions } from '../../core/src';
3
+ import { ExtensionKitOptions } from '../../extension-kit/src';
4
+
5
+ type PresetTemplates = ReturnType<typeof templatesV1.listPresetTemplates>;
6
+ export interface UseSlideEditorProps extends Omit<Partial<EditorOptions>, 'extensions'> {
7
+ /**
8
+ * Initial content for the editor. If omitted, a single preset slide is used.
9
+ */
10
+ content?: EditorOptions['content'];
11
+ /**
12
+ * Called on every update with the current JSON document.
13
+ */
14
+ onChange?: (doc: JSONContent, editor: Editor) => void;
15
+ /**
16
+ * Additional extensions to append after the ExtensionKit bundle.
17
+ */
18
+ extensions?: AnyExtension[];
19
+ /**
20
+ * Customize or disable pieces of ExtensionKit (e.g., bubbleMenu: false).
21
+ */
22
+ extensionKitOptions?: ExtensionKitOptions;
23
+ /**
24
+ * Optional preset list to power the add-slide button.
25
+ */
26
+ presetTemplates?: PresetTemplates;
27
+ /**
28
+ * Called once when an editor instance is ready.
29
+ */
30
+ onEditorReady?: (editor: Editor) => void;
31
+ }
32
+ export declare const useSlideEditor: (props?: UseSlideEditorProps) => {
33
+ editor: import('vue').ShallowRef<import('./Editor.js').Editor | undefined, import('./Editor.js').Editor | undefined>;
34
+ presets: import('vue').ComputedRef<templatesV1.PresetTemplate[]>;
35
+ };
36
+ export {};
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@blockslides/vue-3",
3
+ "description": "Vue 3 components for blockslides",
4
+ "version": "0.2.0",
5
+ "homepage": "https://github.com/keivanmojmali/blockslides",
6
+ "keywords": [
7
+ "blockslides",
8
+ "blockslides vue components",
9
+ "editor",
10
+ "wysiwyg"
11
+ ],
12
+ "license": "MIT",
13
+ "exports": {
14
+ ".": {
15
+ "types": {
16
+ "import": "./dist/index.d.ts",
17
+ "require": "./dist/index.d.cts"
18
+ },
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ },
22
+ "./menus": {
23
+ "types": {
24
+ "import": "./dist/menus/index.d.ts",
25
+ "require": "./dist/menus/index.d.cts"
26
+ },
27
+ "import": "./dist/menus/index.js",
28
+ "require": "./dist/menus/index.cjs"
29
+ }
30
+ },
31
+ "main": "dist/index.cjs",
32
+ "module": "dist/index.js",
33
+ "types": "dist/index.d.ts",
34
+ "type": "module",
35
+ "files": [
36
+ "src",
37
+ "dist"
38
+ ],
39
+ "devDependencies": {
40
+ "@floating-ui/dom": "^1.0.0",
41
+ "@vitejs/plugin-vue": "^5.0.0",
42
+ "vite": "^5.0.0",
43
+ "vite-plugin-dts": "^3.0.0",
44
+ "vue": "^3.5.13",
45
+ "@blockslides/core": "^0.3.2",
46
+ "@blockslides/pm": "^0.1.1"
47
+ },
48
+ "optionalDependencies": {
49
+ "@blockslides/extension-bubble-menu": "^0.1.1",
50
+ "@blockslides/extension-floating-menu": "^0.1.1"
51
+ },
52
+ "peerDependencies": {
53
+ "@floating-ui/dom": "^1.0.0",
54
+ "vue": "^3.0.0",
55
+ "@blockslides/core": "^0.3.2",
56
+ "@blockslides/pm": "^0.1.1"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/keivanmojmali/blockslides",
61
+ "directory": "packages/vue-3"
62
+ },
63
+ "sideEffects": false,
64
+ "author": "keivanmojmali",
65
+ "scripts": {
66
+ "build": "vite build",
67
+ "lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
68
+ }
69
+ }
package/src/Editor.ts ADDED
@@ -0,0 +1,93 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import type { EditorOptions, Storage } from '@blockslides/core'
3
+ import { Editor as CoreEditor } from '@blockslides/core'
4
+ import type { EditorState, Plugin, PluginKey } from '@blockslides/pm/state'
5
+ import type { AppContext, ComponentInternalInstance, ComponentPublicInstance, Ref } from 'vue'
6
+ import { customRef, markRaw } from 'vue'
7
+
8
+ function useDebouncedRef<T>(value: T) {
9
+ return customRef<T>((track, trigger) => {
10
+ return {
11
+ get() {
12
+ track()
13
+ return value
14
+ },
15
+ set(newValue) {
16
+ // update state
17
+ value = newValue
18
+
19
+ // update view as soon as possible
20
+ requestAnimationFrame(() => {
21
+ requestAnimationFrame(() => {
22
+ trigger()
23
+ })
24
+ })
25
+ },
26
+ }
27
+ })
28
+ }
29
+
30
+ export type ContentComponent = ComponentInternalInstance & {
31
+ ctx: ComponentPublicInstance
32
+ }
33
+
34
+ export class Editor extends CoreEditor {
35
+ private reactiveState: Ref<EditorState>
36
+
37
+ private reactiveExtensionStorage: Ref<Storage>
38
+
39
+ public contentComponent: ContentComponent | null = null
40
+
41
+ public appContext: AppContext | null = null
42
+
43
+ constructor(options: Partial<EditorOptions> = {}) {
44
+ super(options)
45
+
46
+ this.reactiveState = useDebouncedRef(this.view.state)
47
+ this.reactiveExtensionStorage = useDebouncedRef(this.extensionStorage)
48
+
49
+ this.on('beforeTransaction', ({ nextState }) => {
50
+ this.reactiveState.value = nextState
51
+ this.reactiveExtensionStorage.value = this.extensionStorage
52
+ })
53
+
54
+ return markRaw(this) // eslint-disable-line
55
+ }
56
+
57
+ get state() {
58
+ return this.reactiveState ? this.reactiveState.value : this.view.state
59
+ }
60
+
61
+ get storage() {
62
+ return this.reactiveExtensionStorage ? this.reactiveExtensionStorage.value : super.storage
63
+ }
64
+
65
+ /**
66
+ * Register a ProseMirror plugin.
67
+ */
68
+ public registerPlugin(
69
+ plugin: Plugin,
70
+ handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[],
71
+ ): EditorState {
72
+ const nextState = super.registerPlugin(plugin, handlePlugins)
73
+
74
+ if (this.reactiveState) {
75
+ this.reactiveState.value = nextState
76
+ }
77
+
78
+ return nextState
79
+ }
80
+
81
+ /**
82
+ * Unregister a ProseMirror plugin.
83
+ */
84
+ public unregisterPlugin(nameOrPluginKey: string | PluginKey): EditorState | undefined {
85
+ const nextState = super.unregisterPlugin(nameOrPluginKey)
86
+
87
+ if (this.reactiveState && nextState) {
88
+ this.reactiveState.value = nextState
89
+ }
90
+
91
+ return nextState
92
+ }
93
+ }
@@ -0,0 +1,77 @@
1
+ import type { PropType, Ref } from 'vue'
2
+ import { defineComponent, getCurrentInstance, h, nextTick, onBeforeUnmount, ref, unref, watchEffect } from 'vue'
3
+
4
+ import type { Editor } from './Editor.js'
5
+
6
+ export const EditorContent = defineComponent({
7
+ name: 'EditorContent',
8
+
9
+ props: {
10
+ editor: {
11
+ default: null,
12
+ type: Object as PropType<Editor>,
13
+ },
14
+ },
15
+
16
+ setup(props) {
17
+ const rootEl: Ref<Element | undefined> = ref()
18
+ const instance = getCurrentInstance()
19
+
20
+ watchEffect(() => {
21
+ const editor = props.editor
22
+
23
+ if (editor && editor.options.element && rootEl.value) {
24
+ nextTick(() => {
25
+ if (!rootEl.value || !editor.view.dom?.parentNode) {
26
+ return
27
+ }
28
+
29
+ // TODO using the new editor.mount method might allow us to remove this
30
+ const element = unref(rootEl.value)
31
+
32
+ rootEl.value.append(...editor.view.dom.parentNode.childNodes)
33
+
34
+ // @ts-ignore
35
+ editor.contentComponent = instance.ctx._
36
+
37
+ if (instance) {
38
+ editor.appContext = {
39
+ ...instance.appContext,
40
+ // Vue internally uses prototype chain to forward/shadow injects across the entire component chain
41
+ // so don't use object spread operator or 'Object.assign' and just set `provides` as is on editor's appContext
42
+ // @ts-expect-error forward instance's 'provides' into appContext
43
+ provides: instance.provides,
44
+ }
45
+ }
46
+
47
+ editor.setOptions({
48
+ element,
49
+ })
50
+
51
+ editor.createNodeViews()
52
+ })
53
+ }
54
+ })
55
+
56
+ onBeforeUnmount(() => {
57
+ const editor = props.editor
58
+
59
+ if (!editor) {
60
+ return
61
+ }
62
+
63
+ editor.contentComponent = null
64
+ editor.appContext = null
65
+ })
66
+
67
+ return { rootEl }
68
+ },
69
+
70
+ render() {
71
+ return h('div', {
72
+ ref: (el: any) => {
73
+ this.rootEl = el
74
+ },
75
+ })
76
+ },
77
+ })
@@ -0,0 +1,21 @@
1
+ import { defineComponent, h } from 'vue'
2
+
3
+ export const NodeViewContent = defineComponent({
4
+ name: 'NodeViewContent',
5
+
6
+ props: {
7
+ as: {
8
+ type: String,
9
+ default: 'div',
10
+ },
11
+ },
12
+
13
+ render() {
14
+ return h(this.as, {
15
+ style: {
16
+ whiteSpace: 'pre-wrap',
17
+ },
18
+ 'data-node-view-content': '',
19
+ })
20
+ },
21
+ })
@@ -0,0 +1,31 @@
1
+ import { defineComponent, h } from 'vue'
2
+
3
+ export const NodeViewWrapper = defineComponent({
4
+ name: 'NodeViewWrapper',
5
+
6
+ props: {
7
+ as: {
8
+ type: String,
9
+ default: 'div',
10
+ },
11
+ },
12
+
13
+ inject: ['onDragStart', 'decorationClasses'],
14
+
15
+ render() {
16
+ return h(
17
+ this.as,
18
+ {
19
+ // @ts-ignore
20
+ class: this.decorationClasses,
21
+ style: {
22
+ whiteSpace: 'normal',
23
+ },
24
+ 'data-node-view-wrapper': '',
25
+ // @ts-ignore (https://github.com/vuejs/vue-next/issues/3031)
26
+ onDragstart: this.onDragStart,
27
+ },
28
+ this.$slots.default?.(),
29
+ )
30
+ },
31
+ })
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { computed, watch, onMounted } from 'vue'
3
+ import { EditorContent } from './EditorContent'
4
+ import BubbleMenuPreset from './menus/BubbleMenuPreset.vue'
5
+ import { useSlideEditor, type UseSlideEditorProps } from './useSlideEditor'
6
+ import type { Editor } from '@blockslides/core'
7
+
8
+ export interface BubbleMenuPresetProps {
9
+ editor: Editor
10
+ [key: string]: any
11
+ }
12
+
13
+ export interface SlideEditorProps extends UseSlideEditorProps {
14
+ /**
15
+ * Toggle or customize the built-in BubbleMenuPreset.
16
+ * - true (default): render with defaults
17
+ * - false: disable entirely
18
+ * - object: pass through to BubbleMenuPreset
19
+ */
20
+ bubbleMenuPreset?: boolean | BubbleMenuPresetProps
21
+ className?: string
22
+ style?: any
23
+ }
24
+
25
+ const props = withDefaults(defineProps<SlideEditorProps>(), {
26
+ bubbleMenuPreset: true,
27
+ })
28
+
29
+ const {
30
+ bubbleMenuPreset,
31
+ className,
32
+ style,
33
+ ...hookProps
34
+ } = props
35
+
36
+ const { editor } = useSlideEditor(hookProps)
37
+
38
+ const bubbleMenuProps = computed(() => {
39
+ if (props.bubbleMenuPreset === false) return null
40
+ if (props.bubbleMenuPreset === true) return {}
41
+ return props.bubbleMenuPreset
42
+ })
43
+
44
+ </script>
45
+
46
+ <template>
47
+ <div v-if="editor" :class="className" :style="style">
48
+ <div class="bs-viewport">
49
+ <EditorContent :editor="editor" />
50
+ <BubbleMenuPreset v-if="bubbleMenuProps" :editor="editor" v-bind="bubbleMenuProps" />
51
+ </div>
52
+ </div>
53
+ </template>
54
+
@@ -0,0 +1,130 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ import type { MarkViewProps, MarkViewRenderer, MarkViewRendererOptions } from '@blockslides/core'
3
+ import { MarkView } from '@blockslides/core'
4
+ import type { Component, PropType } from 'vue'
5
+ import { defineComponent, h, toRaw } from 'vue'
6
+
7
+ import type { Editor } from './Editor.js'
8
+ import { VueRenderer } from './VueRenderer.js'
9
+
10
+ export interface VueMarkViewRendererOptions extends MarkViewRendererOptions {
11
+ as?: string
12
+ className?: string
13
+ attrs?: { [key: string]: string }
14
+ }
15
+
16
+ export const markViewProps = {
17
+ editor: {
18
+ type: Object as PropType<MarkViewProps['editor']>,
19
+ required: true as const,
20
+ },
21
+ mark: {
22
+ type: Object as PropType<MarkViewProps['mark']>,
23
+ required: true as const,
24
+ },
25
+ extension: {
26
+ type: Object as PropType<MarkViewProps['extension']>,
27
+ required: true as const,
28
+ },
29
+ inline: {
30
+ type: Boolean as PropType<MarkViewProps['inline']>,
31
+ required: true as const,
32
+ },
33
+ view: {
34
+ type: Object as PropType<MarkViewProps['view']>,
35
+ required: true as const,
36
+ },
37
+ updateAttributes: {
38
+ type: Function as PropType<MarkViewProps['updateAttributes']>,
39
+ required: true as const,
40
+ },
41
+ HTMLAttributes: {
42
+ type: Object as PropType<MarkViewProps['HTMLAttributes']>,
43
+ required: true as const,
44
+ },
45
+ }
46
+
47
+ export const MarkViewContent = defineComponent({
48
+ name: 'MarkViewContent',
49
+
50
+ props: {
51
+ as: {
52
+ type: String,
53
+ default: 'span',
54
+ },
55
+ },
56
+
57
+ render() {
58
+ return h(this.as, {
59
+ style: {
60
+ whiteSpace: 'inherit',
61
+ },
62
+ 'data-mark-view-content': '',
63
+ })
64
+ },
65
+ })
66
+
67
+ export class VueMarkView extends MarkView<Component, VueMarkViewRendererOptions> {
68
+ renderer: VueRenderer
69
+
70
+ constructor(component: Component, props: MarkViewProps, options?: Partial<VueMarkViewRendererOptions>) {
71
+ super(component, props, options)
72
+
73
+ const componentProps = { ...props, updateAttributes: this.updateAttributes.bind(this) } satisfies MarkViewProps
74
+
75
+ // Create extended component with provide
76
+ const extendedComponent = defineComponent({
77
+ extends: { ...component },
78
+ props: Object.keys(componentProps),
79
+ template: (this.component as any).template,
80
+ setup: reactiveProps => {
81
+ return (component as any).setup?.(reactiveProps, {
82
+ expose: () => undefined,
83
+ })
84
+ },
85
+ // Add support for scoped styles
86
+ __scopeId: (component as any).__scopeId,
87
+ __cssModules: (component as any).__cssModules,
88
+ __name: (component as any).__name,
89
+ __file: (component as any).__file,
90
+ })
91
+ this.renderer = new VueRenderer(extendedComponent, {
92
+ editor: this.editor,
93
+ props: componentProps,
94
+ })
95
+ }
96
+
97
+ get dom() {
98
+ return this.renderer.element as HTMLElement
99
+ }
100
+
101
+ get contentDOM() {
102
+ return this.dom.querySelector('[data-mark-view-content]') as HTMLElement | null
103
+ }
104
+
105
+ updateAttributes(attrs: Record<string, any>): void {
106
+ // since this.mark is now an proxy, we need to get the actual mark from it
107
+ const unproxiedMark = toRaw(this.mark)
108
+ super.updateAttributes(attrs, unproxiedMark)
109
+ }
110
+
111
+ destroy() {
112
+ this.renderer.destroy()
113
+ }
114
+ }
115
+
116
+ export function VueMarkViewRenderer(
117
+ component: Component,
118
+ options: Partial<VueMarkViewRendererOptions> = {},
119
+ ): MarkViewRenderer {
120
+ return props => {
121
+ // try to get the parent component
122
+ // this is important for vue devtools to show the component hierarchy correctly
123
+ // maybe it's `undefined` because <editor-content> isn't rendered yet
124
+ if (!(props.editor as Editor).contentComponent) {
125
+ return {} as unknown as MarkView<any, any>
126
+ }
127
+
128
+ return new VueMarkView(component, props, options)
129
+ }
130
+ }