Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
881ce5b
chore(migrate): useEditorMixin to useEditor composable
max-nextcloud Jun 16, 2025
8d998f0
chore(migrate): setContent mixin...
max-nextcloud Jun 17, 2025
13a5ce1
chore(migrate): to useEditorFlags composable
max-nextcloud Jun 18, 2025
d958a4d
chore(cleanup): fix small review remarks
max-nextcloud Jun 18, 2025
b0790dc
chore(migrate): use.find instead of deprecated .contains
max-nextcloud Jun 19, 2025
8b4e635
enh(editor): store session in separate extension
max-nextcloud Jun 20, 2025
9d3cc87
chore(refactor): configure mention in rich text extension
max-nextcloud Jun 20, 2025
c4f863f
chore(types): collaborationCursor extension to typescript
max-nextcloud Jun 20, 2025
354ad41
chore(simplify): rely on updateUser command
max-nextcloud Jun 20, 2025
022e483
chore(simplify): replace computed fileExtension with temp
max-nextcloud Jun 20, 2025
0809c8e
enh(code): start to load syntax highlighting during setup
max-nextcloud Jun 20, 2025
b326875
chore(refactor): load editor in mounted
max-nextcloud Jun 20, 2025
3e5cfd8
chore(refactor): create editor in created instead of mounted
max-nextcloud Jun 26, 2025
ab26032
chore(refactor): create ydoc in setup
max-nextcloud Jun 26, 2025
2831462
fix(character-count): always provide the current editors doc
max-nextcloud Jun 26, 2025
d7df4c0
fix(character-count): use the NcActionTexts name prop
max-nextcloud Jun 26, 2025
e24d90a
fix(loading): only show main container when content loaded
max-nextcloud Jun 26, 2025
0d5f4f7
fix(mention): use shallowRef for connection
max-nextcloud Jun 27, 2025
057a682
fix(load): create initial YjsState with dir
max-nextcloud Jun 27, 2025
fae62fc
chore(refactor): extract useEditor into its own file
max-nextcloud Jun 28, 2025
17f0d62
chore(refactor): extract useEditorFlags into its own file
max-nextcloud Jun 28, 2025
90b1d84
test(RichTextReader): basic test
max-nextcloud Jun 28, 2025
fabc79e
test(RichTextReader): update content
max-nextcloud Jun 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/EditorFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const editorProps = {
scrollThreshold: 50,
}

