Skip to content
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

Fix: nodeInputRule() support for group match #1574

Merged
merged 2 commits into from Sep 22, 2021

Conversation

nokola
Copy link
Contributor

@nokola nokola commented Jul 11, 2021

Fixes in nodeInputRule()

Fixes in nodeInputRule()
- add support for "first group match, if any" similar to https://prosemirror.net/docs/ref/#inputrules
- fix issue where rewriting includes extra unnecessary character from the match
@philippkuehn
Copy link
Contributor

Hey, sorry for that delay. Can you please show me a code example of what your PR is trying to fix? Thanks!

@nokola
Copy link
Contributor Author

nokola commented Sep 13, 2021

No problem!
Here's a description of what I was trying to do and the part that didn't work before and works now:
I was trying to add a custom task input rule where when I type "--" and press space, a new "task" node is created.
Steps to reproduce:

  1. Type "--". Press space.

Expected:
A task is created and space persists as well. Click on the images - they are animated gifs, to see the video:
expected

Actual:
task is created, but the space between it and the task text is "eaten" by nodeInputRule:
actual
Fixed by the change in this PR: If nodeInputRule detects a group, it will create the node in-place of the group and leave the other text (in this case the "space") unchanged.

Here's my code, removed some of it for brevity:

declare module '@tiptap/core' {
    interface Commands<ReturnType> {
        scribeTask: {
            /** 
             * Add a scribe task
             */
            setScribeTask: () => ReturnType,
        }
    }
}

export interface ScribeTaskOptions {
    HTMLAttributes: Record<string, any>,
}

export const ScribeTask = Node.create<ScribeTaskOptions>({
    name: 'scribeTask',
    group: 'inline',
    inline: true,

    defaultOptions: {
        HTMLAttributes: {},
    },

    parseHTML() {
        return [{ tag: 'task' }];
    },

    renderHTML({ HTMLAttributes }) {
        return ['task', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
    },

    addNodeView() {
        return ({ editor, node, getPos, HTMLAttributes, decorations, extension }) => {
            const dom = document.createElement('span');

            const toc = new ScribeTaskUI({
                target: dom,
                props: {
                    state: node.attrs.state,
                    // attrs: HTMLAttributes,
                }
            });

            return { dom };
        }
    },

    addCommands() {
// ... removed ...
        };
    },

    addAttributes() {
        return {
            state: {
                default: TaskState.TODO,
                parseHTML: element => (element.getAttribute('data-state') ?? TaskState.TODO),
                renderHTML: attributes => ({
                    'data-state': attributes.state,
                }),
                keepOnSplit: false,
            },
        }
    },

    addInputRules() {
        return [
            nodeInputRule(/^\s*((?:DONE|CUT)?\s*(?:--))\s+$/, this.type as any, parseState),
        ];
    },

    addPasteRules() {
        return [
            nodePasteRule(/^\s*((?:DONE|CUT)?\s*(?:--))\s+.*$/gm, this.type as any, parseState),
        ];
    },
});

enum TaskState {
    TODO = "TODO",
    DONE = "DONE",
    CUT = "CUT"
}

function parseState(match: string[]): { state: string } {
    if (match[1].indexOf(TaskState.DONE) >= 0) {
        return { state: TaskState.DONE };
    }
    if (match[1].indexOf(TaskState.CUT) >= 0) {
        return { state: TaskState.CUT };
    }
    return { state: TaskState.TODO };
}

@sald19
Copy link

sald19 commented Sep 16, 2021

Hi there 👋🏽, I made a hashtag extension using Node.create() and I am having the same problem, which removes a space before the text that is wrapped in the extension <span>

CleanShot 2021-09-16 at 10 49 48

I also have problems adding extra space after the span that creates the extension

CleanShot 2021-09-16 at 10 57 19

@philippkuehn
Copy link
Contributor

@nokola This explains why you replaced start - 1 with start, great.

I’m currently on a rewrite of inputrules and pasterules so things may change anytime soon.

@philippkuehn philippkuehn merged commit 8ee0d67 into ueberdosis:main Sep 22, 2021
@nokola
Copy link
Contributor Author

nokola commented Sep 23, 2021

I ended up extending nodeInputRule() even further to support any text passed by user to replace in replaceText. This makes the rule more stable and easy to understand by callers. Feel free to use this code if it's useful in your updates:

import type { NodeType } from 'prosemirror-model'
import { InputRule } from 'prosemirror-inputrules'

export type NodeMatchInfo = {
    replaceText?: string,
    attrs: Record<string, any>,
};

export default function (regexp: RegExp, type: NodeType, getMatchInfo?: ((match: RegExpExecArray) => NodeMatchInfo)): InputRule {
    return new InputRule(regexp, (state, match, start, end) => {
        const info: NodeMatchInfo | null = getMatchInfo == null ? null : getMatchInfo(match as RegExpExecArray);
        const { tr } = state;

        if (info?.replaceText) {
            const offset = match[0].lastIndexOf(info.replaceText);
            let matchStart = start + offset;
            if (matchStart > end) {
                matchStart = end;
            }
            else {
                end = matchStart + info.replaceText.length;
            }

            // insert last typed character
            const lastChar = match[0][match[0].length - 1];
            tr.insertText(lastChar, start + match[0].length - 1);

            // insert node from input rule
            tr.replaceWith(matchStart, end, type.create(info?.attrs));
        } else if (match[0]) {
            tr.replaceWith(start, end, type.create(info?.attrs));
        }

        return tr;
    })
}

sample use:

    addInputRules() {
        return [
            nodeInputRule(/^\s*((?:DONE|CUT)?\s*(?:--))\s+$/, this.type as any, parseState),
        ];
    },

enum TaskState {
    TODO = "TODO",
    DONE = "DONE",
    CUT = "CUT"
}

function parseState(match: RegExpExecArray): NodeMatchInfo {
    if (match[1].indexOf(TaskState.DONE) >= 0) {
        return {
            replaceText: match[1],
            attrs: { state: TaskState.DONE },
        };
    }
    if (match[1].indexOf(TaskState.CUT) >= 0) {
        return {
            replaceText: match[1],
            attrs: { state: TaskState.CUT },
        };
    }
    return {
        replaceText: match[1],
        attrs: { state: TaskState.TODO },
    };

}

Tasks look like:

-- task 1
DONE-- task 2
CUT-- task 3

nokola added a commit to nokola/tiptap that referenced this pull request Oct 11, 2021
Image input rule leaves erroneous text behind due to my previous change to add group matching in ueberdosis#1574.

This change adds a `( ... )` regex group around the image input rule to have it work with the new code.
philippkuehn pushed a commit that referenced this pull request Oct 11, 2021
Image input rule leaves erroneous text behind due to my previous change to add group matching in #1574.

This change adds a `( ... )` regex group around the image input rule to have it work with the new code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants