Create an atomic widget

Hello. I am trying to create a widget decoration (a checkbox) to replace text such as “- [x]”. I am usingViewPlugin.fromClass to create the plugin, and inside I use Decoration.replace({ widget: new CheckboxWidget(checked) }) to create decorations.

The issue is that when I use arrow keys to move the cursor around the checkbox I am expecting it to be acting like an atomic object (cursor will move to left or right in one key press), but actually the cursor will just stay in one place because looks like it jumps into the hidden text “- [ ]”.

I searched this forum and found the suggestion of using transactionFilter.of but I am not sure how to do it properly. I tried to create another Extension by writing something like EditorState.transactionFilter.of((tr) => {..., I guess I need to see if the position of transaction’s selection has a checkbox decoration and do something with it – seems to be really complicated, and I am not sure if there is an API for “getDecorationAtPosition()” at all.

Is there a plan to add atomic widget support? If not, I wonder if you could help me point to some sample codes for using transactionFilter to resolve this issue?

Thanks a lot!

Hi. The original idea was, indeed, for extensions to handle this with a transaction filter (and that’s probably still a reasonable approach in some cases), but indeed, if you’re using a view plugin to draw your decorations, there’s a pipeline mismatch (the view only updates when the new state has been computed, so a transaction filter, which is part of the process that creates a new state, cannot query view plugins). This patch adds a new feature, PluginField.atomicRanges, which you can use to signal that the ranges in given decoration set should be skipped over by cursor motion. Could you take a look and let me know if that works for you?

1 Like

Thank you Marijin. This is really an elegant solution and appreciate the built-in support.

I tried adding provide: PluginField.atomicRanges.from((v) => v.decorations) into my viewPlugin and it works as expected:

Screen Shot 2021-05-27 at 1.26.28 PM
(the checkbox is a marker for "- [x] " but the cursor can jump between it now)

Does this handle deletes as well?

I’m using a widget similarly to hide some markup and atomicRanges works for cursor movement but does not seem to handle deletes.

So I remapped the deleting keys to select the range before calling the regular delete command and that seemed to do the trick.

Is there a better way to do this? Here’s what my code does:

//only one selection range at a time
function selectTouchingSrcBlock(view: EditorView) {
    for (const {anchor, head} of view.state.selection.ranges) {
        view.plugin(srcBlockPlugin)!.decorations.between(anchor, head, (from: number, to: number, d: Decoration) => {
            view.dispatch(view.state.update({selection: {anchor: from, head: to}}))
            return false
        })
    }                                               
}

const deleteCharOrBlockForward: Command = view => {
    selectTouchingSrcBlock(view)
    return deleteCharForward(view)
}

Only if the command uses the cursor-motion methods to determine its range. By-word and by-line deletion does this, by-character deletion intentionally only deletes a single character, and didn’t use these methods. I’ve pushed a patch to make it explicitly check for atomic ranges.