@bsky.app/tapper 0.1.0 → 0.2.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 +1 -30
- package/build/index.d.ts +36 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +197 -8
- package/build/index.js.map +1 -1
- package/build/types.d.ts +8 -3
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/util.d.ts +10 -1
- package/build/util.d.ts.map +1 -1
- package/build/util.js +33 -10
- package/build/util.js.map +1 -1
- package/package.json +3 -8
- package/src/index.ts +251 -10
- package/src/types.ts +9 -3
- package/src/util.ts +44 -16
- package/build/core.d.ts +0 -30
- package/build/core.d.ts.map +0 -1
- package/build/core.js +0 -148
- package/build/core.js.map +0 -1
- package/build/index.web.d.ts +0 -17
- package/build/index.web.d.ts.map +0 -1
- package/build/index.web.js +0 -33
- package/build/index.web.js.map +0 -1
- package/src/core.ts +0 -185
- package/src/index.web.ts +0 -46
package/src/index.ts
CHANGED
|
@@ -1,20 +1,261 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {useRef, useState, useSyncExternalStore} from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
TapperConfig,
|
|
5
|
+
TapperEvents,
|
|
6
|
+
TapperNode,
|
|
7
|
+
TapperActiveFacet,
|
|
8
|
+
TapperSnapshot,
|
|
9
|
+
} from './types'
|
|
10
|
+
import {
|
|
11
|
+
parseNodesFromText,
|
|
12
|
+
detectActiveFacet,
|
|
13
|
+
deriveTriggers,
|
|
14
|
+
nodeToFacet,
|
|
15
|
+
compileFacetRegexes,
|
|
16
|
+
type CompiledFacetRegexes,
|
|
17
|
+
} from './util'
|
|
3
18
|
|
|
4
19
|
export * from './types'
|
|
5
|
-
|
|
20
|
+
|
|
21
|
+
export class Tapper {
|
|
22
|
+
private facetRegexes: CompiledFacetRegexes
|
|
23
|
+
private triggers: Map<string, string>
|
|
24
|
+
|
|
25
|
+
// Event emitters
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
private listeners = new Map<keyof TapperEvents, Set<(data: any) => void>>()
|
|
28
|
+
|
|
29
|
+
// Public state
|
|
30
|
+
text = ''
|
|
31
|
+
cursor = 0
|
|
32
|
+
nodes: TapperNode[] = []
|
|
33
|
+
activeFacet: TapperActiveFacet | null = null
|
|
34
|
+
|
|
35
|
+
// Internal tracking
|
|
36
|
+
private updating = false
|
|
37
|
+
private pendingCursor: number | null = null
|
|
38
|
+
|
|
39
|
+
// useSyncExternalStore plumbing
|
|
40
|
+
private storeListeners = new Set<() => void>()
|
|
41
|
+
private snapshot: TapperSnapshot
|
|
42
|
+
|
|
43
|
+
constructor(config: TapperConfig) {
|
|
44
|
+
this.facetRegexes = compileFacetRegexes(config.facets)
|
|
45
|
+
this.triggers = deriveTriggers(config.facets)
|
|
46
|
+
this.snapshot = {
|
|
47
|
+
text: this.text,
|
|
48
|
+
cursor: this.cursor,
|
|
49
|
+
nodes: this.nodes,
|
|
50
|
+
activeFacet: this.activeFacet,
|
|
51
|
+
}
|
|
52
|
+
if (config.initialText) {
|
|
53
|
+
this.replaceText(config.initialText)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
subscribe = (listener: () => void) => {
|
|
58
|
+
this.storeListeners.add(listener)
|
|
59
|
+
return () => this.storeListeners.delete(listener)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getSnapshot = (): TapperSnapshot => {
|
|
63
|
+
return this.snapshot
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
on = <K extends keyof TapperEvents>(
|
|
67
|
+
event: K,
|
|
68
|
+
cb: (data: TapperEvents[K]) => void,
|
|
69
|
+
) => {
|
|
70
|
+
if (!this.listeners.has(event)) this.listeners.set(event, new Set())
|
|
71
|
+
this.listeners.get(event)!.add(cb)
|
|
72
|
+
return () => {
|
|
73
|
+
this.listeners.get(event)?.delete(cb)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private emit<K extends keyof TapperEvents>(event: K, data: TapperEvents[K]) {
|
|
78
|
+
this.listeners.get(event)?.forEach(cb => cb(data))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private notify() {
|
|
82
|
+
this.snapshot = {
|
|
83
|
+
text: this.text,
|
|
84
|
+
cursor: this.cursor,
|
|
85
|
+
nodes: this.nodes,
|
|
86
|
+
activeFacet: this.activeFacet,
|
|
87
|
+
}
|
|
88
|
+
this.storeListeners.forEach(l => l())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private update(newText: string, cursor: number) {
|
|
92
|
+
// guard against circular updates when commit() is called during an update cycle
|
|
93
|
+
if (this.updating) return
|
|
94
|
+
this.updating = true
|
|
95
|
+
|
|
96
|
+
const nodes =
|
|
97
|
+
newText === this.text
|
|
98
|
+
? this.nodes
|
|
99
|
+
: parseNodesFromText(newText, this.facetRegexes, this.nodes)
|
|
100
|
+
const prev = this.activeFacet
|
|
101
|
+
const detected = detectActiveFacet(nodes, newText, cursor, this.triggers)
|
|
102
|
+
|
|
103
|
+
// When cursor leaves a facet, commit it
|
|
104
|
+
let committedNode: TapperNode | null = null
|
|
105
|
+
if (prev && (!detected || detected.type !== prev.type)) {
|
|
106
|
+
for (const node of nodes) {
|
|
107
|
+
if (
|
|
108
|
+
node.type === prev.type &&
|
|
109
|
+
node.value === prev.value &&
|
|
110
|
+
node.start === prev.range.start &&
|
|
111
|
+
node.end === prev.range.end
|
|
112
|
+
) {
|
|
113
|
+
node.committed = true
|
|
114
|
+
committedNode = node
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Build activeFacet with commit() baked in
|
|
121
|
+
const active: TapperActiveFacet | null = detected
|
|
122
|
+
? {...detected, commit: this.commit}
|
|
123
|
+
: null
|
|
124
|
+
|
|
125
|
+
this.text = newText
|
|
126
|
+
this.cursor = cursor
|
|
127
|
+
this.nodes = nodes
|
|
128
|
+
this.activeFacet = active
|
|
129
|
+
|
|
130
|
+
this.updating = false
|
|
131
|
+
this.notify()
|
|
132
|
+
|
|
133
|
+
// Fire events after state is finalized
|
|
134
|
+
const facetChanged =
|
|
135
|
+
active?.type !== prev?.type ||
|
|
136
|
+
active?.value !== prev?.value ||
|
|
137
|
+
active?.range.start !== prev?.range.start ||
|
|
138
|
+
active?.range.end !== prev?.range.end
|
|
139
|
+
if (facetChanged) {
|
|
140
|
+
this.emit('activeFacet', active)
|
|
141
|
+
}
|
|
142
|
+
if (committedNode) {
|
|
143
|
+
this.emit('facetCommitted', nodeToFacet(committedNode))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
handleTextChange = (newText: string) => {
|
|
148
|
+
const diff = newText.length - this.text.length
|
|
149
|
+
let newCursor = Math.max(this.cursor + diff, 0)
|
|
150
|
+
|
|
151
|
+
// Atomic deletion: backspace into a facet from outside
|
|
152
|
+
if (diff === -1 && newCursor < this.cursor) {
|
|
153
|
+
for (const node of this.nodes) {
|
|
154
|
+
if (
|
|
155
|
+
node.type !== 'text' &&
|
|
156
|
+
node.committed &&
|
|
157
|
+
this.cursor === node.end
|
|
158
|
+
) {
|
|
159
|
+
const remnant = node.end - node.start - 1
|
|
160
|
+
newText =
|
|
161
|
+
newText.slice(0, node.start) + newText.slice(node.start + remnant)
|
|
162
|
+
newCursor = node.start
|
|
163
|
+
this.pendingCursor = newCursor
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.update(newText, newCursor)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
handleSelectionChange = (e: {nativeEvent: {selection: {start: number}}}) => {
|
|
173
|
+
const newCursor = e.nativeEvent.selection.start
|
|
174
|
+
// After atomic deletion, ignore stale cursor values from the DOM
|
|
175
|
+
if (this.pendingCursor !== null) {
|
|
176
|
+
if (newCursor !== this.pendingCursor) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
this.pendingCursor = null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (newCursor === this.cursor) return
|
|
183
|
+
|
|
184
|
+
this.cursor = newCursor
|
|
185
|
+
this.update(this.text, newCursor)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
replaceText = (text: string, cursor?: number) => {
|
|
189
|
+
const prevActive = this.activeFacet
|
|
190
|
+
const nodes = parseNodesFromText(text, this.facetRegexes)
|
|
191
|
+
// Mark all non-text nodes as committed (pre-existing facets)
|
|
192
|
+
const newlyCommitted: TapperNode[] = []
|
|
193
|
+
for (const node of nodes) {
|
|
194
|
+
if (node.type !== 'text') {
|
|
195
|
+
node.committed = true
|
|
196
|
+
newlyCommitted.push(node)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pos = cursor ?? text.length
|
|
201
|
+
const detected = detectActiveFacet(nodes, text, pos, this.triggers)
|
|
202
|
+
const active: TapperActiveFacet | null = detected
|
|
203
|
+
? {...detected, commit: this.commit}
|
|
204
|
+
: null
|
|
205
|
+
|
|
206
|
+
this.text = text
|
|
207
|
+
this.cursor = pos
|
|
208
|
+
this.nodes = nodes
|
|
209
|
+
this.activeFacet = active
|
|
210
|
+
|
|
211
|
+
this.notify()
|
|
212
|
+
|
|
213
|
+
if (active !== prevActive) {
|
|
214
|
+
this.emit('activeFacet', active)
|
|
215
|
+
}
|
|
216
|
+
for (const node of newlyCommitted) {
|
|
217
|
+
this.emit('facetCommitted', nodeToFacet(node))
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private commit = (value: string, options?: {noTrailingSpace?: boolean}) => {
|
|
222
|
+
if (!this.activeFacet) return
|
|
223
|
+
|
|
224
|
+
const replacement = value + (options?.noTrailingSpace ? '' : ' ')
|
|
225
|
+
const newText =
|
|
226
|
+
this.text.slice(0, this.activeFacet.range.start) +
|
|
227
|
+
replacement +
|
|
228
|
+
this.text.slice(this.activeFacet.range.end)
|
|
229
|
+
const newCursor = this.activeFacet.range.start + replacement.length
|
|
230
|
+
|
|
231
|
+
if (this.activeFacet) {
|
|
232
|
+
this.activeFacet.value = value
|
|
233
|
+
this.activeFacet.range = {
|
|
234
|
+
start: this.activeFacet.range.start,
|
|
235
|
+
end: this.activeFacet.range.start + value.length,
|
|
236
|
+
}
|
|
237
|
+
this.emit('afterInsert', this.activeFacet)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.update(newText, newCursor)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
6
243
|
|
|
7
244
|
export function useTapper(config: TapperConfig) {
|
|
8
|
-
const
|
|
245
|
+
const [store] = useState(() => new Tapper(config))
|
|
246
|
+
const state = useSyncExternalStore(store.subscribe, store.getSnapshot)
|
|
247
|
+
const inputRef = useRef<{focus(): void}>(null)
|
|
9
248
|
|
|
10
249
|
return {
|
|
11
|
-
...
|
|
250
|
+
...state,
|
|
251
|
+
on: store.on,
|
|
252
|
+
focus: () => inputRef.current?.focus(),
|
|
12
253
|
inputProps: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
254
|
+
ref: inputRef as React.RefObject<any>,
|
|
255
|
+
value: state.text,
|
|
256
|
+
selection: {start: state.cursor, end: state.cursor},
|
|
257
|
+
onChangeText: store.handleTextChange,
|
|
258
|
+
onSelectionChange: store.handleSelectionChange,
|
|
18
259
|
},
|
|
19
260
|
}
|
|
20
261
|
}
|
package/src/types.ts
CHANGED
|
@@ -13,13 +13,13 @@ export type TapperNode = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export type TapperFacet = {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
type: string
|
|
17
|
+
value: string
|
|
18
18
|
range: {start: number; end: number}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export type TapperActiveFacet = TapperFacet & {
|
|
22
|
-
|
|
22
|
+
commit: (value: string, options?: {noTrailingSpace?: boolean}) => void
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export type TapperSnapshot = {
|
|
@@ -33,3 +33,9 @@ export type TapperConfig = {
|
|
|
33
33
|
facets: TapperFacetConfigMap
|
|
34
34
|
initialText?: string
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
export type TapperEvents = {
|
|
38
|
+
activeFacet: TapperActiveFacet | null
|
|
39
|
+
facetCommitted: TapperFacet
|
|
40
|
+
afterInsert: TapperFacet
|
|
41
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {TapperFacetConfigMap, TapperNode, TapperFacet} from './types'
|
|
2
|
+
|
|
3
|
+
const WHITESPACE = /\s/
|
|
4
|
+
|
|
5
|
+
export type CompiledFacetRegexes = Map<string, RegExp>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pre-compile facet regexes once at init time. This avoids re-creating
|
|
9
|
+
* RegExp objects on every keystroke in parseNodesFromText. Each Tapper
|
|
10
|
+
* instance gets its own compiled copy so lastIndex state can't leak
|
|
11
|
+
* between instances.
|
|
12
|
+
*/
|
|
13
|
+
export function compileFacetRegexes(
|
|
14
|
+
config: TapperFacetConfigMap,
|
|
15
|
+
): CompiledFacetRegexes {
|
|
16
|
+
const compiled = new Map<string, RegExp>()
|
|
17
|
+
for (const [name, def] of Object.entries(config)) {
|
|
18
|
+
compiled.set(name, new RegExp(def.match.source, def.match.flags))
|
|
19
|
+
}
|
|
20
|
+
return compiled
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
export function parseNodesFromText(
|
|
8
24
|
text: string,
|
|
9
|
-
|
|
25
|
+
regexes: CompiledFacetRegexes,
|
|
10
26
|
prevNodes?: TapperNode[],
|
|
11
27
|
): TapperNode[] {
|
|
12
28
|
const allMatches: {
|
|
@@ -16,8 +32,10 @@ export function parseNodesFromText(
|
|
|
16
32
|
index: number
|
|
17
33
|
}[] = []
|
|
18
34
|
|
|
19
|
-
for (const [name,
|
|
20
|
-
|
|
35
|
+
for (const [name, re] of regexes) {
|
|
36
|
+
// Reset lastIndex so stateful (global) regexes don't carry over
|
|
37
|
+
// match positions from the previous parse call.
|
|
38
|
+
re.lastIndex = 0
|
|
21
39
|
for (const m of text.matchAll(re)) {
|
|
22
40
|
allMatches.push({
|
|
23
41
|
facetName: name,
|
|
@@ -99,7 +117,9 @@ export function parseNodesFromText(
|
|
|
99
117
|
return nodes
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
export function deriveTriggers(
|
|
120
|
+
export function deriveTriggers(
|
|
121
|
+
config: TapperFacetConfigMap,
|
|
122
|
+
): Map<string, string> {
|
|
103
123
|
const triggers = new Map<string, string>()
|
|
104
124
|
for (const [name, def] of Object.entries(config)) {
|
|
105
125
|
const m = def.match.source.match(/^[^\\([\]{}.*+?^$|]+/)
|
|
@@ -108,6 +128,14 @@ export function deriveTriggers(config: TapperFacetConfigMap): Map<string, string
|
|
|
108
128
|
return triggers
|
|
109
129
|
}
|
|
110
130
|
|
|
131
|
+
export function nodeToFacet(node: TapperNode): TapperFacet {
|
|
132
|
+
return {
|
|
133
|
+
type: node.type,
|
|
134
|
+
value: node.value,
|
|
135
|
+
range: {start: node.start, end: node.end},
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
111
139
|
export function detectActiveFacet(
|
|
112
140
|
nodes: TapperNode[],
|
|
113
141
|
text: string,
|
|
@@ -117,8 +145,8 @@ export function detectActiveFacet(
|
|
|
117
145
|
for (const node of nodes) {
|
|
118
146
|
if (node.type !== 'text' && node.start < cursor && cursor <= node.end) {
|
|
119
147
|
return {
|
|
120
|
-
|
|
121
|
-
|
|
148
|
+
type: node.type,
|
|
149
|
+
value: node.value,
|
|
122
150
|
range: {start: node.start, end: node.end},
|
|
123
151
|
}
|
|
124
152
|
}
|
|
@@ -128,12 +156,12 @@ export function detectActiveFacet(
|
|
|
128
156
|
for (let i = cursor - 1; i >= 0; i--) {
|
|
129
157
|
const ch = text[i]
|
|
130
158
|
// Stop at whitespace — triggers don't span across words
|
|
131
|
-
if (
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
159
|
+
if (WHITESPACE.test(ch)) break
|
|
160
|
+
const type = triggers.get(ch)
|
|
161
|
+
if (type) {
|
|
134
162
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
type,
|
|
164
|
+
value: text.slice(i + 1, cursor),
|
|
137
165
|
range: {start: i, end: cursor},
|
|
138
166
|
}
|
|
139
167
|
}
|
package/build/core.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { TapperConfig, TapperNode, TapperActiveFacet, TapperSnapshot } from './types';
|
|
2
|
-
export declare class Tapper {
|
|
3
|
-
private facetConfig;
|
|
4
|
-
private triggers;
|
|
5
|
-
text: string;
|
|
6
|
-
cursor: number;
|
|
7
|
-
nodes: TapperNode[];
|
|
8
|
-
activeFacet: TapperActiveFacet | null;
|
|
9
|
-
private updating;
|
|
10
|
-
private pendingCursor;
|
|
11
|
-
private storeListeners;
|
|
12
|
-
private snapshot;
|
|
13
|
-
constructor(config: TapperConfig);
|
|
14
|
-
subscribe: (listener: () => void) => () => boolean;
|
|
15
|
-
getSnapshot: () => TapperSnapshot;
|
|
16
|
-
private notify;
|
|
17
|
-
private update;
|
|
18
|
-
handleTextChange: (newText: string) => void;
|
|
19
|
-
handleSelectionChange: (newCursor: number) => void;
|
|
20
|
-
replaceText: (text: string, cursor?: number) => void;
|
|
21
|
-
private insert;
|
|
22
|
-
}
|
|
23
|
-
export declare function useTapperCore(config: TapperConfig): {
|
|
24
|
-
store: Tapper;
|
|
25
|
-
text: string;
|
|
26
|
-
cursor: number;
|
|
27
|
-
nodes: TapperNode[];
|
|
28
|
-
activeFacet: TapperActiveFacet | null;
|
|
29
|
-
};
|
|
30
|
-
//# sourceMappingURL=core.d.ts.map
|
package/build/core.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EAEZ,UAAU,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,SAAS,CAAA;AAIhB,qBAAa,MAAM;IACjB,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,QAAQ,CAAqB;IAGrC,IAAI,SAAK;IACT,MAAM,SAAI;IACV,KAAK,EAAE,UAAU,EAAE,CAAK;IACxB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAO;IAG5C,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,aAAa,CAAsB;IAG3C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,MAAM,EAAE,YAAY;IAchC,SAAS,GAAI,UAAU,MAAM,IAAI,mBAGhC;IAED,WAAW,QAAO,cAAc,CAE/B;IAED,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,MAAM;IAiCd,gBAAgB,GAAI,SAAS,MAAM,UAuBlC;IAED,qBAAqB,GAAI,WAAW,MAAM,UAWzC;IAED,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,UAqB3C;IAED,OAAO,CAAC,MAAM,CAoBb;CACF;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY;;;;;;EAIjD"}
|
package/build/core.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { useMemo, useSyncExternalStore } from 'react';
|
|
2
|
-
import { parseNodesFromText, detectActiveFacet, deriveTriggers } from './util';
|
|
3
|
-
export class Tapper {
|
|
4
|
-
facetConfig;
|
|
5
|
-
triggers;
|
|
6
|
-
// Public state
|
|
7
|
-
text = '';
|
|
8
|
-
cursor = 0;
|
|
9
|
-
nodes = [];
|
|
10
|
-
activeFacet = null;
|
|
11
|
-
// Internal tracking
|
|
12
|
-
updating = false;
|
|
13
|
-
pendingCursor = null;
|
|
14
|
-
// useSyncExternalStore plumbing
|
|
15
|
-
storeListeners = new Set();
|
|
16
|
-
snapshot;
|
|
17
|
-
constructor(config) {
|
|
18
|
-
this.facetConfig = config.facets;
|
|
19
|
-
this.triggers = deriveTriggers(config.facets);
|
|
20
|
-
this.snapshot = {
|
|
21
|
-
text: this.text,
|
|
22
|
-
cursor: this.cursor,
|
|
23
|
-
nodes: this.nodes,
|
|
24
|
-
activeFacet: this.activeFacet,
|
|
25
|
-
};
|
|
26
|
-
if (config.initialText) {
|
|
27
|
-
this.replaceText(config.initialText);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
subscribe = (listener) => {
|
|
31
|
-
this.storeListeners.add(listener);
|
|
32
|
-
return () => this.storeListeners.delete(listener);
|
|
33
|
-
};
|
|
34
|
-
getSnapshot = () => {
|
|
35
|
-
return this.snapshot;
|
|
36
|
-
};
|
|
37
|
-
notify() {
|
|
38
|
-
this.snapshot = {
|
|
39
|
-
text: this.text,
|
|
40
|
-
cursor: this.cursor,
|
|
41
|
-
nodes: this.nodes,
|
|
42
|
-
activeFacet: this.activeFacet,
|
|
43
|
-
};
|
|
44
|
-
this.storeListeners.forEach(l => l());
|
|
45
|
-
}
|
|
46
|
-
update(newText, cursor) {
|
|
47
|
-
// guard against circular updates when insert is called during an update cycle
|
|
48
|
-
if (this.updating)
|
|
49
|
-
return;
|
|
50
|
-
this.updating = true;
|
|
51
|
-
const nodes = parseNodesFromText(newText, this.facetConfig, this.nodes);
|
|
52
|
-
const prev = this.activeFacet;
|
|
53
|
-
const detected = detectActiveFacet(nodes, newText, cursor, this.triggers);
|
|
54
|
-
// When cursor leaves a facet, commit it
|
|
55
|
-
if (prev && (!detected || detected.facetType !== prev.facetType)) {
|
|
56
|
-
for (const node of nodes) {
|
|
57
|
-
if (node.type === prev.facetType && node.value === prev.query) {
|
|
58
|
-
node.committed = true;
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Build activeFacet with insert baked in
|
|
64
|
-
const active = detected
|
|
65
|
-
? { ...detected, insert: this.insert }
|
|
66
|
-
: null;
|
|
67
|
-
this.text = newText;
|
|
68
|
-
this.cursor = cursor;
|
|
69
|
-
this.nodes = nodes;
|
|
70
|
-
this.activeFacet = active;
|
|
71
|
-
this.updating = false;
|
|
72
|
-
this.notify();
|
|
73
|
-
}
|
|
74
|
-
handleTextChange = (newText) => {
|
|
75
|
-
const diff = newText.length - this.text.length;
|
|
76
|
-
let newCursor = Math.max(this.cursor + diff, 0);
|
|
77
|
-
// Atomic deletion: backspace into a facet from outside
|
|
78
|
-
if (diff === -1 && newCursor < this.cursor) {
|
|
79
|
-
for (const node of this.nodes) {
|
|
80
|
-
if (node.type !== 'text' &&
|
|
81
|
-
node.committed &&
|
|
82
|
-
this.cursor === node.end) {
|
|
83
|
-
const remnant = node.end - node.start - 1;
|
|
84
|
-
newText =
|
|
85
|
-
newText.slice(0, node.start) + newText.slice(node.start + remnant);
|
|
86
|
-
newCursor = node.start;
|
|
87
|
-
this.pendingCursor = newCursor;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
this.update(newText, newCursor);
|
|
93
|
-
};
|
|
94
|
-
handleSelectionChange = (newCursor) => {
|
|
95
|
-
// After atomic deletion, ignore stale cursor values from the DOM
|
|
96
|
-
if (this.pendingCursor !== null) {
|
|
97
|
-
if (newCursor !== this.pendingCursor) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
this.pendingCursor = null;
|
|
101
|
-
}
|
|
102
|
-
this.cursor = newCursor;
|
|
103
|
-
this.update(this.text, newCursor);
|
|
104
|
-
};
|
|
105
|
-
replaceText = (text, cursor) => {
|
|
106
|
-
const nodes = parseNodesFromText(text, this.facetConfig);
|
|
107
|
-
// Mark all non-text nodes as committed (pre-existing facets)
|
|
108
|
-
for (const node of nodes) {
|
|
109
|
-
if (node.type !== 'text') {
|
|
110
|
-
node.committed = true;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const pos = cursor ?? text.length;
|
|
114
|
-
const detected = detectActiveFacet(nodes, text, pos, this.triggers);
|
|
115
|
-
const active = detected
|
|
116
|
-
? { ...detected, insert: this.insert }
|
|
117
|
-
: null;
|
|
118
|
-
this.text = text;
|
|
119
|
-
this.cursor = pos;
|
|
120
|
-
this.nodes = nodes;
|
|
121
|
-
this.activeFacet = active;
|
|
122
|
-
this.notify();
|
|
123
|
-
};
|
|
124
|
-
insert = (value, options) => {
|
|
125
|
-
const active = this.activeFacet;
|
|
126
|
-
if (!active)
|
|
127
|
-
return;
|
|
128
|
-
const replacement = value + (options?.noTrailingSpace ? '' : ' ');
|
|
129
|
-
const newText = this.text.slice(0, active.range.start) +
|
|
130
|
-
replacement +
|
|
131
|
-
this.text.slice(active.range.end);
|
|
132
|
-
const newCursor = active.range.start + replacement.length;
|
|
133
|
-
this.update(newText, newCursor);
|
|
134
|
-
// Mark the inserted facet as committed
|
|
135
|
-
for (const node of this.nodes) {
|
|
136
|
-
if (node.type !== 'text' && node.start === active.range.start) {
|
|
137
|
-
node.committed = true;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
export function useTapperCore(config) {
|
|
144
|
-
const store = useMemo(() => new Tapper(config), [config]);
|
|
145
|
-
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
146
|
-
return { ...snapshot, store };
|
|
147
|
-
}
|
|
148
|
-
//# sourceMappingURL=core.js.map
|
package/build/core.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAE,oBAAoB,EAAC,MAAM,OAAO,CAAA;AAUnD,OAAO,EAAC,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAC,MAAM,QAAQ,CAAA;AAE5E,MAAM,OAAO,MAAM;IACT,WAAW,CAAsB;IACjC,QAAQ,CAAqB;IAErC,eAAe;IACf,IAAI,GAAG,EAAE,CAAA;IACT,MAAM,GAAG,CAAC,CAAA;IACV,KAAK,GAAiB,EAAE,CAAA;IACxB,WAAW,GAA6B,IAAI,CAAA;IAE5C,oBAAoB;IACZ,QAAQ,GAAG,KAAK,CAAA;IAChB,aAAa,GAAkB,IAAI,CAAA;IAE3C,gCAAgC;IACxB,cAAc,GAAG,IAAI,GAAG,EAAc,CAAA;IACtC,QAAQ,CAAgB;IAEhC,YAAY,MAAoB;QAC9B,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,MAAM,CAAA;QAChC,IAAI,CAAC,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC7C,IAAI,CAAC,QAAQ,GAAG;YACd,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAA;QACD,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;QACtC,CAAC;IACH,CAAC;IAED,SAAS,GAAG,CAAC,QAAoB,EAAE,EAAE;QACnC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACjC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IACnD,CAAC,CAAA;IAED,WAAW,GAAG,GAAmB,EAAE;QACjC,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC,CAAA;IAEO,MAAM;QACZ,IAAI,CAAC,QAAQ,GAAG;YACd,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CAAA;QACD,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;IACvC,CAAC;IAEO,MAAM,CAAC,OAAe,EAAE,MAAc;QAC5C,8EAA8E;QAC9E,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QACzB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;QAEpB,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;QACvE,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAA;QAC7B,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QAEzE,wCAAwC;QACxC,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ,IAAI,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YACjE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;oBAC9D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;oBACrB,MAAK;gBACP,CAAC;YACH,CAAC;QACH,CAAC;QAED,yCAAyC;QACzC,MAAM,MAAM,GAA6B,QAAQ;YAC/C,CAAC,CAAC,EAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAC;YACpC,CAAC,CAAC,IAAI,CAAA;QAER,IAAI,CAAC,IAAI,GAAG,OAAO,CAAA;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAA;QAEzB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAA;QACrB,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;IAED,gBAAgB,GAAG,CAAC,OAAe,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAA;QAC9C,IAAI,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;QAE/C,uDAAuD;QACvD,IAAI,IAAI,KAAK,CAAC,CAAC,IAAI,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC9B,IACE,IAAI,CAAC,IAAI,KAAK,MAAM;oBACpB,IAAI,CAAC,SAAS;oBACd,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,GAAG,EACxB,CAAC;oBACD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;oBACzC,OAAO;wBACL,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,CAAA;oBACpE,SAAS,GAAG,IAAI,CAAC,KAAK,CAAA;oBACtB,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;oBAC9B,MAAK;gBACP,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IACjC,CAAC,CAAA;IAED,qBAAqB,GAAG,CAAC,SAAiB,EAAE,EAAE;QAC5C,iEAAiE;QACjE,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YAChC,IAAI,SAAS,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;gBACrC,OAAM;YACR,CAAC;YACD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAA;QAC3B,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IACnC,CAAC,CAAA;IAED,WAAW,GAAG,CAAC,IAAY,EAAE,MAAe,EAAE,EAAE;QAC9C,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,CAAA;QACxD,6DAA6D;QAC7D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBACzB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;YACvB,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,IAAI,IAAI,CAAC,MAAM,CAAA;QACjC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QACnE,MAAM,MAAM,GAA6B,QAAQ;YAC/C,CAAC,CAAC,EAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAC;YACpC,CAAC,CAAC,IAAI,CAAA;QAER,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,MAAM,GAAG,GAAG,CAAA;QACjB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAA;QAEzB,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC,CAAA;IAEO,MAAM,GAAG,CAAC,KAAa,EAAE,OAAqC,EAAE,EAAE;QACxE,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAA;QAC/B,IAAI,CAAC,MAAM;YAAE,OAAM;QAEnB,MAAM,WAAW,GAAG,KAAK,GAAG,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACjE,MAAM,OAAO,GACX,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;YACtC,WAAW;YACX,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACnC,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,WAAW,CAAC,MAAM,CAAA;QAEzD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;QAE/B,uCAAuC;QACvC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;gBAC9D,IAAI,CAAC,SAAS,GAAG,IAAI,CAAA;gBACrB,MAAK;YACP,CAAC;QACH,CAAC;IACH,CAAC,CAAA;CACF;AAED,MAAM,UAAU,aAAa,CAAC,MAAoB;IAChD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;IACzD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IACzE,OAAO,EAAC,GAAG,QAAQ,EAAE,KAAK,EAAC,CAAA;AAC7B,CAAC","sourcesContent":["import {useMemo, useSyncExternalStore} from 'react'\n\nimport {\n TapperConfig,\n TapperFacetConfigMap,\n TapperNode,\n TapperActiveFacet,\n TapperSnapshot,\n} from './types'\n\nimport {parseNodesFromText, detectActiveFacet, deriveTriggers} from './util'\n\nexport class Tapper {\n private facetConfig: TapperFacetConfigMap\n private triggers: Map<string, string>\n\n // Public state\n text = ''\n cursor = 0\n nodes: TapperNode[] = []\n activeFacet: TapperActiveFacet | null = null\n\n // Internal tracking\n private updating = false\n private pendingCursor: number | null = null\n\n // useSyncExternalStore plumbing\n private storeListeners = new Set<() => void>()\n private snapshot: TapperSnapshot\n\n constructor(config: TapperConfig) {\n this.facetConfig = config.facets\n this.triggers = deriveTriggers(config.facets)\n this.snapshot = {\n text: this.text,\n cursor: this.cursor,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n if (config.initialText) {\n this.replaceText(config.initialText)\n }\n }\n\n subscribe = (listener: () => void) => {\n this.storeListeners.add(listener)\n return () => this.storeListeners.delete(listener)\n }\n\n getSnapshot = (): TapperSnapshot => {\n return this.snapshot\n }\n\n private notify() {\n this.snapshot = {\n text: this.text,\n cursor: this.cursor,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n this.storeListeners.forEach(l => l())\n }\n\n private update(newText: string, cursor: number) {\n // guard against circular updates when insert is called during an update cycle\n if (this.updating) return\n this.updating = true\n\n const nodes = parseNodesFromText(newText, this.facetConfig, this.nodes)\n const prev = this.activeFacet\n const detected = detectActiveFacet(nodes, newText, cursor, this.triggers)\n\n // When cursor leaves a facet, commit it\n if (prev && (!detected || detected.facetType !== prev.facetType)) {\n for (const node of nodes) {\n if (node.type === prev.facetType && node.value === prev.query) {\n node.committed = true\n break\n }\n }\n }\n\n // Build activeFacet with insert baked in\n const active: TapperActiveFacet | null = detected\n ? {...detected, insert: this.insert}\n : null\n\n this.text = newText\n this.cursor = cursor\n this.nodes = nodes\n this.activeFacet = active\n\n this.updating = false\n this.notify()\n }\n\n handleTextChange = (newText: string) => {\n const diff = newText.length - this.text.length\n let newCursor = Math.max(this.cursor + diff, 0)\n\n // Atomic deletion: backspace into a facet from outside\n if (diff === -1 && newCursor < this.cursor) {\n for (const node of this.nodes) {\n if (\n node.type !== 'text' &&\n node.committed &&\n this.cursor === node.end\n ) {\n const remnant = node.end - node.start - 1\n newText =\n newText.slice(0, node.start) + newText.slice(node.start + remnant)\n newCursor = node.start\n this.pendingCursor = newCursor\n break\n }\n }\n }\n\n this.update(newText, newCursor)\n }\n\n handleSelectionChange = (newCursor: number) => {\n // After atomic deletion, ignore stale cursor values from the DOM\n if (this.pendingCursor !== null) {\n if (newCursor !== this.pendingCursor) {\n return\n }\n this.pendingCursor = null\n }\n\n this.cursor = newCursor\n this.update(this.text, newCursor)\n }\n\n replaceText = (text: string, cursor?: number) => {\n const nodes = parseNodesFromText(text, this.facetConfig)\n // Mark all non-text nodes as committed (pre-existing facets)\n for (const node of nodes) {\n if (node.type !== 'text') {\n node.committed = true\n }\n }\n\n const pos = cursor ?? text.length\n const detected = detectActiveFacet(nodes, text, pos, this.triggers)\n const active: TapperActiveFacet | null = detected\n ? {...detected, insert: this.insert}\n : null\n\n this.text = text\n this.cursor = pos\n this.nodes = nodes\n this.activeFacet = active\n\n this.notify()\n }\n\n private insert = (value: string, options?: {noTrailingSpace?: boolean}) => {\n const active = this.activeFacet\n if (!active) return\n\n const replacement = value + (options?.noTrailingSpace ? '' : ' ')\n const newText =\n this.text.slice(0, active.range.start) +\n replacement +\n this.text.slice(active.range.end)\n const newCursor = active.range.start + replacement.length\n\n this.update(newText, newCursor)\n\n // Mark the inserted facet as committed\n for (const node of this.nodes) {\n if (node.type !== 'text' && node.start === active.range.start) {\n node.committed = true\n break\n }\n }\n }\n}\n\nexport function useTapperCore(config: TapperConfig) {\n const store = useMemo(() => new Tapper(config), [config])\n const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot)\n return {...snapshot, store}\n}\n"]}
|
package/build/index.web.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type TapperConfig } from './types';
|
|
2
|
-
export * from './types';
|
|
3
|
-
export * from './core';
|
|
4
|
-
export declare function useTapper(config: TapperConfig): {
|
|
5
|
-
inputProps: {
|
|
6
|
-
ref: import("react").RefObject<HTMLTextAreaElement | null>;
|
|
7
|
-
value: string;
|
|
8
|
-
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
|
9
|
-
onSelect: (e: React.SyntheticEvent<HTMLTextAreaElement>) => void;
|
|
10
|
-
};
|
|
11
|
-
store: import("./core").Tapper;
|
|
12
|
-
text: string;
|
|
13
|
-
cursor: number;
|
|
14
|
-
nodes: import("./types").TapperNode[];
|
|
15
|
-
activeFacet: import("./types").TapperActiveFacet | null;
|
|
16
|
-
};
|
|
17
|
-
//# sourceMappingURL=index.web.d.ts.map
|
package/build/index.web.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,KAAK,YAAY,EAAC,MAAM,SAAS,CAAA;AAEzC,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;;;;sBAatC,KAAK,CAAC,WAAW,CAAC,mBAAmB,CAAC;sBAQtC,KAAK,CAAC,cAAc,CAAC,mBAAmB,CAAC;;;;;;;EAgBhD"}
|
package/build/index.web.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
-
import { useTapperCore } from './core';
|
|
3
|
-
export * from './types';
|
|
4
|
-
export * from './core';
|
|
5
|
-
export function useTapper(config) {
|
|
6
|
-
const tapper = useTapperCore(config);
|
|
7
|
-
const textareaRef = useRef(null);
|
|
8
|
-
// After atomic deletion (or insert), restore cursor position in the DOM
|
|
9
|
-
useLayoutEffect(() => {
|
|
10
|
-
const ta = textareaRef.current;
|
|
11
|
-
if (ta && ta.selectionStart !== tapper.cursor) {
|
|
12
|
-
ta.setSelectionRange(tapper.cursor, tapper.cursor);
|
|
13
|
-
}
|
|
14
|
-
}, [tapper.cursor, tapper.text]);
|
|
15
|
-
const onChange = useCallback((e) => {
|
|
16
|
-
tapper.store.handleTextChange(e.target.value);
|
|
17
|
-
tapper.store.handleSelectionChange(e.target.selectionStart ?? 0);
|
|
18
|
-
}, [tapper.store]);
|
|
19
|
-
const onSelect = useCallback((e) => {
|
|
20
|
-
const target = e.target;
|
|
21
|
-
tapper.store.handleSelectionChange(target.selectionStart ?? 0);
|
|
22
|
-
}, [tapper.store]);
|
|
23
|
-
return {
|
|
24
|
-
...tapper,
|
|
25
|
-
inputProps: {
|
|
26
|
-
ref: textareaRef,
|
|
27
|
-
value: tapper.text,
|
|
28
|
-
onChange,
|
|
29
|
-
onSelect,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
//# sourceMappingURL=index.web.js.map
|
package/build/index.web.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.web.js","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,eAAe,EAAE,MAAM,EAAC,MAAM,OAAO,CAAA;AAE1D,OAAO,EAAC,aAAa,EAAC,MAAM,QAAQ,CAAA;AAGpC,cAAc,SAAS,CAAA;AACvB,cAAc,QAAQ,CAAA;AAEtB,MAAM,UAAU,SAAS,CAAC,MAAoB;IAC5C,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,MAAM,WAAW,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAA;IAErD,wEAAwE;IACxE,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAA;QAC9B,IAAI,EAAE,IAAI,EAAE,CAAC,cAAc,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;YAC9C,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;QACpD,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IAEhC,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,CAAyC,EAAE,EAAE;QAC5C,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC7C,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC,CAAA;IAClE,CAAC,EACD,CAAC,MAAM,CAAC,KAAK,CAAC,CACf,CAAA;IAED,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,CAA4C,EAAE,EAAE;QAC/C,MAAM,MAAM,GAAG,CAAC,CAAC,MAA6B,CAAA;QAC9C,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC,CAAA;IAChE,CAAC,EACD,CAAC,MAAM,CAAC,KAAK,CAAC,CACf,CAAA;IAED,OAAO;QACL,GAAG,MAAM;QACT,UAAU,EAAE;YACV,GAAG,EAAE,WAAW;YAChB,KAAK,EAAE,MAAM,CAAC,IAAI;YAClB,QAAQ;YACR,QAAQ;SACT;KACF,CAAA;AACH,CAAC","sourcesContent":["import {useCallback, useLayoutEffect, useRef} from 'react'\n\nimport {useTapperCore} from './core'\nimport {type TapperConfig} from './types'\n\nexport * from './types'\nexport * from './core'\n\nexport function useTapper(config: TapperConfig) {\n const tapper = useTapperCore(config)\n const textareaRef = useRef<HTMLTextAreaElement>(null)\n\n // After atomic deletion (or insert), restore cursor position in the DOM\n useLayoutEffect(() => {\n const ta = textareaRef.current\n if (ta && ta.selectionStart !== tapper.cursor) {\n ta.setSelectionRange(tapper.cursor, tapper.cursor)\n }\n }, [tapper.cursor, tapper.text])\n\n const onChange = useCallback(\n (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n tapper.store.handleTextChange(e.target.value)\n tapper.store.handleSelectionChange(e.target.selectionStart ?? 0)\n },\n [tapper.store],\n )\n\n const onSelect = useCallback(\n (e: React.SyntheticEvent<HTMLTextAreaElement>) => {\n const target = e.target as HTMLTextAreaElement\n tapper.store.handleSelectionChange(target.selectionStart ?? 0)\n },\n [tapper.store],\n )\n\n return {\n ...tapper,\n inputProps: {\n ref: textareaRef,\n value: tapper.text,\n onChange,\n onSelect,\n },\n }\n}\n"]}
|