diff --git a/docs/docs/core-functionalities/handling-events.md b/docs/docs/core-functionalities/handling-events.md index f66526c28..64027c1a1 100644 --- a/docs/docs/core-functionalities/handling-events.md +++ b/docs/docs/core-functionalities/handling-events.md @@ -4,4 +4,56 @@ sidebar_position: 3 # Handling events - +Since the input is [uncontrolled](/fundamentals/core-concepts#the-input-is-uncontrolled), +events are how you observe it. You change content by calling ref methods; you +react to changes by listening to the callbacks below. + +Full payload shapes of the available callbacks can be found in the `EnrichedTextInput` reference. + +## Content + +- **`onChangeText`** - plain-text content changed. +- **`onChangeHtml`** - the HTML changed. + +:::tip + +The `onChangeHtml` callback has to parse the content into HTML on every keystroke. +This is a heavy computational operation that might slow down your app's performance. Consider using the `getHTML()` ref method instead if it meets your requirements. + +::: + +## Selection and style state + +- **`onChangeSelection`** - the cursor moved or the selection changed. Gives you + `start`, `end`, and the selected `text`. Useful for range-based methods like + [`setLink`](/rich-text-formatting/links). +- **`onChangeState`** - the active styles at the cursor changed. This is the + event that drives a toolbar by using reported `isActive`, `isBlocking`, and + `isConflicting`, plus the current `alignment`. See the + [style state model](/fundamentals/core-concepts#the-style-state-model). + +## Focus + +- **`onFocus`** / **`onBlur`** - the input gained or lost focus. + +## Mentions + +- **`onStartMention`** - a mention started being edited. +- **`onChangeMention`** - the query after the indicator changed. +- **`onEndMention`** - editing a mention stopped. +- **`onMentionDetected`** - the cursor entered or left a mention. + +## Links + +- **`onLinkDetected`** - the cursor entered or left a link. + +## Images + +- **`onPasteImages`** - the user pasted one or more images; hands you each + image's data so you can upload and insert them with + [`setImage`](/rich-text-formatting/inline-images). + +## Keyboard and submission + +- **`onKeyPress`** - a key was pressed. +- **`onSubmitEditing`** - the user presses return/enter key. Fires when `submitBehavior` is set to either `submit` or `blurAndSubmit`. diff --git a/docs/docs/core-functionalities/rendering-rich-text.md b/docs/docs/core-functionalities/rendering-rich-text.md deleted file mode 100644 index be631660e..000000000 --- a/docs/docs/core-functionalities/rendering-rich-text.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Rendering rich text - - diff --git a/docs/docs/core-functionalities/rendering-rich-text.mdx b/docs/docs/core-functionalities/rendering-rich-text.mdx new file mode 100644 index 000000000..95406f7d4 --- /dev/null +++ b/docs/docs/core-functionalities/rendering-rich-text.mdx @@ -0,0 +1,81 @@ +--- +sidebar_position: 2 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import RenderingEditor from '@site/src/examples/RenderingEditor'; +import RenderingEditorSrc from '!!raw-loader!@site/src/examples/RenderingEditor'; + +# Rendering rich text + +`EnrichedTextInput` is for editing. To _display_ rich text without an editor - +a chat message, a comment, an article - use its read-only counterpart, +**`EnrichedText`**. + +Both components speak the same [HTML format](/fundamentals/html-format-and-supported-tags), +so the typical flow is: edit in `EnrichedTextInput`, persist the +`getHTML` output, and later feed that +string to `EnrichedText`. + +## Passing content + +`EnrichedText` takes the HTML string as its `children`: + +```tsx +import { EnrichedText } from 'react-native-enriched-html'; + +{'

Hello world

