@portabletext/editor 1.0.14 → 1.0.16

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.14",
3
+ "version": "1.0.16",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -51,18 +51,18 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@jest/globals": "^29.7.0",
54
- "@playwright/test": "1.46.0",
54
+ "@playwright/test": "1.46.1",
55
55
  "@portabletext/toolkit": "^2.0.15",
56
- "@sanity/block-tools": "^3.54.0",
56
+ "@sanity/block-tools": "^3.55.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
- "@sanity/pkg-utils": "^6.10.9",
61
- "@sanity/schema": "^3.54.0",
60
+ "@sanity/pkg-utils": "^6.10.10",
61
+ "@sanity/schema": "^3.55.0",
62
62
  "@sanity/test": "0.0.1-alpha.1",
63
- "@sanity/types": "^3.54.0",
63
+ "@sanity/types": "^3.55.0",
64
64
  "@sanity/ui": "^2.8.8",
65
- "@sanity/util": "^3.54.0",
65
+ "@sanity/util": "^3.55.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.1.0",
78
- "@typescript-eslint/parser": "^8.1.0",
77
+ "@typescript-eslint/eslint-plugin": "^8.2.0",
78
+ "@typescript-eslint/parser": "^8.2.0",
79
79
  "@vitejs/plugin-react": "^4.3.1",
80
80
  "dotenv": "^16.4.5",
81
81
  "eslint": "^8.57.0",
@@ -84,14 +84,14 @@
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-d0e920e-20240815",
87
+ "eslint-plugin-react-compiler": "0.0.0-experimental-eeb1b2a-20240818",
88
88
  "eslint-plugin-tsdoc": "^0.3.0",
89
89
  "eslint-plugin-unicorn": "^55.0.0",
90
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",
94
- "jest-dev-server": "^10.0.0",
94
+ "jest-dev-server": "^10.1.0",
95
95
  "jest-environment-jsdom": "^29.7.0",
96
96
  "jest-environment-node": "^29.7.0",
97
97
  "node-ipc": "npm:@node-ipc/compat@9.2.5",
@@ -101,7 +101,7 @@
101
101
  "styled-components": "^6.1.12",
102
102
  "tsx": "^4.17.0",
103
103
  "typescript": "5.5.4",
104
- "vite": "^5.4.1"
104
+ "vite": "^5.4.2"
105
105
  },
