mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-05 03:25:10 +08:00
71b5cddea1
Fixes `sentry-7906c03b79a54ede819c56cc15ad9889`
184 lines
5.3 KiB
TypeScript
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>) {}
|
|
}
|