diff --git a/docs/docs/rich-text-formatting/basic-styles.md b/docs/docs/rich-text-formatting/basic-styles.md deleted file mode 100644 index f5945cb02..000000000 --- a/docs/docs/rich-text-formatting/basic-styles.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Basic styles - - diff --git a/docs/docs/rich-text-formatting/basic-styles.mdx b/docs/docs/rich-text-formatting/basic-styles.mdx new file mode 100644 index 000000000..575ceff0e --- /dev/null +++ b/docs/docs/rich-text-formatting/basic-styles.mdx @@ -0,0 +1,116 @@ +--- +sidebar_position: 1 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import BasicStylesEditor from '@site/src/examples/BasicStylesEditor'; +import BasicStylesEditorSrc from '!!raw-loader!@site/src/examples/BasicStylesEditor'; + +# Basic styles + +In [Your first editor](/fundamentals/your-first-editor) you wired up a single +**Bold** button. Every other formatting style works exactly the same way: a +`toggle` method on the `ref` changes the text, and `onChangeState` reports back +whether the style is active. The only exceptions to that rule are **mentions**, +**links** and **inline images**, which work a bit differently. They cannot be toggled +on/off on any text - they require additional data to be valid, but you'll learn more +about them in further sections. This page walks through the rest of the basic rich +text styles and the one core distinction that shapes how they behave - **inline** +versus **paragraph** formatting. + +## Inline styles + +Inline styles wrap a range of characters. Toggling one applies it to exactly the +characters you have selected, or - if nothing is selected - arms it so the next +characters you type come out styled. This is the family the bold belongs to: + +- **Bold** - `toggleBold()` +- **Italic** - `toggleItalic()` +- **Underline** - `toggleUnderline()` +- **Strikethrough** - `toggleStrikeThrough()` +- **Inline code** - `toggleInlineCode()` + +They combine freely, so a word can be bold and italic and underlined at +once. + +:::note + +There are two more inline styles, which you cannot toggle quite like the others - **links** and **mentions**. You'll learn about them in their respective sections, once you finish this chapter. + +::: + +## Paragraph styles + +Paragraph styles apply to a whole paragraph at once. Toggling one affects the entire paragraph the +cursor sits in (or every paragraph the selection touches), no matter how much of +it is actually selected. The common ones are: + +- **Headings** - `toggleH1()` through `toggleH6()` +- **Blockquote** - `toggleBlockQuote()` +- **Code block** - `toggleCodeBlock()` +- **Unordered list** - `toggleUnorderedList()` +- **Ordered list** - `toggleOrderedList()` +- **Checkbox list** - `toggleCheckboxList(checked: boolean)` + +:::note + +We will not touch list styles for now - we'll cover them in-depth in their [own section](/rich-text-formatting/lists). + +::: + +The key difference from inline styles is that **a paragraph can only have one +paragraph style at a time**. A line can't be both a heading and a blockquote, so +toggling a second paragraph style replaces the first rather than stacking on top. +The editor surfaces this through the `isConflicting` flag in `onChangeState`. + +Code blocks go one step further: they **block** inline styles entirely. You +can't make text bold inside a code block, and `onChangeState` reports bold as +`isBlocking` there so you can grey the button out. + +:::tip + +The full table of what every style blocks or conflicts with is available in +[Supported tags](/fundamentals/html-format-and-supported-tags) and the `onChangeState` behavior is described in [Style state model](/fundamentals/core-concepts#the-style-state-model). + +::: + +## Building the toolbar + +A toolbar with many styles is just the [first editor](/fundamentals/your-first-editor) +loop repeated. Each button reads its slice of the `onChangeState` payload for +`isActive` (to highlight it) and `isBlocking` (to disable it), and calls the +matching `toggle` method on press: + +```tsx +const inlineButtons = [ + { label: 'Bold', state: state?.bold, onPress: () => ref.current?.toggleBold() }, + { label: 'Italic', state: state?.italic, onPress: () => ref.current?.toggleItalic() }, + { label: 'Underline', state: state?.underline, onPress: () => ref.current?.toggleUnderline() }, + { label: 'Strike', state: state?.strikeThrough, onPress: () => ref.current?.toggleStrikeThrough() }, +]; + +const paragraphButtons = [ + { label: 'H1', state: state?.h1, onPress: () => ref.current?.toggleH1() }, + { label: 'H2', state: state?.h2, onPress: () => ref.current?.toggleH2() }, + { label: 'Quote', state: state?.blockQuote, onPress: () => ref.current?.toggleBlockQuote() }, + { label: 'Code', state: state?.codeBlock, onPress: () => ref.current?.toggleCodeBlock() }, +]; +``` + +## Try it out + +And just like that we've expanded our editor with several new styles to choose +from a small toolbar. Select some text and toggle inline styles, or place the cursor +on a line and try the paragraph ones - notice how switching from **H1** to **Quote** +swaps the style instead of adding to it, and how the inline buttons grey out once +you're inside a **Code** block. + + + +:::info + +This page is about _what_ each style does, using the built-in defaults. To change +how they look - heading sizes, blockquote borders, code block colors - see +[Styling the input](/core-functionalities/styling-the-input). + +::: diff --git a/docs/docs/rich-text-formatting/inline-images.md b/docs/docs/rich-text-formatting/inline-images.md deleted file mode 100644 index 464ec5746..000000000 --- a/docs/docs/rich-text-formatting/inline-images.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Inline images - - diff --git a/docs/docs/rich-text-formatting/inline-images.mdx b/docs/docs/rich-text-formatting/inline-images.mdx new file mode 100644 index 000000000..ce30e32b3 --- /dev/null +++ b/docs/docs/rich-text-formatting/inline-images.mdx @@ -0,0 +1,61 @@ +--- +sidebar_position: 5 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import ImagesEditor from '@site/src/examples/ImagesEditor'; +import ImagesEditorSrc from '!!raw-loader!@site/src/examples/ImagesEditor'; + +# Inline images + +Aside from just styled text, the library offers much more. This includes inline images - pictures embedded straight into the text flow. + +## Inserting an image + +You can insert it explicitly with the `setImage` ref method: + +```ts +setImage(src: string, width: number, height: number) +``` + +- `src` - an absolute file path or a remote image URL. +- `width` / `height` - the size the image should render at. + +The image lands on its own line at the current selection, replacing any +selected text. + +```tsx +const insertImage = () => { + ref.current?.setImage('https://picsum.photos/320/160', 160, 80); +}; +``` + +:::note + +You are responsible for the `width` and `height` you pass - the editor does not +measure the image for you, so if you want to preserve a picture's aspect ratio you +need to compute it yourself before calling `setImage`. + +::: + +## When the source can't load + +If the `src` can't be resolved - a broken URL, a missing file - the editor doesn't +error out and renders a static placeholder in the image's place instead. + +## Try it out + +Place the cursor in the input and insert an image. The first button inserts a real +photo from [Lorem Picsum](https://picsum.photos) at a matching size; the second +points at a source that doesn't exist, so you get the fallback placeholder rather +than a crash. + + + +:::info + +Images can also arrive by pasting rather than through `setImage` - the editor +surfaces those through the `onPasteImages` event, which hands you each image's URI, +MIME type, and dimensions so you can upload or resize before inserting. + +::: diff --git a/docs/docs/rich-text-formatting/links.md b/docs/docs/rich-text-formatting/links.md deleted file mode 100644 index 549f3ac11..000000000 --- a/docs/docs/rich-text-formatting/links.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Links - - diff --git a/docs/docs/rich-text-formatting/links.mdx b/docs/docs/rich-text-formatting/links.mdx new file mode 100644 index 000000000..3ae976ac5 --- /dev/null +++ b/docs/docs/rich-text-formatting/links.mdx @@ -0,0 +1,118 @@ +--- +sidebar_position: 2 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import LinksEditor from '@site/src/examples/LinksEditor'; +import LinksEditorSrc from '!!raw-loader!@site/src/examples/LinksEditor'; + +# Links + +A link is a piece of text with a URL attached to it. The editor creates links in +two ways - **automatically**, as the user types something that looks like a URL, +and **manually**, when you call `setLink` on a range of text. The two produce +links that behave differently, so it's worth understanding both. + +## Automatic links + +By default the editor watches what the user types and turns anything that looks +like a URL into a link on its own - no code required. These are **autolinks**. + +Because an autolink is tied to the text that produced it, it stays a link only as +long as that text still matches. The moment the user edits the words, the pattern +no longer holds and the link naturally breaks. + +### Customizing detection + +You control what counts as a link with the +`linkRegex` prop. Pass your own `RegExp` +to recognize custom patterns - for example, only `https://` URLs or a custom +scheme that detects references like `issue-123`: + +```tsx +// Detect "issue-123" style tokens as links. +const linkRegex = /issue-\d+/g; + +; +``` + +:::note + +On iOS and Android the pattern is matched by the platform's native regex engine, +so not every JavaScript regex feature is available there - variable-width +lookbehinds, for instance, won't work. Pass `null` to turn automatic detection +off entirely. + +::: + +## Manual links + +Manual links are applied explicitly with the +`setLink` ref method: + +```ts +setLink(start: number, end: number, text: string, url: string) +``` + +It sets a link over the range from `start` to `end`, showing `text` and pointing +at `url`. Unlike an autolink, a manual link is a real, standalone style - the +user can keep editing its text and it stays a link. + +:::tip + +The three positional arguments line up exactly with the +`onChangeSelection` event payload, which reports +the current `start`, `end`, and the `text` it spans. So "link the current +selection" can be implemented quite simply: + +::: + +```tsx +const [selection, setSelection] = useState(null); + +// ... + setSelection(e.nativeEvent)} + /* ... */ +/>; + +const addLink = () => { + if (!selection) return; + ref.current?.setLink( + selection.start, + selection.end, + selection.text, + 'https://swmansion.com', + ); +}; +``` + +:::tip + +Passing a different `text` than what's selected replaces the selected text before +applying the link. To strip a link while keeping its text, call +`removeLink(start, end)`. + +::: + +## Try it out + +The editor below uses a custom `linkRegex` that autolinks `issue-123` tokens - type them and watch it become a link. Then +select some words and hit **Link the selection** to attach a URL manually. +Notice the difference afterward: editing the manual link's text keeps it a link, +while editing an autolinked token breaks it. + + + +## Handling links with events + +There is an event that allows you to handle user's interaction with links inside `EnrichedTextInput`. + +- **`onLinkDetected`** fires when the cursor enters or leaves a created link. + +:::info + +This page covers creating and detecting links. To change how +they look, see [Styling the input](/core-functionalities/styling-the-input). + +::: diff --git a/docs/docs/rich-text-formatting/lists.md b/docs/docs/rich-text-formatting/lists.md deleted file mode 100644 index 8f296a6e0..000000000 --- a/docs/docs/rich-text-formatting/lists.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Lists - - diff --git a/docs/docs/rich-text-formatting/lists.mdx b/docs/docs/rich-text-formatting/lists.mdx new file mode 100644 index 000000000..7eed30edd --- /dev/null +++ b/docs/docs/rich-text-formatting/lists.mdx @@ -0,0 +1,67 @@ +--- +sidebar_position: 4 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import ListsEditor from '@site/src/examples/ListsEditor'; +import ListsEditorSrc from '!!raw-loader!@site/src/examples/ListsEditor'; + +# Lists + +Lists are [paragraph styles](/rich-text-formatting/basic-styles#paragraph-styles), +just like headings and blockquotes - toggling one affects the whole paragraph the +cursor sits in, or every paragraph the selection touches. The editor supports three +kinds: + +- **Unordered list** - `toggleUnorderedList()` +- **Ordered list** - `toggleOrderedList()` +- **Checkbox list** - `toggleCheckboxList(checked: boolean)` + +Because they're paragraph styles, **only one can be active on a paragraph at a +time** exactly as described in +[Basic styles](/rich-text-formatting/basic-styles#paragraph-styles). + +:::note + +The library does not support nested lists. You can only create a single-level list. If you try to insert a nested list in any way, e.g. by calling `setValue` on the editor's `ref`, the list will get flattened by the [normalizer](/fundamentals/core-concepts#normalization). + +::: + +## Bulleted and numbered lists + +These two take no arguments - each call converts the affected paragraphs into a +list, and calling it again turns them back into plain paragraphs: + +```tsx +ref.current?.toggleUnorderedList(); +ref.current?.toggleOrderedList(); +``` + +## Checkbox lists + +This list's items need a starting state. `toggleCheckboxList` takes a `checked` boolean that +decides whether newly created boxes begin checked or unchecked: + +```tsx +// New items start unchecked. +ref.current?.toggleCheckboxList(false); +``` + +Once the list exists, the user can tick individual items by tapping their +checkbox - you don't need to wire anything up for that. + +## Try it out + +Type a few lines, place the cursor on one (or select several), and toggle each +list type. Notice how switching from **Bulleted** to **Numbered** swaps the style +instead of stacking. Inserting a new line preserves the list format by creating another item, and tapping a checkbox toggles just that item. + + + +:::info + +This page covers creating lists. To change how they look - bullet color and size, +marker weight, checkbox size, indentation - see +[Styling the input](/core-functionalities/styling-the-input). + +::: diff --git a/docs/docs/rich-text-formatting/mentions.md b/docs/docs/rich-text-formatting/mentions.md deleted file mode 100644 index ee0a2d492..000000000 --- a/docs/docs/rich-text-formatting/mentions.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Mentions - - diff --git a/docs/docs/rich-text-formatting/mentions.mdx b/docs/docs/rich-text-formatting/mentions.mdx new file mode 100644 index 000000000..2df1fd0b4 --- /dev/null +++ b/docs/docs/rich-text-formatting/mentions.mdx @@ -0,0 +1,98 @@ +--- +sidebar_position: 3 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import MentionEditor from '@site/src/examples/MentionEditor'; +import MentionEditorSrc from '!!raw-loader!@site/src/examples/MentionEditor'; + +# Mentions + +This is a powerful feature allowing for a customizable inline style for a "mentioning" phrase - `@someone`, +`#some-channel`, or whatever pattern fits your app. Each mention is able to carry any custom data you attach to it, so it's more than +styled text: it's a reference you can act on later. + +## Mention indicators + +A mention begins with an **indicator** - a single character that tells the editor +"a mention starts here". The set of recognized indicators is controlled by the +`mentionIndicators` prop, which defaults to +`['@']`: + +```tsx +// Recognize both @user and #channel mentions. +; +``` + +Typing one of these characters starts a mention. You can also start one +programmatically with `startMention(indicator)`. + +## Inserting a mention with `setMention` + +Once a mention is being edited - the user has typed an indicator, or you've +called `startMention` - you insert the finished mention with `setMention`: + +```ts +setMention( + indicator: string, + text: string, + attributes?: Record, +) +``` + +`setMention` replaces whatever the user has typed including the indicator with +`text`. It also stores any `attributes` you pass - this is useful if you want to associate a mention with some data. Those attributes are preserved through the HTML, so they +survive a round-trip through `getHTML` and `setValue`. + +```tsx +const insertMention = (user: { id: string; name: string }) => { + ref.current?.setMention('@', `@${user.name}`, { id: user.id }); +}; +``` + +:::note + +`setMention` is only operational when an actual mention preceded by an indicator is being edited. Otherwise it does nothing. + +::: + +## Try it out + +Type `@`, then tap the button to turn it +into a completed `@mention`. Notice how you can type a part of a mention, e.g. `@Jo`, move the cursor inside its range and the mention insertion will properly replace it. The result is a single styled unit - try deleting one of its characters and see how it's removed. + + + +:::note + +A mention query spans up to two words - the editor keeps tracking the mention after one +space, and a second space ends it. That makes `@John Doe` a valid mention. + +::: + +## Driving mentions with events + +This example is deliberately bare-bones - one hard-coded user behind a button. +A real app watches what the user types after the +indicator and shows a picker. Four events give you everything you need for +that: + +- **`onStartMention`** fires when a mention starts - a good moment to open your suggestion list. +- **`onChangeMention`** fires on every edit to the query, handing you the text + typed after the indicator - use it to filter the list. +- **`onEndMention`** fires when the mention stops being edited - the cursor + moved away, or the query was closed - so you can dismiss the list. +- **`onMentionDetected`** fires when the cursor enters or leaves a created mention. + +You'd open the list on `onStartMention`, filter it on `onChangeMention`, and +call `setMention` when the user picks someone. This example doesn't wire up any +of that; the full picker flow is covered end-to-end in the +[Mention-only input](/guides/mention-only-input) guide. + +:::tip + +Prefix your custom attribute names with `data-` (e.g. `{ 'data-profile-url': user.profileUrl }`). +Non-standard attributes can be dropped when the HTML passes through a sanitizer - +this library's, or one on your own backend - whereas `data-*` attributes are the HTML standard for persisting custom data and are kept. + +::: diff --git a/docs/docs/rich-text-formatting/text-alignment.md b/docs/docs/rich-text-formatting/text-alignment.md deleted file mode 100644 index 1619e7a0a..000000000 --- a/docs/docs/rich-text-formatting/text-alignment.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Text alignment - - diff --git a/docs/docs/rich-text-formatting/text-alignment.mdx b/docs/docs/rich-text-formatting/text-alignment.mdx new file mode 100644 index 000000000..c4735ea44 --- /dev/null +++ b/docs/docs/rich-text-formatting/text-alignment.mdx @@ -0,0 +1,45 @@ +--- +sidebar_position: 6 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import TextAlignmentEditor from '@site/src/examples/TextAlignmentEditor'; +import TextAlignmentEditorSrc from '!!raw-loader!@site/src/examples/TextAlignmentEditor'; + +# Text alignment + +Alignment controls how a paragraph's text sits within the input's width. It's a +per-paragraph property - every paragraph the selection touches gets aligned - and +you set it with the `setTextAlignment` ref method: + +```ts +setTextAlignment(alignment: 'left' | 'center' | 'right' | 'justify' | 'auto') +``` + +The five values are: + +- **`left`**, **`center`**, **`right`** - align the text to that direction. +- **`justify`** - stretch each line to fill the full width. +- **`auto`** - reset to the system's natural alignment. + +:::note + +On Android, `justify` is not supported. Calling `setTextAlignment('justify')` results in natural alignment (the same as `auto`). + +::: +Unlike the paragraph styles from [Basic styles](/rich-text-formatting/basic-styles), +text alignment does not report its state with the `isActive` / `isConflicting` booleans in `onChangeState`. Instead it exposes `alignment` with its relevant string value. + +```tsx +const [state, setState] = useState(null); + +// Highlight the button that matches the paragraph at the cursor. +const isActive = state?.alignment === 'center'; +``` + +## Try it out + +Type a paragraph or two, place the cursor on one, and try each alignment. The +active button reflects `state.alignment` for the paragraph the cursor is in. + + diff --git a/docs/docs/rich-text-formatting/text-shortcuts.md b/docs/docs/rich-text-formatting/text-shortcuts.md deleted file mode 100644 index 9b8de802b..000000000 --- a/docs/docs/rich-text-formatting/text-shortcuts.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -sidebar_position: 7 ---- - -# Text shortcuts - - diff --git a/docs/docs/rich-text-formatting/text-shortcuts.mdx b/docs/docs/rich-text-formatting/text-shortcuts.mdx new file mode 100644 index 000000000..aa8bf82dd --- /dev/null +++ b/docs/docs/rich-text-formatting/text-shortcuts.mdx @@ -0,0 +1,82 @@ +--- +sidebar_position: 7 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import TextShortcutsEditor from '@site/src/examples/TextShortcutsEditor'; +import TextShortcutsEditorSrc from '!!raw-loader!@site/src/examples/TextShortcutsEditor'; + +# Text shortcuts + +Text shortcuts let users format as they type, the way Markdown editors do - +typing `#` turns a line into a heading, wrapping a word in `**` makes it bold. +You configure them with the `textShortcuts` prop, an array that maps a `trigger` +string to a `style`: + +```tsx +; +``` + +The `style` values are the same rich text styles you met in +[Basic styles](/rich-text-formatting/basic-styles) - and the inline vs. paragraph +distinction is exactly what decides how a shortcut fires. + +## Paragraph vs. inline shortcuts + +The two families of styles trigger differently, because they format different +things: + +- **Paragraph shortcuts** (`h1`–`h6`, `blockquote`, `codeblock`, + `unordered_list`, `ordered_list`, `checkbox_list`) fire at the **start of a + paragraph**. The trigger is the prefix you type before the line's content - + e.g. `#` for a heading, `-` for a bulleted list. Since a paragraph can only hold + one paragraph style, these only fire on a **plain** paragraph: if the line is + already a heading or a list item, typing the prefix does nothing. + +- **Inline shortcuts** (`bold`, `italic`, `underline`, `strikethrough`, + `inline_code`) fire when you type a **closing delimiter** around some text, so + typing `**word**` bolds `word`. + +:::note + +The usual [style rules](/rich-text-formatting/basic-styles#paragraph-styles) still +apply to shortcut-triggered styles. If the target style is **blocked** by an +active one (e.g. bold inside a code block), the shortcut has no effect. + +::: + +## Defaults + +Even without the prop, two shortcuts are active out of the box: + +```ts +[ + { trigger: '- ', style: 'unordered_list' }, + { trigger: '1. ', style: 'ordered_list' }, +]; +``` + +Passing your own array replaces these defaults entirely, and passing an empty +array (`textShortcuts={[]}`) disables shortcuts altogether. + +## Try it out + +The editor below wires up one shortcut of each kind - a paragraph one (`#` → H1) +and an inline one (`**` → bold). Start a line with `#` and watch it become a +heading, then wrap a word in `**stars**` to bold it. + + + +:::info + +On Web, the same styles are also reachable through +[keyboard shortcuts](/api-reference/enriched-text-input) like ⌘B / Ctrl+B, +independent of the `textShortcuts` prop. + +::: diff --git a/docs/src/examples/BasicStylesEditor.tsx b/docs/src/examples/BasicStylesEditor.tsx new file mode 100644 index 000000000..8adff21ee --- /dev/null +++ b/docs/src/examples/BasicStylesEditor.tsx @@ -0,0 +1,122 @@ +import { EnrichedTextInput } 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); + + // Inline styles apply to the selected characters. + const inlineButtons = [ + { + label: 'Bold', + state: state?.bold, + onPress: () => ref.current?.toggleBold(), + }, + { + label: 'Italic', + state: state?.italic, + onPress: () => ref.current?.toggleItalic(), + }, + { + label: 'Underline', + state: state?.underline, + onPress: () => ref.current?.toggleUnderline(), + }, + { + label: 'Strike', + state: state?.strikeThrough, + onPress: () => ref.current?.toggleStrikeThrough(), + }, + ]; + + // Paragraph styles apply to the whole line the cursor sits in. + const paragraphButtons = [ + { label: 'H1', state: state?.h1, onPress: () => ref.current?.toggleH1() }, + { label: 'H2', state: state?.h2, onPress: () => ref.current?.toggleH2() }, + { + label: 'Quote', + state: state?.blockQuote, + onPress: () => ref.current?.toggleBlockQuote(), + }, + { + label: 'Code', + state: state?.codeBlock, + onPress: () => ref.current?.toggleCodeBlock(), + }, + ]; + + const renderButton = ( + button: (typeof inlineButtons)[number] | (typeof paragraphButtons)[number] + ) => ( + + + {button.label} + + + ); + + return ( + + setState(e.nativeEvent)} + /> + {inlineButtons.map(renderButton)} + {paragraphButtons.map(renderButton)} + + ); +} + +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', + }, +}); diff --git a/docs/src/examples/ImagesEditor.tsx b/docs/src/examples/ImagesEditor.tsx new file mode 100644 index 000000000..2e459522e --- /dev/null +++ b/docs/src/examples/ImagesEditor.tsx @@ -0,0 +1,67 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { EnrichedTextInputInstance } from 'react-native-enriched-html'; +import { useRef } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +export default function App() { + const ref = useRef(null); + + const insertImage = () => { + // A real, loadable image. You supply the dimensions yourself - here we + // ask Lorem Picsum for a 320x160 photo and pass the proportionally down-scaled size. + ref.current?.setImage('https://picsum.photos/320/160', 160, 80); + }; + + const insertBrokenImage = () => { + // An unreachable source. The editor renders its static placeholder + // instead of failing, so you can see what a broken image looks like. + ref.current?.setImage('https://picsum.photos/does-not-exist', 160, 80); + }; + + return ( + + + + + Insert image + + + Insert broken image + + + + ); +} + +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: { + padding: 8, + paddingHorizontal: 16, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, +}); diff --git a/docs/src/examples/LinksEditor.tsx b/docs/src/examples/LinksEditor.tsx new file mode 100644 index 000000000..e4d64fd94 --- /dev/null +++ b/docs/src/examples/LinksEditor.tsx @@ -0,0 +1,75 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { + EnrichedTextInputInstance, + OnChangeSelectionEvent, +} from 'react-native-enriched-html'; +import { useRef, useState } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +// Autolink any "issue-123" style token. +const linkRegex = /issue-\d+/g; + +export default function App() { + const ref = useRef(null); + const [selection, setSelection] = useState( + null + ); + + const hasSelection = !!selection && selection.start !== selection.end; + + const addLink = () => { + if (!selection) return; + // Turn the current selection into a link pointing at our docs. + ref.current?.setLink( + selection.start, + selection.end, + selection.text, + 'https://swmansion.com' + ); + }; + + return ( + + setSelection(e.nativeEvent)} + /> + + Link the selection + + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + button: { + alignSelf: 'center', + padding: 8, + paddingHorizontal: 16, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + buttonDisabled: { + opacity: 0.4, + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, +}); diff --git a/docs/src/examples/ListsEditor.tsx b/docs/src/examples/ListsEditor.tsx new file mode 100644 index 000000000..4336969f4 --- /dev/null +++ b/docs/src/examples/ListsEditor.tsx @@ -0,0 +1,100 @@ +import { EnrichedTextInput } 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); + + // Lists are paragraph styles - each toggle affects whole lines, and only + // one list type can be active on a paragraph at a time. + const listButtons = [ + { + label: 'Bulleted', + state: state?.unorderedList, + onPress: () => ref.current?.toggleUnorderedList(), + }, + { + label: 'Numbered', + state: state?.orderedList, + onPress: () => ref.current?.toggleOrderedList(), + }, + { + label: 'Checkbox', + state: state?.checkboxList, + // Pass whether new checkboxes start checked or unchecked. + onPress: () => ref.current?.toggleCheckboxList(false), + }, + ]; + + const renderButton = (button: (typeof listButtons)[number]) => ( + + + {button.label} + + + ); + + return ( + + setState(e.nativeEvent)} + /> + {listButtons.map(renderButton)} + + ); +} + +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', + }, +}); diff --git a/docs/src/examples/MentionEditor.tsx b/docs/src/examples/MentionEditor.tsx new file mode 100644 index 000000000..efd3e9216 --- /dev/null +++ b/docs/src/examples/MentionEditor.tsx @@ -0,0 +1,52 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { EnrichedTextInputInstance } from 'react-native-enriched-html'; +import { useRef } from 'react'; +import { View, StyleSheet, Pressable, Text } from 'react-native'; + +const user = { id: '1', name: 'John' }; + +export default function App() { + const ref = useRef(null); + + const insertMention = () => { + // Replaces the active '@' mention the user has started by typing '@'. + ref.current?.setMention('@', `@${user.name}`, { id: user.id }); + }; + + return ( + + + + @{user.name} + + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, + button: { + alignSelf: 'center', + padding: 8, + paddingHorizontal: 16, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, +}); diff --git a/docs/src/examples/TextAlignmentEditor.tsx b/docs/src/examples/TextAlignmentEditor.tsx new file mode 100644 index 000000000..addc7d991 --- /dev/null +++ b/docs/src/examples/TextAlignmentEditor.tsx @@ -0,0 +1,77 @@ +import { EnrichedTextInput } 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'; + +const alignments = ['left', 'center', 'right', 'justify'] as const; + +export default function App() { + const ref = useRef(null); + const [state, setState] = useState(null); + + const renderButton = (alignment: (typeof alignments)[number]) => { + // A single string reports the alignment of the paragraph at the cursor. + const isActive = state?.alignment === alignment; + return ( + ref.current?.setTextAlignment(alignment)}> + + {alignment} + + + ); + }; + + return ( + + setState(e.nativeEvent)} + /> + {alignments.map(renderButton)} + + ); +} + +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', + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, + textActive: { + color: '#eef0ff', + }, +}); diff --git a/docs/src/examples/TextShortcutsEditor.tsx b/docs/src/examples/TextShortcutsEditor.tsx new file mode 100644 index 000000000..d3d26422b --- /dev/null +++ b/docs/src/examples/TextShortcutsEditor.tsx @@ -0,0 +1,37 @@ +import { EnrichedTextInput } from 'react-native-enriched-html'; +import type { EnrichedTextInputInstance } from 'react-native-enriched-html'; +import { useRef } from 'react'; +import { View, StyleSheet } from 'react-native'; + +export default function App() { + const ref = useRef(null); + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { gap: 12 }, + input: { + fontSize: 18, + color: '#232736', + padding: 12, + borderRadius: 12, + minHeight: 96, + backgroundColor: '#eef0ff', + }, +});