-
Notifications
You must be signed in to change notification settings - Fork 109
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
mejo-
left a comment
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:
injectreturns 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 withoutprovideing a value.editorstarts 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 <[email protected]>
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 <[email protected]>
Signed-off-by: Max <[email protected]>
Signed-off-by: Max <[email protected]>
Allow initializing the editor before knowing the session. Signed-off-by: Max <[email protected]>
Only used once. Signed-off-by: Max <[email protected]>
initialize editor before lowlight has been loaded Wait with loading the actual content to ensure syntax highlighting is available. Signed-off-by: Max <[email protected]>
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 <[email protected]>
Signed-off-by: Max <[email protected]>
Signed-off-by: Max <[email protected]>
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 <[email protected]>
The default slot somehow is not reactive. Signed-off-by: Max <[email protected]>
Signed-off-by: Max <[email protected]>
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 <[email protected]>
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 <[email protected]>
40133be to
f186837
Compare
Signed-off-by: Max <[email protected]>
43ade53 to
ff4a5a9
Compare
Signed-off-by: Max <[email protected]>
335d30a to
f7f5d0d
Compare
Signed-off-by: Max <[email protected]>
f7f5d0d to
90b1d84
Compare
Use fixtures to initialize wrapper and destroy after the test Signed-off-by: Max <[email protected]>
mejo-
left a comment
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.