diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/EditorWrapper.provider.js | 13 | ||||
-rw-r--r-- | src/components/EditorWrapper.vue | 114 | ||||
-rw-r--r-- | src/components/GuestNameDialog.vue | 14 | ||||
-rw-r--r-- | src/components/MenuBar.vue | 24 | ||||
-rw-r--r-- | src/components/MenuBubble.vue | 19 |
5 files changed, 102 insertions, 82 deletions
diff --git a/src/components/EditorWrapper.provider.js b/src/components/EditorWrapper.provider.js new file mode 100644 index 000000000..c19ebdc0d --- /dev/null +++ b/src/components/EditorWrapper.provider.js @@ -0,0 +1,13 @@ +export const EDITOR = Symbol('tiptap:editor') +export const SYNC_SERVICE = Symbol('sync:service') + +export const useEditorMixin = { + inject: { + $editor: { from: EDITOR, default: null }, + }, +} +export const useSyncServiceMixin = { + inject: { + $syncService: { from: SYNC_SERVICE, default: null }, + }, +} diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue index 782a91434..913528229 100644 --- a/src/components/EditorWrapper.vue +++ b/src/components/EditorWrapper.vue @@ -37,7 +37,7 @@ </p> </div> <div v-if="displayed" id="editor-wrapper" :class="{'has-conflicts': hasSyncCollission, 'icon-loading': !contentLoaded && !hasConnectionIssue, 'richEditor': isRichEditor, 'show-color-annotations': showAuthorAnnotations}"> - <div v-if="tiptap" + <div v-if="$editor" id="editor" :class="{ draggedOver }" @image-paste="onPaste" @@ -46,8 +46,6 @@ @image-drop="onEditorDrop"> <MenuBar v-if="renderMenus" ref="menubar" - :editor="tiptap" - :sync-service="syncService" :file-path="relativePath" :file-id="fileId" :is-rich-editor="isRichEditor" @@ -63,7 +61,7 @@ {{ lastSavedStatus }} </div> <SessionList :sessions="filteredSessions"> - <GuestNameDialog v-if="isPublic && currentSession.guestName" :sync-service="syncService" /> + <GuestNameDialog v-if="isPublic && currentSession.guestName" /> </SessionList> </div> <slot name="header" /> @@ -71,12 +69,11 @@ <div v-if="!menubarLoaded" class="menubar placeholder" /> <div ref="contentWrapper" class="content-wrapper"> <MenuBubble v-if="renderMenus" - :editor="tiptap" :content-wrapper="contentWrapper" :file-path="relativePath" /> <EditorContent v-show="contentLoaded" class="editor__content" - :editor="tiptap" /> + :editor="$editor" /> </div> </div> <ReadOnlyEditor v-if="hasSyncCollission" @@ -95,6 +92,8 @@ import escapeHtml from 'escape-html' import moment from '@nextcloud/moment' import { showError } from '@nextcloud/dialogs' +import { EDITOR, SYNC_SERVICE } from './EditorWrapper.provider' + import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService' import { endpointUrl, getRandomGuestName } from './../helpers' import { extensionHighlight } from '../helpers/mappings' @@ -144,6 +143,27 @@ export default { isMobile, store, ], + provide() { + const val = {} + + // providers aren't naturally reactive + // and $editor will start as null + // using getters we can always provide the + // actual $editor without being reactive + Object.defineProperty(val, EDITOR, { + get: () => { + return this.$editor + }, + }) + + Object.defineProperty(val, SYNC_SERVICE, { + get: () => { + return this.$syncService + }, + }) + + return val + }, props: { initialSession: { type: Object, @@ -186,10 +206,6 @@ export default { return { IDLE_TIMEOUT, - tiptap: null, - /** @type {SyncService} */ - syncService: null, - document: null, sessions: [], currentSession: null, @@ -297,6 +313,8 @@ export default { this.$parent.$emit('update:loaded', true) }, created() { + this.$editor = null + this.$syncService = null this.saveStatusPolling = setInterval(() => { this.updateLastSavedStatus() }, 2000) @@ -307,11 +325,11 @@ export default { methods: { async close() { clearInterval(this.saveStatusPolling) - if (this.currentSession && this.syncService) { + if (this.currentSession && this.$syncService) { try { - await this.syncService.close() + await this.$syncService.close() this.currentSession = null - this.syncService = null + this.$syncService = null } catch (e) { // Ignore issues closing the session since those might happen due to network issues } @@ -329,16 +347,16 @@ export default { return } const guestName = localStorage.getItem('nick') ? localStorage.getItem('nick') : getRandomGuestName() - this.syncService = new SyncService({ + this.$syncService = new SyncService({ shareToken: this.shareToken, filePath: this.relativePath, guestName, forceRecreate: this.forceRecreate, serialize: (document) => { if (this.isRichEditor) { - return (createMarkdownSerializer(this.tiptap.schema)).serialize(document) + return (createMarkdownSerializer(this.$editor.schema)).serialize(document) } - return serializePlainText(this.tiptap) + return serializePlainText(this.$editor) }, }) @@ -346,7 +364,7 @@ export default { this.currentSession = session this.document = document this.readOnly = document.readOnly - this.lock = this.syncService.lock + this.lock = this.$syncService.lock localStorage.setItem('nick', this.currentSession.guestName) this.$store.dispatch('setCurrentSession', this.currentSession) }) @@ -359,7 +377,7 @@ export default { this.document = document this.syncError = null - this.tiptap.setOptions({ editable: !this.readOnly }) + this.$editor.setOptions({ editable: !this.readOnly }) }) .on('loaded', ({ documentSource }) => { this.hasConnectionIssue = false @@ -368,14 +386,14 @@ export default { : '<pre>' + escapeHtml(documentSource) + '</pre>' const language = extensionHighlight[this.fileExtension] || this.fileExtension loadSyntaxHighlight(language).then(() => { - this.tiptap = createEditor({ + this.$editor = createEditor({ content, onCreate: ({ editor }) => { - this.syncService.state = editor.state - this.syncService.startSync() + this.$syncService.state = editor.state + this.$syncService.startSync() }, onUpdate: ({ editor }) => { - this.syncService.state = editor.state + this.$syncService.state = editor.state }, extensions: [ Collaboration.configure({ @@ -386,8 +404,8 @@ export default { // debounce changes so we can save some bandwidth debounce: EDITOR_PUSH_DEBOUNCE, onSendable: ({ sendable }) => { - if (this.syncService) { - this.syncService.sendSteps() + if (this.$syncService) { + this.$syncService.sendSteps() } }, update: ({ steps, version, editor }) => { @@ -406,7 +424,7 @@ export default { }), Keymap.configure({ 'Mod-s': () => { - this.syncService.save() + this.$syncService.save() return true }, }), @@ -425,25 +443,25 @@ export default { enableRichEditing: this.isRichEditor, currentDirectory: this.currentDirectory, }) - this.tiptap.on('focus', () => { + this.$editor.on('focus', () => { this.$emit('focus') }) - this.tiptap.on('blur', () => { + this.$editor.on('blur', () => { this.$emit('blur') }) - this.syncService.state = this.tiptap.state + this.$syncService.state = this.$editor.state }) }) .on('sync', ({ steps, document }) => { this.hasConnectionIssue = false try { - const collaboration = this.tiptap.extensionManager.extensions.find(e => e.name === 'collaboration') + const collaboration = this.$editor.extensionManager.extensions.find(e => e.name === 'collaboration') collaboration.options.update({ version: document.currentVersion, steps, - editor: this.tiptap, + editor: this.$editor, }) - this.syncService.state = this.tiptap.state + this.$syncService.state = this.$editor.state this.updateLastSavedStatus() } catch (e) { console.error('Failed to update steps in collaboration plugin', e) @@ -452,7 +470,7 @@ export default { this.document = document }) .on('error', (error, data) => { - this.tiptap.setOptions({ editable: false }) + this.$editor.setOptions({ editable: false }) if (error === ERROR_TYPE.SAVE_COLLISSION && (!this.syncError || this.syncError.type !== ERROR_TYPE.SAVE_COLLISSION)) { this.contentLoaded = true this.syncError = { @@ -477,7 +495,7 @@ export default { if (state.initialLoading && !this.contentLoaded) { this.contentLoaded = true if (this.autofocus && !this.readOnly) { - this.tiptap.commands.focus() + this.$editor.commands.focus() } this.$emit('ready') this.$parent.$emit('ready', true) @@ -487,12 +505,12 @@ export default { } }) .on('idle', () => { - this.syncService.close() + this.$syncService.close() this.idle = true this.readOnly = true - this.tiptap.setOptions({ editable: !this.readOnly }) + this.$editor.setOptions({ editable: !this.readOnly }) }) - this.syncService.open({ + this.$syncService.open({ fileId: this.fileId, filePath: this.relativePath, initialSession: this.initialSession, @@ -503,8 +521,8 @@ export default { }, resolveUseThisVersion() { - this.syncService.forceSave() - this.tiptap.setOptions({ editable: !this.readOnly }) + this.$syncService.forceSave() + this.$editor.setOptions({ editable: !this.readOnly }) }, resolveUseServerVersion() { @@ -515,17 +533,17 @@ export default { reconnect() { this.contentLoaded = false this.hasConnectionIssue = false - if (this.syncService) { - this.syncService.close().then(() => { - this.syncService = null - this.tiptap.destroy() + if (this.$syncService) { + this.$syncService.close().then(() => { + this.$syncService = null + this.$editor.destroy() this.initSession() }).catch((e) => { // Ignore issues closing the session since those might happen due to network issues }) } else { - this.syncService = null - this.tiptap.destroy() + this.$syncService = null + this.$editor.destroy() this.initSession() } this.idle = false @@ -597,7 +615,7 @@ export default { return } - return this.syncService.uploadImage(file).then((response) => { + return this.$syncService.uploadImage(file).then((response) => { this.insertAttachmentImage(response.data?.name, response.data?.id, position) }).catch((error) => { console.error(error) @@ -606,7 +624,7 @@ export default { }, insertImagePath(imagePath) { this.uploadingImages = true - this.syncService.insertImageFile(imagePath).then((response) => { + this.$syncService.insertImageFile(imagePath).then((response) => { this.insertAttachmentImage(response.data?.name, response.data?.id) }).catch((error) => { console.error(error) @@ -621,9 +639,9 @@ export default { // as it does not need to be unique and matching the real file name const alt = name.replaceAll(/[[\]]/g, '') if (position) { - this.tiptap.chain().focus(position).setImage({ src, alt }).focus().run() + this.$editor.chain().focus(position).setImage({ src, alt }).focus().run() } else { - this.tiptap.chain().setImage({ src, alt }).focus().run() + this.$editor.chain().setImage({ src, alt }).focus().run() } }, }, diff --git a/src/components/GuestNameDialog.vue b/src/components/GuestNameDialog.vue index 624c8d069..77b4cc03a 100644 --- a/src/components/GuestNameDialog.vue +++ b/src/components/GuestNameDialog.vue @@ -35,6 +35,7 @@ import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' import Avatar from '@nextcloud/vue/dist/Components/Avatar' import { generateUrl } from '@nextcloud/router' +import { useSyncServiceMixin } from './EditorWrapper.provider' export default { name: 'GuestNameDialog', @@ -44,12 +45,7 @@ export default { directives: { tooltip: Tooltip, }, - props: { - syncService: { - type: Object, - default: null, - }, - }, + mixins: [useSyncServiceMixin], data() { return { guestName: '', @@ -69,13 +65,13 @@ export default { }, }, beforeMount() { - this.guestName = this.syncService.session.guestName + this.guestName = this.$syncService.session.guestName this.updateBufferedGuestName() }, methods: { setGuestName() { - const previousGuestName = this.syncService.session.guestName - this.syncService.updateSession(this.guestName).then(() => { + const previousGuestName = this.$syncService.session.guestName + this.$syncService.updateSession(this.guestName).then(() => { localStorage.setItem('nick', this.guestName) this.updateBufferedGuestName() }).catch((e) => { diff --git a/src/components/MenuBar.vue b/src/components/MenuBar.vue index 64637047d..4b0e57fe7 100644 --- a/src/components/MenuBar.vue +++ b/src/components/MenuBar.vue @@ -123,6 +123,8 @@ import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' import menuBarIcons from './../mixins/menubar' import isMobile from './../mixins/isMobile' +import { useEditorMixin } from './EditorWrapper.provider' + import Actions from '@nextcloud/vue/dist/Components/Actions' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import PopoverMenu from '@nextcloud/vue/dist/Components/PopoverMenu' @@ -145,17 +147,9 @@ export default { }, mixins: [ isMobile, + useEditorMixin, ], props: { - editor: { - type: Object, - required: true, - }, - syncService: { - type: Object, - required: false, - default: null, - }, isRichEditor: { type: Boolean, default: true, @@ -211,16 +205,16 @@ export default { return false } const args = Array.isArray(isActive) ? isActive : [isActive] - return this.editor.isActive(...args) + return this.$editor.isActive(...args) } }, isVisible() { - return this.editor.isFocused + return this.$editor.isFocused || Object.values(this.submenuVisibility).find((v) => v) }, disabled() { return (menuItem) => { - return menuItem.action && !menuItem.action(this.editor.can()) + return menuItem.action && !menuItem.action(this.$editor.can()) } }, isChildMenuVisible() { @@ -302,7 +296,7 @@ export default { }) }, refocus() { - this.editor.chain().focus().run() + this.$editor.chain().focus().run() }, clickIcon(icon) { if (icon.click) { @@ -310,7 +304,7 @@ export default { } // Some actions run themselves. // others still need to have .run() called upon them. - const action = icon.action(this.editor.chain().focus()) + const action = icon.action(this.$editor.chain().focus()) action && action.run() }, getWindowWidth(event) { @@ -369,7 +363,7 @@ export default { return current.fill('..').concat(target).join('/') }, addEmoji(icon, emojiObject) { - return icon.action(this.editor.chain(), { id: emojiObject.id, native: emojiObject.native }) + return icon.action(this.$editor.chain(), { id: emojiObject.id, native: emojiObject.native }) .focus() .run() }, diff --git a/src/components/MenuBubble.vue b/src/components/MenuBubble.vue index d63fc9c41..31be99db0 100644 --- a/src/components/MenuBubble.vue +++ b/src/components/MenuBubble.vue @@ -21,7 +21,7 @@ --> <template> - <BubbleMenu :editor="editor" + <BubbleMenu :editor="$editor" :tippy-options="{ onHide: hideLinkMenu, duration: 200, placement: 'bottom' }" class="menububble"> <form v-if="linkMenuIsActive" class="menububble__form" @submit.prevent="setLinkUrl()"> @@ -74,6 +74,8 @@ import { getCurrentUser } from '@nextcloud/auth' import { optimalPath } from './../helpers/files' import { loadState } from '@nextcloud/initial-state' +import { useEditorMixin } from './EditorWrapper.provider' + export default { name: 'MenuBubble', components: { @@ -82,11 +84,8 @@ export default { directives: { tooltip: Tooltip, }, + mixins: [useEditorMixin], props: { - editor: { - type: Object, - required: true, - }, // used to calculate the position based on the scrollOffset contentWrapper: { type: HTMLDivElement, @@ -108,7 +107,7 @@ export default { }, methods: { showLinkMenu() { - const attrs = getMarkAttributes(this.editor.state, 'link') + const attrs = getMarkAttributes(this.$editor.state, 'link') this.linkUrl = attrs.href this.linkMenuIsActive = true this.$nextTick(() => { @@ -131,7 +130,7 @@ export default { const path = optimalPath(this.filePath, `${fileInfo.path}/${fileInfo.name}`) const encodedPath = path.split('/').map(encodeURIComponent).join('/') const href = `${encodedPath}?fileId=${fileInfo.id}` - this.editor.chain().setLink({ href }).focus().run() + this.$editor.chain().setLink({ href }).focus().run() this.hideLinkMenu() }) }, false, [], true, undefined, startPath) @@ -153,14 +152,14 @@ export default { // Avoid issues when parsing urls later on in markdown that might be entered in an invalid format (e.g. "mailto: example@example.com") const href = url.replaceAll(' ', '%20') - this.editor.chain().setLink({ href }).focus().run() + this.$editor.chain().setLink({ href }).focus().run() this.hideLinkMenu() }, removeLinkUrl() { - this.editor.chain().unsetLink().focus().run() + this.$editor.chain().unsetLink().focus().run() }, isActive(selector, args = {}) { - return this.editor.isActive(selector, args) + return this.$editor.isActive(selector, args) }, }, } |