diff options
-rw-r--r-- | src/EditorSync.js | 102 | ||||
-rw-r--r-- | src/components/Editor.vue | 423 | ||||
-rw-r--r-- | src/components/ReadOnlyEditor.vue | 74 | ||||
-rw-r--r-- | src/files.js | 33 | ||||
-rw-r--r-- | src/helpers.js | 38 | ||||
-rw-r--r-- | src/main.js | 7 |
6 files changed, 361 insertions, 316 deletions
diff --git a/src/EditorSync.js b/src/EditorSync.js index 8a0dec498..663f7dc04 100644 --- a/src/EditorSync.js +++ b/src/EditorSync.js @@ -21,37 +21,36 @@ */ import axios from 'nextcloud-axios' -import {schema, defaultMarkdownSerializer} from "prosemirror-markdown" -import {receiveTransaction, sendableSteps, getVersion} from 'prosemirror-collab'; -import {Step} from 'prosemirror-transform'; +import { schema, defaultMarkdownSerializer } from 'prosemirror-markdown' +import { receiveTransaction, sendableSteps, getVersion } from 'prosemirror-collab' +import { Step } from 'prosemirror-transform' /** * Minimum inverval to refetch the document changes * @type {number} */ -const FETCH_INTERVAL = 200; +const FETCH_INTERVAL = 200 /** * Maximum interval between refetches of document state if multiple users have joined * @type {number} */ -const FETCH_INTERVAL_MAX = 2000; +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 FETCH_INTERVAL_SINGLE_EDITOR = 5000 -const MIN_PUSH_RETRY = 500; -const MAX_PUSH_RETRY = 10000; -const WARNING_PUSH_RETRY = 2000; -const COLLABORATOR_DISCONNECT_TIME = 20; +const MIN_PUSH_RETRY = 500 +const MAX_PUSH_RETRY = 10000 -/** - * Define how often the editor should retry to apply local changes, before warning the user - */ -const MAX_REBASE_RETRY = 5; +/* 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 = { /** @@ -62,13 +61,14 @@ const ERROR_TYPE = { /** * Failed to push changes for MAX_REBASE_RETRY times */ - PUSH_FAILURE: 1, + PUSH_FAILURE: 1 } const URL_SYNC = OC.generateUrl('/apps/text/session/sync') -const URL_PUSH = OC.generateUrl('/apps/text/session/push'); +const URL_PUSH = OC.generateUrl('/apps/text/session/push') class EditorSync { + constructor(doc, data) { this.view = null this.doc = doc @@ -79,7 +79,7 @@ class EditorSync { this.lock = false this.retryTime = MIN_PUSH_RETRY this.dirty = false - this.fetchInverval = FETCH_INTERVAL; + this.fetchInverval = FETCH_INTERVAL this.onSyncHandlers = [] this.onErrorHandlers = [] @@ -124,15 +124,15 @@ class EditorSync { fetchSteps() { if (this.lock) { - return; + return } - this.lock = true; + this.lock = true this.triggerStateChange() - const authority = this; - let autosaveContent = undefined + const authority = this + let autosaveContent if ( - this._forcedSave || this._manualSave || - (!sendableSteps(this.view.state) && (authority.steps.length > this.document.lastSavedVersion)) + this._forcedSave || this._manualSave + || (!sendableSteps(this.view.state) && (authority.steps.length > this.document.lastSavedVersion)) ) { autosaveContent = this.content() } @@ -149,22 +149,22 @@ class EditorSync { console.debug('Saved document', response.data.document) this.document = response.data.document } - this.view.setProps({editable: () => true}) + 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(); + this.lock = false + if (response.data.sessions.filter((session) => session.lastContact > Date.now() / 1000 - COLLABORATOR_DISCONNECT_TIME).length < 2) { + this.maximumRefetchTimer() } else { - this.increaseRefetchTimer(); + this.increaseRefetchTimer() } - return; + return } for (let i = 0; i < response.data.steps.length; i++) { - let steps = response.data.steps[i].data.map(j => Step.fromJSON(schema, j)); + 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) @@ -174,30 +174,29 @@ class EditorSync { authority.view.dispatch( receiveTransaction(authority.view.state, newData.steps, newData.clientIDs) ) - console.log(getVersion(authority.view.state)) - this.lock = false; - this._forcedSave = false; - //this.sendSteps() - this.resetRefetchTimer(); + 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; - console.log('fetch error sendSteps') - //this.sendSteps() + this.lock = false + // this.sendSteps() if (e.response.status === 409) { - console.log('Conflict during file save, please resolve') - this.view.setProps({editable: () => false}) + 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; + this._manualSave = false + this._forcedSave = false } resetRefetchTimer() { - this.fetchInverval = FETCH_INTERVAL; + this.fetchInverval = FETCH_INTERVAL clearInterval(this.fetcher) this.fetcher = setInterval(() => this.fetchSteps(), this.fetchInverval) @@ -225,8 +224,9 @@ class EditorSync { 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}) + 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 @@ -242,7 +242,7 @@ class EditorSync { if (!sendable) { this.dirty = false this.triggerStateChange() - return; + return } this.dirty = true this.triggerStateChange() @@ -250,7 +250,7 @@ class EditorSync { setTimeout(() => { this.sendSteps() }, 500) - return; + return } this.lock = true const authority = this @@ -274,18 +274,14 @@ class EditorSync { this.carefulRetryReset() this.lock = false this.fetchSteps() - }).catch((e) => - - - { - console.log('failed to apply steps due to collission, retrying'); + }).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(() => { - console.log('carefulRetry sendSteps') this.sendSteps() }) }) diff --git a/src/components/Editor.vue b/src/components/Editor.vue index e63a508a0..16e24a55b 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -21,254 +21,265 @@ --> <template> - <div id="editor-container" v-if="session && active"> + <div v-if="currentSession && active" id="editor-container"> <div id="editor-session-list"> - <div class="save-status" :class="lastSavedStatusClass" v-tooltip="lastSavedStatusTooltip">{{ lastSavedStatus }}</div> - <avatar v-for="session in activeSessions" :key="session.id" :user="session.userId" :displayName="session.displayName" :style="sessionStyle(session)"></avatar> + <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.displayName" :style="sessionStyle(session)" /> </div> <div> - <p class="msg icon-error" v-if="hasSyncCollission"> + <p v-if="hasSyncCollission" class="msg icon-error"> {{ t('text', 'The document has been changed outside of the editor. The changes cannot be applied.') }} </p> </div> <div id="editor-wrapper" :class="{'has-conflicts': hasSyncCollission, 'icon-loading': !initialLoading}"> - <div id="editor" ref="editor" v-once></div> + <div v-once id="editor" ref="editor" /> <read-only-editor v-if="hasSyncCollission" :content="syncError.data.outsideChange" /> </div> - <div id="resolve-conflicts" v-if="hasSyncCollission"> - <button @click="resolveUseThisVersion">Use your version</button> - <button @click="resolveUseServerVersion">Use the server version</button> + <div v-if="hasSyncCollission" id="resolve-conflicts"> + <button @click="resolveUseThisVersion"> + Use your version + </button> + <button @click="resolveUseServerVersion"> + Use the server version + </button> </div> </div> </template>c <script> - const COLLABORATOR_IDLE_TIME = 5; - const COLLABORATOR_DISCONNECT_TIME = 20; - const EDITOR_PUSH_DEBOUNCE = 200; +import axios from 'nextcloud-axios' +import debounce from 'lodash/debounce' - import axios from 'nextcloud-axios' - import debounce from 'lodash/debounce' +import { EditorSync, ERROR_TYPE } from './../EditorSync' +import { collab, getVersion } from 'prosemirror-collab' +import { EditorState } from 'prosemirror-state' +import { EditorView } from 'prosemirror-view' +import { exampleSetup } from 'prosemirror-example-setup' +import { schema, defaultMarkdownParser } from 'prosemirror-markdown' +import { keymap } from 'prosemirror-keymap' - import { EditorSync, ERROR_TYPE } from './../EditorSync' - import {collab, receiveTransaction, sendableSteps, getVersion} 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 Avatar from 'nextcloud-vue/dist/Components/Avatar' +import ReadOnlyEditor from './ReadOnlyEditor' +import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip' - import Avatar from 'nextcloud-vue/dist/Components/Avatar' - import ReadOnlyEditor from './ReadOnlyEditor' - import Tooltip from 'nextcloud-vue/dist/Directives/Tooltip' +const COLLABORATOR_IDLE_TIME = 5 +const COLLABORATOR_DISCONNECT_TIME = 20 +const EDITOR_PUSH_DEBOUNCE = 200 - export default { - name: 'Editor', - components: { - Avatar, - ReadOnlyEditor +export default { + name: 'Editor', + components: { + Avatar, + ReadOnlyEditor + }, + directives: { + Tooltip + }, + props: { + relativePath: { + type: String, + default: null }, - directives: { - Tooltip + fileId: { + type: String, + default: null }, - beforeMount() { - if (this.active || this.shareToken) { - this.initSession() - } - setInterval(() => { this.updateLastSavedStatus() }, 2000) + active: { + type: Boolean, + default: false + }, + shareToken: { + type: String, + default: null + } + }, + data() { + return { + editor: null, + /** @type EditorSync */ + authority: null, + document: null, + currentSession: null, + sessions: [], + name: 'Guest', + dirty: false, + initialLoading: false, + lastSavedString: '', + syncError: null + } + }, + computed: { + activeSessions() { + // TODO: filter out duplicate user ids + return this.sessions.filter((session) => session.lastContact > Date.now() / 1000 - COLLABORATOR_DISCONNECT_TIME) }, - props: { - relativePath: { - default: null - }, - fileId: { - default: null - }, - active: { - default: false - }, - shareToken: { - default: null + sessionStyle() { + return (session) => { + return { + 'opacity': session.lastContact > Date.now() / 1000 - COLLABORATOR_IDLE_TIME ? 1 : 0.5, + 'border-color': session.color + } } }, - data() { - return { - editor: null, - /** @type EditorSync */ - authority: null, - document: null, - session: null, - sessions: [], - name: 'Guest', - dirty: false, - initialLoading: false, - lastSavedString: '', - syncError: null + lastSavedStatus() { + return (this.hasUnsavedChanges || this.hasUnpushedChanges ? '*' : '') + this.lastSavedString + }, + lastSavedStatusClass() { + return this.syncError && this.lastSavedString !== '' ? 'error' : '' + }, + lastSavedStatusTooltip() { + let message = t('text', 'Last save {lastSave}', { lastSave: this.lastSavedString }) + if (this.hasSyncCollission) { + message = t('text', 'The document has been changed outside of the editor. The changes cannot be applied.') + } + if (this.hasUnpushedChanges) { + message += ' - ' + t('text', 'Unpushed changes') + } + if (this.hasUnsavedChanges) { + message += ' - ' + t('text', 'Unsaved changes') } + return { content: message, placement: 'bottom' } }, - computed: { - activeSessions() { - // TODO: filter out duplicate user ids - return this.sessions.filter((session) => session.lastContact > Date.now()/1000-COLLABORATOR_DISCONNECT_TIME) - }, - sessionStyle() { - return (session) => { - return { - 'opacity': session.lastContact > Date.now()/1000-COLLABORATOR_IDLE_TIME ? 1 : 0.5, - 'border-color': session.color - } - } - }, - lastSavedStatus() { - return (this.hasUnsavedChanges || this.hasUnpushedChanges ? '*' : '') + this.lastSavedString - }, - lastSavedStatusClass() { - return this.syncError && this.lastSavedString !== '' ? 'error' : '' - }, - lastSavedStatusTooltip() { - let message = t('text', 'Last save {lastSave}', {lastSave: this.lastSavedString}) - if (this.hasSyncCollission) { - message = t('text', 'The document has been changed outside of the editor. The changes cannot be applied.') - } - if (this.hasUnpushedChanges) { - message += ' - ' + t('text', 'Unpushed changes') - } - if (this.hasUnsavedChanges) { - message += ' - ' + t('text', 'Unsaved changes') - } - return { content: message, placement: 'bottom' } - }, - hasSyncCollission() { - return this.syncError && this.syncError.type === ERROR_TYPE.SAVE_COLLISSION - }, - hasUnpushedChanges() { - return this.dirty - }, - hasUnsavedChanges() { - return this.authority && this.document.lastSavedVersion !== getVersion(this.authority.view.state) + hasSyncCollission() { + return this.syncError && this.syncError.type === ERROR_TYPE.SAVE_COLLISSION + }, + hasUnpushedChanges() { + return this.dirty + }, + hasUnsavedChanges() { + return this.authority && this.document.lastSavedVersion !== getVersion(this.authority.view.state) + } + }, + beforeMount() { + if (this.active || this.shareToken) { + this.initSession() + } + setInterval(() => { this.updateLastSavedStatus() }, 2000) + }, + methods: { + updateLastSavedStatus() { + if (this.document) { + this.lastSavedString = window.moment(this.document.lastSavedVersionTime * 1000).fromNow() } }, - methods: { - updateLastSavedStatus() { - if (this.document) { - this.lastSavedString = moment(this.document.lastSavedVersionTime * 1000).fromNow(); - } - }, - initSession() { - if (!this.relativePath && !this.shareToken) { - console.error('No relative path given') - this.$emit('error', 'No relative path given') - return; + initSession() { + if (!this.relativePath && !this.shareToken) { + console.error('No relative path given') + this.$emit('error', 'No relative path given') + return + } + axios.get(OC.generateUrl('/apps/text/session/create'), { + // TODO: viewer should provide the file id so we can use it in all places (also for public pages) + params: { + file: this.relativePath, + shareToken: this.shareToken } - axios.get(OC.generateUrl('/apps/text/session/create'), { - // TODO: viewer should provide the file id so we can use it in all places (also for public pages) - params: { - file: this.relativePath, - shareToken: this.shareToken + }).then((response) => { + this.document = response.data.document + this.currentSession = response.data.session + axios.get(OC.generateUrl('/apps/text/session/fetch'), + { + params: { + documentId: this.document.id, + sessionId: this.currentSession.id, + token: this.currentSession.token + } } - }).then((response) => { - this.document = response.data.document; - this.session = response.data.session; - axios.get(OC.generateUrl('/apps/text/session/fetch',), - { - params: { - documentId: this.document.id, - sessionId: this.session.id, - token: this.session.token - } + ).then((fileContent) => { + const { authority } = this.initEditor(this.$refs.editor, response.data, fileContent.data) + this.authority = authority + this.authority.onSync((data) => { + this.syncError = null + if (data.document) { + this.document = data.document } - ).then((fileContent) => { - const {editor, authority} = this.initEditor(this.$refs.editor, response.data, fileContent.data); - this.authority = authority - this.authority.onSync((data) => { - this.syncError = null - if (data.document) { - this.document = data.document - } - this.sessions = data.sessions - }) - this.authority.onError((error, data) => { - if (error === ERROR_TYPE.SAVE_COLLISSION && (!this.syncError || this.syncError.type !== ERROR_TYPE.SAVE_COLLISSION)) { - this.syncError = { - type: ERROR_TYPE.SAVE_COLLISSION, - data: data - } - } - }) - this.authority.onStateChange(() => { - this.dirty = this.authority.dirty - if (!this.initialLoading) { - this.initialLoading = !this.authority.dirty && this.document.lastSavedVersion === getVersion(this.authority.view.state) + this.sessions = data.sessions + }) + this.authority.onError((error, data) => { + if (error === ERROR_TYPE.SAVE_COLLISSION && (!this.syncError || this.syncError.type !== ERROR_TYPE.SAVE_COLLISSION)) { + this.syncError = { + type: ERROR_TYPE.SAVE_COLLISSION, + data: data } - }) + } + }) + this.authority.onStateChange(() => { + this.dirty = this.authority.dirty + if (!this.initialLoading) { + this.initialLoading = !this.authority.dirty && this.document.lastSavedVersion === getVersion(this.authority.view.state) + } + }) - this.$emit('update:loaded', true) - }); - }).catch((error) => { - console.error(error.response) - this.$emit('error', error.response.status) + this.$emit('update:loaded', true) }) + }).catch((error) => { + console.error(error.response) + this.$emit('error', error.response.status) + }) - }, - - resolveUseThisVersion() { - this.authority.forceSave() - this.authority.view.setProps({editable: () => true}) - }, + }, - resolveUseServerVersion() { - this.authority.view.destroy() - this.initSession() - }, + resolveUseThisVersion() { + this.authority.forceSave() + this.authority.view.setProps({ editable: () => true }) + }, - initEditor: (ref, data, initialDocument) => { - const authority = new EditorSync(defaultMarkdownParser.parse(initialDocument), data) + resolveUseServerVersion() { + this.authority.view.destroy() + this.initSession() + }, - const sendStepsDebounce = () => authority.sendSteps() - const sendStepsDebounced = debounce(sendStepsDebounce, EDITOR_PUSH_DEBOUNCE, { maxWait: 500 }) + initEditor: (ref, data, initialDocument) => { + const authority = new EditorSync(defaultMarkdownParser.parse(initialDocument), data) - const view = new EditorView(ref, { - state: EditorState.create({ - doc: authority.doc, - plugins: [ - keymap({ - 'Ctrl-s': () => { - authority.manualSave() - authority.fetchSteps() - return true; - } - }), - ...exampleSetup({schema}), - collab({ - version: authority.steps.length, - clientID: data.session.id - }) - ] - }), - destroy() { - this.view.destroy() - authority.destroy() - }, - dispatchTransaction: (transaction) => { - const state = view.state.apply(transaction); - view.updateState(state); - sendStepsDebounced() - }, + const sendStepsDebounce = () => authority.sendSteps() + const sendStepsDebounced = debounce(sendStepsDebounce, EDITOR_PUSH_DEBOUNCE, { maxWait: 500 }) - }) - authority.view = view; - authority.fetchSteps() - return { - view: view, - authority: authority + const view = new EditorView(ref, { + state: EditorState.create({ + doc: authority.doc, + plugins: [ + keymap({ + 'Ctrl-s': () => { + authority.manualSave() + authority.fetchSteps() + return true + } + }), + ...exampleSetup({ schema }), + collab({ + version: authority.steps.length, + clientID: data.session.id + }) + ] + }), + destroy() { + this.view.destroy() + authority.destroy() + }, + dispatchTransaction: (transaction) => { + const state = view.state.apply(transaction) + view.updateState(state) + sendStepsDebounced() } - }, - onSync(syncState) { - this.sessions = syncState.sessions - this.document = syncState.document + + }) + authority.view = view + authority.fetchSteps() + return { + view: view, + authority: authority } + }, + onSync(syncState) { + this.sessions = syncState.sessions + this.document = syncState.document } } +} </script> <style scoped lang="scss"> @@ -332,8 +343,6 @@ } } - - #editor-container #editor-wrapper.has-conflicts { height: calc(100% - 50px); diff --git a/src/components/ReadOnlyEditor.vue b/src/components/ReadOnlyEditor.vue index 666a027e6..47883ace1 100644 --- a/src/components/ReadOnlyEditor.vue +++ b/src/components/ReadOnlyEditor.vue @@ -21,51 +21,51 @@ --> <template> - <div id="remote" ref="remote"></div> + <div id="remote" ref="remote" /> </template> <script> - import {EditorState} from 'prosemirror-state' - import {EditorView} from 'prosemirror-view' - import {exampleSetup} from 'prosemirror-example-setup' - import {schema, defaultMarkdownParser} from 'prosemirror-markdown' +import { EditorState } from 'prosemirror-state' +import { EditorView } from 'prosemirror-view' +import { exampleSetup } from 'prosemirror-example-setup' +import { schema, defaultMarkdownParser } from 'prosemirror-markdown' - export default { - name: 'ReadOnlyEditor', - props: { - content: { - type: String, - required: true - }, - }, - data: () => { - return { - remoteView: null +export default { + name: 'ReadOnlyEditor', + props: { + content: { + type: String, + required: true + } + }, + data: () => { + return { + remoteView: null + } + }, + mounted() { + this.initRemoteView() + }, + beforeDestroy() { + this.remoteView.destroy() + }, + methods: { + initRemoteView() { + if (this.remoteView) { + return } - }, - mounted() { - this.initRemoteView() - }, - beforeDestroy() { - this.remoteView.destroy() - }, - methods: { - initRemoteView() { - if (this.remoteView) { - return; - } - this.remoteView = new EditorView(this.$refs.remote, { - state: EditorState.create({ - doc: defaultMarkdownParser.parse(this.content), - plugins: [ - ...exampleSetup({schema}) - ] - }), + this.remoteView = new EditorView(this.$refs.remote, { + state: EditorState.create({ + doc: defaultMarkdownParser.parse(this.content), + plugins: [ + ...exampleSetup({ schema }) + ] }) - this.remoteView.setProps({editable: () => false}) - }, + }) + this.remoteView.setProps({ editable: () => false }) } } +} </script> <style scoped> diff --git a/src/files.js b/src/files.js index c08e7629c..ee936bc6f 100644 --- a/src/files.js +++ b/src/files.js @@ -20,14 +20,16 @@ * */ -const newFileMenuPlugin = { +import Editor from './components/Editor' +import { documentReady } from './helpers' - attach: function (menu) { - var fileList = menu.fileList; +const newFileMenuPlugin = { + attach: function(menu) { + var fileList = menu.fileList // only attach to main file list, public view is not supported yet if (fileList.id !== 'files') { - return; + return } // register the new menu entry @@ -37,23 +39,22 @@ const newFileMenuPlugin = { templateName: t('text', 'New text document.md'), iconClass: 'icon-filetype-text', fileType: 'file', - actionHandler: function (name) { - fileList.createFile(name).then(function (status, data) { - let fileInfoModel = new OCA.Files.FileInfoModel(data); - OCA.Files.fileActions.triggerAction('view', fileInfoModel, fileList); - }); + actionHandler: function(name) { + fileList.createFile(name).then(function(status, data) { + let fileInfoModel = new OCA.Files.FileInfoModel(data) + OCA.Files.fileActions.triggerAction('view', fileInfoModel, fileList) + }) } - }); + }) } -}; -OC.Plugins.register('OCA.Files.NewFileMenu', newFileMenuPlugin); +} -import Editor from './components/Editor' -$(document).ready(function() { +OC.Plugins.register('OCA.Files.NewFileMenu', newFileMenuPlugin) +documentReady(() => { OCA.Viewer.registerHandler({ id: 'text', mimes: ['text/markdown'], component: Editor, group: null - }); -}); + }) +}) diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 000000000..f70360bb3 --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,38 @@ +/* + * @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/>. + * + */ + +/** + * Callback that should be executed after the document is ready + * @param callback + */ +const documentReady = function(callback) { + const fn = () => setTimeout(callback, 0) + if (document.attachEvent ? document.readyState === 'complete' : document.readyState !== 'loading') { + fn() + } else { + document.addEventListener('DOMContentLoaded', fn) + } +} + +export { + documentReady +} diff --git a/src/main.js b/src/main.js index 4e21fd43c..f663d615c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,17 +1,18 @@ import Vue from 'vue' -__webpack_nonce__ = btoa(OC.requestToken); // eslint-disable-line no-native-reassign +import Editor from './components/Editor' + +__webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line Vue.prototype.t = t Vue.prototype.OCA = OCA -import Editor from './components/Editor' new Vue({ render: h => h(Editor, { props: { relativePath: '/welcome.md', active: true } - }), + }) }).$mount('#maineditor') |