@live-change/wysiwyg-frontend 0.2.4 → 0.2.6
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/front/src/DocumentEditorTest.vue +3 -3
- package/front/src/EditorWithPreview.vue +3 -3
- package/front/src/{ContentView.js → components/ContentView.js} +1 -1
- package/front/src/components/DocumentEditor.vue +163 -0
- package/front/src/{Editor.vue → components/Editor.vue} +6 -1
- package/front/src/components/EditorMenu.vue +148 -0
- package/front/src/{ImageComponent.vue → components/ImageComponent.vue} +0 -0
- package/front/src/{ImageNode.js → components/ImageNode.js} +0 -0
- package/front/src/{ProseMirrorCollab.js → components/ProseMirrorCollab.js} +0 -0
- package/front/src/components/RemoteAuthority.js +251 -0
- package/front/src/{contentConfig.js → components/contentConfig.js} +0 -0
- package/front/src/{contentConfigExtensions.js → components/contentConfigExtensions.js} +0 -0
- package/front/src/{schemaJson.js → components/schemaJson.js} +0 -0
- package/index.js +8 -8
- package/package.json +15 -15
- package/front/src/DocumentEditor.vue +0 -173
- package/front/src/EditorMenu.vue +0 -123
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<DocumentEditor
|
|
3
|
+
<DocumentEditor targetType="Example" target="one" :type="'rich'" :purpose="'test'"
|
|
4
4
|
:initialContent="emptyContent"
|
|
5
5
|
:config="contentConfig" />
|
|
6
6
|
</div>
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
<script setup>
|
|
10
10
|
|
|
11
|
-
import DocumentEditor from "./DocumentEditor.vue"
|
|
11
|
+
import DocumentEditor from "./components/DocumentEditor.vue"
|
|
12
12
|
import { ref } from 'vue'
|
|
13
|
-
import { basicMarks, messageNodes, richEditorNodes } from "./contentConfig.js"
|
|
13
|
+
import { basicMarks, messageNodes, richEditorNodes } from "./components/contentConfig.js"
|
|
14
14
|
|
|
15
15
|
const contentConfig = {
|
|
16
16
|
marks: {
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
</template>
|
|
16
16
|
|
|
17
17
|
<script setup>
|
|
18
|
-
import ContentView from "./ContentView.js"
|
|
19
|
-
import Editor from "./Editor.vue"
|
|
18
|
+
import ContentView from "./components/ContentView.js"
|
|
19
|
+
import Editor from "./components/Editor.vue"
|
|
20
20
|
import { ref } from 'vue'
|
|
21
|
-
import { basicMarks, messageNodes, richEditorNodes } from "./contentConfig.js"
|
|
21
|
+
import { basicMarks, messageNodes, richEditorNodes } from "./components/contentConfig.js"
|
|
22
22
|
|
|
23
23
|
const contentConfig = {
|
|
24
24
|
marks: {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative">
|
|
3
|
+
<slot v-if="editor" name="menu" :editor="editor" :saveState="saveState">
|
|
4
|
+
<EditorMenu :editor="editor" :config="config" :saveState="saveState">
|
|
5
|
+
<template #before="scope"><slot name="beforeMenu" v-bind="{ ...scope, editor, saveState }" /></template>
|
|
6
|
+
<template #after="scope"><slot name="afterMenu" v-bind="{ ...scope, editor, saveState }" /></template>
|
|
7
|
+
<template #begin="scope"><slot name="menuBegin" v-bind="{ ...scope, editor, saveState }" /></template>
|
|
8
|
+
<template #end="scope"><slot name="menuEnd" v-bind="{ ...scope, editor, saveState }" /></template>
|
|
9
|
+
</EditorMenu>
|
|
10
|
+
</slot>
|
|
11
|
+
<editor-content :editor="editor" class="content" />
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup>
|
|
16
|
+
|
|
17
|
+
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
|
18
|
+
import {
|
|
19
|
+
ref, computed, watch, provide, defineEmits, defineProps, getCurrentInstance, onUnmounted, inject, onMounted
|
|
20
|
+
} from 'vue'
|
|
21
|
+
import { toRefs, useDebounceFn } from '@vueuse/core'
|
|
22
|
+
import EditorMenu from "./EditorMenu.vue"
|
|
23
|
+
import { getExtensions } from "./contentConfigExtensions.js"
|
|
24
|
+
import { getSchemaSpecFromConfig, serializeSchema } from "./schemaJson.js"
|
|
25
|
+
|
|
26
|
+
import { live, path, useApi, inboxReader } from '@live-change/vue3-ssr'
|
|
27
|
+
import ProseMirrorCollab from "./ProseMirrorCollab.js"
|
|
28
|
+
import { sendableSteps, receiveTransaction, getVersion } from 'prosemirror-collab'
|
|
29
|
+
import { Step } from 'prosemirror-transform'
|
|
30
|
+
import RemoteAuthority from "./RemoteAuthority";
|
|
31
|
+
|
|
32
|
+
const props = defineProps({
|
|
33
|
+
targetType: {
|
|
34
|
+
type: String,
|
|
35
|
+
required: true
|
|
36
|
+
},
|
|
37
|
+
target: {
|
|
38
|
+
type: String,
|
|
39
|
+
required: true
|
|
40
|
+
},
|
|
41
|
+
type: {
|
|
42
|
+
type: String,
|
|
43
|
+
required: true
|
|
44
|
+
},
|
|
45
|
+
purpose: {
|
|
46
|
+
type: String,
|
|
47
|
+
required: true
|
|
48
|
+
},
|
|
49
|
+
config: {
|
|
50
|
+
type: Object,
|
|
51
|
+
required: true
|
|
52
|
+
},
|
|
53
|
+
initialContent: {
|
|
54
|
+
type: Object,
|
|
55
|
+
default: () => ({ type: 'doc', content: [ ] })
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const emit = defineEmits(['update:saveState', 'update:version'])
|
|
60
|
+
|
|
61
|
+
const isMounted = ref(false)
|
|
62
|
+
onMounted(() => isMounted.value = true)
|
|
63
|
+
|
|
64
|
+
const appContext = getCurrentInstance().appContext
|
|
65
|
+
const api = useApi(appContext)
|
|
66
|
+
const clientID = api.windowId
|
|
67
|
+
|
|
68
|
+
const { targetType, target } = props
|
|
69
|
+
|
|
70
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|
71
|
+
|
|
72
|
+
const extensions = getExtensions(props.config)
|
|
73
|
+
const authority = new RemoteAuthority(appContext, props.targetType, props.target, props.type, {
|
|
74
|
+
purpose: props.purpose,
|
|
75
|
+
initialContent: props.initialContent
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const saveState = computed(() => authority.synchronizationState ?? 'loading')
|
|
79
|
+
emit('update:saveState', saveState.value)
|
|
80
|
+
watch(() => saveState.value, async (state) => {
|
|
81
|
+
emit('update:saveState', state)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const version = ref()
|
|
85
|
+
watch(() => version.value, async (v) => {
|
|
86
|
+
emit('update:version', v)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
authority.onNewSteps.push(() => {
|
|
90
|
+
console.log("ON NEW STEPS!")
|
|
91
|
+
const { state, view } = editor.value
|
|
92
|
+
const currentVersion = getVersion(state)
|
|
93
|
+
console.log("COLLAB VERSION", currentVersion)
|
|
94
|
+
let { steps, clientIDs } = authority.stepsSince(currentVersion, state.schema)
|
|
95
|
+
console.error("RECEIVE STEPS", steps, clientIDs, clientID)
|
|
96
|
+
const transaction = receiveTransaction(state, steps, clientIDs)
|
|
97
|
+
view.dispatch(transaction)
|
|
98
|
+
version.value = authority.remoteVersion
|
|
99
|
+
})
|
|
100
|
+
authority.onResynchronization.push(async function () {
|
|
101
|
+
const state = editor.value.reactiveState.value
|
|
102
|
+
const sendable = sendableSteps(state)
|
|
103
|
+
if (sendable) {
|
|
104
|
+
console.error("SEND STEPS", sendable.steps, sendable.version)
|
|
105
|
+
authority.receiveSteps(sendable.version, sendable.steps, sendable.clientID)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
onUnmounted(() => authority.dispose())
|
|
110
|
+
const documentData = await authority.loadDocument()
|
|
111
|
+
version.value = authority.remoteVersion
|
|
112
|
+
|
|
113
|
+
console.log("CONFIG", props.config, 'EXTENSIONS', extensions
|
|
114
|
+
)
|
|
115
|
+
console.log("DOCUMENT DATA", documentData)
|
|
116
|
+
const editor = useEditor({
|
|
117
|
+
content: documentData.content,
|
|
118
|
+
extensions: [
|
|
119
|
+
...extensions,
|
|
120
|
+
ProseMirrorCollab.configure({
|
|
121
|
+
version: documentData.version,
|
|
122
|
+
clientID
|
|
123
|
+
})
|
|
124
|
+
],
|
|
125
|
+
onTransaction: ({ editor, transaction }) => {
|
|
126
|
+
console.log("EDITOR UPDATE", editor, transaction)
|
|
127
|
+
for(const step of transaction.steps) {
|
|
128
|
+
window.lastStep = step
|
|
129
|
+
}
|
|
130
|
+
saveDebounced()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const schemaSpec = editor.view
|
|
135
|
+
? serializeSchema(editor.view.state.schema.spec)
|
|
136
|
+
: getSchemaSpecFromConfig(props.config)
|
|
137
|
+
|
|
138
|
+
async function save() {
|
|
139
|
+
const state = editor.value.reactiveState.value
|
|
140
|
+
const sendable = sendableSteps(state)
|
|
141
|
+
if (sendable) {
|
|
142
|
+
console.error("SEND STEPS", sendable.steps, sendable.version)
|
|
143
|
+
authority.receiveSteps(sendable.version, sendable.steps, sendable.clientID)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const saveDebounced = useDebounceFn(save, 100)
|
|
147
|
+
|
|
148
|
+
if(typeof window !== 'undefined') {
|
|
149
|
+
window.editor = editor
|
|
150
|
+
window.testStep = (json) => {
|
|
151
|
+
const testStep = Step.fromJSON(schema, json)
|
|
152
|
+
console.log("TS", testStep)
|
|
153
|
+
const transaction = editor.value.view.state.tr
|
|
154
|
+
transaction.step(testStep)
|
|
155
|
+
console.log("TRANSACTION", transaction)
|
|
156
|
+
transaction.apply(editor.value.view.state)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<style scoped>
|
|
162
|
+
|
|
163
|
+
</style>
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="relative">
|
|
3
3
|
<slot v-if="editor" name="menu" :editor="editor">
|
|
4
|
-
<EditorMenu :editor="editor" :config="config"
|
|
4
|
+
<EditorMenu :editor="editor" :config="config">
|
|
5
|
+
<template #beforeMenu="scope"><slot name="before" v-bind="{ ...scope, editor }" /></template>
|
|
6
|
+
<template #afterMenu="scope"><slot name="after" v-bind="{ ...scope, editor }" /></template>
|
|
7
|
+
<template #begin="scope"><slot name="menuBegin" v-bind="{ ...scope, editor, saveState }" /></template>
|
|
8
|
+
<template #end="scope"><slot name="menuEnd" v-bind="{ ...scope, editor, saveState }" /></template>
|
|
9
|
+
</EditorMenu>
|
|
5
10
|
</slot>
|
|
6
11
|
<editor-content :editor="editor" class="content" />
|
|
7
12
|
</div>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sticky" style="top: 90px; z-index: 1">
|
|
3
|
+
<slot name="before" />
|
|
4
|
+
<div class="surface-card p-1 shadow-2 border-round flex flex-row flex-wrap">
|
|
5
|
+
|
|
6
|
+
<slot name="begin" />
|
|
7
|
+
|
|
8
|
+
<div class="p-buttonset mr-1 mb-1">
|
|
9
|
+
<Button v-if="config.marks.bold" label="B" class="p-button-sm font-bold"
|
|
10
|
+
:class="{ 'p-button-outlined': !editor.isActive('bold') }"
|
|
11
|
+
@click="editor.chain().focus().toggleBold().run()" />
|
|
12
|
+
<Button v-if="config.marks.italic" label="I" class="p-button-sm font-italic"
|
|
13
|
+
:class="{ 'p-button-outlined': !editor.isActive('italic') }"
|
|
14
|
+
@click="editor.chain().focus().toggleItalic().run()" />
|
|
15
|
+
<Button v-if="config.marks.underline" label="U" class="p-button-sm underline"
|
|
16
|
+
:class="{ 'p-button-outlined': !editor.isActive('underline') }"
|
|
17
|
+
@click="editor.chain().focus().toggleUnderline().run()" />
|
|
18
|
+
<Button v-if="config.marks.strike" label="S" class="p-button-sm line-through"
|
|
19
|
+
:class="{ 'p-button-outlined': !editor.isActive('strike') }"
|
|
20
|
+
@click="editor.chain().focus().toggleStrike().run()" />
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="p-buttonset mr-1 mb-1">
|
|
24
|
+
<Button v-if="config.nodes.bulletList" icon="fa-solid fa-list-ul" class="p-button-sm"
|
|
25
|
+
:class="{ 'p-button-outlined': !editor.isActive('bulletlist') }"
|
|
26
|
+
@click="editor.chain().focus().toggleBulletList().run()" />
|
|
27
|
+
<Button v-if="config.nodes.orderedList" icon="fa-solid fa-list-ol" class="p-button-sm"
|
|
28
|
+
:class="{ 'p-button-outlined': !editor.isActive('orderedlist') }"
|
|
29
|
+
@click="editor.chain().focus().toggleOrderedList().run()" />
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div v-if="config.nodes.heading" class="p-buttonset mr-1 mb-1">
|
|
33
|
+
<Button label="H1" class="p-button-sm"
|
|
34
|
+
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 1 }) }"
|
|
35
|
+
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()" />
|
|
36
|
+
<Button label="H2" class="p-button-sm"
|
|
37
|
+
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 2 }) }"
|
|
38
|
+
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()" />
|
|
39
|
+
<Button label="H3" class="p-button-sm"
|
|
40
|
+
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 3 }) }"
|
|
41
|
+
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()" />
|
|
42
|
+
<Button label="H4" class="p-button-sm"
|
|
43
|
+
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 4 }) }"
|
|
44
|
+
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()" />
|
|
45
|
+
<Button label="H5" class="p-button-sm"
|
|
46
|
+
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 5 }) }"
|
|
47
|
+
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()" />
|
|
48
|
+
<Button label="H6" class="p-button-sm"
|
|
49
|
+
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 6 }) }"
|
|
50
|
+
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()" />
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="p-buttonset mr-1 mb-1">
|
|
54
|
+
<Button v-if="config.nodes.blockquote" icon="fa-solid fa-quote-left" class="p-button-sm"
|
|
55
|
+
:class="{ 'p-button-outlined': !editor.isActive('blockquote') }"
|
|
56
|
+
@click="editor.chain().focus().toggleBlockquote().run()" />
|
|
57
|
+
<Button v-if="config.nodes.codeBlock" icon="fa-solid fa-code" class="p-button-sm"
|
|
58
|
+
:class="{ 'p-button-outlined': !editor.isActive('codeblock') }"
|
|
59
|
+
@click="editor.chain().focus().toggleCodeBlock().run()" />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="p-buttonset mr-1 mb-1">
|
|
63
|
+
<Button v-if="config.nodes.hardBreak" label="BR" class="p-button-sm p-button-outlined"
|
|
64
|
+
@click="editor.chain().focus().setHardBreak().run()" />
|
|
65
|
+
<Button v-if="config.nodes.horizontalRule" label="HR" class="p-button-sm p-button-outlined"
|
|
66
|
+
@click="editor.chain().focus().setHorizontalRule().run()" />
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- <div class="p-buttonset mr-1 mb-1">-->
|
|
70
|
+
<!-- <Button v-if="config.nodes.hardBreak" icon="pi pi-image" class="p-button-sm"
|
|
71
|
+
:class="{ 'p-button-outlined': true }"
|
|
72
|
+
@click="editor.chain().focus().setImage().run()" />-->
|
|
73
|
+
<FileInput @input="handleImageUpload" class="inline-block"
|
|
74
|
+
accept="image/png,image/jpeg,image/webp,.jpg,.jpeg,.png,.webp">
|
|
75
|
+
<Button v-if="config.nodes.image" icon="pi pi-image" class="p-button-sm p-button-outlined mr-1 mb-1" />
|
|
76
|
+
</FileInput>
|
|
77
|
+
<!-- </div>-->
|
|
78
|
+
|
|
79
|
+
<span v-if="saveState"
|
|
80
|
+
class="p-button p-component p-button-sm p-button-outlined p-button-secondary cursor-auto
|
|
81
|
+
inline-block mr-1 mb-1"
|
|
82
|
+
style="width: 100px">
|
|
83
|
+
<span v-if="saveState == 'saving'" class="inline-block mr-2 relative" style="width: 20px">
|
|
84
|
+
|
|
85
|
+
<ProgressSpinner class="absolute"
|
|
86
|
+
style="width: 22px; height: 22px; top: -2px; left: 0"
|
|
87
|
+
strokeWidth="4" fill="var(--surface-ground)" animationDuration=".5s" />
|
|
88
|
+
</span>
|
|
89
|
+
|
|
90
|
+
<span v-else class="pi pi-check p-button-icon p-button-icon-left"></span>
|
|
91
|
+
<span class="p-button-label">{{ saveState[0].toUpperCase() + saveState.slice(1) }}</span>
|
|
92
|
+
</span>
|
|
93
|
+
|
|
94
|
+
<slot name="end" />
|
|
95
|
+
|
|
96
|
+
</div>
|
|
97
|
+
<slot name="after" />
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<script setup>
|
|
102
|
+
import Button from "primevue/button"
|
|
103
|
+
import ProgressSpinner from "primevue/progressspinner"
|
|
104
|
+
|
|
105
|
+
import { inject, getCurrentInstance } from "vue"
|
|
106
|
+
import { toRefs } from "@vueuse/core"
|
|
107
|
+
import { uploadImage } from "@live-change/image-frontend"
|
|
108
|
+
import { FileInput } from "@live-change/upload-frontend"
|
|
109
|
+
|
|
110
|
+
const appContext = getCurrentInstance().appContext
|
|
111
|
+
const workingZone = inject('workingZone')
|
|
112
|
+
|
|
113
|
+
const props = defineProps({
|
|
114
|
+
editor: {
|
|
115
|
+
type: Object,
|
|
116
|
+
required: true
|
|
117
|
+
},
|
|
118
|
+
config: {
|
|
119
|
+
type: Object,
|
|
120
|
+
required: true
|
|
121
|
+
},
|
|
122
|
+
saveState: {
|
|
123
|
+
type: String,
|
|
124
|
+
default: null
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const { editor, config, saveState } = toRefs(props)
|
|
129
|
+
|
|
130
|
+
async function handleImageUpload(file) {
|
|
131
|
+
const upload = uploadImage('test', { file },
|
|
132
|
+
{ preparedPreview: true, appContext, generateId: true })
|
|
133
|
+
await workingZone.addPromise('disconnectContact', (async () => {
|
|
134
|
+
editor.value.chain().focus().setImage({ image: upload.id }).run()
|
|
135
|
+
await upload.prepare()
|
|
136
|
+
})())
|
|
137
|
+
console.log("START UPLOAD!")
|
|
138
|
+
await upload.upload()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
window.editor = editor.value
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
<style scoped>
|
|
145
|
+
.p-buttonset .p-button:not(:last-child) { /* buttonset bug fix */
|
|
146
|
+
border-right: 0 none !important;
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { useApi, inboxReader } from '@live-change/vue3-ssr'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { Step } from "prosemirror-transform"
|
|
5
|
+
|
|
6
|
+
import Debug from "debug"
|
|
7
|
+
const debug = Debug("wysiwyg:collab")
|
|
8
|
+
|
|
9
|
+
const stepsThrottle = 100
|
|
10
|
+
|
|
11
|
+
class RemoteAuthority{
|
|
12
|
+
constructor(appContext, targetType, target, type, options) {
|
|
13
|
+
this.appContext = appContext
|
|
14
|
+
this.api = useApi(appContext)
|
|
15
|
+
|
|
16
|
+
this.targetType = targetType
|
|
17
|
+
this.target = target
|
|
18
|
+
this.type = type
|
|
19
|
+
this.options = options
|
|
20
|
+
this.onNewSteps = []
|
|
21
|
+
this.onResynchronization = []
|
|
22
|
+
|
|
23
|
+
this.remoteVersion = undefined
|
|
24
|
+
this.receivedSteps = []
|
|
25
|
+
|
|
26
|
+
this.sentSteps = []
|
|
27
|
+
this.sentVersion = undefined
|
|
28
|
+
|
|
29
|
+
this.handleStepsTimeout = null
|
|
30
|
+
|
|
31
|
+
this.waitingForResync = true
|
|
32
|
+
this.blockNextResync = false
|
|
33
|
+
this.pendingRequests = 0
|
|
34
|
+
|
|
35
|
+
this.synchronizationState = ref('loading')
|
|
36
|
+
|
|
37
|
+
this.api.source.on('connect', () => {
|
|
38
|
+
this.resynchronize()
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resynchronize() {
|
|
43
|
+
debug("RESYNCHRONIZATION REQUESTED!")
|
|
44
|
+
if(this.blockNextResync) return
|
|
45
|
+
if(!this.stepsReader) return
|
|
46
|
+
debug("BUCKET", this.stepsReader.observable.getValue()?.length, this.stepsReader.bucketSize,
|
|
47
|
+
JSON.stringify(this.stepsReader.observable.getValue()))
|
|
48
|
+
debug("QUEUE", this.stepsReader.queue.length)
|
|
49
|
+
if(this.stepsReader.observable.getValue() === undefined
|
|
50
|
+
|| this.stepsReader.observable.getValue().length == this.stepsReader.bucketSize
|
|
51
|
+
|| this.stepsReader.queue.length > 0) { /// wait for reader
|
|
52
|
+
this.waitingForResync = true
|
|
53
|
+
this.blockNextResync = true
|
|
54
|
+
debug("WAIT FOR READER")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
debug("RESYNCHRONIZE!")
|
|
58
|
+
this.waitingForResync = false
|
|
59
|
+
this.sentSteps = []
|
|
60
|
+
this.sentVersion = -1
|
|
61
|
+
|
|
62
|
+
if(this.pendingRequests == 0) {
|
|
63
|
+
this.synchronizationState.value = 'saved' // will be set to saving after any save attempt
|
|
64
|
+
}
|
|
65
|
+
for(const callback of this.onResynchronization) {
|
|
66
|
+
callback()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
handleSteps() {
|
|
71
|
+
for(const listener of this.onNewSteps) listener()
|
|
72
|
+
if(this.receivedSteps.length > 1000) this.receivedSteps = this.receivedSteps.slice(-100)
|
|
73
|
+
this.handleStepsTimeout = null
|
|
74
|
+
this.blockNextResync = false
|
|
75
|
+
if(this.waitingForResync) {
|
|
76
|
+
this.resynchronize()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
handleStepsThrottled() {
|
|
80
|
+
if(this.handleStepsTimeout === null) {
|
|
81
|
+
this.handleStepsTimeout = setTimeout(() => this.handleSteps(), stepsThrottle)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async startInboxReader() {
|
|
86
|
+
const inboxPrefix = JSON.stringify(JSON.stringify(this.targetType)+':'+JSON.stringify(this.target))+':'
|
|
87
|
+
const identifiers = { targetType: this.targetType, target: this.target }
|
|
88
|
+
console.log("START INBOX READER FROM", this.remoteVersion)
|
|
89
|
+
this.stepsReader = inboxReader(
|
|
90
|
+
(rawPosition, bucketSize) => {
|
|
91
|
+
console.log("RP", rawPosition)
|
|
92
|
+
const positionCounter = (+rawPosition.split(':').pop().replace(/"/g, '')) + 1
|
|
93
|
+
console.log("PC", positionCounter)
|
|
94
|
+
const position = inboxPrefix + JSON.stringify(positionCounter.toFixed().padStart(10, '0'))
|
|
95
|
+
const path = ['prosemirror', 'steps', { ...identifiers , gt: position, limit: bucketSize }]
|
|
96
|
+
console.log("P", path)
|
|
97
|
+
return path
|
|
98
|
+
},
|
|
99
|
+
(message) => {
|
|
100
|
+
debug("MESSAGE!", message, this.remoteVersion)
|
|
101
|
+
const modifiedVersion = +message.id.split(':').pop().replace(/"/g, '')
|
|
102
|
+
const originalVersion = modifiedVersion - message.steps.length
|
|
103
|
+
if(originalVersion != this.remoteVersion) throw new Error("message out of order!")
|
|
104
|
+
this.remoteVersion = modifiedVersion
|
|
105
|
+
this.receivedSteps.push(...message.steps.map(step => ({ step, window: message.window })))
|
|
106
|
+
this.handleStepsThrottled()
|
|
107
|
+
},
|
|
108
|
+
inboxPrefix + JSON.stringify((this.remoteVersion - 1).toFixed().padStart(10, '0')),
|
|
109
|
+
{
|
|
110
|
+
bucketSize: 32,
|
|
111
|
+
context: this.appContext
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async loadDocument() {
|
|
117
|
+
debug("LOAD DOCUMENT!")
|
|
118
|
+
const identifier = { targetType: this.targetType, target: this.target }
|
|
119
|
+
let documentData = await this.api.get(['prosemirror', 'document', identifier])
|
|
120
|
+
if(!documentData) {
|
|
121
|
+
documentData = {
|
|
122
|
+
...identifier,
|
|
123
|
+
type: this.type,
|
|
124
|
+
purpose: this.options?.purpose ?? 'document',
|
|
125
|
+
content: this.options?.initialContent ?? { type: 'doc', content: [ ] },
|
|
126
|
+
version: 1
|
|
127
|
+
}
|
|
128
|
+
documentData = await this.api.actions.prosemirror.createDocumentIfNotExists(documentData)
|
|
129
|
+
}
|
|
130
|
+
this.remoteVersion = documentData.version
|
|
131
|
+
this.waitingForResync = false
|
|
132
|
+
if(typeof window != 'undefined') {
|
|
133
|
+
await this.startInboxReader()
|
|
134
|
+
}
|
|
135
|
+
this.synchronizationState.value = 'loaded'
|
|
136
|
+
return documentData
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async startWithLoadedDocument(documentData) {
|
|
140
|
+
debug("START WITH LOADED DOCUMENT!")
|
|
141
|
+
this.remoteVersion = documentData.version
|
|
142
|
+
this.waitingForResync = false
|
|
143
|
+
await this.startInboxReader()
|
|
144
|
+
this.synchronizationState.value = 'loaded'
|
|
145
|
+
return documentData
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
dispose() {
|
|
149
|
+
if(this.stepsReader) {
|
|
150
|
+
this.stepsReader.dispose()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async receiveSteps(version, steps, clientId) {
|
|
155
|
+
if(this.waitingForResync) return
|
|
156
|
+
debug("RECEIVE STEPS", version, '<', this.remoteVersion)
|
|
157
|
+
if(version < this.remoteVersion) return
|
|
158
|
+
|
|
159
|
+
const stepsJson = steps.map((step, index) => ({ version: version + index, step: step.toJSON() }))
|
|
160
|
+
debug("STEPS JSON", stepsJson)
|
|
161
|
+
|
|
162
|
+
/// REMOVE DUPLICATED STEPS:
|
|
163
|
+
let firstOriginalStepIndex = 0
|
|
164
|
+
let resynchronization = false
|
|
165
|
+
debug("VERSION", version, "SENT VERSION", this.sentVersion)
|
|
166
|
+
if(version <= this.sentVersion - this.sentSteps.length) {
|
|
167
|
+
debug("STEPS BUFFER OVERFLOW, WAITING FOR RESYNCHRONIZATION")
|
|
168
|
+
this.waitingForResync = true
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
if(version <= this.sentVersion) { // found resend, check for duplicated steps
|
|
172
|
+
for(let i = 0; i < stepsJson.length; i++) {
|
|
173
|
+
const step = stepsJson[i]
|
|
174
|
+
const sentStep = this.sentSteps.find(({ version }) => version == step.version)
|
|
175
|
+
if(sentStep) {
|
|
176
|
+
//debug('FOUND SENT STEP WITH THE SAME VERSION', sentStep, '==', step)
|
|
177
|
+
if(sentStep.json != step.json) {
|
|
178
|
+
resynchronization = true
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
firstOriginalStepIndex = i + 1
|
|
182
|
+
} else break
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
stepsJson.splice(0, firstOriginalStepIndex)
|
|
186
|
+
debug("STEPS JSON AFTER DUPLICATED REMOVAL", stepsJson.length)
|
|
187
|
+
if(resynchronization && this.sentSteps.length > 0) {
|
|
188
|
+
debug("RESYNCHRONIZATION!")
|
|
189
|
+
const firstVersion = stepsJson[0].version
|
|
190
|
+
this.sentSteps = this.sentSteps.filter(({ version }) => version < firstVersion)
|
|
191
|
+
}
|
|
192
|
+
this.sentSteps = this.sentSteps.concat(stepsJson)
|
|
193
|
+
if(this.sentSteps.length > 200) this.sentSteps = this.sentSteps.slice(-100)
|
|
194
|
+
|
|
195
|
+
if(stepsJson.length == 0) {
|
|
196
|
+
debug("NOTHING TO SEND")
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
this.sentVersion = stepsJson[0].version
|
|
200
|
+
|
|
201
|
+
if(this.pendingRequests == 0) this.synchronizationState.value = 'saving'
|
|
202
|
+
let result
|
|
203
|
+
try {
|
|
204
|
+
this.pendingRequests++
|
|
205
|
+
result = await this.api.actions.prosemirror.edit({
|
|
206
|
+
targetType: this.targetType,
|
|
207
|
+
target: this.target,
|
|
208
|
+
type: this.type,
|
|
209
|
+
version: this.sentVersion, steps: stepsJson.map(({step}) => step),
|
|
210
|
+
window: api.windowId,
|
|
211
|
+
continuation: firstOriginalStepIndex > 0
|
|
212
|
+
})
|
|
213
|
+
} finally {
|
|
214
|
+
this.pendingRequests--
|
|
215
|
+
}
|
|
216
|
+
if(result == 'saved') {
|
|
217
|
+
if(this.pendingRequests == 0) this.synchronizationState.value = 'saved'
|
|
218
|
+
} else {
|
|
219
|
+
this.resynchronize()
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
stepsSince(version, schema) {
|
|
224
|
+
debug("GET STEPS SINCE", version, this.receivedSteps)
|
|
225
|
+
const firstStepVersion = this.remoteVersion - this.receivedSteps.length
|
|
226
|
+
debug(
|
|
227
|
+
"COLLAB REQUESTED VERSION:", version,
|
|
228
|
+
" FIRST STEP VERSION:", firstStepVersion,
|
|
229
|
+
" REMOTE VERSION:", this.remoteVersion
|
|
230
|
+
)
|
|
231
|
+
if(version < firstStepVersion) {
|
|
232
|
+
console.error("COLLAB REQUESTED BACK STEPS!")
|
|
233
|
+
console.info("STARTING TIME TRAVEL!")
|
|
234
|
+
this.receivedSteps = []
|
|
235
|
+
if(!this.stepsReader) throw new Error("steps reader not initialized!")
|
|
236
|
+
this.stepsReader.dispose()
|
|
237
|
+
this.remoteVersion = version
|
|
238
|
+
this.startInboxReader()
|
|
239
|
+
return { steps: [], clientIDs: [] }
|
|
240
|
+
}
|
|
241
|
+
const versionDiff = version - firstStepVersion
|
|
242
|
+
const stepsData = this.receivedSteps.slice(versionDiff)
|
|
243
|
+
console.log("SD", stepsData)
|
|
244
|
+
debug("PARSING STEPS", stepsData.length)
|
|
245
|
+
const steps = stepsData.map(stepData => Step.fromJSON(schema, stepData.step))
|
|
246
|
+
const clientIDs = stepsData.map(stepData => stepData.window)
|
|
247
|
+
return { steps, clientIDs }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default RemoteAuthority
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import * as config from './front/src/contentConfig.js'
|
|
2
|
-
import * as extensions from './front/src/contentConfigExtensions.js'
|
|
1
|
+
import * as config from './front/src/components/contentConfig.js'
|
|
2
|
+
import * as extensions from './front/src/components/contentConfigExtensions.js'
|
|
3
3
|
export { config, extensions }
|
|
4
4
|
|
|
5
|
-
import ContentView from "./front/src/ContentView.js"
|
|
5
|
+
import ContentView from "./front/src/components/ContentView.js"
|
|
6
6
|
export { ContentView }
|
|
7
7
|
|
|
8
|
-
import DocumentEditor from "./front/src/DocumentEditor.vue"
|
|
9
|
-
import Editor from "./front/src/Editor.vue"
|
|
10
|
-
import EditorMenu from "./front/src/EditorMenu.vue"
|
|
11
|
-
import ImageComponent from "./front/src/ImageComponent.vue"
|
|
12
|
-
import ImageNode from "./front/src/ImageNode.js"
|
|
8
|
+
import DocumentEditor from "./front/src/components/DocumentEditor.vue"
|
|
9
|
+
import Editor from "./front/src/components/Editor.vue"
|
|
10
|
+
import EditorMenu from "./front/src/components/EditorMenu.vue"
|
|
11
|
+
import ImageComponent from "./front/src/components/ImageComponent.vue"
|
|
12
|
+
import ImageNode from "./front/src/components/ImageNode.js"
|
|
13
13
|
export { DocumentEditor, Editor, EditorMenu, ImageComponent, ImageNode }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/wysiwyg-frontend",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"memDev": "lcli memDev --enableSessions --initScript ./init.js --dbAccess",
|
|
6
6
|
"localDevInit": "rm tmp.db; lcli localDev --enableSessions --initScript ./init.js",
|
|
@@ -21,16 +21,16 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@fortawesome/fontawesome-free": "^6.1.1",
|
|
24
|
-
"@live-change/cli": "0.
|
|
25
|
-
"@live-change/dao": "0.5.
|
|
26
|
-
"@live-change/dao-vue3": "0.5.
|
|
27
|
-
"@live-change/dao-websocket": "0.5.
|
|
28
|
-
"@live-change/framework": "0.
|
|
29
|
-
"@live-change/password-authentication-service": "0.2
|
|
30
|
-
"@live-change/prosemirror-service": "0.2
|
|
31
|
-
"@live-change/secret-code-service": "0.2
|
|
32
|
-
"@live-change/secret-link-service": "0.2
|
|
33
|
-
"@live-change/session-service": "0.2
|
|
24
|
+
"@live-change/cli": "0.7.4",
|
|
25
|
+
"@live-change/dao": "0.5.8",
|
|
26
|
+
"@live-change/dao-vue3": "0.5.8",
|
|
27
|
+
"@live-change/dao-websocket": "0.5.8",
|
|
28
|
+
"@live-change/framework": "0.7.4",
|
|
29
|
+
"@live-change/password-authentication-service": "0.3.2",
|
|
30
|
+
"@live-change/prosemirror-service": "0.3.2",
|
|
31
|
+
"@live-change/secret-code-service": "0.3.2",
|
|
32
|
+
"@live-change/secret-link-service": "0.3.2",
|
|
33
|
+
"@live-change/session-service": "0.3.2",
|
|
34
34
|
"@live-change/vue3-components": "0.2.15",
|
|
35
35
|
"@live-change/vue3-ssr": "0.2.15",
|
|
36
36
|
"@tiptap/extension-blockquote": "^2.0.0-beta.29",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"cross-env": "^7.0.3",
|
|
61
61
|
"get-port-sync": "1.0.1",
|
|
62
62
|
"primeflex": "^3.2.1",
|
|
63
|
-
"primeicons": "^
|
|
64
|
-
"primevue": "^3.
|
|
63
|
+
"primeicons": "^6.0.1",
|
|
64
|
+
"primevue": "^3.18.1",
|
|
65
65
|
"prosemirror-collab": "^1.3.0",
|
|
66
66
|
"rollup-plugin-node-builtins": "^2.1.2",
|
|
67
67
|
"rollup-plugin-visualizer": "5.6.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"vue3-scroll-border": "0.1.2"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@live-change/codeceptjs-helper": "0.
|
|
76
|
+
"@live-change/codeceptjs-helper": "0.7.4",
|
|
77
77
|
"@wdio/selenium-standalone-service": "^7.20.8",
|
|
78
78
|
"codeceptjs": "^3.3.4",
|
|
79
79
|
"generate-password": "1.7.0",
|
|
@@ -85,5 +85,5 @@
|
|
|
85
85
|
"author": "",
|
|
86
86
|
"license": "ISC",
|
|
87
87
|
"description": "",
|
|
88
|
-
"gitHead": "
|
|
88
|
+
"gitHead": "323185f6d910912093b3c92df102d0f938cef367"
|
|
89
89
|
}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="relative">
|
|
3
|
-
<slot v-if="editor" name="menu" :editor="editor">
|
|
4
|
-
<EditorMenu :editor="editor" :config="config" />
|
|
5
|
-
</slot>
|
|
6
|
-
<editor-content :editor="editor" class="content" />
|
|
7
|
-
</div>
|
|
8
|
-
</template>
|
|
9
|
-
|
|
10
|
-
<script setup>
|
|
11
|
-
|
|
12
|
-
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
|
13
|
-
import { ref, computed, watch, provide, defineEmits, defineProps, getCurrentInstance, onUnmounted } from 'vue'
|
|
14
|
-
import { toRefs, useDebounceFn } from '@vueuse/core'
|
|
15
|
-
import EditorMenu from "./EditorMenu.vue"
|
|
16
|
-
import { getExtensions } from "./contentConfigExtensions.js"
|
|
17
|
-
import { getSchemaSpecFromConfig } from "./schemaJson.js"
|
|
18
|
-
|
|
19
|
-
import { live, path, useApi, inboxReader } from '@live-change/vue3-ssr'
|
|
20
|
-
import ProseMirrorCollab from "./ProseMirrorCollab.js"
|
|
21
|
-
import { sendableSteps, receiveTransaction, getVersion } from 'prosemirror-collab'
|
|
22
|
-
import { Step } from 'prosemirror-transform'
|
|
23
|
-
|
|
24
|
-
const props = defineProps({
|
|
25
|
-
document: {
|
|
26
|
-
type: String,
|
|
27
|
-
required: true
|
|
28
|
-
},
|
|
29
|
-
type: {
|
|
30
|
-
type: String,
|
|
31
|
-
required: true
|
|
32
|
-
},
|
|
33
|
-
purpose: {
|
|
34
|
-
type: String,
|
|
35
|
-
required: true
|
|
36
|
-
},
|
|
37
|
-
config: {
|
|
38
|
-
type: Object,
|
|
39
|
-
required: true
|
|
40
|
-
},
|
|
41
|
-
initialContent: {
|
|
42
|
-
type: Object,
|
|
43
|
-
default: () => ({ type: 'doc', content: [ ] })
|
|
44
|
-
}
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
const appContext = getCurrentInstance().appContext
|
|
48
|
-
const api = useApi(appContext)
|
|
49
|
-
|
|
50
|
-
const { document } = props
|
|
51
|
-
|
|
52
|
-
let documentData = await api.get(['prosemirror', 'document', { document }])
|
|
53
|
-
if(!documentData) {
|
|
54
|
-
documentData = {
|
|
55
|
-
id: document,
|
|
56
|
-
document: props.document,
|
|
57
|
-
type: props.type,
|
|
58
|
-
purpose: props.purpose,
|
|
59
|
-
content: props.initialContent,
|
|
60
|
-
version: 0
|
|
61
|
-
}
|
|
62
|
-
documentData = await api.command(['prosemirror', 'createDocumentIfNotExists'], documentData)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const extensions = getExtensions(props.config)
|
|
66
|
-
//console.log("CONFIG", props.config, 'EXTENSIONS', extensions)
|
|
67
|
-
const editor = useEditor({
|
|
68
|
-
content: documentData.content,
|
|
69
|
-
extensions: [
|
|
70
|
-
...extensions,
|
|
71
|
-
ProseMirrorCollab.configure({
|
|
72
|
-
version: documentData.version,
|
|
73
|
-
clientID: api.windowId
|
|
74
|
-
})
|
|
75
|
-
],
|
|
76
|
-
onTransaction: ({ editor, transaction }) => {
|
|
77
|
-
console.log("EDITOR UPDATE", editor, transaction)
|
|
78
|
-
saveDebounced()
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
let sentSteps = []
|
|
83
|
-
let sentVersion = 0
|
|
84
|
-
|
|
85
|
-
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|
86
|
-
|
|
87
|
-
async function save() {
|
|
88
|
-
const state = editor.value.reactiveState.value
|
|
89
|
-
const sendable = sendableSteps(state)
|
|
90
|
-
if(sendable) {
|
|
91
|
-
let version = sendable.version
|
|
92
|
-
let firstOriginalStepIndex = 0
|
|
93
|
-
let resynchronization = false
|
|
94
|
-
console.log("SENDABLE STEPS", sendable)
|
|
95
|
-
const serializedSteps = sendable.steps.map((step, index) => ({ version: version + index, step: step.toJSON() }))
|
|
96
|
-
console.log("SERIALIZED STEPS", serializedSteps)
|
|
97
|
-
if(sendable.version <= sentVersion) { // found resend, check for duplicated steps
|
|
98
|
-
for(let i = 0; i < serializedSteps.length; i++) {
|
|
99
|
-
const step = serializedSteps[i]
|
|
100
|
-
const sentStep = sentSteps.find(({ version }) => version == step.version)
|
|
101
|
-
if(sentStep) {
|
|
102
|
-
console.log('FOUND SENT STEP WITH THE SAME VERSION', sentStep, '==', step)
|
|
103
|
-
if(JSON.stringify(sentStep) != JSON.stringify(step)) {
|
|
104
|
-
resynchronization = true
|
|
105
|
-
break
|
|
106
|
-
}
|
|
107
|
-
firstOriginalStepIndex = i + 1
|
|
108
|
-
} else break
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
serializedSteps.splice(0, firstOriginalStepIndex)
|
|
112
|
-
if(resynchronization && sentSteps.length > 0) {
|
|
113
|
-
console.log("RESYNCHRONIZATION!")
|
|
114
|
-
const firstVersion = serializedSteps[0].version
|
|
115
|
-
sentSteps = sentSteps.filter(({ version }) => version < firstVersion)
|
|
116
|
-
}
|
|
117
|
-
if(serializedSteps.length == 0) {
|
|
118
|
-
console.log("NOTHING TO SEND")
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
sentVersion = serializedSteps[0].version
|
|
122
|
-
sentSteps = sentSteps.concat(serializedSteps)
|
|
123
|
-
if(sentSteps.length > 200) sentSteps = sentSteps.slice(-100)
|
|
124
|
-
const data = {
|
|
125
|
-
document, type: props.type,
|
|
126
|
-
version: sentVersion, steps: serializedSteps.map(({ step }) => step), window: api.windowId,
|
|
127
|
-
continuation: firstOriginalStepIndex > 0
|
|
128
|
-
}
|
|
129
|
-
console.log("SEND DATA", data)
|
|
130
|
-
//await sleep(1000)
|
|
131
|
-
await api.command(['prosemirror', 'doSteps'], data)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
const saveDebounced = useDebounceFn(save, 100)
|
|
135
|
-
|
|
136
|
-
api.source.on('connect', () => {
|
|
137
|
-
save()
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
if(typeof window != 'undefined') {
|
|
141
|
-
const stepsReader = inboxReader(
|
|
142
|
-
(rawPosition, bucketSize) => {
|
|
143
|
-
const positionCounter = +rawPosition.split(':').pop().replace(/"/g, '')
|
|
144
|
-
const position = `${JSON.stringify(document)}:${JSON.stringify(positionCounter.toFixed().padStart(10, '0'))}`
|
|
145
|
-
return ['prosemirror', 'steps', { document, gt: position, limit: bucketSize }]
|
|
146
|
-
},
|
|
147
|
-
(message) => {
|
|
148
|
-
console.log("MESSAGE!", message)
|
|
149
|
-
const modifiedVersion = +message.id.split(':').pop().replace(/"/g, '')
|
|
150
|
-
const originalVersion = modifiedVersion - message.steps.length
|
|
151
|
-
const state = editor.value.reactiveState.value
|
|
152
|
-
const currentVersion = getVersion(state)
|
|
153
|
-
if(currentVersion < originalVersion) throw new Error("COLLAB REQUESTED BACK STEPS!")
|
|
154
|
-
console.log("COLLAB VERSION", currentVersion, "ORIGINAL VERSION", originalVersion, "MODIFIED VERSION", modifiedVersion)
|
|
155
|
-
const versionDiff = currentVersion - originalVersion
|
|
156
|
-
const newSteps = message.steps.slice(versionDiff)
|
|
157
|
-
console.log("RUNNING STEPS", newSteps.length, "/", message.steps.length)
|
|
158
|
-
const steps = newSteps.map(stepJson => Step.fromJSON(editor.value.schema, stepJson))
|
|
159
|
-
const clientIDs = steps.map(step => message.window)
|
|
160
|
-
const transaction = receiveTransaction(state, steps, clientIDs)
|
|
161
|
-
editor.value.view.dispatch(transaction)
|
|
162
|
-
},
|
|
163
|
-
`${JSON.stringify(document)}:${JSON.stringify(documentData.version.toFixed().padStart(10, '0'))}`
|
|
164
|
-
)
|
|
165
|
-
onUnmounted(() => stepsReader.dispose())
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const schemaSpec = ref(getSchemaSpecFromConfig(props.config))
|
|
169
|
-
</script>
|
|
170
|
-
|
|
171
|
-
<style scoped>
|
|
172
|
-
|
|
173
|
-
</style>
|
package/front/src/EditorMenu.vue
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="surface-card p-1 shadow-2 border-round sticky flex flex-row flex-wrap"
|
|
3
|
-
style="top: 90px; z-index: 1">
|
|
4
|
-
<div class="p-buttonset mr-1 mb-1">
|
|
5
|
-
<Button v-if="config.marks.bold" label="B" class="p-button-sm font-bold"
|
|
6
|
-
:class="{ 'p-button-outlined': !editor.isActive('bold') }"
|
|
7
|
-
@click="editor.chain().focus().toggleBold().run()" />
|
|
8
|
-
<Button v-if="config.marks.italic" label="I" class="p-button-sm font-italic"
|
|
9
|
-
:class="{ 'p-button-outlined': !editor.isActive('italic') }"
|
|
10
|
-
@click="editor.chain().focus().toggleItalic().run()" />
|
|
11
|
-
<Button v-if="config.marks.underline" label="U" class="p-button-sm underline"
|
|
12
|
-
:class="{ 'p-button-outlined': !editor.isActive('underline') }"
|
|
13
|
-
@click="editor.chain().focus().toggleUnderline().run()" />
|
|
14
|
-
<Button v-if="config.marks.strike" label="S" class="p-button-sm line-through"
|
|
15
|
-
:class="{ 'p-button-outlined': !editor.isActive('strike') }"
|
|
16
|
-
@click="editor.chain().focus().toggleStrike().run()" />
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<div class="p-buttonset mr-1 mb-1">
|
|
20
|
-
<Button v-if="config.nodes.bulletList" icon="fa-solid fa-list-ul" class="p-button-sm"
|
|
21
|
-
:class="{ 'p-button-outlined': !editor.isActive('bulletlist') }"
|
|
22
|
-
@click="editor.chain().focus().toggleBulletList().run()" />
|
|
23
|
-
<Button v-if="config.nodes.orderedList" icon="fa-solid fa-list-ol" class="p-button-sm"
|
|
24
|
-
:class="{ 'p-button-outlined': !editor.isActive('orderedlist') }"
|
|
25
|
-
@click="editor.chain().focus().toggleOrderedList().run()" />
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div v-if="config.nodes.heading" class="p-buttonset mr-1 mb-1">
|
|
29
|
-
<Button label="H1" class="p-button-sm"
|
|
30
|
-
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 1 }) }"
|
|
31
|
-
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()" />
|
|
32
|
-
<Button label="H2" class="p-button-sm"
|
|
33
|
-
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 2 }) }"
|
|
34
|
-
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()" />
|
|
35
|
-
<Button label="H3" class="p-button-sm"
|
|
36
|
-
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 3 }) }"
|
|
37
|
-
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()" />
|
|
38
|
-
<Button label="H4" class="p-button-sm"
|
|
39
|
-
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 4 }) }"
|
|
40
|
-
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()" />
|
|
41
|
-
<Button label="H5" class="p-button-sm"
|
|
42
|
-
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 5 }) }"
|
|
43
|
-
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()" />
|
|
44
|
-
<Button label="H6" class="p-button-sm"
|
|
45
|
-
:class="{ 'p-button-outlined': !editor.isActive('heading', { level: 6 }) }"
|
|
46
|
-
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()" />
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
<div class="p-buttonset mr-1 mb-1">
|
|
50
|
-
<Button v-if="config.nodes.blockquote" icon="fa-solid fa-quote-left" class="p-button-sm"
|
|
51
|
-
:class="{ 'p-button-outlined': !editor.isActive('blockquote') }"
|
|
52
|
-
@click="editor.chain().focus().toggleBlockquote().run()" />
|
|
53
|
-
<Button v-if="config.nodes.codeBlock" icon="fa-solid fa-code" class="p-button-sm"
|
|
54
|
-
:class="{ 'p-button-outlined': !editor.isActive('codeblock') }"
|
|
55
|
-
@click="editor.chain().focus().toggleCodeBlock().run()" />
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
<div class="p-buttonset mr-1 mb-1">
|
|
59
|
-
<Button v-if="config.nodes.hardBreak" label="BR" class="p-button-sm"
|
|
60
|
-
:class="{ 'p-button-outlined': true }"
|
|
61
|
-
@click="editor.chain().focus().setHardBreak().run()" />
|
|
62
|
-
<Button v-if="config.nodes.horizontalRule" label="HR" class="p-button-sm"
|
|
63
|
-
:class="{ 'p-button-outlined': true }"
|
|
64
|
-
@click="editor.chain().focus().setHorizontalRule().run()" />
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<div class="p-buttonset mr-1 mb-1">
|
|
68
|
-
<!-- <Button v-if="config.nodes.hardBreak" icon="pi pi-image" class="p-button-sm"
|
|
69
|
-
:class="{ 'p-button-outlined': true }"
|
|
70
|
-
@click="editor.chain().focus().setImage().run()" />-->
|
|
71
|
-
<FileInput @input="handleImageUpload" class="inline-block"
|
|
72
|
-
accept="image/png,image/jpeg,image/webp,.jpg,.jpeg,.png,.webp">
|
|
73
|
-
<Button v-if="config.nodes.hardBreak" icon="pi pi-image" class="p-button-sm"
|
|
74
|
-
:class="{ 'p-button-outlined': true }" />
|
|
75
|
-
</FileInput>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
</div>
|
|
79
|
-
</template>
|
|
80
|
-
|
|
81
|
-
<script setup>
|
|
82
|
-
import Button from "primevue/button"
|
|
83
|
-
|
|
84
|
-
import { inject, getCurrentInstance } from "vue"
|
|
85
|
-
import { toRefs } from "@vueuse/core"
|
|
86
|
-
import { uploadImage } from "@live-change/image-frontend"
|
|
87
|
-
import { FileInput } from "@live-change/upload-frontend"
|
|
88
|
-
|
|
89
|
-
const appContext = getCurrentInstance().appContext
|
|
90
|
-
const workingZone = inject('workingZone')
|
|
91
|
-
|
|
92
|
-
const props = defineProps({
|
|
93
|
-
editor: {
|
|
94
|
-
type: Object,
|
|
95
|
-
required: true
|
|
96
|
-
},
|
|
97
|
-
config: {
|
|
98
|
-
type: Object,
|
|
99
|
-
required: true
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const { editor, config } = toRefs(props)
|
|
104
|
-
|
|
105
|
-
async function handleImageUpload(file) {
|
|
106
|
-
const upload = uploadImage('test', { file },
|
|
107
|
-
{ preparedPreview: true, appContext, generateId: true })
|
|
108
|
-
await workingZone.addPromise('disconnectContact', (async () => {
|
|
109
|
-
editor.value.chain().focus().setImage({ image: upload.id }).run()
|
|
110
|
-
await upload.prepare()
|
|
111
|
-
})())
|
|
112
|
-
console.log("START UPLOAD!")
|
|
113
|
-
await upload.upload()
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
window.editor = editor.value
|
|
117
|
-
</script>
|
|
118
|
-
|
|
119
|
-
<style scoped>
|
|
120
|
-
.p-buttonset .p-button:not(:last-child) { /* buttonset bug fix */
|
|
121
|
-
border-right: 0 none !important;
|
|
122
|
-
}
|
|
123
|
-
</style>
|