toeverything/AFFiNE · error · BlockSuiteError

ErrorCode.InlineEditorError

ErrorCode.InlineEditorError

Error message

failed to find vElement for a text note in an embed element

What it means

Thrown by inlineRangeToDomRange() in blocksuite/framework/std/src/inline/utils/range-conversion.ts, which converts an InlineRange (index/length in the inline text model) into a DOM Range. When the computed anchor text node sits inside an inline embed (isInEmbedElement returns true), the code walks up from startText.parentElement to the nearest 'v-element' custom element to relocate the caret to the embed's neighbor. If the embed's DOM was not rendered with the standard v-element wrapper (or the parent chain is detached), closest('v-element') returns null and this invariant error is thrown.

Solutions

  1. Make custom embed renderers output the standard structure (wrap the embed content in a v-element via the v-element base class / VElement component) so closest('v-element') resolves.
  2. Await the inline editor's render (e.g. requestUpdate + rAF, or inlineEditor.slots.rendered) after mutating embeds before setting a selection that touches the embed.
  3. Wrap selection-setting code in try/catch for BlockSuiteError with code ErrorCode.InlineEditorError and retry after the next render.
  4. If the embed is the last node in a line, ensure the editor still renders a trailing text/gap node after it so neighbor lookups succeed.

Example fix

// before: custom embed renders a bare <img>
render() { return html`<img src=${this.src} />`; }

// after: use the standard inline embed element so the v-element wrapper exists
import { VElement } from '@blocksuite/inline/elements';
render() { return html`<v-element><img src=${this.src} /></v-element>`; }
Defensive patterns

Strategy: try-catch

Validate before calling

// Before setting a range that may land in an embed, verify the embed DOM is standard:
const embed = rootElement.querySelectorAll('v-element')[i];
const isStandard = embed instanceof HTMLElement && embed.closest('v-line') !== null;
if (!isStandard) return; // skip selection into this embed

Type guard

function hasStandardEmbedWrapper(node: Text): boolean {
  return node.parentElement?.closest('v-element') instanceof HTMLElement;
}

Try / catch

import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
try {
  inlineEditor.setInlineRange(range);
} catch (e) {
  if (e instanceof BlockSuiteError && e.code === ErrorCode.InlineEditorError) {
    // DOM not ready / non-standard embed: retry after next paint or clamp range
    requestAnimationFrame(() => inlineEditor.setInlineRange(clampToText(range)));
  } else throw e;
}

Prevention

When it happens

Trigger: Calling inlineEditor.setInlineRange(), syncing native selection, or any API that maps a model range to a DOM range when the range boundary (index) lands inside an inline embed element whose rendered DOM is not wrapped in a <v-element> tag: a custom embed renderer returning non-standard markup, DOM mutated mid-conversion, or a v-element that was removed from the document before conversion ran.

Common situations: Custom inline embeds (mentions, inline images, formulas) whose render() does not use the standard v-element structure; races between a model update and the next Lit render pass; SSR or test environments where the custom elements are not upgraded/registered.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/dcbb1defdd4ea93b. Report an issue: GitHub.

Appendix: source

Thrown at blocksuite/framework/std/src/inline/utils/range-conversion.ts:307

      if (startText && endText) {
        break;
      }

      index += textLength;
    }

    // the one because of the line break
    index += 1;
  }

  if (!startText || !endText) {
    return null;
  }

  if (isInEmbedElement(startText)) {
    const anchorVElement = startText.parentElement?.closest('v-element');
    if (!anchorVElement) {
      throw new BlockSuiteError(
        ErrorCode.InlineEditorError,
        'failed to find vElement for a text note in an embed element'
      );
    }
    const nextSibling = anchorVElement.nextElementSibling;
    if (!nextSibling) {
      throw new BlockSuiteError(
        ErrorCode.InlineEditorError,
        'failed to find nextSibling sibling of an embed element'
      );
    }

    const texts = getTextNodesFromElement(nextSibling);
    if (texts.length === 0) {
      throw new BlockSuiteError(
        ErrorCode.InlineEditorError,
        'text node in v-text not found'
      );

View on GitHub (pinned to b4c8548c09)