Skip to content

Commit 4bf81ec

Browse files
committed
feat: add keyboard shortcuts, read-only mode, focus/blur callbacks, max-length, and word count
1 parent 6d18916 commit 4bf81ec

33 files changed

Lines changed: 3189 additions & 284 deletions

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ tsconfig.json
1111
tools/
1212
node_modules/
1313
package-lock.json
14+
docs/
1415

1516
*.log
1617
.DS_Store

docs/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# @overlap/rte Documentation
2+
3+
Complete documentation for the @overlap/rte Rich Text Editor.
4+
5+
## Table of Contents
6+
7+
| Document | Description |
8+
|---|---|
9+
| [Getting Started](./getting-started.md) | Installation, quick start, basic usage |
10+
| [Editor Props](./editor-props.md) | All available props for the `<Editor>` component |
11+
| [Editor API](./editor-api.md) | Methods available via `onEditorAPIReady` |
12+
| [Plugins](./plugins.md) | Built-in plugins, plugin factories, and creating custom plugins |
13+
| [Settings Object](./settings.md) | Configure features via a single settings object |
14+
| [Keyboard Shortcuts](./keyboard-shortcuts.md) | All keyboard shortcuts |
15+
| [Markdown Shortcuts](./markdown-shortcuts.md) | Auto-formatting via markdown-style triggers |
16+
| [Theming](./theming.md) | CSS variables, theme prop, and class overrides |
17+
| [Features](./features.md) | Complete feature reference |
18+
| [HTML Import/Export](./html-import-export.md) | Working with HTML and the JSON data model |
19+
| [Images](./images.md) | Image upload, drag & drop, paste, and URL insert |
20+
| [Tables](./tables.md) | Table insertion and context menu operations |
21+
| [Links](./links.md) | Link dialog, custom fields, auto-linking, hover tooltip |
22+
| [Custom Plugins](./custom-plugins.md) | How to create your own plugins |
23+
| [Exports](./exports.md) | Full list of everything exported from the package |

