open-wysiwyg-editor 0.1.0 → 1.0.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.
Files changed (41) hide show
  1. package/README.md +488 -52
  2. package/dist/{chunk-CDPXFV6R.js → chunk-2OSX7R22.js} +51 -315
  3. package/dist/chunk-2OSX7R22.js.map +1 -0
  4. package/dist/chunk-2WBWKTLE.js +277 -0
  5. package/dist/chunk-2WBWKTLE.js.map +1 -0
  6. package/dist/chunk-F3DLZFDY.js +1734 -0
  7. package/dist/chunk-F3DLZFDY.js.map +1 -0
  8. package/dist/chunk-HKWFKMAC.cjs +284 -0
  9. package/dist/chunk-HKWFKMAC.cjs.map +1 -0
  10. package/dist/chunk-JZHAUAUJ.cjs +1748 -0
  11. package/dist/chunk-JZHAUAUJ.cjs.map +1 -0
  12. package/dist/chunk-MARI3ZF6.cjs +3184 -0
  13. package/dist/chunk-MARI3ZF6.cjs.map +1 -0
  14. package/dist/content.css +129 -64
  15. package/dist/content.min.css +1 -1
  16. package/dist/editor-8zSxaBqs.d.cts +516 -0
  17. package/dist/editor-8zSxaBqs.d.ts +516 -0
  18. package/dist/editor-BKYDbfo1.d.ts +151 -0
  19. package/dist/editor-CY40AOtO.d.cts +151 -0
  20. package/dist/element.cjs +139 -0
  21. package/dist/element.cjs.map +1 -0
  22. package/dist/element.d.cts +39 -0
  23. package/dist/element.d.ts +39 -0
  24. package/dist/element.js +136 -0
  25. package/dist/element.js.map +1 -0
  26. package/dist/headless.cjs +338 -3428
  27. package/dist/headless.cjs.map +1 -1
  28. package/dist/headless.d.cts +7 -514
  29. package/dist/headless.d.ts +7 -514
  30. package/dist/headless.js +2 -1
  31. package/dist/index.cjs +354 -5111
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +19 -151
  34. package/dist/index.d.ts +19 -151
  35. package/dist/index.js +3 -1699
  36. package/dist/index.js.map +1 -1
  37. package/dist/open-wysiwyg-editor.global.js +16 -16
  38. package/dist/style.css +275 -130
  39. package/dist/style.min.css +1 -1
  40. package/package.json +34 -9
  41. package/dist/chunk-CDPXFV6R.js.map +0 -1
package/README.md CHANGED
@@ -1,57 +1,126 @@
1
- # open-wysiwyg-editor
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
- An accessible rich text editor that is RTL-first and safe under a strict CSP. It works in plain JavaScript and in any framework.
3
+ # Open WYSIWYG Editor
4
4
 
