open-wysiwyg-editor 0.1.0 → 1.0.1
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 +750 -58
- package/dist/chunk-4SVBPZWJ.js +1834 -0
- package/dist/chunk-4SVBPZWJ.js.map +1 -0
- package/dist/chunk-77S4XJMD.js +277 -0
- package/dist/chunk-77S4XJMD.js.map +1 -0
- package/dist/chunk-HNGUZYL2.cjs +3184 -0
- package/dist/chunk-HNGUZYL2.cjs.map +1 -0
- package/dist/chunk-KO7YL3DO.cjs +1850 -0
- package/dist/chunk-KO7YL3DO.cjs.map +1 -0
- package/dist/{chunk-CDPXFV6R.js → chunk-N662ZMGU.js} +51 -315
- package/dist/chunk-N662ZMGU.js.map +1 -0
- package/dist/chunk-YA2CQM23.cjs +284 -0
- package/dist/chunk-YA2CQM23.cjs.map +1 -0
- package/dist/content.css +146 -67
- package/dist/content.min.css +1 -1
- package/dist/editor-8zSxaBqs.d.cts +516 -0
- package/dist/editor-8zSxaBqs.d.ts +516 -0
- package/dist/editor-Cx1TcWt7.d.ts +175 -0
- package/dist/editor-QMoMMBL6.d.cts +175 -0
- package/dist/element.cjs +153 -0
- package/dist/element.cjs.map +1 -0
- package/dist/element.d.cts +41 -0
- package/dist/element.d.ts +41 -0
- package/dist/element.js +150 -0
- package/dist/element.js.map +1 -0
- package/dist/headless.cjs +338 -3428
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +8 -515
- package/dist/headless.d.ts +8 -515
- package/dist/headless.js +2 -1
- package/dist/index.cjs +362 -5111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -151
- package/dist/index.d.ts +19 -151
- package/dist/index.js +3 -1699
- package/dist/index.js.map +1 -1
- package/dist/open-wysiwyg-editor.global.js +16 -16
- package/dist/style.css +312 -147
- package/dist/style.min.css +1 -1
- package/package.json +34 -9
- package/dist/chunk-CDPXFV6R.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,57 +1,126 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center"><a href="https://alhassan73.github.io/open-wysiwyg-editor/"><img src="https://raw.githubusercontent.com/alhassan73/open-wysiwyg-editor/main/.github/assets/banner.png" alt="Open WYSIWYG Editor — accessible, RTL-first rich text editor for every framework" width="100%"></a></p>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Open WYSIWYG Editor
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
- **Complete out of the box.** You get headings, lists, task lists, tables, images, links, code, text and highlight colors, alignment, find & replace, an HTML source view, word count and more.
|
|
9
|
-
- **Headless if you want it.** The same engine is available with no UI, so you can build your own interface on top.
|
|
10
|
-
- **MIT license.** There is no license key, no telemetry and no cloud dependency.
|
|
5
|
+
[](https://www.npmjs.com/package/open-wysiwyg-editor)
|
|
6
|
+
[](https://bundlephobia.com/package/open-wysiwyg-editor)
|
|
7
|
+
[](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/LICENSE)
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
**[Live demo →](https://alhassan73.github.io/open-wysiwyg-editor/)** · [GitHub](https://github.com/alhassan73/open-wysiwyg-editor) · [npm](https://www.npmjs.com/package/open-wysiwyg-editor)
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
An accessible rich text editor that works on **any website**: plain HTML/JS, React, Next.js, Preact, Vue, Nuxt, Angular, Svelte, Solid, Astro, WordPress and more. It's RTL-first, it runs under a strict Content Security Policy, and it ships its own TypeScript types. MIT licensed, with no license key, no telemetry and no cloud service.
|
|
12
|
+
|
|
13
|
+
Built on [ProseMirror](https://prosemirror.net). Content is stored as HTML or JSON.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
| Area | What you get |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| **Formatting** | Headings · Bold, italic, underline, strike, inline code · Subscript / superscript · Text and highlight colors · Alignment · Text direction per block |
|
|
20
|
+
| **Blocks** | Bullet, numbered and task lists · Blockquote · Code block · Horizontal rule · Tables with caption and header rows · Images with alt text and caption · Links |
|
|
21
|
+
| **Tools** | Find & replace · HTML source view · Word and character count · Element path · Keyboard shortcut help · Undo / redo · Markdown-style typing shortcuts |
|
|
22
|
+
| **Accessibility** | WCAG 2.2 AA, ATAG 2.0 and WAI-ARIA patterns · Fully keyboard operable, no keyboard trap · Screen reader announcements · Windows High Contrast · Dark theme |
|
|
23
|
+
| **Security** | DOMPurify on every input · URL allow-list · Trusted Types · No `unsafe-inline` needed |
|
|
24
|
+
| **Languages** | English and Arabic built in (add your own) · Mirrored UI in RTL · `dir="auto"` on every block |
|
|
25
|
+
| **Everywhere** | `<owe-editor>` web component for plain HTML and forms · Packages for React, Next.js, Preact, Vue, Nuxt, Angular, Svelte, Solid and Astro |
|
|
26
|
+
| **Your way** | Full UI by default · Headless mode for your own UI · Custom toolbar buttons · Extensions API · Brand it with one color or CSS variables |
|
|
27
|
+
|
|
28
|
+
Every release is tested end to end in Chromium, Firefox and WebKit, under a strict CSP with Trusted Types and with axe-core accessibility checks.
|
|
15
29
|
|
|
16
30
|
## Install
|
|
17
31
|
|
|
18
|
-
```
|
|
32
|
+
```bash
|
|
19
33
|
npm install open-wysiwyg-editor
|
|
20
34
|
# or
|
|
21
35
|
pnpm add open-wysiwyg-editor
|
|
36
|
+
# or
|
|
37
|
+
yarn add open-wysiwyg-editor
|
|
22
38
|
```
|
|
23
39
|
|
|
24
|
-
|
|
40
|
+
Or with no build step, from a CDN:
|
|
25
41
|
|
|
26
42
|
```html
|
|
27
|
-
<link rel="stylesheet" href="https://
|
|
28
|
-
<script src="https://
|
|
43
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/open-wysiwyg-editor@1/dist/style.min.css" />
|
|
44
|
+
<script src="https://cdn.jsdelivr.net/npm/open-wysiwyg-editor@1/dist/open-wysiwyg-editor.global.js"></script>
|
|
29
45
|
```
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
### Pick your framework
|
|
48
|
+
|
|
49
|
+
The core package works everywhere. Each framework also has its own small package. It re-exports the whole core API, ships the stylesheets, and handles mounting, cleanup, server-side rendering and two-way binding for you. Install **one** of them (it brings the core with it):
|
|
50
|
+
|
|
51
|
+
| Framework | Package | Install |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| Plain JS, HTML forms, CMSs, Lit, Alpine, htmx, Ember, Qwik | [`open-wysiwyg-editor`](https://www.npmjs.com/package/open-wysiwyg-editor) | `npm install open-wysiwyg-editor` |
|
|
54
|
+
| React, Remix, Gatsby, Vite | [`@open-wysiwyg-editor/react`](https://www.npmjs.com/package/@open-wysiwyg-editor/react) | `npm install @open-wysiwyg-editor/react` |
|
|
55
|
+
| Next.js | [`@open-wysiwyg-editor/next`](https://www.npmjs.com/package/@open-wysiwyg-editor/next) | `npm install @open-wysiwyg-editor/next` |
|
|
56
|
+
| Preact | [`@open-wysiwyg-editor/preact`](https://www.npmjs.com/package/@open-wysiwyg-editor/preact) | `npm install @open-wysiwyg-editor/preact` |
|
|
57
|
+
| Vue 3 | [`@open-wysiwyg-editor/vue`](https://www.npmjs.com/package/@open-wysiwyg-editor/vue) | `npm install @open-wysiwyg-editor/vue` |
|
|
58
|
+
| Nuxt | [`@open-wysiwyg-editor/nuxt`](https://www.npmjs.com/package/@open-wysiwyg-editor/nuxt) | `npm install @open-wysiwyg-editor/nuxt` |
|
|
59
|
+
| Angular | [`@open-wysiwyg-editor/angular`](https://www.npmjs.com/package/@open-wysiwyg-editor/angular) | `npm install @open-wysiwyg-editor/angular` |
|
|
60
|
+
| Svelte, SvelteKit | [`@open-wysiwyg-editor/svelte`](https://www.npmjs.com/package/@open-wysiwyg-editor/svelte) | `npm install @open-wysiwyg-editor/svelte` |
|
|
61
|
+
| Solid, SolidStart | [`@open-wysiwyg-editor/solid`](https://www.npmjs.com/package/@open-wysiwyg-editor/solid) | `npm install @open-wysiwyg-editor/solid` |
|
|
62
|
+
| Astro | [`@open-wysiwyg-editor/astro`](https://www.npmjs.com/package/@open-wysiwyg-editor/astro) | `npm install @open-wysiwyg-editor/astro` |
|
|
63
|
+
|
|
64
|
+
All packages share one version number and are released together.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
The core is one function, `createEditor(options)`. It mounts the editor inside the element you pass and returns an `editor` instance. The framework packages do the three steps below for you. If you use the core directly, follow them in every framework:
|
|
69
|
+
|
|
70
|
+
1. Load the stylesheet once: `import "open-wysiwyg-editor/style.css"` (or the `<link>` above).
|
|
71
|
+
2. Call `createEditor()` **in the browser**, after the element exists. It throws during server-side rendering.
|
|
72
|
+
3. Call `editor.destroy()` when the element goes away.
|
|
73
|
+
|
|
74
|
+
### Plain HTML / JavaScript (no framework, no bundler)
|
|
75
|
+
|
|
76
|
+
The CDN script defines the `<owe-editor>` tag, so one tag is enough:
|
|
32
77
|
|
|
33
78
|
```html
|
|
34
|
-
<
|
|
79
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/open-wysiwyg-editor@1/dist/style.min.css" />
|
|
80
|
+
<script src="https://cdn.jsdelivr.net/npm/open-wysiwyg-editor@1/dist/open-wysiwyg-editor.global.js"></script>
|
|
81
|
+
|
|
82
|
+
<owe-editor id="editor" placeholder="Write something…">
|
|
83
|
+
<template><p>Hello <strong>world</strong></p></template>
|
|
84
|
+
</owe-editor>
|
|
85
|
+
|
|
86
|
+
<script>
|
|
87
|
+
document.querySelector("#editor").addEventListener("input", (event) => {
|
|
88
|
+
console.log(event.target.value); // '<p dir="auto">Hello <strong>world</strong></p>'
|
|
89
|
+
});
|
|
90
|
+
</script>
|
|
35
91
|
```
|
|
36
92
|
|
|
37
|
-
|
|
38
|
-
import { createEditor } from "open-wysiwyg-editor";
|
|
39
|
-
import "open-wysiwyg-editor/style.css";
|
|
93
|
+
Or call the function yourself:
|
|
40
94
|
|
|
41
|
-
|
|
95
|
+
```html
|
|
96
|
+
<div id="editor"><p>Hello <strong>world</strong></p></div>
|
|
42
97
|
|
|
43
|
-
|
|
98
|
+
<script>
|
|
99
|
+
const editor = OpenWysiwygEditor.createEditor({
|
|
100
|
+
element: "#editor", // a selector, or document.querySelector("#editor")
|
|
101
|
+
placeholder: "Write something…",
|
|
102
|
+
});
|
|
103
|
+
// editor.getHTML() → '<p dir="auto">Hello <strong>world</strong></p>'
|
|
104
|
+
</script>
|
|
44
105
|
```
|
|
45
106
|
|
|
46
|
-
|
|
107
|
+
This also works in WordPress, Shopify, Webflow, PHP, Django and Rails templates.
|
|
47
108
|
|
|
48
|
-
|
|
49
|
-
const editor = OpenWysiwygEditor.createEditor({ element: document.querySelector("#editor") });
|
|
50
|
-
```
|
|
109
|
+
### HTML forms
|
|
51
110
|
|
|
52
|
-
|
|
111
|
+
Use `<owe-editor name="…">`. It is a form-associated element, so the form submits the editor's HTML under that name, and `form.reset()` restores the starting content. A `<label for>` pointing at its `id` becomes the accessible name.
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
<form method="post">
|
|
115
|
+
<label for="body">Article</label>
|
|
116
|
+
<owe-editor id="body" name="body">
|
|
117
|
+
<template><p>Draft…</p></template>
|
|
118
|
+
</owe-editor>
|
|
119
|
+
<button>Save</button>
|
|
120
|
+
</form>
|
|
121
|
+
```
|
|
53
122
|
|
|
54
|
-
|
|
123
|
+
Or mount the editor on a `<textarea>`. The editor hides the textarea and keeps its value in sync, so the form submits the HTML. The textarea's `<label>` becomes the editor's accessible name.
|
|
55
124
|
|
|
56
125
|
```html
|
|
57
126
|
<form method="post">
|
|
@@ -59,40 +128,270 @@ If you mount the editor on a `<textarea>`, it hides the textarea and keeps its v
|
|
|
59
128
|
<textarea id="body" name="body"><p>Draft…</p></textarea>
|
|
60
129
|
<button>Save</button>
|
|
61
130
|
</form>
|
|
62
|
-
<script
|
|
63
|
-
|
|
64
|
-
createEditor({ element: document.querySelector("#body") });
|
|
131
|
+
<script>
|
|
132
|
+
OpenWysiwygEditor.createEditor({ element: "#body" });
|
|
65
133
|
</script>
|
|
66
134
|
```
|
|
67
135
|
|
|
68
|
-
###
|
|
136
|
+
### Any bundler (Vite, Webpack, Parcel…), JS or TS
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
import { createEditor } from "open-wysiwyg-editor";
|
|
140
|
+
import "open-wysiwyg-editor/style.css";
|
|
141
|
+
|
|
142
|
+
const editor = createEditor({
|
|
143
|
+
element: "#editor", // or an HTMLElement
|
|
144
|
+
onUpdate: (editor) => console.log(editor.getHTML()),
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
To use the `<owe-editor>` tag with a bundler, import its entry once: `import "open-wysiwyg-editor/element";`.
|
|
69
149
|
|
|
70
|
-
|
|
150
|
+
### React (Vite, CRA, Remix, Gatsby)
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
npm install @open-wysiwyg-editor/react
|
|
154
|
+
```
|
|
71
155
|
|
|
72
156
|
```tsx
|
|
73
|
-
|
|
74
|
-
import {
|
|
75
|
-
import
|
|
76
|
-
|
|
157
|
+
import { useState } from "react";
|
|
158
|
+
import { RichTextEditor } from "@open-wysiwyg-editor/react";
|
|
159
|
+
import "@open-wysiwyg-editor/react/style.css";
|
|
160
|
+
|
|
161
|
+
export function Article() {
|
|
162
|
+
const [html, setHtml] = useState("<p>Hello</p>");
|
|
163
|
+
return <RichTextEditor value={html} onChange={setHtml} placeholder="Write something…" />;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
For a custom UI, `useEditor({ ui: false })` returns `{ ref, editor }`. [Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/react#readme)
|
|
168
|
+
|
|
169
|
+
### Next.js
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm install @open-wysiwyg-editor/next
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
App Router: the package is already marked `"use client"`, so you can use it straight from a Server Component. Import the stylesheet once in the layout:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
// app/layout.tsx
|
|
179
|
+
import "@open-wysiwyg-editor/next/style.css";
|
|
180
|
+
|
|
181
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
182
|
+
return (
|
|
183
|
+
<html lang="en">
|
|
184
|
+
<body>{children}</body>
|
|
185
|
+
</html>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
// app/articles/new/page.tsx (a Server Component)
|
|
192
|
+
import { RichTextEditor } from "@open-wysiwyg-editor/next";
|
|
193
|
+
import { saveArticle } from "./actions";
|
|
194
|
+
|
|
195
|
+
export default function Page() {
|
|
196
|
+
return (
|
|
197
|
+
<form action={saveArticle}>
|
|
198
|
+
<RichTextEditor name="body" defaultValue="<p>Hello</p>" />
|
|
199
|
+
<button>Save</button>
|
|
200
|
+
</form>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
77
204
|
|
|
78
|
-
|
|
79
|
-
const host = useRef<HTMLDivElement>(null);
|
|
80
|
-
const editor = useRef<Editor | null>(null);
|
|
205
|
+
`name` keeps a hidden `<input>` in sync, so the Server Action receives the HTML in its `FormData`:
|
|
81
206
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
207
|
+
```ts
|
|
208
|
+
// app/articles/new/actions.ts
|
|
209
|
+
"use server";
|
|
210
|
+
|
|
211
|
+
export async function saveArticle(formData: FormData) {
|
|
212
|
+
const html = String(formData.get("body")); // sanitize again here before you store it
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Pages Router: use the same component in any page and import the stylesheet in `pages/_app.tsx`. [Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/next#readme)
|
|
217
|
+
|
|
218
|
+
### Preact
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npm install @open-wysiwyg-editor/preact
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
import { useState } from "preact/hooks";
|
|
226
|
+
import { RichTextEditor } from "@open-wysiwyg-editor/preact";
|
|
227
|
+
import "@open-wysiwyg-editor/preact/style.css";
|
|
228
|
+
|
|
229
|
+
export function Article() {
|
|
230
|
+
const [html, setHtml] = useState("<p>Hello</p>");
|
|
231
|
+
return <RichTextEditor value={html} onChange={setHtml} class="article-editor" />;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Same API as the React package, with `class` instead of `className`. [Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/preact#readme)
|
|
236
|
+
|
|
237
|
+
### Vue 3
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npm install @open-wysiwyg-editor/vue
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```vue
|
|
244
|
+
<script setup>
|
|
245
|
+
import { ref } from "vue";
|
|
246
|
+
import { RichTextEditor } from "@open-wysiwyg-editor/vue";
|
|
247
|
+
import "@open-wysiwyg-editor/vue/style.css";
|
|
248
|
+
|
|
249
|
+
const html = ref("<p>Hello</p>");
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
<template>
|
|
253
|
+
<RichTextEditor v-model="html" placeholder="Write something…" />
|
|
254
|
+
</template>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Pass more `createEditor` options with `:options="{ … }"`. A template ref exposes `editor`. [Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/vue#readme)
|
|
258
|
+
|
|
259
|
+
### Nuxt
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npm install @open-wysiwyg-editor/nuxt
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
// nuxt.config.ts
|
|
267
|
+
export default defineNuxtConfig({
|
|
268
|
+
modules: ["@open-wysiwyg-editor/nuxt"],
|
|
269
|
+
// optional: wysiwygEditor: { css: true, componentName: "RichTextEditor" },
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The module adds the stylesheet and auto-imports the component, so it works in any page without an import:
|
|
90
274
|
|
|
91
|
-
|
|
275
|
+
```vue
|
|
276
|
+
<script setup>
|
|
277
|
+
const html = ref("<p>Hello</p>");
|
|
278
|
+
</script>
|
|
279
|
+
|
|
280
|
+
<template>
|
|
281
|
+
<RichTextEditor v-model="html" />
|
|
282
|
+
</template>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
[Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/nuxt#readme)
|
|
286
|
+
|
|
287
|
+
### Angular
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
npm install @open-wysiwyg-editor/angular
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Add the stylesheet to `angular.json`, in `"styles": ["node_modules/@open-wysiwyg-editor/angular/style.css"]`. Then use the standalone component. It works with `ngModel` and reactive forms:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { Component } from "@angular/core";
|
|
297
|
+
import { FormsModule } from "@angular/forms";
|
|
298
|
+
import { RichTextEditorComponent } from "@open-wysiwyg-editor/angular";
|
|
299
|
+
|
|
300
|
+
@Component({
|
|
301
|
+
selector: "app-article",
|
|
302
|
+
imports: [FormsModule, RichTextEditorComponent],
|
|
303
|
+
template: `<owe-rich-text-editor [(ngModel)]="html" placeholder="Write something…" />`,
|
|
304
|
+
})
|
|
305
|
+
export class ArticleComponent {
|
|
306
|
+
html = "<p>Hello</p>";
|
|
92
307
|
}
|
|
93
308
|
```
|
|
94
309
|
|
|
95
|
-
|
|
310
|
+
It starts in the browser only (safe with Angular SSR) and runs outside the zone. [Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/angular#readme)
|
|
311
|
+
|
|
312
|
+
### Svelte / SvelteKit
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
npm install @open-wysiwyg-editor/svelte
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```svelte
|
|
319
|
+
<script>
|
|
320
|
+
import { richText } from "@open-wysiwyg-editor/svelte";
|
|
321
|
+
import "@open-wysiwyg-editor/svelte/style.css";
|
|
322
|
+
|
|
323
|
+
let html = "<p>Hello</p>";
|
|
324
|
+
</script>
|
|
325
|
+
|
|
326
|
+
<div use:richText={{ content: html, onUpdate: (e) => (html = e.getHTML()) }}></div>
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Works with Svelte 3, 4 and 5. Actions never run on the server, so this is safe in SvelteKit. [Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/svelte#readme)
|
|
330
|
+
|
|
331
|
+
### Solid / SolidStart
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
npm install @open-wysiwyg-editor/solid
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
import { createSignal } from "solid-js";
|
|
339
|
+
import { richText } from "@open-wysiwyg-editor/solid";
|
|
340
|
+
import "@open-wysiwyg-editor/solid/style.css";
|
|
341
|
+
|
|
342
|
+
export function Article() {
|
|
343
|
+
const [html, setHtml] = createSignal("<p>Hello</p>");
|
|
344
|
+
false && richText; // keeps the import from being tree-shaken, so `use:richText` works
|
|
345
|
+
return <div use:richText={{ content: html(), onUpdate: (e) => setHtml(e.getHTML()) }} />;
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
[Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/solid#readme)
|
|
350
|
+
|
|
351
|
+
### Astro
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
npm install @open-wysiwyg-editor/astro
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```astro
|
|
358
|
+
---
|
|
359
|
+
import { RichTextEditor } from "@open-wysiwyg-editor/astro";
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
<form method="post">
|
|
363
|
+
<RichTextEditor name="body" value="<p>Hello</p>" placeholder="Write something…" />
|
|
364
|
+
<button>Save</button>
|
|
365
|
+
</form>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The component renders `<owe-editor>` and loads the element and stylesheet in the browser, so it works with plain form posts and no UI framework. [Full guide →](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/astro#readme)
|
|
369
|
+
|
|
370
|
+
### Others (Lit, Alpine, htmx, Ember, Qwik, WordPress, PHP)
|
|
371
|
+
|
|
372
|
+
Anything that can write HTML can use the `<owe-editor>` tag from the core package: load the CDN script (or `import "open-wysiwyg-editor/element"`) and the stylesheet, then put the tag in your markup. It dispatches `input` and `change` events and takes part in forms, so htmx, Turbo and plain `<form>` posts work without glue code.
|
|
373
|
+
|
|
374
|
+
```html
|
|
375
|
+
<!-- Alpine -->
|
|
376
|
+
<owe-editor name="body" x-on:input="html = $event.target.value"></owe-editor>
|
|
377
|
+
|
|
378
|
+
<!-- htmx: the form field is sent like any other input -->
|
|
379
|
+
<form hx-post="/articles" hx-swap="outerHTML">
|
|
380
|
+
<owe-editor name="body" value="<p>Hello</p>"></owe-editor>
|
|
381
|
+
<button>Save</button>
|
|
382
|
+
</form>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
// Lit
|
|
387
|
+
import "open-wysiwyg-editor/element";
|
|
388
|
+
import "open-wysiwyg-editor/style.css";
|
|
389
|
+
// html`<owe-editor .value=${this.html} @input=${(e) => (this.html = e.target.value)}></owe-editor>`
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
In WordPress or any PHP template, add the CDN `<link>` and `<script>` in the page head or footer, and print the tag with an escaped `value` attribute (`esc_attr( $html )`).
|
|
393
|
+
|
|
394
|
+
### Headless (bring your own UI)
|
|
96
395
|
|
|
97
396
|
Use the headless entry when you want the engine and every extension without the built-in UI:
|
|
98
397
|
|
|
@@ -108,13 +407,67 @@ editor.subscribe(() => {
|
|
|
108
407
|
|
|
109
408
|
From the main entry, `createEditor({ ui: false })` does the same thing.
|
|
110
409
|
|
|
410
|
+
### Reading and saving content
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
editor.getHTML(); // clean, sanitized HTML: save this
|
|
414
|
+
editor.getJSON(); // or ProseMirror JSON
|
|
415
|
+
editor.setContent("<p>…</p>"); // load new content
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Still sanitize on the server. To display saved HTML on a page that has no editor, load `content.css` and wrap the HTML in `<div class="owe-content-root">` (see [Styling](#styling)).
|
|
419
|
+
|
|
111
420
|
---
|
|
112
421
|
|
|
422
|
+
## The `<owe-editor>` element
|
|
423
|
+
|
|
424
|
+
`<owe-editor>` is the full editor as an HTML tag. It is a form-associated custom element that renders in the light DOM, so `style.css` applies as usual (no Shadow DOM, CSP-safe). The CDN script defines it automatically. With a bundler, `import "open-wysiwyg-editor/element"` defines it.
|
|
425
|
+
|
|
426
|
+
| Attribute | Description |
|
|
427
|
+
| --- | --- |
|
|
428
|
+
| `name` | Form field name. The form submits the editor's HTML under it |
|
|
429
|
+
| `placeholder` | Placeholder text |
|
|
430
|
+
| `readonly` | Makes the content read-only |
|
|
431
|
+
| `disabled` | Disables the editor and leaves it out of the form |
|
|
432
|
+
| `dir` | Base direction of the content (`ltr`, `rtl`, `auto`) |
|
|
433
|
+
| `content-lang` | `lang` of the content (spellcheck and screen readers) |
|
|
434
|
+
| `label` | Accessible name. Or point a `<label for>` at the element's `id` |
|
|
435
|
+
| `theme` | `auto` (follows the system), `light` or `dark` |
|
|
436
|
+
| `brand` | Brand color (any CSS color): buttons, links, focus ring and selection follow it |
|
|
437
|
+
| `value` | Initial content as HTML (read once, when the editor is created) |
|
|
438
|
+
| `language` | UI language, e.g. `ar` (read once). Defaults to `<html lang>` |
|
|
439
|
+
| `toolbar` | Toolbar items separated by spaces (`"bold italic \| link"`), or `none` (read once) |
|
|
440
|
+
|
|
441
|
+
`placeholder`, `readonly`, `disabled`, `dir`, `content-lang`, `label`, `theme` and `brand` follow later changes. `value`, `language` and `toolbar` are only read when the editor starts.
|
|
442
|
+
|
|
443
|
+
| Property | Description |
|
|
444
|
+
| --- | --- |
|
|
445
|
+
| `editor` | The live `Editor`, or `null` while the element isn't in the document |
|
|
446
|
+
| `value` | The content as HTML. Setting it loads new content |
|
|
447
|
+
| `options` | More `createEditor()` options (`extensions`, `ui`, `labels`, callbacks…). Set it **before** the element is added to the page. Attributes win over it |
|
|
448
|
+
| `form` | The surrounding `<form>`, or `null` |
|
|
449
|
+
|
|
450
|
+
| Event | When |
|
|
451
|
+
| --- | --- |
|
|
452
|
+
| `input` | On every change. `event.target.value` has the HTML |
|
|
453
|
+
| `change` | When the editor loses focus after a change |
|
|
454
|
+
|
|
455
|
+
**Initial content** comes from the `value` attribute, else from a `<template>` child, else from the element's own children. Use `value` or `<template>` when the content comes from users: neither can run anything before the editor sanitizes it. Content placed directly as children is parsed by the browser first.
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
const el = document.querySelector("owe-editor");
|
|
459
|
+
el.options = { language: "ar", ui: { stickyToolbar: false } };
|
|
460
|
+
el.value = "<p>New content</p>";
|
|
461
|
+
el.editor?.commands.toggleBold();
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
In Angular, bind the element with `ngDefaultControl [(ngModel)]` (or use the [Angular package](https://github.com/alhassan73/open-wysiwyg-editor/tree/main/packages/angular#readme)).
|
|
465
|
+
|
|
113
466
|
## Options
|
|
114
467
|
|
|
115
468
|
```ts
|
|
116
469
|
createEditor({
|
|
117
|
-
element, // container or <textarea>; omit to create it detached and append editor.root yourself
|
|
470
|
+
element, // container, CSS selector or <textarea>; omit to create it detached and append editor.root yourself
|
|
118
471
|
content, // HTML string (always sanitized) or ProseMirror JSON
|
|
119
472
|
extensions: [StarterKit],
|
|
120
473
|
editable: true,
|
|
@@ -138,6 +491,8 @@ createEditor({
|
|
|
138
491
|
items: { /* custom buttons, see below */ },
|
|
139
492
|
statusbar: { elementPath: true, wordCount: true, characterCount: false }, // or false
|
|
140
493
|
theme: "auto", // "light" | "dark"
|
|
494
|
+
brand: "#e11d48", // one color for buttons, links, focus and selection (see Styling)
|
|
495
|
+
tokens: { radius: "8px" }, // any design token, without the --owe- prefix (see Styling)
|
|
141
496
|
stickyToolbar: true,
|
|
142
497
|
shortcuts: { toolbar: "Alt-F10", help: "Alt-0", find: "Mod-f", link: "Mod-k" },
|
|
143
498
|
},
|
|
@@ -169,7 +524,7 @@ editor.announce("Saved"); // send a message to screen readers
|
|
|
169
524
|
editor.destroy();
|
|
170
525
|
```
|
|
171
526
|
|
|
172
|
-
The main entry also exports `getUI(editor)`, which returns `focusToolbar()`, `openDialog(kind)`, `openFind()`, `toggleSource()`, `isSourceMode()` and `
|
|
527
|
+
The main entry also exports `getUI(editor)`, which returns `focusToolbar()`, `openDialog(kind)`, `openFind()`, `toggleSource()`, `isSourceMode()`, `update()` and `setTheme({ theme, brand, tokens })` (see [Styling](#styling)).
|
|
173
528
|
|
|
174
529
|
### Commands
|
|
175
530
|
|
|
@@ -189,7 +544,7 @@ Color commands accept hex, `rgb()`, `hsl()` or named colors. Anything else is re
|
|
|
189
544
|
|
|
190
545
|
---
|
|
191
546
|
|
|
192
|
-
##
|
|
547
|
+
## Feature guide
|
|
193
548
|
|
|
194
549
|
### Toolbar
|
|
195
550
|
|
|
@@ -355,16 +710,270 @@ Untranslated labels fall back to English, and plurals use `Intl.PluralRules`. Th
|
|
|
355
710
|
| `open-wysiwyg-editor/style.css` (`.min.css`) | The editor, its UI and its content styles |
|
|
356
711
|
| `open-wysiwyg-editor/content.css` (`.min.css`) | Only the content styles, for published pages: wrap saved HTML in `<div class="owe-content-root">` |
|
|
357
712
|
|
|
358
|
-
|
|
713
|
+
Each framework package ships the same files under its own name, for example `@open-wysiwyg-editor/vue/style.css` and `@open-wysiwyg-editor/vue/content.css`, because package managers like pnpm don't expose the core package to your app.
|
|
714
|
+
|
|
715
|
+
All rules are in `@layer owe`, so any of your own CSS outside a layer overrides them.
|
|
716
|
+
|
|
717
|
+
### One color
|
|
718
|
+
|
|
719
|
+
Pick your brand color and the whole editor follows it, in light and dark: the pressed toolbar buttons, the dialog buttons, links, the focus ring, the caret, the text selection and the gradient edge of the card.
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
createEditor({ element, ui: { brand: "#e11d48" } });
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
```html
|
|
726
|
+
<owe-editor brand="#e11d48" theme="dark"></owe-editor>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
```css
|
|
730
|
+
.owe { --owe-brand: #e11d48; }
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
All three do the same thing. `brand` and the attribute take any CSS color. They also pick black or white for the text on brand-colored buttons (`--owe-on-brand`), by contrast, when the color is a hex, `rgb()`, `hsl()` or basic named color. With plain CSS, set `--owe-on-brand` yourself if white doesn't read well on your color. `--owe-brand` can be set on `.owe`, on a wrapper or on `:root`.
|
|
734
|
+
|
|
735
|
+
Everything else is derived from the brand with `color-mix()`, so you only override what you want to change:
|
|
736
|
+
|
|
737
|
+
| Token | Derived as |
|
|
738
|
+
| --- | --- |
|
|
739
|
+
| `--owe-primary` | The brand. Pressed toolbar buttons, primary dialog buttons, the current find match |
|
|
740
|
+
| `--owe-primary-hover` | `--owe-primary` mixed 15% toward black |
|
|
741
|
+
| `--owe-accent` | Text and link color. The brand mixed 30% toward black in light, 40% toward white in dark |
|
|
742
|
+
| `--owe-accent-soft` | The brand at 18% over `--owe-bg` |
|
|
743
|
+
| `--owe-focus`, `--owe-caret` | `--owe-accent` |
|
|
744
|
+
| `--owe-selection` | The brand at 22% opacity in light, 45% in dark |
|
|
745
|
+
| `--owe-on-accent` | `--owe-on-brand` |
|
|
746
|
+
| `--owe-gradient` | `linear-gradient(90deg, var(--owe-brand), var(--owe-brand-2))` |
|
|
747
|
+
|
|
748
|
+
Setting one of these tokens yourself always wins over the derived value. `ui.brand` also sets `--owe-brand-2` to a lighter tint of the brand, so the card edge fades from your color. With plain CSS the second stop stays `#00b8ff`: set `--owe-brand-2` to the same color as `--owe-brand` for a solid edge.
|
|
749
|
+
|
|
750
|
+
The editor can't pick your colors for you. `--owe-accent` keeps 4.5:1 on white for typical brands, but a very pale brand (yellow, light cyan) or a very dark one in dark mode may need an explicit `--owe-accent`. Pressed buttons need 3:1 against the toolbar (WCAG 1.4.11): for a very dark brand in dark mode, set `--owe-btn-active-bg: var(--owe-accent)` and `--owe-btn-active-color` to a dark color.
|
|
751
|
+
|
|
752
|
+
### Design tokens
|
|
753
|
+
|
|
754
|
+
Set any token on `.owe` in your own CSS, or pass it as `ui.tokens` without the `--owe-` prefix (`tokens: { bg: "#0b0d10", radius: "8px" }`). Tokens set from JavaScript are applied through the CSSOM, so they work under a strict CSP. Unknown names are ignored, and so are values that contain `;`, `{`, `}`, `<` or `\`, or that call `url()`, `image()`, `image-set()` or `src()`. In TypeScript the names are the `ThemeToken` type.
|
|
755
|
+
|
|
756
|
+
| Token | Light | Dark | Styles |
|
|
757
|
+
| --- | --- | --- | --- |
|
|
758
|
+
| `--owe-brand` | `#0066ff` | same | The one brand color (see above) |
|
|
759
|
+
| `--owe-brand-2` | `#00b8ff` | same | Second color of the gradient edge |
|
|
760
|
+
| `--owe-on-brand` | `#ffffff` | same | Text and icons on brand fills |
|
|
761
|
+
| `--owe-primary` | brand | brand | Solid fills: pressed buttons, primary buttons |
|
|
762
|
+
| `--owe-primary-hover` | brand, 15% darker | same | Hover on those fills |
|
|
763
|
+
| `--owe-accent` | brand, 30% darker (`#003c9e`) | brand, 40% lighter (`#75a9ff`) | Links, checked menu items, the current path item |
|
|
764
|
+
| `--owe-accent-soft` | `#d1e3ff` | `#132744` | A brand tint for your own UI. The editor itself doesn't use it |
|
|
765
|
+
| `--owe-focus` | accent | accent | Focus ring, selected image and table outlines |
|
|
766
|
+
| `--owe-caret` | accent | accent | Text caret |
|
|
767
|
+
| `--owe-selection` | brand, 22% opacity | brand, 45% opacity | Selected text and table cells |
|
|
768
|
+
| `--owe-gradient` | brand to brand-2 | same | Bottom edge of the card |
|
|
769
|
+
| `--owe-bg` | `#ffffff` | `#17191b` | Card and editing area |
|
|
770
|
+
| `--owe-chrome` | `#fafbfc` | `#131517` | Toolbar, find bar and status bar |
|
|
771
|
+
| `--owe-surface` | `#f4f6f8` | `#1e2124` | Menus and dialogs |
|
|
772
|
+
| `--owe-surface-2` | `#f1f3f5` | `#25282b` | Hover backgrounds, keys in the shortcut list |
|
|
773
|
+
| `--owe-field-bg` | `#ffffff` | `#101214` | Inputs and selects |
|
|
774
|
+
| `--owe-code-bg` | `#f4f6f8` | `#0f1112` | Code, the source view |
|
|
775
|
+
| `--owe-text` | `#101112` | `#ffffff` | Headings and UI text |
|
|
776
|
+
| `--owe-body` | `#1b1f24` | `#dcdedf` | Paragraph text |
|
|
777
|
+
| `--owe-text-2` | `#3d434a` | `#c3c7cb` | Quotes |
|
|
778
|
+
| `--owe-muted` | `#54595f` | `#a1a7ad` | Placeholder, hints, shortcuts, status bar |
|
|
779
|
+
| `--owe-icon` | `#3a3f45` | `#d2d4d5` | Toolbar icons |
|
|
780
|
+
| `--owe-border` | `#8a929a` | `#6b7279` | Input and swatch borders (3:1 on the card) |
|
|
781
|
+
| `--owe-border-subtle` | `#e3e6ea` | `#25282b` | Card edge and dividers |
|
|
782
|
+
| `--owe-popup-border` | `#d5d9de` | `#33383d` | Menu and dialog edge |
|
|
783
|
+
| `--owe-danger` | `#b3261e` | `#f2b8b5` | Errors and destructive buttons |
|
|
784
|
+
| `--owe-warning` | `#8a5300` | `#f6c66b` | Find-match outline |
|
|
785
|
+
| `--owe-success` | `#146c2e` | `#8bd6a0` | For your own success messages. The editor itself doesn't use it |
|
|
786
|
+
| `--owe-mark` | `#fff2a8` | `#6b4e00` | `<mark>` and find matches |
|
|
787
|
+
| `--owe-shadow` | soft grey shadow | black shadow | Menus and dialogs |
|
|
788
|
+
| `--owe-backdrop` | `rgb(16 17 18 / 0.45)` | `rgb(4 5 16 / 0.65)` | Behind dialogs |
|
|
789
|
+
| `--owe-font` | `system-ui, "Segoe UI", "Noto Sans Arabic", …` | same | UI and content typeface. No web fonts are bundled |
|
|
790
|
+
| `--owe-font-mono` | `ui-monospace, Menlo, Consolas, …` | same | Code and the status bar |
|
|
791
|
+
| `--owe-font-size`, `--owe-line-height` | `1rem`, `1.65` | same | Content text |
|
|
792
|
+
| `--owe-radius` | `24px` | same | The card |
|
|
793
|
+
| `--owe-radius-sm`, `-md`, `-lg` | `10px`, `12px`, `16px` | same | Controls, menus, dialogs |
|
|
794
|
+
| `--owe-btn-size` | `40px` | same | Toolbar button size |
|
|
795
|
+
| `--owe-target` | `32px` | same | Minimum height of menu items, the status bar and small controls |
|
|
796
|
+
| `--owe-pad-x`, `--owe-pad-y` | `clamp(16px, 4vw, 32px)`, `28px` | same | Padding of the editing area |
|
|
797
|
+
| `--owe-z-popover` | `1000` | same | `z-index` of menus and tooltips |
|
|
798
|
+
|
|
799
|
+
In dark mode the editor uses the dark column when the system prefers dark, or when `theme` is `dark`. `theme: "light"` turns that off.
|
|
800
|
+
|
|
801
|
+
### Button tokens
|
|
802
|
+
|
|
803
|
+
These restyle the buttons, the toolbar and the card without touching any selector. They have no default value declared, so you can set them on `.owe` or on any element around it, such as `:root` or your dashboard wrapper. The default in the table is what the editor uses when you don't set them.
|
|
804
|
+
|
|
805
|
+
| Token | Default | Styles |
|
|
806
|
+
| --- | --- | --- |
|
|
807
|
+
| `--owe-btn-color` | `--owe-icon` | Toolbar button icon and text |
|
|
808
|
+
| `--owe-btn-bg` | `transparent` | Toolbar button background |
|
|
809
|
+
| `--owe-btn-hover-color` | `--owe-text` | Button and path item text on hover |
|
|
810
|
+
| `--owe-btn-hover-bg` | `--owe-surface-2` | Hover background of buttons, menu items and path items |
|
|
811
|
+
| `--owe-btn-active-color` | `--owe-on-accent` | Text and icon of a pressed or toggled button |
|
|
812
|
+
| `--owe-btn-active-bg` | `--owe-primary` | Background of a pressed or toggled button |
|
|
813
|
+
| `--owe-btn-active-hover-bg` | `--owe-btn-active-bg`, 15% darker | Hover on a pressed button |
|
|
814
|
+
| `--owe-btn-radius` | `--owe-radius-sm` | Corners of buttons, menu items and path items |
|
|
815
|
+
| `--owe-btn-primary-bg` | `--owe-primary` | Primary button in dialogs (Insert, Apply) |
|
|
816
|
+
| `--owe-btn-primary-color` | `--owe-on-accent` | Text of the primary button |
|
|
817
|
+
| `--owe-btn-primary-hover-bg` | `--owe-primary-hover` | Primary button on hover |
|
|
818
|
+
| `--owe-toolbar-bg` | `--owe-chrome` | Toolbar, find bar and status bar background |
|
|
819
|
+
| `--owe-toolbar-border` | `--owe-border-subtle` | Lines between toolbar, content and status bar |
|
|
820
|
+
| `--owe-editor-border` | `--owe-border-subtle` | Border of the card |
|
|
821
|
+
| `--owe-editor-radius` | `--owe-radius` | Corners of the card, toolbar and status bar |
|
|
822
|
+
|
|
823
|
+
A dark toolbar over a light editing area, with smaller corners:
|
|
359
824
|
|
|
360
825
|
```css
|
|
361
826
|
.owe {
|
|
362
|
-
--owe-
|
|
363
|
-
--owe-
|
|
364
|
-
--owe-
|
|
827
|
+
--owe-toolbar-bg: #0f172a;
|
|
828
|
+
--owe-toolbar-border: #0f172a;
|
|
829
|
+
--owe-btn-color: #e2e8f0;
|
|
830
|
+
--owe-btn-hover-bg: #1e293b;
|
|
831
|
+
--owe-btn-hover-color: #ffffff;
|
|
832
|
+
--owe-btn-radius: 6px;
|
|
833
|
+
--owe-editor-radius: 8px;
|
|
365
834
|
}
|
|
366
835
|
```
|
|
367
836
|
|
|
837
|
+
Windows High Contrast (`forced-colors`) and `prefers-contrast: more` still take over where they must: pressed buttons use the system highlight colors whatever you set.
|
|
838
|
+
|
|
839
|
+
### Match your dashboard
|
|
840
|
+
|
|
841
|
+
Point the tokens at the variables your design system already defines. Put the CSS after the editor's stylesheet, or anywhere, because your unlayered CSS beats `@layer owe`. Because the tokens read your variables when they are used, the colors follow your light and dark themes without any script.
|
|
842
|
+
|
|
843
|
+
Tell the editor when your dashboard is dark as well: set `theme` to `dark` (`<owe-editor theme="dark">`, `ui.theme` or `setTheme()`). That switches the parts that are derived from the brand for dark backgrounds (links, selection), the highlight color and `color-scheme`, so scrollbars and native controls match. With `theme` left on `auto`, the editor follows the system setting.
|
|
844
|
+
|
|
845
|
+
**shadcn/ui** (Tailwind v4 variables):
|
|
846
|
+
|
|
847
|
+
```css
|
|
848
|
+
.owe {
|
|
849
|
+
--owe-brand: var(--primary);
|
|
850
|
+
--owe-brand-2: var(--primary);
|
|
851
|
+
--owe-on-brand: var(--primary-foreground);
|
|
852
|
+
--owe-bg: var(--card);
|
|
853
|
+
--owe-text: var(--card-foreground);
|
|
854
|
+
--owe-body: var(--card-foreground);
|
|
855
|
+
--owe-chrome: var(--muted);
|
|
856
|
+
--owe-surface: var(--popover);
|
|
857
|
+
--owe-surface-2: var(--accent);
|
|
858
|
+
--owe-field-bg: var(--background);
|
|
859
|
+
--owe-muted: var(--muted-foreground);
|
|
860
|
+
--owe-icon: var(--foreground);
|
|
861
|
+
--owe-border-subtle: var(--border);
|
|
862
|
+
--owe-popup-border: var(--border);
|
|
863
|
+
/* --owe-border draws input outlines, which need 3:1; shadcn's --border is much lighter */
|
|
864
|
+
--owe-border: color-mix(in oklab, var(--foreground) 45%, var(--card));
|
|
865
|
+
--owe-radius: var(--radius);
|
|
866
|
+
--owe-radius-sm: calc(var(--radius) - 2px);
|
|
867
|
+
--owe-font: inherit;
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Bootstrap 5.3** (`data-bs-theme` switches light and dark):
|
|
872
|
+
|
|
873
|
+
```css
|
|
874
|
+
.owe {
|
|
875
|
+
--owe-brand: var(--bs-primary);
|
|
876
|
+
--owe-brand-2: var(--bs-primary);
|
|
877
|
+
--owe-accent: var(--bs-link-color);
|
|
878
|
+
--owe-bg: var(--bs-body-bg);
|
|
879
|
+
--owe-chrome: var(--bs-tertiary-bg);
|
|
880
|
+
--owe-surface: var(--bs-body-bg);
|
|
881
|
+
--owe-surface-2: var(--bs-secondary-bg);
|
|
882
|
+
--owe-field-bg: var(--bs-body-bg);
|
|
883
|
+
--owe-text: var(--bs-emphasis-color);
|
|
884
|
+
--owe-body: var(--bs-body-color);
|
|
885
|
+
--owe-muted: var(--bs-secondary-color);
|
|
886
|
+
--owe-icon: var(--bs-body-color);
|
|
887
|
+
--owe-border-subtle: var(--bs-border-color);
|
|
888
|
+
--owe-popup-border: var(--bs-border-color);
|
|
889
|
+
--owe-radius: var(--bs-border-radius-xl);
|
|
890
|
+
--owe-radius-sm: var(--bs-border-radius);
|
|
891
|
+
--owe-font: inherit;
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
**Material UI** (v6 and later, with `createTheme({ cssVariables: true, colorSchemes: { dark: true } })`):
|
|
896
|
+
|
|
897
|
+
```css
|
|
898
|
+
.owe {
|
|
899
|
+
--owe-brand: var(--mui-palette-primary-main);
|
|
900
|
+
--owe-brand-2: var(--mui-palette-primary-main);
|
|
901
|
+
--owe-on-brand: var(--mui-palette-primary-contrastText);
|
|
902
|
+
--owe-accent: var(--mui-palette-primary-main);
|
|
903
|
+
--owe-bg: var(--mui-palette-background-paper);
|
|
904
|
+
--owe-chrome: var(--mui-palette-background-default);
|
|
905
|
+
--owe-surface: var(--mui-palette-background-paper);
|
|
906
|
+
--owe-field-bg: var(--mui-palette-background-paper);
|
|
907
|
+
--owe-text: var(--mui-palette-text-primary);
|
|
908
|
+
--owe-body: var(--mui-palette-text-primary);
|
|
909
|
+
--owe-muted: var(--mui-palette-text-secondary);
|
|
910
|
+
--owe-icon: var(--mui-palette-text-secondary);
|
|
911
|
+
--owe-border: var(--mui-palette-text-secondary);
|
|
912
|
+
--owe-border-subtle: var(--mui-palette-divider);
|
|
913
|
+
--owe-popup-border: var(--mui-palette-divider);
|
|
914
|
+
--owe-btn-hover-bg: var(--mui-palette-action-hover);
|
|
915
|
+
--owe-radius: calc(var(--mui-shape-borderRadius) * 3);
|
|
916
|
+
--owe-radius-sm: var(--mui-shape-borderRadius);
|
|
917
|
+
--owe-font: inherit;
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
**A plain dark dashboard** with its own palette:
|
|
922
|
+
|
|
923
|
+
```css
|
|
924
|
+
.owe {
|
|
925
|
+
--owe-brand: #38bdf8;
|
|
926
|
+
--owe-on-brand: #0b1120; /* white is only 2:1 on this blue */
|
|
927
|
+
--owe-bg: #0f172a;
|
|
928
|
+
--owe-chrome: #0b1120;
|
|
929
|
+
--owe-surface: #1e293b;
|
|
930
|
+
--owe-surface-2: #263449;
|
|
931
|
+
--owe-field-bg: #0b1120;
|
|
932
|
+
--owe-text: #f8fafc;
|
|
933
|
+
--owe-body: #e2e8f0;
|
|
934
|
+
--owe-muted: #94a3b8;
|
|
935
|
+
--owe-icon: #cbd5e1;
|
|
936
|
+
--owe-border: #64748b;
|
|
937
|
+
--owe-border-subtle: #1e293b;
|
|
938
|
+
--owe-popup-border: #334155;
|
|
939
|
+
--owe-radius: 12px;
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
Then switch the editor to its dark theme with `<owe-editor theme="dark">`, or the same from JavaScript, without any CSS. `brand` picks the dark text on this blue by itself:
|
|
944
|
+
|
|
945
|
+
```ts
|
|
946
|
+
createEditor({
|
|
947
|
+
element,
|
|
948
|
+
ui: {
|
|
949
|
+
theme: "dark",
|
|
950
|
+
brand: "#38bdf8",
|
|
951
|
+
tokens: { bg: "#0f172a", chrome: "#0b1120", surface: "#1e293b", radius: "12px" },
|
|
952
|
+
},
|
|
953
|
+
});
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### Changing the theme later
|
|
957
|
+
|
|
958
|
+
`<owe-editor>` follows changes to its `theme` and `brand` attributes. For an editor you created with `createEditor()`, call `setTheme()` on its UI. It replaces the whole theme, so pass everything you want to keep:
|
|
959
|
+
|
|
960
|
+
```ts
|
|
961
|
+
import { getUI } from "open-wysiwyg-editor";
|
|
962
|
+
|
|
963
|
+
getUI(editor)?.setTheme({ theme: "dark", brand: "#e11d48", tokens: { radius: "8px" } });
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
The framework components read `ui` once, when the editor starts, so call `setTheme()` when your own state changes:
|
|
967
|
+
|
|
968
|
+
```tsx
|
|
969
|
+
const { ref, editor } = useEditor({ ui: { brand } });
|
|
970
|
+
useEffect(() => {
|
|
971
|
+
if (editor) getUI(editor)?.setTheme({ brand });
|
|
972
|
+
}, [editor, brand]);
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
`applyTheme(element, { theme, brand, tokens })` is the function behind `setTheme()`. Use it on any element, for example the `.owe-content-root` that shows saved HTML. CSS variables also change at runtime: set `--owe-brand` on `.owe`, or on a wrapper, whenever you like.
|
|
976
|
+
|
|
368
977
|
## Extensions
|
|
369
978
|
|
|
370
979
|
Everything is built from extensions. You can write your own the same way:
|
|
@@ -392,10 +1001,93 @@ createEditor({ element, extensions: [StarterKit, Timestamp] });
|
|
|
392
1001
|
|
|
393
1002
|
`defineExtension` also accepts `nodes`, `marks`, `globalAttributes`, `inputRules`, `plugins` (raw ProseMirror plugins), `onCreate` and `onDestroy`. Call `.configure(options)` on any extension to change its options.
|
|
394
1003
|
|
|
1004
|
+
## Writing your own integration
|
|
1005
|
+
|
|
1006
|
+
The framework packages are thin wrappers around a few helpers that the core package exports, so you can wrap the editor for any other framework in the same way:
|
|
1007
|
+
|
|
1008
|
+
| Helper | What it does |
|
|
1009
|
+
| --- | --- |
|
|
1010
|
+
| `RUNTIME_OPTIONS` | The options an existing editor can change through `setOptions()`: `editable`, `placeholder`, `ariaLabel`, `ariaLabelledBy`, `ariaDescribedBy`, `dir`, `contentLang` |
|
|
1011
|
+
| `pickRuntimeOptions(options)` | Returns only those options from an options object |
|
|
1012
|
+
| `sameRuntimeOptions(a, b)` | `true` when two such objects hold the same values, so you can skip a `setOptions()` call |
|
|
1013
|
+
| `forwardCallbacks(get)` | Returns `onCreate`, `onUpdate`, `onSelectionUpdate`, `onFocus`, `onBlur`, `onDestroy` and `onContentError` handlers that always call the latest options' versions, so changing a callback never recreates the editor |
|
|
1014
|
+
| `syncContent(editor, content)` | Loads bound content into the editor, and skips HTML the editor itself just produced, so typing never resets the cursor |
|
|
1015
|
+
|
|
1016
|
+
```ts
|
|
1017
|
+
import {
|
|
1018
|
+
createEditor, forwardCallbacks, pickRuntimeOptions, sameRuntimeOptions, syncContent,
|
|
1019
|
+
type Editor, type EditorOptions,
|
|
1020
|
+
} from "open-wysiwyg-editor";
|
|
1021
|
+
|
|
1022
|
+
function mount(element: HTMLElement, initial: Omit<EditorOptions, "element">) {
|
|
1023
|
+
let options = initial;
|
|
1024
|
+
let runtime = pickRuntimeOptions(options);
|
|
1025
|
+
const editor: Editor = createEditor({ ...options, element, ...forwardCallbacks(() => options) });
|
|
1026
|
+
|
|
1027
|
+
return {
|
|
1028
|
+
update(next: Omit<EditorOptions, "element">) {
|
|
1029
|
+
options = next; // callbacks pick this up through forwardCallbacks
|
|
1030
|
+
if (next.content !== undefined) syncContent(editor, next.content);
|
|
1031
|
+
const nextRuntime = pickRuntimeOptions(next);
|
|
1032
|
+
if (!sameRuntimeOptions(runtime, nextRuntime)) editor.setOptions((runtime = nextRuntime));
|
|
1033
|
+
},
|
|
1034
|
+
destroy: () => editor.destroy(),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
Remember the three rules from [Usage](#usage): load the stylesheet, start in the browser only, and destroy the editor when the element goes away. The [Svelte](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/packages/svelte/src/index.ts) and [Solid](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/packages/solid/src/index.ts) packages are small, complete examples.
|
|
1040
|
+
|
|
1041
|
+
## Package contents
|
|
1042
|
+
|
|
1043
|
+
| Package | Import | Format | Use it for |
|
|
1044
|
+
| --- | --- | --- | --- |
|
|
1045
|
+
| `open-wysiwyg-editor` | `open-wysiwyg-editor` | ESM + CJS + `.d.ts` | The engine, every extension and the accessible UI |
|
|
1046
|
+
| | `open-wysiwyg-editor/headless` | ESM + CJS + `.d.ts` | The engine and extensions without UI code |
|
|
1047
|
+
| | `open-wysiwyg-editor/element` | ESM + CJS + `.d.ts` | Defines the `<owe-editor>` element |
|
|
1048
|
+
| | `open-wysiwyg-editor/style.css` (`.min.css`) | CSS | Editor, UI and content styles |
|
|
1049
|
+
| | `open-wysiwyg-editor/content.css` (`.min.css`) | CSS | Content styles only, for pages that display saved HTML |
|
|
1050
|
+
| | `dist/open-wysiwyg-editor.global.js` | IIFE | `<script>` tag; exposes `window.OpenWysiwygEditor` and defines `<owe-editor>` |
|
|
1051
|
+
| `@open-wysiwyg-editor/react` | `@open-wysiwyg-editor/react` | ESM + CJS + `.d.ts` | `<RichTextEditor>` and `useEditor()` (React 18+) |
|
|
1052
|
+
| `@open-wysiwyg-editor/next` | `@open-wysiwyg-editor/next` | ESM + CJS + `.d.ts` | The React package with `"use client"` for the App Router |
|
|
1053
|
+
| `@open-wysiwyg-editor/preact` | `@open-wysiwyg-editor/preact` | ESM + CJS + `.d.ts` | `<RichTextEditor>` and `useEditor()` on `preact/hooks` |
|
|
1054
|
+
| `@open-wysiwyg-editor/vue` | `@open-wysiwyg-editor/vue` | ESM + CJS + `.d.ts` | `<RichTextEditor v-model>` (Vue 3.3+) |
|
|
1055
|
+
| `@open-wysiwyg-editor/nuxt` | `@open-wysiwyg-editor/nuxt` | Nuxt module | Auto-imported component and stylesheet |
|
|
1056
|
+
| `@open-wysiwyg-editor/angular` | `@open-wysiwyg-editor/angular` | Angular package (ng-packagr) | Standalone component with forms support (Angular 21+) |
|
|
1057
|
+
| `@open-wysiwyg-editor/svelte` | `@open-wysiwyg-editor/svelte` | ESM + CJS + `.d.ts` | `use:richText` action (Svelte 3, 4, 5) |
|
|
1058
|
+
| `@open-wysiwyg-editor/solid` | `@open-wysiwyg-editor/solid` | ESM + CJS + `.d.ts` | `use:richText` directive (Solid 1.6+) |
|
|
1059
|
+
| `@open-wysiwyg-editor/astro` | `@open-wysiwyg-editor/astro` | `.astro` component + `.d.ts` | `RichTextEditor.astro`, which renders `<owe-editor>` |
|
|
1060
|
+
|
|
1061
|
+
Every framework package re-exports the whole core API and ships `style.css`, `style.min.css`, `content.css` and `content.min.css`.
|
|
1062
|
+
|
|
395
1063
|
## Browser support
|
|
396
1064
|
|
|
397
|
-
The editor targets current versions of Chrome, Edge, Firefox and Safari. Every change is tested end to end in Chromium, Firefox and WebKit, under a strict CSP with Trusted Types and with axe-core accessibility checks.
|
|
1065
|
+
The editor targets current versions of Chrome, Edge, Firefox and Safari, on desktop and mobile. Every change is tested end to end in Chromium, Firefox and WebKit, under a strict CSP with Trusted Types and with axe-core accessibility checks.
|
|
1066
|
+
|
|
1067
|
+
## Development
|
|
1068
|
+
|
|
1069
|
+
```bash
|
|
1070
|
+
npm install
|
|
1071
|
+
npm run build # builds every package
|
|
1072
|
+
npm run check # lint + build + typecheck + unit tests
|
|
1073
|
+
npm run e2e # end-to-end + axe tests in Chromium, Firefox and WebKit (Playwright)
|
|
1074
|
+
npm run dev # serve the website locally (http://localhost:3000/open-wysiwyg-editor/)
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
Layout:
|
|
1078
|
+
|
|
1079
|
+
```
|
|
1080
|
+
README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md LICENSE
|
|
1081
|
+
examples/ the documentation website + live demo: one Next.js page in English and Arabic (deployed to GitHub Pages)
|
|
1082
|
+
test/e2e/ Playwright end-to-end + axe tests
|
|
1083
|
+
scripts/ serve-e2e, size-check, license-check, copy-styles, publish, render-banner
|
|
1084
|
+
packages/<name>/ core, react, next, preact, vue, nuxt, angular, svelte, solid, astro
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
Every push to `main` rebuilds `examples/` and deploys it as the [live demo](https://alhassan73.github.io/open-wysiwyg-editor/). This README is the npm page of the core package: it is copied to `packages/core/README.md` when that package is packed.
|
|
1088
|
+
|
|
1089
|
+
See [CONTRIBUTING.md](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/CONTRIBUTING.md), [SECURITY.md](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/SECURITY.md) and the [changelog](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/CHANGELOG.md).
|
|
398
1090
|
|
|
399
1091
|
## License
|
|
400
1092
|
|
|
401
|
-
[MIT](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/LICENSE)
|
|
1093
|
+
[MIT](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/LICENSE) © alhassan-ahmed
|