docs/custom-plugins.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Creating Custom Plugins
2+
3+
Every toolbar feature in the editor is a plugin. Creating your own is straightforward -- it's just a TypeScript object.
4+
5+
## Plugin Interface
6+
7+
```typescript
8+
interface Plugin {
9+
name: string;
10+
type: "inline" | "block" | "command";
11+
command?: string;
12+
renderButton?: (props: ButtonProps & Record<string, unknown>) => React.ReactNode;
13+
execute?: (editor: EditorAPI, value?: string) => void;
14+
isActive?: (editor: EditorAPI) => boolean;
15+
canExecute?: (editor: EditorAPI) => boolean;
16+
getCurrentValue?: (editor: EditorAPI) => string | undefined;
17+
}
18+
```
19+
20+
## Simple Example: Highlight Plugin
21+
22+
```tsx
23+
import { Plugin, EditorAPI, ButtonProps } from "@overlap/rte";
24+
25+
const highlightPlugin: Plugin = {
26+
name: "highlight",
27+
type: "inline",
28+
29+
renderButton: (props: ButtonProps) => (
30+
<button
31+
onClick={props.onClick}
32+
className={`rte-toolbar-button ${
33+
props.isActive ? "rte-toolbar-button-active" : ""
34+
}`}
35+
title="Highlight"
36+
aria-label="Highlight"
37+
>
38+
H
39+
</button>
40+
),
41+
42+
execute: (editor: EditorAPI) => {
43+
editor.executeCommand("backColor", "#ffff00");
44+
},
45+
46+
isActive: () => {
47+
const sel = document.getSelection();
48+
if (!sel || sel.rangeCount === 0) return false;
49+
const el = sel.getRangeAt(0).commonAncestorContainer;
50+
const parent = el.nodeType === Node.TEXT_NODE
51+
? el.parentElement
52+
: (el as HTMLElement);
53+
return parent?.closest('[style*="background-color: rgb(255, 255, 0)"]') !== null;
54+
},
55+
56+
canExecute: () => true,
57+
};
58+
```
59+
60+
## Using Your Plugin
61+
62+
```tsx
63+
import { Editor, defaultPlugins } from "@overlap/rte";
64+
65+
<Editor plugins={[...defaultPlugins, highlightPlugin]} />
66+
```
67+
68+
## Plugin Fields Explained
69+
70+
### `name` (required)
71+
72+
Unique identifier string. Used as the React key in the toolbar.
73+
74+
### `type` (required)
75+
76+
- `"inline"` -- inline formatting (bold, italic, etc.)
77+
- `"block"` -- block-level formatting (headings, lists, etc.)
78+
- `"command"` -- actions (undo, redo, clear formatting, etc.)
79+
80+
### `renderButton`
81+
82+
Returns the React element for the toolbar button. Receives `ButtonProps`:
83+
84+
```typescript
85+
interface ButtonProps {
86+
isActive: boolean; // true if the format is currently applied
87+
onClick: () => void; // triggers execute()
88+
disabled?: boolean; // true if canExecute() returns false
89+
icon?: string;
90+
label?: string;
91+
}
92+
```
93+
94+
Additional props passed by the toolbar:
95+
- `onSelect: (value: string) => void` -- for dropdowns
96+
- `editorAPI: EditorAPI` -- the editor API
97+
- `currentValue: string | undefined` -- from getCurrentValue()
98+
99+
### `execute`
100+
101+
Called when the button is clicked. The `editor` parameter is the `EditorAPI`. The optional `value` parameter is used for dropdowns (e.g. color picker).
102+
103+
### `isActive`
104+
105+
Return `true` when the current selection has this format applied. This controls the active/highlight state of the toolbar button.
106+
107+
### `canExecute`
108+
109+
Return `false` to disable the button. For example, the indent plugin disables when the cursor is not in a list item.
110+
111+
### `getCurrentValue`
112+
113+
For dropdowns (font size, color), return the current value so the dropdown can show the active selection.
114+
115+
## Advanced Example: Emoji Picker Plugin
116+
117+
```tsx
118+
const emojiPlugin: Plugin = {
119+
name: "emoji",
120+
type: "command",
121+
122+
renderButton: (props: ButtonProps & { onSelect?: (v: string) => void }) => {
123+
const [open, setOpen] = useState(false);
124+
const emojis = ["😀", "😂", "❤️", "👍", "🎉", "🔥", "", "💡"];
125+
126+
return (
127+
<div style={{ position: "relative", display: "inline-block" }}>
128+
<button
129+
onClick={() => setOpen(!open)}
130+
className="rte-toolbar-button"
131+
title="Insert Emoji"
132+
>
133+
😀
134+
</button>
135+
{open && (
136+
<div style={{
137+
position: "absolute",
138+
top: "100%",
139+
display: "grid",
140+
gridTemplateColumns: "repeat(4, 1fr)",
141+
gap: 4,
142+
padding: 8,
143+
background: "white",
144+
border: "1px solid #e5e7eb",
145+
borderRadius: 8,
146+
boxShadow: "0 4px 12px rgba(0,0,0,.1)",
147+
zIndex: 1000,
148+
}}>
149+
{emojis.map((e) => (
150+
<button
151+
key={e}
152+
onClick={() => {
153+
document.execCommand("insertText", false, e);
154+
setOpen(false);
155+
}}
156+
style={{
157+
border: "none",
158+
background: "none",
159+
fontSize: 20,
160+
cursor: "pointer",
161+
padding: 4,
162+
borderRadius: 4,
163+
}}
164+
>
165+
{e}
166+
</button>
167+
))}
168+
</div>
169+
)}
170+
</div>
171+
);
172+
},
173+
174+
canExecute: () => true,
175+
};
176+
```
177+
178+
## Tips
179+
180+
- Use `editor.executeCommand()` for standard `document.execCommand` operations
181+
- Use `editor.getSelection()` to read the current selection for custom DOM manipulation
182+
- Use `editor.insertBlock()` or `editor.insertInline()` for element insertion
183+
- Add `onMouseDown={(e) => e.preventDefault()}` to your button containers to prevent selection loss
184+
- Use `aria-label` on buttons for accessibility
185+
- Use the `rte-toolbar-button` and `rte-toolbar-button-active` CSS classes for consistent styling

docs/editor-api.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Editor API
2+
3+
The `EditorAPI` object is available via the `onEditorAPIReady` callback. It provides programmatic control over the editor.
4+
5+
## Accessing the API
6+
7+
```tsx
8+
import { Editor, EditorAPI } from "@overlap/rte";
9+
import { useRef } from "react";
10+
11+
function MyEditor() {
12+
const apiRef = useRef<EditorAPI | null>(null);
13+
14+
const handleSave = () => {
15+
const html = apiRef.current?.exportHtml();
16+
console.log(html);
17+
};
18+
19+
return (
20+
<>
21+
<Editor onEditorAPIReady={(api) => { apiRef.current = api; }} />
22+
<button onClick={handleSave}>Save</button>
23+
</>
24+
);
25+
}
26+
```
27+
28+
## Methods
29+
30+
### Content
31+
32+
| Method | Returns | Description |
33+
|---|---|---|
34+
| `getContent()` | `EditorContent` | Get current content as JSON |
35+
| `setContent(content)` | `void` | Replace editor content from JSON |
36+
| `exportHtml()` | `string` | Export current content as HTML string |
37+
| `importHtml(html)` | `EditorContent` | Import HTML string into the editor and return parsed content |
38+
39+
### Commands
40+
41+
| Method | Returns | Description |
42+
|---|---|---|
43+
| `executeCommand(command, value?)` | `boolean` | Execute a formatting command (wraps `document.execCommand`) |
44+
45+
Common commands:
46+
- `"bold"`, `"italic"`, `"underline"`, `"strikeThrough"`
47+
- `"formatBlock"` with value `"<h1>"`, `"<h2>"`, `"<p>"`, `"<pre>"`, `"<blockquote>"`
48+
- `"insertOrderedList"`, `"insertUnorderedList"`
49+
- `"foreColor"`, `"backColor"` with a hex color value
50+
- `"fontSize"` with a size value
51+
- `"insertHorizontalRule"`
52+
- `"insertImage"` with a URL value
53+
- `"insertCheckboxList"`
54+
55+
### Selection
56+
57+
| Method | Returns | Description |
58+
|---|---|---|
59+
| `getSelection()` | `Selection \| null` | Get the current browser Selection object |
60+
61+
### DOM Insertion
62+
63+
| Method | Returns | Description |
64+
|---|---|---|
65+
| `insertBlock(type, attrs?)` | `void` | Insert a block-level element at the cursor |
66+
| `insertInline(type, attrs?)` | `void` | Insert/wrap an inline element around the selection |
67+
68+
### History
69+
70+
| Method | Returns | Description |
71+
|---|---|---|
72+
| `undo()` | `void` | Undo the last change |
73+
| `redo()` | `void` | Redo the last undone change |
74+
| `canUndo()` | `boolean` | Whether there is an undo entry |
75+
| `canRedo()` | `boolean` | Whether there is a redo entry |
76+
77+
### Clear Formatting
78+
79+
| Method | Returns | Description |
80+
|---|---|---|
81+
| `clearFormatting()` | `void` | Remove all inline formatting from selection |
82+
| `clearTextColor()` | `void` | Remove text color from selection |
83+
| `clearBackgroundColor()` | `void` | Remove background color from selection |
84+
| `clearFontSize()` | `void` | Remove font size from selection |
85+
| `clearLinks()` | `void` | Remove links from selection (keep text) |
86+
87+
### List Operations
88+
89+
| Method | Returns | Description |
90+
|---|---|---|
91+
| `indentListItem()` | `void` | Indent the current list item |
92+
| `outdentListItem()` | `void` | Outdent the current list item |
93+
94+
### Statistics
95+
96+
| Method | Returns | Description |
97+
|---|---|---|
98+
| `getTextStats()` | `{ characters: number; words: number }` | Get current character and word count |
99+
100+
```tsx
101+
const stats = api.getTextStats();
102+
console.log(`${stats.words} words, ${stats.characters} characters`);
103+
```

0 commit comments

Comments
 (0)