Skip to content

Custom wrapper plugin for blocks #7195

@urodstvo

Description

@urodstvo

Affected Packages

Vue TipTap

Version(s)

1.18.0

Bug Description

I need to write a block that could top-level other blocks.
For example, if the cursor is in a table cell and I would create a chunk, then the entire table would be wrapped in a chunk.
My implementation has bugs related to the fact that it is not allowed to merge or split a chunk. Each chunk is unique and has its own identifier.
The most common Uncaught TypeError in the current implementation is: Cannot read properties of null (reading 'nextSibling') and the phantom blocks remaining after deletion, although they are not in the doc.
Please help me fix this plugin, or maybe send me some similar ones. I'd be grateful!

I can't use newer versions of the package

Browser Used

Chrome

Code Example URL

No response

Expected Behavior

  • The plugin must enable the creation of a "chunk" block that serves as a top-level container capable of wrapping other blocks.
  • When a user initiates the creation of a chunk while the cursor is positioned within a nested or child element (e.g., a table cell), the plugin shall automatically wrap the entire parent structure (e.g., the full table) within the new chunk block.
  • This wrapping operation must preserve the integrity and hierarchy of the wrapped blocks, ensuring no loss of content, structure, or attributes during the process.
  • The plugin shall prohibit merging of multiple chunks into a single chunk.
  • The plugin shall prohibit splitting a single chunk into multiple chunks.

Additional Context (Optional)

export default class Chunk extends Node {
    get name() {
        return 'chunk';
    }

    get schema() {
        return {
            attrs: {
                title: {
                    default: '',
                },
                cid: {
                    default: '',
                },
            },
            content: 'block+',
            group: 'block',
            defining: true,
            draggable: false,
            parseDOM: [
                {
                    tag: 'chunk',
                    getAttrs: dom => ({
                        title: dom.getAttribute('title')  '',
                        cid: dom.getAttribute('cid')  '',
                    }),
                },
            ],
            toDOM: node => [
                'chunk',
                {
                    title: node.attrs.title,
                    cid: node.attrs.cid,
                    class: 'chunk-wrapper',
                },
                0,
            ],
        };
    }

    commands({ type }) {
        return {
            createChunk: (attrs = {}) => (state, dispatch) => {
                const blocksToWrap = findTopLevelBlocksToWrap(state);
                if (blocksToWrap.length === 0) {
                    return false;
                }

                const startPos = blocksToWrap[0].start;
                const endPos = blocksToWrap[blocksToWrap.length - 1].end;

                const $start = state.doc.resolve(startPos);
                const $end = state.doc.resolve(endPos);

                const range = $start.blockRange($end);

                if (!range) {
                    return false;
                }

                if (!type.validContent(state.doc.slice(range.start, range.end).content)) {
                    return false;
                }

                const tr = state.tr.wrap(range, [{ type, attrs }]);
                if (dispatch) {
                    dispatch(tr);
                }

                return true;
            },

            deleteChunk: () => (state, dispatch) => {
                const { $from } = state.selection;

                let chunkPos = null;
                let chunkNode = null;

                for (let { depth } = $from; depth > 0; depth--) {
                    const node = $from.node(depth);
                    if (node.type.name === 'chunk') {
                        chunkNode = node;
                        chunkPos = $from.before(depth);
                        break;
                    }
                }

                if (!chunkNode) {
                    return false;
                }

                const chunkStart = chunkPos;
                const chunkEnd = chunkPos + chunkNode.nodeSize;

                const { tr } = state;
                const chunkContent = chunkNode.content;
                tr.replaceWith(chunkStart, chunkEnd, chunkContent);

                if (dispatch) {
                    dispatch(tr);
                }

                return true;
            },
        };
    }
}

export const findTopLevelBlocksToWrap = state => {
    const { $from, $to } = state.selection;
    const blocksToWrap = [];

    state.doc.descendants((node, pos, parent, index) => {
        if (parent === state.doc) {
            const nodeStart = pos + 1;
            const nodeEnd = pos + node.nodeSize;

            if (nodeStart <= $to.pos && nodeEnd >= $from.pos && node.type.isBlock) {
                blocksToWrap.push({
                    node,
                    start: nodeStart,
                    end: nodeEnd,
                    index,
                });
            }
        }
    });

    blocksToWrap.sort((a, b) => a.index - b.index);

    return blocksToWrap;
};

Dependency Updates

  • Yes, I've updated all my dependencies.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Open SourceThe issue or pull reuqest is related to the open source packages of Tiptap.

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions