A React component that lets you combine editable text with any component using annotated text.
- Powerful annotations tool: add, edit, remove, visualize
- TypeScript
- Support for any components
- Flexible and customizable
- Two ways to configure
- Helpers for processing text
- Hooks for advanced components
- Button handling (Left, Right, Delete, Backspace, Esc)
- Overlay with the suggestions component by default
- Zero dependencies
- Cross selection
You can install the package via npm:
npm install rc-marked-input
There are many examples available in the Storybook. You can also try a template on CodeSandbox.
Here are a few examples to get you started:
import {MarkedInput} from "rc-marked-input";
const Mark = (props) => <mark onClick={_ => alert(props.value)}>{props.label}</mark>
const Marked = () => {
const [value, setValue] = useState("Hello, clickable marked @[world](Hello! Hello!)!")
return <MarkedInput Mark={Mark} value={value} onChange={setValue}/>
}
The library allows you to configure the MarkedInput
component in two ways.
Let's declare markups and suggestions data:
const Data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth"]
const AnotherData = ["Seventh", "Eight", "Ninth"]
const Primary = "@[__label__](primary:__value__)"
const Default = "@[__label__](default)"
Using the components
import {MarkedInput} from "rc-marked-input";
export const App = () => {
const [value, setValue] = useState(
"Enter the '@' for creating @[Primary Mark](primary:Hello!) or '/' for @[Default mark](default)!"
)
return (
<MarkedInput Mark={Button} value={value} onChange={setValue} options={[{
markup: Primary,
data: Data,
initMark: ({label, value}) => ({label, primary: true, onClick: () => alert(value)})
}, {
trigger: '/',
markup: Default,
data: AnotherData
}]}/>
)
}
Using the createMarkedInput
:
import {createMarkedInput} from "rc-marked-input";
const ConfiguredMarkedInput = createMarkedInput({
Mark: Button,
options: [{
markup: Primary,
data: ['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth'],
initMark: ({label, value}) => ({label, primary: true, onClick: () => alert(value)})
}, {
markup: Default,
trigger: '/',
data: ['Seventh', 'Eight', 'Ninth'],
initMark: ({label}) => ({label})
}],
})
const App = () => {
const [value, setValue] = useState(
"Enter the '@' for creating @[Primary Mark](primary:Hello!) or '/' for @[Default mark](default)!"
)
return <ConfiguredMarkedInput value={value} onChange={setValue}/>
}
Marks can be dynamic: editable, removable, etc. via the useMark
hook helper.
import {MarkedInput, useMark} from "rc-marked-input";
const Mark = () => {
const {label, change} = useMark()
const handleInput = (e) =>
change({label: e.currentTarget.textContent ?? "", value: " "}, {silent: true})
return <mark contentEditable onInput={handleInput} children={label}/>
}
export const Dynamic = () => {
const [value, setValue] = useState("Hello, dynamical mark @[world]( )!")
return <MarkedInput Mark={Mark} value={value} onChange={setValue}/>
}
Note: The silent option used to prevent re-rendering itself.
const RemovableMark = () => {
const {label, remove} = useMark()
return <mark onClick={remove} children={label}/>
}
export const Removable = () => {
const [value, setValue] = useState("I @[contain]( ) @[removable]( ) by click @[marks]( )!")
return <MarkedInput Mark={RemovableMark} value={value} onChange={setValue}/>
}
If passed the ref
prop of the useMark
hook in ref of a component then it component can be focused by key operations.
A default overlay is the suggestion component, but it can be easily replaced for any other.
export const DefaultOverlay = () => {
const [value, setValue] = useState("Hello, default - suggestion overlay by trigger @!")
return <MarkedInput Mark={Mark} value={value} onChange={setValue} options={[{data:['First', 'Second', 'Third']}]}/>
}
const Overlay = () => <h1>I am the overlay</h1>
export const CustomOverlay = () => {
const [value, setValue] = useState("Hello, custom overlay by trigger @!")
return <MarkedInput Mark={Mark} Overlay={Overlay} value={value} onChange={setValue}/>
}
export const CustomTrigger = () => {
const [value, setValue] = useState("Hello, custom overlay by trigger /!")
return <MarkedInput Mark={() => null} Overlay={Overlay} value={value} onChange={setValue} options={[{trigger: '/'}]}/>
}
The useOverlay
has a left and right absolute coordinate of a current caret position in the style
prop.
const Tooltip = () => {
const {style} = useOverlay()
return <div style={{position: 'absolute', ...style}}>I am the overlay</div>;
}
export const PositionedOverlay = () => {
const [value, setValue] = useState("Hello, positioned overlay by trigger @!")
return <MarkedInput Mark={Mark} Overlay={Tooltip} value={value} onChange={setValue}/>
}
The useOverlay
hook provide some methods like select
for creating a new annotation.
const List = () => {
const {select} = useOverlay()
return <ul>
<li onClick={() => select({label: 'First'})}>Clickable First</li>
<li onClick={() => select({label: 'Second'})}>Clickable Second</li>
</ul>;
}
export const SelectableOverlay = () => {
const [value, setValue] = useState("Hello, suggest overlay by trigger @!")
return <MarkedInput Mark={Mark} Overlay={List} value={value} onChange={setValue}/>
}
Note: Recommend to pass the
ref
for an overlay component. It used to detect outside click.
The children
prop allows to pass elements to overrides internal components.
The div
tag for container. The span
tag for text cell.
<ConfiguredMarkedInput value={value} onChange={setValue}>
<div
onClick = {(e) => console.log('onCLick')}
onInput = {(e) => console.log('onInput')}
/* other props */
onBlur = {(e) => console.log('onBlur')}
onFocus = {(e) => console.log('onFocus')}
onKeyDown={(e) => console.log('onKeyDown')}
/>
<span className='span-class'/>
</>
<MarkedInput Mark={Mark} Overlay={Overlay} value={value} onChange={setValue}> option={[{
trigger: '@',
markup: '@[__label__](__value__)',
data: Data,
initMark: getCustomMarkProps,
}, {
trigger: '/',
markup: '@(__label__)[__value__]',
data: AnotherData,
initMark: getAnotherCustomMarkProps,
}]}/>
Or
const MarkedInput = createMarkedInput({Mark, Overlay, options: [{
trigger: '@',
markup: '@[__label__](__value__)',
data: Data,
initMark: getCustomMarkProps,
}, {
trigger: '/',
markup: '@(__label__)[__value__]',
data: AnotherData,
initMark: getAnotherCustomMarkProps,
}]})
const App = () => <MarkedInput value={value} onChange={setValue}/>
Name | Type | Default | Description |
---|---|---|---|
value | string | Annotated text with markups for mark | |
onChange | (value: string) => void | Change event | |
Mark | ComponentType<T = MarkProps> | undefined |
Component that used for render markups |
Overlay | ComponentType | Suggestions |
Component that is rendered by trigger |
readOnly | boolean | undefined |
Prevents from changing the value |
options | OptionProps[] | [{}] |
Passed options for configure |
trigger | OverlayTrigger | change |
Triggering events for overlay |
Name | Type | Description |
---|---|---|
createMarkedInput | <T = MarkStruct>(configs: MarkedInputProps): ConfiguredMarkedInput | Create the configured MarkedInput component. |
annotate | (markup: Markup, label: string, value?: string) => string | Make annotation from the markup |
denote | (value: string, callback: (mark: Mark) => string, ...markups: Markup[]) => string | Transform the annotated text |
useMark | () => MarkHandler | Allow to use dynamic mark |
useOverlay | () => OverlayHandler | Use overlay props |
useListener | (type, listener, deps) => void | Event listener |
type OverlayTrigger =
| Array<'change' | 'selectionChange'>
| 'change'
| 'selectionChange'
| 'none';
interface MarkStruct {
label: string
value?: string
}
interface OverlayHandler {
/**
* Style with caret absolute position. Used for placing an overlay.
*/
style: {
left: number;
top: number;
};
/**
* Used for close overlay.
*/
close: () => void;
/**
* Used for insert an annotation instead a triggered value.
*/
select: (value: MarkStruct) => void;
/**
* Overlay match details
*/
match: OverlayMatch;
ref: RefObject<HTMLElement>;
}
interface MarkHandler<T> extends MarkStruct {
/**
* MarkStruct ref. Used for focusing and key handling operations.
*/
ref: RefObject<T>
/**
* Change mark.
* @options.silent doesn't change itself label and value, only pass change event.
*/
change: (props: MarkStruct, options?: { silent: boolean }) => void
/**
* Remove itself.
*/
remove: () => void
/**
* Passed the readOnly prop value
*/
readOnly?: boolean
}
type OverlayMatch = {
/**
* Found value via a overlayMatch
*/
value: string;
/**
* Triggered value
*/
source: string;
/**
* Piece of text, in which was a overlayMatch
*/
span: string;
/**
* Html element, in which was a overlayMatch
*/
node: Node;
/**
* Start position of a overlayMatch
*/
index: number;
/**
* OverlayMatch's option
*/
option: Option;
}
export interface Option<T = Record<string, any>> {
/**
* Template string instead of which the mark is rendered.
* Must contain placeholders: `__label__` and optional `__value__`
* @default "@[__label__](__value__)"
*/
markup?: Markup;
/**
* Sequence of symbols for calling the overlay.
* @default "@"
*/
trigger?: string;
/**
* Data for an overlay component. By default, it is suggestions.
*/
data?: string[];
/**
* Function to initialize props for the mark component. Gets arguments from found markup
*/
initMark?: (props: MarkStruct) => T;
}
If you want to contribute, you are welcome! Create an issue or start a discussion.