diff --git a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/latex-node/latex-node.ts b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/latex-node/latex-node.ts
index 0812896bb4..da518f78a2 100644
--- a/blocksuite/affine/components/src/rich-text/inline/presets/nodes/latex-node/latex-node.ts
+++ b/blocksuite/affine/components/src/rich-text/inline/presets/nodes/latex-node/latex-node.ts
@@ -12,7 +12,7 @@ import {
ZERO_WIDTH_NON_JOINER,
ZERO_WIDTH_SPACE,
} from '@blocksuite/inline';
-import { effect, signal } from '@preact/signals-core';
+import { signal } from '@preact/signals-core';
import katex from 'katex';
import { css, html, render } from 'lit';
import { property } from 'lit/decorators.js';
@@ -88,6 +88,8 @@ export class AffineLatexNode extends SignalWatcher(
readonly latex$ = signal('');
+ readonly latexEditorSignal = signal('');
+
get deltaLatex() {
return this.delta.attributes?.latex as string;
}
@@ -100,11 +102,11 @@ export class AffineLatexNode extends SignalWatcher(
const result = super.connectedCallback();
this.latex$.value = this.deltaLatex;
+ this.latexEditorSignal.value = this.deltaLatex;
this.disposables.add(
- effect(() => {
- const latex = this.latex$.value;
-
+ this.latex$.subscribe(latex => {
+ this.latexEditorSignal.value = latex;
if (latex !== this.deltaLatex) {
this.editor.formatText(
{
@@ -116,7 +118,11 @@ export class AffineLatexNode extends SignalWatcher(
}
);
}
+ })
+ );
+ this.disposables.add(
+ this.latexEditorSignal.subscribe(latex => {
this.updateComplete
.then(() => {
const latexContainer = this.latexContainer;
@@ -187,7 +193,7 @@ export class AffineLatexNode extends SignalWatcher(
const portal = createLitPortal({
template: html``,
container: blockComponent.host,
@@ -210,6 +216,20 @@ export class AffineLatexNode extends SignalWatcher(
'abort',
() => {
portal.remove();
+ const latex = this.latexEditorSignal.peek();
+ this.latex$.value = latex;
+
+ if (latex !== this.deltaLatex) {
+ this.editor.formatText(
+ {
+ index: this.startOffset,
+ length: this.endOffset - this.startOffset,
+ },
+ {
+ latex,
+ }
+ );
+ }
},
{ once: true }
);
diff --git a/blocksuite/framework/store/src/__tests__/block.unit.spec.ts b/blocksuite/framework/store/src/__tests__/block.unit.spec.ts
index 720936c1d0..75896e60b8 100644
--- a/blocksuite/framework/store/src/__tests__/block.unit.spec.ts
+++ b/blocksuite/framework/store/src/__tests__/block.unit.spec.ts
@@ -27,12 +27,25 @@ const pageSchema = defineBlockSchema({
version: 1,
},
});
+
+const tableSchema = defineBlockSchema({
+ flavour: 'table',
+ props: () => ({
+ cols: {} as Record,
+ rows: [] as Array<{ color: string }>,
+ }),
+ metadata: {
+ role: 'content',
+ version: 1,
+ },
+});
type RootModel = SchemaToModel;
+type TableModel = SchemaToModel;
function createTestOptions() {
const idGenerator = createAutoIncrementIdGenerator();
const schema = new Schema();
- schema.register([pageSchema]);
+ schema.register([pageSchema, tableSchema]);
return { id: 'test-collection', idGenerator, schema };
}
@@ -249,3 +262,102 @@ test('on change', () => {
foo: 0,
});
});
+
+test('deep sync', () => {
+ const doc = createTestDoc();
+ const yDoc = new Y.Doc();
+ const yBlock = yDoc.getMap('yBlock') as YBlock;
+ yBlock.set('sys:id', '0');
+ yBlock.set('sys:flavour', 'table');
+ yBlock.set('sys:children', new Y.Array());
+
+ const onPropsUpdated = vi.fn();
+ const block = new Block(doc.schema, yBlock, doc, {
+ onChange: onPropsUpdated,
+ });
+ const model = block.model as TableModel;
+ expect(model.cols).toEqual({});
+ expect(model.rows).toEqual([]);
+
+ model.cols = {
+ '1': { color: 'red' },
+ };
+ const onColsUpdated = vi.fn();
+ const onRowsUpdated = vi.fn();
+ effect(() => {
+ onColsUpdated(model.cols$.value);
+ });
+ effect(() => {
+ onRowsUpdated(model.rows$.value);
+ });
+ const getColsMap = () => yBlock.get('prop:cols') as Y.Map;
+ const getRowsArr = () => yBlock.get('prop:rows') as Y.Array;
+ expect(getColsMap().toJSON()).toEqual({
+ '1': { color: 'red' },
+ });
+ expect(model.cols$.value).toEqual({
+ '1': { color: 'red' },
+ });
+
+ onPropsUpdated.mockClear();
+ onColsUpdated.mockClear();
+
+ model.cols['2'] = { color: 'blue' };
+ expect(getColsMap().toJSON()).toEqual({
+ '1': { color: 'red' },
+ '2': { color: 'blue' },
+ });
+ expect(onColsUpdated).toHaveBeenCalledWith({
+ '1': { color: 'red' },
+ '2': { color: 'blue' },
+ });
+ expect(onPropsUpdated).toHaveBeenCalledTimes(1);
+ expect(onColsUpdated).toHaveBeenCalledTimes(1);
+
+ onPropsUpdated.mockClear();
+ onColsUpdated.mockClear();
+
+ const map = new Y.Map();
+ map.set('color', 'green');
+ getColsMap().set('3', map);
+ expect(onPropsUpdated).toHaveBeenCalledWith(
+ expect.anything(),
+ 'cols',
+ expect.anything()
+ );
+ expect(onColsUpdated).toHaveBeenCalledWith({
+ '1': { color: 'red' },
+ '2': { color: 'blue' },
+ '3': { color: 'green' },
+ });
+ expect(onPropsUpdated).toHaveBeenCalledTimes(1);
+ expect(onColsUpdated).toHaveBeenCalledTimes(1);
+
+ onPropsUpdated.mockClear();
+ onRowsUpdated.mockClear();
+
+ model.rows.push({ color: 'yellow' });
+ expect(onPropsUpdated).toHaveBeenCalledWith(
+ expect.anything(),
+ 'rows',
+ expect.anything()
+ );
+ expect(onRowsUpdated).toHaveBeenCalledWith([{ color: 'yellow' }]);
+ expect(onPropsUpdated).toHaveBeenCalledTimes(1);
+ expect(onRowsUpdated).toHaveBeenCalledTimes(1);
+
+ onPropsUpdated.mockClear();
+ onRowsUpdated.mockClear();
+
+ const row1 = getRowsArr().get(0) as Y.Map;
+ row1.set('color', 'green');
+ expect(onRowsUpdated).toHaveBeenCalledWith([{ color: 'green' }]);
+ expect(onPropsUpdated).toHaveBeenCalledWith(
+ expect.anything(),
+ 'rows',
+ expect.anything()
+ );
+ expect(model.rows$.value).toEqual([{ color: 'green' }]);
+ expect(onPropsUpdated).toHaveBeenCalledTimes(1);
+ expect(onRowsUpdated).toHaveBeenCalledTimes(1);
+});
diff --git a/blocksuite/framework/store/src/model/block/sync-controller.ts b/blocksuite/framework/store/src/model/block/sync-controller.ts
index 5126e0467f..c8aad4f7fe 100644
--- a/blocksuite/framework/store/src/model/block/sync-controller.ts
+++ b/blocksuite/framework/store/src/model/block/sync-controller.ts
@@ -222,7 +222,7 @@ export class SyncController {
if (signalKey in this.model) {
this._mutex(() => {
// @ts-expect-error allow magic props
- this.model[signalKey].value = this.model[name];
+ this.model[signalKey].value = y2Native(value);
});
}
},
diff --git a/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts b/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts
index e97323d2b4..852c47f7aa 100644
--- a/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts
+++ b/blocksuite/tests-legacy/edgeless/edgeless-text.spec.ts
@@ -494,6 +494,9 @@ test.describe('edgeless text block', () => {
await page.locator('affine-latex-node').click();
await waitNextFrame(page);
await type(page, 'ccc');
+ const menu = page.locator('latex-editor-menu');
+ const confirm = menu.locator('.latex-editor-confirm');
+ await confirm.click();
await assertRichTextInlineDeltas(
page,
[
@@ -507,6 +510,7 @@ test.describe('edgeless text block', () => {
1
);
+ await page.locator('affine-latex-node').click();
await page.locator('.latex-editor-hint').click();
await type(page, 'sss');
await assertRichTextInlineDeltas(
@@ -524,6 +528,7 @@ test.describe('edgeless text block', () => {
await page.locator('latex-editor-unit').click();
await selectAllByKeyboard(page);
await type(page, 'sss');
+ await confirm.click();
await assertRichTextInlineDeltas(
page,
[