Open rChaoz opened 3 days ago
We also experienced the same bug, but create our own layer as a workaround. It tries to mimics NodePos, but uses a logic that matches with PM's pos values. If our code helps in any way, feel free to use it:
import { Editor } from "@tiptap/core";
import { Node, ResolvedPos } from "@tiptap/pm/model";
import { Transaction } from "@tiptap/pm/state";
class Selector
{
private typeName?: string;
private attributeKey?: string;
private attributeValue?: string;
constructor(selector: string)
{
selector = selector.trim();
const pAttr = selector.indexOf('[');
if (pAttr >= 0 && selector.endsWith(']')) {
const attr = selector.substring(pAttr + 1, selector.length - 1).trim();
selector = selector.substring(0, pAttr).trim();
const pAttrValue = attr.indexOf('=');
if (pAttrValue >= 0) {
this.attributeKey = attr.substring(0, pAttrValue).trim();
this.attributeValue = attr.substring(pAttrValue + 1).trim();
}
else {
this.attributeKey = attr.trim();
}
}
if (selector.length > 0) {
this.typeName = selector;
}
}
public matches(node: Node): boolean
{
if (this.typeName && node.type.name !== this.typeName) {
return false;
}
if (this.attributeKey) {
if (this.attributeValue) {
return node.attrs[this.attributeKey] === this.attributeValue;
}
return this.attributeKey in node.attrs;
}
return true;
}
}
export class ResolvedNodePos
{
public static readonly DOC_POS = -1;
private readonly editor: Editor;
public readonly node: Node | null = null;
public readonly pos: number;
public static atRoot(editor: Editor): ResolvedNodePos
{
return new ResolvedNodePos(editor, ResolvedNodePos.DOC_POS);
}
public static atCursor(editor: Editor): ResolvedNodePos
{
return new ResolvedNodePos(editor, editor.state.selection.from);
}
public static at(editor: Editor, pos: number): ResolvedNodePos
{
return new ResolvedNodePos(editor, pos);
}
private constructor(editor: Editor, pos: number, node?: Node)
{
this.editor = editor;
this.pos = pos;
if (node) {
this.node = node;
}
else {
this.node = pos === ResolvedNodePos.DOC_POS ? editor.state.doc : editor.state.doc.nodeAt(pos);
// If we are not directly at the node, we need to resolve it
if (this.node === null) {
this.node = editor.state.doc.resolve(pos).node();
}
}
}
public get size(): number
{
return this.node ? this.node.nodeSize : 0;
}
public get to()
{
return this.pos + this.size;
}
public get empty(): boolean
{
return this.node === null
|| (this.node.isBlock && this.node.nodeSize === 2)
|| this.node.nodeSize === 0;
}
public closest(selector: string): ResolvedNodePos | undefined
{
const sel = new Selector(selector);
const doc = this.editor.state.doc;
let pos = this.pos >= 0 ? this.pos : 0;
let resolvedPos: ResolvedPos = doc.resolve(pos);
let checkParent = true;
while (checkParent) {
const node = doc.nodeAt(pos);
if (node && sel.matches(node)) {
return new ResolvedNodePos(this.editor, pos, node);
}
if (resolvedPos.depth > 0) {
pos = resolvedPos.before();
resolvedPos = doc.resolve(pos);
}
else {
checkParent = false;
}
}
return undefined;
}
public querySelector(selector: string): ResolvedNodePos | undefined
{
let result: ResolvedNodePos | undefined = undefined;
const sel = new Selector(selector);
const startNode = this.pos === -1 ? this.editor.state.doc : this.node;
startNode?.descendants((descendantNode, offset) => {
if (result !== undefined) {
return false; // Stop the search
}
if (sel.matches(descendantNode)) {
result = new ResolvedNodePos(this.editor, this.pos + offset + 1, descendantNode);
return false; // Stop the search
}
return true;
});
return result;
}
private addMeta(tr: Transaction, meta?: Record<string, any>): void
{
if (meta) {
Object.keys(meta).forEach(key => {
tr.setMeta(key, meta[key]);
});
}
}
public getAttribute(attributeKey: string): string | undefined
{
return this.node ? this.node.attrs[attributeKey] : undefined;
}
public setAttribute(attributeKey: string, attributeValue: any, meta?: Record<string, any>): void
{
if (this.node) {
const tr = this.editor.state.tr;
this.addMeta(tr, meta);
tr.setNodeAttribute(this.pos, attributeKey, attributeValue);
this.editor.view.dispatch(tr);
}
}
public setAttributes(editor: Editor, pos: number, attributes: Record<string, any>, meta?: Record<string, any>): void
{
if (this.node) {
const tr = this.editor.state.tr;
this.addMeta(tr, meta);
Object.keys(attributes).forEach(key => {
tr.setNodeAttribute(pos, key, attributes[key]);
});
this.editor.view.dispatch(tr);
}
}
}
Affected Packages
core
Version(s)
2.7.3
Bug Description
The value in
NodePos
seems to be different than the "real" ProseMirror pos (larger by 1).Browser Used
Firefox
Code Example URL
https://codesandbox.io/p/sandbox/lively-grass-pqndvk
Expected Behavior
The positions are equal
Additional Context (Optional)
TipTap pos is 1 larger than actual PM pos
Dependency Updates