-
Notifications
You must be signed in to change notification settings - Fork 104
chore(migrate): useEditorMixin to useEditor composable #7313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
4a8e79d
to
4fc23ad
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7313 +/- ##
==========================================
+ Coverage 59.22% 59.66% +0.44%
==========================================
Files 481 486 +5
Lines 37149 37285 +136
Branches 1048 1097 +49
==========================================
+ Hits 22000 22245 +245
+ Misses 15047 14934 -113
- Partials 102 106 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
d51ccbd
to
8e33aff
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice work! I didn't properly test it but read through the code changes and have some questions and remarks.
} | ||
this.$editor.on('selectionUpdate', this.onSelection) | ||
this.editor?.on('selectionUpdate', this.onSelection) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
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 withoutprovide
ing a value.editor
starts as undefined in the providing component such asEditor.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.
if (this.$editor.isEditable !== editable) { | ||
this.$editor.setEditable(editable) | ||
} | ||
this.setEditable(this.editMode && !this.requireReconnect) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not directly related to this PR, but I just realized that we have a watcher for requireReconnect
that calls setEditable(!this.requireReconnect)
, which smells a lot like conflicting with this one.
If this.editMode === false
and this.requireReconnect
switches to false
, the editor is set to editable by the watcher, but any subsequent change will switch it back to read-only here in the onChange
handler. Or this is purely theoretical? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It definitely seems messy. I think we should have a single computed that determines if the editor is editable and that we hand to the editor composable where it's watched and acted upon. That way the tiptap commands would live inside the composable only and the components would only deal with vue data structures.
this.$editor = null | ||
this.hasEditor = false | ||
this.editor.destroy() | ||
this.editor = undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that this is a moment when this.editor
can become undefined
. But since it's only called at the very end of beforeDestroy()
it should not be a problem, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. We can also just leave it and not set it to undefined. I was trying to replace this.hasEditor = false
here. It hides a bunch of children. I wonder if they caused exceptions before because $editor was destroyed but they still existed.
this.translateModal = false | ||
}, | ||
applyCommand(fn) { | ||
this.editor?.commands?.command(fn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This again has a little smell for me. We might expect applyCommand()
to actually do what it's called to do and rely on it later. Just silently ignoring when this.editor.commands
is not set looks potentially dangerous to me.
Thinking a bit more about it, I wonder whether such silent handling of undefined objects could contribute to the problem that we still have out-of-sync scenarios where steps didn't get applied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll see if i can initialize the editor right in the setup function to avoid the whole problem all together.
setup() { | ||
const { editor } = provideEditor() | ||
const { setEditable } = useEditorMethods(editor) | ||
provide(editorFlagsKey, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this. Why not use providEditorFlags()
from Editor.provider.ts
here as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to replace the inject that existed previously
[IS_RICH_EDITOR]: {
get: () => true,
},
But now the flags come in one object so I cannot set just one of them.
If we'd just use provideEditorFlags(props)
isRichEditor
might end up being false in particular if rich_editing_enabled
is set to false.
But I will move this to a helper function in Editor.provider.js
. I think that might be cleaner / more clear. Something like provideRichEditorFlag()
410df10
to
6f0db14
Compare
63b0b54
to
1cb8f09
Compare
This comment was marked as resolved.
This comment was marked as resolved.
Signed-off-by: Max <max@nextcloud.com>
The `Session` extension provides the `setSession` and `clearSession` commands. Session data is made available via `editor.storage.session`. This way session data can be provided after initializing the editor. Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
Allow initializing the editor before knowing the session. Signed-off-by: Max <max@nextcloud.com>
Only used once. Signed-off-by: Max <max@nextcloud.com>
initialize editor before lowlight has been loaded Wait with loading the actual content to ensure syntax highlighting is available. Signed-off-by: Max <max@nextcloud.com>
Cannot load it in setup yet as it requires the yjs provider, which in turn requires the sync service, which is only initialized in mounted. Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
The `character-count` extensions storage is a singleton and might be initialized by other editors loaded at the same time. See ueberdosis/tiptap#6060 Signed-off-by: Max <max@nextcloud.com>
The default slot somehow is not reactive. Signed-off-by: Max <max@nextcloud.com>
Signed-off-by: Max <max@nextcloud.com>
The editor storage of tiptap v2 is a singleton. We do not want to share the connection between multiple editors on the same page. Signed-off-by: Max <max@nextcloud.com>
The tiptap-text-direction extension only adds `dir` attributes when the document is updated. Use the `setContent` command in `setInitialYjsState` so it gets triggered and sets the editor up with the final state. This avoids another transaction after loading the yjs that gets recorded in the undo history. Signed-off-by: Max <max@nextcloud.com>
40133be
to
f186837
Compare
Signed-off-by: Max <max@nextcloud.com>
43ade53
to
ff4a5a9
Compare
Signed-off-by: Max <max@nextcloud.com>
335d30a
to
f7f5d0d
Compare
Signed-off-by: Max <max@nextcloud.com>
f7f5d0d
to
90b1d84
Compare
Use fixtures to initialize wrapper and destroy after the test Signed-off-by: Max <max@nextcloud.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice work ❤️
No description provided.