diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 912ddee..65df1d6 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.1.0 + placeholder: 1.2.0 validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 514f454..29828fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] - 2026-09-24 + +> **Read this before upgrading.** The `highlights` prop is gone, so code that +> passes it no longer compiles. Rename it to `initialHighlights` for the value +> the view mounts with, and call `setHighlights()` for every change after that. +> The replacement is not a drop-in: `initialHighlights` is read **once**, when +> the content is built, and changing it later does nothing. +> +> ```diff +> - +> + +> + // later: ref.current?.setHighlights(payload) +> ``` + +### Added + +- `setHighlights(payload)` on the ref replaces every highlight in a mounted + view; `""` clears them. +- An undo history, kept by the view itself: `undo()`, `redo()` and + `clearHistory()` on the ref, and `getHistory()` to read it. +- `onHistoryChange`, which reports where the history stands — including + `canUndo` and `canRedo` — so undo and redo controls enable themselves without + tracking anything. The history records content changes only (highlighting, + unhighlighting, clearing and `setHighlights`) and holds the last 50 states. +- `initialHighlights`, the serialized highlights painted when the view mounts. + +### Changed + +- A payload that is not a serialized highlights string is now refused through + `onError` with `invalid_highlight` **without touching the highlights already + on screen**. It used to remove them first and fail afterwards, so a bad + string cost the reader their highlights. +- Stepping through the history no longer replays a highlight's entrance + animation: an undo restores something the reader has already seen. + +### Removed + +- The `highlights` prop, replaced as described above. It was both the mount + value and a controlled state prop, which meant the view had to guess which + values were genuine and which were its own report coming back — restoring + through it was unpredictable, and undo on top of it needed a stack, an echo + flag and string-identity reasoning in every consumer. + ## [1.1.0] - 2026-09-23 ### Changed @@ -48,6 +91,7 @@ Initial release. - Font helpers (`googleFonts()`, `mergeFonts()`), ignored elements, and viewport zoom options. -[unreleased]: https://github.com/majornutcracker-dev/react-native-selectable-text/compare/v1.1.0...HEAD +[unreleased]: https://github.com/majornutcracker-dev/react-native-selectable-text/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/majornutcracker-dev/react-native-selectable-text/compare/v1.1.0...v1.2.0 [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/README.md b/README.md index bf0b7fe..9b60e27 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ const highlighters: Highlighter[] = [ }, ]; -export default function Screen() { +export default function Screen({ saved }: { saved?: string }) { const ref = React.useRef(null); return ( @@ -95,6 +95,8 @@ export default function Screen() { content="

Hello

Select some text and highlight it.

" css=".content { padding: 16px; font-size: 18px; }" highlighters={highlighters} + // What a previous session stored. Read once, when the view mounts. + initialHighlights={saved} onHighlightsChange={(highlights) => { // Persist this serialized string to restore highlights later. console.log(highlights); @@ -106,12 +108,17 @@ export default function Screen() { ); } -// Highlight the current selection from anywhere with the ref: +// From anywhere, with the ref: // ref.current?.highlightSelection("yellow-highlighter"); +// ref.current?.setHighlights(payload); // replace them; "" clears +// ref.current?.undo(); // and redo(), and clearHistory() ``` -Pass that serialized string back through the `highlights` prop to restore the -highlights on remount. +Store the string `onHighlightsChange` reports, hand it back through +`initialHighlights` on the next mount, and the highlights come back where the +reader left them. On a view that is already on screen, `setHighlights()` +replaces them and `undo()` / `redo()` walk a history the view keeps for you — +see the [API reference](./docs/REFERENCE.md#restoring-replacing-and-undoing). ## Documentation diff --git a/android/build.gradle b/android/build.gradle index 6c36a52..ad9a97b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,13 +4,13 @@ plugins { } group = 'com.majornutcracker.reactnativeselectablewebview' -version = '1.1.0' +version = '1.2.0' android { namespace "com.majornutcracker.reactnativeselectablewebview" defaultConfig { - versionCode 2 - versionName "1.1.0" + versionCode 3 + versionName "1.2.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 8420fd7..b318a60 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.1.0" + "1.2.0" } } } diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index c9dc690..b7be59f 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -15,22 +15,23 @@ This page is the full surface. For a quick start see the | `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. | +| `initialHighlights` | Serialized highlights painted when the view mounts — the string a previous session stored. Read once; changing it later does nothing (the view warns) and `setHighlights()` is how a mounted view is updated. | | `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 -| Callback | Fires when | -| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `onTextSelectionChange(text)` | The selection changes. Receives the selected text, or `""` when the selection is cleared. | -| `onHighlightsChange(highlights, items)` | The serialized highlight payload changes. Receives the payload to persist, plus the `HighlightData[]` it contains — no `getAllHighlightsData()` round-trip needed. | -| `onLink(url)` | A link is tapped. Without this prop, `http(s)` URLs open through `Linking`. | -| `onError(error)` | The WebView SDK reports an error: `{ code, message, details }`. Try highlighting over an existing highlight for `overlapping_highlight`, or with no selection for `empty_selection`. | -| `onHighlightPressed(highlight)` | A highlight is tapped. Receives `{ id, name, text, rect, rects }`. Return a `className` to style the pressed highlight (define it in `css`), or return nothing to leave it unstyled. | -| `onHighlightsVisibilityStateChange(visible)` | Highlight visibility changes. Receives `true` when visible, `false` when hidden. | -| `onCustomMessage(message)` | A script in the page calls `window.SelectableText.postMessage(type, data)`. Receives `{ type, data }`. See [Bridging custom actions](#bridging-custom-actions). | +| Callback | Fires when | +| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `onTextSelectionChange(text)` | The selection changes. Receives the selected text, or `""` when the selection is cleared. | +| `onHighlightsChange(highlights, items)` | The serialized highlight payload changes. Receives the payload to persist, plus the `HighlightData[]` it contains — no `getAllHighlightsData()` round-trip needed. | +| `onHistoryChange(state)` | The undo history moves. Receives `{ change, history, historyIndex, length, canUndo, canRedo }`, so undo and redo controls can enable themselves without tracking anything — see [Undo and redo](#undo-and-redo). | +| `onLink(url)` | A link is tapped. Without this prop, `http(s)` URLs open through `Linking`. | +| `onError(error)` | The WebView SDK reports an error: `{ code, message, details }`. Try highlighting over an existing highlight for `overlapping_highlight`, or with no selection for `empty_selection`. | +| `onHighlightPressed(highlight)` | A highlight is tapped. Receives `{ id, name, text, rect, rects }`. Return a `className` to style the pressed highlight (define it in `css`), or return nothing to leave it unstyled. | +| `onHighlightsVisibilityStateChange(visible)` | Highlight visibility changes. Receives `true` when visible, `false` when hidden. | +| `onCustomMessage(message)` | A script in the page calls `window.SelectableText.postMessage(type, data)`. Receives `{ type, data }`. See [Bridging custom actions](#bridging-custom-actions). | A callback that throws is caught and logged rather than crashing the bridge, so a bug in your handler will not take the component down with it. @@ -77,28 +78,94 @@ afterwards does not update them, so dismiss or re-anchor whatever you placed: `onTextSelectionChange` and a `webViewProps.onScroll` handler are the usual hooks for that. -### Controlling the highlights +### Restoring, replacing and undoing -`onHighlightsChange` hands you both halves of the state at once, so the usual -wiring is a plain controlled component: +Highlights travel as one serialized string. Persist what `onHighlightsChange` +reports, and hand it back on the next mount: ```tsx -const [highlights, setHighlights] = useState(""); -const [items, setItems] = useState([]); +const [saved, setSaved] = useState(undefined); + +useEffect(() => { + AsyncStorage.getItem("highlights").then((value) => setSaved(value ?? "")); +}, []); + +// Wait for storage: the prop is read once, when the view mounts. +if (saved === undefined) return ; { - setHighlights(serialized); - setItems(list); // ids, names and text — already resolved + initialHighlights={saved} + onHighlightsChange={(serialized, items) => { + AsyncStorage.setItem("highlights", serialized); + setItems(items); // ids, names and text — already resolved }} />; ``` -Passing the emitted value straight back does **not** replay the highlights: the -view remembers the last payload it reported and skips restoring an echo of it. -Restoring is reserved for a payload it did not produce — a value loaded from -storage, a different document, or `""` to clear. +To replace the highlights of a view that is already on screen, call the ref: + +```tsx +ref.current?.setHighlights(payload); // "" clears them +``` + +Those are the two halves on purpose. `initialHighlights` is the document the +reader opens with; `setHighlights` is every change after that. A single prop +doing both is what made restoring unpredictable: it had to guess which values +were genuine and which were the view's own report coming back around. + +#### Undo and redo + +The page keeps the history itself — every content change, in order — and reports +where it stands: + +```tsx +const [history, setHistory] = useState({ canUndo: false, canRedo: false }); + +; + +