Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/core-slate-0-142-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@platejs/core": patch
---

Upgrade to slate 0.142.1, slate-dom 0.142.0, slate-react 0.142.0 and slate-hyperscript 0.138.3.
12 changes: 12 additions & 0 deletions .changeset/slate-history-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@platejs/slate": patch
---

Bring the bundled history plugin up to date with slate-history:

- `withoutSaving`, `withoutMerging`, `withMerging` and `withNewBatch` restore their flags when the callback throws, so one failed transform no longer stops history from recording.
- A nested `withNewBatch` keeps the enclosing batch's split.
- An edit no longer merges into the previous undo step just because another operation, such as a selection change, ran earlier in the same tick.
- Inside `withoutMerging`, a transform's normalization stays in the same undo step as the transform, so undoing it no longer leaves an unnormalized document behind.

`editor.hasRange` and `editor.api.hasRange` now share one implementation built on slate's `hasRange`, and still return `false` for a missing range.
5 changes: 5 additions & 0 deletions .changeset/slate-slate-0-142-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@platejs/slate": patch
---

Upgrade to slate 0.142.1, slate-dom 0.142.0, slate-react 0.142.0 and slate-hyperscript 0.138.3.
2 changes: 1 addition & 1 deletion apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@
"sass": "1.89.2",
"shadcn": "4.10.0",
"shadcn-prose": "1.0.8",
"slate": "0.132.0",
"slate": "0.142.1",
"sonner": "2.0.5",
"stream-browserify": "3.0.0",
"tailwind-merge": "3.3.1",
Expand Down
8 changes: 4 additions & 4 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@
"nanoid": "^5.1.5",
"optics-ts": "2.4.1",
"react-compiler-runtime": "^1.0.0",
"slate": "0.132.0",
"slate-dom": "0.132.0",
"slate-hyperscript": "0.129.1",
"slate-react": "0.132.0",
"slate": "0.142.1",
"slate-dom": "0.142.0",
"slate-hyperscript": "0.138.3",
"slate-react": "0.142.0",
"use-deep-compare": "^1.3.0",
"zustand": "^5.0.5",
"zustand-x": "6.2.1"
Expand Down
4 changes: 2 additions & 2 deletions packages/slate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"is-plain-object": "^5.0.0",
"lodash": "^4.17.21",
"scroll-into-view-if-needed": "^3.1.0",
"slate": "0.132.0",
"slate-dom": "0.132.0"
"slate": "0.142.1",
"slate-dom": "0.142.0"
},
"publishConfig": {
"access": "public"
Expand Down
1 change: 1 addition & 0 deletions packages/slate/src/create-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export const createEditor = <V extends Value>({
hasBlocks: bindFirst(hasBlocks, editor),
hasInlines: bindFirst(hasInlines, editor),
hasPath: bindFirst(hasPath, editor as any),
hasRange: bindFirst(hasRange, editor),
hasTexts: bindFirst(hasTexts, editor),
insertNodes: bindFirst(insertNodes, editor),
isBlock: bindFirst(isBlock, editor),
Expand Down
29 changes: 29 additions & 0 deletions packages/slate/src/internal/dom-editor/hasRange.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DOMEditor } from 'slate-dom';

import { createEditor } from '../../create-editor';

describe('hasRange', () => {
const editor = createEditor({
children: [{ children: [{ text: 'abc' }], type: 'p' }] as any,
});
const inside = {
anchor: { offset: 1, path: [0, 0] },
focus: { offset: 1, path: [0, 0] },
};
const outside = {
anchor: { offset: 0, path: [3, 0] },
focus: { offset: 0, path: [3, 0] },
};

it('checks both points through the editor, the api and slate-dom', () => {
expect(editor.api.hasRange(inside)).toBe(true);
expect(editor.api.hasRange(outside)).toBe(false);
expect((editor as any).hasRange(outside)).toBe(false);
expect(DOMEditor.hasRange(editor as any, inside)).toBe(true);
});

it('returns false instead of throwing for a missing range', () => {
expect(editor.api.hasRange(undefined as any)).toBe(false);
expect(DOMEditor.hasRange(editor as any, null as any)).toBe(false);
});
});
4 changes: 2 additions & 2 deletions packages/slate/src/internal/dom-editor/hasRange.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { DOMEditor } from 'slate-dom';
import { hasRange as hasSlateRange } from 'slate';

import type { TRange } from '../../interfaces';
import type { Editor } from '../../interfaces/editor';

export const hasRange = (editor: Editor, range: TRange): boolean => {
try {
return DOMEditor.hasRange(editor as any, range);
return hasSlateRange(editor as any, range);
} catch {}

return false;
Expand Down
35 changes: 25 additions & 10 deletions packages/slate/src/slate-history/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ export const HistoryApi = {
withMerging(editor: Editor, fn: () => void): void {
const prev = editor.api.isMerging();
MERGING.set(editor, true);
fn();
MERGING.set(editor, prev);
try {
fn();
} finally {
MERGING.set(editor, prev);
}
},

/**
Expand All @@ -78,12 +81,18 @@ export const HistoryApi = {
* will be merged as usual.
*/
withNewBatch(editor: Editor, fn: () => void): void {
const prev = editor.api.isMerging();
const prevMerging = editor.api.isMerging();
const prevSplitting = editor.api.isSplittingOnce();
MERGING.set(editor, true);
SPLITTING_ONCE.set(editor, true);
fn();
MERGING.set(editor, prev);
SPLITTING_ONCE.delete(editor);
try {
fn();
} finally {
MERGING.set(editor, prevMerging);
if (!prevSplitting) {
SPLITTING_ONCE.delete(editor);
}
}
},

/**
Expand All @@ -93,8 +102,11 @@ export const HistoryApi = {
withoutMerging(editor: Editor, fn: () => void): void {
const prev = editor.api.isMerging();
MERGING.set(editor, false);
fn();
MERGING.set(editor, prev);
try {
fn();
} finally {
MERGING.set(editor, prev);
}
},

/**
Expand All @@ -104,8 +116,11 @@ export const HistoryApi = {
withoutSaving(editor: Editor, fn: () => void): void {
const prev = editor.api.isSaving();
SAVING.set(editor, false);
fn();
SAVING.set(editor, prev);
try {
fn();
} finally {
SAVING.set(editor, prev);
}
},
};

Expand Down
160 changes: 160 additions & 0 deletions packages/slate/src/slate-history/upstream-parity.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/** @jsx jsxt */

import { jsxt } from '@platejs/test-utils';

import { createEditor } from '../create-editor';
import { withHistory } from './with-history';

jsxt;

const createHistoryEditor = (value: any): any =>
withHistory(createEditor(value));

const flush = () => new Promise((resolve) => setTimeout(resolve, 0));

describe('withHistory upstream parity', () => {
it('keeps an earlier batch when a selection change precedes the next edit', async () => {
const editor = createHistoryEditor(
(
<editor>
<hp>
one
<cursor />
</hp>
</editor>
) as any
);

editor.insertText('a');
await flush();
editor.select({ path: [0, 0], offset: 0 });
editor.insertText('b');
editor.undo();

expect(editor.children).toEqual([
{ children: [{ text: 'onea' }], type: 'p' },
]);
});

it('restores the saving flag when the callback throws', () => {
const editor = createHistoryEditor(
(
<editor>
<hp>
one
<cursor />
</hp>
</editor>
) as any
);

expect(() =>
editor.tf.withoutSaving(() => {
throw new Error('boom');
})
).toThrow('boom');
editor.insertText('x');

expect(editor.history.undos).toHaveLength(1);
});

it('restores the merging flag when the callback throws', () => {
const editor = createHistoryEditor(
(
<editor>
<hp>
one
<cursor />
</hp>
</editor>
) as any
);

expect(() =>
editor.tf.withoutMerging(() => {
throw new Error('boom');
})
).toThrow('boom');

expect(editor.api.isMerging()).toBeUndefined();
});

it('keeps the enclosing split after a nested withNewBatch', () => {
const editor = createHistoryEditor(
(
<editor>
<hp>
one
<cursor />
</hp>
</editor>
) as any
);

editor.insertText('a');
editor.tf.withNewBatch(() => {
editor.tf.withNewBatch(() => {});
editor.insertText('b');
});

expect(editor.history.undos).toHaveLength(2);
});

it('keeps normalization in the same undo step inside withoutMerging', () => {
const editor = withHistory(
createEditor({
children: [{ children: [{ text: 'a' }], type: 'p' }],
}) as any
) as any;
editor.isInline = (element: any) => element.inline === true;
const point = { offset: 0, path: [0, 0] };

editor.tf.withoutMerging(() => {
editor.tf.wrapNodes(
{ children: [], inline: true },
{ at: { anchor: point, focus: point }, split: true, voids: true }
);
});

expect(editor.history.undos).toHaveLength(1);

while (editor.history.undos.length > 0) {
editor.undo();
}

expect(editor.children).toEqual([{ children: [{ text: 'a' }], type: 'p' }]);
});

it('restores a valid selection when undoing a merge into a void inside withoutMerging', () => {
const value = [
{ children: [{ text: 'a' }], type: 'p' },
{ children: [{ text: '' }], type: 'void', void: true },
];
const editor = withHistory(
createEditor({ children: structuredClone(value) }) as any
) as any;
editor.isVoid = (element: any) => element.void === true;
const selection = {
anchor: { offset: 0, path: [1, 0] },
focus: { offset: 0, path: [0, 0] },
};
editor.selection = selection;

editor.tf.withoutMerging(() => {
editor.tf.delete({
at: { offset: 0, path: [1, 0] },
distance: 1,
reverse: true,
unit: 'character',
voids: true,
});
});

while (editor.history.undos.length > 0) {
editor.undo();
}

expect(editor.children).toEqual(value);
expect(editor.selection).toEqual(selection);
});
});
23 changes: 22 additions & 1 deletion packages/slate/src/slate-history/with-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const cloneRange = (range: TRange | null | undefined) =>
export const withHistory = <T extends Editor>(editor: T) => {
const e = editor as Editor & LegacyEditorMethods;
const { apply } = e;
const normalizing = e as unknown as Pick<Editor['tf'], 'normalize'>;
const { normalize } = normalizing;
let isSettled = true;
e.history = { redos: [], undos: [] };

e.redo = () => {
Expand Down Expand Up @@ -103,12 +106,21 @@ export const withHistory = <T extends Editor>(editor: T) => {
if (merge == null) {
if (lastBatch == null) {
merge = false;
} else if (operations.length > 0) {
} else if (lastOp && operations.includes(lastOp)) {
merge = true;
} else {
merge = shouldMerge(op, lastOp);
}
}
if (
!merge &&
!isSettled &&
lastBatch &&
lastOp &&
operations.includes(lastOp)
) {
merge = true;
}
if (e.api.isSplittingOnce()) {
merge = false;
e.tf.setSplittingOnce(undefined);
Expand All @@ -130,9 +142,18 @@ export const withHistory = <T extends Editor>(editor: T) => {
history.redos = [];
}

isSettled = false;
apply(op);
};

normalizing.normalize = (options) => {
normalize(options);

if (e.api.isNormalizing()) {
isSettled = true;
}
};

e.setNodesBatch = (updates: NodeBatchUpdate[]) => {
const ops = buildSetNodeBatchOperations(e, updates as any);

Expand Down
Loading
Loading