Skip to content

Commit 018857f

Browse files
committed
fix: remove Editor-keyed WeakMaps from slate-dom, move state onto DOMEditor
Remove 18 Editor-keyed WeakMaps from slate-dom/utils/weak-maps.ts and replace them with direct properties on the DOMEditor interface: - IS_NODE_MAP_DIRTY → editor.isNodeMapDirty - EDITOR_TO_WINDOW → editor.domWindow - EDITOR_TO_ELEMENT → editor.domElement - EDITOR_TO_PLACEHOLDER → editor.domPlaceholder - EDITOR_TO_PLACEHOLDER_ELEMENT → editor.domPlaceholderElement - EDITOR_TO_KEY_TO_ELEMENT → editor.keyToElement - IS_READ_ONLY → editor.readOnly - IS_FOCUSED → editor.focused - IS_COMPOSING → editor.composing - EDITOR_TO_USER_SELECTION → editor.userSelection - EDITOR_TO_ON_CHANGE → editor.onContextChange - EDITOR_TO_SCHEDULE_FLUSH → editor.scheduleFlush - EDITOR_TO_PENDING_INSERTION_MARKS → editor.pendingInsertionMarks - EDITOR_TO_USER_MARKS → editor.userMarks - EDITOR_TO_PENDING_DIFFS → editor.pendingDiffs - EDITOR_TO_PENDING_ACTION → editor.pendingAction - EDITOR_TO_PENDING_SELECTION → editor.pendingSelection - EDITOR_TO_FORCE_RENDER → editor.forceRender 5 Node/Element-keyed WeakMaps remain in weak-maps.ts for a follow-up. No behaviour changes — pure mechanical substitution.
1 parent 77a10ce commit 018857f

File tree

18 files changed

+157
-264
lines changed

18 files changed

+157
-264
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@portabletext/editor': patch
3+
---
4+
5+
fix: remove Editor-keyed WeakMaps from slate-dom, move state onto DOMEditor
6+
7+
WeakMaps are an upstream Slate pattern for supporting multiple editor instances sharing a module scope. Since PTE owns the editor lifecycle, storing state directly on the editor object is simpler and easier to debug.

packages/editor/src/editor/editor-machine.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {debug} from '../internal-utils/debug'
2020
import type {EventPosition} from '../internal-utils/event-position'
2121
import {sortByPriority} from '../priority/priority.sort'
2222
import {Transforms} from '../slate'
23-
import {EDITOR_TO_PENDING_SELECTION} from '../slate-dom'
2423
import {ReactEditor} from '../slate-react'
2524
import type {NamespaceEvent, OmitFromUnion} from '../type-utils'
2625
import type {EditorSelection} from '../types/editor'
@@ -317,7 +316,7 @@ export const editorMachine = setup({
317316
Transforms.select(slateEditor, currentSelection)
318317

319318
// Tell Slate to use this selection for DOM sync
320-
EDITOR_TO_PENDING_SELECTION.set(slateEditor, slateEditor.selection)
319+
slateEditor.pendingSelection = slateEditor.selection
321320

322321
// Trigger the DOM sync
323322
slateEditor.onChange()

packages/editor/src/operations/operation.insert.child.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {isTextBlock} from '@portabletext/schema'
22
import {getFocusBlock, getFocusSpan} from '../internal-utils/slate-utils'
33
import {VOID_CHILD_KEY} from '../internal-utils/values'
44
import {Transforms} from '../slate'
5-
import {EDITOR_TO_PENDING_SELECTION} from '../slate-dom'
65
import {parseInlineObject, parseSpan} from '../utils/parse-blocks'
76
import type {OperationImplementation} from './operation.types'
87

@@ -57,10 +56,7 @@ export const insertChildOperationImplementation: OperationImplementation<
5756

5857
// This makes sure the selection is set correctly when event handling is run
5958
// through Slate's Android input handling
60-
EDITOR_TO_PENDING_SELECTION.set(
61-
operation.editor,
62-
operation.editor.selection,
63-
)
59+
operation.editor.pendingSelection = operation.editor.selection
6460

6561
return
6662
}

packages/editor/src/operations/operation.select.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {toSlateRange} from '../internal-utils/to-slate-range'
22
import {Transforms} from '../slate'
3-
import {IS_FOCUSED, IS_READ_ONLY} from '../slate-dom'
43
import type {OperationImplementation} from './operation.types'
54

65
export const selectOperationImplementation: OperationImplementation<
@@ -21,7 +20,7 @@ export const selectOperationImplementation: OperationImplementation<
2120
Transforms.deselect(operation.editor)
2221
}
2322

24-
if (IS_FOCUSED.get(operation.editor) && IS_READ_ONLY.get(operation.editor)) {
25-
IS_FOCUSED.set(operation.editor, false)
23+
if (operation.editor.focused && operation.editor.readOnly) {
24+
operation.editor.focused = false
2625
}
2726
}

packages/editor/src/slate-dom/index.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,28 +65,12 @@ export {
6565
} from './utils/range-list'
6666

6767
export {
68-
EDITOR_TO_ELEMENT,
69-
EDITOR_TO_FORCE_RENDER,
70-
EDITOR_TO_KEY_TO_ELEMENT,
71-
EDITOR_TO_ON_CHANGE,
72-
EDITOR_TO_PENDING_ACTION,
73-
EDITOR_TO_PENDING_DIFFS,
74-
EDITOR_TO_PENDING_INSERTION_MARKS,
75-
EDITOR_TO_PENDING_SELECTION,
76-
EDITOR_TO_PLACEHOLDER_ELEMENT,
77-
EDITOR_TO_SCHEDULE_FLUSH,
78-
EDITOR_TO_USER_MARKS,
79-
EDITOR_TO_USER_SELECTION,
80-
EDITOR_TO_WINDOW,
8168
ELEMENT_TO_NODE,
82-
IS_COMPOSING,
83-
IS_FOCUSED,
84-
IS_NODE_MAP_DIRTY,
85-
IS_READ_ONLY,
8669
MARK_PLACEHOLDER_SYMBOL,
8770
NODE_TO_ELEMENT,
8871
NODE_TO_INDEX,
8972
NODE_TO_KEY,
9073
NODE_TO_PARENT,
9174
PLACEHOLDER_SYMBOL,
75+
type Action,
9276
} from './utils/weak-maps'

packages/editor/src/slate-dom/plugin/dom-editor.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import {
66
Transforms,
77
type BaseEditor,
88
type Node,
9+
type Operation,
910
type Path,
1011
type Point,
12+
type RangeRef,
13+
type Text,
1114
} from '../../slate'
1215
import type {TextDiff} from '../utils/diff-text'
1316
import {
@@ -32,18 +35,11 @@ import {
3235
import {IS_ANDROID, IS_CHROME, IS_FIREFOX} from '../utils/environment'
3336
import {Key} from '../utils/key'
3437
import {
35-
EDITOR_TO_ELEMENT,
36-
EDITOR_TO_KEY_TO_ELEMENT,
37-
EDITOR_TO_PENDING_DIFFS,
38-
EDITOR_TO_SCHEDULE_FLUSH,
39-
EDITOR_TO_WINDOW,
4038
ELEMENT_TO_NODE,
41-
IS_COMPOSING,
42-
IS_FOCUSED,
43-
IS_READ_ONLY,
4439
NODE_TO_INDEX,
4540
NODE_TO_KEY,
4641
NODE_TO_PARENT,
42+
type Action,
4743
} from '../utils/weak-maps'
4844

4945
/**
@@ -69,6 +65,25 @@ export interface DOMEditor extends BaseEditor {
6965
data: DataTransfer,
7066
originEvent?: 'drag' | 'copy' | 'cut',
7167
) => void
68+
69+
isNodeMapDirty: boolean
70+
domWindow: Window | null
71+
domElement: HTMLElement | null
72+
domPlaceholder: string
73+
domPlaceholderElement: HTMLElement | null
74+
keyToElement: WeakMap<Key, HTMLElement>
75+
readOnly: boolean
76+
focused: boolean
77+
composing: boolean
78+
userSelection: RangeRef | null
79+
onContextChange: ((options?: {operation?: Operation}) => void) | null
80+
scheduleFlush: (() => void) | null
81+
pendingInsertionMarks: Partial<Text> | null
82+
userMarks: Partial<Text> | null
83+
pendingDiffs: TextDiff[]
84+
pendingAction: Action | null
85+
pendingSelection: Range | null
86+
forceRender: (() => void) | null
7287
}
7388

7489
export interface DOMEditorInterface {
@@ -258,16 +273,16 @@ export interface DOMEditorInterface {
258273

259274
// eslint-disable-next-line no-redeclare
260275
export const DOMEditor: DOMEditorInterface = {
261-
androidPendingDiffs: (editor) => EDITOR_TO_PENDING_DIFFS.get(editor),
276+
androidPendingDiffs: (editor) => editor.pendingDiffs,
262277

263278
androidScheduleFlush: (editor) => {
264-
EDITOR_TO_SCHEDULE_FLUSH.get(editor)?.()
279+
editor.scheduleFlush?.()
265280
},
266281

267282
blur: (editor) => {
268283
const el = DOMEditor.toDOMNode(editor, editor)
269284
const root = DOMEditor.findDocumentOrShadowRoot(editor)
270-
IS_FOCUSED.set(editor, false)
285+
editor.focused = false
271286

272287
if (root.activeElement === el) {
273288
el.blur()
@@ -407,13 +422,13 @@ export const DOMEditor: DOMEditorInterface = {
407422

408423
focus: (editor, options = {retries: 5}) => {
409424
// Return if already focused
410-
if (IS_FOCUSED.get(editor)) {
425+
if (editor.focused) {
411426
return
412427
}
413428

414429
// Return if no dom node is associated with the editor, which means the editor is not yet mounted
415430
// or has been unmounted. This can happen especially, while retrying to focus the editor.
416-
if (!EDITOR_TO_ELEMENT.get(editor)) {
431+
if (!editor.domElement) {
417432
return
418433
}
419434

@@ -448,13 +463,13 @@ export const DOMEditor: DOMEditorInterface = {
448463
}
449464
// IS_FOCUSED should be set before calling el.focus() to ensure that
450465
// FocusedContext is updated to the correct value
451-
IS_FOCUSED.set(editor, true)
466+
editor.focused = true
452467
el.focus({preventScroll: true})
453468
}
454469
},
455470

456471
getWindow: (editor) => {
457-
const window = EDITOR_TO_WINDOW.get(editor)
472+
const window = editor.domWindow
458473
if (!window) {
459474
throw new Error('Unable to find a host window element for this editor')
460475
}
@@ -525,15 +540,15 @@ export const DOMEditor: DOMEditorInterface = {
525540
insertTextData: (editor, data) => editor.insertTextData(data),
526541

527542
isComposing: (editor) => {
528-
return !!IS_COMPOSING.get(editor)
543+
return !!editor.composing
529544
},
530545

531-
isFocused: (editor) => !!IS_FOCUSED.get(editor),
546+
isFocused: (editor) => !!editor.focused,
532547

533-
isReadOnly: (editor) => !!IS_READ_ONLY.get(editor),
548+
isReadOnly: (editor) => !!editor.readOnly,
534549

535550
isTargetInsideNonReadonlyVoid: (editor, target) => {
536-
if (IS_READ_ONLY.get(editor)) {
551+
if (editor.readOnly) {
537552
return false
538553
}
539554

@@ -547,10 +562,9 @@ export const DOMEditor: DOMEditorInterface = {
547562
editor.setFragmentData(data, originEvent),
548563

549564
toDOMNode: (editor, node) => {
550-
const KEY_TO_ELEMENT = EDITOR_TO_KEY_TO_ELEMENT.get(editor)
551565
const domNode = Editor.isEditor(node)
552-
? EDITOR_TO_ELEMENT.get(editor)
553-
: KEY_TO_ELEMENT?.get(DOMEditor.findKey(editor, node))
566+
? editor.domElement
567+
: editor.keyToElement?.get(DOMEditor.findKey(editor, node))
554568

555569
if (!domNode) {
556570
throw new Error(

packages/editor/src/slate-dom/plugin/with-dom.ts

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,7 @@ import {
1616
} from '../utils/diff-text'
1717
import {getPlainText, getSlateFragmentAttribute, isDOMText} from '../utils/dom'
1818
import type {Key} from '../utils/key'
19-
import {
20-
EDITOR_TO_KEY_TO_ELEMENT,
21-
EDITOR_TO_ON_CHANGE,
22-
EDITOR_TO_PENDING_ACTION,
23-
EDITOR_TO_PENDING_DIFFS,
24-
EDITOR_TO_PENDING_SELECTION,
25-
EDITOR_TO_USER_SELECTION,
26-
IS_NODE_MAP_DIRTY,
27-
NODE_TO_KEY,
28-
} from '../utils/weak-maps'
19+
import {NODE_TO_KEY} from '../utils/weak-maps'
2920
import {DOMEditor} from './dom-editor'
3021

3122
/**
@@ -44,40 +35,53 @@ export const withDOM = <T extends Editor>(
4435
const e = editor as T & DOMEditor
4536
const {apply, onChange} = e
4637

47-
// The WeakMap which maps a key to a specific HTMLElement must be scoped to the editor instance to
48-
// avoid collisions between editors in the DOM that share the same value.
49-
EDITOR_TO_KEY_TO_ELEMENT.set(e, new WeakMap())
38+
// Initialize DOMEditor state properties
39+
e.isNodeMapDirty = false
40+
e.domWindow = null
41+
e.domElement = null
42+
e.domPlaceholder = ''
43+
e.domPlaceholderElement = null
44+
e.keyToElement = new WeakMap()
45+
e.readOnly = false
46+
e.focused = false
47+
e.composing = false
48+
e.userSelection = null
49+
e.onContextChange = null
50+
e.scheduleFlush = null
51+
e.pendingInsertionMarks = null
52+
e.userMarks = null
53+
e.pendingDiffs = []
54+
e.pendingAction = null
55+
e.pendingSelection = null
56+
e.forceRender = null
5057

5158
// This attempts to reset the NODE_TO_KEY entry to the correct value
5259
// as apply() changes the object reference and hence invalidates the NODE_TO_KEY entry
5360
e.apply = (op: Operation) => {
5461
const matches: [Path, Key][] = []
5562
const pathRefMatches: [PathRef, Key][] = []
5663

57-
const pendingDiffs = EDITOR_TO_PENDING_DIFFS.get(e)
64+
const pendingDiffs = e.pendingDiffs
5865
if (pendingDiffs?.length) {
5966
const transformed = pendingDiffs
6067
.map((textDiff) => transformTextDiff(textDiff, op))
6168
.filter(Boolean) as TextDiff[]
6269

63-
EDITOR_TO_PENDING_DIFFS.set(e, transformed)
70+
e.pendingDiffs = transformed
6471
}
6572

66-
const pendingSelection = EDITOR_TO_PENDING_SELECTION.get(e)
73+
const pendingSelection = e.pendingSelection
6774
if (pendingSelection) {
68-
EDITOR_TO_PENDING_SELECTION.set(
69-
e,
70-
transformPendingRange(e, pendingSelection, op),
71-
)
75+
e.pendingSelection = transformPendingRange(e, pendingSelection, op)
7276
}
7377

74-
const pendingAction = EDITOR_TO_PENDING_ACTION.get(e)
78+
const pendingAction = e.pendingAction
7579
if (pendingAction?.at) {
7680
const at = Point.isPoint(pendingAction?.at)
7781
? transformPendingPoint(e, pendingAction.at, op)
7882
: transformPendingRange(e, pendingAction.at, op)
7983

80-
EDITOR_TO_PENDING_ACTION.set(e, at ? {...pendingAction, at} : null)
84+
e.pendingAction = at ? {...pendingAction, at} : null
8185
}
8286

8387
switch (op.type) {
@@ -91,8 +95,8 @@ export const withDOM = <T extends Editor>(
9195

9296
case 'set_selection': {
9397
// Selection was manually set, don't restore the user selection after the change.
94-
EDITOR_TO_USER_SELECTION.get(e)?.unref()
95-
EDITOR_TO_USER_SELECTION.delete(e)
98+
e.userSelection?.unref()
99+
e.userSelection = null
96100
break
97101
}
98102

@@ -146,7 +150,7 @@ export const withDOM = <T extends Editor>(
146150
case 'set_selection': {
147151
// FIXME: Rename to something like IS_DOM_EDITOR_DESYNCED
148152
// to better reflect reality, see #5792
149-
IS_NODE_MAP_DIRTY.set(e, true)
153+
e.isNodeMapDirty = true
150154
}
151155
}
152156

@@ -295,7 +299,7 @@ export const withDOM = <T extends Editor>(
295299
}
296300

297301
e.onChange = (options) => {
298-
const onContextChange = EDITOR_TO_ON_CHANGE.get(e)
302+
const onContextChange = e.onContextChange
299303

300304
if (onContextChange) {
301305
onContextChange(options)

packages/editor/src/slate-dom/utils/diff-text.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
Text,
99
type Operation,
1010
} from '../../slate'
11-
import {EDITOR_TO_PENDING_DIFFS} from './weak-maps'
1211

1312
export type StringDiff = {
1413
start: number
@@ -223,7 +222,7 @@ export function transformPendingPoint(
223222
point: Point,
224223
op: Operation,
225224
): Point | null {
226-
const pendingDiffs = EDITOR_TO_PENDING_DIFFS.get(editor)
225+
const pendingDiffs = editor.pendingDiffs
227226
const textDiff = pendingDiffs?.find(({path}) => Path.equals(path, point.path))
228227

229228
if (!textDiff || point.offset <= textDiff.diff.start) {

0 commit comments

Comments
 (0)