Help: Marquee Selection & Selecting Multiple Nodes for Dragging

Hello,

I’ve looked around quite a bit to try and find someone doing this already, but I haven’t had any luck.

Essentially, I’d like to be able to begin a selection in one node, and as I continue dragging outside of the boundaries of that node, I begin selecting the parent node or the next child.

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

I always wanted to try this, but I never found the time. But I think it should be possible.

1 Like

The table cell selection implementation might provide some inspiration, since it sounds similar.

1 Like

Thanks for the reference to table cell impl. It looks like there could be a lot of overlap with what I’m aiming to achieve.

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 could use the extent of the ‘primary’ range, if such a concept is meaningful. Basically, you want to provide something for code that doesn’t know how to work with multi-range selections to act on.

I built a bunch of handy utilities for moving block selections around the tree with arrow keys, similar to Notion. You can play around with it here.

Now I’m trying to extend this to handle mutli-block selections. Wish me luck!

3 Likes

Alright, so I implemented BlockSelection as a plugin that doesn’t use the window’s selection at all.

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?

I think I’m working on the same thing for tiptap :slight_smile:

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.

image

I’m making this open source in the next 1-2 weeks I think. :+1:

6 Likes

Why would you want that? With visible set to false the editor will hide the native selection. But it’s still good to have it, so that native editing actions do something reasonable.

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… :thinking:

A couple thoughts (personal opinions, really) on your GIF there:

  1. 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.

  2. 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…

I don’t get what you mean with this. It should always be possible to create some selection with TextSelection.between.

My point is that when you’re using the mouse the select blocks based on intersecting rects, there’s always a possibility that you aren’t selecting anything at all.

At the end of this gif, you’ll notice that there’s no selection at all:

khXMTfrfTA

When subclassing Selection, however, it appears there must be some selection which (it seems) makes this kind of behavior impossible.

The implementation as seen on the GIF looks awesome! I am currently working on implementing a similar behavior. Can you give any update regarding open sourcing it? Would really appreciate it :slightly_smiling_face:

1 Like

Would love to see this in tiptap! We are using the library extensively, multi range selection is not high priority but it would be a great addition to our app. Thanks for your great work