From 61be6f53137d44d2d1ae910b24f9f7260568f559 Mon Sep 17 00:00:00 2001 From: JoshuaPariona Date: Wed, 23 Sep 2026 15:00:26 -0500 Subject: [PATCH 1/7] fix: load the package in Expo Go with an optional native module --- CHANGELOG.md | 8 ++++++++ src/SelectableTextModule.ts | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c701894..ed2c04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- The native module is now optional, so importing the package no longer throws + where that module is not in the binary — Expo Go and Snack among them, which + already bundle `react-native-webview`. `SelectableTextView` never reads it, + so the component works there; the default export is `null` in those + environments. + ## [1.0.0] - 2026-09-14 Initial release. diff --git a/src/SelectableTextModule.ts b/src/SelectableTextModule.ts index b983539..cf23055 100644 --- a/src/SelectableTextModule.ts +++ b/src/SelectableTextModule.ts @@ -1,9 +1,12 @@ -import { NativeModule, requireNativeModule } from "expo"; +import { NativeModule, requireOptionalNativeModule } from "expo"; declare class MajornutcrackerReactNativeSelectableTextModule extends NativeModule { version: string; } -export default requireNativeModule( +// Optional so the package still loads where the native module is not in the +// binary — Expo Go and Snack, which already bundle `react-native-webview`. +// `SelectableTextView` never reads it, so the component works there too. +export default requireOptionalNativeModule( "MajornutcrackerReactNativeSelectableText" ); From 914e8dc50de676d71716c2ba48fff17e9ad99c7f Mon Sep 17 00:00:00 2001 From: JoshuaPariona Date: Wed, 23 Sep 2026 15:03:39 -0500 Subject: [PATCH 2/7] docs: lead the package description with what the module does --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d059fa..1d86938 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@majornutcracker/react-native-selectable-text", "version": "1.0.0", - "description": "A Majornutcracker Expo module for react-native-webview that enables advanced text selection, custom menus, and persistent highlighting via Rangy. Serialize, sync, and restore HTML content selections.", + "description": "Text selection and persistent, animated highlights for HTML in React Native and Expo (iOS & Android). Serialize, restore and sync highlights. Built on react-native-webview and Rangy.", "main": "build/index.js", "types": "build/index.d.ts", "type": "module", From 15131aa5873cbde5e94c13c6310cdf8d3344741e Mon Sep 17 00:00:00 2001 From: JoshuaPariona Date: Wed, 23 Sep 2026 15:04:28 -0500 Subject: [PATCH 3/7] docs: retarget the npm keywords at how people search --- package.json | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 1d86938..c027262 100644 --- a/package.json +++ b/package.json @@ -39,21 +39,24 @@ "keywords": [ "react-native", "expo", - "@majornutcracker/react-native-selectable-text", - "Majornutcracker", - "selectable text", - "text selection", - "rangy", - "highlighting", - "serialization", - "synchronization", - "restoration", - "native module", - "expo module", + "expo-module", "react-native-webview", - "text selection library", - "react-native text selection", - "expo text selection", + "webview", + "html", + "text-selection", + "selectable-text", + "selection", + "highlight", + "highlights", + "highlighter", + "text-highlight", + "annotation", + "annotations", + "ebook", + "reader", + "notes", + "bookmark", + "rangy", "ios", "android" ], From 8550e75423d66ce7d46e31d3c3f3a6e6f3fd8e6f Mon Sep 17 00:00:00 2001 From: JoshuaPariona Date: Wed, 23 Sep 2026 15:07:17 -0500 Subject: [PATCH 4/7] docs: document building your own selection menu --- docs/REFERENCE.md | 74 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 90b65b7..c9dc690 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -9,16 +9,16 @@ This page is the full surface. For a quick start see the ## Props -| Prop | Description | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `content` | The HTML string rendered inside the WebView. Changing it does **not** re-render — remount the component to show new content. | -| `css` | Injected styles for layout and typography. Declared after the generated highlighter classes, so your rules win on equal specificity and can restyle or re-animate a highlight. | -| `fonts` | WebView font setup via `googleFonts()`, `mergeFonts()`, or custom `preconnect`, `stylesheets`, and `@font-face` rules. Multiple families are supported in a single config. | -| `highlighters` | Named highlight classes. A name must be a valid CSS class name — letters, digits, `-` and `_`, not starting with a digit — and invalid names are dropped with a console warning. | -| `highlights` | **State prop.** Serialized highlights to restore. `undefined` leaves the current highlights untouched; an empty string clears them. Obtain the value from `getHighlights()` or `onHighlightsChange`. A value this view just emitted is ignored, so it is safe to control. | -| `highlighterOptions` | `ignoredElements` — tags or selectors such as `a`, `sup`, `.ignored`. Ignored nodes skip the visible highlight but stay selectable and copyable. | -| `options` | Viewport zoom: `userScalable`, `initialScale`, `maximumScale`. | -| `webViewProps` | Pass-through to `react-native-webview`. `javaScriptEnabled`, `source`, and `onShouldStartLoadWithRequest` are owned by the component and cannot be overridden. | +| Prop | Description | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `content` | The HTML string rendered inside the WebView. Changing it does **not** re-render — remount the component to show new content. | +| `css` | Injected styles for layout and typography. Declared after the generated highlighter classes, so your rules win on equal specificity and can restyle or re-animate a highlight. | +| `fonts` | WebView font setup via `googleFonts()`, `mergeFonts()`, or custom `preconnect`, `stylesheets`, and `@font-face` rules. Multiple families are supported in a single config. | +| `highlighters` | Named highlight classes. A name must be a valid CSS class name — letters, digits, `-` and `_`, not starting with a digit — and invalid names are dropped with a console warning. | +| `highlights` | **State prop.** Serialized highlights to restore. `undefined` leaves the current highlights untouched; an empty string clears them. Obtain the value from `getHighlights()` or `onHighlightsChange`. A value this view just emitted is ignored, so it is safe to control. | +| `highlighterOptions` | `ignoredElements` — tags or selectors such as `a`, `sup`, `.ignored`. Ignored nodes skip the visible highlight but stay selectable and copyable. | +| `options` | Viewport zoom: `userScalable`, `initialScale`, `maximumScale`. | +| `webViewProps` | Pass-through to `react-native-webview`. `javaScriptEnabled`, `source`, and `onShouldStartLoadWithRequest` are owned by the component and cannot be overridden. `menuItems` and `onCustomMenuSelection` pass through untouched — see [Your own selection menu](#your-own-selection-menu). | ## Callbacks @@ -220,6 +220,60 @@ accepts its own), and reject with `"Component unmounted"` if the view goes away while a call is in flight. Calls made before the WebView finishes loading are queued and flushed on load rather than dropped. +## Your own selection menu + +The menu that pops up over a selection is the WebView's, and +`react-native-webview` already lets you replace its items. `webViewProps` passes +`menuItems` and `onCustomMenuSelection` straight through, so the menu stays +native on both platforms and the ref decides what each item does. + +```tsx +const ref = React.useRef(null); + + { + switch (event.nativeEvent.key) { + case "highlight": + ref.current?.highlightSelection("yellow"); + break; + case "unhighlight": + ref.current?.unhighlightSelection(); + break; + case "copy": + Clipboard.setString(event.nativeEvent.selectedText); + break; + } + }, + }} +/>; +``` + +The ref methods act on the **cached** selection, so the handler does not have to +hand the text back; `event.nativeEvent.selectedText` is there for the cases that +need the string itself — copying, sharing, translating, or asking a server +whether the passage may be highlighted. + +An empty `menuItems` array suppresses the menu entirely, and `suppressMenuItems` +(iOS) drops individual system items such as `lookup` or `share`. + +The selection is dropped once the action completes, which dismisses the iOS +callout so a highlight's entrance animation is visible; pass +`{ keepSelection: true }` to chain another action on the same text — see +[`SelectionActionOptions`](#selectionactionoptions). + +[`example/src/screens/Reader.tsx`](https://github.com/majornutcracker-dev/react-native-selectable-text/blob/main/example/src/screens/Reader.tsx) +wires this up with a validated variant as well, and lets you pick which +highlighter the menu applies. + ## Highlighters Each `Highlighter` has a unique `name` — used as the class name of the element From a3b9309fa47074fc9f9345bb62ffc97b36cd949c Mon Sep 17 00:00:00 2001 From: JoshuaPariona Date: Wed, 23 Sep 2026 15:08:44 -0500 Subject: [PATCH 5/7] docs: open the readme with badges and a fit table --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 04a5162..a1e5f28 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,28 @@ # @majornutcracker/react-native-selectable-text +[![npm](https://img.shields.io/npm/v/@majornutcracker/react-native-selectable-text.svg)](https://www.npmjs.com/package/@majornutcracker/react-native-selectable-text) +[![downloads](https://img.shields.io/npm/dw/@majornutcracker/react-native-selectable-text.svg)](https://www.npmjs.com/package/@majornutcracker/react-native-selectable-text) +[![license](https://img.shields.io/npm/l/@majornutcracker/react-native-selectable-text.svg)](./LICENSE) +![platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20Android-lightgrey.svg) + Expo module for **iOS and Android** built on `react-native-webview`. It renders HTML with advanced text selection, custom context menus, and persistent highlighting via [Rangy](https://github.com/timdown/rangy). Serialize, sync, restore selections and more Web is not supported. +## Is this the right library? + +| You need | This module | +| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Highlights that survive a remount, a restart, or a new device | **Yes** — serialize them, store the string, pass it back | +| Rich content: articles, chapters, anything already HTML | **Yes** — you hand it HTML and CSS | +| Your own selection menu | **Yes** — [native menu items](./docs/REFERENCE.md#your-own-selection-menu), your actions | +| A tap target on each highlight, with its position | **Yes** — `onHighlightPressed` reports the rects | +| Selection on a native `` tree | No — this renders a WebView | +| Web support | No — iOS and Android only | +| Selection or highlights inside a PDF | No | + ## Demo Basic usage from the [example app](https://github.com/majornutcracker-dev/react-native-selectable-text/tree/main/example): selecting text, highlight From 3bb5ab199c78aabba0b79a298185df46654b855c Mon Sep 17 00:00:00 2001 From: JoshuaPariona Date: Wed, 23 Sep 2026 16:05:32 -0500 Subject: [PATCH 6/7] bump: v1.1.0 --- CHANGELOG.md | 14 ++++++++++++-- android/build.gradle | 6 +++--- ...jornutcrackerReactNativeSelectableTextModule.kt | 2 +- ...nutcrackerReactNativeSelectableTextModule.swift | 2 +- package.json | 2 +- src/types.ts | 2 +- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed2c04e..514f454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0] - 2026-09-23 + ### Changed - The native module is now optional, so importing the package no longer throws where that module is not in the binary — Expo Go and Snack among them, which already bundle `react-native-webview`. `SelectableTextView` never reads it, so the component works there; the default export is `null` in those - environments. + environments. Verified in Expo Go and in a prebuilt app, on iOS and Android. + +### Added + +- **Your own selection menu** in the API reference: `webViewProps.menuItems` and + `onCustomMenuSelection` reach `react-native-webview` untouched, so the native + menu can drive the ref's highlight methods. +- The README opens with badges and an _Is this the right library?_ table. ## [1.0.0] - 2026-09-14 @@ -39,5 +48,6 @@ Initial release. - Font helpers (`googleFonts()`, `mergeFonts()`), ignored elements, and viewport zoom options. -[unreleased]: https://github.com/majornutcracker-dev/react-native-selectable-text/compare/v1.0.0...HEAD +[unreleased]: https://github.com/majornutcracker-dev/react-native-selectable-text/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/majornutcracker-dev/react-native-selectable-text/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/majornutcracker-dev/react-native-selectable-text/releases/tag/v1.0.0 diff --git a/android/build.gradle b/android/build.gradle index 031a692..6c36a52 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,13 +4,13 @@ plugins { } group = 'com.majornutcracker.reactnativeselectablewebview' -version = '1.0.0' +version = '1.1.0' android { namespace "com.majornutcracker.reactnativeselectablewebview" defaultConfig { - versionCode 1 - versionName "1.0.0" + versionCode 2 + versionName "1.1.0" } lintOptions { abortOnError false diff --git a/android/src/main/java/com/majornutcracker/reactnativeselectablewebview/MajornutcrackerReactNativeSelectableTextModule.kt b/android/src/main/java/com/majornutcracker/reactnativeselectablewebview/MajornutcrackerReactNativeSelectableTextModule.kt index 5766b83..8420fd7 100644 --- a/android/src/main/java/com/majornutcracker/reactnativeselectablewebview/MajornutcrackerReactNativeSelectableTextModule.kt +++ b/android/src/main/java/com/majornutcracker/reactnativeselectablewebview/MajornutcrackerReactNativeSelectableTextModule.kt @@ -8,7 +8,7 @@ class MajornutcrackerReactNativeSelectableTextModule : Module() { Name("MajornutcrackerReactNativeSelectableText") Constant("version") { - "1.0.0" + "1.1.0" } } } diff --git a/ios/MajornutcrackerReactNativeSelectableTextModule.swift b/ios/MajornutcrackerReactNativeSelectableTextModule.swift index 1944bbf..e3d19e0 100644 --- a/ios/MajornutcrackerReactNativeSelectableTextModule.swift +++ b/ios/MajornutcrackerReactNativeSelectableTextModule.swift @@ -5,7 +5,7 @@ public class MajornutcrackerReactNativeSelectableTextModule: Module { Name("MajornutcrackerReactNativeSelectableText") Constant("version") { - "1.0.0" + "1.1.0" } } } diff --git a/package.json b/package.json index c027262..30bc9e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@majornutcracker/react-native-selectable-text", - "version": "1.0.0", + "version": "1.1.0", "description": "Text selection and persistent, animated highlights for HTML in React Native and Expo (iOS & Android). Serialize, restore and sync highlights. Built on react-native-webview and Rangy.", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/src/types.ts b/src/types.ts index 583159e..488cd01 100644 --- a/src/types.ts +++ b/src/types.ts @@ -598,4 +598,4 @@ export const BridgingNames = { }, }; -export const VERSION = "1.0.0"; +export const VERSION = "1.1.0"; From aba89644d1e6348178782cadb1c92f19e3d1b7fa Mon Sep 17 00:00:00 2001 From: JoshuaPariona Date: Wed, 23 Sep 2026 16:10:36 -0500 Subject: [PATCH 7/7] docs: map every hardcoded version in the update guide --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- docs/VERSION-UPDATE.md | 56 +++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ea261f2..912ddee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -37,7 +37,7 @@ body: id: package-version attributes: label: "@majornutcracker/react-native-selectable-text version" - placeholder: 1.0.0 + placeholder: 1.1.0 validations: required: true diff --git a/docs/VERSION-UPDATE.md b/docs/VERSION-UPDATE.md index 4fd7c0f..32fec5b 100644 --- a/docs/VERSION-UPDATE.md +++ b/docs/VERSION-UPDATE.md @@ -2,7 +2,9 @@ The package version follows [Semantic Versioning](https://semver.org/). `package.json` is the source of truth, but several native files **hardcode** the same version and must be -kept in sync by hand. This guide lists every place to change and how to pick the bump. +kept in sync by hand. This guide lists every place to change and how to pick the bump, then +the references that are merely cosmetic and the other version strings in the repo that drift +the same way without being the package version. ## Which bump? @@ -32,13 +34,24 @@ Given `MAJOR.MINOR.PATCH`: regardless of whether the release is a patch, minor, or major. It is independent of `versionName`/semver. +## Cosmetic references (nice to keep current, never release-blocking) + +The release workflow does not look at these, and a stale value here publishes a perfectly +good package. Refresh them when you remember. + +| File | What to change | Note | +| --------------------------------------- | -------------------------------------- | --------------------------------------------------- | +| `.github/ISSUE_TEMPLATE/bug_report.yml` | `placeholder:` under the version input | Shows a greyed-out example version in the bug form. | + ## Do NOT edit (auto-derived) - `ios/MajornutcrackerReactNativeSelectableText.podspec` — `s.version = package['version']` reads `package.json` at pod-install time. Its `summary`, `description`, `license`, `author`, and `homepage` also come from `package.json`. Leave it alone. -- `example/` — the example app has its own versioning that is irrelevant to the published - package. Do not bump it as part of a release. +- `example/package.json` and `example/app.json` — the example app has its own versioning, + irrelevant to the published package. Do not bump it as part of a release. +- This guide — the versions in it (`1.0.0 → 1.1.0`, `git tag v1.1.0`, …) are examples of the + procedure, not values to keep in sync with the package. ## Changelog @@ -73,12 +86,47 @@ grep -RIn "1\.1\.0" \ Every listed file should appear (and `versionCode` should be one higher than before). +## Other version strings in this repo + +These are **not** the package version and do not move during a release, but they are +duplicated by hand in the same way, so they drift in the same way. Each list is every place +that has to agree. + +### The supported `react-native-webview` range + +Changing which versions the module supports means touching all four: + +| File | What | +| ---------------------- | ---------------------------------------------------------------------------------------- | +| `package.json` | `devDependencies` — the exact version developed and tested against. | +| `package.json` | `peerDependencies` — the range a consumer's app must satisfy. | +| `README.md` | The **Peer dependencies** sentence, which repeats the range in prose. | +| `example/package.json` | The example app's own pin. Keep it inside the peer range, or the example proves nothing. | + +### The vendored Rangy copy + +Rangy is vendored as `src/rangy@1.3.2/`, so its version is part of a **directory name** and +turns up in imports, tooling config and docs. Upgrading it is a rename plus all of these: + +- `src/utils.ts` and `src/__tests__/ignoredElements.test.ts` — the import paths. +- `eslint.config.js`, `.prettierignore`, `.gitattributes` — the lint, format and vendoring rules. +- `.github/CODEOWNERS` — the ownership entry. +- `docs/THIRD-PARTY-NOTICES.md` — the `## Rangy (v1.3.2)` heading and the path below it. +- `CONTRIBUTING.md` and `SECURITY.md` — both name the folder in prose. + +To catch the stragglers after a rename: + +```sh +grep -rIn "rangy@" --exclude-dir=node_modules --exclude-dir=build . +``` + ## Suggested release flow Publishing is automated: the [`Release`](../.github/workflows/release.yml) workflow triggers on any pushed tag matching `v*.*.*`. Do **not** run `npm publish` by hand. -1. Bump all files above and update `CHANGELOG.md`. +1. Bump every file in [Files to update](#files-to-update-must-all-match) and update + `CHANGELOG.md`. The cosmetic references can ride along. 2. Commit — `bump:` or `chore:` per the commit conventions, e.g. `bump: v1.1.0`. 3. Push the commit to `main` and let CI pass. 4. Tag and push the tag: