@portabletext/editor 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -47,22 +47,22 @@
47
47
  "is-hotkey-esm": "^1.0.0",
48
48
  "lodash": "^4.17.21",
49
49
  "slate": "0.103.0",
50
- "slate-react": "0.107.1"
50
+ "slate-react": "0.108.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@jest/globals": "^29.7.0",
54
54
  "@playwright/test": "1.46.0",
55
55
  "@portabletext/toolkit": "^2.0.15",
56
- "@sanity/block-tools": "^3.53.0",
56
+ "@sanity/block-tools": "^3.54.0",
57
57
  "@sanity/diff-match-patch": "^3.1.1",
58
58
  "@sanity/eslint-config-i18n": "^1.1.0",
59
59
  "@sanity/eslint-config-studio": "^4.0.0",
60
60
  "@sanity/pkg-utils": "^6.10.9",
61
- "@sanity/schema": "^3.53.0",
61
+ "@sanity/schema": "^3.54.0",
62
62
  "@sanity/test": "0.0.1-alpha.1",
63
- "@sanity/types": "^3.53.0",
63
+ "@sanity/types": "^3.54.0",
64
64
  "@sanity/ui": "^2.8.8",
65
- "@sanity/util": "^3.53.0",
65
+ "@sanity/util": "^3.54.0",
66
66
  "@testing-library/dom": "^10.4.0",
67
67
  "@testing-library/react": "^16.0.0",
68
68
  "@types/debug": "^4.1.5",
@@ -74,8 +74,8 @@
74
74
  "@types/react": "^18.3.3",
75
75
  "@types/react-dom": "^18.3.0",
76
76
  "@types/ws": "~8.5.12",
77
- "@typescript-eslint/eslint-plugin": "^8.0.1",
78
- "@typescript-eslint/parser": "^8.0.1",
77
+ "@typescript-eslint/eslint-plugin": "^8.1.0",
78
+ "@typescript-eslint/parser": "^8.1.0",
79
79
  "@vitejs/plugin-react": "^4.3.1",
80
80
  "dotenv": "^16.4.5",
81
81
  "eslint": "^8.57.0",
@@ -84,10 +84,10 @@
84
84
  "eslint-import-resolver-typescript": "^3.6.1",
85
85
  "eslint-plugin-import": "^2.29.1",
86
86
  "eslint-plugin-prettier": "^5.2.1",
87
- "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
87
+ "eslint-plugin-react-compiler": "0.0.0-experimental-d0e920e-20240815",
88
88
  "eslint-plugin-tsdoc": "^0.3.0",
89
89
  "eslint-plugin-unicorn": "^55.0.0",
90
- "eslint-plugin-unused-imports": "^4.0.1",
90
+ "eslint-plugin-unused-imports": "^4.1.3",
91
91
  "express": "^4.19.2",
92
92
  "express-ws": "^5.0.2",
93
93
  "jest": "^29.7.0",
@@ -99,9 +99,9 @@
99
99
  "react-dom": "^18.3.1",
100
100
  "rxjs": "^7.8.1",
101
101
  "styled-components": "^6.1.12",
102
- "tsx": "^4.16.5",
102
+ "tsx": "^4.17.0",
103
103
  "typescript": "5.5.4",
104
- "vite": "^5.3.5"
104
+ "vite": "^5.4.1"
105
105
  },
