@trendr/core 0.2.10 → 0.4.0
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/README.md +284 -24
- package/index.js +6 -2
- package/package.json +29 -34
- package/src/animation.js +25 -12
- package/src/ansi.js +7 -0
- package/src/buffer.js +63 -50
- package/src/button.js +2 -2
- package/src/checkbox.js +2 -2
- package/src/diff-engine.js +313 -0
- package/src/diff-view.js +238 -0
- package/src/diff.js +2 -2
- package/src/dropdown.js +147 -0
- package/src/focus.js +89 -27
- package/src/hooks.js +43 -25
- package/src/hotkey.js +51 -16
- package/src/input.js +211 -72
- package/src/layout.js +34 -13
- package/src/list.js +29 -16
- package/src/markdown.js +177 -0
- package/src/menu.js +13 -6
- package/src/menubar.js +46 -115
- package/src/miller-nav.js +36 -11
- package/src/modal.js +3 -1
- package/src/pick-list.js +16 -17
- package/src/progress.js +3 -3
- package/src/radio.js +16 -5
- package/src/renderer.js +315 -58
- package/src/scheduler.js +28 -10
- package/src/scroll-box.js +12 -11
- package/src/scrollable-text.js +3 -2
- package/src/select.js +65 -173
- package/src/selection.js +92 -0
- package/src/shimmer.js +2 -2
- package/src/signal.js +20 -3
- package/src/table.js +8 -27
- package/src/tabs.js +10 -7
- package/src/task.js +3 -3
- package/src/text-area.js +96 -41
- package/src/text-input.js +70 -28
- package/src/wrap.js +205 -84
package/src/hotkey.js
CHANGED
|
@@ -1,34 +1,69 @@
|
|
|
1
1
|
import { useInput } from './hooks.js'
|
|
2
2
|
import { registerHook } from './renderer.js'
|
|
3
3
|
|
|
4
|
+
const KEY_ALIASES = { enter: 'return', esc: 'escape' }
|
|
5
|
+
const MOD_NAMES = new Set(['ctrl', 'alt', 'meta', 'shift'])
|
|
6
|
+
|
|
4
7
|
function parseDescriptor(desc) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
8
|
+
const mods = { ctrl: false, meta: false, shift: false }
|
|
9
|
+
let key = desc
|
|
10
|
+
|
|
11
|
+
while (true) {
|
|
12
|
+
const idx = key.indexOf('+')
|
|
13
|
+
if (idx <= 0 || idx === key.length - 1) break
|
|
14
|
+
const mod = key.slice(0, idx).toLowerCase()
|
|
15
|
+
if (!MOD_NAMES.has(mod)) break
|
|
16
|
+
if (mod === 'ctrl') mods.ctrl = true
|
|
17
|
+
else if (mod === 'shift') mods.shift = true
|
|
18
|
+
else mods.meta = true
|
|
19
|
+
key = key.slice(idx + 1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (key.length > 1) {
|
|
23
|
+
key = key.toLowerCase()
|
|
24
|
+
if (KEY_ALIASES[key]) key = KEY_ALIASES[key]
|
|
11
25
|
}
|
|
26
|
+
|
|
27
|
+
// an uppercase letter descriptor implies shift
|
|
28
|
+
if (key.length === 1 && key !== key.toLowerCase()) {
|
|
29
|
+
mods.shift = true
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
return { key, ...mods }
|
|
13
33
|
}
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
function matches(parsed, event) {
|
|
36
|
+
const key = event.key
|
|
37
|
+
if (typeof key !== 'string') return false
|
|
38
|
+
|
|
39
|
+
// uppercase single-char keys imply shift even when the terminal
|
|
40
|
+
// doesn't report the modifier
|
|
41
|
+
const eventShift = !!event.shift || (key.length === 1 && key !== key.toLowerCase())
|
|
42
|
+
|
|
43
|
+
const keyMatch = parsed.key.length === 1 && key.length === 1
|
|
44
|
+
? parsed.key.toLowerCase() === key.toLowerCase()
|
|
45
|
+
: parsed.key === key || (parsed.shift && parsed.key === 'tab' && key === 'shift-tab')
|
|
46
|
+
|
|
47
|
+
if (!keyMatch) return false
|
|
48
|
+
if (parsed.ctrl !== !!event.ctrl) return false
|
|
49
|
+
if (parsed.meta !== !!event.meta) return false
|
|
50
|
+
if (parsed.shift !== eventShift && !(parsed.shift && key === 'shift-tab')) return false
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
16
53
|
|
|
17
54
|
export function useHotkey(descriptor, handler, { when } = {}) {
|
|
18
|
-
const
|
|
19
|
-
|
|
55
|
+
const ref = registerHook(() => ({ descriptor: undefined, parsed: null, handler, when }))
|
|
56
|
+
if (ref.descriptor !== descriptor) {
|
|
57
|
+
ref.descriptor = descriptor
|
|
58
|
+
ref.parsed = parseDescriptor(descriptor)
|
|
59
|
+
}
|
|
20
60
|
ref.handler = handler
|
|
21
61
|
ref.when = when
|
|
22
62
|
|
|
23
63
|
useInput((event) => {
|
|
24
64
|
if (ref.when && !ref.when()) return
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const keyMatch = event.key === rawKey || event.key === parsed.key
|
|
28
|
-
if (!keyMatch) return
|
|
29
|
-
if (parsed.ctrl !== event.ctrl) return
|
|
30
|
-
if (parsed.meta !== event.meta) return
|
|
31
|
-
|
|
65
|
+
if (event.key === 'paste') return
|
|
66
|
+
if (!matches(ref.parsed, event)) return
|
|
32
67
|
ref.handler()
|
|
33
68
|
event.stopPropagation()
|
|
34
69
|
})
|
package/src/input.js
CHANGED
|
@@ -5,8 +5,10 @@ const SPECIAL_KEYS = {
|
|
|
5
5
|
'\x1b[D': 'left',
|
|
6
6
|
'\x1b[H': 'home',
|
|
7
7
|
'\x1b[F': 'end',
|
|
8
|
+
'\x1b[1~': 'home',
|
|
8
9
|
'\x1b[2~': 'insert',
|
|
9
10
|
'\x1b[3~': 'delete',
|
|
11
|
+
'\x1b[4~': 'end',
|
|
10
12
|
'\x1b[5~': 'pageup',
|
|
11
13
|
'\x1b[6~': 'pagedown',
|
|
12
14
|
'\x1bOP': 'f1',
|
|
@@ -40,20 +42,34 @@ export function parseMouse(raw) {
|
|
|
40
42
|
const y = parseInt(m[3], 10) - 1
|
|
41
43
|
const release = m[4] === 'm'
|
|
42
44
|
const button = cb & 3
|
|
43
|
-
const
|
|
45
|
+
const extended = (cb & 128) !== 0
|
|
46
|
+
const scroll = !extended && (cb & 64) !== 0
|
|
44
47
|
const motion = (cb & 32) !== 0
|
|
45
48
|
|
|
46
49
|
if (scroll) {
|
|
47
|
-
|
|
50
|
+
// wheel codes: 0 up, 1 down, 2 left, 3 right (trackpads emit left/right
|
|
51
|
+
// during a vertical scroll - don't fold those into 'down')
|
|
52
|
+
const direction = button === 0 ? 'up' : button === 1 ? 'down' : button === 2 ? 'left' : 'right'
|
|
53
|
+
return { type: 'mouse', action: 'scroll', direction, x, y }
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
const buttonName =
|
|
56
|
+
const buttonName = extended
|
|
57
|
+
? (button === 0 ? 'back' : button === 1 ? 'forward' : button === 2 ? 'button10' : 'button11')
|
|
58
|
+
: (button === 0 ? 'left' : button === 1 ? 'middle' : 'right')
|
|
51
59
|
const action = release ? 'release' : motion ? 'drag' : 'press'
|
|
52
60
|
return { type: 'mouse', action, button: buttonName, x, y }
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
const MODIFY_OTHER_RE = /^\x1b\[27;(\d+);(\d+)~$/
|
|
56
64
|
const CSI_U_RE = /^\x1b\[(\d+)(?:;(\d+))?u$/
|
|
65
|
+
const CSI_MOD_LETTER_RE = /^\x1b\[1;(\d+)([ABCDHF])$/
|
|
66
|
+
const CSI_MOD_TILDE_RE = /^\x1b\[(\d+);(\d+)~$/
|
|
67
|
+
|
|
68
|
+
const CSI_LETTER_KEYS = { A: 'up', B: 'down', C: 'right', D: 'left', H: 'home', F: 'end' }
|
|
69
|
+
const CSI_TILDE_KEYS = {
|
|
70
|
+
1: 'home', 2: 'insert', 3: 'delete', 4: 'end', 5: 'pageup', 6: 'pagedown',
|
|
71
|
+
15: 'f5', 17: 'f6', 18: 'f7', 19: 'f8', 20: 'f9', 21: 'f10', 23: 'f11', 24: 'f12',
|
|
72
|
+
}
|
|
57
73
|
|
|
58
74
|
function codeToKey(code) {
|
|
59
75
|
if (code === 13 || code === 10) return 'return'
|
|
@@ -66,10 +82,10 @@ function codeToKey(code) {
|
|
|
66
82
|
|
|
67
83
|
// modifier param is 1-based with a bitmask in the low bits: shift=1, alt=2,
|
|
68
84
|
// ctrl=4 (so plain=1, shift=2, ctrl=5, etc)
|
|
69
|
-
function
|
|
85
|
+
function withMods(key, mod, raw) {
|
|
70
86
|
const bits = Math.max(0, mod - 1)
|
|
71
87
|
return {
|
|
72
|
-
key
|
|
88
|
+
key,
|
|
73
89
|
ctrl: (bits & 4) !== 0,
|
|
74
90
|
meta: (bits & 2) !== 0,
|
|
75
91
|
shift: (bits & 1) !== 0,
|
|
@@ -77,6 +93,11 @@ function modifiedKey(code, mod, raw) {
|
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
|
|
96
|
+
function isSingleCodePoint(s) {
|
|
97
|
+
if (s.length === 1) return true
|
|
98
|
+
return s.length === 2 && s.codePointAt(0) > 0xffff
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
export function parseKey(data) {
|
|
81
102
|
const raw = typeof data === 'string' ? data : data.toString()
|
|
82
103
|
|
|
@@ -84,11 +105,26 @@ export function parseKey(data) {
|
|
|
84
105
|
return { key: SPECIAL_KEYS[raw], ctrl: false, meta: false, shift: false, raw }
|
|
85
106
|
}
|
|
86
107
|
|
|
108
|
+
if (raw === '\x1b\x1b') {
|
|
109
|
+
return { key: 'escape', ctrl: false, meta: false, shift: false, raw }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (raw.length > 2 && raw.startsWith('\x1b\x1b')) {
|
|
113
|
+
const inner = parseKey(raw.slice(1))
|
|
114
|
+
return { key: inner.key, ctrl: inner.ctrl, meta: true, shift: inner.shift, raw }
|
|
115
|
+
}
|
|
116
|
+
|
|
87
117
|
let m = MODIFY_OTHER_RE.exec(raw)
|
|
88
|
-
if (m) return
|
|
118
|
+
if (m) return withMods(codeToKey(parseInt(m[2], 10)), parseInt(m[1], 10), raw)
|
|
89
119
|
|
|
90
120
|
m = CSI_U_RE.exec(raw)
|
|
91
|
-
if (m) return
|
|
121
|
+
if (m) return withMods(codeToKey(parseInt(m[1], 10)), m[2] ? parseInt(m[2], 10) : 1, raw)
|
|
122
|
+
|
|
123
|
+
m = CSI_MOD_LETTER_RE.exec(raw)
|
|
124
|
+
if (m) return withMods(CSI_LETTER_KEYS[m[2]], parseInt(m[1], 10), raw)
|
|
125
|
+
|
|
126
|
+
m = CSI_MOD_TILDE_RE.exec(raw)
|
|
127
|
+
if (m && CSI_TILDE_KEYS[m[1]]) return withMods(CSI_TILDE_KEYS[m[1]], parseInt(m[2], 10), raw)
|
|
92
128
|
|
|
93
129
|
if (raw.length === 1) {
|
|
94
130
|
const code = raw.charCodeAt(0)
|
|
@@ -106,94 +142,193 @@ export function parseKey(data) {
|
|
|
106
142
|
return { key: raw, ctrl: false, meta: false, shift: false, raw }
|
|
107
143
|
}
|
|
108
144
|
|
|
109
|
-
if (
|
|
110
|
-
return { key: raw
|
|
145
|
+
if (isSingleCodePoint(raw)) {
|
|
146
|
+
return { key: raw, ctrl: false, meta: false, shift: false, raw }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (raw[0] === '\x1b' && isSingleCodePoint(raw.slice(1))) {
|
|
150
|
+
const inner = parseKey(raw.slice(1))
|
|
151
|
+
return { key: inner.key, ctrl: inner.ctrl, meta: true, shift: inner.shift, raw }
|
|
111
152
|
}
|
|
112
153
|
|
|
113
154
|
return { key: raw, ctrl: false, meta: false, shift: false, raw }
|
|
114
155
|
}
|
|
115
156
|
|
|
157
|
+
// reads one token off the head of s. returns null when the head is an
|
|
158
|
+
// incomplete escape sequence (or a split surrogate pair) that needs more bytes
|
|
159
|
+
function nextToken(s) {
|
|
160
|
+
if (s[0] !== '\x1b') {
|
|
161
|
+
const c0 = s.charCodeAt(0)
|
|
162
|
+
if (c0 >= 0xd800 && c0 <= 0xdbff && s.length === 1) return null
|
|
163
|
+
const cp = s.codePointAt(0)
|
|
164
|
+
return s.slice(0, cp > 0xffff ? 2 : 1)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (s.length === 1) return null
|
|
168
|
+
|
|
169
|
+
let i = 1
|
|
170
|
+
if (s[1] === '\x1b') {
|
|
171
|
+
// could be double-esc (two escape events) or an alt-prefixed sequence
|
|
172
|
+
// like \x1b\x1b[A - wait for a third byte to disambiguate
|
|
173
|
+
if (s.length === 2) return null
|
|
174
|
+
if (s[2] === '[' || s[2] === 'O') i = 2
|
|
175
|
+
else return '\x1b'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const c = s[i]
|
|
179
|
+
|
|
180
|
+
if (c === '[') {
|
|
181
|
+
let j = i + 1
|
|
182
|
+
if (j < s.length && (s[j] === '<' || s[j] === '?')) j++
|
|
183
|
+
while (j < s.length && ((s[j] >= '0' && s[j] <= '9') || s[j] === ';' || s[j] === ':')) j++
|
|
184
|
+
if (j >= s.length) return null
|
|
185
|
+
return s.slice(0, j + 1)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (c === 'O') {
|
|
189
|
+
if (i + 1 >= s.length) return null
|
|
190
|
+
return s.slice(0, i + 2)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const cc = s.charCodeAt(i)
|
|
194
|
+
if (cc >= 0xd800 && cc <= 0xdbff && i + 1 >= s.length) return null
|
|
195
|
+
const cp = s.codePointAt(i)
|
|
196
|
+
return s.slice(0, i + (cp > 0xffff ? 2 : 1))
|
|
197
|
+
}
|
|
198
|
+
|
|
116
199
|
export function splitKeys(data) {
|
|
117
200
|
const raw = typeof data === 'string' ? data : data.toString()
|
|
118
201
|
const keys = []
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
while (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
} else if (i + 1 < raw.length) {
|
|
138
|
-
keys.push(raw.slice(i, i + 2))
|
|
139
|
-
i += 2
|
|
140
|
-
} else {
|
|
141
|
-
keys.push(raw[i])
|
|
142
|
-
i++
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
keys.push(raw[i])
|
|
146
|
-
i++
|
|
202
|
+
let s = raw
|
|
203
|
+
|
|
204
|
+
while (s.length > 0) {
|
|
205
|
+
const token = nextToken(s)
|
|
206
|
+
if (token !== null) {
|
|
207
|
+
keys.push(token)
|
|
208
|
+
s = s.slice(token.length)
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
// incomplete tail - split leading escapes apart, keep the rest whole
|
|
212
|
+
if (s === '\x1b') {
|
|
213
|
+
keys.push(s)
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
if (s.startsWith('\x1b\x1b')) {
|
|
217
|
+
keys.push('\x1b')
|
|
218
|
+
s = s.slice(1)
|
|
219
|
+
continue
|
|
147
220
|
}
|
|
221
|
+
keys.push(s)
|
|
222
|
+
break
|
|
148
223
|
}
|
|
149
224
|
|
|
150
225
|
return keys
|
|
151
226
|
}
|
|
152
227
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
228
|
+
const PASTE_START = '\x1b[200~'
|
|
229
|
+
const PASTE_END = '\x1b[201~'
|
|
230
|
+
|
|
231
|
+
// longest suffix of s that is a proper prefix of marker
|
|
232
|
+
function partialSuffixLen(s, marker) {
|
|
233
|
+
const max = Math.min(marker.length - 1, s.length)
|
|
234
|
+
for (let len = max; len > 0; len--) {
|
|
235
|
+
if (s.endsWith(marker.slice(0, len))) return len
|
|
236
|
+
}
|
|
237
|
+
return 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function createInputHandler(stream, options = {}) {
|
|
241
|
+
const {
|
|
242
|
+
escDelay = 25,
|
|
243
|
+
setTimer = (fn, ms) => setTimeout(fn, ms),
|
|
244
|
+
clearTimer = (id) => clearTimeout(id),
|
|
245
|
+
isEligible = () => true,
|
|
246
|
+
} = options
|
|
247
|
+
|
|
248
|
+
const keyListeners = new Map()
|
|
249
|
+
const mouseListeners = new Map()
|
|
250
|
+
|
|
251
|
+
let pending = ''
|
|
252
|
+
let inPaste = false
|
|
253
|
+
let pasteData = ''
|
|
254
|
+
let escTimer = null
|
|
255
|
+
|
|
256
|
+
function fire(event, listeners) {
|
|
257
|
+
event.stopPropagation = () => { event._stopped = true }
|
|
258
|
+
const snapshot = [...listeners].reverse()
|
|
259
|
+
for (const [fn, owner] of snapshot) {
|
|
260
|
+
if (!isEligible(owner)) continue
|
|
261
|
+
fn(event)
|
|
262
|
+
if (event._stopped) break
|
|
263
|
+
}
|
|
264
|
+
}
|
|
156
265
|
|
|
157
266
|
function dispatch(keyStr) {
|
|
158
267
|
const mouse = parseMouse(keyStr)
|
|
159
268
|
if (mouse) {
|
|
160
|
-
mouse
|
|
161
|
-
const snapshot = [...mouseListeners].reverse()
|
|
162
|
-
for (const fn of snapshot) {
|
|
163
|
-
fn(mouse)
|
|
164
|
-
if (mouse._stopped) break
|
|
165
|
-
}
|
|
269
|
+
fire(mouse, mouseListeners)
|
|
166
270
|
return
|
|
167
271
|
}
|
|
272
|
+
fire(parseKey(keyStr), keyListeners)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function dispatchPaste(text) {
|
|
276
|
+
const normalized = text.replace(/\r\n?/g, '\n')
|
|
277
|
+
fire({ key: 'paste', text: normalized, ctrl: false, meta: false, shift: false, raw: normalized }, keyListeners)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function processPending() {
|
|
281
|
+
while (pending.length > 0) {
|
|
282
|
+
if (inPaste) {
|
|
283
|
+
const idx = pending.indexOf(PASTE_END)
|
|
284
|
+
if (idx === -1) {
|
|
285
|
+
const keep = partialSuffixLen(pending, PASTE_END)
|
|
286
|
+
pasteData += pending.slice(0, pending.length - keep)
|
|
287
|
+
pending = pending.slice(pending.length - keep)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
pasteData += pending.slice(0, idx)
|
|
291
|
+
pending = pending.slice(idx + PASTE_END.length)
|
|
292
|
+
inPaste = false
|
|
293
|
+
const text = pasteData
|
|
294
|
+
pasteData = ''
|
|
295
|
+
dispatchPaste(text)
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const token = nextToken(pending)
|
|
300
|
+
if (token === null) return
|
|
301
|
+
pending = pending.slice(token.length)
|
|
302
|
+
if (token === PASTE_START) {
|
|
303
|
+
inPaste = true
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
if (token === PASTE_END) continue
|
|
307
|
+
dispatch(token)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
168
310
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
fn(event)
|
|
174
|
-
if (event._stopped) break
|
|
311
|
+
function clearEscTimer() {
|
|
312
|
+
if (escTimer !== null) {
|
|
313
|
+
clearTimer(escTimer)
|
|
314
|
+
escTimer = null
|
|
175
315
|
}
|
|
176
316
|
}
|
|
177
317
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
318
|
+
function flushPending() {
|
|
319
|
+
escTimer = null
|
|
320
|
+
if (pending.length === 0 || inPaste) return
|
|
321
|
+
const flush = pending
|
|
322
|
+
pending = ''
|
|
323
|
+
for (const key of splitKeys(flush)) dispatch(key)
|
|
181
324
|
}
|
|
182
325
|
|
|
183
326
|
function onData(data) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for (const fn of snapshot) {
|
|
190
|
-
fn(event)
|
|
191
|
-
if (event._stopped) break
|
|
192
|
-
}
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
for (const key of splitKeys(data)) {
|
|
196
|
-
dispatch(key)
|
|
327
|
+
clearEscTimer()
|
|
328
|
+
pending += typeof data === 'string' ? data : data.toString('utf8')
|
|
329
|
+
processPending()
|
|
330
|
+
if (pending.length > 0 && !inPaste) {
|
|
331
|
+
escTimer = setTimer(flushPending, escDelay)
|
|
197
332
|
}
|
|
198
333
|
}
|
|
199
334
|
|
|
@@ -209,10 +344,14 @@ export function createInputHandler(stream) {
|
|
|
209
344
|
if (!attached) return
|
|
210
345
|
attached = false
|
|
211
346
|
stream.off('data', onData)
|
|
347
|
+
clearEscTimer()
|
|
348
|
+
pending = ''
|
|
349
|
+
inPaste = false
|
|
350
|
+
pasteData = ''
|
|
212
351
|
}
|
|
213
352
|
|
|
214
|
-
function onKey(fn) {
|
|
215
|
-
keyListeners.
|
|
353
|
+
function onKey(fn, owner = null) {
|
|
354
|
+
keyListeners.set(fn, owner)
|
|
216
355
|
if (keyListeners.size + mouseListeners.size === 1) attach()
|
|
217
356
|
return () => {
|
|
218
357
|
keyListeners.delete(fn)
|
|
@@ -220,8 +359,8 @@ export function createInputHandler(stream) {
|
|
|
220
359
|
}
|
|
221
360
|
}
|
|
222
361
|
|
|
223
|
-
function onMouse(fn) {
|
|
224
|
-
mouseListeners.
|
|
362
|
+
function onMouse(fn, owner = null) {
|
|
363
|
+
mouseListeners.set(fn, owner)
|
|
225
364
|
if (keyListeners.size + mouseListeners.size === 1) attach()
|
|
226
365
|
return () => {
|
|
227
366
|
mouseListeners.delete(fn)
|
package/src/layout.js
CHANGED
|
@@ -111,8 +111,14 @@ export function computeLayout(node, rect) {
|
|
|
111
111
|
node._childHeights = heights
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// absolute children anchor to the padding box (inside borders, ignoring
|
|
115
|
+
// padding), matching CSS containing-block behavior
|
|
116
|
+
const padBoxX = box.x + be.left
|
|
117
|
+
const padBoxY = box.y + be.top
|
|
118
|
+
const padBoxW = Math.max(0, box.width - be.left - be.right)
|
|
119
|
+
const padBoxH = Math.max(0, box.height - be.top - be.bottom)
|
|
114
120
|
for (const child of absChildren) {
|
|
115
|
-
layoutAbsolute(child,
|
|
121
|
+
layoutAbsolute(child, padBoxX, padBoxY, padBoxW, padBoxH)
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
|
|
@@ -166,7 +172,9 @@ function layoutFlex(children, ctx) {
|
|
|
166
172
|
totalFlex += grow
|
|
167
173
|
childInfo.push({ child, cs, grow, minMain, marginMain, marginCross, margin, measured: null })
|
|
168
174
|
} else {
|
|
169
|
-
const
|
|
175
|
+
const measureW = isRow ? width : Math.max(0, width - marginCross)
|
|
176
|
+
const measureH = isRow ? Math.max(0, height - marginCross) : height
|
|
177
|
+
const measured = measureChild(child, cs, isRow, measureW, measureH)
|
|
170
178
|
const childMain = isRow ? measured.width : measured.height
|
|
171
179
|
usedMain += childMain + marginMain
|
|
172
180
|
childInfo.push({ child, cs, grow: 0, minMain: childMain, marginMain, marginCross, margin, measured })
|
|
@@ -197,19 +205,27 @@ function layoutFlex(children, ctx) {
|
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
let pos = mainOffset
|
|
208
|
+
let cumFlex = 0
|
|
209
|
+
let flexAllocated = 0
|
|
200
210
|
|
|
201
211
|
for (const info of childInfo) {
|
|
202
212
|
const { child, cs, grow, minMain, marginMain, marginCross, margin, measured } = info
|
|
203
213
|
|
|
204
214
|
let childMain
|
|
205
215
|
if (grow > 0) {
|
|
206
|
-
|
|
207
|
-
|
|
216
|
+
cumFlex += grow
|
|
217
|
+
const target = Math.floor(remaining * (cumFlex / totalFlex))
|
|
218
|
+
childMain = minMain + (target - flexAllocated)
|
|
219
|
+
flexAllocated = target
|
|
208
220
|
} else {
|
|
209
221
|
childMain = minMain
|
|
210
222
|
}
|
|
211
223
|
|
|
212
|
-
|
|
224
|
+
// pos accumulates fractional space-between/around spacing, so rects
|
|
225
|
+
// always get integer coordinates
|
|
226
|
+
const mainPos = Math.round(pos)
|
|
227
|
+
|
|
228
|
+
const mainRemaining = mainSize - mainPos - marginMain
|
|
213
229
|
if (childMain > mainRemaining) childMain = Math.max(0, mainRemaining)
|
|
214
230
|
|
|
215
231
|
const explicitCross = isRow
|
|
@@ -237,8 +253,8 @@ function layoutFlex(children, ctx) {
|
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
const childRect = isRow
|
|
240
|
-
? { x: x +
|
|
241
|
-
: { x: x + crossOffset, y: y +
|
|
256
|
+
? { x: x + mainPos + marginBefore, y: y + crossOffset, width: childMain, height: childCross }
|
|
257
|
+
: { x: x + crossOffset, y: y + mainPos + marginBefore, width: childCross, height: childMain }
|
|
242
258
|
|
|
243
259
|
computeLayout(child, childRect)
|
|
244
260
|
|
|
@@ -277,7 +293,7 @@ function measureChild(child, cs, isRow, availW, availH) {
|
|
|
277
293
|
w = explicitW
|
|
278
294
|
h = explicitH
|
|
279
295
|
} else {
|
|
280
|
-
const intrinsic = measureIntrinsic(leaf, availW, availH)
|
|
296
|
+
const intrinsic = measureIntrinsic(leaf, explicitW ?? availW, explicitH ?? availH)
|
|
281
297
|
w = explicitW ?? intrinsic.width
|
|
282
298
|
h = explicitH ?? intrinsic.height
|
|
283
299
|
}
|
|
@@ -330,19 +346,24 @@ function measureIntrinsic(node, availW, availH) {
|
|
|
330
346
|
totalFlex += grow
|
|
331
347
|
infos.push({ child, cs, grow, marginMain, marginCross, measured: null })
|
|
332
348
|
} else {
|
|
333
|
-
const measured = measureChild(child, cs, true, innerW, innerH)
|
|
349
|
+
const measured = measureChild(child, cs, true, innerW, Math.max(0, innerH - marginCross))
|
|
334
350
|
usedByFixed += measured.width + marginMain
|
|
335
351
|
infos.push({ child, cs, grow: 0, marginMain, marginCross, measured })
|
|
336
352
|
}
|
|
337
353
|
}
|
|
338
354
|
|
|
339
355
|
const flexSpace = Math.max(0, innerW - usedByFixed)
|
|
356
|
+
let cumFlex = 0
|
|
357
|
+
let flexAllocated = 0
|
|
340
358
|
|
|
341
359
|
for (const info of infos) {
|
|
342
360
|
let measured = info.measured
|
|
343
361
|
if (info.grow > 0) {
|
|
344
|
-
|
|
345
|
-
|
|
362
|
+
cumFlex += info.grow
|
|
363
|
+
const target = Math.floor(flexSpace * (cumFlex / totalFlex))
|
|
364
|
+
const childW = target - flexAllocated
|
|
365
|
+
flexAllocated = target
|
|
366
|
+
measured = measureChild(info.child, info.cs, true, childW, Math.max(0, innerH - info.marginCross))
|
|
346
367
|
}
|
|
347
368
|
mainTotal += (measured.width + info.marginMain)
|
|
348
369
|
const childCross = measured.height + info.marginCross
|
|
@@ -353,10 +374,10 @@ function measureIntrinsic(node, availW, availH) {
|
|
|
353
374
|
} else {
|
|
354
375
|
for (const child of children) {
|
|
355
376
|
const cs = childStyle(child)
|
|
356
|
-
const measured = measureChild(child, cs, false, innerW, innerH)
|
|
357
377
|
const margin = resolveMargin(cs)
|
|
358
378
|
const marginMain = margin.top + margin.bottom
|
|
359
379
|
const marginCross = margin.left + margin.right
|
|
380
|
+
const measured = measureChild(child, cs, false, Math.max(0, innerW - marginCross), innerH)
|
|
360
381
|
|
|
361
382
|
mainTotal += measured.height + marginMain
|
|
362
383
|
const childCross = measured.width + marginCross
|
|
@@ -392,8 +413,8 @@ function resolveSize(value, available) {
|
|
|
392
413
|
}
|
|
393
414
|
|
|
394
415
|
function clampSize(value, min, max) {
|
|
395
|
-
if (min != null && value < min) value = min
|
|
396
416
|
if (max != null && value > max) value = max
|
|
417
|
+
if (min != null && value < min) value = min
|
|
397
418
|
return Math.max(0, Math.floor(value))
|
|
398
419
|
}
|
|
399
420
|
|