@rcarls/rc-textarea 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -1176
- package/dist/custom-elements.json +168 -72
- package/dist/{line-actions-controller-BsM6DJ0b.js → line-actions-controller-ChmbiQ-G.js} +573 -532
- package/dist/line-actions-controller-ChmbiQ-G.js.map +1 -0
- package/dist/rc-textarea-define.js +2 -2
- package/dist/rc-textarea.js +1 -1
- package/dist/types/packages/rc-textarea/src/blots.d.ts +7 -7
- package/dist/types/packages/rc-textarea/src/decoration.d.ts +5 -2
- package/dist/types/packages/rc-textarea/src/document.d.ts +7 -9
- package/dist/types/packages/rc-textarea/src/line-actions-controller.d.ts +12 -9
- package/dist/types/packages/rc-textarea/src/line-decorator.d.ts +9 -8
- package/dist/types/packages/rc-textarea/src/pattern-matcher.d.ts +4 -2
- package/dist/types/packages/rc-textarea/src/rc-textarea.d.ts +101 -32
- package/dist/types/packages/rc-textarea/src/selection.d.ts +17 -13
- package/dist/types/packages/rc-textarea/src/test-helpers.d.ts +6 -4
- package/dist/types/packages/rc-textarea/src/types.d.ts +119 -40
- package/package.json +7 -8
- package/dist/demo.css +0 -85
- package/dist/demo.js +0 -40
- package/dist/line-actions-controller-BsM6DJ0b.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,128 +1,24 @@
|
|
|
1
1
|
# rc-textarea
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Textarea wrapper with line decorations, gutter rendering, inline widgets, and plugin hooks.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Docs: [https://richardcarls.github.io/rc-webcomponents/components/rc-textarea](https://richardcarls.github.io/rc-webcomponents/components/rc-textarea).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Table of Contents
|
|
10
|
-
|
|
11
|
-
1. [Key Capabilities](#key-capabilities)
|
|
12
|
-
2. [Architecture](#architecture)
|
|
13
|
-
3. [Installation](#installation)
|
|
14
|
-
4. [Basic Usage](#basic-usage)
|
|
15
|
-
5. [Attributes & Properties](#attributes--properties)
|
|
16
|
-
6. [Value Property](#value-property)
|
|
17
|
-
7. [Events](#events)
|
|
18
|
-
8. [CSS Customization](#css-customization)
|
|
19
|
-
9. [Plugin API](#plugin-api)
|
|
20
|
-
10. [Decoration Types](#decoration-types)
|
|
21
|
-
11. [Pattern API](#pattern-api)
|
|
22
|
-
12. [Plugin Helpers](#plugin-helpers)
|
|
23
|
-
13. [Selection API](#selection-api)
|
|
24
|
-
14. [Decoration Lifecycle](#decoration-lifecycle)
|
|
25
|
-
15. [Undo / Redo](#undo--redo)
|
|
26
|
-
16. [Keyboard Behavior](#keyboard-behavior)
|
|
27
|
-
17. [Form Integration](#form-integration)
|
|
28
|
-
18. [Accessibility](#accessibility)
|
|
29
|
-
19. [Performance Considerations](#performance-considerations)
|
|
30
|
-
20. [Troubleshooting](#troubleshooting)
|
|
7
|
+
Made with [Lit](https://lit.dev) and [Parchment](https://github.com/quilljs/parchment).
|
|
31
8
|
|
|
32
9
|
---
|
|
33
10
|
|
|
34
|
-
##
|
|
11
|
+
## Feature Highlights
|
|
35
12
|
|
|
36
|
-
- **Mixed inline formatting**
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## Architecture
|
|
49
|
-
|
|
50
|
-
### DOM Structure
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
rc-textarea (LitElement shadow host, delegatesFocus=true)
|
|
54
|
-
├── #root (flex container)
|
|
55
|
-
│ ├── #gutter (line/list/custom gutter, optional)
|
|
56
|
-
│ │ └── #gutter-cells (container for .gutter-cell spans, one per line)
|
|
57
|
-
│ └── #editor-area (flex item, grows to fill)
|
|
58
|
-
│ ├── #editor (contenteditable div — Parchment ScrollBlot root)
|
|
59
|
-
│ │ └── .v2-line divs (one per logical line, V2BlockBlot)
|
|
60
|
-
│ │ ├── text nodes (plain text content)
|
|
61
|
-
│ │ ├── .v2-mark spans (V2InlineBlot — mark decorations)
|
|
62
|
-
│ │ ├── .v2-widget spans (V2WidgetBlot — inline widgets, contenteditable=false)
|
|
63
|
-
│ │ └── [data-message] attr (error-lens: used by ::after pseudo-element)
|
|
64
|
-
│ └── <slot> (lightDOM textarea — hidden, form-only)
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
**Shadow DOM usage**: `delegatesFocus=true` ensures focus management works correctly. Line numbers are in the shadow DOM but read-only.
|
|
68
|
-
|
|
69
|
-
### Editing Loop (Data Flow)
|
|
70
|
-
|
|
71
|
-
1. **Browser handles edit** — User types/pastes/deletes text in the contenteditable div
|
|
72
|
-
2. **`input` event fires** → `_onInput()` handler:
|
|
73
|
-
- Save DOM selection to plain-text offsets via `saveSelection()`
|
|
74
|
-
- Extract plain text via `extractEditorText()`
|
|
75
|
-
- Update `this._value`
|
|
76
|
-
- Dispatch `rc-textarea-change` event
|
|
77
|
-
- Map existing decorations through the text change via `mapDecorationsThroughChange()`
|
|
78
|
-
- Schedule RAF render pass
|
|
79
|
-
3. **RAF render pass** → `_performRender()`:
|
|
80
|
-
- Extract plugin decorations
|
|
81
|
-
- Re-run patterns (rebuild all pattern decorations from regex)
|
|
82
|
-
- Ask plugin for new decorations (call `plugin.update()` or `plugin.highlight()`)
|
|
83
|
-
- Build `V2Document` from text + all decorations
|
|
84
|
-
- Tear down and fully rebuild the blot tree
|
|
85
|
-
- Restore DOM selection via `restoreSelection()`
|
|
86
|
-
- Update line number gutter if `lineNumbers=true`
|
|
87
|
-
4. **Decorations stay in sync** — All existing decorations persist through edits (mapped to new offsets)
|
|
88
|
-
|
|
89
|
-
**Key insight for LLM agents**: The blot tree is **rebuilt on every render frame**. This is necessary because decoration changes require DOM restructuring. The undo stack stores plain-text values, not DOM snapshots.
|
|
90
|
-
|
|
91
|
-
### Parchment Integration
|
|
92
|
-
|
|
93
|
-
- **`V2ScrollBlot`** wraps the `#editor` div and suppresses Parchment's `MutationObserver` (which would fight with our render loop)
|
|
94
|
-
- **`V2BlockBlot`** represents a single line (`.v2-line` div)
|
|
95
|
-
- **`V2InlineBlot`** represents a mark decoration (`.v2-mark` span)
|
|
96
|
-
- **`V2WidgetBlot`** represents an inline widget (`.v2-widget` span, `contenteditable=false`)
|
|
97
|
-
- **`V2Document`** class contains the build logic: `build(text, decorations) → BlotTree`
|
|
98
|
-
|
|
99
|
-
The blot tree is **immutable** — each render creates a new tree from scratch, then the DOM is replaced.
|
|
100
|
-
|
|
101
|
-
### Cursor Management
|
|
102
|
-
|
|
103
|
-
Plain-text offsets are essential for:
|
|
104
|
-
- Storing selection state across DOM rebuilds
|
|
105
|
-
- Decoration `from`/`to` ranges
|
|
106
|
-
- Plugin API (selection, cursor position)
|
|
107
|
-
- Undo/redo stack
|
|
108
|
-
|
|
109
|
-
**`selection.ts` module**:
|
|
110
|
-
- `saveSelection()` — convert DOM range to plain-text offsets, skipping widget spans
|
|
111
|
-
- `restoreSelection()` — convert plain-text offsets back to DOM range after rebuild
|
|
112
|
-
|
|
113
|
-
### File Organization
|
|
114
|
-
|
|
115
|
-
| File | Purpose |
|
|
116
|
-
|------|---------|
|
|
117
|
-
| `src/rc-textarea.ts` | Main LitElement: editing loop, plugin/pattern management, form wiring, undo/redo stack, gutter, event dispatch |
|
|
118
|
-
| `src/document.ts` | `V2Document` class — builds Parchment tree from text + decorations; `extractEditorText()` reverse operation |
|
|
119
|
-
| `src/blots.ts` | Parchment blot subclasses (`V2ScrollBlot`, `V2BlockBlot`, `V2InlineBlot`, `V2WidgetBlot`) + blot registry |
|
|
120
|
-
| `src/selection.ts` | `saveSelection()` / `restoreSelection()` for plain-text ↔ DOM range conversion |
|
|
121
|
-
| `src/decoration.ts` | `mapDecorationsThroughChange()` — map existing decorations through text edits; `isLargeChange()` heuristic |
|
|
122
|
-
| `src/pattern-matcher.ts` | `matchPatternResults()` — run `TextPattern` array against text, return mark + line decorations |
|
|
123
|
-
| `src/line-decorator.ts` | `createLineDecoratorPlugin()` factory — wraps per-line decoration logic in a full `RCTextareaPlugin` |
|
|
124
|
-
| `src/types.ts` | All exported TypeScript interfaces and utility types |
|
|
125
|
-
| `src/rc-textarea.styles.ts` | Component CSS (custom properties, parts, internal layout) |
|
|
13
|
+
- **Mixed inline formatting** - bold, italic, color, background, and underline
|
|
14
|
+
- **Imperative APIs** that behave well with reactive frameworks
|
|
15
|
+
- **Error-lens style line annotations** that stay separate from text content
|
|
16
|
+
- **Inline widgets** - color swatches, icons, tooltips or quick action buttons are possible
|
|
17
|
+
- **Simple Pattern API** that auto-decorates text matching regular expressions
|
|
18
|
+
- **Plugin API** - imperative decoration control + highlight.js / prism.js HTML compatibility bridge
|
|
19
|
+
- **Line numbers, word wrap, auto-grow** - declarative features for common use cases
|
|
20
|
+
- **Progressive enhancement** - wraps a native `<textarea>` for form submission, label association
|
|
21
|
+
- **Undo/redo** - durable internally-tracked undo stack
|
|
126
22
|
|
|
127
23
|
---
|
|
128
24
|
|
|
@@ -130,411 +26,185 @@ Plain-text offsets are essential for:
|
|
|
130
26
|
|
|
131
27
|
```bash
|
|
132
28
|
npm install @rcarls/rc-textarea
|
|
133
|
-
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```bash
|
|
134
32
|
yarn add @rcarls/rc-textarea
|
|
135
33
|
```
|
|
136
34
|
|
|
137
|
-
Import to
|
|
35
|
+
Import the define entry to register `<rc-textarea>`:
|
|
138
36
|
|
|
139
37
|
```ts
|
|
140
|
-
import '@rcarls/rc-textarea';
|
|
38
|
+
import '@rcarls/rc-textarea/define';
|
|
141
39
|
```
|
|
142
40
|
|
|
143
|
-
|
|
41
|
+
Import public types when you need typed access:
|
|
144
42
|
|
|
145
43
|
```ts
|
|
146
|
-
import { RCTextarea } from '@rcarls/rc-textarea';
|
|
44
|
+
import type { RCTextarea, RCTextareaPlugin } from '@rcarls/rc-textarea';
|
|
147
45
|
```
|
|
148
46
|
|
|
149
|
-
---
|
|
150
|
-
|
|
151
47
|
## Basic Usage
|
|
152
48
|
|
|
153
|
-
Slot a native `<textarea>` as the direct child. It is hidden from view and used
|
|
49
|
+
Slot a native `<textarea>` as the direct child. It is hidden from view and used
|
|
50
|
+
only for form wiring.
|
|
154
51
|
|
|
155
52
|
```html
|
|
156
|
-
<
|
|
157
|
-
|
|
53
|
+
<label for="message">Message</label>
|
|
54
|
+
|
|
55
|
+
<rc-textarea line-numbers word-wrap auto-grow>
|
|
56
|
+
<textarea id="message" name="message" rows="10" placeholder="Start typing..."></textarea>
|
|
158
57
|
</rc-textarea>
|
|
159
58
|
```
|
|
160
59
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
### JavaScript access
|
|
60
|
+
Use the `value` property for controlled updates, `defaultValue` for an initial uncontrolled
|
|
61
|
+
value, and `rc-textarea-change` for user-originated changes.
|
|
164
62
|
|
|
165
63
|
```ts
|
|
166
64
|
const editor = document.querySelector('rc-textarea');
|
|
167
65
|
|
|
168
|
-
|
|
169
|
-
console.log(editor.value);
|
|
170
|
-
|
|
171
|
-
// Set programmatically
|
|
172
|
-
editor.value = 'new content';
|
|
66
|
+
editor.value = 'Programmatic updates are silent.';
|
|
173
67
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
console.log('New value:', e.detail.value);
|
|
68
|
+
editor.addEventListener('rc-textarea-change', (event) => {
|
|
69
|
+
console.log(event.detail.value);
|
|
177
70
|
});
|
|
178
71
|
```
|
|
179
72
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
## Attributes & Properties
|
|
183
|
-
|
|
184
|
-
All attributes reflect to properties. Use attributes in HTML or properties in JS.
|
|
185
|
-
|
|
186
|
-
| Attribute | Property | Type | Default | Description |
|
|
187
|
-
|-----------|----------|------|---------|-------------|
|
|
188
|
-
| `line-numbers` | `lineNumbers` | `boolean` | `false` | Display line number gutter on the left |
|
|
189
|
-
| `list-numbers` | `listNumbers` | `boolean` | `false` | Display a numbered list gutter (skips blank lines, resets counter) |
|
|
190
|
-
| `gutter` | `gutter` | `boolean` | `false` | Show a gutter column without built-in content (plugins fill cells via `LineDecoration.gutterContent`) |
|
|
191
|
-
| `word-wrap` | `wordWrap` | `boolean` | `false` | Enable word wrapping (default: scroll horizontally) |
|
|
192
|
-
| `auto-grow` | `autoGrow` | `boolean` | `false` | Grow container height to fit content |
|
|
193
|
-
| `read-only` | `readOnly` | `boolean` | `false` | Disable text editing; still selectable |
|
|
194
|
-
| `label` | `label` | `string \| null` | `null` | Sets `aria-label` on the editor div |
|
|
195
|
-
|
|
196
|
-
### Example
|
|
73
|
+
When the user edits, the slotted textarea is kept in sync and dispatches a native bubbling
|
|
74
|
+
`input` event. Submitting a form reads the textarea's plain-text value normally.
|
|
197
75
|
|
|
198
76
|
```html
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
auto-grow
|
|
204
|
-
label="Code editor"
|
|
205
|
-
>
|
|
206
|
-
<textarea name="code" rows="20"></textarea>
|
|
207
|
-
</rc-textarea>
|
|
208
|
-
```
|
|
77
|
+
<form>
|
|
78
|
+
<rc-textarea>
|
|
79
|
+
<textarea name="body" required maxlength="5000"></textarea>
|
|
80
|
+
</rc-textarea>
|
|
209
81
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log(editor.lineNumbers); // true
|
|
213
|
-
console.log(editor.wordWrap); // true
|
|
82
|
+
<button type="submit">Send</button>
|
|
83
|
+
</form>
|
|
214
84
|
```
|
|
215
85
|
|
|
216
|
-
|
|
86
|
+
## Common Options
|
|
217
87
|
|
|
218
|
-
|
|
88
|
+
| Attribute | Property | Description |
|
|
89
|
+
| -------------- | ------------- | ----------------------------------------------------------------------------------- |
|
|
90
|
+
| `line-numbers` | `lineNumbers` | Show sequential line numbers in the gutter. |
|
|
91
|
+
| `gutter` | `gutter` | Show an empty gutter that plugins can populate with `LineDecoration.gutterContent`. |
|
|
92
|
+
| `word-wrap` | `wordWrap` | Wrap long lines instead of scrolling horizontally. |
|
|
93
|
+
| `auto-grow` | `autoGrow` | Let the field grow vertically with content. |
|
|
94
|
+
| `read-only` | `readOnly` | Render selectable, non-editable content. |
|
|
219
95
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
// Read
|
|
224
|
-
const plainText = editor.value;
|
|
225
|
-
|
|
226
|
-
// Write — decorations are mapped through the change
|
|
227
|
-
editor.value = 'hello world';
|
|
96
|
+
`list-numbers` and `label` still exist for compatibility but are deprecated. Prefer a plugin
|
|
97
|
+
with `LineDecoration.gutterContent` for sparse numbering, and put accessible names on the
|
|
98
|
+
slotted textarea with `aria-label` or a real `<label for="...">`.
|
|
228
99
|
|
|
229
|
-
|
|
230
|
-
const textarea = editor.querySelector('textarea');
|
|
231
|
-
textarea.value = 'test';
|
|
232
|
-
editor.value = textarea.value; // sync if needed
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Setting `value` programmatically:
|
|
236
|
-
1. Updates `this._value` and the slotted textarea
|
|
237
|
-
2. Dispatches `rc-textarea-change` event
|
|
238
|
-
3. Schedules a RAF render pass
|
|
239
|
-
4. Existing decorations are **mapped through the change** (shifted, clamped, or cleared)
|
|
100
|
+
The main JavaScript-only properties are:
|
|
240
101
|
|
|
241
|
-
|
|
102
|
+
| Property | Type | Description |
|
|
103
|
+
| --------------------------------- | -------------------------- | ------------------------------------------------------------------------- |
|
|
104
|
+
| `value` | `string` | Current plain-text value. Host writes are silent. |
|
|
105
|
+
| `defaultValue` | `string \| undefined` | Initial uncontrolled value, used before `value` or textarea content wins. |
|
|
106
|
+
| `plugin` | `RCTextareaPlugin \| null` | Declarative plugin hook for reactive frameworks. |
|
|
107
|
+
| `decorations` | `DecorationInput[]` | External decoration layer merged with plugin and pattern decorations. |
|
|
108
|
+
| `selectionStart` / `selectionEnd` | `number` | Current plain-text selection offsets. |
|
|
242
109
|
|
|
243
110
|
## Events
|
|
244
111
|
|
|
245
|
-
All events bubble and are composed
|
|
112
|
+
All public events bubble and are composed.
|
|
246
113
|
|
|
247
|
-
| Event
|
|
248
|
-
|
|
249
|
-
| `rc-textarea-change` | `{ value: string }`
|
|
250
|
-
| `rc-textarea-focus` |
|
|
251
|
-
| `rc-textarea-blur` |
|
|
252
|
-
| `rc-textarea-select` | `{ selectionStart: number, selectionEnd: number }` |
|
|
114
|
+
| Event | Detail | Fires when |
|
|
115
|
+
| -------------------- | -------------------------------------------------- | -------------------------------------------------- |
|
|
116
|
+
| `rc-textarea-change` | `{ value: string }` | User editing changes the plain-text value. |
|
|
117
|
+
| `rc-textarea-focus` | none | The editor receives focus. |
|
|
118
|
+
| `rc-textarea-blur` | none | The editor loses focus. |
|
|
119
|
+
| `rc-textarea-select` | `{ selectionStart: number, selectionEnd: number }` | The selection changes while the editor is focused. |
|
|
253
120
|
|
|
254
|
-
|
|
121
|
+
## Pattern Highlights
|
|
255
122
|
|
|
256
|
-
|
|
257
|
-
const editor = document.querySelector('rc-textarea');
|
|
258
|
-
|
|
259
|
-
editor.addEventListener('rc-textarea-change', (e) => {
|
|
260
|
-
console.log('Text changed to:', e.detail.value);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
editor.addEventListener('rc-textarea-select', (e) => {
|
|
264
|
-
const { selectionStart, selectionEnd } = e.detail;
|
|
265
|
-
console.log(`Selection: ${selectionStart}–${selectionEnd}`);
|
|
266
|
-
});
|
|
123
|
+
Use `addPattern()` for lightweight regex decoration without writing a plugin.
|
|
267
124
|
|
|
268
|
-
|
|
269
|
-
|
|
125
|
+
```ts
|
|
126
|
+
const todoPatternId = editor.addPattern({
|
|
127
|
+
pattern: /\bTODO\b/g,
|
|
128
|
+
bold: true,
|
|
129
|
+
color: 'var(--editor-todo-color)',
|
|
270
130
|
});
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
---
|
|
274
131
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
### Custom Properties
|
|
278
|
-
|
|
279
|
-
Set on the host element or any ancestor to style the editor.
|
|
280
|
-
|
|
281
|
-
| Property | Default | Description |
|
|
282
|
-
|----------|---------|-------------|
|
|
283
|
-
| `--rc-textarea-font-family` | `monospace` | Font family for text and line numbers |
|
|
284
|
-
| `--rc-textarea-font-size` | `1em` | Base font size |
|
|
285
|
-
| `--rc-textarea-line-height` | `1.5` | Line height affects gutter alignment |
|
|
286
|
-
| `--rc-textarea-padding` | `0.5em` | Inner padding of editor area |
|
|
287
|
-
| `--rc-textarea-background` | `Field` | Editor background color (system color keyword) |
|
|
288
|
-
| `--rc-textarea-color` | `FieldText` | Text color (system color keyword) |
|
|
289
|
-
| `--rc-textarea-caret-color` | (auto) | Cursor/caret color |
|
|
290
|
-
| `--rc-textarea-border` | `1px solid ButtonBorder` | Editor border (system color keyword) |
|
|
291
|
-
| `--rc-textarea-border-radius` | `2px` | Corner rounding |
|
|
292
|
-
| `--rc-textarea-focus-outline` | `2px solid AccentColor` | Focus ring (system color keyword) |
|
|
293
|
-
| `--rc-textarea-active-line-bg` | `transparent` | Background of the line containing the cursor |
|
|
294
|
-
| `--rc-textarea-gutter-color` | `GrayText` | Line number text color |
|
|
295
|
-
| `--rc-textarea-gutter-bg` | `Canvas` | Gutter background |
|
|
296
|
-
| `--rc-textarea-gutter-border` | `1px solid ButtonBorder` | Gutter right border |
|
|
297
|
-
| `--rc-textarea-gutter-padding-inline-end` | `0.75em` | Gap between gutter numbers and editor content |
|
|
298
|
-
|
|
299
|
-
### Example
|
|
300
|
-
|
|
301
|
-
```css
|
|
302
|
-
rc-textarea {
|
|
303
|
-
--rc-textarea-font-family: 'Fira Code', monospace;
|
|
304
|
-
--rc-textarea-font-size: 13px;
|
|
305
|
-
--rc-textarea-line-height: 1.6;
|
|
306
|
-
--rc-textarea-background: #1e1e2e;
|
|
307
|
-
--rc-textarea-color: #cdd6f4;
|
|
308
|
-
--rc-textarea-caret-color: #89b4fa;
|
|
309
|
-
--rc-textarea-border: 1px solid #313244;
|
|
310
|
-
--rc-textarea-border-radius: 4px;
|
|
311
|
-
}
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### CSS Parts
|
|
315
|
-
|
|
316
|
-
Use `::part()` pseudo-element for advanced styling.
|
|
317
|
-
|
|
318
|
-
| Part | Element | Supports |
|
|
319
|
-
|------|---------|----------|
|
|
320
|
-
| `root` | Outer flex container | All CSS |
|
|
321
|
-
| `gutter` | Gutter outer div | All CSS |
|
|
322
|
-
| `gutter-cells` | Inner container holding `.gutter-cell` spans | All CSS |
|
|
323
|
-
| `editor-area` | Container wrapping editor + slot | All CSS |
|
|
324
|
-
| `editor` | The `contenteditable` div | All CSS |
|
|
325
|
-
|
|
326
|
-
### Example
|
|
327
|
-
|
|
328
|
-
```css
|
|
329
|
-
rc-textarea::part(editor) {
|
|
330
|
-
border-radius: 4px;
|
|
331
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
rc-textarea::part(gutter) {
|
|
335
|
-
background: #f5f5f5;
|
|
336
|
-
font-size: 0.9em;
|
|
337
|
-
}
|
|
132
|
+
editor.removePattern(todoPatternId);
|
|
133
|
+
editor.clearPatterns();
|
|
338
134
|
```
|
|
339
135
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
Inline decorations are applied to `.v2-mark` spans. Inline styles handle standard properties (bold, italic, color, etc.). For custom classes:
|
|
136
|
+
Patterns can also style named capture groups and add line diagnostics.
|
|
343
137
|
|
|
344
138
|
```ts
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
139
|
+
editor.addPattern({
|
|
140
|
+
pattern: /^(?<key>\w[\w-]*):\s*(?<value>.+)$/gm,
|
|
141
|
+
captureGroups: {
|
|
142
|
+
key: { bold: true, color: 'var(--editor-key-color)' },
|
|
143
|
+
value: { color: 'var(--editor-value-color)' },
|
|
144
|
+
},
|
|
145
|
+
createLineDecoration: () => ({ className: 'config-line' }),
|
|
349
146
|
});
|
|
350
|
-
|
|
351
|
-
// Add decorations with a custom class
|
|
352
|
-
api.setDecorations([
|
|
353
|
-
{ type: 'mark', from: 5, to: 10, className: 'my-highlight' }
|
|
354
|
-
]);
|
|
355
|
-
|
|
356
|
-
// Style it in an adopted stylesheet
|
|
357
|
-
api.adoptStyleSheet(`
|
|
358
|
-
.v2-mark.my-highlight { background: yellow; }
|
|
359
|
-
`);
|
|
360
147
|
```
|
|
361
148
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
## Plugin API
|
|
149
|
+
See [PLUGIN_AUTHORING.md](PLUGIN_AUTHORING.md) for the complete decoration and plugin model.
|
|
365
150
|
|
|
366
|
-
|
|
151
|
+
## Markdown Plugin Package
|
|
367
152
|
|
|
368
|
-
|
|
153
|
+
Install the Markdown plugin package when you want Markdown-oriented decorations and preview
|
|
154
|
+
HTML without writing your own parser bridge.
|
|
369
155
|
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
mount?(api: RCTextareaPluginAPI): void;
|
|
374
|
-
/** Called when the plugin is replaced or the element disconnects. */
|
|
375
|
-
destroy?(): void;
|
|
376
|
-
/**
|
|
377
|
-
* Display-layer value transform — called before `update`/`highlight` in
|
|
378
|
-
* read-only mode. Return a non-null string to substitute it as the rendered
|
|
379
|
-
* text. The underlying `element.value` is never modified.
|
|
380
|
-
*/
|
|
381
|
-
transform?(value: string, api: RCTextareaPluginAPI): string | null | void;
|
|
382
|
-
/** Imperative decoration API — called on each value change. */
|
|
383
|
-
update?(value: string, api: RCTextareaPluginAPI): void | Promise<void>;
|
|
384
|
-
/**
|
|
385
|
-
* HTML-based compat — called on each value change.
|
|
386
|
-
* Return an HTML string (e.g. from hljs/prism); it will be parsed into
|
|
387
|
-
* mark decorations via `api.decorationsFromHtml()`.
|
|
388
|
-
*/
|
|
389
|
-
highlight?(value: string, api: RCTextareaPluginAPI):
|
|
390
|
-
string | null | void | Promise<string | null | void>;
|
|
391
|
-
}
|
|
156
|
+
```bash
|
|
157
|
+
npm install @rcarls/rc-textarea @rcarls/rc-textarea-plugin-markdown \
|
|
158
|
+
mdast-util-from-markdown micromark unist-util-visit
|
|
392
159
|
```
|
|
393
160
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
### PluginAPI Methods
|
|
399
|
-
|
|
400
|
-
```ts
|
|
401
|
-
interface RCTextareaPluginAPI {
|
|
402
|
-
readonly host: HTMLElement; // rc-textarea element
|
|
403
|
-
readonly value: string; // Current plain text
|
|
404
|
-
readonly selectionStart: number; // Normalized selection start (≤ selectionEnd)
|
|
405
|
-
readonly selectionEnd: number; // Normalized selection end (≥ selectionStart)
|
|
406
|
-
|
|
407
|
-
getCursorRect(): DOMRect | null; // Cursor position in viewport coords (or null if not focused)
|
|
408
|
-
getWordAtCursor(): { word: string; from: number; to: number } | null;
|
|
409
|
-
onCursorMove(cb: (start: number, end: number) => void): () => void;
|
|
410
|
-
|
|
411
|
-
addDecoration(d: DecorationInput): string; // Add single decoration, return ID
|
|
412
|
-
removeDecoration(id: string): void; // Remove by ID
|
|
413
|
-
clearDecorations(): void; // Remove all plugin decorations
|
|
414
|
-
setDecorations(decorations: DecorationInput[]): void; // Replace all with new set
|
|
415
|
-
|
|
416
|
-
scheduleUpdate(): void; // Trigger render outside input events
|
|
417
|
-
|
|
418
|
-
adoptStyleSheet(sheetOrCssText: CSSStyleSheet | string): CSSStyleSheet;
|
|
419
|
-
removeStyleSheet(sheet: CSSStyleSheet): void;
|
|
420
|
-
decorationsFromHtml(html: string): Omit<MarkDecoration, 'id'>[];
|
|
421
|
-
}
|
|
161
|
+
```bash
|
|
162
|
+
yarn add @rcarls/rc-textarea @rcarls/rc-textarea-plugin-markdown \
|
|
163
|
+
mdast-util-from-markdown micromark unist-util-visit
|
|
422
164
|
```
|
|
423
165
|
|
|
424
|
-
### Mounting and Lifecycle
|
|
425
|
-
|
|
426
166
|
```ts
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
let api: RCTextareaPluginAPI;
|
|
430
|
-
|
|
431
|
-
editor.usePlugin({
|
|
432
|
-
mount(a) {
|
|
433
|
-
api = a; // Save reference for external use
|
|
434
|
-
console.log('Plugin mounted');
|
|
435
|
-
},
|
|
436
|
-
update(value, a) {
|
|
437
|
-
// Called on each value change
|
|
438
|
-
const decorations = parseText(value);
|
|
439
|
-
a.setDecorations(decorations);
|
|
440
|
-
},
|
|
441
|
-
destroy() {
|
|
442
|
-
console.log('Plugin unmounting');
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// From outside plugin lifecycle, using saved reference
|
|
447
|
-
api.setDecorations([
|
|
448
|
-
{ type: 'mark', from: 0, to: 5, bold: true }
|
|
449
|
-
]);
|
|
450
|
-
api.scheduleUpdate();
|
|
451
|
-
```
|
|
167
|
+
import '@rcarls/rc-textarea/define';
|
|
168
|
+
import { createMarkdownPlugin } from '@rcarls/rc-textarea-plugin-markdown';
|
|
452
169
|
|
|
453
|
-
|
|
170
|
+
const editor = document.querySelector('rc-textarea');
|
|
171
|
+
const markdown = createMarkdownPlugin();
|
|
454
172
|
|
|
455
|
-
|
|
173
|
+
editor.usePlugin(markdown);
|
|
456
174
|
|
|
457
|
-
|
|
458
|
-
const markdownLitePlugin = {
|
|
459
|
-
update(value, api) {
|
|
460
|
-
const decorations = [];
|
|
461
|
-
|
|
462
|
-
// Bold: **text**
|
|
463
|
-
const boldRe = /\*\*(.+?)\*\*/g;
|
|
464
|
-
let m;
|
|
465
|
-
while ((m = boldRe.exec(value)) !== null) {
|
|
466
|
-
decorations.push({
|
|
467
|
-
type: 'mark',
|
|
468
|
-
from: m.index,
|
|
469
|
-
to: m.index + m[0].length,
|
|
470
|
-
bold: true
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Italic: *text*
|
|
475
|
-
const italicRe = /\*([^*]+)\*/g;
|
|
476
|
-
while ((m = italicRe.exec(value)) !== null) {
|
|
477
|
-
decorations.push({
|
|
478
|
-
type: 'mark',
|
|
479
|
-
from: m.index,
|
|
480
|
-
to: m.index + m[0].length,
|
|
481
|
-
italic: true
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
api.setDecorations(decorations);
|
|
486
|
-
},
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
editor.usePlugin(markdownLitePlugin);
|
|
175
|
+
preview.innerHTML = markdown.getPreviewHtml(editor.value);
|
|
490
176
|
```
|
|
491
177
|
|
|
492
|
-
|
|
178
|
+
The package decorates common Markdown syntax using `mdast-util-from-markdown` and exposes
|
|
179
|
+
`getMarkdownPreviewHtml()` / `plugin.getPreviewHtml()` for preview rendering.
|
|
493
180
|
|
|
494
|
-
|
|
495
|
-
editor.usePlugin({
|
|
496
|
-
async update(value, api) {
|
|
497
|
-
const decorations = await myWasmParser.tokenize(value);
|
|
498
|
-
api.setDecorations(decorations);
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
```
|
|
181
|
+
## highlight.js And Prism Bridge
|
|
502
182
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
183
|
+
`rc-textarea` can consume the HTML strings produced by highlighters that wrap token text in
|
|
184
|
+
`<span class="...">...</span>` nodes. Return that HTML from `highlight()` or call
|
|
185
|
+
`api.parseDecorationsFromHtml()` yourself inside `update()`.
|
|
506
186
|
|
|
507
187
|
```ts
|
|
508
188
|
import hljs from 'highlight.js/lib/core';
|
|
509
189
|
import javascript from 'highlight.js/lib/languages/javascript';
|
|
190
|
+
|
|
510
191
|
hljs.registerLanguage('javascript', javascript);
|
|
511
192
|
|
|
512
193
|
editor.usePlugin({
|
|
513
194
|
mount(api) {
|
|
514
|
-
// Inject theme CSS into shadow root (hljs classes live inside)
|
|
515
195
|
api.adoptStyleSheet(`
|
|
516
|
-
.hljs-keyword { color:
|
|
517
|
-
.hljs-string
|
|
518
|
-
.hljs-
|
|
519
|
-
.hljs-comment { color: #6c7086; font-style: italic; }
|
|
520
|
-
.hljs-title { color: #89b4fa; font-weight: bold; }
|
|
521
|
-
.hljs-built_in { color: #89dceb; }
|
|
522
|
-
.hljs-literal { color: #f5c2e7; }
|
|
196
|
+
.hljs-keyword { color: var(--editor-syntax-keyword); font-weight: 600; }
|
|
197
|
+
.hljs-string { color: var(--editor-syntax-string); }
|
|
198
|
+
.hljs-comment { color: var(--editor-syntax-comment); font-style: italic; }
|
|
523
199
|
`);
|
|
524
200
|
},
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// Convert HTML token markup to decorations
|
|
529
|
-
api.setDecorations(api.decorationsFromHtml(html));
|
|
201
|
+
|
|
202
|
+
highlight(value) {
|
|
203
|
+
return hljs.highlight(value, { language: 'javascript' }).value;
|
|
530
204
|
},
|
|
531
205
|
});
|
|
532
206
|
```
|
|
533
207
|
|
|
534
|
-
**Important**: hljs token class spans live inside the shadow root. Light-DOM stylesheets cannot pierce the shadow boundary, so theme CSS must be injected via `api.adoptStyleSheet()`.
|
|
535
|
-
|
|
536
|
-
### Example 4: Prism.js Integration
|
|
537
|
-
|
|
538
208
|
```ts
|
|
539
209
|
import Prism from 'prismjs';
|
|
540
210
|
import 'prismjs/components/prism-python';
|
|
@@ -542,770 +212,97 @@ import 'prismjs/components/prism-python';
|
|
|
542
212
|
editor.usePlugin({
|
|
543
213
|
mount(api) {
|
|
544
214
|
api.adoptStyleSheet(`
|
|
545
|
-
.token.keyword { color:
|
|
546
|
-
.token.string
|
|
547
|
-
.token.
|
|
548
|
-
.token.comment { color: #75715e; font-style: italic; }
|
|
549
|
-
.token.function { color: #a1efe4; }
|
|
215
|
+
.token.keyword { color: var(--editor-syntax-keyword); font-weight: 600; }
|
|
216
|
+
.token.string { color: var(--editor-syntax-string); }
|
|
217
|
+
.token.comment { color: var(--editor-syntax-comment); font-style: italic; }
|
|
550
218
|
`);
|
|
551
219
|
},
|
|
552
|
-
highlight(value, api) {
|
|
553
|
-
const html = Prism.highlight(value, Prism.languages.python, 'python');
|
|
554
|
-
api.setDecorations(api.decorationsFromHtml(html));
|
|
555
|
-
},
|
|
556
|
-
});
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
### Switching / Removing Plugins
|
|
560
|
-
|
|
561
|
-
Only one plugin can be active at a time. Calling `usePlugin()` with a new plugin calls `destroy()` on the previous one.
|
|
562
|
-
|
|
563
|
-
```ts
|
|
564
|
-
editor.usePlugin(newPlugin); // previous plugin is destroyed
|
|
565
|
-
editor.removePlugin(); // current plugin destroyed, decorations cleared
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
### Accessing Slotted Textarea
|
|
569
220
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
```ts
|
|
573
|
-
editor.usePlugin({
|
|
574
|
-
mount(api) {
|
|
575
|
-
const textarea = api.host.querySelector('textarea');
|
|
576
|
-
if (textarea) {
|
|
577
|
-
console.log('Form name:', textarea.name);
|
|
578
|
-
console.log('Max length:', textarea.maxLength);
|
|
579
|
-
}
|
|
221
|
+
highlight(value) {
|
|
222
|
+
return Prism.highlight(value, Prism.languages.python, 'python');
|
|
580
223
|
},
|
|
581
224
|
});
|
|
582
225
|
```
|
|
583
226
|
|
|
584
|
-
|
|
227
|
+
For Lezer, Unified, and Shiki integrations, see `@rcarls/rc-textarea-adapters`.
|
|
585
228
|
|
|
586
|
-
##
|
|
229
|
+
## Theming
|
|
587
230
|
|
|
588
|
-
|
|
231
|
+
`rc-textarea` is design-system neutral and uses CSS system colors by default. Theme it with broad tokens
|
|
232
|
+
for the field, then add component tokens for editor-specific surfaces.
|
|
589
233
|
|
|
590
|
-
###
|
|
234
|
+
### Broad Theme Tokens
|
|
591
235
|
|
|
592
|
-
|
|
236
|
+
The component reads inherited tokens where possible:
|
|
593
237
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
from: number; // Inclusive start (0-based character offset into plain text)
|
|
599
|
-
to: number; // Exclusive end
|
|
600
|
-
className?: string; // CSS class added to the span
|
|
601
|
-
bold?: boolean; // Inline: font-weight: bold
|
|
602
|
-
italic?: boolean; // Inline: font-style: italic
|
|
603
|
-
color?: string; // Inline: color (CSS color value)
|
|
604
|
-
background?: string; // Inline: background-color
|
|
605
|
-
underline?: 'solid' | 'wavy' | 'dotted' | 'dashed'; // text-decoration-style
|
|
606
|
-
underlineColor?: string; // text-decoration-color
|
|
607
|
-
attributes?: Record<string, string>; // Extra HTML attributes (data-*, title, etc.)
|
|
608
|
-
}
|
|
609
|
-
```
|
|
238
|
+
| Token | Use |
|
|
239
|
+
| -------------- | ------------------------------------------------------------ |
|
|
240
|
+
| `--rc-text` | Fallback text color before `--rc-textarea-color`. |
|
|
241
|
+
| `color-scheme` | Inherited by the host so system colors match the page theme. |
|
|
610
242
|
|
|
611
|
-
###
|
|
243
|
+
### Component Tokens
|
|
612
244
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
Error-lens messages are rendered via CSS `::after` pseudo-element — they do not appear in selection or clipboard.
|
|
245
|
+
| Token | Default | Use |
|
|
246
|
+
| ----------------------------------------- | ------------------------------------- | ------------------------------------- |
|
|
247
|
+
| `--rc-textarea-font-family` | `monospace` | Editor and gutter font family. |
|
|
248
|
+
| `--rc-textarea-font-size` | `1em` | Editor and gutter font size. |
|
|
249
|
+
| `--rc-textarea-line-height` | `1.5` | Editor and gutter line height. |
|
|
250
|
+
| `--rc-textarea-padding` | `0.5em` | Editor and gutter padding. |
|
|
251
|
+
| `--rc-textarea-background` | `Field` | Field background. |
|
|
252
|
+
| `--rc-textarea-color` | `var(--rc-text, FieldText)` | Field text color. |
|
|
253
|
+
| `--rc-textarea-caret-color` | `var(--rc-textarea-color, FieldText)` | Caret color. |
|
|
254
|
+
| `--rc-textarea-border` | `1px solid ButtonBorder` | Field border. |
|
|
255
|
+
| `--rc-textarea-border-radius` | `2px` | Field corner radius. |
|
|
256
|
+
| `--rc-textarea-focus-outline` | `2px solid Highlight` | Focus ring. |
|
|
257
|
+
| `--rc-textarea-active-line-bg` | `transparent` | Active line background. |
|
|
258
|
+
| `--rc-textarea-gutter-bg` | `Canvas` | Gutter background. |
|
|
259
|
+
| `--rc-textarea-gutter-color` | `GrayText` | Gutter text color. |
|
|
260
|
+
| `--rc-textarea-gutter-border` | `1px solid ButtonBorder` | Gutter separator. |
|
|
261
|
+
| `--rc-textarea-gutter-padding-inline-end` | `0.75em` | Space between gutter labels and text. |
|
|
632
262
|
|
|
633
263
|
```css
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
### WidgetDecoration — non-editable inline element
|
|
644
|
-
|
|
645
|
-
A DOM element inserted at a character offset. Widgets are purely visual and do not appear in the plain text value.
|
|
646
|
-
|
|
647
|
-
```ts
|
|
648
|
-
interface WidgetDecoration {
|
|
649
|
-
id: string;
|
|
650
|
-
type: 'widget';
|
|
651
|
-
offset: number; // Character offset — widget placed before/after this position
|
|
652
|
-
create(): HTMLElement; // Factory called each render — must return a **new** element
|
|
653
|
-
side?: 'before' | 'after'; // Placement relative to character. Default: 'before'
|
|
654
|
-
}
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
**Important**: `create()` is called on every render frame. Return a new element each time; do not reuse the same DOM node.
|
|
658
|
-
|
|
659
|
-
```ts
|
|
660
|
-
api.setDecorations([
|
|
661
|
-
{
|
|
662
|
-
type: 'widget',
|
|
663
|
-
offset: 5,
|
|
664
|
-
create() {
|
|
665
|
-
const el = document.createElement('span');
|
|
666
|
-
el.textContent = '👉';
|
|
667
|
-
el.style.color = '#fab387';
|
|
668
|
-
return el;
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
]);
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
### DecorationInput (for API calls)
|
|
675
|
-
|
|
676
|
-
When calling `api.addDecoration()`, `api.setDecorations()`, etc., omit the `id` field:
|
|
677
|
-
|
|
678
|
-
```ts
|
|
679
|
-
type DecorationInput =
|
|
680
|
-
| Omit<MarkDecoration, 'id'>
|
|
681
|
-
| Omit<LineDecoration, 'id'>
|
|
682
|
-
| Omit<WidgetDecoration, 'id'>;
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
---
|
|
686
|
-
|
|
687
|
-
## Pattern API
|
|
688
|
-
|
|
689
|
-
Patterns automatically apply decorations to all regex matches on every value change, without manual event handling.
|
|
690
|
-
|
|
691
|
-
### Adding and Removing Patterns
|
|
692
|
-
|
|
693
|
-
```ts
|
|
694
|
-
const editor = document.querySelector('rc-textarea');
|
|
695
|
-
|
|
696
|
-
// Boolean pattern — bold + orange all "TODO" occurrences
|
|
697
|
-
const patternId = editor.addPattern({
|
|
698
|
-
pattern: /\bTODO\b/g,
|
|
699
|
-
bold: true,
|
|
700
|
-
color: '#fab387',
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
// Remove a specific pattern
|
|
704
|
-
editor.removePattern(patternId);
|
|
705
|
-
|
|
706
|
-
// Remove all patterns
|
|
707
|
-
editor.clearPatterns();
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
### TextPattern Interface
|
|
711
|
-
|
|
712
|
-
```ts
|
|
713
|
-
interface TextPattern {
|
|
714
|
-
id: string; // Auto-assigned — omit when calling addPattern()
|
|
715
|
-
pattern: RegExp; // Global flag is added automatically if missing
|
|
716
|
-
className?: string;
|
|
717
|
-
bold?: boolean;
|
|
718
|
-
italic?: boolean;
|
|
719
|
-
color?: string;
|
|
720
|
-
background?: string;
|
|
721
|
-
underline?: 'solid' | 'wavy' | 'dotted' | 'dashed';
|
|
722
|
-
underlineColor?: string;
|
|
723
|
-
attributes?: Record<string, string>;
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Per-named-capture-group styles. When set, one MarkDecoration is emitted
|
|
727
|
-
* per named group instead of one for the whole match. The 'd' flag
|
|
728
|
-
* (indices) is added to the pattern automatically.
|
|
729
|
-
* Unmatched optional groups are silently skipped.
|
|
730
|
-
*/
|
|
731
|
-
captureGroups?: Record<string, MarkDecorationStyle>;
|
|
732
|
-
|
|
733
|
-
// Callback to generate a LineDecoration for matching lines
|
|
734
|
-
createLineDecoration?: (match: RegExpMatchArray) =>
|
|
735
|
-
| Omit<LineDecoration, 'id' | 'type' | 'line'>
|
|
736
|
-
| null;
|
|
264
|
+
rc-textarea {
|
|
265
|
+
color-scheme: dark;
|
|
266
|
+
--rc-textarea-font-family: 'Fira Code', monospace;
|
|
267
|
+
--rc-textarea-font-size: 13px;
|
|
268
|
+
--rc-textarea-background: #1e1e2e;
|
|
269
|
+
--rc-textarea-color: #cdd6f4;
|
|
270
|
+
--rc-textarea-border: 1px solid #313244;
|
|
271
|
+
--rc-textarea-active-line-bg: rgb(255 255 255 / 0.06);
|
|
737
272
|
}
|
|
738
|
-
|
|
739
|
-
/** Subset of MarkDecoration properties used for styling (no id / from / to). */
|
|
740
|
-
type MarkDecorationStyle = Pick<MarkDecoration,
|
|
741
|
-
'className' | 'bold' | 'italic' | 'color' | 'background' |
|
|
742
|
-
'underline' | 'underlineColor' | 'attributes'
|
|
743
|
-
>;
|
|
744
273
|
```
|
|
745
274
|
|
|
746
|
-
###
|
|
275
|
+
### Parts And Decoration Styles
|
|
747
276
|
|
|
748
|
-
|
|
749
|
-
// Red wavy underline for "FIXME"
|
|
750
|
-
editor.addPattern({
|
|
751
|
-
pattern: /\bFIXME\b/g,
|
|
752
|
-
color: '#f38ba8',
|
|
753
|
-
underline: 'wavy',
|
|
754
|
-
underlineColor: '#f38ba8',
|
|
755
|
-
});
|
|
756
|
-
```
|
|
277
|
+
The exposed CSS parts are `root`, `gutter`, `gutter-cells`, `editor-area`, and `editor`.
|
|
757
278
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
editor.addPattern({
|
|
762
|
-
pattern: /\bFIXME\b/g,
|
|
763
|
-
bold: true,
|
|
764
|
-
color: '#f38ba8',
|
|
765
|
-
underline: 'wavy',
|
|
766
|
-
createLineDecoration: (match) => ({
|
|
767
|
-
className: 'fixme-line',
|
|
768
|
-
message: 'Review before release',
|
|
769
|
-
messageClassName: 'fixme-message',
|
|
770
|
-
}),
|
|
771
|
-
});
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
Then style the message:
|
|
775
|
-
|
|
776
|
-
```ts
|
|
777
|
-
api.adoptStyleSheet(`
|
|
778
|
-
.v2-line[data-message-class~="fixme-message"]::after {
|
|
779
|
-
color: #f38ba8;
|
|
780
|
-
font-weight: bold;
|
|
781
|
-
}
|
|
782
|
-
`);
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
### Example: Multi-line Regex (JavaScript comments)
|
|
786
|
-
|
|
787
|
-
```ts
|
|
788
|
-
editor.addPattern({
|
|
789
|
-
pattern: /\/\*[\s\S]*?\*\//g,
|
|
790
|
-
italic: true,
|
|
791
|
-
color: '#6c7086',
|
|
792
|
-
createLineDecoration: () => ({
|
|
793
|
-
message: '(comment)',
|
|
794
|
-
messageClassName: 'comment-marker',
|
|
795
|
-
}),
|
|
796
|
-
});
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
### Example: Named Capture Groups (`captureGroups`)
|
|
800
|
-
|
|
801
|
-
Use `captureGroups` to style different parts of a match independently without
|
|
802
|
-
manual offset arithmetic. One `MarkDecoration` is emitted per named group;
|
|
803
|
-
unmatched optional groups are skipped.
|
|
804
|
-
|
|
805
|
-
```ts
|
|
806
|
-
// key: value lines — key in purple, value in green
|
|
807
|
-
editor.addPattern({
|
|
808
|
-
pattern: /^(?<key>\w[\w-]*):\s*(?<value>.+)$/gm,
|
|
809
|
-
captureGroups: {
|
|
810
|
-
key: { bold: true, color: '#c792ea' },
|
|
811
|
-
value: { color: '#c3e88d' },
|
|
812
|
-
},
|
|
813
|
-
});
|
|
814
|
-
```
|
|
815
|
-
|
|
816
|
-
```ts
|
|
817
|
-
// Ingredient lines — quantity/measure bold, prep text muted
|
|
818
|
-
editor.addPattern({
|
|
819
|
-
pattern: /^(?<qty>[\d\s\/\u215B-\u215E]+\s+\w+)\s+(?<name>[^,]+?)(?<prep>,\s+.+)?$/gm,
|
|
820
|
-
captureGroups: {
|
|
821
|
-
qty: { bold: true },
|
|
822
|
-
name: { bold: true, color: 'var(--color-primary)' },
|
|
823
|
-
prep: { italic: true, color: 'var(--color-text-muted)' },
|
|
824
|
-
},
|
|
825
|
-
});
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
---
|
|
829
|
-
|
|
830
|
-
## Plugin Helpers
|
|
831
|
-
|
|
832
|
-
Two utilities are exported from the package to reduce plugin boilerplate.
|
|
833
|
-
|
|
834
|
-
### `matchPatternResults(value, patterns)`
|
|
835
|
-
|
|
836
|
-
Run a `TextPattern` array against `value` and return all matches as
|
|
837
|
-
decoration objects — without registering the patterns on the editor element.
|
|
838
|
-
Useful when a plugin needs to combine pattern-matched decorations with custom
|
|
839
|
-
decorations in a single `api.setDecorations()` call.
|
|
840
|
-
|
|
841
|
-
```ts
|
|
842
|
-
import { matchPatternResults } from '@rcarls/rc-textarea';
|
|
843
|
-
|
|
844
|
-
const KEYWORD_PATTERNS = [
|
|
845
|
-
{ id: 'kw-function', pattern: /\bfunction\b/g, bold: true, color: '#c792ea' },
|
|
846
|
-
{ id: 'kw-return', pattern: /\breturn\b/g, bold: true, color: '#89ddff' },
|
|
847
|
-
];
|
|
848
|
-
|
|
849
|
-
editor.usePlugin({
|
|
850
|
-
update(value, api) {
|
|
851
|
-
const { markDecorations, lineDecorations } =
|
|
852
|
-
matchPatternResults(value, KEYWORD_PATTERNS);
|
|
853
|
-
|
|
854
|
-
// Merge with custom diagnostics from a parser
|
|
855
|
-
const diagnostics = myParser.lint(value);
|
|
856
|
-
|
|
857
|
-
api.setDecorations([
|
|
858
|
-
...markDecorations,
|
|
859
|
-
...lineDecorations,
|
|
860
|
-
...diagnostics,
|
|
861
|
-
]);
|
|
862
|
-
},
|
|
863
|
-
});
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
### `createLineDecoratorPlugin(decorator, options?)`
|
|
867
|
-
|
|
868
|
-
Factory that wraps a `LineDecoratorPlugin` in a full `RCTextareaPlugin`. It handles:
|
|
869
|
-
|
|
870
|
-
- CSS injection via `api.adoptStyleSheet` on `mount()`
|
|
871
|
-
- `lineStart` offset bookkeeping — `decorateLine()` works with **line-relative** offsets (0 = start of the line), not absolute document offsets
|
|
872
|
-
- Optional `watch` subscriptions that call `api.scheduleUpdate()` when external values change (framework-agnostic)
|
|
873
|
-
- Cleanup of subscribers on `destroy()`
|
|
874
|
-
|
|
875
|
-
```ts
|
|
876
|
-
import { createLineDecoratorPlugin } from '@rcarls/rc-textarea';
|
|
877
|
-
import type { LineDecoratorPlugin } from '@rcarls/rc-textarea';
|
|
878
|
-
|
|
879
|
-
const KEYWORD_CSS = '.kw { font-weight: bold; color: #c792ea; }';
|
|
880
|
-
|
|
881
|
-
const keywordDecorator: LineDecoratorPlugin = {
|
|
882
|
-
styles: KEYWORD_CSS,
|
|
883
|
-
decorateLine(line) {
|
|
884
|
-
const results = [];
|
|
885
|
-
for (const m of line.matchAll(/\bfunction\b/g)) {
|
|
886
|
-
results.push({
|
|
887
|
-
type: 'mark' as const,
|
|
888
|
-
from: m.index!,
|
|
889
|
-
to: m.index! + m[0].length,
|
|
890
|
-
className: 'kw',
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
return results;
|
|
894
|
-
},
|
|
895
|
-
};
|
|
896
|
-
|
|
897
|
-
editor.usePlugin(createLineDecoratorPlugin(keywordDecorator));
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
#### With `extraDecorations` (merging whole-document results)
|
|
901
|
-
|
|
902
|
-
```ts
|
|
903
|
-
editor.usePlugin(createLineDecoratorPlugin(
|
|
904
|
-
myLineDecorator,
|
|
905
|
-
{
|
|
906
|
-
extraDecorations: (value) => {
|
|
907
|
-
// e.g. whole-document diagnostics from a parser
|
|
908
|
-
return myParser.lint(value).map(d => ({
|
|
909
|
-
type: 'line' as const,
|
|
910
|
-
line: d.line,
|
|
911
|
-
message: d.message,
|
|
912
|
-
messageClassName: 'diagnostic-error',
|
|
913
|
-
}));
|
|
914
|
-
},
|
|
915
|
-
},
|
|
916
|
-
));
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
#### With `watch` (react to external signal changes)
|
|
920
|
-
|
|
921
|
-
`watch` is an array of subscriber setup functions — each receives an `onChange`
|
|
922
|
-
callback and may return an optional cleanup function. This is
|
|
923
|
-
intentionally framework-agnostic.
|
|
924
|
-
|
|
925
|
-
```ts
|
|
926
|
-
// Vanilla JS: subscribe to a custom event
|
|
927
|
-
editor.usePlugin(createLineDecoratorPlugin(
|
|
928
|
-
myDecorator,
|
|
929
|
-
{
|
|
930
|
-
watch: [
|
|
931
|
-
(onChange) => {
|
|
932
|
-
window.addEventListener('theme-change', onChange);
|
|
933
|
-
return () => window.removeEventListener('theme-change', onChange);
|
|
934
|
-
},
|
|
935
|
-
],
|
|
936
|
-
},
|
|
937
|
-
));
|
|
938
|
-
|
|
939
|
-
// Solid.js: pass reactive signals
|
|
940
|
-
import { createEffect, on } from 'solid-js';
|
|
941
|
-
|
|
942
|
-
editor.usePlugin(createLineDecoratorPlugin(
|
|
943
|
-
myDecorator,
|
|
944
|
-
{
|
|
945
|
-
watch: [
|
|
946
|
-
(cb) => createEffect(on(mySignal, cb, { defer: true })),
|
|
947
|
-
],
|
|
948
|
-
},
|
|
949
|
-
));
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
---
|
|
953
|
-
|
|
954
|
-
## Selection API
|
|
955
|
-
|
|
956
|
-
The component tracks the current selection as plain-text offsets (not DOM nodes or ranges).
|
|
957
|
-
|
|
958
|
-
### Getting Selection
|
|
959
|
-
|
|
960
|
-
```ts
|
|
961
|
-
interface RCTextareaPluginAPI {
|
|
962
|
-
readonly selectionStart: number;
|
|
963
|
-
readonly selectionEnd: number;
|
|
964
|
-
|
|
965
|
-
getCursorRect(): DOMRect | null;
|
|
966
|
-
getWordAtCursor(): { word: string; from: number; to: number } | null;
|
|
967
|
-
onCursorMove(callback: (start: number, end: number) => void): () => void;
|
|
279
|
+
```css
|
|
280
|
+
rc-textarea::part(editor) {
|
|
281
|
+
tab-size: 2;
|
|
968
282
|
}
|
|
969
283
|
```
|
|
970
284
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
let unsub: () => void;
|
|
975
|
-
|
|
976
|
-
editor.usePlugin({
|
|
977
|
-
mount(api) {
|
|
978
|
-
unsub = api.onCursorMove((start, end) => {
|
|
979
|
-
if (start === end) {
|
|
980
|
-
// Cursor is collapsed (no selection)
|
|
981
|
-
const word = api.getWordAtCursor();
|
|
982
|
-
if (word) {
|
|
983
|
-
console.log(`Cursor on word: "${word.word}" at ${word.from}–${word.to}`);
|
|
984
|
-
}
|
|
985
|
-
} else {
|
|
986
|
-
// Selection active
|
|
987
|
-
console.log(`Selection: ${start}–${end}`);
|
|
988
|
-
}
|
|
989
|
-
});
|
|
990
|
-
},
|
|
991
|
-
destroy() {
|
|
992
|
-
unsub?.();
|
|
993
|
-
},
|
|
994
|
-
});
|
|
995
|
-
```
|
|
996
|
-
|
|
997
|
-
### Example: Anchoring Autocomplete Popup
|
|
998
|
-
|
|
999
|
-
```ts
|
|
1000
|
-
editor.usePlugin({
|
|
1001
|
-
mount(api) {
|
|
1002
|
-
const unsub = api.onCursorMove(() => {
|
|
1003
|
-
const rect = api.getCursorRect();
|
|
1004
|
-
if (rect) {
|
|
1005
|
-
const popup = document.querySelector('#autocomplete');
|
|
1006
|
-
popup.style.left = rect.left + 'px';
|
|
1007
|
-
popup.style.top = (rect.top + rect.height) + 'px';
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
return () => unsub?.();
|
|
1011
|
-
},
|
|
1012
|
-
});
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
---
|
|
1016
|
-
|
|
1017
|
-
## Decoration Lifecycle
|
|
1018
|
-
|
|
1019
|
-
Decorations undergo transformation as the text changes:
|
|
1020
|
-
|
|
1021
|
-
### Mapping Decorations Through Changes
|
|
1022
|
-
|
|
1023
|
-
When text is edited, existing plugin decorations are automatically adjusted:
|
|
1024
|
-
|
|
1025
|
-
- **Before the edit region**: start/end offsets unchanged
|
|
1026
|
-
- **After the edit region**: start/end offsets shifted by the character delta
|
|
1027
|
-
- **Overlapping the edit region**: start/end clamped to edit boundaries
|
|
1028
|
-
- **Zero-width after clamping**: decoration is dropped
|
|
1029
|
-
|
|
1030
|
-
### Large Change Heuristic
|
|
1031
|
-
|
|
1032
|
-
If a change is detected as **"large"** (heuristic: `changeSize > 50 chars AND changeSize > 50% of document`), all plugin decorations are cleared to avoid mapping errors. This handles:
|
|
1033
|
-
|
|
1034
|
-
- Paste of large text blocks
|
|
1035
|
-
- Select-all + type
|
|
1036
|
-
- Programmatic `value` set with very different content
|
|
1037
|
-
|
|
1038
|
-
Pattern decorations are **always** fully recomputed on each change (regex is re-run).
|
|
1039
|
-
|
|
1040
|
-
### Architecture Note for Agents
|
|
1041
|
-
|
|
1042
|
-
Decoration mapping occurs in `mapDecorationsThroughChange()` (in `decoration.ts`). The function:
|
|
1043
|
-
1. Calls `findEdit()` to locate insertion/deletion boundaries
|
|
1044
|
-
2. Applies geometry transformation to each decoration
|
|
1045
|
-
3. Returns a new decoration map (old map is not mutated)
|
|
1046
|
-
|
|
1047
|
-
---
|
|
1048
|
-
|
|
1049
|
-
## Undo / Redo
|
|
1050
|
-
|
|
1051
|
-
The component maintains its own undo/redo stack because DOM rebuilds (which happen on every render frame) invalidate the browser's native contenteditable undo history.
|
|
1052
|
-
|
|
1053
|
-
### Keyboard Shortcuts
|
|
1054
|
-
|
|
1055
|
-
| Key | Action |
|
|
1056
|
-
|-----|--------|
|
|
1057
|
-
| `Ctrl+Z` / `Cmd+Z` | Undo |
|
|
1058
|
-
| `Ctrl+Y` / `Cmd+Y` / `Ctrl+Shift+Z` | Redo |
|
|
1059
|
-
|
|
1060
|
-
### Stack Details
|
|
1061
|
-
|
|
1062
|
-
- **Capacity**: 100 entries (MAX_UNDO)
|
|
1063
|
-
- **Stored per entry**: plain text value + cursor position (anchorOffset, focusOffset)
|
|
1064
|
-
- **Decorations**: not stored; recomputed from the restored value
|
|
1065
|
-
- **Granularity**: one entry per input event (or scheduled update)
|
|
1066
|
-
|
|
1067
|
-
### Architecture
|
|
1068
|
-
|
|
1069
|
-
The undo stack is stored in:
|
|
1070
|
-
```ts
|
|
1071
|
-
private _undoStack: UndoEntry[] = [];
|
|
1072
|
-
private _undoIndex = -1; // Current position in stack
|
|
1073
|
-
```
|
|
1074
|
-
|
|
1075
|
-
Each entry is created when text changes via input event. The stack is pruned to MAX_UNDO when full.
|
|
1076
|
-
|
|
1077
|
-
---
|
|
285
|
+
Decoration elements are inside the shadow root. Use `api.adoptStyleSheet()` from a plugin to
|
|
286
|
+
style custom classes such as `.my-highlight`, `.line[data-message]`, or token classes from a
|
|
287
|
+
syntax highlighter.
|
|
1078
288
|
|
|
1079
289
|
## Keyboard Behavior
|
|
1080
290
|
|
|
1081
|
-
| Key
|
|
1082
|
-
|
|
1083
|
-
| `Tab`
|
|
1084
|
-
| `Ctrl/Cmd+Z`
|
|
1085
|
-
| `Ctrl/Cmd+Y` / `Ctrl/Cmd+Shift+Z` | Redo
|
|
1086
|
-
| `Paste`
|
|
1087
|
-
| `Enter`
|
|
1088
|
-
|
|
1089
|
-
### Paste Handling
|
|
1090
|
-
|
|
1091
|
-
The component uses `input` event to detect and normalize pasted content. HTML markup is discarded; only plain text characters are inserted.
|
|
1092
|
-
|
|
1093
|
-
---
|
|
1094
|
-
|
|
1095
|
-
## Form Integration
|
|
1096
|
-
|
|
1097
|
-
The slotted `<textarea>` participates in form submission normally:
|
|
1098
|
-
|
|
1099
|
-
1. **Hidden visually** — inline styles: `position: absolute; left: -9999px; opacity: 0; clip: rect(0 0 0 0)`
|
|
1100
|
-
2. **DOM remains** — slotted at the light DOM so form traversal finds it
|
|
1101
|
-
3. **Value synced** — component keeps textarea's value in sync with `this._value`
|
|
1102
|
-
4. **Attributes adopted** — `name`, `required`, `disabled`, `maxlength`, `placeholder` are read from textarea on mount
|
|
1103
|
-
5. **Submission** — form submission reads textarea's value directly
|
|
1104
|
-
|
|
1105
|
-
### Example
|
|
1106
|
-
|
|
1107
|
-
```html
|
|
1108
|
-
<form id="myform">
|
|
1109
|
-
<rc-textarea>
|
|
1110
|
-
<textarea
|
|
1111
|
-
name="body"
|
|
1112
|
-
required
|
|
1113
|
-
maxlength="5000"
|
|
1114
|
-
placeholder="Enter your message…"
|
|
1115
|
-
></textarea>
|
|
1116
|
-
</rc-textarea>
|
|
1117
|
-
<button type="submit">Send</button>
|
|
1118
|
-
</form>
|
|
1119
|
-
```
|
|
1120
|
-
|
|
1121
|
-
```ts
|
|
1122
|
-
document.getElementById('myform').addEventListener('submit', (e) => {
|
|
1123
|
-
e.preventDefault();
|
|
1124
|
-
const formData = new FormData(e.target);
|
|
1125
|
-
console.log(formData.get('body')); // Plain text value
|
|
1126
|
-
});
|
|
1127
|
-
```
|
|
1128
|
-
|
|
1129
|
-
---
|
|
1130
|
-
|
|
1131
|
-
## Accessibility
|
|
1132
|
-
|
|
1133
|
-
The component is designed with accessibility in mind:
|
|
1134
|
-
|
|
1135
|
-
### ARIA Roles & Attributes
|
|
1136
|
-
|
|
1137
|
-
- **`role="textbox"`** — identifies the editor as a text input
|
|
1138
|
-
- **`aria-multiline="true"`** — indicates multiline text support
|
|
1139
|
-
- **`aria-label`** — set via the `label` attribute (optional)
|
|
1140
|
-
- **`delegatesFocus=true`** — shadow DOM focus is delegated to the editable region
|
|
1141
|
-
|
|
1142
|
-
### Keyboard Navigation
|
|
1143
|
-
|
|
1144
|
-
- Tab and Shift+Tab focus/blur the editor normally
|
|
1145
|
-
- Arrow keys, Home/End, Ctrl+Arrow, etc. work natively
|
|
1146
|
-
- Screen readers can read selected text and cursor position
|
|
1147
|
-
|
|
1148
|
-
### Visual Accessibility
|
|
1149
|
-
|
|
1150
|
-
- High contrast by default (system colors)
|
|
1151
|
-
- `:focus-visible` indicator (can be styled via CSS custom property)
|
|
1152
|
-
- Line numbers are `aria-hidden="true"` (not part of screen reader navigation)
|
|
1153
|
-
|
|
1154
|
-
### Spellcheck Disabled
|
|
1155
|
-
|
|
1156
|
-
Spell checking is disabled to prevent vendor-specific squiggles from interfering with custom decorations. Set explicitly in the editor:
|
|
1157
|
-
|
|
1158
|
-
```ts
|
|
1159
|
-
editor.spellcheck = false;
|
|
1160
|
-
editor.autocorrect = 'off';
|
|
1161
|
-
editor.autocapitalize = 'off';
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
---
|
|
1165
|
-
|
|
1166
|
-
## Performance Considerations
|
|
1167
|
-
|
|
1168
|
-
### DOM Rebuild on Every Render
|
|
1169
|
-
|
|
1170
|
-
The blot tree is **completely rebuilt** on each render frame (RAF-batched). This is necessary because:
|
|
1171
|
-
- Decoration changes require DOM restructuring
|
|
1172
|
-
- Plain-text ↔ DOM offset conversion requires a stable tree
|
|
1173
|
-
- Cursor restoration requires rebuilding after changes
|
|
1174
|
-
|
|
1175
|
-
For documents **< 10,000 characters**, this is imperceptible. For very large documents, consider:
|
|
1176
|
-
- Truncating visible content (virtual scrolling)
|
|
1177
|
-
- Debouncing plugin updates
|
|
1178
|
-
- Using pattern decorations instead of plugin updates (patterns are optimized)
|
|
291
|
+
| Key | Behavior |
|
|
292
|
+
| --------------------------------- | ------------------------------------------------------------ |
|
|
293
|
+
| `Tab` | Inserts `\t`. |
|
|
294
|
+
| `Ctrl/Cmd+Z` | Undo. |
|
|
295
|
+
| `Ctrl/Cmd+Y` / `Ctrl/Cmd+Shift+Z` | Redo. |
|
|
296
|
+
| `Paste` | Inserts plain text only and normalizes line endings to `\n`. |
|
|
297
|
+
| `Enter` | Inserts a line break. |
|
|
1179
298
|
|
|
1180
|
-
|
|
299
|
+
## More Detail
|
|
1181
300
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
If density is high, profile with DevTools to identify bottlenecks.
|
|
1188
|
-
|
|
1189
|
-
### Memory
|
|
1190
|
-
|
|
1191
|
-
The undo/redo stack stores 100 entries of `{ value, anchorOffset, focusOffset }`. For a 10 KB document, this is ~1 MB. For very large documents, consider limiting undo depth or using an external undo manager.
|
|
1192
|
-
|
|
1193
|
-
---
|
|
1194
|
-
|
|
1195
|
-
## Troubleshooting
|
|
1196
|
-
|
|
1197
|
-
### Selection is lost after edit
|
|
1198
|
-
|
|
1199
|
-
Normal behavior — selection is saved before render and restored after. If you're updating text and immediately reading `selectionStart`, use `onCursorMove()` or `setTimeout()` to wait for the next render frame.
|
|
1200
|
-
|
|
1201
|
-
```ts
|
|
1202
|
-
editor.value = 'new text';
|
|
1203
|
-
// DON'T do this:
|
|
1204
|
-
console.log(editor.selectionStart); // May be stale
|
|
1205
|
-
|
|
1206
|
-
// DO this:
|
|
1207
|
-
await editor.updateComplete;
|
|
1208
|
-
const start = editor.value === 'new text' ? api.selectionStart : null;
|
|
1209
|
-
```
|
|
1210
|
-
|
|
1211
|
-
### Decorations disappear after large paste
|
|
1212
|
-
|
|
1213
|
-
If you paste a large block of text (> 50 chars and > 50% of document), plugin decorations are cleared by the "large change heuristic" to avoid mapping errors. Pattern decorations are reapplied. To preserve plugin decorations, update them in the plugin's `update()` method:
|
|
1214
|
-
|
|
1215
|
-
```ts
|
|
1216
|
-
editor.usePlugin({
|
|
1217
|
-
update(value, api) {
|
|
1218
|
-
// Recompute decorations from the new value
|
|
1219
|
-
const decs = parseText(value);
|
|
1220
|
-
api.setDecorations(decs);
|
|
1221
|
-
},
|
|
1222
|
-
});
|
|
1223
|
-
```
|
|
1224
|
-
|
|
1225
|
-
### Other's DOM change events fire inside editor
|
|
1226
|
-
|
|
1227
|
-
The component does not suppress mutation events on the editor. If you're listening for `MutationObserver` events on the editor element, you'll observe all blot tree rebuilds. To avoid this, listen outside the editor or throttle updates.
|
|
1228
|
-
|
|
1229
|
-
### cursor doesn't stay at expected position
|
|
1230
|
-
|
|
1231
|
-
Cursor restoration uses plain-text offsets. If decorations change (especially widgets, which take up no space in the value), the visual cursor position may shift. This is expected. Use `getCursorRect()` to anchor UI elements if precise positioning is critical.
|
|
1232
|
-
|
|
1233
|
-
### Text is very long (> 100 KB), editor is slow
|
|
1234
|
-
|
|
1235
|
-
The entire blot tree is rebuilt on each character input. For very large documents:
|
|
1236
|
-
- Profile with DevTools Performance tab to identify bottleneck
|
|
1237
|
-
- Consider truncating visible content (virtual scrolling)
|
|
1238
|
-
- Use `word-wrap: false` to reduce line breaks (fewer blots)
|
|
1239
|
-
- Limit undo depth: only keep last 10 entries instead of 100
|
|
1240
|
-
|
|
1241
|
-
### Plugin's `highlight()` result is overwritten immediately
|
|
1242
|
-
|
|
1243
|
-
If both `update()` and `highlight()` are provided, only one runs per render. The component runs `highlight()` if it returns a truthy value; otherwise, `update()` is called.
|
|
1244
|
-
|
|
1245
|
-
---
|
|
1246
|
-
|
|
1247
|
-
## API Reference Summary
|
|
1248
|
-
|
|
1249
|
-
### Properties
|
|
1250
|
-
|
|
1251
|
-
| Property | Type | Default | Reflects |
|
|
1252
|
-
|----------|------|---------|----------|
|
|
1253
|
-
| `value` | `string` | `''` | N/A |
|
|
1254
|
-
| `lineNumbers` | `boolean` | `false` | Yes (*line-numbers* attr) |
|
|
1255
|
-
| `listNumbers` | `boolean` | `false` | Yes (*list-numbers* attr) |
|
|
1256
|
-
| `gutter` | `boolean` | `false` | Yes (*gutter* attr) |
|
|
1257
|
-
| `wordWrap` | `boolean` | `false` | Yes (*word-wrap* attr) |
|
|
1258
|
-
| `autoGrow` | `boolean` | `false` | Yes (*auto-grow* attr) |
|
|
1259
|
-
| `readOnly` | `boolean` | `false` | Yes (*read-only* attr) |
|
|
1260
|
-
| `label` | `string \| null` | `null` | No |
|
|
1261
|
-
| `selectionStart` | `number` | `0` | No (readonly) |
|
|
1262
|
-
| `selectionEnd` | `number` | `0` | No (readonly) |
|
|
1263
|
-
|
|
1264
|
-
### Methods
|
|
1265
|
-
|
|
1266
|
-
| Method | Signature | Returns |
|
|
1267
|
-
|--------|-----------|---------|
|
|
1268
|
-
| `usePlugin()` | `(plugin: RCTextareaPlugin) => void` | — |
|
|
1269
|
-
| `removePlugin()` | `() => void` | — |
|
|
1270
|
-
| `addPattern()` | `(pattern: Omit<TextPattern, 'id'>) => string` | Pattern ID |
|
|
1271
|
-
| `removePattern()` | `(id: string) => void` | — |
|
|
1272
|
-
| `clearPatterns()` | `() => void` | — |
|
|
1273
|
-
|
|
1274
|
-
### Exported Helpers
|
|
1275
|
-
|
|
1276
|
-
| Export | Kind | Description |
|
|
1277
|
-
| ------ | ---- | ----------- |
|
|
1278
|
-
| `matchPatternResults` | function | Run `TextPattern[]` against a string; returns `{ markDecorations, lineDecorations }` |
|
|
1279
|
-
| `createLineDecoratorPlugin` | function | Wrap a `LineDecoratorPlugin` in a full `RCTextareaPlugin` |
|
|
1280
|
-
| `MarkDecorationStyle` | type | Styling-only subset of `MarkDecoration` (used in `TextPattern.captureGroups`) |
|
|
1281
|
-
| `LineDecoratorPlugin` | interface | Per-line decorator with line-relative offsets |
|
|
1282
|
-
| `LineDecoratorPluginOptions` | interface | Options for `createLineDecoratorPlugin` |
|
|
1283
|
-
|
|
1284
|
-
### Events
|
|
1285
|
-
|
|
1286
|
-
| Event | Bubbles | Composed | Detail |
|
|
1287
|
-
|-------|---------|----------|--------|
|
|
1288
|
-
| `rc-textarea-change` | Yes | Yes | `{ value: string }` |
|
|
1289
|
-
| `rc-textarea-focus` | Yes | Yes | — |
|
|
1290
|
-
| `rc-textarea-blur` | Yes | Yes | — |
|
|
1291
|
-
| `rc-textarea-select` | Yes | Yes | `{ selectionStart: number, selectionEnd: number }` |
|
|
1292
|
-
|
|
1293
|
-
### CSS Custom Properties
|
|
1294
|
-
|
|
1295
|
-
All custom properties are listed in [CSS Customization](#css-customization).
|
|
1296
|
-
|
|
1297
|
-
### CSS Parts
|
|
1298
|
-
|
|
1299
|
-
All parts are listed in [CSS Customization](#css-customization).
|
|
1300
|
-
|
|
1301
|
-
---
|
|
1302
|
-
|
|
1303
|
-
## Support & Contributing
|
|
1304
|
-
|
|
1305
|
-
For issues, feature requests, or contributions, visit the [rc-webcomponents repository](https://github.com/richardcarls/rc-webcomponents).
|
|
1306
|
-
|
|
1307
|
-
---
|
|
301
|
+
- [PLUGIN_AUTHORING.md](PLUGIN_AUTHORING.md) covers custom plugins, decoration types,
|
|
302
|
+
selection APIs, stylesheet injection, and parser/highlighter recipes.
|
|
303
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) is an internal contributor reference for the rendering
|
|
304
|
+
loop, Parchment integration, selection mapping, and gutter synchronization.
|
|
1308
305
|
|
|
1309
306
|
## License
|
|
1310
307
|
|
|
1311
|
-
[MIT](LICENSE)
|
|
308
|
+
[MIT](../../LICENSE)
|