@bsky.app/tapper 0.2.3 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @bsky.app/tapper
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`f215a99`](https://github.com/bluesky-social/toolbox/commit/f215a99913fec45d8842da22f7c02737c14301ef) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Bump minor, packages are mostly stable
8
+
9
+ ## 0.2.4
10
+
11
+ ### Patch Changes
12
+
13
+ - [`de1fb73`](https://github.com/bluesky-social/toolbox/commit/de1fb73c07745fd3c5d9e55332a27ef0c113ed6a) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Add a `raw` field to facets to match `Node`
14
+
15
+ - [`c2a6730`](https://github.com/bluesky-social/toolbox/commit/c2a67303aca2c388c060f24187db8e41345d720c) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Fix selection state to allow for ranged selections
16
+
3
17
  ## 0.2.3
4
18
 
5
19
  ### Patch Changes
package/build/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { TapperConfig, TapperEvents, TapperNode, TapperActiveFacet, TapperSnapshot } from './types';
1
+ import { TapperConfig, TapperEvents, TapperNode, TapperActiveFacet, TapperSelection, TapperSnapshot } from './types';
2
2
  export * from './types';
3
3
  export * from './facets';
4
4
  export declare class Tapper {
@@ -6,11 +6,12 @@ export declare class Tapper {
6
6
  private triggers;
7
7
  private listeners;
8
8
  text: string;
9
- cursor: number;
9
+ selection: TapperSelection;
10
10
  nodes: TapperNode[];
11
11
  activeFacet: TapperActiveFacet | null;
12
+ private pendingMutation;
13
+ private static MUTATION_DEBOUNCE_MS;
12
14
  private updating;
13
- private pendingCursor;
14
15
  private storeListeners;
15
16
  private snapshot;
16
17
  constructor(config?: TapperConfig);
@@ -25,6 +26,7 @@ export declare class Tapper {
25
26
  nativeEvent: {
26
27
  selection: {
27
28
  start: number;
29
+ end: number;
28
30
  };
29
31
  };
30
32
  }) => void;
@@ -37,21 +39,19 @@ export declare function useTapper(config: TapperConfig): {
37
39
  inputProps: {
38
40
  ref: React.RefObject<any>;
39
41
  value: string;
40
- selection: {
41
- start: number;
42
- end: number;
43
- };
42
+ selection: TapperSelection;
44
43
  onChangeText: (newText: string) => void;
45
44
  onSelectionChange: (e: {
46
45
  nativeEvent: {
47
46
  selection: {
48
47
  start: number;
48
+ end: number;
49
49
  };
50
50
  };
51
51
  }) => void;
52
52
  };
53
53
  text: string;
54
- cursor: number;
54
+ selection: TapperSelection;
55
55
  nodes: TapperNode[];
56
56
  activeFacet: TapperActiveFacet | null;
57
57
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,SAAS,CAAA;AAWhB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AAExB,qBAAa,MAAM;IACjB,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,QAAQ,CAAqB;IAIrC,OAAO,CAAC,SAAS,CAA0D;IAG3E,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,CAAC,EAAE,YAAY;IAejC,SAAS,GAAI,UAAU,MAAM,IAAI,mBAGhC;IAED,WAAW,QAAO,cAAc,CAE/B;IAED,EAAE,GAAI,CAAC,SAAS,MAAM,YAAY,EAChC,OAAO,CAAC,EACR,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,gBAOpC;IAED,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,MAAM;IA8Dd,gBAAgB,GAAI,SAAS,MAAM,UAuBlC;IAED,qBAAqB,GAAI,GAAG;QAAC,WAAW,EAAE;YAAC,SAAS,EAAE;gBAAC,KAAK,EAAE,MAAM,CAAA;aAAC,CAAA;SAAC,CAAA;KAAC,UActE;IAED,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,UA+B3C;IAED,OAAO,CAAC,MAAM,CAoBb;CACF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;SAxLtC,CAAC,SAAS,MAAM,YAAY,2CAED,IAAI;;;aAgMhB,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC;;;;;;gCA3GZ,MAAM;+BAyBP;YAAC,WAAW,EAAE;gBAAC,SAAS,EAAE;oBAAC,KAAK,EAAE,MAAM,CAAA;iBAAC,CAAA;aAAC,CAAA;SAAC;;;;;;EAyFxE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,cAAc,EACf,MAAM,SAAS,CAAA;AAWhB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AAExB,qBAAa,MAAM;IACjB,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,QAAQ,CAAqB;IAIrC,OAAO,CAAC,SAAS,CAA0D;IAG3E,IAAI,SAAK;IACT,SAAS,EAAE,eAAe,CAAqB;IAC/C,KAAK,EAAE,UAAU,EAAE,CAAK;IACxB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAO;IAwB5C,OAAO,CAAC,eAAe,CAAuD;IAC9E,OAAO,CAAC,MAAM,CAAC,oBAAoB,CAAM;IAEzC,OAAO,CAAC,QAAQ,CAAQ;IAGxB,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,MAAM,CAAC,EAAE,YAAY;IAejC,SAAS,GAAI,UAAU,MAAM,IAAI,mBAGhC;IAED,WAAW,QAAO,cAAc,CAE/B;IAED,EAAE,GAAI,CAAC,SAAS,MAAM,YAAY,EAChC,OAAO,CAAC,EACR,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,IAAI,gBAOpC;IAED,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM;IAUd,OAAO,CAAC,MAAM;IAmEd,gBAAgB,GAAI,SAAS,MAAM,UA4ClC;IAED,qBAAqB,GAAI,GAAG;QAC1B,WAAW,EAAE;YAAC,SAAS,EAAE;gBAAC,KAAK,EAAE,MAAM,CAAC;gBAAC,GAAG,EAAE,MAAM,CAAA;aAAC,CAAA;SAAC,CAAA;KACvD,UA4BA;IAED,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,UA+B3C;IAED,OAAO,CAAC,MAAM,CAyBb;CACF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;SAvOtC,CAAC,SAAS,MAAM,YAAY,2CAED,IAAI;;;aA+OhB,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC;;;gCArJZ,MAAM;+BA8CP;YAC1B,WAAW,EAAE;gBAAC,SAAS,EAAE;oBAAC,KAAK,EAAE,MAAM,CAAC;oBAAC,GAAG,EAAE,MAAM,CAAA;iBAAC,CAAA;aAAC,CAAA;SACvD;;;;;;EA4GF"}
package/build/index.js CHANGED
@@ -11,12 +11,34 @@ export class Tapper {
11
11
  listeners = new Map();
12
12
  // Public state
13
13
  text = '';
14
- cursor = 0;
14
+ selection = { start: 0, end: 0 };
15
15
  nodes = [];
16
16
  activeFacet = null;
17
- // Internal tracking
17
+ /*
18
+ * Guard against stale native events after programmatic text replacement.
19
+ *
20
+ * After insert() or atomic deletion, we update this.text and this.selection
21
+ * synchronously. However, iOS (especially on the old architecture bridge)
22
+ * may then fire stale onChangeText and onSelectionChange events reflecting
23
+ * the *previous* text/cursor state. These arrive within ~60ms.
24
+ *
25
+ * We record the expected textLength and a timestamp, then use two guards:
26
+ *
27
+ * 1. handleTextChange: if the incoming text length doesn't match the
28
+ * expected length AND we're within the debounce window, it's a stale
29
+ * event echoing the pre-replacement text. Skip it.
30
+ *
31
+ * 2. handleSelectionChange: if we're within the debounce window, skip
32
+ * ALL selection events. The correct selection was already set by
33
+ * update(), and any events within the window are either duplicates
34
+ * (no-ops) or stale positions from the old text.
35
+ *
36
+ * After the window passes, the guard clears itself and normal event
37
+ * handling resumes (cursor taps, typing, etc).
38
+ */
39
+ pendingMutation = null;
40
+ static MUTATION_DEBOUNCE_MS = 100;
18
41
  updating = false;
19
- pendingCursor = null;
20
42
  // useSyncExternalStore plumbing
21
43
  storeListeners = new Set();
22
44
  snapshot;
@@ -26,7 +48,7 @@ export class Tapper {
26
48
  this.triggers = deriveTriggers(facets);
27
49
  this.snapshot = {
28
50
  text: this.text,
29
- cursor: this.cursor,
51
+ selection: this.selection,
30
52
  nodes: this.nodes,
31
53
  activeFacet: this.activeFacet,
32
54
  };
@@ -55,22 +77,26 @@ export class Tapper {
55
77
  notify() {
56
78
  this.snapshot = {
57
79
  text: this.text,
58
- cursor: this.cursor,
80
+ selection: this.selection,
59
81
  nodes: this.nodes,
60
82
  activeFacet: this.activeFacet,
61
83
  };
62
84
  this.storeListeners.forEach(l => l());
63
85
  }
64
- update(newText, cursor) {
65
- // guard against circular updates when commit() is called during an update cycle
86
+ update(newText, selection) {
87
+ // guard against circular updates when insert() is called during an update cycle
66
88
  if (this.updating)
67
89
  return;
68
90
  this.updating = true;
91
+ const cursor = selection.end;
92
+ const isRange = selection.start !== selection.end;
69
93
  const nodes = newText === this.text
70
94
  ? this.nodes
71
95
  : parseNodesFromText(newText, this.facetRegexes, this.nodes, cursor, this.triggers);
72
96
  const prev = this.activeFacet;
73
- const detected = detectActiveFacet(nodes, newText, cursor, this.triggers);
97
+ const detected = isRange
98
+ ? null
99
+ : detectActiveFacet(nodes, newText, cursor, this.triggers);
74
100
  // When cursor leaves a facet, commit it
75
101
  let committedNode = null;
76
102
  if (prev && (!detected || detected.type !== prev.type)) {
@@ -85,12 +111,12 @@ export class Tapper {
85
111
  }
86
112
  }
87
113
  }
88
- // Build activeFacet with commit() baked in
114
+ // Build activeFacet with insert() baked in
89
115
  const active = detected
90
116
  ? { ...detected, insert: this.insert }
91
117
  : null;
92
118
  this.text = newText;
93
- this.cursor = cursor;
119
+ this.selection = selection;
94
120
  this.nodes = nodes;
95
121
  this.activeFacet = active;
96
122
  this.updating = false;
@@ -108,38 +134,70 @@ export class Tapper {
108
134
  }
109
135
  }
110
136
  handleTextChange = (newText) => {
137
+ if (newText === this.text)
138
+ return;
139
+ /*
140
+ * Guard 1: stale text. After insert/atomic deletion, iOS may echo
141
+ * onChangeText with the pre-replacement text. If the incoming length
142
+ * doesn't match what we expect and we're within the debounce window,
143
+ * this is a stale echo — skip it.
144
+ */
145
+ if (this.pendingMutation !== null) {
146
+ const elapsed = Date.now() - this.pendingMutation.timestamp;
147
+ if (elapsed < Tapper.MUTATION_DEBOUNCE_MS &&
148
+ newText.length !== this.pendingMutation.textLength) {
149
+ return;
150
+ }
151
+ }
111
152
  const diff = newText.length - this.text.length;
112
- let newCursor = Math.max(this.cursor + diff, 0);
153
+ let newEnd = Math.max(this.selection.end + diff, 0);
113
154
  // Atomic deletion: backspace into a facet from outside
114
- if (diff === -1 && newCursor < this.cursor) {
155
+ if (diff === -1 && newEnd < this.selection.end) {
115
156
  for (const node of this.nodes) {
116
157
  if (node.type === 'facet' &&
117
158
  node.committed &&
118
- this.cursor === node.end) {
159
+ this.selection.end === node.end) {
119
160
  const remnant = node.end - node.start - 1;
120
161
  newText =
121
162
  newText.slice(0, node.start) + newText.slice(node.start + remnant);
122
- newCursor = node.start;
123
- this.pendingCursor = newCursor;
163
+ newEnd = node.start;
164
+ this.pendingMutation = {
165
+ textLength: newText.length,
166
+ timestamp: Date.now(),
167
+ };
124
168
  break;
125
169
  }
126
170
  }
127
171
  }
128
- this.update(newText, newCursor);
172
+ this.update(newText, { start: newEnd, end: newEnd });
129
173
  };
130
174
  handleSelectionChange = (e) => {
131
- const newCursor = e.nativeEvent.selection.start;
132
- // After atomic deletion, ignore stale cursor values from the DOM
133
- if (this.pendingCursor !== null) {
134
- if (newCursor !== this.pendingCursor) {
175
+ const { start, end } = e.nativeEvent.selection;
176
+ /*
177
+ * Guard 2: stale selection. After insert/atomic deletion, iOS may fire
178
+ * onSelectionChange with cursor positions from the old text state
179
+ * (e.g. a "deselection" event at the pre-insert cursor). Within the
180
+ * debounce window, skip ALL selection events — the correct selection
181
+ * was already set synchronously by update().
182
+ */
183
+ if (this.pendingMutation !== null) {
184
+ const elapsed = Date.now() - this.pendingMutation.timestamp;
185
+ if (elapsed < Tapper.MUTATION_DEBOUNCE_MS)
135
186
  return;
136
- }
137
- this.pendingCursor = null;
187
+ this.pendingMutation = null;
138
188
  }
139
- if (newCursor === this.cursor)
189
+ /*
190
+ * Guard 3: premature selection. iOS sometimes fires onSelectionChange
191
+ * before onChangeText for the same edit, reporting a cursor position
192
+ * that's beyond the current text length. Skip it — handleTextChange
193
+ * will set the correct position when it arrives.
194
+ */
195
+ if (end > this.text.length)
196
+ return;
197
+ if (start === this.selection.start && end === this.selection.end)
140
198
  return;
141
- this.cursor = newCursor;
142
- this.update(this.text, newCursor);
199
+ this.selection = { start, end };
200
+ this.update(this.text, { start, end });
143
201
  };
144
202
  replaceText = (text, cursor) => {
145
203
  const prevActive = this.activeFacet;
@@ -158,7 +216,7 @@ export class Tapper {
158
216
  ? { ...detected, insert: this.insert }
159
217
  : null;
160
218
  this.text = text;
161
- this.cursor = pos;
219
+ this.selection = { start: pos, end: pos };
162
220
  this.nodes = nodes;
163
221
  this.activeFacet = active;
164
222
  this.notify();
@@ -176,8 +234,9 @@ export class Tapper {
176
234
  const newText = this.text.slice(0, this.activeFacet.range.start) +
177
235
  replacement +
178
236
  this.text.slice(this.activeFacet.range.end);
179
- const newCursor = this.activeFacet.range.start + replacement.length;
237
+ const newEnd = this.activeFacet.range.start + replacement.length;
180
238
  if (this.activeFacet) {
239
+ this.activeFacet.raw = value;
181
240
  this.activeFacet.value = value;
182
241
  this.activeFacet.range = {
183
242
  start: this.activeFacet.range.start,
@@ -185,7 +244,11 @@ export class Tapper {
185
244
  };
186
245
  this.emit('afterInsert', this.activeFacet);
187
246
  }
188
- this.update(newText, newCursor);
247
+ this.pendingMutation = {
248
+ textLength: newText.length,
249
+ timestamp: Date.now(),
250
+ };
251
+ this.update(newText, { start: newEnd, end: newEnd });
189
252
  };
190
253
  }
191
254
  export function useTapper(config) {
@@ -199,7 +262,7 @@ export function useTapper(config) {
199
262
  inputProps: {
200
263
  ref: inputRef,
201
264
  value: state.text,
202
- selection: { start: state.cursor, end: state.cursor },
265
+ selection: state.selection,
203
266
  onChangeText: store.handleTextChange,
204
267
  onSelectionChange: store.handleSelectionChange,
205
268
  },
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {useRef, useState, useSyncExternalStore} from 'react'\n\nimport {\n TapperConfig,\n TapperEvents,\n TapperNode,\n TapperActiveFacet,\n TapperSnapshot,\n} from './types'\nimport {\n parseNodesFromText,\n detectActiveFacet,\n deriveTriggers,\n nodeToFacet,\n compileFacetRegexes,\n type CompiledFacetRegexes,\n} from './util'\nimport * as defaultFacets from './facets'\n\nexport * from './types'\nexport * from './facets'\n\nexport class Tapper {\n private facetRegexes: CompiledFacetRegexes\n private triggers: Map<string, string>\n\n // Event emitters\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private listeners = new Map<keyof TapperEvents, Set<(data: any) => void>>()\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 const facets = config?.facets || defaultFacets\n this.facetRegexes = compileFacetRegexes(facets)\n this.triggers = deriveTriggers(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 on = <K extends keyof TapperEvents>(\n event: K,\n cb: (data: TapperEvents[K]) => void,\n ) => {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set())\n this.listeners.get(event)!.add(cb)\n return () => {\n this.listeners.get(event)?.delete(cb)\n }\n }\n\n private emit<K extends keyof TapperEvents>(event: K, data: TapperEvents[K]) {\n this.listeners.get(event)?.forEach(cb => cb(data))\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 commit() is called during an update cycle\n if (this.updating) return\n this.updating = true\n\n const nodes =\n newText === this.text\n ? this.nodes\n : parseNodesFromText(\n newText,\n this.facetRegexes,\n this.nodes,\n cursor,\n this.triggers,\n )\n const prev = this.activeFacet\n const detected = detectActiveFacet(nodes, newText, cursor, this.triggers)\n\n // When cursor leaves a facet, commit it\n let committedNode: TapperNode | null = null\n if (prev && (!detected || detected.type !== prev.type)) {\n for (const node of nodes) {\n if (\n node.type === 'facet' &&\n node.facetType === prev.type &&\n node.start === prev.range.start &&\n node.end === prev.range.end\n ) {\n node.committed = true\n committedNode = node\n break\n }\n }\n }\n\n // Build activeFacet with commit() 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 // Fire events after state is finalized\n const facetChanged =\n active?.type !== prev?.type ||\n active?.value !== prev?.value ||\n active?.range.start !== prev?.range.start ||\n active?.range.end !== prev?.range.end\n if (facetChanged) {\n this.emit('activeFacet', active)\n }\n if (committedNode) {\n this.emit('facetCommitted', nodeToFacet(committedNode))\n }\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 === 'facet' &&\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 = (e: {nativeEvent: {selection: {start: number}}}) => {\n const newCursor = e.nativeEvent.selection.start\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 if (newCursor === this.cursor) return\n\n this.cursor = newCursor\n this.update(this.text, newCursor)\n }\n\n replaceText = (text: string, cursor?: number) => {\n const prevActive = this.activeFacet\n const nodes = parseNodesFromText(text, this.facetRegexes)\n // Mark all non-text nodes as committed (pre-existing facets)\n const newlyCommitted: TapperNode[] = []\n for (const node of nodes) {\n if (node.type === 'facet') {\n node.committed = true\n newlyCommitted.push(node)\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 if (active !== prevActive) {\n this.emit('activeFacet', active)\n }\n for (const node of newlyCommitted) {\n this.emit('facetCommitted', nodeToFacet(node))\n }\n }\n\n private insert = (value: string, options?: {noTrailingSpace?: boolean}) => {\n if (!this.activeFacet) return\n\n const replacement = value + (options?.noTrailingSpace ? '' : ' ')\n const newText =\n this.text.slice(0, this.activeFacet.range.start) +\n replacement +\n this.text.slice(this.activeFacet.range.end)\n const newCursor = this.activeFacet.range.start + replacement.length\n\n if (this.activeFacet) {\n this.activeFacet.value = value\n this.activeFacet.range = {\n start: this.activeFacet.range.start,\n end: this.activeFacet.range.start + value.length,\n }\n this.emit('afterInsert', this.activeFacet)\n }\n\n this.update(newText, newCursor)\n }\n}\n\nexport function useTapper(config: TapperConfig) {\n const [store] = useState(() => new Tapper(config))\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot)\n const inputRef = useRef<{focus(): void}>(null)\n\n return {\n ...state,\n on: store.on,\n focus: () => inputRef.current?.focus(),\n inputProps: {\n ref: inputRef as React.RefObject<any>,\n value: state.text,\n selection: {start: state.cursor, end: state.cursor},\n onChangeText: store.handleTextChange,\n onSelectionChange: store.handleSelectionChange,\n },\n }\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {useRef, useState, useSyncExternalStore} from 'react'\n\nimport {\n TapperConfig,\n TapperEvents,\n TapperNode,\n TapperActiveFacet,\n TapperSelection,\n TapperSnapshot,\n} from './types'\nimport {\n parseNodesFromText,\n detectActiveFacet,\n deriveTriggers,\n nodeToFacet,\n compileFacetRegexes,\n type CompiledFacetRegexes,\n} from './util'\nimport * as defaultFacets from './facets'\n\nexport * from './types'\nexport * from './facets'\n\nexport class Tapper {\n private facetRegexes: CompiledFacetRegexes\n private triggers: Map<string, string>\n\n // Event emitters\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private listeners = new Map<keyof TapperEvents, Set<(data: any) => void>>()\n\n // Public state\n text = ''\n selection: TapperSelection = {start: 0, end: 0}\n nodes: TapperNode[] = []\n activeFacet: TapperActiveFacet | null = null\n\n /*\n * Guard against stale native events after programmatic text replacement.\n *\n * After insert() or atomic deletion, we update this.text and this.selection\n * synchronously. However, iOS (especially on the old architecture bridge)\n * may then fire stale onChangeText and onSelectionChange events reflecting\n * the *previous* text/cursor state. These arrive within ~60ms.\n *\n * We record the expected textLength and a timestamp, then use two guards:\n *\n * 1. handleTextChange: if the incoming text length doesn't match the\n * expected length AND we're within the debounce window, it's a stale\n * event echoing the pre-replacement text. Skip it.\n *\n * 2. handleSelectionChange: if we're within the debounce window, skip\n * ALL selection events. The correct selection was already set by\n * update(), and any events within the window are either duplicates\n * (no-ops) or stale positions from the old text.\n *\n * After the window passes, the guard clears itself and normal event\n * handling resumes (cursor taps, typing, etc).\n */\n private pendingMutation: {textLength: number; timestamp: number} | null = null\n private static MUTATION_DEBOUNCE_MS = 100\n\n private updating = false\n\n // useSyncExternalStore plumbing\n private storeListeners = new Set<() => void>()\n private snapshot: TapperSnapshot\n\n constructor(config?: TapperConfig) {\n const facets = config?.facets || defaultFacets\n this.facetRegexes = compileFacetRegexes(facets)\n this.triggers = deriveTriggers(facets)\n this.snapshot = {\n text: this.text,\n selection: this.selection,\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 on = <K extends keyof TapperEvents>(\n event: K,\n cb: (data: TapperEvents[K]) => void,\n ) => {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set())\n this.listeners.get(event)!.add(cb)\n return () => {\n this.listeners.get(event)?.delete(cb)\n }\n }\n\n private emit<K extends keyof TapperEvents>(event: K, data: TapperEvents[K]) {\n this.listeners.get(event)?.forEach(cb => cb(data))\n }\n\n private notify() {\n this.snapshot = {\n text: this.text,\n selection: this.selection,\n nodes: this.nodes,\n activeFacet: this.activeFacet,\n }\n this.storeListeners.forEach(l => l())\n }\n\n private update(newText: string, selection: TapperSelection) {\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 cursor = selection.end\n const isRange = selection.start !== selection.end\n\n const nodes =\n newText === this.text\n ? this.nodes\n : parseNodesFromText(\n newText,\n this.facetRegexes,\n this.nodes,\n cursor,\n this.triggers,\n )\n const prev = this.activeFacet\n const detected = isRange\n ? null\n : detectActiveFacet(nodes, newText, cursor, this.triggers)\n\n // When cursor leaves a facet, commit it\n let committedNode: TapperNode | null = null\n if (prev && (!detected || detected.type !== prev.type)) {\n for (const node of nodes) {\n if (\n node.type === 'facet' &&\n node.facetType === prev.type &&\n node.start === prev.range.start &&\n node.end === prev.range.end\n ) {\n node.committed = true\n committedNode = node\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.selection = selection\n this.nodes = nodes\n this.activeFacet = active\n\n this.updating = false\n this.notify()\n\n // Fire events after state is finalized\n const facetChanged =\n active?.type !== prev?.type ||\n active?.value !== prev?.value ||\n active?.range.start !== prev?.range.start ||\n active?.range.end !== prev?.range.end\n if (facetChanged) {\n this.emit('activeFacet', active)\n }\n if (committedNode) {\n this.emit('facetCommitted', nodeToFacet(committedNode))\n }\n }\n\n handleTextChange = (newText: string) => {\n if (newText === this.text) return\n\n /*\n * Guard 1: stale text. After insert/atomic deletion, iOS may echo\n * onChangeText with the pre-replacement text. If the incoming length\n * doesn't match what we expect and we're within the debounce window,\n * this is a stale echo — skip it.\n */\n if (this.pendingMutation !== null) {\n const elapsed = Date.now() - this.pendingMutation.timestamp\n if (\n elapsed < Tapper.MUTATION_DEBOUNCE_MS &&\n newText.length !== this.pendingMutation.textLength\n ) {\n return\n }\n }\n\n const diff = newText.length - this.text.length\n let newEnd = Math.max(this.selection.end + diff, 0)\n\n // Atomic deletion: backspace into a facet from outside\n if (diff === -1 && newEnd < this.selection.end) {\n for (const node of this.nodes) {\n if (\n node.type === 'facet' &&\n node.committed &&\n this.selection.end === 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 newEnd = node.start\n this.pendingMutation = {\n textLength: newText.length,\n timestamp: Date.now(),\n }\n break\n }\n }\n }\n\n this.update(newText, {start: newEnd, end: newEnd})\n }\n\n handleSelectionChange = (e: {\n nativeEvent: {selection: {start: number; end: number}}\n }) => {\n const {start, end} = e.nativeEvent.selection\n\n /*\n * Guard 2: stale selection. After insert/atomic deletion, iOS may fire\n * onSelectionChange with cursor positions from the old text state\n * (e.g. a \"deselection\" event at the pre-insert cursor). Within the\n * debounce window, skip ALL selection events — the correct selection\n * was already set synchronously by update().\n */\n if (this.pendingMutation !== null) {\n const elapsed = Date.now() - this.pendingMutation.timestamp\n if (elapsed < Tapper.MUTATION_DEBOUNCE_MS) return\n this.pendingMutation = null\n }\n\n /*\n * Guard 3: premature selection. iOS sometimes fires onSelectionChange\n * before onChangeText for the same edit, reporting a cursor position\n * that's beyond the current text length. Skip it — handleTextChange\n * will set the correct position when it arrives.\n */\n if (end > this.text.length) return\n\n if (start === this.selection.start && end === this.selection.end) return\n\n this.selection = {start, end}\n this.update(this.text, {start, end})\n }\n\n replaceText = (text: string, cursor?: number) => {\n const prevActive = this.activeFacet\n const nodes = parseNodesFromText(text, this.facetRegexes)\n // Mark all non-text nodes as committed (pre-existing facets)\n const newlyCommitted: TapperNode[] = []\n for (const node of nodes) {\n if (node.type === 'facet') {\n node.committed = true\n newlyCommitted.push(node)\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.selection = {start: pos, end: pos}\n this.nodes = nodes\n this.activeFacet = active\n\n this.notify()\n\n if (active !== prevActive) {\n this.emit('activeFacet', active)\n }\n for (const node of newlyCommitted) {\n this.emit('facetCommitted', nodeToFacet(node))\n }\n }\n\n private insert = (value: string, options?: {noTrailingSpace?: boolean}) => {\n if (!this.activeFacet) return\n\n const replacement = value + (options?.noTrailingSpace ? '' : ' ')\n const newText =\n this.text.slice(0, this.activeFacet.range.start) +\n replacement +\n this.text.slice(this.activeFacet.range.end)\n const newEnd = this.activeFacet.range.start + replacement.length\n\n if (this.activeFacet) {\n this.activeFacet.raw = value\n this.activeFacet.value = value\n this.activeFacet.range = {\n start: this.activeFacet.range.start,\n end: this.activeFacet.range.start + value.length,\n }\n this.emit('afterInsert', this.activeFacet)\n }\n\n this.pendingMutation = {\n textLength: newText.length,\n timestamp: Date.now(),\n }\n this.update(newText, {start: newEnd, end: newEnd})\n }\n}\n\nexport function useTapper(config: TapperConfig) {\n const [store] = useState(() => new Tapper(config))\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot)\n const inputRef = useRef<{focus(): void}>(null)\n\n return {\n ...state,\n on: store.on,\n focus: () => inputRef.current?.focus(),\n inputProps: {\n ref: inputRef as React.RefObject<any>,\n value: state.text,\n selection: state.selection,\n onChangeText: store.handleTextChange,\n onSelectionChange: store.handleSelectionChange,\n },\n }\n}\n"]}
package/build/types.d.ts CHANGED
@@ -26,6 +26,7 @@ export type TapperNode = {
26
26
  };
27
27
  export type TapperFacet = {
28
28
  type: string;
29
+ raw: string;
29
30
  value: string;
30
31
  range: {
31
32
  start: number;
@@ -37,9 +38,13 @@ export type TapperActiveFacet = TapperFacet & {
37
38
  noTrailingSpace?: boolean;
38
39
  }) => void;
39
40
  };
41
+ export type TapperSelection = {
42
+ start: number;
43
+ end: number;
44
+ };
40
45
  export type TapperSnapshot = {
41
46
  text: string;
42
- cursor: number;
47
+ selection: TapperSelection;
43
48
  nodes: TapperNode[];
44
49
  activeFacet: TapperActiveFacet | null;
45
50
  };
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEtD,MAAM,MAAM,UAAU,GAClB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,GACD;IACE,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB,GACD;IACE,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAEL,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC5C,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACtC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,cAAc,EAAE,WAAW,CAAA;IAC3B,WAAW,EAAE,WAAW,CAAA;CACzB,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEtD,MAAM,MAAM,UAAU,GAClB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB,GACD;IACE,IAAI,EAAE,SAAS,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB,GACD;IACE,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAEL,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC5C,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAC,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC,CAAA;AAE1D,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,eAAe,CAAA;IAC1B,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;CACtC,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,cAAc,EAAE,WAAW,CAAA;IAC3B,WAAW,EAAE,WAAW,CAAA;CACzB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type TapperFacetConfig = Record<string, RegExp>\n\nexport type TapperNode =\n | {\n type: 'text'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType?: undefined\n }\n | {\n type: 'trigger'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType: string\n }\n | {\n type: 'facet'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType: string\n }\n\nexport type TapperFacet = {\n type: string\n value: string\n range: {start: number; end: number}\n}\n\nexport type TapperActiveFacet = TapperFacet & {\n insert: (value: string, options?: {noTrailingSpace?: boolean}) => void\n}\n\nexport type TapperSnapshot = {\n text: string\n cursor: number\n nodes: TapperNode[]\n activeFacet: TapperActiveFacet | null\n}\n\nexport type TapperConfig = {\n facets?: TapperFacetConfig\n initialText?: string\n}\n\nexport type TapperEvents = {\n activeFacet: TapperActiveFacet | null\n facetCommitted: TapperFacet\n afterInsert: TapperFacet\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export type TapperFacetConfig = Record<string, RegExp>\n\nexport type TapperNode =\n | {\n type: 'text'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType?: undefined\n }\n | {\n type: 'trigger'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType: string\n }\n | {\n type: 'facet'\n raw: string\n value: string\n start: number\n end: number\n committed?: boolean\n facetType: string\n }\n\nexport type TapperFacet = {\n type: string\n raw: string\n value: string\n range: {start: number; end: number}\n}\n\nexport type TapperActiveFacet = TapperFacet & {\n insert: (value: string, options?: {noTrailingSpace?: boolean}) => void\n}\n\nexport type TapperSelection = {start: number; end: number}\n\nexport type TapperSnapshot = {\n text: string\n selection: TapperSelection\n nodes: TapperNode[]\n activeFacet: TapperActiveFacet | null\n}\n\nexport type TapperConfig = {\n facets?: TapperFacetConfig\n initialText?: string\n}\n\nexport type TapperEvents = {\n activeFacet: TapperActiveFacet | null\n facetCommitted: TapperFacet\n afterInsert: TapperFacet\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,UAAU,EAAE,WAAW,EAAC,MAAM,SAAS,CAAA;AAIlE,MAAM,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEtD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAMtB;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,EAC7B,SAAS,CAAC,EAAE,UAAU,EAAE,EACxB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,UAAU,EAAE,CAqJd;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAO7E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAMzD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,WAAW,GAAG,IAAI,CAkCpB"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,UAAU,EAAE,WAAW,EAAC,MAAM,SAAS,CAAA;AAIlE,MAAM,MAAM,oBAAoB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEtD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CAMtB;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,EAC7B,SAAS,CAAC,EAAE,UAAU,EAAE,EACxB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,UAAU,EAAE,CAqJd;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAO7E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,WAAW,CAOzD;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,EAAE,EACnB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,WAAW,GAAG,IAAI,CAsCpB"}
package/build/util.js CHANGED
@@ -157,6 +157,7 @@ export function deriveTriggers(config) {
157
157
  export function nodeToFacet(node) {
158
158
  return {
159
159
  type: node.facetType,
160
+ raw: node.raw,
160
161
  value: node.value,
161
162
  range: { start: node.start, end: node.end },
162
163
  };
@@ -166,6 +167,7 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
166
167
  if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {
167
168
  return {
168
169
  type: node.facetType,
170
+ raw: node.raw,
169
171
  value: node.value,
170
172
  range: { start: node.start, end: node.end },
171
173
  };
@@ -173,6 +175,7 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
173
175
  if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
174
176
  return {
175
177
  type: node.facetType,
178
+ raw: node.raw,
176
179
  value: node.value,
177
180
  range: { start: node.start, end: node.end },
178
181
  };
@@ -186,8 +189,10 @@ export function detectActiveFacet(nodes, text, cursor, triggers) {
186
189
  break;
187
190
  const type = triggers.get(ch);
188
191
  if (type) {
192
+ const raw = text.slice(i, cursor);
189
193
  return {
190
194
  type,
195
+ raw,
191
196
  value: text.slice(i + 1, cursor),
192
197
  range: { start: i, end: cursor },
193
198
  };
package/build/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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","sourcesContent":["import {TapperFacetConfig, TapperNode, TapperFacet} from './types'\n\nconst WHITESPACE = /\\s/\n\nexport type CompiledFacetRegexes = Map<string, RegExp>\n\n/**\n * Pre-compile facet regexes once at init time. This avoids re-creating\n * RegExp objects on every keystroke in parseNodesFromText. Each Tapper\n * instance gets its own compiled copy so lastIndex state can't leak\n * between instances.\n */\nexport function compileFacetRegexes(\n config: TapperFacetConfig,\n): CompiledFacetRegexes {\n const compiled = new Map<string, RegExp>()\n for (const [name, re] of Object.entries(config)) {\n compiled.set(name, new RegExp(re.source, re.flags))\n }\n return compiled\n}\n\nexport function parseNodesFromText(\n text: string,\n regexes: CompiledFacetRegexes,\n prevNodes?: TapperNode[],\n cursor?: number,\n triggers?: Map<string, string>,\n): TapperNode[] {\n const allMatches: {\n facetName: string\n fullMatch: string\n capture: string\n index: number\n }[] = []\n\n for (const [name, re] of regexes) {\n // Reset lastIndex so stateful (global) regexes don't carry over\n // match positions from the previous parse call.\n re.lastIndex = 0\n for (const m of text.matchAll(re)) {\n allMatches.push({\n facetName: name,\n fullMatch: m[0],\n capture: m[1] ?? m[0],\n index: m.index,\n })\n }\n }\n\n allMatches.sort(\n (a, b) => a.index - b.index || b.fullMatch.length - a.fullMatch.length,\n )\n\n const accepted: typeof allMatches = []\n let lastEnd = 0\n for (const m of allMatches) {\n if (m.index >= lastEnd) {\n accepted.push(m)\n lastEnd = m.index + m.fullMatch.length\n }\n }\n\n const nodes: TapperNode[] = []\n let pos = 0\n\n for (const m of accepted) {\n if (m.index > pos) {\n const raw = text.slice(pos, m.index)\n nodes.push({\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: m.index,\n })\n }\n nodes.push({\n type: 'facet',\n facetType: m.facetName,\n raw: m.fullMatch,\n value: m.capture,\n start: m.index,\n end: m.index + m.fullMatch.length,\n })\n pos = m.index + m.fullMatch.length\n }\n\n if (pos < text.length) {\n const raw = text.slice(pos)\n nodes.push({\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: text.length,\n })\n }\n\n // If the cursor is right after a trigger char that the regex didn't match,\n // splice a 'trigger' node out of the containing text node.\n if (cursor != null && triggers) {\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n if (WHITESPACE.test(ch)) break\n const facetType = triggers.get(ch)\n if (facetType) {\n // Only create a trigger node if the trigger is inside a text node\n // (i.e. the regex didn't already match it as a facet)\n const textNodeIdx = nodes.findIndex(\n n => n.type === 'text' && n.start <= i && n.end > i,\n )\n if (textNodeIdx !== -1) {\n const node = nodes[textNodeIdx]\n const spliced: TapperNode[] = []\n if (node.start < i) {\n const raw = text.slice(node.start, i)\n spliced.push({\n type: 'text',\n raw,\n value: raw,\n start: node.start,\n end: i,\n })\n }\n const triggerRaw = text.slice(i, cursor)\n spliced.push({\n type: 'trigger',\n facetType,\n raw: triggerRaw,\n value: text.slice(i + ch.length, cursor),\n start: i,\n end: cursor,\n })\n if (cursor < node.end) {\n const raw = text.slice(cursor, node.end)\n spliced.push({\n type: 'text',\n raw,\n value: raw,\n start: cursor,\n end: node.end,\n })\n }\n nodes.splice(textNodeIdx, 1, ...spliced)\n }\n break\n }\n }\n }\n\n // Transfer committed flags from previous nodes by facetType + occurrence index\n if (prevNodes) {\n const counts = new Map<string, number>()\n const committedByTypeIndex = new Set<string>()\n\n for (const node of prevNodes) {\n if (node.type !== 'facet') continue\n const idx = counts.get(node.facetType!) ?? 0\n counts.set(node.facetType!, idx + 1)\n if (node.committed) {\n committedByTypeIndex.add(`${node.facetType}:${idx}`)\n }\n }\n\n counts.clear()\n for (const node of nodes) {\n if (node.type !== 'facet') continue\n const idx = counts.get(node.facetType!) ?? 0\n counts.set(node.facetType!, idx + 1)\n if (committedByTypeIndex.has(`${node.facetType}:${idx}`)) {\n node.committed = true\n }\n }\n }\n\n return nodes\n}\n\nexport function deriveTriggers(config: TapperFacetConfig): Map<string, string> {\n const triggers = new Map<string, string>()\n for (const [name, re] of Object.entries(config)) {\n const m = re.source.match(/^[^\\\\([\\]{}.*+?^$|]+/)\n if (m) triggers.set(m[0], name)\n }\n return triggers\n}\n\nexport function nodeToFacet(node: TapperNode): TapperFacet {\n return {\n type: node.facetType!,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n}\n\nexport function detectActiveFacet(\n nodes: TapperNode[],\n text: string,\n cursor: number,\n triggers: Map<string, string>,\n): TapperFacet | null {\n for (const node of nodes) {\n if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n }\n\n // Scan backward from cursor for a trigger char (partial facet not yet matched by regex)\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n // Stop at whitespace — triggers don't span across words\n if (WHITESPACE.test(ch)) break\n const type = triggers.get(ch)\n if (type) {\n return {\n type,\n value: text.slice(i + 1, cursor),\n range: {start: i, end: cursor},\n }\n }\n }\n\n return null\n}\n"]}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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","sourcesContent":["import {TapperFacetConfig, TapperNode, TapperFacet} from './types'\n\nconst WHITESPACE = /\\s/\n\nexport type CompiledFacetRegexes = Map<string, RegExp>\n\n/**\n * Pre-compile facet regexes once at init time. This avoids re-creating\n * RegExp objects on every keystroke in parseNodesFromText. Each Tapper\n * instance gets its own compiled copy so lastIndex state can't leak\n * between instances.\n */\nexport function compileFacetRegexes(\n config: TapperFacetConfig,\n): CompiledFacetRegexes {\n const compiled = new Map<string, RegExp>()\n for (const [name, re] of Object.entries(config)) {\n compiled.set(name, new RegExp(re.source, re.flags))\n }\n return compiled\n}\n\nexport function parseNodesFromText(\n text: string,\n regexes: CompiledFacetRegexes,\n prevNodes?: TapperNode[],\n cursor?: number,\n triggers?: Map<string, string>,\n): TapperNode[] {\n const allMatches: {\n facetName: string\n fullMatch: string\n capture: string\n index: number\n }[] = []\n\n for (const [name, re] of regexes) {\n // Reset lastIndex so stateful (global) regexes don't carry over\n // match positions from the previous parse call.\n re.lastIndex = 0\n for (const m of text.matchAll(re)) {\n allMatches.push({\n facetName: name,\n fullMatch: m[0],\n capture: m[1] ?? m[0],\n index: m.index,\n })\n }\n }\n\n allMatches.sort(\n (a, b) => a.index - b.index || b.fullMatch.length - a.fullMatch.length,\n )\n\n const accepted: typeof allMatches = []\n let lastEnd = 0\n for (const m of allMatches) {\n if (m.index >= lastEnd) {\n accepted.push(m)\n lastEnd = m.index + m.fullMatch.length\n }\n }\n\n const nodes: TapperNode[] = []\n let pos = 0\n\n for (const m of accepted) {\n if (m.index > pos) {\n const raw = text.slice(pos, m.index)\n nodes.push({\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: m.index,\n })\n }\n nodes.push({\n type: 'facet',\n facetType: m.facetName,\n raw: m.fullMatch,\n value: m.capture,\n start: m.index,\n end: m.index + m.fullMatch.length,\n })\n pos = m.index + m.fullMatch.length\n }\n\n if (pos < text.length) {\n const raw = text.slice(pos)\n nodes.push({\n type: 'text',\n raw,\n value: raw,\n start: pos,\n end: text.length,\n })\n }\n\n // If the cursor is right after a trigger char that the regex didn't match,\n // splice a 'trigger' node out of the containing text node.\n if (cursor != null && triggers) {\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n if (WHITESPACE.test(ch)) break\n const facetType = triggers.get(ch)\n if (facetType) {\n // Only create a trigger node if the trigger is inside a text node\n // (i.e. the regex didn't already match it as a facet)\n const textNodeIdx = nodes.findIndex(\n n => n.type === 'text' && n.start <= i && n.end > i,\n )\n if (textNodeIdx !== -1) {\n const node = nodes[textNodeIdx]\n const spliced: TapperNode[] = []\n if (node.start < i) {\n const raw = text.slice(node.start, i)\n spliced.push({\n type: 'text',\n raw,\n value: raw,\n start: node.start,\n end: i,\n })\n }\n const triggerRaw = text.slice(i, cursor)\n spliced.push({\n type: 'trigger',\n facetType,\n raw: triggerRaw,\n value: text.slice(i + ch.length, cursor),\n start: i,\n end: cursor,\n })\n if (cursor < node.end) {\n const raw = text.slice(cursor, node.end)\n spliced.push({\n type: 'text',\n raw,\n value: raw,\n start: cursor,\n end: node.end,\n })\n }\n nodes.splice(textNodeIdx, 1, ...spliced)\n }\n break\n }\n }\n }\n\n // Transfer committed flags from previous nodes by facetType + occurrence index\n if (prevNodes) {\n const counts = new Map<string, number>()\n const committedByTypeIndex = new Set<string>()\n\n for (const node of prevNodes) {\n if (node.type !== 'facet') continue\n const idx = counts.get(node.facetType!) ?? 0\n counts.set(node.facetType!, idx + 1)\n if (node.committed) {\n committedByTypeIndex.add(`${node.facetType}:${idx}`)\n }\n }\n\n counts.clear()\n for (const node of nodes) {\n if (node.type !== 'facet') continue\n const idx = counts.get(node.facetType!) ?? 0\n counts.set(node.facetType!, idx + 1)\n if (committedByTypeIndex.has(`${node.facetType}:${idx}`)) {\n node.committed = true\n }\n }\n }\n\n return nodes\n}\n\nexport function deriveTriggers(config: TapperFacetConfig): Map<string, string> {\n const triggers = new Map<string, string>()\n for (const [name, re] of Object.entries(config)) {\n const m = re.source.match(/^[^\\\\([\\]{}.*+?^$|]+/)\n if (m) triggers.set(m[0], name)\n }\n return triggers\n}\n\nexport function nodeToFacet(node: TapperNode): TapperFacet {\n return {\n type: node.facetType!,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n}\n\nexport function detectActiveFacet(\n nodes: TapperNode[],\n text: string,\n cursor: number,\n triggers: Map<string, string>,\n): TapperFacet | null {\n for (const node of nodes) {\n if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {\n return {\n type: node.facetType,\n raw: node.raw,\n value: node.value,\n range: {start: node.start, end: node.end},\n }\n }\n }\n\n // Scan backward from cursor for a trigger char (partial facet not yet matched by regex)\n for (let i = cursor - 1; i >= 0; i--) {\n const ch = text[i]\n // Stop at whitespace — triggers don't span across words\n if (WHITESPACE.test(ch)) break\n const type = triggers.get(ch)\n if (type) {\n const raw = text.slice(i, cursor)\n return {\n type,\n raw,\n value: text.slice(i + 1, cursor),\n range: {start: i, end: cursor},\n }\n }\n }\n\n return null\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsky.app/tapper",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "A minimal rich text editor for React Native and web.",
6
6
  "repository": "https://github.com/bluesky-social/toolbox",
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  TapperEvents,
6
6
  TapperNode,
7
7
  TapperActiveFacet,
8
+ TapperSelection,
8
9
  TapperSnapshot,
9
10
  } from './types'
10
11
  import {
@@ -30,13 +31,36 @@ export class Tapper {
30
31
 
31
32
  // Public state
32
33
  text = ''
33
- cursor = 0
34
+ selection: TapperSelection = {start: 0, end: 0}
34
35
  nodes: TapperNode[] = []
35
36
  activeFacet: TapperActiveFacet | null = null
36
37
 
37
- // Internal tracking
38
+ /*
39
+ * Guard against stale native events after programmatic text replacement.
40
+ *
41
+ * After insert() or atomic deletion, we update this.text and this.selection
42
+ * synchronously. However, iOS (especially on the old architecture bridge)
43
+ * may then fire stale onChangeText and onSelectionChange events reflecting
44
+ * the *previous* text/cursor state. These arrive within ~60ms.
45
+ *
46
+ * We record the expected textLength and a timestamp, then use two guards:
47
+ *
48
+ * 1. handleTextChange: if the incoming text length doesn't match the
49
+ * expected length AND we're within the debounce window, it's a stale
50
+ * event echoing the pre-replacement text. Skip it.
51
+ *
52
+ * 2. handleSelectionChange: if we're within the debounce window, skip
53
+ * ALL selection events. The correct selection was already set by
54
+ * update(), and any events within the window are either duplicates
55
+ * (no-ops) or stale positions from the old text.
56
+ *
57
+ * After the window passes, the guard clears itself and normal event
58
+ * handling resumes (cursor taps, typing, etc).
59
+ */
60
+ private pendingMutation: {textLength: number; timestamp: number} | null = null
61
+ private static MUTATION_DEBOUNCE_MS = 100
62
+
38
63
  private updating = false
39
- private pendingCursor: number | null = null
40
64
 
41
65
  // useSyncExternalStore plumbing
42
66
  private storeListeners = new Set<() => void>()
@@ -48,7 +72,7 @@ export class Tapper {
48
72
  this.triggers = deriveTriggers(facets)
49
73
  this.snapshot = {
50
74
  text: this.text,
51
- cursor: this.cursor,
75
+ selection: this.selection,
52
76
  nodes: this.nodes,
53
77
  activeFacet: this.activeFacet,
54
78
  }
@@ -84,18 +108,21 @@ export class Tapper {
84
108
  private notify() {
85
109
  this.snapshot = {
86
110
  text: this.text,
87
- cursor: this.cursor,
111
+ selection: this.selection,
88
112
  nodes: this.nodes,
89
113
  activeFacet: this.activeFacet,
90
114
  }
91
115
  this.storeListeners.forEach(l => l())
92
116
  }
93
117
 
94
- private update(newText: string, cursor: number) {
95
- // guard against circular updates when commit() is called during an update cycle
118
+ private update(newText: string, selection: TapperSelection) {
119
+ // guard against circular updates when insert() is called during an update cycle
96
120
  if (this.updating) return
97
121
  this.updating = true
98
122
 
123
+ const cursor = selection.end
124
+ const isRange = selection.start !== selection.end
125
+
99
126
  const nodes =
100
127
  newText === this.text
101
128
  ? this.nodes
@@ -107,7 +134,9 @@ export class Tapper {
107
134
  this.triggers,
108
135
  )
109
136
  const prev = this.activeFacet
110
- const detected = detectActiveFacet(nodes, newText, cursor, this.triggers)
137
+ const detected = isRange
138
+ ? null
139
+ : detectActiveFacet(nodes, newText, cursor, this.triggers)
111
140
 
112
141
  // When cursor leaves a facet, commit it
113
142
  let committedNode: TapperNode | null = null
@@ -126,13 +155,13 @@ export class Tapper {
126
155
  }
127
156
  }
128
157
 
129
- // Build activeFacet with commit() baked in
158
+ // Build activeFacet with insert() baked in
130
159
  const active: TapperActiveFacet | null = detected
131
160
  ? {...detected, insert: this.insert}
132
161
  : null
133
162
 
134
163
  this.text = newText
135
- this.cursor = cursor
164
+ this.selection = selection
136
165
  this.nodes = nodes
137
166
  this.activeFacet = active
138
167
 
@@ -154,44 +183,81 @@ export class Tapper {
154
183
  }
155
184
 
156
185
  handleTextChange = (newText: string) => {
186
+ if (newText === this.text) return
187
+
188
+ /*
189
+ * Guard 1: stale text. After insert/atomic deletion, iOS may echo
190
+ * onChangeText with the pre-replacement text. If the incoming length
191
+ * doesn't match what we expect and we're within the debounce window,
192
+ * this is a stale echo — skip it.
193
+ */
194
+ if (this.pendingMutation !== null) {
195
+ const elapsed = Date.now() - this.pendingMutation.timestamp
196
+ if (
197
+ elapsed < Tapper.MUTATION_DEBOUNCE_MS &&
198
+ newText.length !== this.pendingMutation.textLength
199
+ ) {
200
+ return
201
+ }
202
+ }
203
+
157
204
  const diff = newText.length - this.text.length
158
- let newCursor = Math.max(this.cursor + diff, 0)
205
+ let newEnd = Math.max(this.selection.end + diff, 0)
159
206
 
160
207
  // Atomic deletion: backspace into a facet from outside
161
- if (diff === -1 && newCursor < this.cursor) {
208
+ if (diff === -1 && newEnd < this.selection.end) {
162
209
  for (const node of this.nodes) {
163
210
  if (
164
211
  node.type === 'facet' &&
165
212
  node.committed &&
166
- this.cursor === node.end
213
+ this.selection.end === node.end
167
214
  ) {
168
215
  const remnant = node.end - node.start - 1
169
216
  newText =
170
217
  newText.slice(0, node.start) + newText.slice(node.start + remnant)
171
- newCursor = node.start
172
- this.pendingCursor = newCursor
218
+ newEnd = node.start
219
+ this.pendingMutation = {
220
+ textLength: newText.length,
221
+ timestamp: Date.now(),
222
+ }
173
223
  break
174
224
  }
175
225
  }
176
226
  }
177
227
 
178
- this.update(newText, newCursor)
228
+ this.update(newText, {start: newEnd, end: newEnd})
179
229
  }
180
230
 
181
- handleSelectionChange = (e: {nativeEvent: {selection: {start: number}}}) => {
182
- const newCursor = e.nativeEvent.selection.start
183
- // After atomic deletion, ignore stale cursor values from the DOM
184
- if (this.pendingCursor !== null) {
185
- if (newCursor !== this.pendingCursor) {
186
- return
187
- }
188
- this.pendingCursor = null
231
+ handleSelectionChange = (e: {
232
+ nativeEvent: {selection: {start: number; end: number}}
233
+ }) => {
234
+ const {start, end} = e.nativeEvent.selection
235
+
236
+ /*
237
+ * Guard 2: stale selection. After insert/atomic deletion, iOS may fire
238
+ * onSelectionChange with cursor positions from the old text state
239
+ * (e.g. a "deselection" event at the pre-insert cursor). Within the
240
+ * debounce window, skip ALL selection events — the correct selection
241
+ * was already set synchronously by update().
242
+ */
243
+ if (this.pendingMutation !== null) {
244
+ const elapsed = Date.now() - this.pendingMutation.timestamp
245
+ if (elapsed < Tapper.MUTATION_DEBOUNCE_MS) return
246
+ this.pendingMutation = null
189
247
  }
190
248
 
191
- if (newCursor === this.cursor) return
249
+ /*
250
+ * Guard 3: premature selection. iOS sometimes fires onSelectionChange
251
+ * before onChangeText for the same edit, reporting a cursor position
252
+ * that's beyond the current text length. Skip it — handleTextChange
253
+ * will set the correct position when it arrives.
254
+ */
255
+ if (end > this.text.length) return
192
256
 
193
- this.cursor = newCursor
194
- this.update(this.text, newCursor)
257
+ if (start === this.selection.start && end === this.selection.end) return
258
+
259
+ this.selection = {start, end}
260
+ this.update(this.text, {start, end})
195
261
  }
196
262
 
197
263
  replaceText = (text: string, cursor?: number) => {
@@ -213,7 +279,7 @@ export class Tapper {
213
279
  : null
214
280
 
215
281
  this.text = text
216
- this.cursor = pos
282
+ this.selection = {start: pos, end: pos}
217
283
  this.nodes = nodes
218
284
  this.activeFacet = active
219
285
 
@@ -235,9 +301,10 @@ export class Tapper {
235
301
  this.text.slice(0, this.activeFacet.range.start) +
236
302
  replacement +
237
303
  this.text.slice(this.activeFacet.range.end)
238
- const newCursor = this.activeFacet.range.start + replacement.length
304
+ const newEnd = this.activeFacet.range.start + replacement.length
239
305
 
240
306
  if (this.activeFacet) {
307
+ this.activeFacet.raw = value
241
308
  this.activeFacet.value = value
242
309
  this.activeFacet.range = {
243
310
  start: this.activeFacet.range.start,
@@ -246,7 +313,11 @@ export class Tapper {
246
313
  this.emit('afterInsert', this.activeFacet)
247
314
  }
248
315
 
249
- this.update(newText, newCursor)
316
+ this.pendingMutation = {
317
+ textLength: newText.length,
318
+ timestamp: Date.now(),
319
+ }
320
+ this.update(newText, {start: newEnd, end: newEnd})
250
321
  }
251
322
  }
252
323
 
@@ -262,7 +333,7 @@ export function useTapper(config: TapperConfig) {
262
333
  inputProps: {
263
334
  ref: inputRef as React.RefObject<any>,
264
335
  value: state.text,
265
- selection: {start: state.cursor, end: state.cursor},
336
+ selection: state.selection,
266
337
  onChangeText: store.handleTextChange,
267
338
  onSelectionChange: store.handleSelectionChange,
268
339
  },
package/src/types.ts CHANGED
@@ -31,6 +31,7 @@ export type TapperNode =
31
31
 
32
32
  export type TapperFacet = {
33
33
  type: string
34
+ raw: string
34
35
  value: string
35
36
  range: {start: number; end: number}
36
37
  }
@@ -39,9 +40,11 @@ export type TapperActiveFacet = TapperFacet & {
39
40
  insert: (value: string, options?: {noTrailingSpace?: boolean}) => void
40
41
  }
41
42
 
43
+ export type TapperSelection = {start: number; end: number}
44
+
42
45
  export type TapperSnapshot = {
43
46
  text: string
44
- cursor: number
47
+ selection: TapperSelection
45
48
  nodes: TapperNode[]
46
49
  activeFacet: TapperActiveFacet | null
47
50
  }
package/src/util.ts CHANGED
@@ -189,6 +189,7 @@ export function deriveTriggers(config: TapperFacetConfig): Map<string, string> {
189
189
  export function nodeToFacet(node: TapperNode): TapperFacet {
190
190
  return {
191
191
  type: node.facetType!,
192
+ raw: node.raw,
192
193
  value: node.value,
193
194
  range: {start: node.start, end: node.end},
194
195
  }
@@ -204,6 +205,7 @@ export function detectActiveFacet(
204
205
  if (node.type === 'trigger' && node.start < cursor && cursor <= node.end) {
205
206
  return {
206
207
  type: node.facetType,
208
+ raw: node.raw,
207
209
  value: node.value,
208
210
  range: {start: node.start, end: node.end},
209
211
  }
@@ -211,6 +213,7 @@ export function detectActiveFacet(
211
213
  if (node.type === 'facet' && node.start < cursor && cursor <= node.end) {
212
214
  return {
213
215
  type: node.facetType,
216
+ raw: node.raw,
214
217
  value: node.value,
215
218
  range: {start: node.start, end: node.end},
216
219
  }
@@ -224,8 +227,10 @@ export function detectActiveFacet(
224
227
  if (WHITESPACE.test(ch)) break
225
228
  const type = triggers.get(ch)
226
229
  if (type) {
230
+ const raw = text.slice(i, cursor)
227
231
  return {
228
232
  type,
233
+ raw,
229
234
  value: text.slice(i + 1, cursor),
230
235
  range: {start: i, end: cursor},
231
236
  }