Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/text.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulius Härtl <jus@bitgrid.net>2019-06-11 11:38:01 +0300
committerJulius Härtl <jus@bitgrid.net>2019-06-11 11:40:52 +0300
commit5792c6c0993e4b22b2e39db9ec511502b5e654ff (patch)
tree7c748572b57cfe9086ec3696b4746c090d2a6c21
parent71bb5fbd1f9232f8a6e08dbd925c9c875f402045 (diff)
Fix code style and add missing files
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r--css/prosemirror.scss119
-rw-r--r--src/EditorFactory.js38
-rw-r--r--src/EditorSync.js299
-rw-r--r--src/components/CollisionResolveDialog.vue8
-rw-r--r--src/components/EditorWrapper.vue141
-rw-r--r--src/components/GuestNameDialog.vue67
-rw-r--r--src/components/ReadOnlyEditor.vue8
-rw-r--r--src/extensions/Keymap.js1
-rw-r--r--src/helpers.js1
-rw-r--r--src/main.js2
-rw-r--r--src/marks/index.js4
-rw-r--r--src/public.js1
-rw-r--r--src/services/PollingBackend.js27
-rw-r--r--src/services/SyncService.js30
14 files changed, 321 insertions, 425 deletions
diff --git a/css/prosemirror.scss b/css/prosemirror.scss
new file mode 100644
index 000000000..575c7033d
--- /dev/null
+++ b/css/prosemirror.scss
@@ -0,0 +1,119 @@
+/* Document rendering styles */
+
+.ProseMirror {
+ margin-top: 44px;
+ height: 100%;
+ position: relative;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ -webkit-font-variant-ligatures: none;
+ font-variant-ligatures: none;
+ padding: 4px 8px 4px 14px;
+ line-height: 150%;
+ font-size: 14px;
+ outline: none;
+}
+
+.ProseMirror p:first-child,
+.ProseMirror h1:first-child,
+.ProseMirror h2:first-child,
+.ProseMirror h3:first-child,
+.ProseMirror h4:first-child,
+.ProseMirror h5:first-child,
+.ProseMirror h6:first-child {
+ margin-top: 10px;
+}
+
+.ProseMirror a {
+ color: var(--color-primary);
+ text-decoration: underline;
+}
+
+.ProseMirror p {
+ margin-bottom: 1em;
+ line-height: 150%;
+}
+.ProseMirror em {
+ font-style: italic;
+}
+
+.ProseMirror h1 {
+ font-size: 24px;
+}
+.ProseMirror h2 {
+ font-size: 22px;
+}
+.ProseMirror h3 {
+ font-size: 20px;
+}
+.ProseMirror h4 {
+ font-size: 18px;
+}
+.ProseMirror h5 {
+ font-size: 16px;
+}
+.ProseMirror h6 {
+ font-size: 14px;
+}
+.ProseMirror h1,
+.ProseMirror h2,
+.ProseMirror h3,
+.ProseMirror h4,
+.ProseMirror h5,
+.ProseMirror h6 {
+ font-weight: 600;
+ margin-top: 10px;
+ margin-bottom: 20px;
+}
+
+.ProseMirror img {
+ cursor: default;
+ max-height: 50vh;
+ max-width: 100%;
+}
+
+.ProseMirror-focused .ProseMirror-gapcursor {
+ display: block;
+}
+/* Add space around the hr to make clicking it easier */
+
+.ProseMirror hr {
+ padding: 2px 10px;
+ border: none;
+ margin: 1em 0;
+ width: 100%;
+}
+
+.ProseMirror hr:after {
+ content: "";
+ display: block;
+ height: 1px;
+ background-color: silver;
+ line-height: 2px;
+}
+
+.ProseMirror pre {
+ white-space: pre-wrap;
+ background-color: var(--color-background-dark);
+ border-radius: 5px;
+ padding: 5px;
+ padding-left: 11px;
+}
+
+.ProseMirror li {
+ position: relative;
+}
+
+.ProseMirror ul, .ProseMirror ol {
+ padding-left: 30px;
+}
+
+.ProseMirror ul li {
+ list-style-type: disc;
+}
+
+.ProseMirror blockquote {
+ padding-left: 1em;
+ border-left: 3px solid var(--color-text-lighter);
+ margin-left: 0; margin-right: 0;
+}
diff --git a/src/EditorFactory.js b/src/EditorFactory.js
index 2e2e435fc..871164aac 100644
--- a/src/EditorFactory.js
+++ b/src/EditorFactory.js
@@ -19,11 +19,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
-import {Editor} from 'tiptap';
+import { Editor } from 'tiptap'
import {
HardBreak,
Heading,
- Bold,
Code,
Link,
BulletList,
@@ -37,31 +36,30 @@ import {
import { Strong, Italic } from './marks'
import MarkdownIt from 'markdown-it'
-const createEditor = ({content, onUpdate, extensions}) => {
- extensions = extensions ? extensions : []
+const createEditor = ({ content, onUpdate, extensions }) => {
+ extensions = extensions || []
return new Editor({
content: content,
onUpdate: onUpdate,
extensions: [
- new HardBreak,
- new Heading,
- new Code,
- new Strong,
- new Italic,
- new BulletList,
- new OrderedList,
- new Blockquote,
- new CodeBlock,
- new ListItem,
- new Link,
- new Image,
+ new HardBreak(),
+ new Heading(),
+ new Code(),
+ new Strong(),
+ new Italic(),
+ new BulletList(),
+ new OrderedList(),
+ new Blockquote(),
+ new CodeBlock(),
+ new ListItem(),
+ new Link(),
+ new Image(),
new History()
- ].concat(extensions),
+ ].concat(extensions)
})
}
-const markdownit = MarkdownIt('commonmark', {html: false});
-
+const markdownit = MarkdownIt('commonmark', { html: false })
export default createEditor
-export { markdownit, createEditor}
+export { markdownit, createEditor }
diff --git a/src/EditorSync.js b/src/EditorSync.js
deleted file mode 100644
index 69ea9bc5f..000000000
--- a/src/EditorSync.js
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * @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 axios from 'nextcloud-axios'
-import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown'
-import { receiveTransaction, sendableSteps, getVersion } from 'prosemirror-collab'
-import { Step } from 'prosemirror-transform'
-import { endpointUrl } from './helpers'
-
-/**
- * Minimum inverval to refetch the document changes
- * @type {number}
- */
-const FETCH_INTERVAL = 200
-
-/**
- * Maximum interval between refetches of document state if multiple users have joined
- * @type {number}
- */
-const FETCH_INTERVAL_MAX = 2000
-
-/**
- * Interval to check for changes when there is only one user joined
- * @type {number}
- */
-const FETCH_INTERVAL_SINGLE_EDITOR = 5000
-
-const MIN_PUSH_RETRY = 500
-const MAX_PUSH_RETRY = 10000
-
-/* Timeout after that a PUSH_FAILURE error is emitted */
-const WARNING_PUSH_RETRY = 5000
-
-/* Timeout for sessions to be marked as disconnected */
-const COLLABORATOR_DISCONNECT_TIME = 20
-
-const ERROR_TYPE = {
- /**
- * Failed to save collaborative document due to external change
- * collission needs to be resolved manually
- */
- SAVE_COLLISSION: 0,
- /**
- * Failed to push changes for MAX_REBASE_RETRY times
- */
- PUSH_FAILURE: 1
-}
-
-
-class EditorSync {
-
- constructor(data, shareToken) {
- this.view = null
- this.session = data.session
- this.document = data.document
- this.steps = []
- this.stepClientIDs = []
- this.lock = false
- this.retryTime = MIN_PUSH_RETRY
- this.dirty = false
- this.fetchInverval = FETCH_INTERVAL
- this.shareToken = shareToken
-
- this.onSyncHandlers = []
- this.onErrorHandlers = []
- this.onStateChangeHandlers = []
-
- // example for polling
- // the interval will be adjusted dynamically depending on the time without any change
- this.fetcher = setInterval(() => this.fetchSteps(), this.fetchInverval)
- }
-
- isPublic() {
- return !!this.shareToken
- }
-
- destroy() {
- clearInterval(this.fetcher)
- }
-
- onSync(handler) {
- this.onSyncHandlers.push(handler)
- }
-
- onStateChange(handler) {
- this.onStateChangeHandlers.push(handler)
- }
-
- triggerStateChange() {
- this.onStateChangeHandlers.forEach((handler) => handler())
- }
-
- onError(handler) {
- this.onErrorHandlers.push(handler)
- }
-
- content() {
- return defaultMarkdownSerializer.serialize(this.view.state.doc)
- }
-
- forceSave() {
- this._forcedSave = true
- }
-
- manualSave() {
- this._manualSave = true
- }
-
- fetchSteps() {
- if (this.lock) {
- return
- }
- this.lock = true
- this.triggerStateChange()
- const authority = this
- let autosaveContent
- if (
- this._forcedSave || this._manualSave
- || (!sendableSteps(this.view.state) && (authority.steps.length > this.document.lastSavedVersion))
- ) {
- autosaveContent = this.content()
- }
- axios.post(endpointUrl('session/sync', this.isPublic()), {
- documentId: this.document.id,
- sessionId: this.session.id,
- sessionToken: this.session.token,
- version: authority.steps.length,
- autosaveContent,
- force: !!this._forcedSave,
- manualSave: !!this._manualSave,
- token: this.shareToken
- }).then((response) => {
- if (this.document.lastSavedVersion < response.data.document.lastSavedVersion) {
- console.debug('Saved document', response.data.document)
- this.document = response.data.document
- }
- if (!this.view.props.editable) {
- this.view.setProps({ editable: () => true })
- }
-
- this.onSyncHandlers.forEach((handler) => handler(response.data))
-
- if (response.data.steps.length === 0) {
- this.lock = false
- if (response.data.sessions.filter((session) => session.lastContact > Date.now() / 1000 - COLLABORATOR_DISCONNECT_TIME).length < 2) {
- this.maximumRefetchTimer()
- } else {
- this.increaseRefetchTimer()
- }
- return
- }
-
- for (let i = 0; i < response.data.steps.length; i++) {
- let steps = response.data.steps[i].data.map(j => Step.fromJSON(schema, j))
- steps.forEach(step => {
- authority.steps.push(step)
- authority.stepClientIDs.push(response.data.steps[i].sessionId)
- })
- }
- let newData = authority.stepsSince(getVersion(authority.view.state))
- /*authority.view.dispatch(
- receiveTransaction(authority.view.state, newData.steps, newData.clientIDs)
- )*/
- console.debug('Synced new steps, current version is ' + getVersion(authority.view.state))
- this.lock = false
- this._forcedSave = false
- // this.sendSteps()
- this.resetRefetchTimer()
- }).catch((e) => {
- this.lock = false
- // this.sendSteps()
- if (e.response.status === 409) {
- console.error('Conflict during file save, please resolve')
- this.view.setProps({ editable: () => false })
- // TODO recover
- this.onErrorHandlers.forEach((handler) => handler(ERROR_TYPE.SAVE_COLLISSION, {
- outsideChange: e.response.data.outsideChange
- }))
- }
- })
- this._manualSave = false
- this._forcedSave = false
- }
-
- resetRefetchTimer() {
- this.fetchInverval = FETCH_INTERVAL
- clearInterval(this.fetcher)
- this.fetcher = setInterval(() => this.fetchSteps(), this.fetchInverval)
-
- }
-
- increaseRefetchTimer() {
- this.fetchInverval = Math.min(this.fetchInverval + 100, FETCH_INTERVAL_MAX)
- clearInterval(this.fetcher)
- this.fetcher = setInterval(() => this.fetchSteps(), this.fetchInverval)
- }
-
- maximumRefetchTimer() {
- this.fetchInverval = FETCH_INTERVAL_SINGLE_EDITOR
- clearInterval(this.fetcher)
- this.fetcher = setInterval(() => this.fetchSteps(), this.fetchInverval)
- }
-
- stepsSince(version) {
- return {
- steps: this.steps.slice(version),
- clientIDs: this.stepClientIDs.slice(version)
- }
- }
-
- carefulRetry(callback) {
- let newRetry = this.retryTime ? Math.min(this.retryTime * 2, MAX_PUSH_RETRY) : MIN_PUSH_RETRY
- if (newRetry > WARNING_PUSH_RETRY && this.retryTime < WARNING_PUSH_RETRY) {
- OC.Notification.showTemporary('Changes could not be sent yet')
- this.view.setProps({ editable: () => false })
- this.onErrorHandlers.forEach((handler) => handler(ERROR_TYPE.PUSH_FAILURE, {}))
- // TODO recover
- }
- this.retryTime = newRetry
- setTimeout(callback, this.retryTime)
- }
-
- carefulRetryReset() {
- this.retryTime = MIN_PUSH_RETRY
- }
-
- sendSteps() {
- let sendable = sendableSteps(this.view.state)
- if (!sendable) {
- this.dirty = false
- this.triggerStateChange()
- return
- }
- this.dirty = true
- this.triggerStateChange()
- if (this.lock) {
- setTimeout(() => {
- this.sendSteps()
- }, 500)
- return
- }
- this.lock = true
- const authority = this
- let steps = sendable.steps
- axios.post(endpointUrl('session/push', this.isPublic()), {
- documentId: this.document.id,
- sessionId: this.session.id,
- sessionToken: this.session.token,
- steps: steps.map(s => s.toJSON()) || [],
- version: getVersion(authority.view.state),
- token: this.shareToken
- }).then((response) => {
- // sucessfully applied steps on the server
- /*steps.forEach(step => {
- authority.steps.push(step)
- authority.stepClientIDs.push(this.session.id)
- })
- let newData = authority.stepsSince(getVersion(authority.view.state))
- authority.view.dispatch(
- receiveTransaction(authority.view.state, newData.steps, newData.clientIDs)
- )*/
- this.carefulRetryReset()
- this.lock = false
- this.fetchSteps()
- }).catch((e) => {
- console.error('failed to apply steps due to collission, retrying')
- // TODO: increase retry counter to check against MAX_REBASE_RETRY
- this.lock = false
- // TODO: remove if we have state machine
- this.fetchSteps()
-
- this.carefulRetry(() => {
- this.sendSteps()
- })
- })
- }
-
-}
-
-export { EditorSync, ERROR_TYPE, endpointUrl }
diff --git a/src/components/CollisionResolveDialog.vue b/src/components/CollisionResolveDialog.vue
index a64d09a51..d3ee725ed 100644
--- a/src/components/CollisionResolveDialog.vue
+++ b/src/components/CollisionResolveDialog.vue
@@ -21,7 +21,7 @@
-->
<template>
- <div class="collision-resolve-dialog" id="resolve-conflicts">
+ <div id="resolve-conflicts" class="collision-resolve-dialog">
<button @click="$emit('resolveUseThisVersion')">
Use your version
</button>
@@ -32,9 +32,9 @@
</template>
<script>
- export default {
- name: 'CollisionResolveDialog'
- }
+export default {
+ name: 'CollisionResolveDialog'
+}
</script>
<style scoped lang="scss">
diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue
index ca694a8fc..478463e3f 100644
--- a/src/components/EditorWrapper.vue
+++ b/src/components/EditorWrapper.vue
@@ -22,14 +22,14 @@
<template>
<div id="editor-container">
- <div id="editor-session-list" v-if="currentSession && active">
+ <div v-if="currentSession && active" id="editor-session-list">
<div v-tooltip="lastSavedStatusTooltip" class="save-status" :class="lastSavedStatusClass">
{{ lastSavedStatus }}
</div>
<avatar v-for="session in activeSessions" :key="session.id"
- :user="session.userId"
- :display-name="session.guestName ? session.guestName : session.displayName"
- :style="sessionStyle(session)" />
+ :user="session.userId"
+ :display-name="session.guestName ? session.guestName : session.displayName"
+ :style="sessionStyle(session)" />
</div>
<div v-if="currentSession && active">
<p v-if="hasSyncCollission" class="msg icon-error">
@@ -38,45 +38,62 @@
</div>
<div v-if="currentSession && active" id="editor-wrapper" :class="{'has-conflicts': hasSyncCollission, 'icon-loading': !initialLoading}">
<div id="editor">
- <editor-menu-bar :editor="tiptap" v-slot="{ commands, isActive }" v-if="!syncError && !readOnly">
+ <editor-menu-bar v-if="!syncError && !readOnly" v-slot="{ commands, isActive }" :editor="tiptap">
<div class="menubar">
- <button class="icon-bold" :class="{ 'is-active': isActive.strong() }" @click="commands.strong"></button>
- <button class="icon-italic" :class="{ 'is-active': isActive.em() }" @click="commands.em"></button>
- <button class="icon-code" :class="{ 'is-active': isActive.code() }" @click="commands.code"></button>
-
- <button :class="{ 'is-active': isActive.heading({ level: 1 }) }" @click="commands.heading({ level: 1 })">H1</button>
- <button :class="{ 'is-active': isActive.heading({ level: 2 }) }" @click="commands.heading({ level: 2 })">H2</button>
- <button :class="{ 'is-active': isActive.heading({ level: 3 }) }" @click="commands.heading({ level: 3 })">H3</button>
- <Actions>
- <ActionButton icon="icon-paragraph" @click="commands.heading({ level: 4 })">Heading 4</ActionButton>
- <ActionButton icon="icon-paragraph" @click="commands.heading({ level: 5 })">Heading 5</ActionButton>
- <ActionButton icon="icon-paragraph" @click="commands.heading({ level: 6 })">Heading 6</ActionButton>
- <ActionButton icon="icon-code" @click="commands.code_block()">Code block</ActionButton>
- <ActionButton icon="icon-quote" @click="commands.blockquote()">Blockquote</ActionButton>
- </Actions>
-
- <button class="icon-ul" :class="{ 'is-active': isActive.bullet_list() }" @click="commands.bullet_list"></button>
- <button class="icon-ol" :class="{ 'is-active': isActive.ordered_list() }" @click="commands.ordered_list"></button>
-
- <button v-if="!isPublic" class="icon-image" @click="showImagePrompt(commands.image)"></button>
+ <button class="icon-bold" :class="{ 'is-active': isActive.strong() }" @click="commands.strong" />
+ <button class="icon-italic" :class="{ 'is-active': isActive.em() }" @click="commands.em" />
+ <button class="icon-code" :class="{ 'is-active': isActive.code() }" @click="commands.code" />
+
+ <button :class="{ 'is-active': isActive.heading({ level: 1 }) }" @click="commands.heading({ level: 1 })">
+ H1
+ </button>
+ <button :class="{ 'is-active': isActive.heading({ level: 2 }) }" @click="commands.heading({ level: 2 })">
+ H2
+ </button>
+ <button :class="{ 'is-active': isActive.heading({ level: 3 }) }" @click="commands.heading({ level: 3 })">
+ H3
+ </button>
+ <actions>
+ <action-button icon="icon-paragraph" @click="commands.heading({ level: 4 })">
+ Heading 4
+ </action-button>
+ <action-button icon="icon-paragraph" @click="commands.heading({ level: 5 })">
+ Heading 5
+ </action-button>
+ <action-button icon="icon-paragraph" @click="commands.heading({ level: 6 })">
+ Heading 6
+ </action-button>
+ <action-button icon="icon-code" @click="commands.code_block()">
+ Code block
+ </action-button>
+ <action-button icon="icon-quote" @click="commands.blockquote()">
+ Blockquote
+ </action-button>
+ </actions>
+
+ <button class="icon-ul" :class="{ 'is-active': isActive.bullet_list() }" @click="commands.bullet_list" />
+ <button class="icon-ol" :class="{ 'is-active': isActive.ordered_list() }" @click="commands.ordered_list" />
+
+ <button v-if="!isPublic" class="icon-image" @click="showImagePrompt(commands.image)" />
</div>
</editor-menu-bar>
- <editor-menu-bubble v-if="!readOnly" class="menububble" :editor="tiptap" @hide="hideLinkMenu" v-slot="{ commands, isActive, getMarkAttrs, menu }">
+ <editor-menu-bubble v-if="!readOnly" v-slot="{ commands, isActive, getMarkAttrs, menu }" class="menububble"
+ :editor="tiptap" @hide="hideLinkMenu">
<div class="menububble" :class="{ 'is-active': menu.isActive }" :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`">
-
- <form class="menububble__form" v-if="linkMenuIsActive" @submit.prevent="setLinkUrl(commands.link, linkUrl)">
- <input class="menububble__input" type="text" v-model="linkUrl" placeholder="https://" ref="linkInput" @keydown.esc="hideLinkMenu" />
- <button class="menububble__button" @click="setLinkUrl(commands.link, null)" type="button"></button>
+ <form v-if="linkMenuIsActive" class="menububble__form" @submit.prevent="setLinkUrl(commands.link, linkUrl)">
+ <input ref="linkInput" v-model="linkUrl" class="menububble__input"
+ type="text" placeholder="https://" @keydown.esc="hideLinkMenu">
+ <button class="menububble__button" type="button" @click="setLinkUrl(commands.link, null)" />
</form>
<template v-else>
<button
- class="menububble__button"
- @click="showLinkMenu(getMarkAttrs('link'))"
- :class="{ 'is-active': isActive.link() }"
- ><span>{{ isActive.link() ? 'Update Link' : 'Add Link'}}</span></button>
+ class="menububble__button"
+ :class="{ 'is-active': isActive.link() }"
+ @click="showLinkMenu(getMarkAttrs('link'))">
+ <span>{{ isActive.link() ? 'Update Link' : 'Add Link' }}</span>
+ </button>
</template>
-
</div>
</editor-menu-bubble>
<editor-content class="editor__content" :editor="tiptap" />
@@ -97,9 +114,9 @@ import { SyncService, ERROR_TYPE } from './../services/SyncService'
import { endpointUrl } from './../helpers'
import { createEditor, markdownit } from './../EditorFactory'
-import { defaultMarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown'
+import { defaultMarkdownSerializer } from 'prosemirror-markdown'
-import { Editor, EditorContent, EditorMenuBar, EditorMenuBubble } from 'tiptap'
+import { EditorContent, EditorMenuBar, EditorMenuBubble } from 'tiptap'
import { Collaboration } from 'tiptap-extensions'
import { Keymap } from './../extensions'
import { getVersion } from 'prosemirror-collab'
@@ -111,7 +128,7 @@ import ActionButton from 'nextcloud-vue/dist/Components/ActionButton'
import ReadOnlyEditor from './ReadOnlyEditor'
import GuestNameDialog from './GuestNameDialog'
-import CollisionResolveDialog from './CollisionResolveDialog';
+import CollisionResolveDialog from './CollisionResolveDialog'
const COLLABORATOR_IDLE_TIME = 5
const COLLABORATOR_DISCONNECT_TIME = 20
@@ -173,7 +190,7 @@ export default {
guestNameConfirmed: false,
linkUrl: null,
- linkMenuIsActive: false,
+ linkMenuIsActive: false
}
},
computed: {
@@ -215,7 +232,7 @@ export default {
return this.dirty
},
hasUnsavedChanges() {
- return this.syncService && this.tiptap && this.tiptap.state && this.document.lastSavedVersion !== getVersion(this.tiptap.state)
+ return this.syncService && this.tiptap && this.tiptap.state && this.document.lastSavedVersion !== getVersion(this.tiptap.state)
},
backendUrl() {
return (endpoint) => {
@@ -260,7 +277,7 @@ export default {
this.lastSavedString = window.moment(this.document.lastSavedVersionTime * 1000).fromNow()
}
},
- initSession () {
+ initSession() {
if (!this.hasDocumentParameters) {
this.$emit('error', 'No valid file provided')
return
@@ -272,30 +289,26 @@ export default {
return defaultMarkdownSerializer.serialize(document)
}
})
- .on('opened', ({document, session}) => {
+ .on('opened', ({ document, session }) => {
this.currentSession = session
this.document = document
this.readOnly = document.readOnly
})
- .on('change', ({document, sessions}) => {
+ .on('change', ({ document, sessions }) => {
if (this.document.baseVersionEtag !== '' && document.baseVersionEtag !== this.document.baseVersionEtag) {
this.resolveUseServerVersion()
return
}
- this.updateSessions.bind(this)(sessions);
+ this.updateSessions.bind(this)(sessions)
this.document = document
this.syncError = null
- this.tiptap.setOptions({editable: !this.readOnly})
+ this.tiptap.setOptions({ editable: !this.readOnly })
})
- .on('loaded', ({document, session, documentSource}) => {
- const documentData = {document, session}
- const initialDocument = defaultMarkdownParser.parse(documentSource)
-
+ .on('loaded', ({ document, session, documentSource }) => {
this.tiptap = createEditor({
content: markdownit.render(documentSource),
- onUpdate: ({state}) => {
- console.log("=> FROM doc")
- console.log(defaultMarkdownSerializer.serialize(state.doc))
+ onUpdate: ({ state }) => {
+ console.debug(defaultMarkdownSerializer.serialize(state.doc))
this.syncService.state = state
},
extensions: [
@@ -305,7 +318,7 @@ export default {
version: this.syncService.steps.length,
clientID: this.currentSession.id,
// debounce changes so we can save some bandwidth
- debounce: 250,
+ debounce: EDITOR_PUSH_DEBOUNCE,
onSendable: ({ sendable }) => {
// This is not working properly with polling and the careful retry logic
this.syncService.sendSteps()
@@ -314,17 +327,16 @@ export default {
new Keymap({
'Ctrl-s': () => {
this.syncService.save()
- console.log('save', this);
- return true;
+ return true
}
})
- ],
+ ]
})
this.syncService.state = this.tiptap.state
this.$emit('update:loaded', true)
this.tiptap.focus('end')
})
- .on('sync', ({steps, document}) => {
+ .on('sync', ({ steps, document }) => {
this.tiptap.extensions.options.collaboration.update({
version: document.currentVersion,
steps: steps
@@ -338,7 +350,7 @@ export default {
type: ERROR_TYPE.SAVE_COLLISSION,
data: data
}
- this.tiptap.setOptions({editable: false})
+ this.tiptap.setOptions({ editable: false })
}
})
@@ -348,12 +360,12 @@ export default {
}
this.dirty = state.dirty
})
- this.syncService.open({ fileId: this.fileId, filePath: this.filePath})
+ this.syncService.open({ fileId: this.fileId, filePath: this.filePath })
},
resolveUseThisVersion() {
this.syncService.forceSave()
- this.tiptap.setOptions({editable: true && !this.readOnly})
+ this.tiptap.setOptions({ editable: true && !this.readOnly })
},
resolveUseServerVersion() {
@@ -364,10 +376,8 @@ export default {
},
updateSessions(sessions) {
- this.sessions = sessions.sort((a,b) => b.lastContact - a.lastContact)
+ this.sessions = sessions.sort((a, b) => b.lastContact - a.lastContact)
let currentSessionIds = this.sessions.map((session) => session.userId)
- const stillExistingSessions = Object.keys(this.filteredSessions)
- .filter(sessionId => currentSessionIds.includes(sessionId))
const removedSessions = Object.keys(this.filteredSessions)
.filter(sessionId => !currentSessionIds.includes(sessionId))
@@ -412,13 +422,13 @@ export default {
showImagePrompt(command) {
const _command = command
- OC.dialogs.filepicker('Insert an image', (file) => {
+ OC.dialogs.filepicker('Insert an image', (file) => {
const src = OC.generateUrl('/core/preview.png?') + `file=${file}&x=1024&y=1024&a=true`
_command({ src })
// TODO: check permissions
// TODO: check for available preview
}, false, false)
- },
+ }
}
}
</script>
@@ -519,7 +529,6 @@ export default {
}
}
-
$color-white: #fff;
$color-black: #000;
@@ -577,7 +586,7 @@ export default {
}
.editor__content {
- max-width: 800px;
+ max-width: 500px;
margin: auto;
}
diff --git a/src/components/GuestNameDialog.vue b/src/components/GuestNameDialog.vue
new file mode 100644
index 000000000..e774f7333
--- /dev/null
+++ b/src/components/GuestNameDialog.vue
@@ -0,0 +1,67 @@
+<!--
+ - @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/>.
+ -
+ -->
+
+<template>
+ <div class="guest-name-dialog">
+ <p>{{ t('text', 'Please enter a name to identify you as a public editor:') }}</p>
+ <form @submit.prevent="setGuestName()">
+ <input ref="guestNameField" type="text" :value="value">
+ <input type="submit" class="icon-confirm" value="">
+ </form>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'GuestNameDialog',
+ props: {
+ value: {
+ type: String,
+ default: ''
+ }
+ },
+ methods: {
+ setGuestName() {
+ this.$emit('input', this.$refs.guestNameField.value)
+ }
+ }
+}
+</script>
+
+<style scoped lang="scss">
+ .guest-name-dialog {
+ padding: 30px;
+ text-align: center;
+
+ form {
+ display: flex;
+ width: 100%;
+ max-width: 200px;
+ margin: auto;
+ margin-top: 30px;
+
+ input[type=text] {
+ flex-grow: 1;
+ }
+ }
+ }
+</style>
diff --git a/src/components/ReadOnlyEditor.vue b/src/components/ReadOnlyEditor.vue
index 6272abdef..4440cb8f1 100644
--- a/src/components/ReadOnlyEditor.vue
+++ b/src/components/ReadOnlyEditor.vue
@@ -21,16 +21,16 @@
-->
<template>
- <EditorContent id="read-only-editor" v-if="editor" :editor="editor"></EditorContent>
+ <editor-content v-if="editor" id="read-only-editor" :editor="editor" />
</template>
<script>
import { EditorContent } from 'tiptap'
-import {createEditor, markdownit} from '../EditorFactory'
+import { createEditor, markdownit } from '../EditorFactory'
export default {
name: 'ReadOnlyEditor',
- components: {EditorContent},
+ components: { EditorContent },
props: {
content: {
type: String,
@@ -46,7 +46,7 @@ export default {
this.editor = createEditor({
content: markdownit.render(this.content)
})
- this.editor.setOptions({editable: false})
+ this.editor.setOptions({ editable: false })
},
beforeDestroy() {
this.editor.destroy()
diff --git a/src/extensions/Keymap.js b/src/extensions/Keymap.js
index 7eac49618..f5e74755e 100644
--- a/src/extensions/Keymap.js
+++ b/src/extensions/Keymap.js
@@ -31,4 +31,5 @@ export default class Keymap extends Extension {
keys({ schema }) {
return this.options
}
+
}
diff --git a/src/helpers.js b/src/helpers.js
index c126937a7..f68d18d46 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -41,7 +41,6 @@ const endpointUrl = (endpoint, isPublic = false) => {
return `${_baseUrl}/${endpoint}`
}
-
export {
documentReady,
endpointUrl
diff --git a/src/main.js b/src/main.js
index f1afc053e..f4639a102 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,6 +1,6 @@
import Vue from 'vue'
-import Editor from './components/Editor'
+import Editor from './components/EditorWrapper'
__webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line
__webpack_public_path__ = OC.linkTo('text', 'js/') // eslint-disable-line
diff --git a/src/marks/index.js b/src/marks/index.js
index 3466a0fac..0768b83ba 100644
--- a/src/marks/index.js
+++ b/src/marks/index.js
@@ -28,15 +28,19 @@ import { Bold, Italic as TipTapItalic } from 'tiptap-extensions'
*/
class Strong extends Bold {
+
get name() {
return 'strong'
}
+
}
class Italic extends TipTapItalic {
+
get name() {
return 'em'
}
+
}
/** Strike is currently unsupported by prosemirror-markdown */
diff --git a/src/public.js b/src/public.js
index 313d84788..0cc42fe24 100644
--- a/src/public.js
+++ b/src/public.js
@@ -4,7 +4,6 @@ import { documentReady } from './helpers'
__webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line
__webpack_public_path__ = OC.linkTo('text', 'js') // eslint-disable-line
-console.log(__webpack_public_path__)
Vue.prototype.t = t
Vue.prototype.OCA = OCA
diff --git a/src/services/PollingBackend.js b/src/services/PollingBackend.js
index 6042a68d6..23140a640 100644
--- a/src/services/PollingBackend.js
+++ b/src/services/PollingBackend.js
@@ -21,9 +21,8 @@
*/
import axios from 'nextcloud-axios'
import { endpointUrl } from '../helpers'
-import {ERROR_TYPE} from '../EditorSync'
-import {sendableSteps} from 'prosemirror-collab';
-
+import { ERROR_TYPE } from './SyncService'
+import { sendableSteps } from 'prosemirror-collab'
/**
* Minimum inverval to refetch the document changes
@@ -86,11 +85,10 @@ class PollingBackend {
return
}
this.lock = true
- const authority = this
let autosaveContent
- if (this._forcedSave || this._manualSave ||
- (!sendableSteps(this._authority.state) &&
- (this._authority.steps.length > this._authority.document.lastSavedVersion))
+ if (this._forcedSave || this._manualSave
+ || (!sendableSteps(this._authority.state)
+ && (this._authority.steps.length > this._authority.document.lastSavedVersion))
) {
autosaveContent = this._authority._getContent()
}
@@ -111,7 +109,7 @@ class PollingBackend {
this._authority.document = response.data.document
this._authority.sessions = response.data.sessions
- this._authority.emit('change', {document: this._authority.document, sessions: this._authority.sessions })
+ this._authority.emit('change', { document: this._authority.document, sessions: this._authority.sessions })
if (response.data.steps.length === 0) {
this.lock = false
@@ -120,8 +118,8 @@ class PollingBackend {
} else {
this.increaseRefetchTimer()
}
- this._authority.emit('stateChange', { dirty: false})
- this._authority.emit('stateChange', { initialLoading: true})
+ this._authority.emit('stateChange', { dirty: false })
+ this._authority.emit('stateChange', { initialLoading: true })
return
}
@@ -156,8 +154,7 @@ class PollingBackend {
return
}
this.lock = true
- const authority = this
- let sendable = (typeof _sendable === 'function') ? _sendable() : _sendable;
+ 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,
@@ -176,9 +173,9 @@ class PollingBackend {
this.lock = false
this._fetchSteps()
- /*this.carefulRetry(() => {
+ /* this.carefulRetry(() => {
this.sendSteps(sendable)
- })*/
+ }) */
})
}
@@ -209,7 +206,7 @@ class PollingBackend {
let newRetry = this.retryTime ? Math.min(this.retryTime * 2, MAX_PUSH_RETRY) : MIN_PUSH_RETRY
if (newRetry > WARNING_PUSH_RETRY && this.retryTime < WARNING_PUSH_RETRY) {
OC.Notification.showTemporary('Changes could not be sent yet')
- this._authority.emit('error',ERROR_TYPE.PUSH_FAILURE, {})
+ this._authority.emit('error', ERROR_TYPE.PUSH_FAILURE, {})
}
this.retryTime = newRetry
setTimeout(callback, this.retryTime)
diff --git a/src/services/SyncService.js b/src/services/SyncService.js
index 0207555f2..8d93540b5 100644
--- a/src/services/SyncService.js
+++ b/src/services/SyncService.js
@@ -28,7 +28,7 @@ import { getVersion, sendableSteps } from 'prosemirror-collab'
const defaultOptions = {
shareToken: null,
serialize: (document) => document
-};
+}
const ERROR_TYPE = {
/**
@@ -41,11 +41,12 @@ const ERROR_TYPE = {
*/
PUSH_FAILURE: 1,
- LOAD_ERROR: 2,
+ LOAD_ERROR: 2
}
class SyncService {
- constructor (options) {
+
+ constructor(options) {
this.eventHandlers = {
/* Document state */
opened: [],
@@ -59,7 +60,7 @@ class SyncService {
/* error */
error: [],
/* Events for session and document meta data */
- change: [],
+ change: []
}
this.backend = new PollingBackend(this)
@@ -79,13 +80,13 @@ class SyncService {
return this
}
- open({fileId, filePath}) {
- return this._openDocument({fileId, filePath}).then(() => {
+ open({ fileId, filePath }) {
+ return this._openDocument({ fileId, filePath }).then(() => {
this.emit('opened', {
document: this.document,
- session: this.session,
+ session: this.session
})
- return this._fetchDocument().then(({data}) => {
+ return this._fetchDocument().then(({ data }) => {
this.emit('loaded', {
document: this.document,
session: this.session,
@@ -102,7 +103,7 @@ class SyncService {
})
}
- _openDocument({fileId, filePath}) {
+ _openDocument({ fileId, filePath }) {
return axios.get(endpointUrl('session/create', !!this.options.shareToken), {
params: {
fileId: fileId,
@@ -132,7 +133,7 @@ class SyncService {
}
sendSteps(_sendable) {
- let sendable = _sendable ? _sendable : sendableSteps(this.state)
+ let sendable = _sendable || sendableSteps(this.state)
if (!sendable) {
return
}
@@ -146,7 +147,7 @@ class SyncService {
}
}
- _receiveSteps({steps, document}) {
+ _receiveSteps({ steps, document }) {
let newSteps = []
for (let i = 0; i < steps.length; i++) {
let singleSteps = steps[i].data
@@ -158,9 +159,9 @@ class SyncService {
})
})
}
- console.log(newSteps);
- this.emit('sync', {steps: newSteps, document})
- console.log('receivedSteps', 'newVersion', getVersion(this.state))
+ console.debug(newSteps)
+ this.emit('sync', { steps: newSteps, document })
+ console.debug('receivedSteps', 'newVersion', getVersion(this.state))
}
_getVersion() {
@@ -219,6 +220,7 @@ class SyncService {
console.error('Event not found', event)
}
}
+
}
export default SyncService