@portabletext/editor 1.12.0 → 1.12.2

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.12.0",
3
+ "version": "1.12.2",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -58,11 +58,11 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@portabletext/toolkit": "^2.0.16",
61
- "@sanity/block-tools": "^3.64.3",
61
+ "@sanity/block-tools": "^3.65.1",
62
62
  "@sanity/diff-match-patch": "^3.1.1",
63
- "@sanity/pkg-utils": "^6.11.12",
64
- "@sanity/schema": "^3.64.3",
65
- "@sanity/types": "^3.64.3",
63
+ "@sanity/pkg-utils": "^6.11.13",
64
+ "@sanity/schema": "^3.65.1",
65
+ "@sanity/types": "^3.65.1",
66
66
  "@testing-library/jest-dom": "^6.6.3",
67
67
  "@testing-library/react": "^16.0.1",
68
68
  "@types/debug": "^4.1.5",
@@ -72,27 +72,27 @@
72
72
  "@types/react-dom": "^18.3.1",
73
73
  "@typescript-eslint/eslint-plugin": "^8.15.0",
74
74
  "@typescript-eslint/parser": "^8.15.0",
75
- "@vitejs/plugin-react": "^4.3.3",
75
+ "@vitejs/plugin-react": "^4.3.4",
76
76
  "@vitest/browser": "^2.1.5",
77
77
  "babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
78
78
  "eslint": "8.57.1",
79
79
  "eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
80
80
  "eslint-plugin-react-hooks": "^5.0.0",
81
81
  "jsdom": "^25.0.1",
82
- "racejar": "1.0.0",
83
82
  "react": "^18.3.1",
84
83
  "react-dom": "^18.3.1",
85
84
  "rxjs": "^7.8.1",
86
85
  "styled-components": "^6.1.13",
87
- "typescript": "5.6.3",
86
+ "typescript": "5.7.2",
88
87
  "vite": "^5.4.11",
89
88
  "vitest": "^2.1.5",
90
- "vitest-browser-react": "^0.0.3"
89
+ "vitest-browser-react": "^0.0.4",
90
+ "racejar": "1.0.1"
91
91
  },
92
92
  "peerDependencies": {
93
- "@sanity/block-tools": "^3.64.3",
94
- "@sanity/schema": "^3.64.3",
95
- "@sanity/types": "^3.64.3",
93
+ "@sanity/block-tools": "^3.65.1",
94
+ "@sanity/schema": "^3.65.1",
95
+ "@sanity/types": "^3.65.1",
96
96
  "react": "^16.9 || ^17 || ^18",
97
97
  "rxjs": "^7.8.1",
98
98
  "styled-components": "^6.1.13"
@@ -12,7 +12,7 @@ export function insertBlock({
12
12
  schema,
13
13
  }: {
14
14
  block: Descendant
15
- placement: 'auto' | 'after'
15
+ placement: 'auto' | 'after' | 'before'
16
16
  editor: PortableTextSlateEditor
17
17
  schema: PortableTextMemberSchemaTypes
18
18
  }) {
@@ -50,6 +50,8 @@ export function insertBlock({
50
50
  anchor: {path: [nextPath[0], 0], offset: 0},
51
51
  focus: {path: [nextPath[0], 0], offset: 0},
52
52
  })
53
+ } else if (placement === 'before') {
54
+ Transforms.insertNodes(editor, block, {at: focusBlockPath})
53
55
  } else {
54
56
  Editor.insertNode(editor, block)
55
57
  }
@@ -1,4 +1,3 @@
1
- import {isPortableTextSpan} from '@portabletext/toolkit'
2
1
  import {isPortableTextTextBlock} from '@sanity/types'
3
2
  import type {PortableTextMemberSchemaTypes} from '../../types/editor'
4
3
  import {defineBehavior} from './behavior.types'
@@ -10,6 +9,7 @@ import {
10
9
  selectionIsCollapsed,
11
10
  } from './behavior.utils'
12
11
  import {spanSelectionPointToBlockOffset} from './behavior.utils.block-offset'
12
+ import {getBlockTextBefore} from './behavior.utilts.get-text-before'
13
13
 
14
14
  /**
15
15
  * @alpha
@@ -146,13 +146,23 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
146
146
  return false
147
147
  }
148
148
 
149
- const onlyText = focusBlock.node.children.every(isPortableTextSpan)
150
- const blockText = focusBlock.node.children
151
- .map((child) => child.text ?? '')
152
- .join('')
149
+ const textBefore = getBlockTextBefore({
150
+ value: context.value,
151
+ point: context.selection.focus,
152
+ })
153
+ const hrBlockOffsets = {
154
+ anchor: {
155
+ path: focusBlock.path,
156
+ offset: 0,
157
+ },
158
+ focus: {
159
+ path: focusBlock.path,
160
+ offset: 3,
161
+ },
162
+ }
153
163
 
154
- if (onlyText && blockText === `${hrCharacter}${hrCharacter}`) {
155
- return {hrObject, focusBlock, hrCharacter}
164
+ if (textBefore === `${hrCharacter}${hrCharacter}`) {
165
+ return {hrObject, focusBlock, hrCharacter, hrBlockOffsets}
156
166
  }
157
167
 
158
168
  return false
@@ -164,19 +174,15 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
164
174
  text: hrCharacter,
165
175
  },
166
176
  ],
167
- (_, {hrObject, focusBlock}) => [
177
+ (_, {hrObject, hrBlockOffsets}) => [
168
178
  {
169
179
  type: 'insert block object',
170
- placement: 'after',
180
+ placement: 'before',
171
181
  blockObject: hrObject,
172
182
  },
173
183
  {
174
- type: 'delete block',
175
- blockPath: focusBlock.path,
176
- },
177
- {
178
- type: 'insert text block',
179
- placement: 'after',
184
+ type: 'delete text',
185
+ ...hrBlockOffsets,
180
186
  },
181
187
  ],
182
188
  ],
@@ -412,11 +418,11 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
412
418
  }
413
419
  }
414
420
 
415
- const looksLikeOrderedList = /^1./.test(focusSpan.node.text)
421
+ const looksLikeOrderedList = /^1\./.test(blockText)
416
422
  const orderedListStyle = config.orderedListStyle?.({
417
423
  schema: context.schema,
418
424
  })
419
- const caretAtTheEndOfOrderedList = context.selection.focus.offset === 2
425
+ const caretAtTheEndOfOrderedList = blockOffset.offset === 2
420
426
 
421
427
  if (
422
428
  defaultStyle &&
@@ -105,7 +105,7 @@ export type BehaviorActionIntend =
105
105
  | BehaviorEvent
106
106
  | {
107
107
  type: 'insert block object'
108
- placement: 'auto' | 'after'
108
+ placement: 'auto' | 'after' | 'before'
109
109
  blockObject: {
110
110
  name: string
111
111
  value?: {[prop: string]: unknown}
@@ -122,7 +122,7 @@ export type BehaviorActionIntend =
122
122
  }
123
123
  | {
124
124
  type: 'insert text block'
125
- placement: 'auto' | 'after'
125
+ placement: 'auto' | 'after' | 'before'
126
126
  textBlock?: {
127
127
  children?: PortableTextTextBlock['children']
128
128
  }
@@ -0,0 +1,92 @@
1
+ import {
2
+ isPortableTextSpan,
3
+ isPortableTextTextBlock,
4
+ type PortableTextBlock,
5
+ } from '@sanity/types'
6
+ import type {EditorSelection} from '../../types/editor'
7
+ import {isKeyedSegment} from './behavior.utils.is-keyed-segment'
8
+ import {reverseSelection} from './behavior.utils.reverse-selection'
9
+
10
+ export function getSelectionText({
11
+ value,
12
+ selection,
13
+ }: {
14
+ value: Array<PortableTextBlock>
15
+ selection: NonNullable<EditorSelection>
16
+ }): string {
17
+ let text = ''
18
+
19
+ if (!value || !selection) {
20
+ return text
21
+ }
22
+
23
+ const forwardSelection = selection.backward
24
+ ? reverseSelection(selection)
25
+ : selection
26
+
27
+ if (!forwardSelection) {
28
+ return text
29
+ }
30
+
31
+ for (const block of value) {
32
+ if (
33
+ isKeyedSegment(forwardSelection.anchor.path[0]) &&
34
+ block._key !== forwardSelection.anchor.path[0]._key
35
+ ) {
36
+ continue
37
+ }
38
+
39
+ if (!isPortableTextTextBlock(block)) {
40
+ continue
41
+ }
42
+
43
+ for (const child of block.children) {
44
+ if (isPortableTextSpan(child)) {
45
+ if (
46
+ isKeyedSegment(forwardSelection.anchor.path[2]) &&
47
+ child._key === forwardSelection.anchor.path[2]._key &&
48
+ isKeyedSegment(forwardSelection.focus.path[2]) &&
49
+ child._key === forwardSelection.focus.path[2]._key
50
+ ) {
51
+ text =
52
+ text +
53
+ child.text.slice(
54
+ forwardSelection.anchor.offset,
55
+ forwardSelection.focus.offset,
56
+ )
57
+
58
+ break
59
+ }
60
+
61
+ if (
62
+ isKeyedSegment(forwardSelection.anchor.path[2]) &&
63
+ child._key === forwardSelection.anchor.path[2]._key
64
+ ) {
65
+ text = text + child.text.slice(forwardSelection.anchor.offset)
66
+ continue
67
+ }
68
+
69
+ if (
70
+ isKeyedSegment(forwardSelection.focus.path[2]) &&
71
+ child._key === forwardSelection.focus.path[2]._key
72
+ ) {
73
+ text = text + child.text.slice(0, forwardSelection.focus.offset)
74
+ break
75
+ }
76
+
77
+ if (text.length > 0) {
78
+ text + child.text
79
+ }
80
+ }
81
+ }
82
+
83
+ if (
84
+ isKeyedSegment(forwardSelection.focus.path[0]) &&
85
+ block._key === forwardSelection.focus.path[0]._key
86
+ ) {
87
+ break
88
+ }
89
+ }
90
+
91
+ return text
92
+ }
@@ -0,0 +1,26 @@
1
+ import {
2
+ isPortableTextTextBlock,
3
+ type KeyedSegment,
4
+ type PortableTextBlock,
5
+ } from '@sanity/types'
6
+ import type {EditorSelectionPoint} from '../../types/editor'
7
+
8
+ export function getStartPoint({
9
+ node,
10
+ path,
11
+ }: {
12
+ node: PortableTextBlock
13
+ path: [KeyedSegment]
14
+ }): EditorSelectionPoint {
15
+ if (isPortableTextTextBlock(node)) {
16
+ return {
17
+ path: [...path, 'children', {_key: node.children[0]._key}],
18
+ offset: 0,
19
+ }
20
+ }
21
+
22
+ return {
23
+ path,
24
+ offset: 0,
25
+ }
26
+ }
@@ -0,0 +1,5 @@
1
+ import type {KeyedSegment, PathSegment} from '@sanity/types'
2
+
3
+ export function isKeyedSegment(segment: PathSegment): segment is KeyedSegment {
4
+ return typeof segment === 'object' && segment !== null && '_key' in segment
5
+ }
@@ -0,0 +1,21 @@
1
+ import type {EditorSelection} from '../../types/editor'
2
+
3
+ export function reverseSelection(selection: EditorSelection): EditorSelection {
4
+ if (!selection) {
5
+ return selection
6
+ }
7
+
8
+ if (selection.backward) {
9
+ return {
10
+ anchor: selection.focus,
11
+ focus: selection.anchor,
12
+ backward: false,
13
+ }
14
+ }
15
+
16
+ return {
17
+ anchor: selection.focus,
18
+ focus: selection.anchor,
19
+ backward: true,
20
+ }
21
+ }
@@ -0,0 +1,31 @@
1
+ import type {PortableTextBlock} from '@sanity/types'
2
+ import type {EditorSelectionPoint} from '../../types/editor'
3
+ import {getSelectionText} from './behavior.utils.get-selection-text'
4
+ import {getStartPoint} from './behavior.utils.get-start-point'
5
+ import {isKeyedSegment} from './behavior.utils.is-keyed-segment'
6
+
7
+ export function getBlockTextBefore({
8
+ value,
9
+ point,
10
+ }: {
11
+ value: Array<PortableTextBlock>
12
+ point: EditorSelectionPoint
13
+ }) {
14
+ const key = isKeyedSegment(point.path[0]) ? point.path[0]._key : undefined
15
+
16
+ const block = key ? value.find((block) => block._key === key) : undefined
17
+
18
+ if (!block) {
19
+ return ''
20
+ }
21
+
22
+ const startPoint = getStartPoint({node: block, path: [{_key: block._key}]})
23
+
24
+ return getSelectionText({
25
+ value,
26
+ selection: {
27
+ anchor: startPoint,
28
+ focus: point,
29
+ },
30
+ })
31
+ }