Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ 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. 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

Initial release.
Expand All @@ -31,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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 `<Text>` 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
Expand Down
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class MajornutcrackerReactNativeSelectableTextModule : Module() {
Name("MajornutcrackerReactNativeSelectableText")

Constant("version") {
"1.0.0"
"1.1.0"
}
}
}
74 changes: 64 additions & 10 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<SelectableTextViewRef>(null);

<SelectableTextView
ref={ref}
content={html}
highlighters={highlighters}
webViewProps={{
menuItems: [
{ key: "highlight", label: "Highlight" },
{ key: "unhighlight", label: "Unhighlight" },
{ key: "copy", label: "Copy" },
],
onCustomMenuSelection: (event) => {
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
Expand Down
56 changes: 52 additions & 4 deletions docs/VERSION-UPDATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion ios/MajornutcrackerReactNativeSelectableTextModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public class MajornutcrackerReactNativeSelectableTextModule: Module {
Name("MajornutcrackerReactNativeSelectableText")

Constant("version") {
"1.0.0"
"1.1.0"
}
}
}
35 changes: 19 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -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.",
"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",
"type": "module",
Expand Down Expand Up @@ -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"
],
Expand Down
7 changes: 5 additions & 2 deletions src/SelectableTextModule.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { NativeModule, requireNativeModule } from "expo";
import { NativeModule, requireOptionalNativeModule } from "expo";

declare class MajornutcrackerReactNativeSelectableTextModule extends NativeModule {
version: string;
}

export default requireNativeModule<MajornutcrackerReactNativeSelectableTextModule>(
// 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<MajornutcrackerReactNativeSelectableTextModule>(
"MajornutcrackerReactNativeSelectableText"
);
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,4 +598,4 @@ export const BridgingNames = {
},
};

export const VERSION = "1.0.0";
export const VERSION = "1.1.0";
Loading