fix(editor): use nullable inline editor root element (#9320)

Fixes `sentry-7906c03b79a54ede819c56cc15ad9889`
This commit is contained in:
doodlewind
2024-12-26 07:55:15 +00:00
parent cb4dd127fd
commit 71b5cddea1
16 changed files with 58 additions and 21 deletions

View File

@@ -1,4 +1,4 @@
/* eslint-disable no-useless-escape */ /* oxlint-disable no-useless-escape */
import type { BlockComponent, ExtensionType } from '@blocksuite/block-std'; import type { BlockComponent, ExtensionType } from '@blocksuite/block-std';
import { import {
KEYBOARD_ALLOW_DEFAULT, KEYBOARD_ALLOW_DEFAULT,
@@ -450,6 +450,7 @@ export const LatexExtension = InlineMarkdownExtension({
undoManager.stopCapturing(); undoManager.stopCapturing();
if (!inlineEditor.rootElement) return KEYBOARD_ALLOW_DEFAULT;
const blockComponent = const blockComponent =
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]'); inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
if (!blockComponent) return KEYBOARD_ALLOW_DEFAULT; if (!blockComponent) return KEYBOARD_ALLOW_DEFAULT;

View File

@@ -103,7 +103,8 @@ export class AffineLink extends ShadowlessElement {
// Please also note that when readonly mode active, // Please also note that when readonly mode active,
// this workaround is not necessary and links work normally. // this workaround is not necessary and links work normally.
get block() { get block() {
const block = this.inlineEditor?.rootElement.closest<BlockComponent>( if (!this.inlineEditor?.rootElement) return null;
const block = this.inlineEditor.rootElement.closest<BlockComponent>(
`[${BLOCK_ID_ATTR}]` `[${BLOCK_ID_ATTR}]`
); );
return block; return block;

View File

@@ -148,7 +148,8 @@ export class AffineReference extends WithDisposable(ShadowlessElement) {
} }
get block() { get block() {
const block = this.inlineEditor?.rootElement.closest<BlockComponent>( if (!this.inlineEditor?.rootElement) return null;
const block = this.inlineEditor.rootElement.closest<BlockComponent>(
`[${BLOCK_ID_ATTR}]` `[${BLOCK_ID_ATTR}]`
); );
return block; return block;

View File

@@ -138,7 +138,7 @@ export class ReferencePopup extends WithDisposable(LitElement) {
} }
get block() { get block() {
const block = this.inlineEditor.rootElement.closest<BlockComponent>( const block = this.inlineEditor.rootElement?.closest<BlockComponent>(
`[${BLOCK_ID_ATTR}]` `[${BLOCK_ID_ATTR}]`
); );
assertExists(block); assertExists(block);

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { TextUtils } from '@blocksuite/affine-block-surface'; import { TextUtils } from '@blocksuite/affine-block-surface';
import type { RichText } from '@blocksuite/affine-components/rich-text'; import type { RichText } from '@blocksuite/affine-components/rich-text';
import type { ConnectorElementModel } from '@blocksuite/affine-model'; import type { ConnectorElementModel } from '@blocksuite/affine-model';
@@ -71,6 +70,8 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
const { connector, edgeless } = this; const { connector, edgeless } = this;
if (!connector || !edgeless) return; if (!connector || !edgeless) return;
if (!this.inlineEditorContainer) return;
const newWidth = this.inlineEditorContainer.scrollWidth; const newWidth = this.inlineEditorContainer.scrollWidth;
const newHeight = this.inlineEditorContainer.scrollHeight; const newHeight = this.inlineEditorContainer.scrollHeight;
const center = connector.getPointByOffsetDistance( const center = connector.getPointByOffsetDistance(
@@ -137,7 +138,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
const isModEnter = onlyCmd && key === 'Enter'; const isModEnter = onlyCmd && key === 'Enter';
const isEscape = key === 'Escape'; const isEscape = key === 'Escape';
if (!isComposing && (isModEnter || isEscape)) { if (!isComposing && (isModEnter || isEscape)) {
this.inlineEditorContainer.blur(); this.inlineEditorContainer?.blur();
edgeless.service.selection.set({ edgeless.service.selection.set({
elements: [connector.id], elements: [connector.id],
@@ -192,6 +193,8 @@ export class EdgelessConnectorLabelEditor extends WithDisposable(
}); });
}); });
if (!this.inlineEditorContainer) return;
this.disposables.addFromEvent( this.disposables.addFromEvent(
this.inlineEditorContainer, this.inlineEditorContainer,
'blur', 'blur',

View File

@@ -94,6 +94,8 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
this.disposables.add(dispatcher.add('click', () => true)); this.disposables.add(dispatcher.add('click', () => true));
this.disposables.add(dispatcher.add('doubleClick', () => true)); this.disposables.add(dispatcher.add('doubleClick', () => true));
if (!this.inlineEditor.rootElement) return;
this.disposables.addFromEvent( this.disposables.addFromEvent(
this.inlineEditor.rootElement, this.inlineEditor.rootElement,
'blur', 'blur',

View File

@@ -79,6 +79,8 @@ export class EdgelessGroupTitleEditor extends WithDisposable(
this.disposables.add(dispatcher.add('click', () => true)); this.disposables.add(dispatcher.add('click', () => true));
this.disposables.add(dispatcher.add('doubleClick', () => true)); this.disposables.add(dispatcher.add('doubleClick', () => true));
if (!this.inlineEditorContainer) return;
this.disposables.addFromEvent( this.disposables.addFromEvent(
this.inlineEditorContainer, this.inlineEditorContainer,
'blur', 'blur',

View File

@@ -212,6 +212,8 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
this._updateElementWH(); this._updateElementWH();
}) })
); );
if (!this.inlineEditorContainer) return;
this.disposables.addFromEvent( this.disposables.addFromEvent(
this.inlineEditorContainer, this.inlineEditorContainer,
'blur', 'blur',

View File

@@ -73,7 +73,7 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
const edgeless = this.edgeless; const edgeless = this.edgeless;
const element = this.element; const element = this.element;
if (!edgeless || !element) return; if (!edgeless || !element || !this.inlineEditorContainer) return;
const newWidth = this.inlineEditorContainer.scrollWidth; const newWidth = this.inlineEditorContainer.scrollWidth;
const newHeight = this.inlineEditorContainer.scrollHeight; const newHeight = this.inlineEditorContainer.scrollHeight;
@@ -206,6 +206,7 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
}); });
}); });
if (!this.inlineEditorContainer) return;
this.disposables.addFromEvent( this.disposables.addFromEvent(
this.inlineEditorContainer, this.inlineEditorContainer,
'blur', 'blur',

View File

@@ -117,7 +117,8 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
offset = scrollTopOffset ?? 0; offset = scrollTopOffset ?? 0;
} }
container?.scrollTo({ if (!inlineEditor.rootElement || !container) return;
container.scrollTo({
top: top:
inlineEditor.rootElement.getBoundingClientRect().top + inlineEditor.rootElement.getBoundingClientRect().top +
containerScrollTop - containerScrollTop -

View File

@@ -1,5 +1,5 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { assertExists, DisposableGroup, Slot } from '@blocksuite/global/utils'; import { DisposableGroup, Slot } from '@blocksuite/global/utils';
import { type Signal, signal } from '@preact/signals-core'; import { type Signal, signal } from '@preact/signals-core';
import { nothing, render, type TemplateResult } from 'lit'; import { nothing, render, type TemplateResult } from 'lit';
import type * as Y from 'yjs'; import type * as Y from 'yjs';
@@ -136,7 +136,6 @@ export class InlineEditor<
private _rootElement: InlineRootElement<TextAttributes> | null = null; private _rootElement: InlineRootElement<TextAttributes> | null = null;
get rootElement() { get rootElement() {
assertExists(this._rootElement);
return this._rootElement; return this._rootElement;
} }
@@ -244,7 +243,7 @@ export class InlineEditor<
this._rootElement.dataset.vRoot = 'true'; this._rootElement.dataset.vRoot = 'true';
this.setReadonly(isReadonly); this.setReadonly(isReadonly);
this.rootElement.replaceChildren(); this._rootElement.replaceChildren();
delete (this.rootElement as any)['_$litPart$']; delete (this.rootElement as any)['_$litPart$'];
@@ -259,10 +258,12 @@ export class InlineEditor<
} }
unmount() { unmount() {
if (this.rootElement.isConnected) { if (this.rootElement) {
render(nothing, this.rootElement); if (this.rootElement.isConnected) {
render(nothing, this.rootElement);
}
this.rootElement.removeAttribute(INLINE_ROOT_ATTR);
} }
this.rootElement.removeAttribute(INLINE_ROOT_ATTR);
this._rootElement = null; this._rootElement = null;
this._mounted = false; this._mounted = false;
this.disposables.dispose(); this.disposables.dispose();
@@ -272,7 +273,7 @@ export class InlineEditor<
setReadonly(isReadonly: boolean): void { setReadonly(isReadonly: boolean): void {
const value = isReadonly ? 'false' : 'true'; const value = isReadonly ? 'false' : 'true';
if (this.rootElement.contentEditable !== value) { if (this.rootElement && this.rootElement.contentEditable !== value) {
this.rootElement.contentEditable = value; this.rootElement.contentEditable = value;
} }

View File

@@ -19,6 +19,8 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
if (range.commonAncestorContainer.ownerDocument !== document) return false; if (range.commonAncestorContainer.ownerDocument !== document) return false;
const rootElement = this.editor.rootElement; const rootElement = this.editor.rootElement;
if (!rootElement) return false;
const rootRange = document.createRange(); const rootRange = document.createRange();
rootRange.selectNode(rootElement); rootRange.selectNode(rootElement);
@@ -140,7 +142,9 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
private readonly _onCompositionEnd = async (event: CompositionEvent) => { private readonly _onCompositionEnd = async (event: CompositionEvent) => {
this._isComposing = false; this._isComposing = false;
if (!this.editor.rootElement.isConnected) return; if (!this.editor.rootElement || !this.editor.rootElement.isConnected) {
return;
}
const range = this.editor.rangeService.getNativeRange(); const range = this.editor.rangeService.getNativeRange();
if ( if (
@@ -181,6 +185,7 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
private readonly _onCompositionStart = () => { private readonly _onCompositionStart = () => {
this._isComposing = true; this._isComposing = true;
if (!this.editor.rootElement) return;
// embeds is not editable and it will break IME // embeds is not editable and it will break IME
const embeds = this.editor.rootElement.querySelectorAll( const embeds = this.editor.rootElement.querySelectorAll(
'[data-v-embed="true"]' '[data-v-embed="true"]'
@@ -198,7 +203,9 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
}; };
private readonly _onCompositionUpdate = () => { private readonly _onCompositionUpdate = () => {
if (!this.editor.rootElement.isConnected) return; if (!this.editor.rootElement || !this.editor.rootElement.isConnected) {
return;
}
const range = this.editor.rangeService.getNativeRange(); const range = this.editor.rangeService.getNativeRange();
if ( if (
@@ -272,6 +279,8 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
private readonly _onSelectionChange = () => { private readonly _onSelectionChange = () => {
const rootElement = this.editor.rootElement; const rootElement = this.editor.rootElement;
if (!rootElement) return;
const previousInlineRange = this.editor.getInlineRange(); const previousInlineRange = this.editor.getInlineRange();
if (this._isComposing) { if (this._isComposing) {
return; return;
@@ -361,7 +370,9 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
'keydown', 'keydown',
this._onKeyDown this._onKeyDown
); );
this.editor.disposables.addFromEvent(rootElement, 'click', this._onClick); if (rootElement) {
this.editor.disposables.addFromEvent(rootElement, 'click', this._onClick);
}
}; };
get isComposing() { get isComposing() {

View File

@@ -63,6 +63,8 @@ export class RangeService<TextAttributes extends BaseTextAttributes> {
rangeIndexRelatedToLine: number; rangeIndexRelatedToLine: number;
} | null => { } | null => {
const rootElement = this.editor.rootElement; const rootElement = this.editor.rootElement;
if (!rootElement) return null;
const lineElements = Array.from(rootElement.querySelectorAll('v-line')); const lineElements = Array.from(rootElement.querySelectorAll('v-line'));
let beforeIndex = 0; let beforeIndex = 0;
@@ -100,6 +102,8 @@ export class RangeService<TextAttributes extends BaseTextAttributes> {
getTextPoint = (rangeIndex: InlineRange['index']): TextPoint | null => { getTextPoint = (rangeIndex: InlineRange['index']): TextPoint | null => {
const rootElement = this.editor.rootElement; const rootElement = this.editor.rootElement;
if (!rootElement) return null;
const vLines = Array.from(rootElement.querySelectorAll('v-line')); const vLines = Array.from(rootElement.querySelectorAll('v-line'));
let index = 0; let index = 0;
@@ -280,6 +284,7 @@ export class RangeService<TextAttributes extends BaseTextAttributes> {
const handler = () => { const handler = () => {
const selection = document.getSelection(); const selection = document.getSelection();
if (!selection) return; if (!selection) return;
if (!this.editor.rootElement) return;
if (inlineRange === null) { if (inlineRange === null) {
if (selection.rangeCount > 0) { if (selection.rangeCount > 0) {
@@ -321,6 +326,7 @@ export class RangeService<TextAttributes extends BaseTextAttributes> {
*/ */
toDomRange = (inlineRange: InlineRange): Range | null => { toDomRange = (inlineRange: InlineRange): Range | null => {
const rootElement = this.editor.rootElement; const rootElement = this.editor.rootElement;
if (!rootElement) return null;
return inlineRangeToDomRange(rootElement, inlineRange); return inlineRangeToDomRange(rootElement, inlineRange);
}; };
@@ -358,7 +364,7 @@ export class RangeService<TextAttributes extends BaseTextAttributes> {
*/ */
toInlineRange = (range: Range): InlineRange | null => { toInlineRange = (range: Range): InlineRange | null => {
const { rootElement, yText } = this.editor; const { rootElement, yText } = this.editor;
if (!rootElement || !yText) return null;
return domRangeToInlineRange(range, rootElement, yText); return domRangeToInlineRange(range, rootElement, yText);
}; };

View File

@@ -78,7 +78,7 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
} }
// render current deltas to VLines // render current deltas to VLines
render = () => { render = () => {
if (!this.editor.mounted) return; if (!this.editor.rootElement) return;
this._rendering = true; this._rendering = true;
@@ -160,7 +160,7 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
rerenderWholeEditor = () => { rerenderWholeEditor = () => {
const rootElement = this.editor.rootElement; const rootElement = this.editor.rootElement;
if (!rootElement.isConnected) return; if (!rootElement || !rootElement.isConnected) return;
rootElement.replaceChildren(); rootElement.replaceChildren();
// Because we bypassed Lit and disrupted the DOM structure, this will cause an inconsistency in the original state of `ChildPart`. // Because we bypassed Lit and disrupted the DOM structure, this will cause an inconsistency in the original state of `ChildPart`.
@@ -172,6 +172,7 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
}; };
waitForUpdate = async () => { waitForUpdate = async () => {
if (!this.editor.rootElement) return;
const vLines = Array.from( const vLines = Array.from(
this.editor.rootElement.querySelectorAll('v-line') this.editor.rootElement.querySelectorAll('v-line')
); );

View File

@@ -43,6 +43,7 @@ export class FrameCardTitleEditor extends WithDisposable(ShadowlessElement) {
}); });
const inlineEditorContainer = this.inlineEditor.rootElement; const inlineEditorContainer = this.inlineEditor.rootElement;
if (!inlineEditorContainer) return;
this.disposables.addFromEvent(inlineEditorContainer, 'blur', () => { this.disposables.addFromEvent(inlineEditorContainer, 'blur', () => {
this._unmount(); this._unmount();

View File

@@ -321,6 +321,9 @@ export class AtMenuConfigService extends Service {
close(); close();
const getRect = () => { const getRect = () => {
if (!inlineEditor.rootElement) {
return { x: 0, y: 0, width: 0, height: 0 };
}
let rect = inlineEditor.getNativeRange()?.getBoundingClientRect(); let rect = inlineEditor.getNativeRange()?.getBoundingClientRect();
if (!rect || rect.width === 0 || rect.height === 0) { if (!rect || rect.width === 0 || rect.height === 0) {