-
Notifications
You must be signed in to change notification settings - Fork 63
docs: guides section #717
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
docs: guides section #717
Changes from all commits
c1b6daa
b216109
56a2cb4
9752471
50479b6
4c82b0b
89bb38f
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 |
|---|---|---|
| @@ -1,7 +1,138 @@ | ||
| --- | ||
| sidebar_position: 4 | ||
| sidebar_position: 3 | ||
| --- | ||
|
|
||
| # Custom context menu | ||
|
|
||
| <!-- TODO: write content for this page --> | ||
| The `contextMenuItems` prop lets you add your own actions to the native | ||
| text-selection menu - the popover that e.g. shows **Copy / Paste / Cut** when the | ||
| user long-presses selected text. | ||
|
|
||
| :::info | ||
|
|
||
| This is a **native-only** feature (iOS and Android) - which | ||
| is why this page has no live preview. To see it in action, run the snippet below in the | ||
| [example app](https://github.com/software-mansion/react-native-enriched-html/tree/main/apps/example). | ||
|
|
||
| ::: | ||
|
|
||
| ## The shape of an item | ||
|
|
||
| ```ts | ||
| interface ContextMenuItem { | ||
| text: string; // the label shown in the menu | ||
| visible?: boolean; // whether to show it (defaults to true) | ||
| onPress: (args: { | ||
| text: string; // the currently selected text | ||
| selection: { start: number; end: number }; // its range | ||
| styleState: OnChangeStateEvent; // active styles | ||
| }) => void; | ||
| } | ||
|
hejsztynx marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| Every `onPress` receives the same three-field payload, resolved at the moment | ||
| the item is tapped: | ||
|
|
||
| - **`text`** - the selected text. | ||
| - **`selection`** - the `start` and `end` offsets of the selection. | ||
| - **`styleState`** - the latest style state, the same object you get from | ||
| `onChangeState`. | ||
|
|
||
| `visible` is read when the menu opens, so you can drive it from state to show an | ||
| item only in the right context. | ||
|
|
||
| ## Example | ||
|
|
||
| This editor adds three items. The first two read the selection payload and run an | ||
| editor command. The third links the selection, so it's only shown via `visible` | ||
| when there's actually a ranged selection to link: | ||
|
|
||
| ```tsx | ||
| import { EnrichedTextInput } from 'react-native-enriched-html'; | ||
| import type { | ||
| ContextMenuItem, | ||
| EnrichedTextInputInstance, | ||
| OnChangeSelectionEvent, | ||
| } from 'react-native-enriched-html'; | ||
| import { useMemo, useRef, useState } from 'react'; | ||
| import { View, StyleSheet, Alert } from 'react-native'; | ||
|
|
||
| export default function App() { | ||
| const ref = useRef<EnrichedTextInputInstance>(null); | ||
| const [selection, setSelection] = useState<OnChangeSelectionEvent | null>( | ||
| null | ||
| ); | ||
|
|
||
| const hasRangedSelection = !!selection && selection.start !== selection.end; | ||
|
|
||
| const contextMenuItems: ContextMenuItem[] = useMemo( | ||
|
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 think |
||
| () => [ | ||
| { | ||
| // `text` and `selection` describe what the user long-pressed. | ||
| text: 'Show selection', | ||
| onPress: ({ text, selection: range }) => { | ||
| Alert.alert( | ||
| 'Selection', | ||
| `"${text}" at [${range.start}, ${range.end}]` | ||
| ); | ||
| }, | ||
| }, | ||
| { | ||
| // Menu items can call any editor command through the ref. | ||
| text: 'Bold', | ||
| onPress: () => { | ||
| ref.current?.toggleBold(); | ||
| }, | ||
| }, | ||
| { | ||
| // Only useful with a ranged selection, so hide it otherwise; when | ||
| // shown, `selection` lets you target the exact range you were given. | ||
| text: 'Link to Software Mansion', | ||
| visible: hasRangedSelection, | ||
| onPress: ({ text, selection: range }) => { | ||
| ref.current?.setLink( | ||
| range.start, | ||
| range.end, | ||
| text, | ||
| 'https://swmansion.com' | ||
| ); | ||
| }, | ||
| }, | ||
| ], | ||
| [hasRangedSelection] | ||
| ); | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| <EnrichedTextInput | ||
| ref={ref} | ||
| style={styles.input} | ||
| placeholder="Select some text, then long-press it..." | ||
| contextMenuItems={contextMenuItems} | ||
| onChangeSelection={(e) => setSelection(e.nativeEvent)} | ||
| /> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { gap: 12 }, | ||
| input: { | ||
| fontSize: 18, | ||
| color: '#232736', | ||
| padding: 12, | ||
| borderRadius: 12, | ||
| minHeight: 96, | ||
| backgroundColor: '#eef0ff', | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| :::note | ||
|
|
||
| Item placement differs per platform. On **iOS** your items appear in array | ||
| order, before the system items (Copy/Paste/Cut). On **Android** there is no | ||
| guaranteed order, and depending on the device manufacturer your items may be | ||
| tucked into an overflow submenu. | ||
|
|
||
| ::: | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| --- | ||
| sidebar_position: 2 | ||
| --- | ||
|
|
||
| import InteractiveExample from '@site/src/components/InteractiveExample'; | ||
| import EmojiEditor from '@site/src/examples/EmojiEditor'; | ||
| import EmojiEditorSrc from '!!raw-loader!@site/src/examples/EmojiEditor'; | ||
|
|
||
| # Emojis | ||
|
|
||
| Mentions are powerful enough that you can build much more with them. An emoji picker is just a mention where the display text does not match the query. We'll use `:` as the indicator: the user types `:smile`, picks from a list, and the shortcode is | ||
| replaced with the emoji itself. If the mention events are new to you, start with | ||
| [Mentions](/rich-text-formatting/mentions) and the | ||
| [User and channel mentions](/guides/user-and-channel-mentions) guide; this page reuses the | ||
| same flow. | ||
|
|
||
| ## A shortcode table | ||
|
|
||
| Map each shortcode to the glyph it inserts: | ||
|
|
||
| ```tsx | ||
| const EMOJIS = [ | ||
| { shortcode: 'smile', char: '😄' }, | ||
| { shortcode: 'heart', char: '❤️' }, | ||
| { shortcode: 'fire', char: '🔥' }, | ||
| { shortcode: 'rocket', char: '🚀' }, | ||
| ]; | ||
| ``` | ||
|
|
||
| ## Wiring it up | ||
|
|
||
| Register `:` as the only indicator and filter the table with the text typed | ||
| after it. Because people habitually close the shortcode (`:smile:`), strip a | ||
| trailing colon before matching: | ||
|
|
||
| ```tsx | ||
| <EnrichedTextInput | ||
| mentionIndicators={[':']} | ||
| onStartMention={() => setOpen(true)} | ||
| onChangeMention={({ text }) => setQuery(text)} | ||
| onEndMention={() => setOpen(false)} | ||
| // ... | ||
| />; | ||
|
|
||
| const q = query.replace(/:$/, '').toLowerCase(); | ||
| const suggestions = EMOJIS.filter(e => e.shortcode.startsWith(q)); | ||
| ``` | ||
|
|
||
| When the user picks, `setMention` inserts the glyph as the mention's display | ||
| text. The `data-shortcode` makes a handy attribute if you ever need to reconstruct it: | ||
|
|
||
| ```tsx | ||
| const pick = (emoji) => { | ||
| ref.current?.setMention(':', emoji.char, { | ||
| 'data-shortcode': emoji.shortcode, | ||
| }); | ||
| }; | ||
| ``` | ||
|
|
||
| Since the emoji glyph is the whole mention, drop the usual highlight so it | ||
| reads as plain text: | ||
|
|
||
| ```tsx | ||
| const htmlStyle = { | ||
| mention: { | ||
| ':': { | ||
| color: '#232736', | ||
| backgroundColor: 'transparent', | ||
| textDecorationLine: 'none', | ||
| }, | ||
| }, | ||
| }; | ||
| ``` | ||
|
|
||
| ## Try it out | ||
|
|
||
| Type `:` followed by a name - `:fire`, `:heart` - then tap a suggestion. The shortcode is replaced with a single emoji that you can place anywhere in the text. | ||
|
|
||
| <InteractiveExample src={EmojiEditorSrc} component={EmojiEditor} /> | ||
|
|
||
| :::info | ||
|
|
||
| If you want to see the whole code used to build this example, you can find it by switching the tab from `Preview` to `Code`. | ||
|
|
||
| ::: | ||
|
Comment on lines
+81
to
+85
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. Same here, I think it's redundant |
||
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,112 @@ | ||||||||||||
| --- | ||||||||||||
| sidebar_position: 1 | ||||||||||||
| --- | ||||||||||||
|
|
||||||||||||
| import InteractiveExample from '@site/src/components/InteractiveExample'; | ||||||||||||
| import MentionOnlyEditor from '@site/src/examples/MentionOnlyEditor'; | ||||||||||||
| import MentionOnlyEditorSrc from '!!raw-loader!@site/src/examples/MentionOnlyEditor'; | ||||||||||||
|
|
||||||||||||
| # User and channel mentions | ||||||||||||
|
|
||||||||||||
| This guide wires the mention events into a complete picker - the flow behind a | ||||||||||||
| chat composer where typing `@` suggests people and `#` suggests channels. If you | ||||||||||||
| haven't met the mention API yet, read | ||||||||||||
| [Mentions](/rich-text-formatting/mentions) first; here we move quickly and | ||||||||||||
| assume the events and `setMention` are familiar. | ||||||||||||
|
|
||||||||||||
| ## The data behind a mention | ||||||||||||
|
|
||||||||||||
| Mentions are particularly useful if they point at something. Keep two lists around - one | ||||||||||||
| for users, one for channels - each item carrying an `id` you can attach | ||||||||||||
| to the finished mention: | ||||||||||||
|
|
||||||||||||
| ```tsx | ||||||||||||
| const USERS = [ | ||||||||||||
| { id: 'u1', name: 'John Doe' }, | ||||||||||||
| { id: 'u2', name: 'Jane Smith' }, | ||||||||||||
| { id: 'u3', name: 'Alice Johnson' }, | ||||||||||||
| { id: 'u4', name: 'Bob Brown' }, | ||||||||||||
| ]; | ||||||||||||
|
|
||||||||||||
| const CHANNELS = [ | ||||||||||||
| { id: 'c1', name: 'general' }, | ||||||||||||
| { id: 'c2', name: 'engineering' }, | ||||||||||||
| { id: 'c3', name: 'random' }, | ||||||||||||
| { id: 'c4', name: 'announcements' }, | ||||||||||||
| ]; | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| ## Wiring the picker | ||||||||||||
|
|
||||||||||||
| Now let's register both indicators and the events callbacks: | ||||||||||||
|
|
||||||||||||
| ```tsx | ||||||||||||
| <EnrichedTextInput | ||||||||||||
| mentionIndicators={['@', '#']} | ||||||||||||
| onStartMention={openPicker} // fired when '@' or '#' is typed | ||||||||||||
| onChangeMention={updateQuery} // fired on every keystroke after an indicator | ||||||||||||
| onEndMention={closePicker} // fired when the mention is left | ||||||||||||
| // ... | ||||||||||||
| /> | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| `onStartMention` hands you the indicator, so you know whether to show people or | ||||||||||||
| channels. `onChangeMention` hands you the `text` typed so far - filter your list | ||||||||||||
| with it. `onEndMention` fires when the cursor leaves the mention, so you dismiss | ||||||||||||
| the list. | ||||||||||||
|
|
||||||||||||
| When the user taps a suggestion, finish the mention with `setMention`. Pass the | ||||||||||||
| same indicator that started it, the display text, and | ||||||||||||
| the item's data as attributes: | ||||||||||||
|
|
||||||||||||
| ```tsx | ||||||||||||
| const pick = (item) => { | ||||||||||||
| ref.current?.setMention(indicator, `${indicator}${item.name}`, { | ||||||||||||
| id: item.id, | ||||||||||||
| }); | ||||||||||||
| }; | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| Now let's give each indicator its own look through | ||||||||||||
| `htmlStyle.mention`: | ||||||||||||
|
|
||||||||||||
| ```tsx | ||||||||||||
| const htmlStyle = { | ||||||||||||
| mention: { | ||||||||||||
| '@': { color: '#2b7a4b', backgroundColor: '#d8f3e3' }, | ||||||||||||
| '#': { color: '#2b5f9e', backgroundColor: '#d8e6f9' }, | ||||||||||||
| }, | ||||||||||||
| }; | ||||||||||||
| ``` | ||||||||||||
|
|
||||||||||||
| ## Try it out | ||||||||||||
|
|
||||||||||||
| Type `@` to filter people or `#` to filter channels, keep typing to narrow the | ||||||||||||
| list, then tap a suggestion. | ||||||||||||
|
|
||||||||||||
| <InteractiveExample src={MentionOnlyEditorSrc} component={MentionOnlyEditor} /> | ||||||||||||
|
hejsztynx marked this conversation as resolved.
|
||||||||||||
|
|
||||||||||||
| :::info | ||||||||||||
|
|
||||||||||||
| If you want to see the whole code used to build this example, you can find it by switching the tab from `Preview` to `Code`. | ||||||||||||
|
|
||||||||||||
| ::: | ||||||||||||
|
Comment on lines
+89
to
+93
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. We don't need this explanation, we already have those snippets everywhere in the docs, so user should be familiar with that.
Suggested change
|
||||||||||||
|
|
||||||||||||
| :::tip | ||||||||||||
|
|
||||||||||||
| A mention is only active while the editor is focused - if it blurs, | ||||||||||||
| `onEndMention` fires and `setMention` becomes a no-op. On native, tapping a | ||||||||||||
| suggestion never steals focus, so it just works. On web it does, so the rows | ||||||||||||
| call `preventDefault` on `mousedown` to keep the editor focused. The example | ||||||||||||
| wraps that in a small `keepEditorFocused` helper (see the Code tab). | ||||||||||||
|
|
||||||||||||
| ::: | ||||||||||||
|
|
||||||||||||
| :::note | ||||||||||||
|
|
||||||||||||
| The attributes you pass to `setMention` (here `{ id }`) ride along in the HTML | ||||||||||||
| and survive a round-trip through `getHTML` / `setValue`. Prefix custom keys with | ||||||||||||
| `data-` if they need to outlive a sanitizer - see the note in | ||||||||||||
| [Mentions](/rich-text-formatting/mentions). | ||||||||||||
|
|
||||||||||||
| ::: | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a video/gif then?