5
- - **Accessible by design.** Built against WCAG 2.2 AA, ATAG 2.0 and the WAI-ARIA Authoring Practices. You can do everything from the keyboard and never get trapped. Screen readers hear announcements, and Windows High Contrast is supported.
6
- - **Secure by default.** All HTML input passes through DOMPurify, URLs are checked against an allow-list, and Trusted Types are supported. It runs under `script-src 'self'; style-src 'self'` with no `unsafe-inline`.
7
- - **Arabic and RTL first.** It ships with English and Arabic UIs, mirrors the layout, and lets each block carry its own direction.
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
+ [![npm version](https://img.shields.io/npm/v/open-wysiwyg-editor)](https://www.npmjs.com/package/open-wysiwyg-editor)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/open-wysiwyg-editor)](https://bundlephobia.com/package/open-wysiwyg-editor)
7
+ [![license](https://img.shields.io/npm/l/open-wysiwyg-editor)](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/LICENSE)
8
+
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)
10
+
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.
11
12
 
12
13
  Built on [ProseMirror](https://prosemirror.net). Content is stored as HTML or JSON, and node and mark names match Tiptap's.
13
14
 
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 · Theme with 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
- ```sh
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
- Without a bundler, load the browser build from a CDN:
40
+ Or with no build step, from a CDN:
25
41
 
26
42
  ```html
27
- <link rel="stylesheet" href="https://unpkg.com/open-wysiwyg-editor/dist/style.min.css" />
28
- <script src="https://unpkg.com/open-wysiwyg-editor/dist/open-wysiwyg-editor.global.js"></script>
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
- ## Quick start
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
- <div id="editor"><p>Hello <strong>world</strong></p></div>
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
- ```js
38
- import { createEditor } from "open-wysiwyg-editor";
39
- import "open-wysiwyg-editor/style.css";
93
+ Or call the function yourself:
40
94
 
41
- const editor = createEditor({ element: document.querySelector("#editor") });
95
+ ```html
96
+ <div id="editor"><p>Hello <strong>world</strong></p></div>
42
97
 
43
- editor.getHTML(); // "<p dir=\"auto\">Hello <strong>world</strong></p>"
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
- With the global build:
107
+ This also works in WordPress, Shopify, Webflow, PHP, Django and Rails templates.
47
108
 
48
- ```js
49
- const editor = OpenWysiwygEditor.createEditor({ element: document.querySelector("#editor") });
50
- ```
109
+ ### HTML forms
110
+
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.
51
112
 
52
- ### Plain HTML forms
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
- If you mount the editor on a `<textarea>`, it hides the textarea and keeps its value in sync, so the form submits the HTML. It also uses the textarea's `<label>` as its accessible name.
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 type="module">
63
- import { createEditor } from "open-wysiwyg-editor";
64
- createEditor({ element: document.querySelector("#body") });
131
+ <script>
132
+ OpenWysiwygEditor.createEditor({ element: "#body" });
65
133
  </script>
66
134
  ```
67
135
 
68
- ### React / Next.js
136
+ ### Any bundler (Vite, Webpack, Parcel…), JS or TS
69
137
 
70
- Dedicated adapters for React, Vue, Svelte and Angular, and an `<owe-editor>` web component, are on the way. Until they ship, the core works in any framework. Mount it in an effect and destroy it on cleanup:
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";`.
149
+
150
+ ### React (Vite, CRA, Remix, Gatsby)
151
+
152
+ ```bash
153
+ npm install @open-wysiwyg-editor/react
154
+ ```
71
155
 
72
156
  ```tsx
73
- "use client"; // Next.js: the editor needs the DOM
74
- import { useEffect, useRef } from "react";
75
- import { createEditor, type Editor } from "open-wysiwyg-editor";
76
- import "open-wysiwyg-editor/style.css";
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
+ ```
204
+
205
+ `name` keeps a hidden `<input>` in sync, so the Server Action receives the HTML in its `FormData`:
206
+
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:
274
+
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>";
307
+ }
308
+ ```
309
+
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>
77
325
 
78
- export function RichText({ value, onChange }: { value: string; onChange: (html: string) => void }) {
79
- const host = useRef<HTMLDivElement>(null);
80
- const editor = useRef<Editor | null>(null);
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
81
332
 
82
- useEffect(() => {
83
- editor.current = createEditor({
84
- element: host.current,
85
- content: value,
86
- onUpdate: (e) => onChange(e.getHTML()),
87
- });
88
- return () => editor.current?.destroy();
89
- }, []);
333
+ ```bash
334
+ npm install @open-wysiwyg-editor/solid
335
+ ```
90
336
 
91
- return <div ref={host} />;
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()) }} />;
92
346
  }
93
347
  ```
94
348
 
95
- ### Headless
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="&lt;p&gt;Hello&lt;/p&gt;"></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,65 @@ 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
+ | `value` | Initial content as HTML (read once, when the editor is created) |
436
+ | `language` | UI language, e.g. `ar` (read once). Defaults to `<html lang>` |
437
+ | `toolbar` | Toolbar items separated by spaces (`"bold italic \| link"`), or `none` (read once) |
438
+
439
+ `placeholder`, `readonly`, `disabled`, `dir`, `content-lang` and `label` follow later changes. `value`, `language` and `toolbar` are only read when the editor starts.
440
+
441
+ | Property | Description |
442
+ | --- | --- |
443
+ | `editor` | The live `Editor`, or `null` while the element isn't in the document |
444
+ | `value` | The content as HTML. Setting it loads new content |
445
+ | `options` | More `createEditor()` options (`extensions`, `ui`, `labels`, callbacks…). Set it **before** the element is added to the page. Attributes win over it |
446
+ | `form` | The surrounding `<form>`, or `null` |
447
+
448
+ | Event | When |
449
+ | --- | --- |
450
+ | `input` | On every change. `event.target.value` has the HTML |
451
+ | `change` | When the editor loses focus after a change |
452
+
453
+ **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.
454
+
455
+ ```js
456
+ const el = document.querySelector("owe-editor");
457
+ el.options = { language: "ar", ui: { stickyToolbar: false } };
458
+ el.value = "<p>New content</p>";
459
+ el.editor?.commands.toggleBold();
460
+ ```
461
+
462
+ 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)).
463
+
113
464
  ## Options
114
465
 
115
466
  ```ts
116
467
  createEditor({
117
- element, // container or <textarea>; omit to create it detached and append editor.root yourself
468
+ element, // container, CSS selector or <textarea>; omit to create it detached and append editor.root yourself
118
469
  content, // HTML string (always sanitized) or ProseMirror JSON
119
470
  extensions: [StarterKit],
120
471
  editable: true,
@@ -189,7 +540,7 @@ Color commands accept hex, `rgb()`, `hsl()` or named colors. Anything else is re
189
540
 
190
541
  ---
191
542
 
192
- ## Features
543
+ ## Feature guide
193
544
 
194
545
  ### Toolbar
195
546
 
@@ -355,6 +706,8 @@ Untranslated labels fall back to English, and plurals use `Intl.PluralRules`. Th
355
706
  | `open-wysiwyg-editor/style.css` (`.min.css`) | The editor, its UI and its content styles |
356
707
  | `open-wysiwyg-editor/content.css` (`.min.css`) | Only the content styles, for published pages: wrap saved HTML in `<div class="owe-content-root">` |
357
708
 
709
+ 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.
710
+
358
711
  All rules are in `@layer owe`, so any of your own CSS outside a layer overrides them. To theme the editor, set the design tokens:
359
712
 
360
713
  ```css
@@ -392,10 +745,93 @@ createEditor({ element, extensions: [StarterKit, Timestamp] });
392
745
 
393
746
  `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
747
 
748
+ ## Writing your own integration
749
+
750
+ 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:
751
+
752
+ | Helper | What it does |
753
+ | --- | --- |
754
+ | `RUNTIME_OPTIONS` | The options an existing editor can change through `setOptions()`: `editable`, `placeholder`, `ariaLabel`, `ariaLabelledBy`, `ariaDescribedBy`, `dir`, `contentLang` |
755
+ | `pickRuntimeOptions(options)` | Returns only those options from an options object |
756
+ | `sameRuntimeOptions(a, b)` | `true` when two such objects hold the same values, so you can skip a `setOptions()` call |
757
+ | `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 |
758
+ | `syncContent(editor, content)` | Loads bound content into the editor, and skips HTML the editor itself just produced, so typing never resets the cursor |
759
+
760
+ ```ts
761
+ import {
762
+ createEditor, forwardCallbacks, pickRuntimeOptions, sameRuntimeOptions, syncContent,
763
+ type Editor, type EditorOptions,
764
+ } from "open-wysiwyg-editor";
765
+
766
+ function mount(element: HTMLElement, initial: Omit<EditorOptions, "element">) {
767
+ let options = initial;
768
+ let runtime = pickRuntimeOptions(options);
769
+ const editor: Editor = createEditor({ ...options, element, ...forwardCallbacks(() => options) });
770
+
771
+ return {
772
+ update(next: Omit<EditorOptions, "element">) {
773
+ options = next; // callbacks pick this up through forwardCallbacks
774
+ if (next.content !== undefined) syncContent(editor, next.content);
775
+ const nextRuntime = pickRuntimeOptions(next);
776
+ if (!sameRuntimeOptions(runtime, nextRuntime)) editor.setOptions((runtime = nextRuntime));
777
+ },
778
+ destroy: () => editor.destroy(),
779
+ };
780
+ }
781
+ ```
782
+
783
+ 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.
784
+
785
+ ## Package contents
786
+
787
+ | Package | Import | Format | Use it for |
788
+ | --- | --- | --- | --- |
789
+ | `open-wysiwyg-editor` | `open-wysiwyg-editor` | ESM + CJS + `.d.ts` | The engine, every extension and the accessible UI |
790
+ | | `open-wysiwyg-editor/headless` | ESM + CJS + `.d.ts` | The engine and extensions without UI code |
791
+ | | `open-wysiwyg-editor/element` | ESM + CJS + `.d.ts` | Defines the `<owe-editor>` element |
792
+ | | `open-wysiwyg-editor/style.css` (`.min.css`) | CSS | Editor, UI and content styles |
793
+ | | `open-wysiwyg-editor/content.css` (`.min.css`) | CSS | Content styles only, for pages that display saved HTML |
794
+ | | `dist/open-wysiwyg-editor.global.js` | IIFE | `<script>` tag; exposes `window.OpenWysiwygEditor` and defines `<owe-editor>` |
795
+ | `@open-wysiwyg-editor/react` | `@open-wysiwyg-editor/react` | ESM + CJS + `.d.ts` | `<RichTextEditor>` and `useEditor()` (React 18+) |
796
+ | `@open-wysiwyg-editor/next` | `@open-wysiwyg-editor/next` | ESM + CJS + `.d.ts` | The React package with `"use client"` for the App Router |
797
+ | `@open-wysiwyg-editor/preact` | `@open-wysiwyg-editor/preact` | ESM + CJS + `.d.ts` | `<RichTextEditor>` and `useEditor()` on `preact/hooks` |
798
+ | `@open-wysiwyg-editor/vue` | `@open-wysiwyg-editor/vue` | ESM + CJS + `.d.ts` | `<RichTextEditor v-model>` (Vue 3.3+) |
799
+ | `@open-wysiwyg-editor/nuxt` | `@open-wysiwyg-editor/nuxt` | Nuxt module | Auto-imported component and stylesheet |
800
+ | `@open-wysiwyg-editor/angular` | `@open-wysiwyg-editor/angular` | Angular package (ng-packagr) | Standalone component with forms support (Angular 21+) |
801
+ | `@open-wysiwyg-editor/svelte` | `@open-wysiwyg-editor/svelte` | ESM + CJS + `.d.ts` | `use:richText` action (Svelte 3, 4, 5) |
802
+ | `@open-wysiwyg-editor/solid` | `@open-wysiwyg-editor/solid` | ESM + CJS + `.d.ts` | `use:richText` directive (Solid 1.6+) |
803
+ | `@open-wysiwyg-editor/astro` | `@open-wysiwyg-editor/astro` | `.astro` component + `.d.ts` | `RichTextEditor.astro`, which renders `<owe-editor>` |
804
+
805
+ Every framework package re-exports the whole core API and ships `style.css`, `style.min.css`, `content.css` and `content.min.css`.
806
+
395
807
  ## Browser support
396
808
 
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.
809
+ 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.
810
+
811
+ ## Development
812
+
813
+ ```bash
814
+ npm install
815
+ npm run build # builds every package
816
+ npm run check # lint + build + typecheck + unit tests
817
+ npm run e2e # end-to-end + axe tests in Chromium, Firefox and WebKit (Playwright)
818
+ npm run dev # serve the demo site locally
819
+ ```
820
+
821
+ Layout:
822
+
823
+ ```
824
+ README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md LICENSE
825
+ examples/ the documentation website + live demo (deployed to GitHub Pages)
826
+ test/e2e/ Playwright end-to-end + axe tests
827
+ scripts/ serve-e2e, size-check, license-check, copy-styles, publish, render-banner
828
+ packages/<name>/ core, react, next, preact, vue, nuxt, angular, svelte, solid, astro
829
+ ```
830
+
831
+ 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.
832
+
833
+ 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
834
 
399
835
  ## License
400
836
 
401
- [MIT](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/LICENSE)
837
+ [MIT](https://github.com/alhassan73/open-wysiwyg-editor/blob/main/LICENSE) © alhassan-ahmed