-
Notifications
You must be signed in to change notification settings - Fork 63
docs: Rich text formatting section #713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f04da21
a02b7f6
72cc637
1a6b5a9
313d619
094cf02
7b90280
fd0fbd0
480ed9a
8c7da88
6789640
8f82336
77fcddb
d2efde7
295df83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| --- | ||
| 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. Almost 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 | ||
|
hejsztynx marked this conversation as resolved.
|
||
| 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 | ||
|
hejsztynx marked this conversation as resolved.
|
||
| 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 - enables it so the next | ||
| characters you type come out styled. This family includes: | ||
|
|
||
| - **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)` | ||
| - **Text alignment** - `setTextAlignment(alignment: 'left' | 'center' | 'right' | 'justify' | 'auto')` | ||
|
|
||
|
hejsztynx marked this conversation as resolved.
|
||
| :::note | ||
|
|
||
| We will not touch list styles or text alignment for now - we'll cover them in-depth in respectively | ||
| [Lists](/rich-text-formatting/lists) and [Text alignment](/rich-text-formatting/text-alignment) sections. | ||
|
|
||
| ::: | ||
|
|
||
| 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 toolbar 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() }, | ||
| ]; | ||
| ``` | ||
|
Comment on lines
+87
to
+100
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would remove
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure, as that code would not match the actual code of the example component. It might be more confusing if the code is not correct by itself and not fully copy-pasteable |
||
|
|
||
| ## 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. | ||
|
|
||
| <InteractiveExample src={BasicStylesEditorSrc} component={BasicStylesEditor} /> | ||
|
hejsztynx marked this conversation as resolved.
|
||
|
|
||
| :::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). | ||
|
|
||
| ::: | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 gets inserted 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. | ||
|
|
||
| <InteractiveExample src={ImagesEditorSrc} component={ImagesEditor} /> | ||
|
|
||
| :::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. | ||
|
|
||
| ::: |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| --- | ||
| 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 URLs like `enriched://home`: | ||
|
|
||
| ```tsx | ||
| // Detect "enriched://" URLs as links. | ||
| const linkRegex = /enriched:\/\/\S+/g; | ||
|
|
||
| <EnrichedTextInput linkRegex={linkRegex} /* ... */ />; | ||
| ``` | ||
|
|
||
| :::tip | ||
|
|
||
| The detected links don't have to be an actual URL scheme like `https://` or `enriched://`. | ||
| You can freely customize the `linkRegex`, e.g. by setting it to `issue-\d+`, which will | ||
| detect specific phrases, such as `issue-123`. | ||
|
|
||
| ::: | ||
|
|
||
| :::tip | ||
|
|
||
| You can pass `null` to turn automatic detection | ||
| off entirely. | ||
|
|
||
| ::: | ||
|
|
||
| :::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. | ||
|
|
||
| ::: | ||
|
|
||
| ## 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, pointing at the same `url`. | ||
|
|
||
| :::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<OnChangeSelectionEvent | null>(null); | ||
|
|
||
| // ... | ||
| <EnrichedTextInput | ||
| onChangeSelection={e => 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. | ||
|
|
||
| ::: | ||
|
|
||
| :::tip | ||
|
|
||
| To strip a link while keeping its text, | ||
| you can call `removeLink(start, end)`. | ||
|
|
||
| ::: | ||
|
|
||
| :::caution | ||
|
|
||
| If you are using links with custom URL schemes, e.g. `custom://`, the `linkRegex` used | ||
| for auto-detection might not be enough. On web the built-in sanitization | ||
| will strip your `href` attributes. To prevent this, you also need | ||
| to use `sanitizationConfig` to whitelist your custom scheme. | ||
|
|
||
| ::: | ||
|
|
||
| ## Try it out | ||
|
|
||
| The editor below uses a custom `linkRegex` that autolinks `enriched://` URLs - 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. | ||
|
|
||
| <InteractiveExample src={LinksEditorSrc} component={LinksEditor} /> | ||
|
hejsztynx marked this conversation as resolved.
|
||
|
|
||
| ## Handling links with events | ||
|
|
||
| There are two events that allows you to handle user interaction with links, one in each component. | ||
|
|
||
| In `EnrichedTextInput`: | ||
| - **`onLinkDetected`** fires when the cursor enters or leaves a created link. | ||
|
hejsztynx marked this conversation as resolved.
|
||
|
|
||
| In `EnrichedText`: | ||
| - **`onLinkPress`** fires when the user presses a link. | ||
|
|
||
| :::info | ||
|
|
||
| This page covers creating and detecting links. To change how | ||
| they look, see [Styling the input](/core-functionalities/styling-the-input). | ||
|
|
||
| ::: | ||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.