This mimics the behavior of selections in a software called Notion.
How would everyone recommend going about this?
I started down a path of assigning all draggable “blocks” unique ids, so I could potentially manage block selection decoration via CSS by targeting individual ids in a style tag (rather than updating the dom), but I’m not sure if that’s a worthy approach.
I would prefer it if I didn’t have to modify the node positions in order to support this as I think that could make multiplayer more complex.
Please let me know if you have any suggestions at all for this problem! I am just getting started, so I have a lot of flexibility to take alternative approaches.
For reference, Notion uses the content editable approach, but each block is an isolated content editable instance. So, they end up managing all block selection and dragging separate from content editable functionaliy. Another interesting constraint in Notion, is that there aren’t any embedded blocks, all blocks live at the top level. In my app with Prosemirror, there is quite a bit of nesting and constraints for what can go where.
Screenshot of Notion’s DOM during a selection for reference
In my opinion that’s annoying behavior that users don’t really want. But if you really want it to work like that why not use a block style editor that works like that out of the box? Like https://editorjs.io/
For non-continuous multi-node selections – like an array of NodeSelection – what’s the sensible approach when extending Selection (if there is one)? this.ranges makes sense to use, but are there meaningful values for this.$anchor and this.$head?
I tried looking at the table cell selection for inspiration but because its still a continuous row or column cell selection, extending the selection with “proper” anchor and head values there is easier.
You can play with it here – escape will create a BlockSelection and you can shift-click and shift-arrow around to create multi-block selections. For now, there’s no drag selection or disjointed selections.
The weird thing I noticed is that it’s not possible for ProseMirror to not have a selection so there’s always a text selection going on behind the BlockSelection.
Any ideas how to make this BlockSelection work as a proper selection rather than a plugin? Is it possible to create a totally custom selection type that doesn’t set the browser window selection?
There are two extensions working together here. One for the custom selection and the other for dragging multiple blocks with a drag handle.
I created a custom selection type called NodeRangeSelection (you can look at the CellSelection im prosemirror-tables for inspiration). It behaves like a range of node selections on a shared depth. This depth is configurable so you are able to select only top level nodes if you want to. There is also support for Shift+Arrow navigation.
I’m making this open source in the next 1-2 weeks I think.
Suppose I want to create multiple disjointed block selections. Maybe I want to select 3 non-contiguous blocks and do something with them (maybe delete them or highlight them or drag them somewhere).
Or maybe I’m just misunderstanding something… I guess NodeSelection uses visible: false to hide the actual native selection. And if I subclass Selection then I can pass multiple disjoint ranges… I suppose the benefit of subclassing Selection rather than using a totally custom data structure is to interoperate with other commands that operate across selection ranges…
A couple thoughts (personal opinions, really) on your GIF there:
I think that ideally, block selection and text selection should be different modes. One of the biggest complaints about Notion’s editor is that you can’t select text across multiple paragraphs as you’d expect (people end up selecting multiple blocks, pasting into a “normal editor” and then clipping the selection they really want). This is especially troublesome on mobile where there is (currently) no block multi-selection (and if there was, it would definitely be modal).
I personally dislike modal editors like Vim, but I think there’s some UX tricks that could make things simpler – selection that starts from outside the document margin can be block selection while selection from inside the document is text selection. There are also some other interactions you can do for transitioning between the two such as double clicking.
The <ul> wrappers are a little bit cumbersome to deal with from a UX perspective. I don’t think users have an understanding of the underlying HTML tree when editing a document. For example, its possible to see two bullets next to each other that are in different ul’s. User’s don’t see that or really even care because it looks the same. But its weird when users are creating a block selection and notice this ul selection for the entire list – its a part of the hierarchy that isn’t really visible to the user. I’ve been trying to figure out how to make it so that the wrapping ul is never selected as part of the hierarchy so that users can simply work with list items instead and always collapse consecutive ul/ols into the same list. (I’m not sure this point is very clear, but let me know if it makes sense)
Yes, that would be the way to implement multi-range selections. There’s still one ‘primary’ range, which code that doesn’t handle multiple selections will act on, but the Selection class is intended to be general enough for use cases like this. A totally custom data structure wouldn’t actually be stored as editor state selection and thus produce a lot more issues.
@marijn I’ve got multi-selections working by subclassing Selection. But now I’m trying to implement a drag selection similar to Notion.
The tricky thing is that if you don’t select across any blocks, then there’s “no selection”. I remember seeing mention of this in some older threads, but I’m curious how you think I might implement this kind of “no selection” behavior.
My plan is that when holding the meta key, you get a rectangular selection area that selects nodes who’s rects intersect with the selection. However, it’s totally possible to start your selection in the margin of the document and never intersect with a node’s rect…
If I choose something arbitrary like placing the cursor at position 0, then I’d still have to override behaviors for things like Delete. And if we allow for arbitrary “none” selections, then we’d introduce a bunch of runtime errors expecting state.selection.$from to be defined…