106
106
  "peerDependencies": {
107
107
  "@sanity/block-tools": "^3.47.1",
@@ -9,7 +9,7 @@
9
9
  import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
10
10
  import {isEqual, uniq} from 'lodash'
11
11
  import {type Subject} from 'rxjs'
12
- import {type Descendant, Editor, Element, Path, Range, Text, Transforms} from 'slate'
12
+ import {type Descendant, Editor, Element, Node, Path, Range, Text, Transforms} from 'slate'
13
13
 
14
14
  import {
15
15
  type EditorChange,
@@ -54,29 +54,38 @@ export function createWithPortableTextMarkModel(
54
54
 
55
55
  // Extend Slate's default normalization. Merge spans with same set of .marks when doing merge_node operations, and clean up markDefs / marks
56
56
  editor.normalizeNode = (nodeEntry) => {
57
- normalizeNode(nodeEntry)
58
- if (
59
- editor.operations.some((op) =>
60
- [
61
- 'insert_node',
62
- 'insert_text',
63
- 'merge_node',
64
- 'remove_node',
65
- 'remove_text',
66
- 'set_node',
67
- ].includes(op.type),
68
- )
69
- ) {
70
- mergeSpans(editor)
71
- }
72
57
  const [node, path] = nodeEntry
58
+
73
59
  const isSpan = Text.isText(node) && node._type === types.span.name
74
60
  const isTextBlock = editor.isTextBlock(node)
61
+
62
+ if (editor.isTextBlock(node)) {
63
+ const children = Node.children(editor, path)
64
+
65
+ for (const [child, childPath] of children) {
66
+ const nextNode = node.children[childPath[1] + 1]
67
+
68
+ if (
69
+ editor.isTextSpan(child) &&
70
+ editor.isTextSpan(nextNode) &&
71
+ isEqual(child.marks, nextNode.marks)
72
+ ) {
73
+ debug(
74
+ 'Merging spans',
75
+ JSON.stringify(child, null, 2),
76
+ JSON.stringify(nextNode, null, 2),
77
+ )
78
+ Transforms.mergeNodes(editor, {at: [childPath[0], childPath[1] + 1], voids: true})
79
+ return
80
+ }
81
+ }
82
+ }
83
+
75
84
  if (isSpan || isTextBlock) {
76
85
  if (isSpan && !Array.isArray(node.marks)) {
77
86
  debug('Adding .marks to span node')
78
87
  Transforms.setNodes(editor, {marks: []}, {at: path})
79
- editor.onChange()
88
+ return
80
89
  }
81
90
  const hasSpanMarks = isSpan && (node.marks || []).length > 0
82
91
  if (hasSpanMarks) {
@@ -100,7 +109,7 @@ export function createWithPortableTextMarkModel(
100
109
  {marks: spanMarks.filter((mark) => !orphanedMarks.includes(mark))},
101
110
  {at: path},
102
111
  )
103
- editor.onChange()
112
+ return
104
113
  }
105
114
  }
106
115
  }
@@ -124,7 +133,7 @@ export function createWithPortableTextMarkModel(
124
133
  // eslint-disable-next-line max-depth
125
134
  if (!isNormalized) {
126
135
  Transforms.setNodes(editor, {markDefs: newMarkDefs}, {at: targetPath, voids: false})
127
- editor.onChange()
136
+ return
128
137
  }
129
138
  }
130
139
  }
@@ -148,7 +157,7 @@ export function createWithPortableTextMarkModel(
148
157
  {markDefs: uniq([...oldDefs, ...op.properties.markDefs])},
149
158
  {at: targetPath, voids: false},
150
159
  )
151
- editor.onChange()
160
+ return
152
161
  }
153
162
  }
154
163
  // Make sure marks are reset, if a block is split at the end.
@@ -169,7 +178,7 @@ export function createWithPortableTextMarkModel(
169
178
  child.marks.length > 0
170
179
  ) {
171
180
  Transforms.setNodes(editor, {marks: []}, {at: childPath, voids: false})
172
- editor.onChange()
181
+ return
173
182
  }
174
183
  }
175
184
  // Make sure markDefs are reset, if a block is split at start.
@@ -192,19 +201,10 @@ export function createWithPortableTextMarkModel(
192
201
  (!block.children[0].marks || block.children[0].marks.length === 0)
193
202
  ) {
194
203
  Transforms.setNodes(editor, {markDefs: []}, {at: blockPath})
195
- editor.onChange()
204
+ return
196
205
  }
197
206
  }
198
207
  }
199
- // Empty marks if text is empty
200
- if (
201
- isSpan &&
202
- Array.isArray(node.marks) &&
203
- (!node.marks || (node.marks.length > 0 && node.text === ''))
204
- ) {
205
- Transforms.setNodes(editor, {marks: []}, {at: path, voids: false})
206
- editor.onChange()
207
- }
208
208
  }
209
209
  // Check consistency of markDefs (unless we are merging two nodes)
210
210
  if (
@@ -229,9 +229,11 @@ export function createWithPortableTextMarkModel(
229
229
  },
230
230
  {at: path},
231
231
  )
232
- editor.onChange()
232
+ return
233
233
  }
234
234
  }
235
+
236
+ normalizeNode(nodeEntry)
235
237
  }
236
238
 
237
239
  editor.apply = (op) => {
@@ -343,6 +345,18 @@ export function createWithPortableTextMarkModel(
343
345
  editor.onChange()
344
346
  return
345
347
  }
348
+
349
+ const nodeHasMarks = node.marks !== undefined && node.marks.length > 0
350
+
351
+ if (nodeHasMarks && deletingAllText) {
352
+ Editor.withoutNormalizing(editor, () => {
353
+ apply(op)
354
+ Transforms.setNodes(editor, {marks: []}, {at: op.path})
355
+ })
356
+
357
+ editor.onChange()
358
+ return
359
+ }
346
360
  }