const createRichEditor = ({ extensions = [], session, relativePath, isEmbedded = false } = {}) => {
const createRichEditor = ({ extensions = [], connection, relativePath, isEmbedded = false } = {}) => {
return new Editor({
editorProps,
extensions: [
Expand All @@ -48,7 +48,7 @@ const createRichEditor = ({ extensions = [], session, relativePath, isEmbedded =
extensions: [
Mention.configure({
suggestion: MentionSuggestion({
session,
connection,
}),
}),
],
Expand Down
48 changes: 24 additions & 24 deletions src/components/Assistant.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<template>
<div v-if="showAssistant" class="text-assistant">
<FloatingMenu
v-if="$editor"
v-if="editor"
plugin-key="assistantMenu"
:editor="$editor"
:editor="editor"
:tippy-options="floatingOptions()"
:should-show="floatingShow"
class="floating-menu"
Expand Down Expand Up @@ -162,12 +162,9 @@ import NcActionButton from '@nextcloud/vue/components/NcActionButton'
import NcActionSeparator from '@nextcloud/vue/components/NcActionSeparator'
import NcListItem from '@nextcloud/vue/components/NcListItem'
import NcModal from '@nextcloud/vue/components/NcModal'
import {
useEditorMixin,
useIsRichWorkspaceMixin,
useFileMixin,
useIsPublicMixin,
} from './Editor.provider.js'
import { useFileMixin } from './Editor.provider.ts'
import { useEditorFlags } from '../composables/useEditorFlags.ts'
import { useEditor } from '../composables/useEditor.ts'
import { FloatingMenu } from '@tiptap/vue-2'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
import markdownit from '../markdownit/index.js'
Expand Down Expand Up @@ -206,12 +203,12 @@ export default {
NcListItem,
NcModal,
},
mixins: [
useEditorMixin,
useIsPublicMixin,
useIsRichWorkspaceMixin,
useFileMixin,
],
mixins: [useFileMixin],
setup() {
const { editor } = useEditor()
const { isPublic, isRichWorkspace } = useEditorFlags()
return { editor, isPublic, isRichWorkspace }
},
data() {
return {
taskTypes: OCP.InitialState.loadState('text', 'taskprocessing'),
Expand All @@ -232,8 +229,8 @@ export default {
computed: {
showAssistant() {
return (
!this.$isRichWorkspace
&& !this.$isPublic
!this.isRichWorkspace
&& !this.isPublic
&& window.OCA.Assistant?.openAssistantForm
)
},
Expand Down Expand Up @@ -271,7 +268,7 @@ export default {
return
}

this.$editor.on('selectionUpdate', this.onSelection)
this.editor?.on('selectionUpdate', this.onSelection)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You now check in many places whether editor exists. This means that we cannot be sure that useEditor returns an editor object and always have to deal with editor being undefined? It looks a bit like an anti-pattern to me 🤔

If I get it right, then editor can be set by all the base components that call provideEditor() in setup() and then set this.editor in created() (i.e. BaseReader, MarkdownContentEditor and Editor). Since all consumers of useEditor() should be descendants (i.e child/granchild components) of the base editor components, editor should always already be set when used there, no?

Copy link
Collaborator Author

@max-nextcloud max-nextcloud Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two things that come into play here right now:

  1. inject returns an optional value - i.e. can always be undefined. It's possible to cast it to always be defined (see last example in the docs for typing provide/inject ). That implies that the component will behave in unpredicted ways - most likely throwing exceptions when being used without provideing a value.
  2. editor starts as undefined in the providing component such as Editor.vue. We only initialize it once we loaded the content.

2 is maybe artifact from how the content used to be loaded. I think we could be able to create the editor itself in the components setup routine these days. I'm a bit hesitant to changing the loading order right now as it feels fragile - but initializing the editor in the setup function rather than in loaded seems like it might even be a win for load times.

Thus far we hid these issues by only rendering subcomponents once the $editor was loaded. Whenever we removed the v-if='hasEditor' somewhere we'd get a lot of errors about this.$editor being undefined where it was not expected.

In addition the file that is rendered in an editor component never changes. If we switch between files in collective I believe the Editor component is unmounted and mounted again with a new value for the file related props. This is not how other components work, where one can just update a prop. This also means that for example the entire Menubar is recreated - with basically the same content. I was thinking that we might be able to keep the Editor component and change the props instead at some point. So far my assumption was that this would lead to editor being undefined while the new file is loaded. The ydoc that tracks the editing would also need to be replaced and it's part of the collaboration plugin config. But maybe we can even keep the editor around and "just" replace its content, the ydoc and the file related parts of the sync.

Anyway... I will take a look and see if I can initialize the editor in the setup function already. Then we might be able to do away with the ? everywhere.

this.fetchTasks()
subscribe('notifications:notification:received', this.checkNotification)
},
Expand All @@ -280,7 +277,7 @@ export default {
return
}

this.$editor.off('selectionUpdate', this.onSelection)
this.editor?.off('selectionUpdate', this.onSelection)
unsubscribe('notifications:notification:received', this.checkNotification)
},
methods: {
Expand Down Expand Up @@ -313,7 +310,10 @@ export default {
await this.fetchTasks()
},
onSelection() {
const { state } = this.$editor
const { state } = this.editor ?? {}
if (!state) {
return
}
const { from, to } = state.selection
this.selection = state.doc.textBetween(from, to, ' ')
},
Expand Down Expand Up @@ -343,7 +343,7 @@ export default {
},
openTranslateDialog() {
if (!this.selection.trim().length) {
this.$editor.commands.selectAll()
this.editor?.commands.selectAll()
}
emit('text:translate-modal:show', { content: this.selection || '' })
},
Expand All @@ -367,7 +367,7 @@ export default {
const content = isMarkdown
? markdownit.render(task.output.output)
: task.output.output
this.$editor.commands.insertContent(content)
this.editor?.commands.insertContent(content)
this.showTaskList = false
},
async copyResult(task) {
Expand Down Expand Up @@ -402,9 +402,9 @@ export default {
.querySelector('.ProseMirror')
.getBoundingClientRect()
const pos = posToDOMRect(
this.$editor.view,
this.$editor.state.selection.from,
this.$editor.state.selection.to,
this.editor?.view,
this.editor?.state.selection.from,
this.editor?.state.selection.to,
)
let rightSpacing = 0

Expand Down
33 changes: 14 additions & 19 deletions src/components/BaseReader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,24 @@
<EditorOutline />
</div>
<EditorContent
v-if="$editor"
v-if="editor"
id="read-only-editor"
class="editor__content text-editor__content"
:editor="$editor" />
:editor="editor" />
<div class="text-editor__content-wrapper__right" />
</div>
</template>

<script>
import { Editor } from '@tiptap/core'
import { EditorContent } from '@tiptap/vue-2'
import { EDITOR } from './Editor.provider.js'
import { provideEditor } from '../composables/useEditor.ts'
import {
useOutlineStateMixin,
useOutlineActions,
} from './Editor/Wrapper.provider.js'
import EditorOutline from './Editor/EditorOutline.vue'
import { useEditorMethods } from '../composables/useEditorMethods.ts'

export default {
name: 'BaseReader',
Expand All @@ -41,18 +42,6 @@ export default {

mixins: [useOutlineStateMixin, useOutlineActions],

provide() {
const val = {}

Object.defineProperties(val, {
[EDITOR]: {
get: () => this.$editor,
},
})

return val
},

// extensions is a factory building a list of extensions for the editor
inject: ['renderHtml', 'extensions'],

Expand All @@ -63,6 +52,12 @@ export default {
},
},

setup() {
const { editor } = provideEditor()
const { setEditable } = useEditorMethods(editor)
return { editor, setEditable }
},

computed: {
htmlContent() {
return this.renderHtml(this.content)
Expand All @@ -79,12 +74,12 @@ export default {
},

created() {
this.$editor = this.createEditor()
this.$editor.setEditable(false)
this.editor = this.createEditor()
this.setEditable(false)
},

beforeDestroy() {
this.$editor.destroy()
this.editor?.destroy()
},

methods: {
Expand All @@ -96,7 +91,7 @@ export default {
},

updateContent() {
this.$editor.commands.setContent(this.htmlContent, true)
this.editor?.commands.setContent(this.htmlContent, true)
},
},
}
Expand Down
24 changes: 14 additions & 10 deletions src/components/CollisionResolveDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,29 @@
</template>

<script>
import {
useEditorMixin,
useIsRichEditorMixin,
useSyncServiceMixin,
} from './Editor.provider.js'
import { useSyncServiceMixin } from './Editor.provider.ts'
import { useEditorFlags } from '../composables/useEditorFlags.ts'
import { useEditor } from '../composables/useEditor.ts'
import NcButton from '@nextcloud/vue/components/NcButton'
import setContent from './../mixins/setContent.js'
import { useEditorMethods } from '../composables/useEditorMethods.ts'
export default {
name: 'CollisionResolveDialog',
components: {
NcButton,
},
mixins: [useEditorMixin, useIsRichEditorMixin, setContent, useSyncServiceMixin],
mixins: [useSyncServiceMixin],
props: {
syncError: {
type: Object,
default: null,
},
},
setup() {
const { editor } = useEditor()
const { setContent, setEditable } = useEditorMethods(editor)
const { isRichEditor } = useEditorFlags()
return { editor, isRichEditor, setContent, setEditable }
},
data() {
return {
clicked: false,
Expand All @@ -57,13 +61,13 @@ export default {
resolveThisVersion() {
this.clicked = true
this.$syncService.forceSave().then(() => this.$syncService.syncUp())
this.$editor.setEditable(!this.readOnly)
this.setEditable(!this.readOnly)
},
resolveServerVersion() {
const { outsideChange } = this.syncError.data
this.clicked = true
this.$editor.setEditable(!this.readOnly)
this.setContent(outsideChange, { isRichEditor: this.$isRichEditor })
this.setEditable(!this.readOnly)
this.setContent(outsideChange, { isRichEditor: this.isRichEditor })
this.$syncService.forceSave().then(() => this.$syncService.syncUp())
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,20 @@

import { logger } from '../helpers/logger.js'

export const EDITOR = Symbol('tiptap:editor')
export const FILE = Symbol('editor:file')
export const ATTACHMENT_RESOLVER = Symbol('attachment:resolver')
export const IS_MOBILE = Symbol('editor:is-mobile')
export const IS_PUBLIC = Symbol('editor:is-public')
export const IS_RICH_EDITOR = Symbol('editor:is-rich-editor')
export const IS_RICH_WORKSPACE = Symbol('editor:is-rich-woskapace')
export const SYNC_SERVICE = Symbol('sync:service')
export const EDITOR_UPLOAD = Symbol('editor:upload')
export const HOOK_MENTION_SEARCH = Symbol('hook:mention-search')
export const HOOK_MENTION_INSERT = Symbol('hook:mention-insert')

export const useEditorMixin = {
inject: {
$editor: { from: EDITOR, default: null },
},
}

export const useSyncServiceMixin = {
inject: {
$syncService: { from: SYNC_SERVICE, default: null },
},
}

export const useIsPublicMixin = {
inject: {
$isPublic: { from: IS_PUBLIC, default: false },
},
}

export const useIsRichWorkspaceMixin = {
inject: {
$isRichWorkspace: { from: IS_RICH_WORKSPACE, default: false },
},
}

export const useIsRichEditorMixin = {
inject: {
$isRichEditor: { from: IS_RICH_EDITOR, default: false },
},
}

export const useIsMobileMixin = {
inject: {
$isMobile: { from: IS_MOBILE, default: false },
Expand All @@ -71,7 +43,7 @@ export const useAttachmentResolver = {
$attachmentResolver: {
from: ATTACHMENT_RESOLVER,
default: {
resolve(src) {
resolve(src: string) {
logger.warn(
'No attachment resolver provided. Some attachment sources cannot be resolved.',
)
Expand Down
Loading
Loading