'}
; +``` + +## Styling + +Styling mirrors the input. `style` controls the container and base typography, +and `htmlStyle` controls per-element appearance. `EnrichedText` extends +`htmlStyle` with **press states** for interactive elements, since links and +mentions are pressable here: + +```tsx + + {html} + +``` + +The added `pressColor` / `pressBackgroundColor` fields on `a` and `mention` are +the only shape difference from the input's `htmlStyle`. See the +`EnrichedText` reference for the full type. + +## Notable props + +- **`selectable`** - allow the user to select and copy the rendered text. + Defaults to `false`. +- **`onLinkPress` / `onMentionPress`** - fire when a link or mention is pressed. +- **`numberOfLines` / `ellipsizeMode`** - truncate long content to a fixed + number of lines with an ellipsis. +- **`useHtmlNormalizer`** - normalize external or messy HTML into the library's canonical + tag subset before rendering. Defaults to `true`. See + [Normalization](/fundamentals/core-concepts#normalization). + +:::note + +On web, the default behavior of the pressed `` tag is suppressed. To navigate to the link's URL, you need to properly handle the `onLinkPress` event. + +::: + +## Try it out + +Format some text in the editor, then press **Render** - the current HTML is read +with `getHTML()` and handed to an `EnrichedText` below. + + + +:::caution + +On iOS and Android, `EnrichedText` does not sanitize HTML for you. Sanitize anything you render that +came from users or other untrusted sources. To know more about the web's built-in sanitization, visit [Web support](/core-functionalities/web-support#sanitization). + +::: diff --git a/docs/docs/core-functionalities/styling-the-input.md b/docs/docs/core-functionalities/styling-the-input.md index f517a6deb..9e3d8ff4b 100644 --- a/docs/docs/core-functionalities/styling-the-input.md +++ b/docs/docs/core-functionalities/styling-the-input.md @@ -4,4 +4,80 @@ sidebar_position: 1 # Styling the input - +`EnrichedTextInput` is styled through two separate props. Together they cover +everything from the container's dimensions down to the color of a bullet point. + +- **`style`** - the container's layout behavior and its base typography (`fontSize`, `color`, `fontFamily`, …). It accepts a subset of React Native's `TextStyle`, described by + `EnrichedInputStyle`. +- **`htmlStyle`** - the appearance of individual rich text elements: heading + sizes, blockquote borders, code colors, list markers, mention colors, and so + on. + +```tsx + +``` + +## `htmlStyle` + +`htmlStyle` maps each supported element to a small config object. Anything you +omit falls back to the built-in default. The available keys are: + +| Key | Styles | Notable options | +| ------------ | -------------- | ----------------------------------------------------------- | +| `h1`–`h6` | Headings | `fontSize`, `bold` | +| `blockquote` | Blockquote | `borderColor`, `borderWidth`, `gapWidth`, `color` | +| `codeblock` | Code block | `color`, `backgroundColor`, `borderRadius` | +| `code` | Inline code | `color`, `backgroundColor` | +| `a` | Links | `color`, `textDecorationLine` | +| `mention` | Mentions | `color`, `backgroundColor`, `textDecorationLine` | +| `ol` | Ordered list | `markerColor`, `markerFontWeight`, `marginLeft`, `gapWidth` | +| `ul` | Unordered list | `bulletColor`, `bulletSize`, `marginLeft`, `gapWidth` | +| `ulCheckbox` | Checkbox list | `boxColor`, `boxSize`, `marginLeft`, `gapWidth` | + +The full list of properties, defaults, and platform notes lives in the +`EnrichedTextInput` reference. + +### Styling mentions per indicator + +`mention` accepts either a single config applied to every mention, or a record +keyed by [indicator](/rich-text-formatting/mentions) so each mention type gets +its own look: + +```tsx +htmlStyle={{ + mention: { + '@': { color: '#2563eb', backgroundColor: '#dbeafe' }, + '#': { color: '#16a34a', backgroundColor: '#dcfce7' }, + }, +}} +``` + +:::tip + +You can also create a default `mention` style config, by using the `'default'` key. + +```tsx +htmlStyle={{ + mention: { + 'default': { color: '#2563eb', backgroundColor: '#dbeafe' }, + '#': { color: '#16a34a', backgroundColor: '#dcfce7' }, + }, +}} +``` + +This way you can create a style for any mention indicator to fallback if it doesn't have one fully defined. + +::: diff --git a/docs/docs/core-functionalities/web-support.md b/docs/docs/core-functionalities/web-support.md index bc5958b68..493227c68 100644 --- a/docs/docs/core-functionalities/web-support.md +++ b/docs/docs/core-functionalities/web-support.md @@ -4,4 +4,93 @@ sidebar_position: 4 # Web support - +Both `EnrichedTextInput` and `EnrichedText` run on the web. On native the editor +is backed by the platform's text engine; on the web it is built on +[Tiptap](https://tiptap.dev/) (on top of ProseMirror). That implementation +detail stays behind the same public API. + +## One API across platforms + +The web build exposes the **same props, ref methods, and events** as native. +Events keep their native shape too - they arrive as +`NativeSyntheticEvent`, read off `e.nativeEvent`, so +[event-handling](/core-functionalities/handling-events) code is portable as-is: + +```tsx + setHtml(e.nativeEvent.value)} + onChangeState={e => setState(e.nativeEvent)} +/> +``` + +The interactive examples throughout these docs are the web build running live. + +## Keyboard shortcuts + +The web editor ships desktop-style formatting shortcuts out of the box, along +with native browser **undo/redo**. `Mod` is `⌘` on macOS and `Ctrl` on +Windows/Linux. + +| Action | macOS | Windows / Linux | +| ------------------- | -------------- | --------------------------- | +| Bold | `⌘B` | `Ctrl+B` | +| Italic | `⌘I` | `Ctrl+I` | +| Underline | `⌘U` | `Ctrl+U` | +| Strikethrough | `⌘⇧X` | `Ctrl+Shift+X` | +| Inline code | `⌘⇧C` | `Ctrl+Shift+C` | +| Code block | `⌘⌥⇧C` | `Ctrl+Alt+Shift+C` | +| Normal paragraph | `⌘⌥0` | `Ctrl+Alt+0` | +| Heading 1–6 | `⌘⌥1` – `⌘⌥6` | `Ctrl+Alt+1` – `Ctrl+Alt+6` | +| Numbered list | `⌘⇧7` | `Ctrl+Shift+7` | +| Unordered list | `⌘⇧8` | `Ctrl+Shift+8` | +| Checkbox list | `⌘⇧9` | `Ctrl+Shift+9` | +| Paste as plain text | `⌘⇧V` | `Ctrl+Shift+V` | +| Undo | `⌘Z` | `Ctrl+Z` | +| Redo | `⌘⇧Z` | `Ctrl+Shift+Z` | +| Select all | `⌘A` | `Ctrl+A` | + +## Platform differences + +A few native-only features have no web equivalent and are ignored there: + +- **`contextMenuItems`** - the native editing menu isn't available; use your own + UI instead. +- **`returnKeyLabel`** - can't be set inside a browser. `returnKeyType` maps to + the browser's [`enterkeyhint`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint). +- **RN layout ref methods** - `measure`, `measureInWindow`, `measureLayout`, and + `setNativeProps` are no-ops. + +The [`EnrichedTextInput`](/api-reference/enriched-text-input) and +[`EnrichedText`](/api-reference/enriched-text) references note per-prop platform +support. + +:::note + +On web, `onPasteImages` gives each image a `blob:` URL. If you hold onto those +URIs, call `URL.revokeObjectURL(uri)` once you're done with them (e.g. after an +upload) so the browser can release the memory. + +::: + +## Sanitization + +Unlike the native platforms, the web build sanitizes HTML for you. It runs +[DOMPurify](https://github.com/cure53/DOMPurify) at **every entrypoint** - the +`children` of `EnrichedText`, and `defaultValue`, `setValue`, and pasted content +on `EnrichedTextInput`. Sanitization is also run on the input component's **output** - `getHTML()`. +This ensures untrusted markup can't inject scripts or unsafe attributes into the DOM. +:::caution + +Sanitization is tied to link detection: the [`linkRegex`](/rich-text-formatting/links) +you provide determines which `href` values are allowed to survive on `` +elements. Anchors whose URLs don't match the pattern have their `href` stripped +during sanitization. + +::: + +## Server-side rendering + +The library does **not** support SSR. Normalization and sanitization both need a +DOM to work against, which isn't available during server rendering. In an SSR +framework (Next.js, Remix, …), make sure both `EnrichedTextInput` and +`EnrichedText` render **client-side only**. diff --git a/docs/src/examples/RenderingEditor.tsx b/docs/src/examples/RenderingEditor.tsx new file mode 100644 index 000000000..3040dd015 --- /dev/null +++ b/docs/src/examples/RenderingEditor.tsx @@ -0,0 +1,129 @@ +import { EnrichedTextInput, EnrichedText } from 'react-native-enriched-html'; +import type { + EnrichedTextInputInstance, + OnChangeStateEvent, +} from 'react-native-enriched-html'; +import { useRef, useState } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +export default function App() { + const ref = useRef(null); + const [state, setState] = useState(null); + const [html, setHtml] = useState( + '

Edit the input above, then press Render.

' + ); + + const buttons = [ + { + label: 'Bold', + state: state?.bold, + onPress: () => ref.current?.toggleBold(), + }, + { + label: 'Italic', + state: state?.italic, + onPress: () => ref.current?.toggleItalic(), + }, + { label: 'H1', state: state?.h1, onPress: () => ref.current?.toggleH1() }, + { + label: 'Quote', + state: state?.blockQuote, + onPress: () => ref.current?.toggleBlockQuote(), + }, + ]; + + // Pull the current HTML off the editor and hand it to the viewer. + const render = async () => { + const value = await ref.current?.getHTML(); + if (value) setHtml(value); + }; + + return ( + + setState(e.nativeEvent)} + /> + + {buttons.map(button => ( + + + {button.label} + + + ))} + + + + + Render ↓ + + + + + {html} + + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + row: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 8, + }, + button: { + width: 90, + padding: 8, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + buttonActive: { + borderColor: '#57b495', + backgroundColor: '#57b495', + }, + buttonDisabled: { + opacity: 0.4, + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, + textActive: { + color: '#eef0ff', + }, + viewer: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 64, + backgroundColor: '#eef0ff', + }, +});