diff --git a/docs/docs/api-reference/enriched-text-input.md b/docs/docs/api-reference/enriched-text-input.md index 382e60a39..eb4339d43 100644 --- a/docs/docs/api-reference/enriched-text-input.md +++ b/docs/docs/api-reference/enriched-text-input.md @@ -347,6 +347,12 @@ interface OnChangeStateEvent { mention: StyleState; checkboxList: StyleState; alignment: string; + customStyle: { + foregroundColor: string; + backgroundColor: string; + fontSize: number; + fontFamily: string; + }; } ``` @@ -356,6 +362,10 @@ interface OnChangeStateEvent { - `isConflicting` - toggling the style removes a conflicting active style - `alignment` - current text alignment of the paragraph at the cursor: `'left'`, `'center'`, `'right'`, `'justify'`, or `'auto'` +- `customStyle` - the [custom style](/rich-text-formatting/custom-style) values + within the current selection. A field that isn't set, or + differs across the selection, is empty - `''` for strings and `0` for + `fontSize`. Colors are reported as hex strings (e.g. `#FF0000`) :::note @@ -973,6 +983,27 @@ up with natural alignment, the same as `'auto'`. ::: +### `.setStyle()` + +```ts +setStyle: (customStyle: { + foregroundColor?: ColorValue | null; + backgroundColor?: ColorValue | null; + fontSize?: number | null; + fontFamily?: string | null; +}) => void; +``` + +Sets the [custom style](/rich-text-formatting/custom-style) at the current +selection. Omitting a field leaves the given formatting untouched, whereas passing `null` for a field removes it. + +- `foregroundColor` - text color; use a hex (e.g. `#FFE066`) or named (e.g. + `'red'`) color +- `backgroundColor` - text highlight color; use a hex (e.g. `#FFE066`) or named + (e.g. `'red'`) color +- `fontSize` - font size of the text +- `fontFamily` - font family of the text + ### `.startMention()` ```ts diff --git a/docs/docs/fundamentals/core-concepts.md b/docs/docs/fundamentals/core-concepts.md index 52fd5593b..e391df2b9 100644 --- a/docs/docs/fundamentals/core-concepts.md +++ b/docs/docs/fundamentals/core-concepts.md @@ -71,7 +71,7 @@ The HTML format that both components expect is identical, what allows you to int ## The style state model -Not every style can be combined with every other. For example, a paragraph can't be both a heading and a list item, code blocks don't support inline formatting such as bold or italic. The editor tracks this and reports it through `onChangeState`, which gives each style three booleans: +Not every style can be combined with every other. For example, a paragraph can't be both a heading and a list item, code blocks don't support inline formatting such as bold or italic. The editor tracks this and reports it through `onChangeState`, which gives the majority of styles three booleans: - **`isActive`** - whether the style is applied at the current selection. Use it to highlight a toolbar button. @@ -82,6 +82,8 @@ Not every style can be combined with every other. For example, a paragraph can't For example, switching a blockquote paragraph to a heading removes the blockquote. Use it to hint that the toggle is a swap, not an addition. +Only [text alignment](/rich-text-formatting/text-alignment) and [custom style](/rich-text-formatting/custom-style) do not expose those three booleans in the `onChangeState` event, as they can be applied on any existing style. Instead, they simply carry their current value. To see the exact shape of the event's payload, see the `onChangeState` [API reference](/api-reference/enriched-text-input#onchangestate). + Driving your toolbar from these three flags keeps the UI honest: buttons light up and grey out according to the editor's state. diff --git a/docs/docs/fundamentals/html-format-and-supported-tags.md b/docs/docs/fundamentals/html-format-and-supported-tags.md index 962dae581..ce2488e26 100644 --- a/docs/docs/fundamentals/html-format-and-supported-tags.md +++ b/docs/docs/fundamentals/html-format-and-supported-tags.md @@ -32,6 +32,10 @@ Both show up in the [`onChangeState`](/fundamentals/core-concepts#the-style-stat | Link | `` | ``, ``, `` | ``, `` | | Mention | `` | ``, `` | ``, `` | | Image | `` | ``, `` | `` | +| Custom style | `` | -- | -- | + +`` carries the [custom style](/rich-text-formatting/custom-style) in its +`style` attribute - `color`, `background-color`, `font-size` and `font-family`. :::note diff --git a/docs/docs/rich-text-formatting/basic-styles.mdx b/docs/docs/rich-text-formatting/basic-styles.mdx index ce52600e2..41e9b8395 100644 --- a/docs/docs/rich-text-formatting/basic-styles.mdx +++ b/docs/docs/rich-text-formatting/basic-styles.mdx @@ -35,7 +35,7 @@ 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. +There are a few more inline styles, which you cannot toggle quite like the others - **custom style**, **links** and **mentions**. You'll learn about them in their respective sections, once you finish this chapter. ::: diff --git a/docs/docs/rich-text-formatting/custom-style.mdx b/docs/docs/rich-text-formatting/custom-style.mdx new file mode 100644 index 000000000..4c3789f25 --- /dev/null +++ b/docs/docs/rich-text-formatting/custom-style.mdx @@ -0,0 +1,98 @@ +--- +sidebar_position: 2 +--- + +import InteractiveExample from '@site/src/components/InteractiveExample'; +import CustomStyleEditor from '@site/src/examples/CustomStyleEditor'; +import CustomStyleEditorSrc from '!!raw-loader!@site/src/examples/CustomStyleEditor'; + +# Custom style + +Aside from the predefined [basic styles](/rich-text-formatting/basic-styles) +you can apply a **custom style**, which consists of four pieces of formatting: +text color, highlight, font size and font family. Similarly as the style-toggling +methods, it applies to the current input's selection. + +## Setting a custom style + +`setStyle` accepts an object with four optional fields: + +```tsx +setStyle( + { + foregroundColor?: string, + backgroundColor?: string, + fontSize?: number, + fontFamily?: string, + } +); +``` + +Each call respects the already present custom style and modifies it with whatever +fields you pass. For example, if you want to remove a single field, pass `null`: + +```tsx +// Remove the highlight, keep the color, size and font as they are. +ref.current?.setStyle({ backgroundColor: null }); +``` + +Custom style doesn't conflict with or block any other style, so it combines +freely with both inline styles and paragraph styles. + +:::note + +If a current style modifies the same thing that **custom style** sets, we have the following priority: +**paragraph style** < **custom style** < **inline style**. E.g. if you try to apply `foregroundColor` custom style to a text with a link (which is an inline style), the link's foreground color stays. + +::: + +## Reading the current style + +Since custom style carries values rather than a simple on/off flag, `onChangeState` +reports it as a separate `customStyle` object instead of the usual +`isActive` / `isBlocking` / `isConflicting` +[booleans](/fundamentals/core-concepts#the-style-state-model): + +```ts +customStyle: { + foregroundColor: string; + backgroundColor: string; + fontSize: number; + fontFamily: string; +} +``` + +Each field holds the value at the current selection. When a field isn't set, it comes back +empty: `''` for strings and `0` for `fontSize`. That makes it easy to check whether a field +is applied: + +```tsx +const customStyleButtons = [ + { + label: 'Color', + isActive: !!state?.customStyle.foregroundColor, + onPress: () => ref.current?.setStyle({ foregroundColor: '#e5484d' }), + }, + { + label: 'Font size', + isActive: !!state?.customStyle.fontSize, + onPress: () => ref.current?.setStyle({ fontSize: 26 }), + }, +]; +``` + +:::tip + +Pass colors to `setStyle` as hex or named colors (e.g. `'red'`). They are +always reported as uppercase hex strings - `#RRGGBB`, or `#RRGGBBAA`. + +::: + +## Try it out + +Each button below sets one field of the custom style on the selection, and +removes it when the field is already applied. Select a word and give it a color, +then a highlight - notice how the second call doesn't touch the first one. Try +mixing them with different fonts and sizes. + + diff --git a/docs/docs/rich-text-formatting/inline-images.mdx b/docs/docs/rich-text-formatting/inline-images.mdx index 4677ad594..a88b448a5 100644 --- a/docs/docs/rich-text-formatting/inline-images.mdx +++ b/docs/docs/rich-text-formatting/inline-images.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 6 --- import InteractiveExample from '@site/src/components/InteractiveExample'; diff --git a/docs/docs/rich-text-formatting/links.mdx b/docs/docs/rich-text-formatting/links.mdx index 556ace638..7df5a5070 100644 --- a/docs/docs/rich-text-formatting/links.mdx +++ b/docs/docs/rich-text-formatting/links.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 --- import InteractiveExample from '@site/src/components/InteractiveExample'; diff --git a/docs/docs/rich-text-formatting/lists.mdx b/docs/docs/rich-text-formatting/lists.mdx index 118cf1d57..c8beb3338 100644 --- a/docs/docs/rich-text-formatting/lists.mdx +++ b/docs/docs/rich-text-formatting/lists.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 4 +sidebar_position: 5 --- import InteractiveExample from '@site/src/components/InteractiveExample'; diff --git a/docs/docs/rich-text-formatting/mentions.mdx b/docs/docs/rich-text-formatting/mentions.mdx index 147a1d1e8..17142018d 100644 --- a/docs/docs/rich-text-formatting/mentions.mdx +++ b/docs/docs/rich-text-formatting/mentions.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 4 --- import InteractiveExample from '@site/src/components/InteractiveExample'; diff --git a/docs/docs/rich-text-formatting/text-alignment.mdx b/docs/docs/rich-text-formatting/text-alignment.mdx index 9bca7329e..9dc27f4cd 100644 --- a/docs/docs/rich-text-formatting/text-alignment.mdx +++ b/docs/docs/rich-text-formatting/text-alignment.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 --- import InteractiveExample from '@site/src/components/InteractiveExample'; diff --git a/docs/docs/rich-text-formatting/text-shortcuts.mdx b/docs/docs/rich-text-formatting/text-shortcuts.mdx index 4a5e19938..551b36a73 100644 --- a/docs/docs/rich-text-formatting/text-shortcuts.mdx +++ b/docs/docs/rich-text-formatting/text-shortcuts.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 7 +sidebar_position: 8 --- import InteractiveExample from '@site/src/components/InteractiveExample'; diff --git a/docs/src/examples/CustomStyleEditor.tsx b/docs/src/examples/CustomStyleEditor.tsx new file mode 100644 index 000000000..c52e5fc09 --- /dev/null +++ b/docs/src/examples/CustomStyleEditor.tsx @@ -0,0 +1,117 @@ +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'; +import { htmlStyle } from './htmlStyle'; + +const TEXT_COLOR = '#ff8560'; +const HIGHLIGHT_COLOR = '#c1c6e5'; +const FONT_SIZE = 26; +const FONT_FAMILY = 'cursive'; + +export default function App() { + const ref = useRef(null); + const [state, setState] = useState(null); + + const customStyle = state?.customStyle; + + const buttons = [ + { + label: 'Color', + isActive: !!customStyle?.foregroundColor, + onPress: () => + ref.current?.setStyle({ + foregroundColor: customStyle?.foregroundColor ? null : TEXT_COLOR, + }), + }, + { + label: 'Highlight', + isActive: !!customStyle?.backgroundColor, + onPress: () => + ref.current?.setStyle({ + backgroundColor: customStyle?.backgroundColor + ? null + : HIGHLIGHT_COLOR, + }), + }, + { + label: 'Font size', + isActive: !!customStyle?.fontSize, + onPress: () => + ref.current?.setStyle({ + fontSize: customStyle?.fontSize ? null : FONT_SIZE, + }), + }, + { + label: 'Font family', + isActive: !!customStyle?.fontFamily, + onPress: () => + ref.current?.setStyle({ + fontFamily: customStyle?.fontFamily ? null : FONT_FAMILY, + }), + }, + ]; + + return ( + + setState(e.nativeEvent)} + /> + + {buttons.map(button => ( + + + {button.label} + + + ))} + + + ); +} + +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: 120, + padding: 8, + borderRadius: 24, + borderWidth: 1, + borderColor: '#919fcf', + }, + buttonActive: { + borderColor: '#57b495', + backgroundColor: '#57b495', + }, + text: { + textAlign: 'center', + color: '#919fcf', + }, + textActive: { + color: '#eef0ff', + }, +});