106
106
  "peerDependencies": {
107
107
  "@sanity/block-tools": "^3.47.1",
@@ -11,7 +11,6 @@ import {validateValue} from '../../utils/validateValue'
11
11
  import {toSlateValue, VOID_CHILD_KEY} from '../../utils/values'
12
12
  import {isChangingLocally, isChangingRemotely, withRemoteChanges} from '../../utils/withChanges'
13
13
  import {withoutPatching} from '../../utils/withoutPatching'
14
- import {withPreserveKeys} from '../../utils/withPreserveKeys'
15
14
  import {withoutSaving} from '../plugins/createWithUndoRedo'
16
15
  import {type PortableTextEditor} from '../PortableTextEditor'
17
16
 
@@ -184,10 +183,8 @@ export function useSyncValue(
184
183
  currentBlock,
185
184
  )
186
185
  if (validation.valid || validation.resolution?.autoResolve) {
187
- withPreserveKeys(slateEditor, () => {
188
- Transforms.insertNodes(slateEditor, currentBlock, {
189
- at: [currentBlockIndex],
190
- })
186
+ Transforms.insertNodes(slateEditor, currentBlock, {
187
+ at: [currentBlockIndex],
191
188
  })
192
189
  } else {
193
190
  debug('Invalid', validation)
@@ -267,9 +264,7 @@ function _replaceBlock(
267
264
  Transforms.deselect(slateEditor)
268
265
  }
269
266
  Transforms.removeNodes(slateEditor, {at: [currentBlockIndex]})
270
- withPreserveKeys(slateEditor, () => {
271
- Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
272
- })
267
+ Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
273
268
  slateEditor.onChange()
274
269
  if (selectionFocusOnBlock) {
275
270
  Transforms.select(slateEditor, currentSelection)
@@ -350,21 +345,17 @@ function _updateBlock(
350
345
  Transforms.removeNodes(slateEditor, {
351
346
  at: [currentBlockIndex, currentBlockChildIndex],
352
347
  })
353
- withPreserveKeys(slateEditor, () => {
354
- Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
355
- at: [currentBlockIndex, currentBlockChildIndex],
356
- })
348
+ Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
349
+ at: [currentBlockIndex, currentBlockChildIndex],
357
350
  })
358
351
  slateEditor.onChange()
359
352
  // Insert it if it didn't exist before
360
353
  } else if (!oldBlockChild) {
361
354
  debug('Inserting new child', currentBlockChild)
362
- withPreserveKeys(slateEditor, () => {
363
- Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
364
- at: [currentBlockIndex, currentBlockChildIndex],
365
- })
366
- slateEditor.onChange()
355
+ Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
356
+ at: [currentBlockIndex, currentBlockChildIndex],
367
357
  })
358
+ slateEditor.onChange()
368
359
  }
369
360
  }
370
361
  })
@@ -1,4 +1,6 @@
1
1
  import {type PortableTextSlateEditor} from '../../types/editor'
2
+ import {isChangingRemotely} from '../../utils/withChanges'
3
+ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
2
4
 
3
5
  /**
4
6
  * This plugin makes sure that the PTE maxBlocks prop is respected
@@ -8,6 +10,24 @@ export function createWithMaxBlocks(maxBlocks: number) {
8
10
  return function withMaxBlocks(editor: PortableTextSlateEditor): PortableTextSlateEditor {
9
11
  const {apply} = editor
10
12
  editor.apply = (operation) => {
13
+ /**
14
+ * We don't want to run any side effects when the editor is processing
15
+ * remote changes.
16
+ */
17
+ if (isChangingRemotely(editor)) {
18
+ apply(operation)
19
+ return
20
+ }
21
+
22
+ /**
23
+ * We don't want to run any side effects when the editor is undoing or
24
+ * redoing operations.
25
+ */
26
+ if (isUndoing(editor) || isRedoing(editor)) {
27
+ apply(operation)
28
+ return
29
+ }
30
+
11
31
  const rows = maxBlocks
