tulih-editor 0.1.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/LICENSE +21 -0
- package/README.md +331 -0
- package/dist/tulih-editor.css +1 -0
- package/dist/tulih-editor.es.js +3051 -0
- package/dist/tulih-editor.es.js.map +1 -0
- package/dist/tulih-editor.umd.js +8 -0
- package/dist/tulih-editor.umd.js.map +1 -0
- package/dist/types/core/Editor.d.ts +20 -0
- package/dist/types/core/PluginManager.d.ts +22 -0
- package/dist/types/core/helpers.d.ts +22 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/plugins/align.d.ts +3 -0
- package/dist/types/plugins/autoLinkify.d.ts +3 -0
- package/dist/types/plugins/autosave.d.ts +3 -0
- package/dist/types/plugins/block.d.ts +3 -0
- package/dist/types/plugins/caseTransform.d.ts +3 -0
- package/dist/types/plugins/codeBlock.d.ts +3 -0
- package/dist/types/plugins/colors.d.ts +3 -0
- package/dist/types/plugins/darkMode.d.ts +3 -0
- package/dist/types/plugins/direction.d.ts +3 -0
- package/dist/types/plugins/dragDrop.d.ts +3 -0
- package/dist/types/plugins/emoji.d.ts +3 -0
- package/dist/types/plugins/emojiAutocomplete.d.ts +3 -0
- package/dist/types/plugins/findReplace.d.ts +3 -0
- package/dist/types/plugins/floatingToolbar.d.ts +3 -0
- package/dist/types/plugins/fontFamily.d.ts +3 -0
- package/dist/types/plugins/fontSize.d.ts +3 -0
- package/dist/types/plugins/fullscreen.d.ts +3 -0
- package/dist/types/plugins/history.d.ts +3 -0
- package/dist/types/plugins/hr.d.ts +3 -0
- package/dist/types/plugins/iframe.d.ts +3 -0
- package/dist/types/plugins/image.d.ts +3 -0
- package/dist/types/plugins/imageProps.d.ts +3 -0
- package/dist/types/plugins/imageTools.d.ts +3 -0
- package/dist/types/plugins/indent.d.ts +3 -0
- package/dist/types/plugins/index.d.ts +2 -0
- package/dist/types/plugins/inline.d.ts +3 -0
- package/dist/types/plugins/inlineCode.d.ts +3 -0
- package/dist/types/plugins/keyboardShortcuts.d.ts +3 -0
- package/dist/types/plugins/lineHeight.d.ts +3 -0
- package/dist/types/plugins/link.d.ts +3 -0
- package/dist/types/plugins/linkTooltip.d.ts +3 -0
- package/dist/types/plugins/list.d.ts +3 -0
- package/dist/types/plugins/markdown.d.ts +3 -0
- package/dist/types/plugins/mediaEmbed.d.ts +3 -0
- package/dist/types/plugins/pasteImage.d.ts +3 -0
- package/dist/types/plugins/pastePlain.d.ts +3 -0
- package/dist/types/plugins/pre.d.ts +3 -0
- package/dist/types/plugins/readOnly.d.ts +3 -0
- package/dist/types/plugins/shortcutCustomizer.d.ts +3 -0
- package/dist/types/plugins/shortcutsHelp.d.ts +3 -0
- package/dist/types/plugins/source.d.ts +3 -0
- package/dist/types/plugins/specialChars.d.ts +3 -0
- package/dist/types/plugins/statusBar.d.ts +3 -0
- package/dist/types/plugins/subSuper.d.ts +3 -0
- package/dist/types/plugins/table.d.ts +3 -0
- package/dist/types/plugins/tableBg.d.ts +3 -0
- package/dist/types/plugins/tableTools.d.ts +3 -0
- package/dist/types/plugins/toolbarCollapse.d.ts +3 -0
- package/dist/types/plugins/unlink.d.ts +3 -0
- package/dist/types/plugins/wordCount.d.ts +3 -0
- package/dist/types/types.d.ts +226 -0
- package/package.json +66 -0
- package/src/core/Editor.ts +460 -0
- package/src/core/PluginManager.ts +140 -0
- package/src/core/helpers.ts +209 -0
- package/src/css.d.ts +2 -0
- package/src/index.ts +87 -0
- package/src/plugins/align.ts +72 -0
- package/src/plugins/autoLinkify.ts +34 -0
- package/src/plugins/autosave.ts +69 -0
- package/src/plugins/block.ts +32 -0
- package/src/plugins/caseTransform.ts +54 -0
- package/src/plugins/codeBlock.ts +93 -0
- package/src/plugins/colors.ts +68 -0
- package/src/plugins/darkMode.ts +123 -0
- package/src/plugins/direction.ts +30 -0
- package/src/plugins/dragDrop.ts +68 -0
- package/src/plugins/emoji.ts +188 -0
- package/src/plugins/emojiAutocomplete.ts +183 -0
- package/src/plugins/findReplace.ts +229 -0
- package/src/plugins/floatingToolbar.ts +258 -0
- package/src/plugins/fontFamily.ts +41 -0
- package/src/plugins/fontSize.ts +32 -0
- package/src/plugins/fullscreen.ts +36 -0
- package/src/plugins/history.ts +14 -0
- package/src/plugins/hr.ts +118 -0
- package/src/plugins/iframe.ts +88 -0
- package/src/plugins/image.ts +107 -0
- package/src/plugins/imageProps.ts +119 -0
- package/src/plugins/imageTools.ts +344 -0
- package/src/plugins/indent.ts +29 -0
- package/src/plugins/index.ts +101 -0
- package/src/plugins/inline.ts +17 -0
- package/src/plugins/inlineCode.ts +21 -0
- package/src/plugins/keyboardShortcuts.ts +92 -0
- package/src/plugins/lineHeight.ts +40 -0
- package/src/plugins/link.ts +344 -0
- package/src/plugins/linkTooltip.ts +63 -0
- package/src/plugins/list.ts +141 -0
- package/src/plugins/markdown.ts +61 -0
- package/src/plugins/mediaEmbed.ts +44 -0
- package/src/plugins/pasteImage.ts +61 -0
- package/src/plugins/pastePlain.ts +43 -0
- package/src/plugins/pre.ts +11 -0
- package/src/plugins/readOnly.ts +46 -0
- package/src/plugins/shortcutCustomizer.ts +125 -0
- package/src/plugins/shortcutsHelp.ts +51 -0
- package/src/plugins/source.ts +77 -0
- package/src/plugins/specialChars.ts +64 -0
- package/src/plugins/statusBar.ts +85 -0
- package/src/plugins/subSuper.ts +20 -0
- package/src/plugins/table.ts +166 -0
- package/src/plugins/tableBg.ts +11 -0
- package/src/plugins/tableTools.ts +475 -0
- package/src/plugins/toolbarCollapse.ts +14 -0
- package/src/plugins/unlink.ts +29 -0
- package/src/plugins/wordCount.ts +34 -0
- package/src/styles/base.css +258 -0
- package/src/styles/editor.css +309 -0
- package/src/styles/index.css +6 -0
- package/src/types.ts +278 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hendra Randy Nomura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# TulihEditor
|
|
2
|
+
|
|
3
|
+
A lightweight, **zero-runtime-dependency** WYSIWYG rich text editor for the browser,
|
|
4
|
+
built on `contenteditable` and `document.execCommand()`. Written in **TypeScript**, it
|
|
5
|
+
ships a modular plugin system with **47 built-in plugins** and ships its own CSS — no
|
|
6
|
+
Bootstrap or CSS framework required on your page.
|
|
7
|
+
|
|
8
|
+
- 🧩 **Plugin architecture** — every feature is a self-contained plugin
|
|
9
|
+
- 🪶 **Tiny & dependency-free** — ~36 KB gzipped, no runtime dependencies
|
|
10
|
+
- 🎨 **Self-contained styling** — works without Bootstrap; icons via Tabler Icons (auto-loaded)
|
|
11
|
+
- 🔒 **Built-in HTML sanitizer** — configurable allowed tags/attributes, URL scheme allow-lists, iframe domain allow-list
|
|
12
|
+
- 📦 **ESM + UMD builds** — `import` it or drop a `<script>` tag
|
|
13
|
+
- ⌨️ **TypeScript-first** — full type definitions for the API and the plugin contract
|
|
14
|
+
|
|
15
|
+
> Ported to a standalone package from the original implementation that lived inside a
|
|
16
|
+
> Laravel app. The runtime behavior is identical; this repo is the framework-agnostic,
|
|
17
|
+
> open-source distribution.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install tulih-editor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
import TulihEditor from 'tulih-editor';
|
|
27
|
+
import 'tulih-editor/style.css';
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or via a `<script>` tag (UMD global `TulihEditor`):
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<link rel="stylesheet" href="https://unpkg.com/tulih-editor/dist/tulih-editor.css">
|
|
34
|
+
<script src="https://unpkg.com/tulih-editor/dist/tulih-editor.umd.js"></script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The toolbar icons use [Tabler Icons](https://tabler.io/icons); the webfont (pinned to the
|
|
38
|
+
`v3` line) is loaded automatically from jsDelivr on first use, only if a `tabler-icons`
|
|
39
|
+
stylesheet is not already present on the page. To self-host instead, add your own
|
|
40
|
+
`tabler-icons` stylesheet to the page before TulihEditor loads — the auto-loader will
|
|
41
|
+
detect it and skip the CDN request.
|
|
42
|
+
|
|
43
|
+
## Basic usage
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<textarea data-tulih-editor>Initial content here</textarea>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The editor auto-attaches to every `<textarea data-tulih-editor>` on `DOMContentLoaded`.
|
|
50
|
+
|
|
51
|
+
### Manual initialization
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const editor = TulihEditor.attach('#my-textarea', {
|
|
55
|
+
height: 500,
|
|
56
|
+
features: {
|
|
57
|
+
table: false,
|
|
58
|
+
source: false,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
editor.getContent(); // get HTML
|
|
63
|
+
editor.setContent('<p>New content</p>');
|
|
64
|
+
editor.getText(); // get plain text
|
|
65
|
+
editor.getWordCount(); // number of words
|
|
66
|
+
editor.getCharCount(); // number of characters
|
|
67
|
+
editor.setReadOnly(true);
|
|
68
|
+
editor.isReadOnly();
|
|
69
|
+
editor.destroy(); // remove editor, restore the textarea
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
| Option | Type | Default | Description |
|
|
75
|
+
|--------|------|---------|-------------|
|
|
76
|
+
| `features` | `object` | all `true` | Enable/disable individual features (plugins) |
|
|
77
|
+
| `minimal` | `boolean` | `false` | Hide the classic toolbar; floating toolbar only |
|
|
78
|
+
| `toolbar` | `string[][]` | auto | Custom toolbar layout (groups of plugin names) |
|
|
79
|
+
| `height` | `number \| string` | `300px` | Editor body height (a number = pixels) |
|
|
80
|
+
| `sanitize` | `object` | — | Custom HTML sanitization rules |
|
|
81
|
+
| `allowedUrlSchemes` | `string[]` | `http, https, mailto, tel` | Allowed URL schemes for links |
|
|
82
|
+
| `allowedImageSchemes` | `string[]` | `http, https, data` | Allowed URL schemes for images |
|
|
83
|
+
| `iframeAllowlist` | `string[]` | youtube.com, vimeo.com, … | Allowed iframe domains |
|
|
84
|
+
| `iframeSandbox` | `string` | `allow-scripts allow-same-origin allow-presentation` | `sandbox` attr for embeds |
|
|
85
|
+
| `iframeAllow` | `string` | `accelerometer; autoplay; …` | `allow` attr for embeds |
|
|
86
|
+
| `readOnly` | `boolean` | `false` | Start in read-only mode |
|
|
87
|
+
|
|
88
|
+
### Custom toolbar layout
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
TulihEditor.attach('textarea', {
|
|
92
|
+
toolbar: [
|
|
93
|
+
['inline', 'inlineCode', 'subSuper', 'removeFormat'],
|
|
94
|
+
['block', 'align', 'colors', 'tableBg'],
|
|
95
|
+
['link', 'image', 'iframe', 'table', 'hr', 'emoji'],
|
|
96
|
+
['pastePlain', 'findReplace', 'indent', 'direction', 'unlink'],
|
|
97
|
+
['source', 'history', 'fullscreen', 'readOnly'],
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each inner array is one toolbar group (separated by a divider). Plugin names not listed
|
|
103
|
+
are hidden.
|
|
104
|
+
|
|
105
|
+
### Image upload handler
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
TulihEditor.onUploadImage = (file, callback) => {
|
|
109
|
+
const formData = new FormData();
|
|
110
|
+
formData.append('image', file);
|
|
111
|
+
fetch('/upload', { method: 'POST', body: formData })
|
|
112
|
+
.then((r) => r.json())
|
|
113
|
+
.then((data) => callback(data.url));
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Used for both **drag & drop** and **paste image** from the clipboard. If no handler is set,
|
|
118
|
+
images are inlined as `data:` URLs.
|
|
119
|
+
|
|
120
|
+
### Image browse handler
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
TulihEditor.setBrowseImage((context, callback) => {
|
|
124
|
+
FileManager.open((url) => callback(url));
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Feature toggles
|
|
129
|
+
|
|
130
|
+
Each plugin can be disabled via `features`. In **normal mode** all features are enabled by
|
|
131
|
+
default. Set `minimal: true` to hide the classic toolbar — in minimal mode only the
|
|
132
|
+
floating toolbar is active and other features are off by default:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
TulihEditor.attach('textarea', { minimal: true });
|
|
136
|
+
// opt specific features back in:
|
|
137
|
+
TulihEditor.attach('textarea', { minimal: true, features: { statusBar: true } });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
TulihEditor.attach('textarea', {
|
|
142
|
+
features: {
|
|
143
|
+
block: false, // hide heading/block select
|
|
144
|
+
link: false, // hide link button
|
|
145
|
+
emoji: false, // hide emoji picker
|
|
146
|
+
darkMode: false, // hide dark-mode toggle
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## All plugins
|
|
152
|
+
|
|
153
|
+
| Plugin | Order | Toolbar | Description |
|
|
154
|
+
|--------|-------|---------|-------------|
|
|
155
|
+
| `autoLinkify` | 1 | — | Auto-convert typed/pasted URLs into clickable links |
|
|
156
|
+
| `mediaEmbed` | 2 | — | Auto-convert YouTube/Vimeo URL to embed |
|
|
157
|
+
| `dragDrop` | 3 | — | Drag & drop image files into the editor |
|
|
158
|
+
| `linkTooltip` | 4 | — | Hover tooltip showing link URL |
|
|
159
|
+
| `keyboardShortcuts` | 5 | — | Customizable Ctrl+B/I/U/Z/Y/S/K shortcuts |
|
|
160
|
+
| `markdown` | 5 | — | Markdown shortcuts (`# `, `- `, `> `, `1. `) |
|
|
161
|
+
| `emojiAutocomplete` | 6 | — | Type `:` then letters for emoji suggestions |
|
|
162
|
+
| `shortcutCustomizer` | 6 | Button | Modal UI to customize keyboard shortcuts |
|
|
163
|
+
| `block` | 10 | Select | Block format: Paragraph, H1–H6, Quote, Code |
|
|
164
|
+
| `inline` | 20 | Buttons | Bold, Italic, Underline, Strikethrough, quote |
|
|
165
|
+
| `pre` | 21 | Button | Insert preformatted text |
|
|
166
|
+
| `inlineCode` | 22 | Button | Wrap selection in `<code>` |
|
|
167
|
+
| `codeBlock` | 23 | Button | Insert code block (16 languages) |
|
|
168
|
+
| `pastePlain` | 24 | Button | Toggle paste-as-plain-text |
|
|
169
|
+
| `indent` | 25 | Buttons | Indent / Outdent |
|
|
170
|
+
| `list` | 25 | Selects | Ordered/unordered list styles |
|
|
171
|
+
| `subSuper` | 26 | Buttons | Subscript / Superscript |
|
|
172
|
+
| `lineHeight` | 26 | Select | Line height 1–3 |
|
|
173
|
+
| `fontSize` | 27 | Select | Font size 8px–72px |
|
|
174
|
+
| `fontFamily` | 28 | Select | Font family (11 fonts) |
|
|
175
|
+
| `specialChars` | 29 | Button | Special characters popup (~280 symbols) |
|
|
176
|
+
| `colors` | 30 | Color inputs | Text color / Background color |
|
|
177
|
+
| `findReplace` | 31 | Button | Find & replace popup |
|
|
178
|
+
| `caseTransform` | 31 | Button | UPPER / lower / Title / Sentence case |
|
|
179
|
+
| `align` | 40 | Buttons | Left, Center, Right, Justify |
|
|
180
|
+
| `link` | 50 | Button + Modal | Link insertion; inline edit on click |
|
|
181
|
+
| `image` | 51 | Button + Modal | Image insertion with URL or browse |
|
|
182
|
+
| `imageProps` | 52 | Modal (dblclick) | Image properties: size, alt, class, CSS |
|
|
183
|
+
| `iframe` | 53 | Button + Modal | Iframe embed with sandbox/allow |
|
|
184
|
+
| `table` | 54 | Button + Grid | Table insertion (10×10 grid) + properties |
|
|
185
|
+
| `hr` | 55 | Button | Horizontal rule with style popup |
|
|
186
|
+
| `tableTools` | 55 | Floating bar | Rows/cols, merge, header, caption, cell bg, valign |
|
|
187
|
+
| `tableBg` | 56 | Color input | Table cell background color |
|
|
188
|
+
| `imageTools` | 56 | Floating bar | Resize handles, border, rounded corners |
|
|
189
|
+
| `unlink` | 58 | Button | Remove link from selection |
|
|
190
|
+
| `floatingToolbar` | 58 | — | Mini toolbar near a text selection |
|
|
191
|
+
| `darkMode` | 59 | Button | Toggle dark mode |
|
|
192
|
+
| `emoji` | 60 | Button + Popup | Emoji picker grid with search |
|
|
193
|
+
| `direction` | 61 | Buttons | LTR / RTL text direction |
|
|
194
|
+
| `source` | 65 | Button | Toggle source (HTML) mode |
|
|
195
|
+
| `history` | 70 | Buttons | Undo / Redo |
|
|
196
|
+
| `readOnly` | 75 | Button | Toggle read-only mode |
|
|
197
|
+
| `fullscreen` | 75 | Button | Toggle fullscreen |
|
|
198
|
+
| `shortcutsHelp` | 90 | Button | Keyboard shortcuts reference |
|
|
199
|
+
| `statusBar` | 100 | Status bar | Live tag indicator, word/char count, autosave badge |
|
|
200
|
+
| `autosave` | 210 | — | Auto-save to localStorage with Saved/Unsaved badge |
|
|
201
|
+
| `toolbarCollapse` | 1000 | Button | Collapse/expand the toolbar |
|
|
202
|
+
|
|
203
|
+
## Keyboard shortcuts
|
|
204
|
+
|
|
205
|
+
| Shortcut | Action |
|
|
206
|
+
|----------|--------|
|
|
207
|
+
| Ctrl+B | Bold |
|
|
208
|
+
| Ctrl+I | Italic |
|
|
209
|
+
| Ctrl+U | Underline |
|
|
210
|
+
| Ctrl+S | Strikethrough |
|
|
211
|
+
| Ctrl+Z | Undo |
|
|
212
|
+
| Ctrl+Shift+Z / Ctrl+Y | Redo |
|
|
213
|
+
| Ctrl+K | Open link dialog |
|
|
214
|
+
|
|
215
|
+
Shortcuts can be rebound via the **shortcut customizer** button, or programmatically:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
localStorage.setItem('te-shortcut-bold', JSON.stringify({ key: 'b', ctrl: true, shift: false }));
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Each key follows the format `te-shortcut-{action}`.
|
|
222
|
+
|
|
223
|
+
## Plugin system
|
|
224
|
+
|
|
225
|
+
Every feature is a plugin implementing the `Plugin` interface:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import type { Plugin, PluginContext } from 'tulih-editor';
|
|
229
|
+
|
|
230
|
+
const myPlugin: Plugin = {
|
|
231
|
+
name: 'myPlugin', // unique name (also the feature flag key)
|
|
232
|
+
order: 45, // toolbar position (divider per 10-range)
|
|
233
|
+
deps: ['link'], // optional dependencies
|
|
234
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light" data-cmd="myCmd">My</button>',
|
|
235
|
+
modalHTML: '<div class="te-modal">…</div>', // optional
|
|
236
|
+
css: '.te-my { color: red; }', // optional, injected once
|
|
237
|
+
init(ctx: PluginContext) {
|
|
238
|
+
const btn = ctx.wrapper.querySelector('[data-cmd="myCmd"]');
|
|
239
|
+
btn?.addEventListener('click', () => { /* … */ });
|
|
240
|
+
},
|
|
241
|
+
destroy(ctx: PluginContext) { /* cleanup */ },
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export default myPlugin;
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### The plugin context (`ctx`)
|
|
248
|
+
|
|
249
|
+
| Property | Description |
|
|
250
|
+
|----------|-------------|
|
|
251
|
+
| `wrapper` | Wrapper element |
|
|
252
|
+
| `editor` | Contenteditable body (`.te-editor`) |
|
|
253
|
+
| `toolbar` | Toolbar element |
|
|
254
|
+
| `textarea` | Original textarea |
|
|
255
|
+
| `features` | Resolved feature flags |
|
|
256
|
+
| `options` | Resolved options |
|
|
257
|
+
| `utils` | `sanitizeHTML`, `isSafeUrl`, `isAllowedIframeUrl` |
|
|
258
|
+
| `TulihEditor` | The global TulihEditor object |
|
|
259
|
+
| `saveSel()` / `restoreSel()` | Save / restore the selection range |
|
|
260
|
+
| `updateActiveStates()` | Refresh toolbar active states |
|
|
261
|
+
|
|
262
|
+
### Registering a custom plugin
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import TulihEditor from 'tulih-editor';
|
|
266
|
+
import myPlugin from './myPlugin';
|
|
267
|
+
|
|
268
|
+
TulihEditor.pluginManager.register('myPlugin', myPlugin);
|
|
269
|
+
|
|
270
|
+
TulihEditor.attach('textarea', { features: { myPlugin: true } });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Project structure
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
tulih-editor/
|
|
277
|
+
├── src/
|
|
278
|
+
│ ├── index.ts # entry point + public API
|
|
279
|
+
│ ├── types.ts # public TypeScript types
|
|
280
|
+
│ ├── styles/
|
|
281
|
+
│ │ ├── base.css # framework-free primitives (buttons/forms/grid)
|
|
282
|
+
│ │ ├── editor.css # editor theme + layout
|
|
283
|
+
│ │ └── index.css # stylesheet entry (imported by index.ts)
|
|
284
|
+
│ ├── core/
|
|
285
|
+
│ │ ├── Editor.ts # editor instance (class)
|
|
286
|
+
│ │ ├── PluginManager.ts # plugin registry & lifecycle
|
|
287
|
+
│ │ └── helpers.ts # shared DOM utilities
|
|
288
|
+
│ └── plugins/
|
|
289
|
+
│ ├── index.ts # registers the 47 default plugins
|
|
290
|
+
│ └── *.ts # one file per plugin
|
|
291
|
+
├── test/ # jsdom end-to-end tests
|
|
292
|
+
└── dist/ # build output (gitignored)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Development
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm install
|
|
299
|
+
npm run dev # Vite dev server (see examples/)
|
|
300
|
+
npm run typecheck # tsc --noEmit
|
|
301
|
+
npm run build # ESM + UMD bundles, CSS, and .d.ts declarations
|
|
302
|
+
npm test # jsdom end-to-end tests (run after build)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Build output (`dist/`):
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
tulih-editor.es.js # ESM bundle
|
|
309
|
+
tulih-editor.umd.js # UMD bundle (global: TulihEditor)
|
|
310
|
+
tulih-editor.css # stylesheet (base + theme)
|
|
311
|
+
types/ # .d.ts declarations
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Browser support
|
|
315
|
+
|
|
316
|
+
Modern evergreen browsers (Chrome, Firefox, Safari, Edge).
|
|
317
|
+
|
|
318
|
+
## Credits
|
|
319
|
+
|
|
320
|
+
- Toolbar icons: [Tabler Icons](https://tabler.io/icons) by Paweł Kuna — licensed under
|
|
321
|
+
[MIT](https://github.com/tabler/tabler-icons/blob/main/LICENSE). The webfont is referenced
|
|
322
|
+
from a CDN at runtime and is **not** bundled or redistributed by this package. If you
|
|
323
|
+
choose to self-host/bundle the Tabler webfont, include its MIT license notice alongside it.
|
|
324
|
+
|
|
325
|
+
## Contributing
|
|
326
|
+
|
|
327
|
+
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
[MIT](LICENSE) © Hendra Randy Nomura
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;font-family:inherit;font-size:1rem;font-weight:400;line-height:1.5;text-align:center;text-decoration:none;vertical-align:middle;white-space:nowrap;cursor:pointer;-webkit-user-select:none;user-select:none;padding:.375rem .75rem;border:1px solid transparent;border-radius:.375rem;background-color:transparent;color:#212529;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn:focus-visible{outline:2px solid rgba(26,26,26,.35);outline-offset:1px}:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn:disabled,:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn[disabled]{opacity:.5;pointer-events:none}:where(.te-container,.te-float-toolbar,.te-float-link-input,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}:where(.te-container,.te-float-toolbar,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn-light{color:#3a3a3a;background-color:#f8f9fa;border-color:transparent}:where(.te-container,.te-float-toolbar,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-emoji-popup,.te-sc-popup,.te-codeblock-popup) .btn-light:hover{background-color:#f2ede2}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-hr-popup,.te-fr-popup,.te-codeblock-popup,.te-table-tools) .btn-primary{color:#fff;background-color:#1a1a1a;border-color:#1a1a1a}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-hr-popup,.te-fr-popup,.te-codeblock-popup,.te-table-tools) .btn-primary:hover{background-color:#333;border-color:#333}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-hr-popup,.te-fr-popup,.te-codeblock-popup,.te-table-tools) .btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-hr-popup,.te-fr-popup,.te-codeblock-popup,.te-table-tools) .btn-secondary:hover{background-color:#5c636a;border-color:#565e64}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-secondary{color:#3a3a3a;border-color:#dee2e6}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-secondary:hover{background-color:#f2ede2;border-color:#ccc}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-danger{color:#c1272d;border-color:#c1272d}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-danger:hover{background-color:#c1272d;color:#fff}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-primary{color:#e8a317;border-color:#e8a317}:where(.te-container,.te-link-tools,.te-link-edit-popup,.te-image-tools,.te-table-tools,.te-hr-popup,.te-fr-popup,.te-codeblock-popup) .btn-outline-primary:hover{background-color:#e8a317;color:#fff}:where(.te-container) .btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em;border:0;border-radius:.375rem;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;cursor:pointer;opacity:.5}:where(.te-container) .btn-close:hover{opacity:.8}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup,.te-float-link-input,.te-link-edit-popup) .form-control,:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup,.te-float-link-input,.te-link-edit-popup) .form-select{display:block;width:100%;padding:.375rem .75rem;font-family:inherit;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;border:1px solid #ced4da;border-radius:.375rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup,.te-float-link-input,.te-link-edit-popup) .form-control:focus,:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup,.te-float-link-input,.te-link-edit-popup) .form-select:focus{outline:0;border-color:#86b7fe;box-shadow:0 0 0 .2rem #0d6efd33}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup) .form-select{padding-right:2.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup) .form-control-sm,:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup) .form-select-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}:where(.te-container,.te-fr-popup,.te-hr-popup,.te-codeblock-popup) .form-select-sm{padding-right:2rem}:where(.te-container,.te-hr-popup) .form-control-color{width:3rem;height:calc(1.5em + .75rem + 2px);padding:.25rem}:where(.te-container) .form-label{display:inline-block;margin-bottom:.5rem;font-size:.9rem;font-weight:500;color:#212529}:where(.te-container) .form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}:where(.te-container) .form-check-input{width:1em;height:1em;margin-top:.25em;margin-left:-1.5em;vertical-align:top;background-color:#fff;border:1px solid rgba(0,0,0,.25);border-radius:.25em;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer}:where(.te-container) .form-check-input:checked{background-color:#1a1a1a;border-color:#1a1a1a;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center;background-size:contain}:where(.te-container) .form-check-label{cursor:pointer}:where(.te-container) .row{display:flex;flex-wrap:wrap;margin-right:-.5rem;margin-left:-.5rem}:where(.te-container) .row>.col,:where(.te-container) .row>[class^=col-],:where(.te-container) .row>[class*=" col-"]{flex:1 0 0%;padding-right:.5rem;padding-left:.5rem}:where(.te-container) .col-6{flex:0 0 auto;width:50%}:where(.te-container) .col-4{flex:0 0 auto;width:33.3333%}:where(.te-container) .col-3{flex:0 0 auto;width:25%}:where(.te-container) .g-2{gap:.5rem 0}:where(.te-container) .mb-0{margin-bottom:0!important}:where(.te-container) .mb-2{margin-bottom:.5rem!important}:where(.te-container) .mb-3{margin-bottom:1rem!important}:where(.te-container) .mt-2{margin-top:.5rem!important}:where(.te-container) .mt-3{margin-top:1rem!important}:where(.te-container) .small{font-size:.875em}:where(.te-container) .w-100{width:100%!important}:where(.te-container) .d-block{display:block!important}:where(.te-container) .d-flex{display:flex!important}:where(.te-container) .text-muted{color:#6c757d!important}.te-container{max-width:900px;position:relative}.te-toolbar{display:flex;flex-wrap:wrap;gap:6px;border:1px solid #dee2e6;background:#fbf9f4;padding:8px;border-bottom:none;border-radius:.375rem .375rem 0 0}.te-toolbar .btn,.te-toolbar .btn-light{color:#3a3a3a;background:transparent;border-color:transparent}.te-toolbar .btn:hover,.te-toolbar .btn-light:hover{background:#f2ede2}.te-toolbar .btn.active,.te-toolbar .btn-light.active{background:#fbead6;border-color:#fbead6}.te-toolbar .btn-outline-secondary{color:#3a3a3a;border-color:#dee2e6}.te-toolbar select{touch-action:manipulation}.te-dropdown-icon{display:inline-flex;align-items:center;vertical-align:middle;margin-right:2px}.te-dropdown-icon svg{width:18px;height:18px;stroke-width:2}.te-dropdown-icon-text{font-size:13px;font-weight:600;line-height:1;color:#3a3a3a}.te-toolbar .btn-primary,.te-toolbar .btn.btn-primary{background:#1a1a1a;border-color:#1a1a1a;color:#fff}.te-toolbar .btn-primary:hover,.te-toolbar .btn.btn-primary:hover{background:#333;border-color:#333}.te-toolbar .btn-outline-danger,.te-toolbar .btn.btn-outline-danger{color:#c1272d;border-color:#c1272d}.te-toolbar .btn-outline-danger:hover,.te-toolbar .btn.btn-outline-danger:hover{background:#c1272d;color:#fff}.te-toolbar .btn-outline-primary,.te-toolbar .btn.btn-outline-primary{color:#e8a317;border-color:#e8a317}.te-toolbar .btn-outline-primary:hover,.te-toolbar .btn.btn-outline-primary:hover{background:#e8a317;color:#fff}.te-divider{width:1px;background:#dee2e6;align-self:stretch;margin:0 4px}.te-editor{height:300px;overflow:auto;border:1px solid #dee2e6;border-radius:0;padding:12px;background:#fff}.te-editor:focus{outline:none;box-shadow:inset 0 0 0 2px #0d6efd40}.te-toolbar .btn .ti{pointer-events:none}.te-editor[placeholder]:empty:before{content:attr(placeholder);color:#6c757d}.te-toolbar select.form-select{width:auto;padding-right:2rem}.te-status{color:#6c757d;font-size:.875rem}@keyframes te-modal-in{0%{opacity:0;transform:scale(.95) translateY(-10px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes te-backdrop-in{0%{opacity:0}to{opacity:1}}.te-modal{position:absolute;top:0;right:0;bottom:0;left:0;display:none;align-items:center;justify-content:center;z-index:2500;overflow-y:auto;padding:1rem}.te-modal.is-open{display:flex}.te-modal.is-open .te-modal-dialog{animation:te-modal-in .2s ease-out}.te-modal.is-open .te-modal-backdrop{animation:te-backdrop-in .2s ease-out}.te-modal-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;background:#0006}.te-modal-dialog{position:relative;background:#fff;border-radius:.75rem;box-shadow:0 20px 60px #0000002e;width:540px;max-width:100%;overflow:hidden;z-index:1}.te-modal-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;background:#fbf9f4;border-bottom:1px solid #eee}.te-modal-header .te-modal-title{font-size:1.05rem;font-weight:600;color:#1a1a1a}.te-modal-header .btn-close{font-size:.85rem;opacity:.5;transition:opacity .15s}.te-modal-header .btn-close:hover{opacity:.8}.te-modal-body{padding:1.25rem;color:#212529;max-height:60vh;overflow-y:auto}.te-modal-footer{display:flex;gap:.5rem;justify-content:flex-end;padding:1rem 1.25rem;background:#fbf9f4;border-top:1px solid #eee}.te-modal .btn-primary{background:#1a1a1a;border-color:#1a1a1a;color:#fff}.te-modal .btn-primary:hover{background:#333;border-color:#333}.te-modal .btn-outline-secondary{color:#3a3a3a;border-color:#dee2e6}.te-modal .btn-outline-secondary:hover{background:#f2ede2;border-color:#ccc}.te-modal .btn-outline-danger{color:#c1272d;border-color:#c1272d}.te-modal .btn-outline-danger:hover{background:#c1272d;color:#fff}.te-modal .btn-outline-primary{color:#e8a317;border-color:#e8a317}.te-modal .btn-outline-primary:hover{background:#e8a317;color:#fff}.te-editor a{text-decoration:none}.te-editor .te-table{width:100%;border-collapse:collapse}.te-editor .te-table td,.te-editor .te-table th{border:1px solid #dee2e6;padding:6px}@media(max-width:576px){.te-toolbar .btn{padding:.35rem .5rem;font-size:.8rem}.te-toolbar select.form-select{font-size:.8rem;padding:.2rem 1.5rem .2rem .5rem}.te-editor{padding:8px;font-size:.95rem}.te-container .te-toolbar{gap:3px;padding:4px}.te-divider{margin:0 2px}.te-modal-dialog{border-radius:.5rem}.te-modal-header,.te-modal-body,.te-modal-footer{padding:.75rem 1rem}.te-modal-body{max-height:50vh}}.te-minimal .te-toolbar{display:none}
|