@bsky.app/tapper 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +24 -12
- package/build/index.js.map +1 -1
- package/eslint.config.js +4 -0
- package/package.json +2 -1
- package/src/index.ts +33 -15
- package/vitest.config.ts +6 -0
- package/.eslintrc.js +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @bsky.app/tapper
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#30](https://github.com/bluesky-social/toolbox/pull/30) [`846da1c`](https://github.com/bluesky-social/toolbox/commit/846da1c734113a058fac16693c0936d2d74b621b) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Do not atomic-delete URL facets
|
|
8
|
+
|
|
9
|
+
## 0.5.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`0a98094`](https://github.com/bluesky-social/toolbox/commit/0a98094abc42b1a6eaed0460c57365a9af7cd510) Thanks [@estrattonbailey](https://github.com/estrattonbailey)! - Commit facets on paste
|
|
14
|
+
|
|
3
15
|
## 0.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,SAAS,EAAC,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,SAAS,EAAC,MAAM,cAAc,CAAA;AAGhD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,iBAAiB,EACjB,eAAe,EACf,cAAc,EACf,MAAM,SAAS,CAAA;AAUhB,cAAc,SAAS,CAAA;AACvB,OAAO,KAAK,MAAM,MAAM,UAAU,CAAA;AAIlC,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;IAE5C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,QAAQ,CAAQ;IAGxB,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,MAAM,CAAC,EAAE,YAAY;IAejC,WAAW,GAAI,MAAM,SAAS,GAAG,IAAI,UAEpC;IAED,OAAO,CAAC,iBAAiB;IAUzB,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;IAgFd,gBAAgB,GAAI,SAAS,MAAM,UAkClC;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,UA+BA;IAED,WAAW,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,UA+B3C;IAED,OAAO,CAAC,OAAO,CAsBd;IAED,MAAM,GAAI,MAAM,MAAM,UAMrB;CACF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY;;SAlPtC,CAAC,SAAS,MAAM,YAAY,2CAED,IAAI;mBAuOrB,MAAM;;;;;;;oBAgBb,SAAS,GAAG,IAAI;;gCAhJI,MAAM;+BAoCP;YAC1B,WAAW,EAAE;gBAAC,SAAS,EAAE;oBAAC,KAAK,EAAE,MAAM,CAAC;oBAAC,GAAG,EAAE,MAAM,CAAA;iBAAC,CAAA;aAAC,CAAA;SACvD;;EAoIF"}
|
package/build/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
2
|
import { Platform } from 'react-native';
|
|
3
|
-
import { parseNodesFromText, detectActiveFacet, deriveTriggers, nodeToFacet, compileFacetRegexes, } from './util';
|
|
4
3
|
import * as defaultFacets from './facets';
|
|
4
|
+
import { parseNodesFromText, detectActiveFacet, deriveTriggers, nodeToFacet, compileFacetRegexes, } from './util';
|
|
5
5
|
export * from './types';
|
|
6
6
|
export * as facets from './facets';
|
|
7
7
|
const IS_WEB = Platform.OS === 'web';
|
|
@@ -9,7 +9,6 @@ export class Tapper {
|
|
|
9
9
|
facetRegexes;
|
|
10
10
|
triggers;
|
|
11
11
|
// Event emitters
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
12
|
listeners = new Map();
|
|
14
13
|
// Public state
|
|
15
14
|
text = '';
|
|
@@ -76,7 +75,7 @@ export class Tapper {
|
|
|
76
75
|
};
|
|
77
76
|
this.storeListeners.forEach(l => l());
|
|
78
77
|
}
|
|
79
|
-
update(newText, selection) {
|
|
78
|
+
update(newText, selection, commitAll) {
|
|
80
79
|
// guard against circular updates when insert() is called during an update cycle
|
|
81
80
|
if (this.updating)
|
|
82
81
|
return;
|
|
@@ -87,19 +86,28 @@ export class Tapper {
|
|
|
87
86
|
? this.nodes
|
|
88
87
|
: parseNodesFromText(newText, this.facetRegexes, this.nodes, cursor, this.triggers);
|
|
89
88
|
const prev = this.activeFacet;
|
|
90
|
-
const detected = isRange
|
|
89
|
+
const detected = isRange || commitAll
|
|
91
90
|
? null
|
|
92
91
|
: detectActiveFacet(nodes, newText, cursor, this.triggers);
|
|
93
|
-
// When cursor leaves a facet, commit it
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
// When cursor leaves a facet, commit it.
|
|
93
|
+
// On paste (commitAll), commit every uncommitted facet.
|
|
94
|
+
const committedNodes = [];
|
|
95
|
+
if (commitAll) {
|
|
96
|
+
for (const node of nodes) {
|
|
97
|
+
if (node.type === 'facet' && !node.committed) {
|
|
98
|
+
node.committed = true;
|
|
99
|
+
committedNodes.push(node);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (prev && (!detected || detected.type !== prev.type)) {
|
|
96
104
|
for (const node of nodes) {
|
|
97
105
|
if (node.type === 'facet' &&
|
|
98
106
|
node.facetType === prev.type &&
|
|
99
107
|
node.start === prev.range.start &&
|
|
100
108
|
node.end === prev.range.end) {
|
|
101
109
|
node.committed = true;
|
|
102
|
-
|
|
110
|
+
committedNodes.push(node);
|
|
103
111
|
break;
|
|
104
112
|
}
|
|
105
113
|
}
|
|
@@ -122,8 +130,8 @@ export class Tapper {
|
|
|
122
130
|
if (facetChanged) {
|
|
123
131
|
this.emit('activeFacet', active);
|
|
124
132
|
}
|
|
125
|
-
|
|
126
|
-
this.emit('facetCommitted', nodeToFacet(
|
|
133
|
+
for (const node of committedNodes) {
|
|
134
|
+
this.emit('facetCommitted', nodeToFacet(node));
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
handleTextChange = (newText) => {
|
|
@@ -131,11 +139,13 @@ export class Tapper {
|
|
|
131
139
|
return;
|
|
132
140
|
const diff = newText.length - this.text.length;
|
|
133
141
|
let newEnd = Math.max(this.selection.end + diff, 0);
|
|
134
|
-
// Atomic deletion: backspace into a facet from outside
|
|
142
|
+
// Atomic deletion: backspace into a facet from outside.
|
|
143
|
+
// URLs are excluded — users typically want to edit them character by character.
|
|
135
144
|
let didAtomicDelete = false;
|
|
136
145
|
if (diff === -1 && newEnd < this.selection.end) {
|
|
137
146
|
for (const node of this.nodes) {
|
|
138
147
|
if (node.type === 'facet' &&
|
|
148
|
+
node.facetType !== 'url' &&
|
|
139
149
|
node.committed &&
|
|
140
150
|
this.selection.end === node.end) {
|
|
141
151
|
const remnant = node.end - node.start - 1;
|
|
@@ -147,7 +157,9 @@ export class Tapper {
|
|
|
147
157
|
}
|
|
148
158
|
}
|
|
149
159
|
}
|
|
150
|
-
|
|
160
|
+
// Paste: commit all facets immediately
|
|
161
|
+
const mayBePaste = diff > 1;
|
|
162
|
+
this.update(newText, { start: newEnd, end: newEnd }, mayBePaste);
|
|
151
163
|
if (didAtomicDelete) {
|
|
152
164
|
this.setInputSelection(newEnd, newEnd);
|
|
153
165
|
}
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {useCallback, useRef, useState, useSyncExternalStore} from 'react'\nimport {Platform, TextInput} from 'react-native'\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 * as facets from './facets'\n\nconst IS_WEB = Platform.OS === 'web'\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 private inputRef: TextInput | null = null\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 setInputRef = (node: TextInput | null) => {\n this.inputRef = node\n }\n\n private setInputSelection(start: number, end: number) {\n const el = this.inputRef as any\n if (!el) return\n if (typeof el.setSelection === 'function') {\n el.setSelection(start, end)\n } else if (typeof el.setSelectionRange === 'function') {\n el.setSelectionRange(start, end)\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, replace: this.replace}\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 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 let didAtomicDelete = false\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 didAtomicDelete = true\n break\n }\n }\n }\n\n this.update(newText, {start: newEnd, end: newEnd})\n if (didAtomicDelete) {\n this.setInputSelection(newEnd, newEnd)\n }\n }\n\n handleSelectionChange = (e: {\n nativeEvent: {selection: {start: number; end: number}}\n }) => {\n const {start, end} = e.nativeEvent.selection\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\n /*\n * For selection-only changes (no text change), check if the active facet\n * would change. If not, skip the full update() cycle — just update the\n * selection in the snapshot.\n */\n const isRange = start !== end\n const detected = isRange\n ? null\n : detectActiveFacet(this.nodes, this.text, end, this.triggers)\n const prev = this.activeFacet\n const facetChanged =\n detected?.type !== prev?.type ||\n detected?.value !== prev?.value ||\n detected?.range.start !== prev?.range.start ||\n detected?.range.end !== prev?.range.end\n if (!facetChanged) {\n this.snapshot = {...this.snapshot, selection: this.selection}\n this.storeListeners.forEach(l => l())\n return\n }\n\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, replace: this.replace}\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 replace = (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.update(newText, {start: newEnd, end: newEnd})\n this.setInputSelection(newEnd, newEnd)\n }\n\n insert = (text: string) => {\n const pos = this.selection.end\n const newText = this.text.slice(0, pos) + text + this.text.slice(pos)\n const newEnd = pos + text.length\n this.update(newText, {start: newEnd, end: newEnd})\n this.setInputSelection(newEnd, 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 [input, setInput] = useState<TextInput | null>(null)\n const inputRef = useRef<{focus(): void}>(null)\n\n const handleSetInput = useCallback(\n (node: TextInput | null) => {\n inputRef.current = node\n store.setInputRef(node)\n setInput(node)\n },\n [store],\n )\n const focus = useCallback(() => input?.focus(), [input])\n const blur = useCallback(() => input?.blur(), [input])\n\n return {\n state,\n on: store.on,\n insert: store.insert,\n input: {\n element: input,\n focus,\n blur,\n },\n inputProps: {\n ref: handleSetInput,\n value: IS_WEB ? state.text : undefined,\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 {useCallback, useRef, useState, useSyncExternalStore} from 'react'\nimport {Platform, TextInput} from 'react-native'\n\nimport * as defaultFacets from './facets'\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'\n\nexport * from './types'\nexport * as facets from './facets'\n\nconst IS_WEB = Platform.OS === 'web'\n\nexport class Tapper {\n private facetRegexes: CompiledFacetRegexes\n private triggers: Map<string, string>\n\n // Event emitters\n\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 private inputRef: TextInput | null = null\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 setInputRef = (node: TextInput | null) => {\n this.inputRef = node\n }\n\n private setInputSelection(start: number, end: number) {\n const el = this.inputRef as any\n if (!el) return\n if (typeof el.setSelection === 'function') {\n el.setSelection(start, end)\n } else if (typeof el.setSelectionRange === 'function') {\n el.setSelectionRange(start, end)\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(\n newText: string,\n selection: TapperSelection,\n commitAll?: boolean,\n ) {\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 =\n isRange || commitAll\n ? null\n : detectActiveFacet(nodes, newText, cursor, this.triggers)\n\n // When cursor leaves a facet, commit it.\n // On paste (commitAll), commit every uncommitted facet.\n const committedNodes: TapperNode[] = []\n if (commitAll) {\n for (const node of nodes) {\n if (node.type === 'facet' && !node.committed) {\n node.committed = true\n committedNodes.push(node)\n }\n }\n } else 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 committedNodes.push(node)\n break\n }\n }\n }\n\n // Build activeFacet with insert() baked in\n const active: TapperActiveFacet | null = detected\n ? {...detected, replace: this.replace}\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 for (const node of committedNodes) {\n this.emit('facetCommitted', nodeToFacet(node))\n }\n }\n\n handleTextChange = (newText: string) => {\n if (newText === this.text) return\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 // URLs are excluded — users typically want to edit them character by character.\n let didAtomicDelete = false\n if (diff === -1 && newEnd < this.selection.end) {\n for (const node of this.nodes) {\n if (\n node.type === 'facet' &&\n node.facetType !== 'url' &&\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 didAtomicDelete = true\n break\n }\n }\n }\n\n // Paste: commit all facets immediately\n const mayBePaste = diff > 1\n\n this.update(newText, {start: newEnd, end: newEnd}, mayBePaste)\n if (didAtomicDelete) {\n this.setInputSelection(newEnd, newEnd)\n }\n }\n\n handleSelectionChange = (e: {\n nativeEvent: {selection: {start: number; end: number}}\n }) => {\n const {start, end} = e.nativeEvent.selection\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\n /*\n * For selection-only changes (no text change), check if the active facet\n * would change. If not, skip the full update() cycle — just update the\n * selection in the snapshot.\n */\n const isRange = start !== end\n const detected = isRange\n ? null\n : detectActiveFacet(this.nodes, this.text, end, this.triggers)\n const prev = this.activeFacet\n const facetChanged =\n detected?.type !== prev?.type ||\n detected?.value !== prev?.value ||\n detected?.range.start !== prev?.range.start ||\n detected?.range.end !== prev?.range.end\n if (!facetChanged) {\n this.snapshot = {...this.snapshot, selection: this.selection}\n this.storeListeners.forEach(l => l())\n return\n }\n\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, replace: this.replace}\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 replace = (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.update(newText, {start: newEnd, end: newEnd})\n this.setInputSelection(newEnd, newEnd)\n }\n\n insert = (text: string) => {\n const pos = this.selection.end\n const newText = this.text.slice(0, pos) + text + this.text.slice(pos)\n const newEnd = pos + text.length\n this.update(newText, {start: newEnd, end: newEnd})\n this.setInputSelection(newEnd, 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 [input, setInput] = useState<TextInput | null>(null)\n const inputRef = useRef<{focus(): void}>(null)\n\n const handleSetInput = useCallback(\n (node: TextInput | null) => {\n inputRef.current = node\n store.setInputRef(node)\n setInput(node)\n },\n [store],\n )\n const focus = useCallback(() => input?.focus(), [input])\n const blur = useCallback(() => input?.blur(), [input])\n\n return {\n state,\n on: store.on,\n insert: store.insert,\n input: {\n element: input,\n focus,\n blur,\n },\n inputProps: {\n ref: handleSetInput,\n value: IS_WEB ? state.text : undefined,\n onChangeText: store.handleTextChange,\n onSelectionChange: store.handleSelectionChange,\n },\n }\n}\n"]}
|
package/eslint.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsky.app/tapper",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"build": "expo-module clean && expo-module build",
|
|
32
32
|
"clean": "expo-module clean",
|
|
33
33
|
"lint": "expo-module lint",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
34
35
|
"test": "vitest run",
|
|
35
36
|
"expo-module": "expo-module",
|
|
36
37
|
"open:ios": "xed example/ios",
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {useCallback, useRef, useState, useSyncExternalStore} from 'react'
|
|
2
2
|
import {Platform, TextInput} from 'react-native'
|
|
3
3
|
|
|
4
|
+
import * as defaultFacets from './facets'
|
|
4
5
|
import {
|
|
5
6
|
TapperConfig,
|
|
6
7
|
TapperEvents,
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
compileFacetRegexes,
|
|
18
19
|
type CompiledFacetRegexes,
|
|
19
20
|
} from './util'
|
|
20
|
-
import * as defaultFacets from './facets'
|
|
21
21
|
|
|
22
22
|
export * from './types'
|
|
23
23
|
export * as facets from './facets'
|
|
@@ -29,7 +29,7 @@ export class Tapper {
|
|
|
29
29
|
private triggers: Map<string, string>
|
|
30
30
|
|
|
31
31
|
// Event emitters
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
private listeners = new Map<keyof TapperEvents, Set<(data: any) => void>>()
|
|
34
34
|
|
|
35
35
|
// Public state
|
|
@@ -108,7 +108,11 @@ export class Tapper {
|
|
|
108
108
|
this.storeListeners.forEach(l => l())
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
private update(
|
|
111
|
+
private update(
|
|
112
|
+
newText: string,
|
|
113
|
+
selection: TapperSelection,
|
|
114
|
+
commitAll?: boolean,
|
|
115
|
+
) {
|
|
112
116
|
// guard against circular updates when insert() is called during an update cycle
|
|
113
117
|
if (this.updating) return
|
|
114
118
|
this.updating = true
|
|
@@ -127,13 +131,22 @@ export class Tapper {
|
|
|
127
131
|
this.triggers,
|
|
128
132
|
)
|
|
129
133
|
const prev = this.activeFacet
|
|
130
|
-
const detected =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
const detected =
|
|
135
|
+
isRange || commitAll
|
|
136
|
+
? null
|
|
137
|
+
: detectActiveFacet(nodes, newText, cursor, this.triggers)
|
|
138
|
+
|
|
139
|
+
// When cursor leaves a facet, commit it.
|
|
140
|
+
// On paste (commitAll), commit every uncommitted facet.
|
|
141
|
+
const committedNodes: TapperNode[] = []
|
|
142
|
+
if (commitAll) {
|
|
143
|
+
for (const node of nodes) {
|
|
144
|
+
if (node.type === 'facet' && !node.committed) {
|
|
145
|
+
node.committed = true
|
|
146
|
+
committedNodes.push(node)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else if (prev && (!detected || detected.type !== prev.type)) {
|
|
137
150
|
for (const node of nodes) {
|
|
138
151
|
if (
|
|
139
152
|
node.type === 'facet' &&
|
|
@@ -142,7 +155,7 @@ export class Tapper {
|
|
|
142
155
|
node.end === prev.range.end
|
|
143
156
|
) {
|
|
144
157
|
node.committed = true
|
|
145
|
-
|
|
158
|
+
committedNodes.push(node)
|
|
146
159
|
break
|
|
147
160
|
}
|
|
148
161
|
}
|
|
@@ -170,8 +183,8 @@ export class Tapper {
|
|
|
170
183
|
if (facetChanged) {
|
|
171
184
|
this.emit('activeFacet', active)
|
|
172
185
|
}
|
|
173
|
-
|
|
174
|
-
this.emit('facetCommitted', nodeToFacet(
|
|
186
|
+
for (const node of committedNodes) {
|
|
187
|
+
this.emit('facetCommitted', nodeToFacet(node))
|
|
175
188
|
}
|
|
176
189
|
}
|
|
177
190
|
|
|
@@ -181,12 +194,14 @@ export class Tapper {
|
|
|
181
194
|
const diff = newText.length - this.text.length
|
|
182
195
|
let newEnd = Math.max(this.selection.end + diff, 0)
|
|
183
196
|
|
|
184
|
-
// Atomic deletion: backspace into a facet from outside
|
|
197
|
+
// Atomic deletion: backspace into a facet from outside.
|
|
198
|
+
// URLs are excluded — users typically want to edit them character by character.
|
|
185
199
|
let didAtomicDelete = false
|
|
186
200
|
if (diff === -1 && newEnd < this.selection.end) {
|
|
187
201
|
for (const node of this.nodes) {
|
|
188
202
|
if (
|
|
189
203
|
node.type === 'facet' &&
|
|
204
|
+
node.facetType !== 'url' &&
|
|
190
205
|
node.committed &&
|
|
191
206
|
this.selection.end === node.end
|
|
192
207
|
) {
|
|
@@ -200,7 +215,10 @@ export class Tapper {
|
|
|
200
215
|
}
|
|
201
216
|
}
|
|
202
217
|
|
|
203
|
-
|
|
218
|
+
// Paste: commit all facets immediately
|
|
219
|
+
const mayBePaste = diff > 1
|
|
220
|
+
|
|
221
|
+
this.update(newText, {start: newEnd, end: newEnd}, mayBePaste)
|
|
204
222
|
if (didAtomicDelete) {
|
|
205
223
|
this.setInputSelection(newEnd, newEnd)
|
|
206
224
|
}
|
package/vitest.config.ts
CHANGED
|
@@ -3,5 +3,11 @@ import {defineConfig} from 'vitest/config'
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
test: {
|
|
5
5
|
include: ['src/__tests__/**/*.test.ts'],
|
|
6
|
+
alias: {
|
|
7
|
+
'react-native': new URL(
|
|
8
|
+
'src/__tests__/__mocks__/react-native.ts',
|
|
9
|
+
import.meta.url,
|
|
10
|
+
).pathname,
|
|
11
|
+
},
|
|
6
12
|
},
|
|
7
13
|
})
|