12
32
  if (rows > 0 && editor.children.length >= rows) {
13
33
  if (
@@ -1,7 +1,8 @@
1
1
  import {Editor, Element, Node, Transforms} from 'slate'
2
2
 
3
3
  import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
4
- import {isPreservingKeys, PRESERVE_KEYS} from '../../utils/withPreserveKeys'
4
+ import {isChangingRemotely} from '../../utils/withChanges'
5
+ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
5
6
 
6
7
  /**
7
8
  * This plugin makes sure that every new node in the editor get a new _key prop when created
@@ -12,23 +13,36 @@ export function createWithObjectKeys(
12
13
  keyGenerator: () => string,
13
14
  ) {
14
15
  return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor {
15
- PRESERVE_KEYS.set(editor, false)
16
16
  const {apply, normalizeNode} = editor
17
17
 
18
- // The apply function can be called with a scope (withPreserveKeys) that will
19
- // preserve keys for the produced nodes if they have a _key property set already.
20
18
  // The default behavior is to always generate a new key here.
21
19
  // For example, when undoing and redoing we want to retain the keys, but
22
20
  // when we create a new bold span by splitting a non-bold-span we want the produced node to get a new key.
23
21
  editor.apply = (operation) => {
24
- if (operation.type === 'split_node') {
25
- const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.properties)
22
+ /**
23
+ * We don't want to run any side effects when the editor is processing
24
+ * remote changes.
25
+ */
26
+ if (isChangingRemotely(editor)) {
27
+ apply(operation)
28
+ return
29
+ }
30
+
31
+ /**
32
+ * We don't want to run any side effects when the editor is undoing or
33
+ * redoing operations.
34
+ */
35
+ if (isUndoing(editor) || isRedoing(editor)) {
36
+ apply(operation)
37
+ return
38
+ }
26
39
 
40
+ if (operation.type === 'split_node') {
27
41
  apply({
28
42
  ...operation,
29
43
  properties: {
30
44
  ...operation.properties,
31
- ...(withNewKey ? {_key: keyGenerator()} : {}),
45
+ _key: keyGenerator(),
32
46
  },
33
47
  })
34
48
 
@@ -36,15 +50,12 @@ export function createWithObjectKeys(
36
50
  }
37
51
 
38
52
  if (operation.type === 'insert_node') {
39
- // Must be given a new key or adding/removing marks while typing gets in trouble (duped keys)!
40
- const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.node)
41
-
42
53
  if (!Editor.isEditor(operation.node)) {
43
54
  apply({
44
55
  ...operation,
45
56
  node: {
46
57
  ...operation.node,
47
- ...(withNewKey ? {_key: keyGenerator()} : {}),
58
+ _key: keyGenerator(),
48
59
  },
49
60
  })
50
61
 
@@ -27,7 +27,6 @@ import {fromSlateValue, isEqualToEmptyEditor} from '../../utils/values'
27
27
  import {IS_PROCESSING_REMOTE_CHANGES, KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
28
28
  import {withRemoteChanges} from '../../utils/withChanges'
29
29
  import {isPatching, PATCHING, withoutPatching} from '../../utils/withoutPatching'
30
- import {withPreserveKeys} from '../../utils/withPreserveKeys'
31
30
  import {withoutSaving} from './createWithUndoRedo'
32
31
 
33
32
  const debug = debugWithName('plugin:withPatches')
@@ -117,11 +116,9 @@ export function createWithPatches({
117
116
  Editor.withoutNormalizing(editor, () => {
118
117
  withoutPatching(editor, () => {
119
118
  withoutSaving(editor, () => {
120
- withPreserveKeys(editor, () => {
121
- patches.forEach((patch) => {
122
- if (debug.enabled) debug(`Handling remote patch ${JSON.stringify(patch)}`)
123
- changed = applyPatch(editor, patch)
124
- })
119
+ patches.forEach((patch) => {
120
+ if (debug.enabled) debug(`Handling remote patch ${JSON.stringify(patch)}`)
121
+ changed = applyPatch(editor, patch)
125
122
  })
126
123
  })
127
124
  })
@@ -3,6 +3,8 @@ import {Editor, Path} from 'slate'
3
3
  import {type PortableTextSlateEditor} from '../../types/editor'
4
4
  import {type SlateTextBlock, type VoidElement} from '../../types/slate'
5
5
  import {debugWithName} from '../../utils/debug'
6
+ import {isChangingRemotely} from '../../utils/withChanges'
7
+ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
6
8
 
7
9
  const debug = debugWithName('plugin:withPlaceholderBlock')
8
10
 
@@ -17,6 +19,24 @@ export function createWithPlaceholderBlock(): (
17
19
  const {apply} = editor
18
20
 
19
21
  editor.apply = (op) => {
22
+ /**
23
+ * We don't want to run any side effects when the editor is processing
24
+ * remote changes.
25
+ */
26
+ if (isChangingRemotely(editor)) {
27
+ apply(op)
28
+ return
29
+ }
30
+
31
+ /**
32
+ * We don't want to run any side effects when the editor is undoing or
33
+ * redoing operations.
34
+ */
35
+ if (isUndoing(editor) || isRedoing(editor)) {
36
+ apply(op)
37
+ return
38
+ }
39
+
20
40
  if (op.type === 'remove_node') {
21
41
  const node = op.node as SlateTextBlock | VoidElement
22
42
  if (op.path[0] === 0 && Editor.isVoid(editor, node)) {
@@ -19,7 +19,8 @@ import {
19
19
  import {debugWithName} from '../../utils/debug'
20
20
  import {toPortableTextRange} from '../../utils/ranges'
21
21
  import {EMPTY_MARKS} from '../../utils/values'
22
- import {withoutPreserveKeys} from '../../utils/withPreserveKeys'
22
+ import {isChangingRemotely} from '../../utils/withChanges'
23
+ import {isRedoing, isUndoing} from '../../utils/withUndoRedo'
23
24
 
24
25
  const debug = debugWithName('plugin:withPortableTextMarkModel')
25
26
 
@@ -234,6 +235,24 @@ export function createWithPortableTextMarkModel(
234
235
  }
235
236
 
236
237
  editor.apply = (op) => {
238
+ /**
239
+ * We don't want to run any side effects when the editor is processing
240
+ * remote changes.
241
+ */
242
+ if (isChangingRemotely(editor)) {
243
+ apply(op)
244
+ return
245
+ }
246
+
247
+ /**
248
+ * We don't want to run any side effects when the editor is undoing or
249
+ * redoing operations.
250
+ */
251
+ if (isUndoing(editor) || isRedoing(editor)) {
252
+ apply(op)
253
+ return
254
+ }
255
+
237
256
  // Special hook before inserting text at the end of an annotation.
238
257
  if (op.type === 'insert_text') {
239
258
  const {selection} = editor
@@ -295,21 +314,10 @@ export function createWithPortableTextMarkModel(
295
314
  const deletingFromTheEnd = op.offset + op.text.length === node.text.length
296
315
 
297
316
  if (nodeHasAnnotations && deletingPartOfTheNode && deletingFromTheEnd) {
298
- /**
299
- * If all of these conditions match then override the ordinary
300
- * `remove_text` operation and turn it into `split_nodes` followed
301
- * by `remove_nodes`. This is so if the operation can be properly
302
- * undone. Undoing a `remove_text` results in an `insert_text` and
303
- * we want to bail out of that in this exact scenario to make sure
304
- * the inserted text is annotated. (See custom logic regarding
305
- * `insert_text`)
306
- */
307
317
  Editor.withoutNormalizing(editor, () => {
308
- withoutPreserveKeys(editor, () => {
309
- Transforms.splitNodes(editor, {
310
- match: Text.isText,
311
- at: {path: op.path, offset: op.offset},
312
- })
318
+ Transforms.splitNodes(editor, {
319
+ match: Text.isText,
320
+ at: {path: op.path, offset: op.offset},
313
321
  })
314
322
  Transforms.removeNodes(editor, {at: Path.next(op.path)})
315
323
  })
@@ -317,6 +325,24 @@ export function createWithPortableTextMarkModel(
317
325
  editor.onChange()
318
326
  return
319
327
  }
328
+
329
+ const deletingAllText = op.offset === 0 && deletingFromTheEnd
330
+
331
+ if (nodeHasAnnotations && deletingAllText) {
332
+ const marksWithoutAnnotationMarks: string[] = (
333
+ {
334
+ ...(Editor.marks(editor) || {}),
335
+ }.marks || []
336
+ ).filter((mark) => decorators.includes(mark))
337
+
338
+ Editor.withoutNormalizing(editor, () => {
339
+ apply(op)
340
+ Transforms.setNodes(editor, {marks: marksWithoutAnnotationMarks}, {at: op.path})
341
+ })
342
+
343
+ editor.onChange()
344
+ return
345
+ }
320
346
  }
321
347
  }
322
348
 
@@ -12,7 +12,7 @@ import {type Descendant, Editor, Operation, Path, type SelectionOperation, Trans
12
12
  import {type PatchObservable, type PortableTextSlateEditor} from '../../types/editor'
13
13
  import {debugWithName} from '../../utils/debug'
14
14
  import {fromSlateValue} from '../../utils/values'
15
- import {withPreserveKeys} from '../../utils/withPreserveKeys'
15
+ import {setIsRedoing, setIsUndoing, withRedoing, withUndoing} from '../../utils/withUndoRedo'
16
16
 
17
17
  const debug = debugWithName('plugin:withUndoRedo')
18
18
  const debugVerbose = debug.enabled && false
@@ -150,7 +150,7 @@ export function createWithUndoRedo(
150
150
 
151
151
  try {
152
152
  Editor.withoutNormalizing(editor, () => {
153
- withPreserveKeys(editor, () => {
153
+ withUndoing(editor, () => {
154
154
  withoutSaving(editor, () => {
155
155
  reversedOperations.forEach((op) => {
156
156
  editor.apply(op)
@@ -166,6 +166,7 @@ export function createWithUndoRedo(
166
166
  Transforms.deselect(editor)
167
167
  editor.history = {undos: [], redos: []}
168
168
  SAVING.set(editor, true)
169
+ setIsUndoing(editor, false)
169
170
  editor.onChange()
170
171
  return
171
172
  }
@@ -195,7 +196,7 @@ export function createWithUndoRedo(
195
196
  })
196
197
  try {
197
198
  Editor.withoutNormalizing(editor, () => {
198
- withPreserveKeys(editor, () => {
199
+ withRedoing(editor, () => {
199
200
  withoutSaving(editor, () => {
200
201
  // eslint-disable-next-line max-nested-callbacks
201
202
  transformedOperations.forEach((op) => {
@@ -212,6 +213,7 @@ export function createWithUndoRedo(
212
213
  Transforms.deselect(editor)
213
214
  editor.history = {undos: [], redos: []}
214
215
  SAVING.set(editor, true)
216
+ setIsRedoing(editor, false)
215
217
  editor.onChange()
216
218
  return
217
219
  }
@@ -0,0 +1,34 @@
1
+ import {type Editor} from 'slate'
2
+
3
+ const IS_UDOING: WeakMap<Editor, boolean | undefined> = new WeakMap()
4
+ const IS_REDOING: WeakMap<Editor, boolean | undefined> = new WeakMap()
5
+
6
+ export function withUndoing(editor: Editor, fn: () => void) {
7
+ const prev = isUndoing(editor)
8
+ IS_UDOING.set(editor, true)
9
+ fn()
10
+ IS_UDOING.set(editor, prev)
11
+ }
12
+
13
+ export function isUndoing(editor: Editor) {
14
+ return IS_UDOING.get(editor) ?? false
15
+ }
16
+
17
+ export function setIsUndoing(editor: Editor, isUndoing: boolean) {
18
+ IS_UDOING.set(editor, isUndoing)
19
+ }
20
+
21
+ export function withRedoing(editor: Editor, fn: () => void) {
22
+ const prev = isRedoing(editor)
23
+ IS_REDOING.set(editor, true)
24
+ fn()
25
+ IS_REDOING.set(editor, prev)
26
+ }
27
+
28
+ export function isRedoing(editor: Editor) {
29
+ return IS_REDOING.get(editor) ?? false
30
+ }
31
+
32
+ export function setIsRedoing(editor: Editor, isRedoing: boolean) {
33
+ IS_REDOING.set(editor, isRedoing)
34
+ }
@@ -1,21 +0,0 @@
1
- import {type Editor} from 'slate'
2
-
3
- export const PRESERVE_KEYS: WeakMap<Editor, boolean | undefined> = new WeakMap()
4
-
5
- export function withPreserveKeys(editor: Editor, fn: () => void): void {
6
- const prev = isPreservingKeys(editor)
7
- PRESERVE_KEYS.set(editor, true)
8
- fn()
9
- PRESERVE_KEYS.set(editor, prev)
10
- }
11
-
12
- export function withoutPreserveKeys(editor: Editor, fn: () => void): void {
13
- const prev = isPreservingKeys(editor)
14
- PRESERVE_KEYS.set(editor, false)
15
- fn()
16
- PRESERVE_KEYS.set(editor, prev)
17
- }
18
-
19
- export function isPreservingKeys(editor: Editor): boolean | undefined {
20
- return PRESERVE_KEYS.get(editor)
21
- }