-
Notifications
You must be signed in to change notification settings - Fork 6
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
What is a minimal way of implementing suggestions (autocomplete) with prosemirror-svelte? #1
Comments
Do you mean autocomplete for a single-value field or hashtag/mention support within a document? Let me assume that you mean Hashtags/Mentions (because this is usually a bit more involved). Essentially, these individual points need to be solved:
Note: Point number 4 could be done by either simply replacing some text (and rendering hashtags by a decoration plugin) or by replacing it with a new (inline) entity from your schema. OK. There are two ways to approach all of this:
Prosemirror-Svelte is essentially a result of using Prosemirror inside a (yet to launch) production application and in that context I decided to go for the second route. It works well, but unfortunately it's rather specific code and would not serve as an easily digestible example. Nevertheless, also based on other feedback, I recently have been contemplating a lot about how to best provide extension functionality and especially how to merge Svelte and non-Svelte frameworks like Prosemirror. Probably the best solution (especially for a modular framework) would be to render Svelte components directly from a prosemirror plugin. I already built a basic prototype to to do just that (it's not that hard using the client component api but have not yet decided on a decent API to put this into this library. I should be able to allocate some time later this week to take another shot at this. Until then: I hope this already helped a bit. |
Thanks for the detailed explanation.
I'm not sure what you mean by single-value field, but mention is definitely what I meant. In a more general way, Atlassian's Atlaskit has a great React implementation of a comprehensive Prosemirror-based editor, which also includes autocomplete, see this example, try to type
This architecture may stop us using existing pure PM plugins, fo instance prosemirror-suggestions plugin which is headless and leaves the ui to library user. I think ui plugins should to be designed in this way to be platform independent. We need some layer of abstraction to make building Svelte ui on top such plugins easier. I do not know if there is a general enough pattern to let such abstraction happen.
Glad to know this is being used in production and it's not just a hobby project! |
Prosemirror-Suggestions (i.e. just dispatching events from the plugin) is essentially what I did before. Yet, this has the downside that the UI on top is decoupled from the plugin. At the beginning I actually liked this approach, but I more and more feel like it would be better to encapsulate all behavior into the plugin (mainly because the way how Svelte handles component composition). Maybe even going as far as encapsulating the schema extension. To ensure the plugin stays "renderless", the render logic could be passed when creating the plugin instance. In some frameworks, like React, I would probably choose something like passing a renderProp to the plugin. Svelte does not really have this concept - even slots require an existing component. But it would be possible to pass a complete component to be rendered to the plugin. What I meant was something along the lines of this example. It's really just a rough prototype, but it proofs that it's possible to instantiate the plugin with a component - which can communicate with the other Svelte components, e.g. using stores. |
I actually I find this a plus. In prosemirror-suggestions plugin, there is a lot of logic that can and should be shared between different ui frameworks. There is no reason to once implement this logic for Vue, then for Svelte, then for React etc. Feels like this is a design problem with Prosemirror, where plugins must be hardwired to the UI. Maybe it needs something like a concept of plugin models where the logic can be shared among frameworks. Of course what can be done today is to simply setup a renderless plugin like prosemirror-suggestions with even listeners that render svelte components. I believe this is what your example is supposed to do, but it did not work for me when I tried to run it. |
Well, to a large degree that is possible with Prosemirror, but at some point you need to render UI... I guess it's more an issue with a lot of frameworks. Svelte - to my experience - is relatively flexible in this regard. It lets you render components from anywhere using the client component API. In the meantime I created another prototype on my local machine and it worked pretty well.
The example renders a popup at the current cursor (selection). It moves with the cursor as long as the selection is empty and hides once text is selected. The component is rendered using svelte but from within a plugin. The plugin is defined in plugin.js. It takes a Svelte component (which in this example should implement the props top, left and visible) and then updates it when the prosemirror editor view updates. import {Plugin} from 'prosemirror-state'
export const currentPosition = Component => new Plugin({
view: function view (editorView) {
const _popup = new Component({
target: document.body,
props: { top: 100, left: 0, visible: false }
})
return {
_popup,
update: function update(editorView) {
const selection = editorView.state.selection
const visible = selection.empty && editorView.hasFocus()
const pos = editorView.coordsAtPos(selection.head)
this._popup.$set({top: pos.top + 20, left: pos.left, visible})
},
destroy: function destroy() {
this._popup.$destroy()
} Here is the point where the plugin is instantiated and receives the Svelte component: import Popup from './Popup.svelte'
import { currentPosition, focusEvents } from './plugin.js'
const currentPositionPopup = currentPosition(Popup) Previously I implemented the tag functionality like this: <ProsemirrorEditor {editorState}
...
on:tagover={handleTagOver}
on:tagleave={handleTagLeave}/>
{#if tags}
<TagSelector
...
{tags}
on:select={handleSelectTag}/>
{/if} It means that any component that potentially needs to supports tags needs to setup their own handlers and UI. If the whole functionality could be encapsulated inside a plugin then the components can stay "clean" and depending on how the editor state is initialized, functionality is added on top. |
I may be missing some point here. In this part, what is the benefit of passing the Popup component to the plugin generator? const currentPositionPopup = currentPosition(Popup) Why not instantiate Popup component directly in plugin.js? export const currentPosition = () => new Plugin({
view: function view (editorView) {
const _popup = new Popup({
target: document.body,
props: { top: 100, left: 0, visible: false }
})
// ... |
This way the plugin only contains its core logic but leaves the definition of the UI to the caller. The plugin (e.g. to show hashtags or a menu) could be provided by the library - passing props from the current editor state/ editor props to it (e.g. where the cursor is currently located or whether it is currently inside a hashtag). And the component to render a UI based on that props could be provided by whoever wants to use the plugin. In frameworks like React, it would probably be more elegant to use render props, but this concept does not really exist in Svelte (there are slots, but they would be tied to the editor - and therefore make it difficult/ impossible for external plugins to use them). Note: The example uses document.body for simplicity sake. But it is possible to use a node around the editor as mounting point as well, therefore also not causing any affects to the rest of the page. I already tried to conditionally render the editor with components rendered next to it and they properly mount/unmount along with the editor. /** Called when the plugin is linked to the editor view
* @param editorView {EditorView} */
view(editorView) {
const state = pluginKey.getState(editorView.state);
let svelteComponent;
if (Component) {
const editorDiv = editorView.dom;
const container = editorDiv.parentNode;
svelteComponent = new Component({
target: container,
props: {...}
});
} |
@christianheine , sorry for the offtopic, didn't want to create issue for this, but do you plan to improve this repo? I thought it was kind of dead until you responded last issue |
Fully understand your concern. The answer is a bit complicated. The repo started out from the personal need to link between Prosemirror and Svelte. I needed a flexible and extensible text editor, but I did not want to have a heavy overhead like TipTap (I felt that Prosemirror was already powerful enough). So I created this very straightforward integration and open-sourced it because I wanted more people harness the power of these two frameworks. ProsemirrorSvelte was born. Btw, it's not just a pet project, but it powers the text entry in my startup Faden (https://www.faden.cloud/). Quickly after ProsemirrorSvelte's inception, I received various requests on how to do certain things. But, at a deeper level almost all of them were really about "how do I do certain things in Prosemirror"? Prosemirror is a great framework, but due to its flexibility & modularity it requires a certain amount of upfront investment to really understand how things work. I feel it's worth the time, but I understand that some people just want a drop-in editor. In the app I mentioned above, I also realized that some things - like mapping the prosemirror state to a different data model - came up repeatedly. I did not want to bloat this repo (since some people might just want the EditorState directly), so I started to an abstraction layer on top of this one (the project was christened "Promira"). Promira got all the bells and whistles which would be required to make the whole thing more accessible. It uses a kind of wrapped editor state, which makes transformation, serialization & deserialization much easier. But, it always had some annoying quirks when the two worlds were bridged (e.g. selection control from reactive updates. Especially when the user is currently editing). I found stable ways to address these in the above mentioned app, but these fixes required an even deeper level of understanding of both Prosemirror and Svelte than if you would use the basic integration. In fact, without wanting it, I went halfway down the "TipTap" route (which is not a bad thing. Others tried the same. It's just a huge time commitment). My current plan is to take the best from Promira but move back to the core philosophy of this repo - using EditorState directly. Let me take another look at this over the weekend. At least to give you a better idea on what I could commit to in foreseeable future. Out of curiosity: What are you mainly missing from this repo? |
OK, so I reviewed the current codebase. I think I should be able to ease out the edges in 1-2 days of effort. Will try to get this done over the next ~2 weeks. |
@christianheine it was not smth in particular. I just started with it and it felt like a too basic experience for fast development. I used createTextEditor functions but then I realized they need customization options which they don`t have, so I modified your files and started going deep into Prosemirror, unfortunately on this stage I always have not much time in bulk to get it finished. Generally speaking I was looking for a library that can simplify Prosemirror development, perhaps by taking away some features from me. Or having implemented examples that will be enough to cover basic needs. Think same feeling as a topic starter. Generally speaking I think a lot of guys are looking for the most basics things, and if any of these can be implemented / simplified - that would be cool:
|
Another month has passed and no updates here, I know. For the last few of months, I've been pulled deeper and deeper into the mobile development rabbit hole with Flutter & Dart. So for anyone waiting for an update, let me at least express that I'm sorry things are pretty stale here. In the meantime, I stumbled upon another repo by @PierBover |
Tiptap and tiptap-svelte come with an implementation of suggestions (autocomplete), but I personally find their approach complicated and busy.
As you can see, the suggestions example of tiptap-svelte import many moving parts, where PM and svelte are not well separated.
I really like the approach in this project over Tiptap, I think it makes the overall application code size less and with less complexity. What would be the right way of implementing a suggestion capability using this library?
This does not have to be part of the library, but an example of demonstrating how to implement such thing would be great.
The text was updated successfully, but these errors were encountered: