{"record":{"id":"dcbb1defdd4ea93b","repo":"toeverything/AFFiNE","slug":"errorcode-inlineeditorerror-dcbb1d","errorCode":"ErrorCode.InlineEditorError","errorMessage":"failed to find vElement for a text note in an embed element","messagePattern":"failed to find vElement for a text note in an embed element","errorType":"exception","errorClass":"BlockSuiteError","httpStatus":null,"severity":"error","filePath":"blocksuite/framework/std/src/inline/utils/range-conversion.ts","lineNumber":307,"sourceCode":"      if (startText && endText) {\n        break;\n      }\n\n      index += textLength;\n    }\n\n    // the one because of the line break\n    index += 1;\n  }\n\n  if (!startText || !endText) {\n    return null;\n  }\n\n  if (isInEmbedElement(startText)) {\n    const anchorVElement = startText.parentElement?.closest('v-element');\n    if (!anchorVElement) {\n      throw new BlockSuiteError(\n        ErrorCode.InlineEditorError,\n        'failed to find vElement for a text note in an embed element'\n      );\n    }\n    const nextSibling = anchorVElement.nextElementSibling;\n    if (!nextSibling) {\n      throw new BlockSuiteError(\n        ErrorCode.InlineEditorError,\n        'failed to find nextSibling sibling of an embed element'\n      );\n    }\n\n    const texts = getTextNodesFromElement(nextSibling);\n    if (texts.length === 0) {\n      throw new BlockSuiteError(\n        ErrorCode.InlineEditorError,\n        'text node in v-text not found'\n      );","sourceCodeStart":289,"sourceCodeEnd":325,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/blocksuite/framework/std/src/inline/utils/range-conversion.ts#L289-L325","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","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.","Wrap selection-setting code in try/catch for BlockSuiteError with code ErrorCode.InlineEditorError and retry after the next render.","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."],"exampleFix":"// before: custom embed renders a bare <img>\nrender() { return html`<img src=${this.src} />`; }\n\n// after: use the standard inline embed element so the v-element wrapper exists\nimport { VElement } from '@blocksuite/inline/elements';\nrender() { return html`<v-element><img src=${this.src} /></v-element>`; }","handlingStrategy":"try-catch","validationCode":"// Before setting a range that may land in an embed, verify the embed DOM is standard:\nconst embed = rootElement.querySelectorAll('v-element')[i];\nconst isStandard = embed instanceof HTMLElement && embed.closest('v-line') !== null;\nif (!isStandard) return; // skip selection into this embed","typeGuard":"function hasStandardEmbedWrapper(node: Text): boolean {\n  return node.parentElement?.closest('v-element') instanceof HTMLElement;\n}","tryCatchPattern":"import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';\ntry {\n  inlineEditor.setInlineRange(range);\n} catch (e) {\n  if (e instanceof BlockSuiteError && e.code === ErrorCode.InlineEditorError) {\n    // DOM not ready / non-standard embed: retry after next paint or clamp range\n    requestAnimationFrame(() => inlineEditor.setInlineRange(clampToText(range)));\n  } else throw e;\n}","preventionTips":["Build custom inline embeds on the standard VElement base so the v-element wrapper always exists.","Only call setInlineRange after the inline editor's render pass (await requestUpdate / rendered slot).","Avoid range indexes that land inside embeds; clamp to adjacent text."],"tags":["blocksuite","inline-editor","selection","dom-range","embed"],"backgroundTag":"rich-text-selection-range-mapping","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}