@portabletext/editor 1.12.2 → 1.12.3
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/lib/index.esm.js +112 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +116 -4
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +112 -1
- package/lib/index.mjs.map +1 -1
- package/package.json +6 -3
- package/src/editor/plugins/createWithHotKeys.ts +1 -1
- package/src/utils/is-hotkey.test.ts +61 -0
- package/src/utils/is-hotkey.ts +209 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.3",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"@xstate/react": "^5.0.0",
|
|
47
47
|
"debug": "^4.3.4",
|
|
48
48
|
"get-random-values-esm": "^1.0.2",
|
|
49
|
-
"is-hotkey-esm": "^1.0.0",
|
|
50
49
|
"lodash": "^4.17.21",
|
|
51
50
|
"lodash.startcase": "^4.4.0",
|
|
52
51
|
"react-compiler-runtime": "19.0.0-beta-df7b47d-20241124",
|
|
@@ -112,6 +111,10 @@
|
|
|
112
111
|
"dev": "pkg-utils watch",
|
|
113
112
|
"lint:fix": "biome lint --write .",
|
|
114
113
|
"test": "vitest --run",
|
|
115
|
-
"test:watch": "vitest"
|
|
114
|
+
"test:watch": "vitest",
|
|
115
|
+
"test:chromium": "vitest --run --project chromium",
|
|
116
|
+
"test:chromium:watch": "vitest --project chromium",
|
|
117
|
+
"test:unit": "vitest --run --project unit",
|
|
118
|
+
"test:unit:watch": "vitest --project unit"
|
|
116
119
|
}
|
|
117
120
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {isPortableTextSpan, isPortableTextTextBlock} from '@sanity/types'
|
|
2
|
-
import {isHotkey} from 'is-hotkey-esm'
|
|
3
2
|
import type {KeyboardEvent} from 'react'
|
|
4
3
|
import {Editor, Node, Path, Range, Transforms} from 'slate'
|
|
5
4
|
import type {ReactEditor} from 'slate-react'
|
|
@@ -7,6 +6,7 @@ import type {PortableTextSlateEditor} from '../../types/editor'
|
|
|
7
6
|
import type {HotkeyOptions} from '../../types/options'
|
|
8
7
|
import type {SlateTextBlock, VoidElement} from '../../types/slate'
|
|
9
8
|
import {debugWithName} from '../../utils/debug'
|
|
9
|
+
import {isHotkey} from '../../utils/is-hotkey'
|
|
10
10
|
import type {EditorActor} from '../editor-machine'
|
|
11
11
|
import type {PortableTextEditor} from '../PortableTextEditor'
|
|
12
12
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {expect, test} from 'vitest'
|
|
2
|
+
import {isHotkey, type KeyboardEventLike} from './is-hotkey'
|
|
3
|
+
|
|
4
|
+
function e(value: string | number, ...modifiers: string[]) {
|
|
5
|
+
return {
|
|
6
|
+
...(typeof value === 'string' ? {key: value} : {keyCode: value}),
|
|
7
|
+
altKey: modifiers.includes('alt'),
|
|
8
|
+
ctrlKey: modifiers.includes('ctrl'),
|
|
9
|
+
metaKey: modifiers.includes('meta'),
|
|
10
|
+
shiftKey: modifiers.includes('shift'),
|
|
11
|
+
} as KeyboardEventLike
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type TestCase = [KeyboardEventLike, string, boolean]
|
|
15
|
+
|
|
16
|
+
const testCases = [
|
|
17
|
+
[e(83, 'meta'), 'Meta+S', true],
|
|
18
|
+
[e(83, 'alt', 'meta'), 'Meta+Alt+s', true],
|
|
19
|
+
[e(83, 'meta'), 'meta+s', true],
|
|
20
|
+
[e(83, 'meta'), 'cmd+s', true],
|
|
21
|
+
[e(32, 'meta'), 'cmd+space', true],
|
|
22
|
+
[e(187, 'meta'), 'cmd+=', true],
|
|
23
|
+
[e(83, 'ctrl'), 'mod+s', true],
|
|
24
|
+
[e(16, 'shift'), 'shift', true],
|
|
25
|
+
[e(93, 'meta'), 'meta', true],
|
|
26
|
+
[e(65), 'a', true],
|
|
27
|
+
[e(83, 'alt', 'meta'), 'cmd+s', false],
|
|
28
|
+
[e('a', 'ctrl'), 'a', false],
|
|
29
|
+
[e(83, 'alt', 'meta'), 'cmd+alt?+s', true],
|
|
30
|
+
[e(83, 'meta'), 'cmd+alt?+s', true],
|
|
31
|
+
[e('?'), '?', true],
|
|
32
|
+
[e(13), 'enter', true],
|
|
33
|
+
[e(65, 'meta'), 'cmd+a', true],
|
|
34
|
+
[e(83, 'meta'), 'cmd+s', true],
|
|
35
|
+
[e('s', 'meta'), 'Meta+S', true],
|
|
36
|
+
[e('ß', 'alt', 'meta'), 'Meta+Alt+ß', true],
|
|
37
|
+
[e('s', 'meta'), 'meta+s', true],
|
|
38
|
+
[e('s', 'meta'), 'cmd+s', true],
|
|
39
|
+
[e(' ', 'meta'), 'cmd+space', true],
|
|
40
|
+
[e('+', 'meta'), 'cmd++', true],
|
|
41
|
+
[e('s', 'ctrl'), 'mod+s', true],
|
|
42
|
+
[e('Shift', 'shift'), 'shift', true],
|
|
43
|
+
[e('a'), 'a', true],
|
|
44
|
+
[e('s', 'alt', 'meta'), 'cmd+s', false],
|
|
45
|
+
[e('a', 'ctrl'), 'a', false],
|
|
46
|
+
[e('s', 'alt', 'meta'), 'cmd+alt?+s', true],
|
|
47
|
+
[e('s', 'meta'), 'cmd+alt?+s', true],
|
|
48
|
+
[e('Enter'), 'enter', true],
|
|
49
|
+
[e('a', 'meta'), 'meta+a', true],
|
|
50
|
+
[e('s', 'meta'), 'meta+s', true],
|
|
51
|
+
] satisfies Array<TestCase>
|
|
52
|
+
|
|
53
|
+
test(isHotkey.name, () => {
|
|
54
|
+
for (const testCase of testCases) {
|
|
55
|
+
expect(isHotkey(testCase[1], testCase[0])).toBe(testCase[2])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(() => isHotkey('ctrlalt+k', e('k', 'ctrl', 'alt'))).toThrowError(
|
|
59
|
+
'Unknown modifier: "ctrlalt"',
|
|
60
|
+
)
|
|
61
|
+
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
export interface KeyboardEventLike {
|
|
2
|
+
key: string
|
|
3
|
+
keyCode: number
|
|
4
|
+
altKey: boolean
|
|
5
|
+
ctrlKey: boolean
|
|
6
|
+
metaKey: boolean
|
|
7
|
+
shiftKey: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface HotKey {
|
|
11
|
+
keyCode?: number | undefined
|
|
12
|
+
key?: string | undefined
|
|
13
|
+
altKey: boolean | null
|
|
14
|
+
ctrlKey: boolean | null
|
|
15
|
+
metaKey: boolean | null
|
|
16
|
+
shiftKey: boolean | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const IS_MAC =
|
|
20
|
+
typeof window !== 'undefined' &&
|
|
21
|
+
/Mac|iPod|iPhone|iPad/.test(window.navigator.userAgent)
|
|
22
|
+
|
|
23
|
+
type Modifier = 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'
|
|
24
|
+
|
|
25
|
+
const modifiers: Record<string, Modifier | undefined> = {
|
|
26
|
+
alt: 'altKey',
|
|
27
|
+
control: 'ctrlKey',
|
|
28
|
+
meta: 'metaKey',
|
|
29
|
+
shift: 'shiftKey',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const aliases: Record<string, string | undefined> = {
|
|
33
|
+
add: '+',
|
|
34
|
+
break: 'pause',
|
|
35
|
+
cmd: 'meta',
|
|
36
|
+
command: 'meta',
|
|
37
|
+
ctl: 'control',
|
|
38
|
+
ctrl: 'control',
|
|
39
|
+
del: 'delete',
|
|
40
|
+
down: 'arrowdown',
|
|
41
|
+
esc: 'escape',
|
|
42
|
+
ins: 'insert',
|
|
43
|
+
left: 'arrowleft',
|
|
44
|
+
mod: IS_MAC ? 'meta' : 'control',
|
|
45
|
+
opt: 'alt',
|
|
46
|
+
option: 'alt',
|
|
47
|
+
return: 'enter',
|
|
48
|
+
right: 'arrowright',
|
|
49
|
+
space: ' ',
|
|
50
|
+
spacebar: ' ',
|
|
51
|
+
up: 'arrowup',
|
|
52
|
+
win: 'meta',
|
|
53
|
+
windows: 'meta',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const keyCodes: Record<string, number | undefined> = {
|
|
57
|
+
'backspace': 8,
|
|
58
|
+
'tab': 9,
|
|
59
|
+
'enter': 13,
|
|
60
|
+
'shift': 16,
|
|
61
|
+
'control': 17,
|
|
62
|
+
'alt': 18,
|
|
63
|
+
'pause': 19,
|
|
64
|
+
'capslock': 20,
|
|
65
|
+
'escape': 27,
|
|
66
|
+
' ': 32,
|
|
67
|
+
'pageup': 33,
|
|
68
|
+
'pagedown': 34,
|
|
69
|
+
'end': 35,
|
|
70
|
+
'home': 36,
|
|
71
|
+
'arrowleft': 37,
|
|
72
|
+
'arrowup': 38,
|
|
73
|
+
'arrowright': 39,
|
|
74
|
+
'arrowdown': 40,
|
|
75
|
+
'insert': 45,
|
|
76
|
+
'delete': 46,
|
|
77
|
+
'meta': 91,
|
|
78
|
+
'numlock': 144,
|
|
79
|
+
'scrolllock': 145,
|
|
80
|
+
';': 186,
|
|
81
|
+
'=': 187,
|
|
82
|
+
',': 188,
|
|
83
|
+
'-': 189,
|
|
84
|
+
'.': 190,
|
|
85
|
+
'/': 191,
|
|
86
|
+
'`': 192,
|
|
87
|
+
'[': 219,
|
|
88
|
+
'\\': 220,
|
|
89
|
+
']': 221,
|
|
90
|
+
"'": 222,
|
|
91
|
+
'f1': 112,
|
|
92
|
+
'f2': 113,
|
|
93
|
+
'f3': 114,
|
|
94
|
+
'f4': 115,
|
|
95
|
+
'f5': 116,
|
|
96
|
+
'f6': 117,
|
|
97
|
+
'f7': 118,
|
|
98
|
+
'f8': 119,
|
|
99
|
+
'f9': 120,
|
|
100
|
+
'f10': 121,
|
|
101
|
+
'f11': 122,
|
|
102
|
+
'f12': 123,
|
|
103
|
+
'f13': 124,
|
|
104
|
+
'f14': 125,
|
|
105
|
+
'f15': 126,
|
|
106
|
+
'f16': 127,
|
|
107
|
+
'f17': 128,
|
|
108
|
+
'f18': 129,
|
|
109
|
+
'f19': 130,
|
|
110
|
+
'f20': 131,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isHotkey(hotkey: string, event: KeyboardEventLike): boolean {
|
|
114
|
+
return compareHotkey(parseHotkey(hotkey), event)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseHotkey(hotkey: string): HotKey {
|
|
118
|
+
// Ensure that all the modifiers are set to false unless the hotkey has them.
|
|
119
|
+
const parsedHotkey: HotKey = {
|
|
120
|
+
altKey: false,
|
|
121
|
+
ctrlKey: false,
|
|
122
|
+
metaKey: false,
|
|
123
|
+
shiftKey: false,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Special case to handle the `+` key since we use it as a separator.
|
|
127
|
+
const hotkeySegments = hotkey.replace('++', '+add').split('+')
|
|
128
|
+
|
|
129
|
+
for (const rawHotkeySegment of hotkeySegments) {
|
|
130
|
+
const optional =
|
|
131
|
+
rawHotkeySegment.endsWith('?') && rawHotkeySegment.length > 1
|
|
132
|
+
const hotkeySegment = optional
|
|
133
|
+
? rawHotkeySegment.slice(0, -1)
|
|
134
|
+
: rawHotkeySegment
|
|
135
|
+
const keyName = toKeyName(hotkeySegment)
|
|
136
|
+
const modifier = modifiers[keyName]
|
|
137
|
+
const alias = aliases[hotkeySegment]
|
|
138
|
+
const code = keyCodes[keyName]
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
hotkeySegment.length > 1 &&
|
|
142
|
+
modifier === undefined &&
|
|
143
|
+
alias === undefined &&
|
|
144
|
+
code === undefined
|
|
145
|
+
) {
|
|
146
|
+
throw new TypeError(`Unknown modifier: "${hotkeySegment}"`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (hotkeySegments.length === 1 || modifier === undefined) {
|
|
150
|
+
parsedHotkey.key = keyName
|
|
151
|
+
parsedHotkey.keyCode = toKeyCode(hotkeySegment)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (modifier !== undefined) {
|
|
155
|
+
parsedHotkey[modifier] = optional ? null : true
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return parsedHotkey
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function compareHotkey(
|
|
163
|
+
parsedHotkey: HotKey,
|
|
164
|
+
event: KeyboardEventLike,
|
|
165
|
+
): boolean {
|
|
166
|
+
const matchingModifiers =
|
|
167
|
+
(parsedHotkey.altKey != null
|
|
168
|
+
? parsedHotkey.altKey === event.altKey
|
|
169
|
+
: true) &&
|
|
170
|
+
(parsedHotkey.ctrlKey != null
|
|
171
|
+
? parsedHotkey.ctrlKey === event.ctrlKey
|
|
172
|
+
: true) &&
|
|
173
|
+
(parsedHotkey.metaKey != null
|
|
174
|
+
? parsedHotkey.metaKey === event.metaKey
|
|
175
|
+
: true) &&
|
|
176
|
+
(parsedHotkey.shiftKey != null
|
|
177
|
+
? parsedHotkey.shiftKey === event.shiftKey
|
|
178
|
+
: true)
|
|
179
|
+
|
|
180
|
+
if (!matchingModifiers) {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (parsedHotkey.keyCode !== undefined && event.keyCode !== undefined) {
|
|
185
|
+
if (parsedHotkey.keyCode === 91 && event.keyCode === 93) {
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return parsedHotkey.keyCode === event.keyCode
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
parsedHotkey.keyCode === event.keyCode ||
|
|
194
|
+
parsedHotkey.key === event.key.toLowerCase()
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function toKeyCode(name: string): number {
|
|
199
|
+
const keyName = toKeyName(name)
|
|
200
|
+
const keyCode = keyCodes[keyName] ?? keyName.toUpperCase().charCodeAt(0)
|
|
201
|
+
|
|
202
|
+
return keyCode
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function toKeyName(name: string): string {
|
|
206
|
+
const keyName = name.toLowerCase()
|
|
207
|
+
|
|
208
|
+
return aliases[keyName] ?? keyName
|
|
209
|
+
}
|