From 71b5cddea13ead0ca065dcc004f6656939e85ebd Mon Sep 17 00:00:00 2001 From: doodlewind <7312949+doodlewind@users.noreply.github.com> Date: Thu, 26 Dec 2024 07:55:15 +0000 Subject: [PATCH] fix(editor): use nullable inline editor root element (#9320) Fixes `sentry-7906c03b79a54ede819c56cc15ad9889` --- .../src/rich-text/inline/presets/markdown.ts | 3 ++- .../presets/nodes/link-node/affine-link.ts | 3 ++- .../nodes/reference-node/reference-node.ts | 3 ++- .../nodes/reference-node/reference-popup.ts | 2 +- .../text/edgeless-connector-label-editor.ts | 7 +++++-- .../text/edgeless-frame-title-editor.ts | 2 ++ .../text/edgeless-group-title-editor.ts | 2 ++ .../text/edgeless-shape-text-editor.ts | 2 ++ .../components/text/edgeless-text-editor.ts | 3 ++- .../linked-doc/mobile-linked-doc-menu.ts | 3 ++- .../framework/inline/src/inline-editor.ts | 15 ++++++++------- .../framework/inline/src/services/event.ts | 17 ++++++++++++++--- .../framework/inline/src/services/range.ts | 8 +++++++- .../framework/inline/src/services/render.ts | 5 +++-- .../frame-panel/card/frame-card-title-editor.ts | 1 + .../modules/at-menu-config/services/index.ts | 3 +++ 16 files changed, 58 insertions(+), 21 deletions(-) diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/markdown.ts b/blocksuite/affine/components/src/rich-text/inline/presets/markdown.ts index 74aae1e603..aff1f63ccf 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/markdown.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/markdown.ts @@ -1,4 +1,4 @@ -/* eslint-disable no-useless-escape */ +/* oxlint-disable no-useless-escape */ import type { BlockComponent, ExtensionType } from '@blocksuite/block-std'; import { KEYBOARD_ALLOW_DEFAULT, @@ -450,6 +450,7 @@ export const LatexExtension = InlineMarkdownExtension({ undoManager.stopCapturing(); + if (!inlineEditor.rootElement) return KEYBOARD_ALLOW_DEFAULT; const blockComponent = inlineEditor.rootElement.closest('[data-block-id]'); if (!blockComponent) return KEYBOARD_ALLOW_DEFAULT; diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/link-node/affine-link.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/link-node/affine-link.ts index d8f74b8900..ba39a90a7f 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/link-node/affine-link.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/link-node/affine-link.ts @@ -103,7 +103,8 @@ export class AffineLink extends ShadowlessElement { // Please also note that when readonly mode active, // this workaround is not necessary and links work normally. get block() { - const block = this.inlineEditor?.rootElement.closest( + if (!this.inlineEditor?.rootElement) return null; + const block = this.inlineEditor.rootElement.closest( `[${BLOCK_ID_ATTR}]` ); return block; diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-node.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-node.ts index 10cf460568..c06d1a365b 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-node.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-node.ts @@ -148,7 +148,8 @@ export class AffineReference extends WithDisposable(ShadowlessElement) { } get block() { - const block = this.inlineEditor?.rootElement.closest( + if (!this.inlineEditor?.rootElement) return null; + const block = this.inlineEditor.rootElement.closest( `[${BLOCK_ID_ATTR}]` ); return block; diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-popup.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-popup.ts index cc55aa43b2..509dafe3a8 100644 --- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-popup.ts +++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/reference-node/reference-popup.ts @@ -138,7 +138,7 @@ export class ReferencePopup extends WithDisposable(LitElement) { } get block() { - const block = this.inlineEditor.rootElement.closest( + const block = this.inlineEditor.rootElement?.closest( `[${BLOCK_ID_ATTR}]` ); assertExists(block); diff --git a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-connector-label-editor.ts b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-connector-label-editor.ts index 3d5af296ae..3c5275bbf1 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-connector-label-editor.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-connector-label-editor.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { TextUtils } from '@blocksuite/affine-block-surface'; import type { RichText } from '@blocksuite/affine-components/rich-text'; import type { ConnectorElementModel } from '@blocksuite/affine-model'; @@ -71,6 +70,8 @@ export class EdgelessConnectorLabelEditor extends WithDisposable( const { connector, edgeless } = this; if (!connector || !edgeless) return; + if (!this.inlineEditorContainer) return; + const newWidth = this.inlineEditorContainer.scrollWidth; const newHeight = this.inlineEditorContainer.scrollHeight; const center = connector.getPointByOffsetDistance( @@ -137,7 +138,7 @@ export class EdgelessConnectorLabelEditor extends WithDisposable( const isModEnter = onlyCmd && key === 'Enter'; const isEscape = key === 'Escape'; if (!isComposing && (isModEnter || isEscape)) { - this.inlineEditorContainer.blur(); + this.inlineEditorContainer?.blur(); edgeless.service.selection.set({ elements: [connector.id], @@ -192,6 +193,8 @@ export class EdgelessConnectorLabelEditor extends WithDisposable( }); }); + if (!this.inlineEditorContainer) return; + this.disposables.addFromEvent( this.inlineEditorContainer, 'blur', diff --git a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-frame-title-editor.ts b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-frame-title-editor.ts index 0b2eaaf7cb..93ae12807a 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-frame-title-editor.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-frame-title-editor.ts @@ -94,6 +94,8 @@ export class EdgelessFrameTitleEditor extends WithDisposable( this.disposables.add(dispatcher.add('click', () => true)); this.disposables.add(dispatcher.add('doubleClick', () => true)); + + if (!this.inlineEditor.rootElement) return; this.disposables.addFromEvent( this.inlineEditor.rootElement, 'blur', diff --git a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-group-title-editor.ts b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-group-title-editor.ts index ae8f748a91..b818cd72bb 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-group-title-editor.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-group-title-editor.ts @@ -79,6 +79,8 @@ export class EdgelessGroupTitleEditor extends WithDisposable( this.disposables.add(dispatcher.add('click', () => true)); this.disposables.add(dispatcher.add('doubleClick', () => true)); + + if (!this.inlineEditorContainer) return; this.disposables.addFromEvent( this.inlineEditorContainer, 'blur', diff --git a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-shape-text-editor.ts b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-shape-text-editor.ts index d7f04fe9b2..e63ab3551e 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-shape-text-editor.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-shape-text-editor.ts @@ -212,6 +212,8 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) { this._updateElementWH(); }) ); + + if (!this.inlineEditorContainer) return; this.disposables.addFromEvent( this.inlineEditorContainer, 'blur', diff --git a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-text-editor.ts b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-text-editor.ts index 942ea21555..82612a6c4b 100644 --- a/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-text-editor.ts +++ b/blocksuite/blocks/src/root-block/edgeless/components/text/edgeless-text-editor.ts @@ -73,7 +73,7 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) { const edgeless = this.edgeless; const element = this.element; - if (!edgeless || !element) return; + if (!edgeless || !element || !this.inlineEditorContainer) return; const newWidth = this.inlineEditorContainer.scrollWidth; const newHeight = this.inlineEditorContainer.scrollHeight; @@ -206,6 +206,7 @@ export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) { }); }); + if (!this.inlineEditorContainer) return; this.disposables.addFromEvent( this.inlineEditorContainer, 'blur', diff --git a/blocksuite/blocks/src/root-block/widgets/linked-doc/mobile-linked-doc-menu.ts b/blocksuite/blocks/src/root-block/widgets/linked-doc/mobile-linked-doc-menu.ts index 34d7e4e055..5487676c89 100644 --- a/blocksuite/blocks/src/root-block/widgets/linked-doc/mobile-linked-doc-menu.ts +++ b/blocksuite/blocks/src/root-block/widgets/linked-doc/mobile-linked-doc-menu.ts @@ -117,7 +117,8 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher( offset = scrollTopOffset ?? 0; } - container?.scrollTo({ + if (!inlineEditor.rootElement || !container) return; + container.scrollTo({ top: inlineEditor.rootElement.getBoundingClientRect().top + containerScrollTop - diff --git a/blocksuite/framework/inline/src/inline-editor.ts b/blocksuite/framework/inline/src/inline-editor.ts index 4155d49df6..97eef6512d 100644 --- a/blocksuite/framework/inline/src/inline-editor.ts +++ b/blocksuite/framework/inline/src/inline-editor.ts @@ -1,5 +1,5 @@ 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 { nothing, render, type TemplateResult } from 'lit'; import type * as Y from 'yjs'; @@ -136,7 +136,6 @@ export class InlineEditor< private _rootElement: InlineRootElement | null = null; get rootElement() { - assertExists(this._rootElement); return this._rootElement; } @@ -244,7 +243,7 @@ export class InlineEditor< this._rootElement.dataset.vRoot = 'true'; this.setReadonly(isReadonly); - this.rootElement.replaceChildren(); + this._rootElement.replaceChildren(); delete (this.rootElement as any)['_$litPart$']; @@ -259,10 +258,12 @@ export class InlineEditor< } unmount() { - if (this.rootElement.isConnected) { - render(nothing, this.rootElement); + if (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._mounted = false; this.disposables.dispose(); @@ -272,7 +273,7 @@ export class InlineEditor< setReadonly(isReadonly: boolean): void { const value = isReadonly ? 'false' : 'true'; - if (this.rootElement.contentEditable !== value) { + if (this.rootElement && this.rootElement.contentEditable !== value) { this.rootElement.contentEditable = value; } diff --git a/blocksuite/framework/inline/src/services/event.ts b/blocksuite/framework/inline/src/services/event.ts index 732dca5662..f01ead34ff 100644 --- a/blocksuite/framework/inline/src/services/event.ts +++ b/blocksuite/framework/inline/src/services/event.ts @@ -19,6 +19,8 @@ export class EventService { if (range.commonAncestorContainer.ownerDocument !== document) return false; const rootElement = this.editor.rootElement; + if (!rootElement) return false; + const rootRange = document.createRange(); rootRange.selectNode(rootElement); @@ -140,7 +142,9 @@ export class EventService { private readonly _onCompositionEnd = async (event: CompositionEvent) => { this._isComposing = false; - if (!this.editor.rootElement.isConnected) return; + if (!this.editor.rootElement || !this.editor.rootElement.isConnected) { + return; + } const range = this.editor.rangeService.getNativeRange(); if ( @@ -181,6 +185,7 @@ export class EventService { private readonly _onCompositionStart = () => { this._isComposing = true; + if (!this.editor.rootElement) return; // embeds is not editable and it will break IME const embeds = this.editor.rootElement.querySelectorAll( '[data-v-embed="true"]' @@ -198,7 +203,9 @@ export class EventService { }; private readonly _onCompositionUpdate = () => { - if (!this.editor.rootElement.isConnected) return; + if (!this.editor.rootElement || !this.editor.rootElement.isConnected) { + return; + } const range = this.editor.rangeService.getNativeRange(); if ( @@ -272,6 +279,8 @@ export class EventService { private readonly _onSelectionChange = () => { const rootElement = this.editor.rootElement; + if (!rootElement) return; + const previousInlineRange = this.editor.getInlineRange(); if (this._isComposing) { return; @@ -361,7 +370,9 @@ export class EventService { 'keydown', this._onKeyDown ); - this.editor.disposables.addFromEvent(rootElement, 'click', this._onClick); + if (rootElement) { + this.editor.disposables.addFromEvent(rootElement, 'click', this._onClick); + } }; get isComposing() { diff --git a/blocksuite/framework/inline/src/services/range.ts b/blocksuite/framework/inline/src/services/range.ts index 4c32a25641..7dee7f857f 100644 --- a/blocksuite/framework/inline/src/services/range.ts +++ b/blocksuite/framework/inline/src/services/range.ts @@ -63,6 +63,8 @@ export class RangeService { rangeIndexRelatedToLine: number; } | null => { const rootElement = this.editor.rootElement; + if (!rootElement) return null; + const lineElements = Array.from(rootElement.querySelectorAll('v-line')); let beforeIndex = 0; @@ -100,6 +102,8 @@ export class RangeService { getTextPoint = (rangeIndex: InlineRange['index']): TextPoint | null => { const rootElement = this.editor.rootElement; + if (!rootElement) return null; + const vLines = Array.from(rootElement.querySelectorAll('v-line')); let index = 0; @@ -280,6 +284,7 @@ export class RangeService { const handler = () => { const selection = document.getSelection(); if (!selection) return; + if (!this.editor.rootElement) return; if (inlineRange === null) { if (selection.rangeCount > 0) { @@ -321,6 +326,7 @@ export class RangeService { */ toDomRange = (inlineRange: InlineRange): Range | null => { const rootElement = this.editor.rootElement; + if (!rootElement) return null; return inlineRangeToDomRange(rootElement, inlineRange); }; @@ -358,7 +364,7 @@ export class RangeService { */ toInlineRange = (range: Range): InlineRange | null => { const { rootElement, yText } = this.editor; - + if (!rootElement || !yText) return null; return domRangeToInlineRange(range, rootElement, yText); }; diff --git a/blocksuite/framework/inline/src/services/render.ts b/blocksuite/framework/inline/src/services/render.ts index 60dda4c051..e6549d8a7e 100644 --- a/blocksuite/framework/inline/src/services/render.ts +++ b/blocksuite/framework/inline/src/services/render.ts @@ -78,7 +78,7 @@ export class RenderService { } // render current deltas to VLines render = () => { - if (!this.editor.mounted) return; + if (!this.editor.rootElement) return; this._rendering = true; @@ -160,7 +160,7 @@ export class RenderService { rerenderWholeEditor = () => { const rootElement = this.editor.rootElement; - if (!rootElement.isConnected) return; + if (!rootElement || !rootElement.isConnected) return; rootElement.replaceChildren(); // 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 { }; waitForUpdate = async () => { + if (!this.editor.rootElement) return; const vLines = Array.from( this.editor.rootElement.querySelectorAll('v-line') ); diff --git a/blocksuite/presets/src/fragments/frame-panel/card/frame-card-title-editor.ts b/blocksuite/presets/src/fragments/frame-panel/card/frame-card-title-editor.ts index 4a95ce7144..8ae8ce32f3 100644 --- a/blocksuite/presets/src/fragments/frame-panel/card/frame-card-title-editor.ts +++ b/blocksuite/presets/src/fragments/frame-panel/card/frame-card-title-editor.ts @@ -43,6 +43,7 @@ export class FrameCardTitleEditor extends WithDisposable(ShadowlessElement) { }); const inlineEditorContainer = this.inlineEditor.rootElement; + if (!inlineEditorContainer) return; this.disposables.addFromEvent(inlineEditorContainer, 'blur', () => { this._unmount(); diff --git a/packages/frontend/core/src/modules/at-menu-config/services/index.ts b/packages/frontend/core/src/modules/at-menu-config/services/index.ts index abad2ed75a..bdabefec7b 100644 --- a/packages/frontend/core/src/modules/at-menu-config/services/index.ts +++ b/packages/frontend/core/src/modules/at-menu-config/services/index.ts @@ -321,6 +321,9 @@ export class AtMenuConfigService extends Service { close(); const getRect = () => { + if (!inlineEditor.rootElement) { + return { x: 0, y: 0, width: 0, height: 0 }; + } let rect = inlineEditor.getNativeRange()?.getBoundingClientRect(); if (!rect || rect.width === 0 || rect.height === 0) {