Files
AFFiNE-Mirror/blocksuite/framework/inline/src/services/render.ts
T
doodlewind 71b5cddea1 fix(editor): use nullable inline editor root element (#9320)
Fixes `sentry-7906c03b79a54ede819c56cc15ad9889`
2024-12-26 07:55:15 +00:00

184 lines
5.3 KiB
TypeScript

import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { assertExists } from '@blocksuite/global/utils';
import { html, render } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import * as Y from 'yjs';
import type { VLine } from '../components/v-line.js';
import type { InlineEditor } from '../inline-editor.js';
import type { InlineRange } from '../types.js';
import type { BaseTextAttributes } from '../utils/base-attributes.js';
import { deltaInsertsToChunks } from '../utils/delta-convert.js';
export class RenderService<TextAttributes extends BaseTextAttributes> {
private readonly _onYTextChange = (
_: Y.YTextEvent,
transaction: Y.Transaction
) => {
this.editor.slots.textChange.emit();
const yText = this.editor.yText;
if (yText.toString().includes('\r')) {
throw new BlockSuiteError(
ErrorCode.InlineEditorError,
'yText must not contain "\\r" because it will break the range synchronization'
);
}
this.render();
const inlineRange = this.editor.inlineRange$.peek();
if (!inlineRange || transaction.local) return;
const lastStartRelativePosition = this.editor.lastStartRelativePosition;
const lastEndRelativePosition = this.editor.lastEndRelativePosition;
if (!lastStartRelativePosition || !lastEndRelativePosition) return;
const doc = this.editor.yText.doc;
assertExists(doc);
const absoluteStart = Y.createAbsolutePositionFromRelativePosition(
lastStartRelativePosition,
doc
);
const absoluteEnd = Y.createAbsolutePositionFromRelativePosition(
lastEndRelativePosition,
doc
);
const startIndex = absoluteStart?.index;
const endIndex = absoluteEnd?.index;
if (!startIndex || !endIndex) return;
const newInlineRange: InlineRange = {
index: startIndex,
length: endIndex - startIndex,
};
if (!this.editor.isValidInlineRange(newInlineRange)) return;
this.editor.setInlineRange(newInlineRange);
this.editor.syncInlineRange();
};
mount = () => {
const editor = this.editor;
const yText = editor.yText;
yText.observe(this._onYTextChange);
editor.disposables.add({
dispose: () => {
yText.unobserve(this._onYTextChange);
},
});
};
private _rendering = false;
get rendering() {
return this._rendering;
}
// render current deltas to VLines
render = () => {
if (!this.editor.rootElement) return;
this._rendering = true;
const rootElement = this.editor.rootElement;
const embedDeltas = this.editor.deltaService.embedDeltas;
const chunks = deltaInsertsToChunks(embedDeltas);
let deltaIndex = 0;
// every chunk is a line
const lines = chunks.map((chunk, lineIndex) => {
if (lineIndex > 0) {
deltaIndex += 1; // for '\n'
}
const lineStartOffset = deltaIndex;
if (chunk.length > 0) {
const elements: VLine['elements'] = chunk.map(delta => {
const startOffset = deltaIndex;
deltaIndex += delta.insert.length;
const endOffset = deltaIndex;
return [
html`<v-element
.inlineEditor=${this.editor}
.delta=${{
insert: delta.insert,
attributes: this.editor.attributeService.normalizeAttributes(
delta.attributes
),
}}
.startOffset=${startOffset}
.endOffset=${endOffset}
.lineIndex=${lineIndex}
></v-element>`,
delta,
];
});
return html`<v-line
.elements=${elements}
.index=${lineIndex}
.startOffset=${lineStartOffset}
.endOffset=${deltaIndex}
></v-line>`;
} else {
return html`<v-line
.elements=${[]}
.index=${lineIndex}
.startOffset=${lineStartOffset}
.endOffset=${deltaIndex}
></v-line>`;
}
});
try {
render(
repeat(
lines.map((line, i) => ({ line, index: i })),
entry => entry.index,
entry => entry.line
),
rootElement
);
} catch {
// Lit may be crashed by IME input and we need to rerender whole editor for it
this.editor.rerenderWholeEditor();
}
this.editor
.waitForUpdate()
.then(() => {
this._rendering = false;
this.editor.slots.renderComplete.emit();
this.editor.syncInlineRange();
})
.catch(console.error);
};
rerenderWholeEditor = () => {
const rootElement = this.editor.rootElement;
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`.
// Therefore, we need to remove the original `ChildPart`.
// https://github.com/lit/lit/blob/a2cd76cfdea4ed717362bb1db32710d70550469d/packages/lit-html/src/lit-html.ts#L2248
delete (rootElement as any)['_$litPart$'];
this.render();
};
waitForUpdate = async () => {
if (!this.editor.rootElement) return;
const vLines = Array.from(
this.editor.rootElement.querySelectorAll('v-line')
);
await Promise.all(vLines.map(line => line.updateComplete));
};
constructor(readonly editor: InlineEditor<TextAttributes>) {}
}