347
361
  }
348
362
 
@@ -353,34 +367,35 @@ export function createWithPortableTextMarkModel(
353
367
  editor.addMark = (mark: string) => {
354
368
  if (editor.selection) {
355
369
  if (Range.isExpanded(editor.selection)) {
356
- // Split if needed
357
- Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
358
- // Use new selection
359
- const splitTextNodes = [
360
- ...Editor.nodes(editor, {at: editor.selection, match: Text.isText}),
361
- ]
362
- const shouldRemoveMark = splitTextNodes.every((node) => node[0].marks?.includes(mark))
363
-
364
- if (shouldRemoveMark) {
365
- editor.removeMark(mark)
366
- return editor
367
- }
368
370
  Editor.withoutNormalizing(editor, () => {
369
- splitTextNodes.forEach(([node, path]) => {
370
- const marks = [
371
- ...(Array.isArray(node.marks) ? node.marks : []).filter(
372
- (eMark: string) => eMark !== mark,
373
- ),
374
- mark,
375
- ]
376
- Transforms.setNodes(
377
- editor,
378
- {marks},
379
- {at: path, match: Text.isText, split: true, hanging: true},
380
- )
381
- })
371
+ // Split if needed
372
+ Transforms.setNodes(editor, {}, {match: Text.isText, split: true})
373
+ // Use new selection
374
+ const splitTextNodes = Range.isRange(editor.selection)
375
+ ? [...Editor.nodes(editor, {at: editor.selection, match: Text.isText})]
376
+ : []
377
+ const shouldRemoveMark =
378
+ splitTextNodes.length > 1 &&
379
+ splitTextNodes.every((node) => node[0].marks?.includes(mark))
380
+
381
+ if (shouldRemoveMark) {
382
+ editor.removeMark(mark)
383
+ } else {
384
+ splitTextNodes.forEach(([node, path]) => {
385
+ const marks = [
386
+ ...(Array.isArray(node.marks) ? node.marks : []).filter(
387
+ (eMark: string) => eMark !== mark,
388
+ ),
389
+ mark,
390
+ ]
391
+ Transforms.setNodes(
392
+ editor,
393
+ {marks},
394
+ {at: path, match: Text.isText, split: true, hanging: true},
395
+ )
396
+ })
397
+ }
382
398
  })
383
- Editor.normalize(editor)
384
399
  } else {
385
400
  const existingMarks: string[] =
386
401
  {
@@ -486,36 +501,4 @@ export function createWithPortableTextMarkModel(
486
501
  }
487
502
  return editor
488
503
  }
489
-
490
- /**
491
- * Normalize re-marked spans in selection
492
- */
493
- function mergeSpans(editor: PortableTextSlateEditor) {
494
- const {selection} = editor
495
-
496
- if (selection) {
497
- const textNodesInSelection = Array.from(
498
- Editor.nodes(editor, {
499
- at: Editor.range(editor, [selection.anchor.path[0]], [selection.focus.path[0]]),
500
- match: Text.isText,
501
- reverse: true,
502
- }),
503
- )
504
-
505
- for (const [node, path] of textNodesInSelection) {
506
- const [parent] = path.length > 1 ? Editor.node(editor, Path.parent(path)) : [undefined]
507
- const nextPath = [path[0], path[1] + 1]
508
-
509
- if (editor.isTextBlock(parent)) {
510
- const nextNode = parent.children[nextPath[1]]
511
-
512
- if (Text.isText(nextNode) && isEqual(nextNode.marks, node.marks)) {
513
- debug('Merging spans')
514
- Transforms.mergeNodes(editor, {at: nextPath, voids: true})
515
- editor.onChange()
516
- }
517
- }
518
- }
519
- }
520
- }
521
504
  }