зеркало из https://github.com/nextcloud/text.git
Try to pull in tiptap
Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Родитель
bd0818afd9
Коммит
6016c09079
|
@ -35,6 +35,8 @@
|
|||
"prosemirror-schema-list": "^1.0.3",
|
||||
"prosemirror-state": "^1.2.3",
|
||||
"prosemirror-view": "^1.9.6",
|
||||
"tiptap": "^1.19.2",
|
||||
"tiptap-extensions": "^1.19.3",
|
||||
"uuid": "^3.3.2",
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.6.10",
|
||||
|
|
|
@ -35,7 +35,97 @@
|
|||
</p>
|
||||
</div>
|
||||
<div id="editor-wrapper" :class="{'has-conflicts': hasSyncCollission, 'icon-loading': !initialLoading}">
|
||||
<div v-once id="editor" ref="editor" />
|
||||
<div id="editor">
|
||||
<editor-menu-bar :editor="tiptap" v-slot="{ commands, isActive }">
|
||||
<div class="menubar">
|
||||
<Actions>
|
||||
<ActionButton
|
||||
icon="icon-info"
|
||||
:class="{ 'is-active': isActive.bold() }"
|
||||
@click="commands.bold">
|
||||
B </ActionButton>
|
||||
</Actions>
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.bold() }"
|
||||
@click="commands.bold"
|
||||
>
|
||||
B
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.italic() }"
|
||||
@click="commands.italic"
|
||||
>
|
||||
I
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.heading({ level: 1 }) }"
|
||||
@click="commands.heading({ level: 1 })"
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.heading({ level: 2 }) }"
|
||||
@click="commands.heading({ level: 2 })"
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.heading({ level: 3 }) }"
|
||||
@click="commands.heading({ level: 3 })"
|
||||
>
|
||||
H3
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.bullet_list() }"
|
||||
@click="commands.bullet_list"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.ordered_list() }"
|
||||
@click="commands.ordered_list"
|
||||
>
|
||||
1.
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.blockquote() }"
|
||||
@click="commands.blockquote"
|
||||
>
|
||||
"
|
||||
</button>
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.code() }"
|
||||
@click="commands.code"
|
||||
>
|
||||
{}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="menubar__button"
|
||||
:class="{ 'is-active': isActive.paragraph() }"
|
||||
@click="commands.paragraph"
|
||||
>
|
||||
P
|
||||
</button>
|
||||
</div>
|
||||
</editor-menu-bar>
|
||||
<editor-content class="editor__content" :editor="tiptap" />
|
||||
</div>
|
||||
<read-only-editor v-if="hasSyncCollission" :content="syncError.data.outsideChange" />
|
||||
</div>
|
||||
<div v-if="hasSyncCollission" id="resolve-conflicts">
|
||||
|
@ -50,22 +140,43 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
import { EditorSync, ERROR_TYPE } from './../EditorSync'
|
||||
import SyncService from './../services/SyncService'
|
||||
import { endpointUrl } from './../helpers'
|
||||
|
||||
import {collab, getVersion, receiveTransaction, sendableSteps} from 'prosemirror-collab'
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { exampleSetup } from 'prosemirror-example-setup'
|
||||
import { schema, defaultMarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { getVersion } from 'prosemirror-collab'
|
||||
import { defaultMarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown'
|
||||
|
||||
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
|
||||
import {
|
||||
HardBreak,
|
||||
Heading,
|
||||
Bold,
|
||||
Code,
|
||||
Italic,
|
||||
Strike,
|
||||
Link,
|
||||
Underline,
|
||||
BulletList,
|
||||
OrderedList,
|
||||
ListItem,
|
||||
Blockquote,
|
||||
CodeBlock,
|
||||
History,
|
||||
Collaboration,
|
||||
} from 'tiptap-extensions'
|
||||
import { Keymap } from './../extensions'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
|
||||
import Avatar from 'nextcloud-vue/dist/Components/Avatar'
|
||||
import ReadOnlyEditor from './ReadOnlyEditor'
|
||||
import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip'
|
||||
import Actions from 'nextcloud-vue/dist/Components/Actions'
|
||||
import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
|
||||
|
||||
const COLLABORATOR_IDLE_TIME = 5
|
||||
const COLLABORATOR_DISCONNECT_TIME = 20
|
||||
|
@ -74,8 +185,11 @@ const EDITOR_PUSH_DEBOUNCE = 200
|
|||
export default {
|
||||
name: 'Editor',
|
||||
components: {
|
||||
Avatar,
|
||||
ReadOnlyEditor
|
||||
Avatar,Actions,
|
||||
ReadOnlyEditor,
|
||||
EditorContent,
|
||||
EditorMenuBar,
|
||||
ActionButton
|
||||
},
|
||||
directives: {
|
||||
Tooltip
|
||||
|
@ -100,6 +214,8 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
tiptap: null,
|
||||
/** @type EditorSync */
|
||||
authority: null,
|
||||
/** @type SyncService */
|
||||
|
@ -128,6 +244,24 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
filteredSessions() {
|
||||
let filteredSessions = {}
|
||||
for (let index in this.sessions) {
|
||||
let session = this.sessions[index]
|
||||
if (!session.userId) {
|
||||
session.userId = session.id
|
||||
}
|
||||
if (this.filteredSessions.hasOwnProperty(session.userId)) {
|
||||
if (filteredSessions[session.userId].lastContact < session.lastContact) {
|
||||
filteredSessions[session.userId] = session
|
||||
}
|
||||
} else {
|
||||
filteredSessions[session.userId] = session
|
||||
}
|
||||
}
|
||||
return filteredSessions
|
||||
},
|
||||
|
||||
lastSavedStatus() {
|
||||
return (this.hasUnsavedChanges || this.hasUnpushedChanges ? '*' : '') + this.lastSavedString
|
||||
},
|
||||
|
@ -154,7 +288,7 @@ export default {
|
|||
return this.dirty
|
||||
},
|
||||
hasUnsavedChanges() {
|
||||
return this.authority && this.document.lastSavedVersion !== getVersion(this.authority.view.state)
|
||||
return this.authority && this.document.lastSavedVersion !== getVersion(this.tiptap.state)
|
||||
},
|
||||
backendUrl() {
|
||||
return (endpoint) => {
|
||||
|
@ -176,7 +310,6 @@ export default {
|
|||
this.currentSession = null
|
||||
this.syncService.close()
|
||||
this.syncService = null
|
||||
this.view.destroy()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -192,7 +325,6 @@ export default {
|
|||
}
|
||||
this.syncService = new SyncService({
|
||||
shareToken: this.shareToken,
|
||||
schema: schema,
|
||||
serialize: (document) => {
|
||||
return defaultMarkdownSerializer.serialize(document)
|
||||
}
|
||||
|
@ -203,21 +335,7 @@ export default {
|
|||
})
|
||||
.on('change', ({document, sessions}) => {
|
||||
this.sessions = sessions.sort((a,b) => b.lastContact - a.lastContact)
|
||||
this.filteredSessions = {}
|
||||
for (let index in this.sessions) {
|
||||
let session = this.sessions[index]
|
||||
if (!session.userId) {
|
||||
session.userId = session.id
|
||||
}
|
||||
if (this.filteredSessions.hasOwnProperty(session.userId)) {
|
||||
if (this.filteredSessions[session.userId].lastContact < session.lastContact) {
|
||||
this.filteredSessions[session.userId] = session
|
||||
}
|
||||
} else {
|
||||
this.filteredSessions[session.userId] = session
|
||||
}
|
||||
}
|
||||
console.log(this.filteredSessions)
|
||||
|
||||
|
||||
this.document = document
|
||||
})
|
||||
|
@ -228,42 +346,58 @@ export default {
|
|||
const sendStepsDebounce = () => this.syncService.sendSteps()
|
||||
const sendStepsDebounced = debounce(sendStepsDebounce, EDITOR_PUSH_DEBOUNCE, { maxWait: 5000 })
|
||||
|
||||
this.view = new EditorView(this.$refs.editor, {
|
||||
state: EditorState.create({
|
||||
doc: initialDocument,
|
||||
plugins: [
|
||||
keymap({
|
||||
'Ctrl-s': () => {
|
||||
//authority.manualSave()
|
||||
//authority.fetchSteps()
|
||||
return true
|
||||
}
|
||||
}),
|
||||
...exampleSetup({ schema, floatingMenu: false }),
|
||||
collab({
|
||||
version: this.syncService.steps.length,
|
||||
clientID: this.currentSession.id
|
||||
})
|
||||
]
|
||||
}),
|
||||
dispatchTransaction: (transaction) => {
|
||||
const state = this.view.state.apply(transaction)
|
||||
this.view.updateState(state)
|
||||
/** tiptap */
|
||||
this.markdownit = MarkdownIt('commonmark', {html: false});
|
||||
this.tiptap = new Editor({
|
||||
content: "<p>Hello world</p>", //this.markdownit.render(documentSource),
|
||||
onUpdate: ({state}) => {
|
||||
console.log(defaultMarkdownSerializer.serialize(state.doc))
|
||||
this.syncService.state = state
|
||||
sendStepsDebounced()
|
||||
}
|
||||
|
||||
},
|
||||
extensions: [
|
||||
new HardBreak(),
|
||||
new Heading({ levels: [1, 2, 3] }),
|
||||
new Bold(),
|
||||
new Underline,
|
||||
new Strike,
|
||||
new Code(),
|
||||
new Italic(),
|
||||
new BulletList(),
|
||||
new OrderedList(),
|
||||
new Blockquote(),
|
||||
new CodeBlock(),
|
||||
new ListItem,
|
||||
new Link,
|
||||
new History(),
|
||||
new Collaboration({
|
||||
// the initial version we start with
|
||||
// version is an integer which is incremented with every change
|
||||
version: this.syncService.steps.length,
|
||||
clientID: this.currentSession.id,
|
||||
// debounce changes so we can save some bandwidth
|
||||
debounce: 250,
|
||||
onSendable: ({ sendable }) => {
|
||||
// This is not working properly with polling and the careful retry logic
|
||||
this.syncService.sendSteps(sendable)
|
||||
}
|
||||
}),
|
||||
new Keymap({
|
||||
'Ctrl-s': () => {
|
||||
this.syncService.save()
|
||||
console.log('save', this);
|
||||
return true;
|
||||
}
|
||||
})
|
||||
],
|
||||
})
|
||||
this.syncService.state = this.view.state
|
||||
this.syncService.state = this.tiptap.state
|
||||
this.$emit('update:loaded', true)
|
||||
})
|
||||
.on('sync', (steps) => {
|
||||
let newData = this.syncService.stepsSince(getVersion(this.view.state))
|
||||
this.view.dispatch(
|
||||
receiveTransaction(this.view.state, newData.steps, newData.clientIDs)
|
||||
)
|
||||
this.syncService.state = this.view.state
|
||||
|
||||
.on('sync', ({steps, document}) => {
|
||||
this.tiptap.extensions.options.collaboration.update({
|
||||
version: document.version,
|
||||
steps: steps
|
||||
})
|
||||
})
|
||||
.on('stateChange', (state) => {
|
||||
if (state.initialLoading && !this.initialLoading) {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { Extension } from 'tiptap'
|
||||
|
||||
export default class Keymap extends Extension {
|
||||
|
||||
get name() {
|
||||
return 'save'
|
||||
}
|
||||
|
||||
keys({ schema }) {
|
||||
return this.options
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import Keymap from './Keymap'
|
||||
|
||||
export {
|
||||
Keymap
|
||||
}
|
|
@ -116,7 +116,7 @@ class PollingBackend {
|
|||
return
|
||||
}
|
||||
|
||||
this._authority._receiveSteps(response.data.steps)
|
||||
this._authority._receiveSteps(response.data)
|
||||
this.lock = false
|
||||
this._forcedSave = false
|
||||
this.resetRefetchTimer()
|
||||
|
@ -137,23 +137,24 @@ class PollingBackend {
|
|||
this._forcedSave = false
|
||||
}
|
||||
|
||||
sendSteps(sendable) {
|
||||
sendSteps(_sendable) {
|
||||
this._authority.emit('stateChange', { dirty: true })
|
||||
if (this.lock) {
|
||||
setTimeout(() => {
|
||||
this._authority.sendSteps(sendable)
|
||||
this._authority.sendSteps(_sendable)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
this.lock = true
|
||||
const authority = this
|
||||
let sendable = (typeof _sendable === 'function') ? _sendable() : _sendable;
|
||||
let steps = sendable.steps
|
||||
axios.post(endpointUrl('session/push', !!this._authority.options.shareToken), {
|
||||
documentId: this._authority.document.id,
|
||||
sessionId: this._authority.session.id,
|
||||
sessionToken: this._authority.session.token,
|
||||
steps: steps.map(s => s.toJSON()) || [],
|
||||
version: this._authority._getVersion(),
|
||||
steps: steps.map(s => s.toJSON ? s.toJSON() : s) || [],
|
||||
version: sendable.version,
|
||||
token: this._authority.options.shareToken
|
||||
}).then((response) => {
|
||||
this._authority.emit('stateChange', { dirty: false })
|
||||
|
@ -165,9 +166,9 @@ class PollingBackend {
|
|||
this.lock = false
|
||||
this._fetchSteps()
|
||||
|
||||
this.carefulRetry(() => {
|
||||
/*this.carefulRetry(() => {
|
||||
this.sendSteps(sendable)
|
||||
})
|
||||
})*/
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ import {getVersion, sendableSteps} from 'prosemirror-collab'
|
|||
|
||||
const defaultOptions = {
|
||||
shareToken: null,
|
||||
schema: null,
|
||||
serialize: (document) => document
|
||||
};
|
||||
|
||||
|
@ -131,8 +130,8 @@ class SyncService {
|
|||
)
|
||||
}
|
||||
|
||||
sendSteps() {
|
||||
let sendable = sendableSteps(this.state)
|
||||
sendSteps(_sendable) {
|
||||
let sendable = _sendable ? _sendable : sendableSteps(this.state)
|
||||
if (!sendable) {
|
||||
return
|
||||
}
|
||||
|
@ -146,15 +145,20 @@ class SyncService {
|
|||
}
|
||||
}
|
||||
|
||||
_receiveSteps(steps) {
|
||||
_receiveSteps({steps, document}) {
|
||||
let newSteps = []
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
let singleSteps = steps[i].data.map(j => Step.fromJSON(this.options.schema, j))
|
||||
let singleSteps = steps[i].data
|
||||
singleSteps.forEach(step => {
|
||||
this.steps.push(step)
|
||||
this.stepClientIDs.push(steps[i].sessionId)
|
||||
newSteps.push({
|
||||
step,
|
||||
clientID: steps[i].sessionId
|
||||
})
|
||||
})
|
||||
}
|
||||
this.emit('sync', steps)
|
||||
this.emit('sync', {steps: newSteps, document})
|
||||
console.log('receivedSteps', 'newVersion', getVersion(this.state))
|
||||
}
|
||||
|
||||
_getVersion() {
|
||||
|
|
Загрузка…
Ссылка в новой задаче