neiki-page-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/README.md ADDED
@@ -0,0 +1,997 @@
1
+ <p align="center">
2
+ <img src="assets/img/logo.svg" alt="Neiki's Page Editor" width="400">
3
+ </p>
4
+
5
+ <h1 align="center">Neiki's Page Editor</h1>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E" alt="JavaScript">
9
+ <img src="https://img.shields.io/badge/php-%23777BB4.svg?style=for-the-badge&logo=php&logoColor=white" alt="PHP">
10
+ <img src="https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white" alt="HTML5">
11
+ <img src="https://img.shields.io/badge/css-%23663399.svg?style=for-the-badge&logo=css&logoColor=white" alt="CSS"><br>
12
+ <img src="https://img.shields.io/badge/License-Source%20Available-2563EB?style=for-the-badge&logo=open-source-initiative&logoColor=white&labelColor=000F15&logoWidth=20" alt="License">
13
+ <img src="https://img.shields.io/badge/Version-0.1.0-2563EB?style=for-the-badge&logo=semantic-release&logoColor=white&labelColor=000F15&logoWidth=20" alt="Version">
14
+ </p>
15
+
16
+ <p align="center">
17
+ <b>Lightweight Visual Page / CMS Editor</b><br>
18
+ <i>Full-page HTML &amp; CSS editing inside an isolated iframe canvas. Zero dependencies.</i>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <img src="https://img.shields.io/badge/Features-30%2B%20Tools-3b82f6?style=flat&labelColor=383C43" />
23
+ <img src="https://img.shields.io/badge/Themes-5%20Built--in-8b5cf6?style=flat&labelColor=383C43" />
24
+ <img src="https://img.shields.io/badge/Setup-Zero%20Config-22c55e?style=flat&labelColor=383C43" />
25
+ <img src="https://img.shields.io/badge/iframe-CSS%20Isolated-f97316?style=flat&labelColor=383C43" />
26
+ </p>
27
+
28
+ ---
29
+
30
+ <p align="center">
31
+ <img src="assets/img/preview.png" alt="Neiki's Page Editor" width="900">
32
+ </p>
33
+
34
+ ---
35
+
36
+ **Live version:** [https://neikiri.dev/page-editor](https://neikiri.dev/page-editor)
37
+
38
+ ---
39
+
40
+ ## Table of Contents
41
+
42
+ - [Overview](#overview)
43
+ - [Why Neiki's Page Editor?](#why-neikis-page-editor)
44
+ - [Features](#features)
45
+ - [Getting started](#getting-started)
46
+ - [Quick Start — CDN](#quick-start--cdn)
47
+ - [Quick Start — npm / ESM](#quick-start--npm--esm)
48
+ - [HTML/CSS Rendering Model](#htmlcss-rendering-model)
49
+ - [Options](#options)
50
+ - [Public API](#public-api)
51
+ - [Loading and Saving (Database Integration)](#loading-and-saving-database-integration)
52
+ - [Plugins](#plugins)
53
+ - [Localization (i18n)](#localization-i18n)
54
+ - [Themes](#themes)
55
+ - [Security and Sanitization](#security-and-sanitization)
56
+ - [Browser Support](#browser-support)
57
+ - [License](#license)
58
+
59
+ ---
60
+
61
+ ## Overview
62
+
63
+ Neiki's Page Editor is a visual page editor written in plain JavaScript with **zero dependencies**. You attach it to any `<div>` and it becomes a full editing surface — toolbar, formatting tools, tables, images, video, source view, and a status bar — all rendered inside an isolated `<iframe>` canvas so the page looks exactly as it would in a real browser tab.
64
+
65
+ ```html
66
+ <div id="editor"></div>
67
+ <script src="https://cdn.neikiri.dev/neiki-page-editor/neiki-page-editor.min.js"></script>
68
+ <script>
69
+ const editor = new NeikiPageEditor('#editor', {
70
+ loadHandler: async () => ({ html: '<p>Start editing.</p>', css: '' }),
71
+ saveHandler: async (payload) => console.log('Saved', payload),
72
+ });
73
+ </script>
74
+ ```
75
+
76
+ That snippet is a complete, working page editor. The minified build bundles its own CSS, so a single `<script>` tag is enough to get started. From there you can configure the toolbar, wire up load/save callbacks, switch themes, and extend behaviour through the plugin API.
77
+
78
+ ---
79
+
80
+ ## Why Neiki's Page Editor?
81
+
82
+ Most CMS editors ask you to make a trade-off: either render inside a plain `<div contenteditable>` and lose CSS fidelity, or build a full page-preview environment and take on a heavy dependency. Neiki's Page Editor avoids that trade-off.
83
+
84
+ - **One file, no dependencies.** The editor ships as a single script. The minified build embeds its CSS, so there is nothing else to install, import, or bundle. Drop it into a static page, a PHP template, or a SPA component — it behaves the same way.
85
+
86
+ - **Real CSS rendering, not a preview stub.** Page content lives inside a sandboxed `<iframe>` with `allow-same-origin`. Your actual page CSS renders with browser-accurate fidelity — fonts, layout, spacing, colours — all exactly as the end user will see them, while the editor chrome stays completely isolated.
87
+
88
+ - **Full-page editing, not just a body fragment.** You can load a complete HTML document — `<link>` stylesheets, `<style>` blocks, body content — and the editor extracts, sanitizes, and wires it all up correctly. Load, edit, and save entire pages through your own async handlers.
89
+
90
+ - **Zero-config by default, configurable when you need it.** `new NeikiPageEditor('#editor')` gives you the full toolbar immediately. Every option is optional, so you only reach for configuration when you actually want to change something.
91
+
92
+ - **Production editing features, not just bold and italic.** Image resize handles, table column resizing, a table context menu, drag-and-drop block reordering, a floating selection toolbar, find & replace with regex, an HTML + CSS source view, autosave, fullscreen, and print are all built in.
93
+
94
+ - **Built-in sanitization.** All HTML entering the editor is sanitized client-side through a DOMParser-based allowlist, and the bundled PHP helper exposes a server-side `sanitize()` method. Security is part of the editor, not an afterthought (see [Security and Sanitization](#security-and-sanitization)).
95
+
96
+ - **CSS isolation guaranteed.** Host page styles never bleed into the canvas. Canvas page styles never bleed into the editor chrome. The iframe sandbox enforces this at the browser level — no hacks, no specificity battles.
97
+
98
+ - **Extensible through a clean plugin API.** Custom toolbar buttons, commands, and translation keys can be registered without touching editor internals. A `destroy()` method makes clean teardown in SPA components straightforward.
99
+
100
+ If you want a page editor that renders real CSS, works from a single file, and integrates cleanly with any backend through load/save callbacks — that is the gap this project fills.
101
+
102
+ ---
103
+
104
+ ## Features
105
+
106
+ - **Accurate rendering** — page HTML and CSS render in an isolated iframe exactly as they would in a real browser tab.
107
+ - **CMS/database-ready** — pages are loaded and saved through developer-configured async callbacks; no backend is assumed.
108
+ - **One-script CDN usage** — a single `<script>` tag is all you need; no build step required for consumers.
109
+ - **Familiar toolbar UX** — layout and controls match Neiki's Editor exactly.
110
+ - **Full-page editing** — load a body fragment or an entire HTML document with `<style>` blocks and external stylesheets.
111
+ - **Zero runtime dependencies** — vanilla JavaScript only.
112
+ - **Five built-in themes** — light, dark, blue, dark-blue, midnight.
113
+ - **English and Czech UI** — with an extensible i18n system.
114
+ - **PHP sanitization helper** — optional server-side complement for database persistence.
115
+
116
+ ---
117
+
118
+ ## Getting started
119
+
120
+ The recommended install is the single bundled script from the CDN. CSS is included automatically.
121
+
122
+ ```html
123
+ <script src="https://cdn.neikiri.dev/neiki-page-editor/neiki-page-editor.min.js"></script>
124
+ ```
125
+
126
+ <details>
127
+ <summary><b>Other installation options</b> (pinned version, separate CSS/JS, jsDelivr, npm, self-hosted)</summary>
128
+
129
+ <br>
130
+
131
+ **Pin a specific version**
132
+
133
+ ```html
134
+ <script src="https://cdn.neikiri.dev/neiki-page-editor/0.1.0/neiki-page-editor.min.js"></script>
135
+ ```
136
+
137
+ **Load CSS and JS separately**
138
+
139
+ ```html
140
+ <!-- Latest -->
141
+ <link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-page-editor/neiki-page-editor.css">
142
+ <script src="https://cdn.neikiri.dev/neiki-page-editor/neiki-page-editor.js"></script>
143
+
144
+ <!-- Or pinned -->
145
+ <link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-page-editor/0.1.0/neiki-page-editor.css">
146
+ <script src="https://cdn.neikiri.dev/neiki-page-editor/0.1.0/neiki-page-editor.js"></script>
147
+ ```
148
+
149
+ **Alternative CDN — jsDelivr**
150
+
151
+ ```html
152
+ <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-page-editor@latest/dist/neiki-page-editor.min.js"></script>
153
+
154
+ <!-- Pinned -->
155
+ <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-page-editor@0.1.0/dist/neiki-page-editor.min.js"></script>
156
+ ```
157
+
158
+ **Package manager**
159
+
160
+ ```bash
161
+ npm install neiki-page-editor
162
+ # or
163
+ yarn add neiki-page-editor
164
+ # or
165
+ pnpm add neiki-page-editor
166
+ ```
167
+
168
+ **Self-hosted**
169
+
170
+ ```html
171
+ <script src="path/to/neiki-page-editor.min.js"></script>
172
+
173
+ <!-- Or separate files -->
174
+ <link rel="stylesheet" href="path/to/neiki-page-editor.css">
175
+ <script src="path/to/neiki-page-editor.js"></script>
176
+ ```
177
+
178
+ </details>
179
+
180
+ > **Note:** When using separate CSS and JS files, load the CSS **before** the JS so the editor renders correctly during initialization.
181
+
182
+ ---
183
+
184
+ ## Quick Start — CDN
185
+
186
+
187
+ The minified CDN build embeds all editor CSS and exposes `window.NeikiPageEditor`.
188
+
189
+ ```html
190
+ <!DOCTYPE html>
191
+ <html lang="en">
192
+ <head>
193
+ <meta charset="UTF-8">
194
+ <title>My CMS</title>
195
+ </head>
196
+ <body>
197
+ <!-- Target element -->
198
+ <div id="editor"></div>
199
+
200
+ <!-- Single script — no extra CSS file needed -->
201
+ <script src="dist/neiki-page-editor.min.js"></script>
202
+ <script>
203
+ const editor = new NeikiPageEditor('#editor', {
204
+ loadHandler: async () => {
205
+ const res = await fetch('/api/page/1');
206
+ return res.json(); // { html, css }
207
+ },
208
+ saveHandler: async (payload) => {
209
+ await fetch('/api/page/1', {
210
+ method: 'PUT',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: JSON.stringify(payload),
213
+ });
214
+ },
215
+ onReady: (editor) => console.log('Editor ready'),
216
+ });
217
+ </script>
218
+ </body>
219
+ </html>
220
+ ```
221
+
222
+ ### Version-pinned CDN URL
223
+
224
+ When distributing over a CDN, pin to a specific version to avoid unexpected breaking changes:
225
+
226
+ ```html
227
+ <script src="https://cdn.example.com/neiki-page-editor/0.1.0/neiki-page-editor.min.js"></script>
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Quick Start — npm / ESM
233
+
234
+ ### Install
235
+
236
+ ```bash
237
+ npm install neiki-page-editor
238
+ ```
239
+
240
+ ### ES Module usage
241
+
242
+ ```js
243
+ import NeikiPageEditor from 'neiki-page-editor';
244
+ // If your bundler doesn't handle CSS imports, also import the CSS separately:
245
+ // import 'neiki-page-editor/css';
246
+
247
+ const editor = new NeikiPageEditor('#editor', {
248
+ theme: 'dark',
249
+ language: 'cs',
250
+ loadHandler: async () => ({
251
+ html: '<p>Hello <strong>world</strong></p>',
252
+ css: 'body { font-family: sans-serif; }',
253
+ }),
254
+ saveHandler: async (payload) => {
255
+ console.log('Saved:', payload);
256
+ },
257
+ });
258
+ ```
259
+
260
+ ### CommonJS / require
261
+
262
+ ```js
263
+ const { NeikiPageEditor } = require('neiki-page-editor');
264
+ ```
265
+
266
+ ### Separate CSS build
267
+
268
+ When using the ESM or CJS build without embedded CSS, load the standalone stylesheet:
269
+
270
+ ```html
271
+ <link rel="stylesheet" href="node_modules/neiki-page-editor/dist/neiki-page-editor.css">
272
+ ```
273
+
274
+ ### package.json exports
275
+
276
+ ```json
277
+ {
278
+ "main": "dist/neiki-page-editor.js",
279
+ "module": "dist/neiki-page-editor.esm.js",
280
+ "exports": {
281
+ ".": { "import": "./dist/neiki-page-editor.esm.js", "require": "./dist/neiki-page-editor.js" },
282
+ "./css": "./dist/neiki-page-editor.css"
283
+ },
284
+ "files": ["dist/", "php/", "README.md"]
285
+ }
286
+ ```
287
+
288
+ ---
289
+
290
+ ## HTML/CSS Rendering Model
291
+
292
+ Understanding how Neiki's Page Editor renders HTML and CSS is essential for accurate CMS integration.
293
+
294
+ ### The iframe Canvas
295
+
296
+ The editor renders page content inside a sandboxed `<iframe>` element:
297
+
298
+ ```html
299
+ <iframe sandbox="allow-same-origin"></iframe>
300
+ ```
301
+
302
+ - `allow-same-origin` is set so the editor can access `contentDocument`, `contenteditable`, and the Selection API.
303
+ - `allow-scripts` is **intentionally omitted** — page JavaScript never executes inside the editor.
304
+
305
+ ### iframe Document Structure
306
+
307
+ The iframe document is written using this template:
308
+
309
+ ```html
310
+ <!DOCTYPE html>
311
+ <html>
312
+ <head>
313
+ <meta charset="UTF-8">
314
+ <base href="[assetsBaseUrl]"> <!-- only when assetsBaseUrl is provided -->
315
+ <style id="npe-base"></style> <!-- minimal editing reset -->
316
+ <!-- validated <link> stylesheet URLs from cssUrls -->
317
+ <style id="npe-page"></style> <!-- page CSS string -->
318
+ <!-- extracted safe <style> blocks from fullHtml -->
319
+ <style id="npe-helper"></style> <!-- non-invasive editing helpers -->
320
+ </head>
321
+ <body contenteditable="true" spellcheck="true">
322
+ <!-- sanitized page HTML -->
323
+ </body>
324
+ </html>
325
+ ```
326
+
327
+ ### CSS Injection Order
328
+
329
+ CSS is injected in this deterministic order (must not be changed):
330
+
331
+ | Order | Source | Element |
332
+ |---|---|---|
333
+ | 1 | Minimal editing reset | `<style id="npe-base">` |
334
+ | 2 | Validated external stylesheets | `<link data-npe-external>` |
335
+ | 3 | Page CSS string | `<style id="npe-page">` |
336
+ | 4 | Extracted `<style>` blocks from `fullHtml` | `<style data-npe-extracted>` |
337
+ | 5 | Non-invasive editing helpers | `<style id="npe-helper">` |
338
+
339
+ This order ensures page-level CSS (3–4) can override framework/library CSS (2) while the editor's own minimal styles (1, 5) never conflict with page content.
340
+
341
+ ### CSS Isolation Rules
342
+
343
+ - **Host → canvas**: Host page CSS **never** affects content inside the iframe canvas.
344
+ - **Canvas → host**: Page CSS loaded into the iframe **never** affects the editor toolbar, modals, or any `.npe-*` element in the host page.
345
+ - **Theme → canvas**: Editor theme CSS **never** forcibly overrides page layout, typography, colors, or component styles inside the iframe.
346
+
347
+ ### Page Loading Flow
348
+
349
+ When you call `setPage({ html, css, cssUrls })` or the `loadHandler` returns a payload:
350
+
351
+ 1. If `fullHtml` is provided, `FullHtmlParser` extracts:
352
+ - `<body>` innerHTML → page HTML
353
+ - `<head><style>` blocks → extracted style blocks
354
+ - `<link rel="stylesheet">` hrefs (validated) → external CSS URLs
355
+ 2. Page HTML is sanitized through the allowlist sanitizer.
356
+ 3. External stylesheet URLs are validated (default: must be `https?://` and end in `.css`).
357
+ 4. The iframe document is written with the template above.
358
+ 5. CSS is injected through `StyleManager` in the correct order.
359
+ 6. The `onReady` callback fires.
360
+
361
+ ### Full HTML Document Loading
362
+
363
+ You can load an entire HTML document (not just a body fragment):
364
+
365
+ ```js
366
+ const editor = new NeikiPageEditor('#editor', {
367
+ loadHandler: async () => ({
368
+ fullHtml: `<!DOCTYPE html>
369
+ <html>
370
+ <head>
371
+ <link rel="stylesheet" href="https://cdn.example.com/theme.css">
372
+ <style>h1 { color: navy; }</style>
373
+ </head>
374
+ <body>
375
+ <h1>Page Title</h1>
376
+ <p>Content</p>
377
+ </body>
378
+ </html>`,
379
+ }),
380
+ });
381
+ ```
382
+
383
+ `FullHtmlParser` extracts the `<body>`, the `<style>` block, and the validated `<link>` URL automatically.
384
+
385
+ ### Editor DOM Layout
386
+
387
+ ```
388
+ target element (#editor)
389
+ └── .npe-editor
390
+ ├── .npe-toolbar ← toolbar (host document)
391
+ ├── .npe-canvas-wrapper
392
+ │ ├── iframe[sandbox="allow-same-origin"]
393
+ │ │ └── html > body[contenteditable] ← page content
394
+ │ └── .npe-overlay-layer ← overlays (host document)
395
+ └── .npe-statusbar
396
+ ```
397
+
398
+ All `.npe-*` class names are prefixed to avoid conflicts with page content.
399
+
400
+ ---
401
+
402
+ ## Options
403
+
404
+ Pass options as the second argument to `new NeikiPageEditor(selector, options)`.
405
+
406
+ ### Content options
407
+
408
+ | Option | Type | Default | Description |
409
+ |---|---|---|---|
410
+ | `initialContent` | `string` | `''` | HTML to load when no `loadHandler` is provided |
411
+ | `pageStyles` | `string` | `''` | CSS string to inject into the iframe on init |
412
+ | `cssUrls` | `string[]` | `[]` | Validated external stylesheet URLs to load |
413
+ | `assetsBaseUrl` | `string` | `''` | Base URL for relative asset paths inside the iframe |
414
+
415
+ ### Layout options
416
+
417
+ | Option | Type | Default | Description |
418
+ |---|---|---|---|
419
+ | `minHeight` | `number` | `300` | Minimum canvas height in px |
420
+ | `maxHeight` | `number\|null` | `null` | Maximum canvas height in px (`null` = unlimited) |
421
+
422
+ ### Editing options
423
+
424
+ | Option | Type | Default | Description |
425
+ |---|---|---|---|
426
+ | `autofocus` | `boolean` | `false` | Focus the canvas on init |
427
+ | `spellcheck` | `boolean` | `true` | Enable browser spellcheck in the canvas |
428
+ | `readonly` | `boolean` | `false` | Start in read-only mode |
429
+ | `editMode` | `'body'\|'regions'` | `'body'` | `'body'`: entire iframe body is editable. `'regions'`: only `[data-npe-editable]` elements are editable |
430
+ | `editableSelector` | `string` | `'[data-npe-editable]'` | CSS selector for editable regions (when `editMode: 'regions'`) |
431
+
432
+ ### UI options
433
+
434
+ | Option | Type | Default | Description |
435
+ |---|---|---|---|
436
+ | `theme` | `string` | `'light'` | Initial theme: `'light'`, `'dark'`, `'blue'`, `'dark-blue'`, `'midnight'` |
437
+ | `persistTheme` | `boolean` | `false` | Persist theme choice in `localStorage` |
438
+ | `language` | `string` | `'en'` | UI language code: `'en'`, `'cs'`, or custom |
439
+ | `translations` | `object` | `{}` | Per-instance translation key overrides |
440
+ | `customClass` | `string\|null` | `null` | Extra CSS class added to the `.npe-editor` shell |
441
+ | `toolbar` | `string[]` | (default) | Override the toolbar items array |
442
+ | `showHelp` | `boolean` | `true` | Show help panel with keyboard shortcuts |
443
+
444
+ ### Security options
445
+
446
+ | Option | Type | Default | Description |
447
+ |---|---|---|---|
448
+ | `allowDataUris` | `boolean` | `false` | Allow safe image/video `data:` URIs in content |
449
+ | `stylesheetUrlValidator` | `(url: string) => boolean` \| `null` | `null` | Custom validator for external stylesheet URLs |
450
+
451
+ ### Upload handlers
452
+
453
+ | Option | Type | Default | Description |
454
+ |---|---|---|---|
455
+ | `imageUploadHandler` | `(file: File) => Promise<string>` \| `null` | `null` | Called with a `File`; resolves to a URL |
456
+ | `videoUploadHandler` | `(file: File) => Promise<string>` \| `null` | `null` | Called with a `File`; resolves to a URL |
457
+
458
+ ### Backend callbacks
459
+
460
+ | Option | Type | Default | Description |
461
+ |---|---|---|---|
462
+ | `loadHandler` | `() => Promise<LoadPayload>` \| `null` | `null` | Async function to load page content |
463
+ | `saveHandler` | `(payload: SavePayload) => Promise<void>` \| `null` | `null` | Async function to persist content |
464
+
465
+ ### Lifecycle callbacks
466
+
467
+ | Option | Type | Default | Description |
468
+ |---|---|---|---|
469
+ | `onReady` | `(editor) => void` | `null` | Fires once the editor and content are ready |
470
+ | `onChange` | `(html: string) => void` | `null` | Fires (debounced) when content changes |
471
+ | `onSave` | `(payload: SavePayload) => void` | `null` | Fires after a successful save |
472
+ | `onFocus` | `() => void` | `null` | Fires when the canvas receives focus |
473
+ | `onBlur` | `() => void` | `null` | Fires when the canvas loses focus |
474
+
475
+ ### Autosave
476
+
477
+ | Option | Type | Default | Description |
478
+ |---|---|---|---|
479
+ | `autosaveKey` | `string\|null` | `null` | `localStorage` key prefix for autosave drafts. If `null`, the editor derives a key from the target element's `id` |
480
+
481
+ ### Default toolbar
482
+
483
+ The default toolbar order (use `'|'` for separators):
484
+
485
+ ```js
486
+ [
487
+ 'viewCode', 'undo', 'redo', 'findReplace', '|',
488
+ 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'code', 'removeFormat', '|',
489
+ 'heading', 'fontFamily', 'fontSize', '|',
490
+ 'foreColor', 'backColor', '|',
491
+ 'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', '|',
492
+ 'indent', 'outdent', '|',
493
+ 'bulletList', 'numberedList', 'blockquote', 'horizontalRule', '|',
494
+ 'insertDropdown', '|',
495
+ 'moreMenu',
496
+ ]
497
+ ```
498
+
499
+ ---
500
+
501
+ ## Public API
502
+
503
+ ### Instance Methods
504
+
505
+ #### Content
506
+
507
+ ```js
508
+ editor.getContent() // → string: sanitized HTML of canvas body
509
+ editor.setContent(html) // set canvas HTML (sanitized before writing)
510
+ editor.getText() // → string: plain text without HTML tags
511
+ editor.isEmpty() // → boolean: true when canvas has no meaningful content
512
+ ```
513
+
514
+ #### Page
515
+
516
+ ```js
517
+ editor.getPage() // → PagePayload: { html, css?, cssUrls?, assetsBaseUrl?, metadata? }
518
+ editor.setPage(payload) // load a full page payload (HTML + CSS + cssUrls)
519
+ editor.getStyles() // → string: current page CSS string
520
+ editor.setStyles(css) // update page CSS (replaces current page CSS only)
521
+ ```
522
+
523
+ #### Lifecycle
524
+
525
+ ```js
526
+ editor.focus() // move focus into the canvas
527
+ editor.blur() // remove focus from the canvas
528
+ editor.enable() // make the canvas editable
529
+ editor.disable() // make the canvas read-only
530
+ editor.triggerSave() // → Promise<void>: call saveHandler with current payload
531
+ editor.destroy() // remove all DOM, listeners, and iframe references (idempotent)
532
+ ```
533
+
534
+ #### UI
535
+
536
+ ```js
537
+ editor.toggleFullscreen() // toggle fullscreen mode on the editor shell
538
+ editor.setTheme(name) // set theme: 'light' | 'dark' | 'blue' | 'dark-blue' | 'midnight'
539
+ editor.toggleTheme() // cycle to the next theme
540
+ editor.getTheme() // → string: current theme name
541
+ ```
542
+
543
+ ### Static Methods
544
+
545
+ ```js
546
+ NeikiPageEditor.registerPlugin(plugin) // register a plugin for all instances
547
+ NeikiPageEditor.getPlugins() // → Plugin[]: all registered plugins
548
+ NeikiPageEditor.addTranslation(lang, keys) // add/extend a language translation map
549
+ ```
550
+
551
+ ### Payload Types
552
+
553
+ ```ts
554
+ interface LoadPayload {
555
+ html?: string // body HTML fragment
556
+ fullHtml?: string // complete HTML document
557
+ css?: string // page CSS string
558
+ cssUrls?: string[] // validated external stylesheet URLs
559
+ assetsBaseUrl?: string // base URL for relative assets
560
+ metadata?: Record<string, unknown> // arbitrary metadata (passed through to getPage/save)
561
+ }
562
+
563
+ interface SavePayload {
564
+ html: string // sanitized canvas HTML
565
+ css?: string // current page CSS
566
+ cssUrls?: string[] // current external stylesheet URLs
567
+ assetsBaseUrl?: string // current assetsBaseUrl
568
+ metadata?: Record<string, unknown> // metadata from last load
569
+ }
570
+
571
+ interface PagePayload {
572
+ html: string
573
+ css?: string
574
+ cssUrls?: string[]
575
+ assetsBaseUrl?: string
576
+ metadata?: Record<string, unknown>
577
+ }
578
+ ```
579
+
580
+ ### Keyboard Shortcuts
581
+
582
+ | Shortcut | Action |
583
+ |---|---|
584
+ | `Ctrl+B` | Bold |
585
+ | `Ctrl+I` | Italic |
586
+ | `Ctrl+U` | Underline |
587
+ | `Ctrl+K` | Insert Link |
588
+ | `Ctrl+S` | Save |
589
+ | `Ctrl+Z` | Undo |
590
+ | `Ctrl+Y` / `Ctrl+Shift+Z` | Redo |
591
+ | `Tab` | Indent (list) / next editable region |
592
+ | `Shift+Tab` | Outdent (list) / previous editable region |
593
+
594
+ ---
595
+
596
+ ## Loading and Saving (Database Integration)
597
+
598
+ ### Loading from a database
599
+
600
+ ```js
601
+ const editor = new NeikiPageEditor('#editor', {
602
+ loadHandler: async () => {
603
+ const page = await myApi.getPage(pageId);
604
+ return {
605
+ html: page.content,
606
+ css: page.styles,
607
+ cssUrls: page.stylesheets,
608
+ metadata: { pageId: page.id, version: page.version },
609
+ };
610
+ },
611
+ });
612
+ ```
613
+
614
+ If `loadHandler` throws (e.g. network error), the editor falls back to `initialContent` or the target element's existing innerHTML.
615
+
616
+ ### Saving to a database
617
+
618
+ ```js
619
+ const editor = new NeikiPageEditor('#editor', {
620
+ saveHandler: async (payload) => {
621
+ await myApi.updatePage(pageId, {
622
+ content: payload.html,
623
+ styles: payload.css,
624
+ stylesheets: payload.cssUrls,
625
+ });
626
+ },
627
+ onSave: (payload) => showNotification('Saved!'),
628
+ });
629
+
630
+ // Programmatic save
631
+ document.querySelector('#save-btn').addEventListener('click', async () => {
632
+ try {
633
+ await editor.triggerSave();
634
+ } catch (err) {
635
+ // saveHandler threw — the editor already shows a toast
636
+ console.error('Save failed:', err);
637
+ }
638
+ });
639
+ ```
640
+
641
+ ### Loading a full HTML page
642
+
643
+ ```js
644
+ const editor = new NeikiPageEditor('#editor', {
645
+ loadHandler: async () => ({
646
+ fullHtml: await fetchPageHtml(pageId),
647
+ // fullHtml is parsed — body HTML, <style> blocks,
648
+ // and <link rel="stylesheet"> hrefs are extracted automatically.
649
+ metadata: { pageId },
650
+ }),
651
+ });
652
+ ```
653
+
654
+ ### External stylesheets with custom validation
655
+
656
+ ```js
657
+ const editor = new NeikiPageEditor('#editor', {
658
+ cssUrls: ['https://cdn.example.com/theme.css'],
659
+ // Allow URLs from your CDN without requiring a .css extension
660
+ stylesheetUrlValidator: (url) => url.startsWith('https://cdn.example.com/'),
661
+ loadHandler: async () => ({ html: '<p>Content</p>' }),
662
+ });
663
+ ```
664
+
665
+ ### `editMode: 'regions'` for full pages
666
+
667
+ Use `'regions'` when the page contains fixed areas (navigation, footer) that should not be editable:
668
+
669
+ ```html
670
+ <nav>Not editable</nav>
671
+ <main>
672
+ <h1 data-npe-editable>Edit this heading</h1>
673
+ <p data-npe-editable>Edit this paragraph</p>
674
+ </main>
675
+ <footer>Not editable</footer>
676
+ ```
677
+
678
+ ```js
679
+ const editor = new NeikiPageEditor('#editor', {
680
+ editMode: 'regions',
681
+ loadHandler: async () => ({ html: fullPageHtml }),
682
+ });
683
+ ```
684
+
685
+ ### Saving HTML with embedded CSS for download/preview
686
+
687
+ ```js
688
+ const page = editor.getPage();
689
+ // page.html — sanitized body HTML
690
+ // page.css — page CSS string
691
+
692
+ // Reconstruct a full document for storage:
693
+ const fullDoc = `<!DOCTYPE html>
694
+ <html><head>
695
+ <meta charset="UTF-8">
696
+ <style>${page.css || ''}</style>
697
+ </head><body>
698
+ ${page.html}
699
+ </body></html>`;
700
+ ```
701
+
702
+ ---
703
+
704
+ ## Plugins
705
+
706
+ Plugins extend the editor with custom toolbar buttons, commands, translations, and lifecycle hooks without modifying editor internals.
707
+
708
+ ### Plugin interface
709
+
710
+ ```ts
711
+ interface Plugin {
712
+ id: string
713
+ init(editor: NeikiPageEditor): void
714
+ destroy?(): void
715
+ }
716
+ ```
717
+
718
+ ### Register a plugin
719
+
720
+ ```js
721
+ NeikiPageEditor.registerPlugin({
722
+ id: 'my-word-count',
723
+ init(editor) {
724
+ // Register a custom translation key
725
+ NeikiPageEditor.addTranslation('en', {
726
+ 'toolbar.myWordCount': 'Word Count',
727
+ });
728
+
729
+ // Listen to content changes
730
+ editor._editor.getBus().on('content:change', () => {
731
+ const count = editor.getText().trim().split(/\s+/).filter(Boolean).length;
732
+ console.log(`Words: ${count}`);
733
+ });
734
+ },
735
+ destroy() {
736
+ // Cleanup (called by destroy())
737
+ },
738
+ });
739
+ ```
740
+
741
+ ### Add a custom toolbar button
742
+
743
+ ```js
744
+ import { ToolbarBuilder } from 'neiki-page-editor/src/toolbar/ToolbarBuilder.js';
745
+
746
+ ToolbarBuilder.registerPluginButton({
747
+ id: 'insertTimestamp',
748
+ label: 'Insert Timestamp',
749
+ icon: '🕐',
750
+ toggle: false,
751
+ action() {
752
+ // Handled via toolbar:command event
753
+ },
754
+ });
755
+
756
+ const editor = new NeikiPageEditor('#editor', {
757
+ toolbar: ['bold', 'italic', '|', 'insertTimestamp'],
758
+ });
759
+
760
+ editor._editor.getBus().on('toolbar:command', (id) => {
761
+ if (id === 'insertTimestamp') {
762
+ const ts = new Date().toLocaleString();
763
+ editor.setContent(editor.getContent() + `<p>${ts}</p>`);
764
+ }
765
+ });
766
+ ```
767
+
768
+ ---
769
+
770
+ ## Localization (i18n)
771
+
772
+ ### Built-in languages
773
+
774
+ - `en` — English (default)
775
+ - `cs` — Czech
776
+
777
+ ### Setting the language
778
+
779
+ ```js
780
+ const editor = new NeikiPageEditor('#editor', { language: 'cs' });
781
+ ```
782
+
783
+ ### Per-instance translation overrides
784
+
785
+ ```js
786
+ const editor = new NeikiPageEditor('#editor', {
787
+ translations: {
788
+ 'toolbar.bold': 'BOLD',
789
+ 'menu.more.save': 'Publish',
790
+ },
791
+ });
792
+ ```
793
+
794
+ ### Adding a new language
795
+
796
+ ```js
797
+ NeikiPageEditor.addTranslation('de', {
798
+ 'toolbar.bold': 'Fett',
799
+ 'toolbar.italic': 'Kursiv',
800
+ 'toolbar.underline': 'Unterstrichen',
801
+ 'menu.more.save': 'Speichern',
802
+ // ... all required keys
803
+ });
804
+
805
+ const editor = new NeikiPageEditor('#editor', { language: 'de' });
806
+ ```
807
+
808
+ Translation lookup chain: **custom per-instance overrides → registered language → English → key itself**.
809
+
810
+ ### Translation key reference
811
+
812
+ Keys follow the flat dot-separated style:
813
+
814
+ ```
815
+ toolbar.bold toolbar.italic toolbar.underline
816
+ toolbar.heading toolbar.fontFamily toolbar.fontSize
817
+ toolbar.foreColor toolbar.backColor
818
+ toolbar.insertDropdown toolbar.moreMenu
819
+ modal.link.title modal.link.url modal.link.text
820
+ modal.image.title modal.video.title modal.table.title
821
+ statusbar.words statusbar.characters statusbar.block
822
+ menu.more.save menu.more.preview menu.more.changeTheme
823
+ error.saveFailed error.loadFailed error.uploadFailed
824
+ ```
825
+
826
+ ---
827
+
828
+ ## Themes
829
+
830
+ Five built-in themes affect the editor chrome (toolbar, status bar, modals, dropdowns) only. Page content inside the iframe is **never** forcibly re-themed.
831
+
832
+ | Theme name | Description | CSS class on `.npe-editor` |
833
+ |---|---|---|
834
+ | `light` | Default light theme | _(none)_ |
835
+ | `dark` | Dark toolbar and chrome | `npe-dark` |
836
+ | `blue` | Blue accent toolbar | `npe-theme-blue` |
837
+ | `dark-blue` | Dark with blue accents | `npe-theme-dark-blue` |
838
+ | `midnight` | Deep dark theme | `npe-theme-midnight` |
839
+
840
+ ### Switching themes programmatically
841
+
842
+ ```js
843
+ editor.setTheme('dark');
844
+ editor.toggleTheme(); // cycles: light → dark → blue → dark-blue → midnight → light
845
+ editor.getTheme(); // → 'dark'
846
+ ```
847
+
848
+ ### Persisting theme preference
849
+
850
+ ```js
851
+ const editor = new NeikiPageEditor('#editor', {
852
+ persistTheme: true, // saves theme to localStorage
853
+ theme: 'dark', // initial theme (overridden by localStorage if persistTheme is true)
854
+ });
855
+ ```
856
+
857
+ ### Custom theme CSS
858
+
859
+ Override theme CSS custom properties on `.npe-editor`:
860
+
861
+ ```css
862
+ .npe-editor {
863
+ --npe-color-bg: #1e1e1e;
864
+ --npe-color-toolbar: #252526;
865
+ --npe-color-text: #d4d4d4;
866
+ --npe-color-accent: #0078d4;
867
+ --npe-color-border: #3c3c3c;
868
+ }
869
+ ```
870
+
871
+ ---
872
+
873
+ ## Security and Sanitization
874
+
875
+ ### Client-side sanitizer
876
+
877
+ All HTML entering the editor through any path is sanitized by a DOMParser-based allowlist sanitizer before rendering:
878
+
879
+ - Allowed structural tags: `div`, `section`, `article`, `main`, `header`, `footer`, `nav`, `aside`, `figure`, `figcaption`, `p`, `h1`–`h6`, `blockquote`, `pre`, `hr`, `br`
880
+ - Allowed inline tags: `span`, `strong`, `em`, `u`, `s`, `sub`, `sup`, `code`, `a`
881
+ - Allowed media tags: `img`, `video`
882
+ - Allowed list tags: `ul`, `ol`, `li`
883
+ - Allowed table tags: `table`, `thead`, `tbody`, `tfoot`, `tr`, `th`, `td`
884
+
885
+ Blocked unconditionally:
886
+ - Executable tags: `script`, `iframe`, `object`, `embed`
887
+ - Form tags: `form`, `input`, `button`, `select`, `textarea`
888
+ - Metadata tags: `meta`, `base`, `link`, `style`
889
+ - All `on*` event attributes
890
+ - `javascript:` and `vbscript:` URLs
891
+ - `data:` URIs (by default)
892
+
893
+ ### Allowing media data URIs
894
+
895
+ Data URIs are blocked by default. Enable them only for uploaded images/videos when no upload handler is available:
896
+
897
+ ```js
898
+ const editor = new NeikiPageEditor('#editor', {
899
+ allowDataUris: true,
900
+ });
901
+ ```
902
+
903
+ Even with `allowDataUris: true`:
904
+ - Only `img[src]` and `video[src]` accept `data:` URIs.
905
+ - Allowed MIME types: `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/avif`, `video/mp4`, `video/webm`.
906
+ - SVG data URIs (`image/svg+xml`) are **never** allowed.
907
+ - `data:` in `href`, `poster`, or any other attribute is always blocked.
908
+
909
+ ### External stylesheet validation
910
+
911
+ External stylesheet URLs (from `cssUrls` or parsed from `fullHtml`) are validated before injection. Default validator requires:
912
+ - HTTP or HTTPS protocol
913
+ - Path ending in `.css`
914
+
915
+ Provide a custom validator for other URL patterns:
916
+
917
+ ```js
918
+ const editor = new NeikiPageEditor('#editor', {
919
+ stylesheetUrlValidator: (url) =>
920
+ url.startsWith('https://assets.mycompany.com/'),
921
+ });
922
+ ```
923
+
924
+ ### Server-side sanitization (required)
925
+
926
+ The client-side sanitizer is a UX safeguard, not a security boundary. **Always sanitize HTML server-side before persisting to the database.**
927
+
928
+ The optional PHP helper provides a server-side complement:
929
+
930
+ ```php
931
+ require_once 'vendor/neiki-page-editor/php/NeikiPageEditorSanitizer.php';
932
+
933
+ // In your save handler:
934
+ $safe = NeikiPageEditorSanitizer::sanitize($_POST['html']);
935
+ $db->updatePage($pageId, ['content' => $safe]);
936
+ ```
937
+
938
+ The PHP sanitizer uses the same allowlist as the JavaScript sanitizer and requires no Composer dependencies — it uses PHP's built-in `DOMDocument`.
939
+
940
+ ### CSP compatibility
941
+
942
+ The editor does not require `unsafe-eval` or `unsafe-inline` in the host page's Content-Security-Policy. Editor CSS is injected into the iframe document only. Inline styles on page content elements are page content, not editor infrastructure.
943
+
944
+ ---
945
+
946
+ ## Browser Support
947
+
948
+ | Browser | Support |
949
+ |---|---|
950
+ | Chrome (latest stable) | ✅ Full support |
951
+ | Firefox (latest stable) | ✅ Full support |
952
+ | Safari (latest stable) | ✅ Full support |
953
+ | Edge (latest stable) | ✅ Full support |
954
+ | Internet Explorer | ❌ Not supported |
955
+
956
+ The editor targets ES2017+ (`async/await`, `class`, `const/let`, template literals, `Map`, `Set`). No polyfills are bundled.
957
+
958
+ ---
959
+
960
+ ## Development
961
+
962
+ ```bash
963
+ # Install dev dependencies
964
+ npm install
965
+
966
+ # Build all dist files
967
+ npm run build
968
+
969
+ # Run unit tests
970
+ npm test
971
+
972
+ # Run property-based tests
973
+ npm run test:property
974
+
975
+ # Run integration tests
976
+ npm run test:integration
977
+
978
+ # Run all tests
979
+ npm run test:all
980
+ ```
981
+
982
+ ### Build outputs
983
+
984
+ | File | Size (gzip) | Purpose |
985
+ |---|---|---|
986
+ | `dist/neiki-page-editor.min.js` | ~165 KB raw | CDN build; embeds CSS; exposes `window.NeikiPageEditor` |
987
+ | `dist/neiki-page-editor.js` | ~271 KB raw | Unminified UMD build |
988
+ | `dist/neiki-page-editor.esm.js` | ~270 KB raw | ES module for bundlers |
989
+ | `dist/neiki-page-editor.css` | ~34 KB raw | Standalone editor CSS |
990
+
991
+ ---
992
+
993
+ ## License
994
+
995
+ [Source Available](LICENSE) — see the LICENSE file for terms.
996
+
997
+ This project uses a custom source-available license. The source code is publicly visible for reference and non-commercial use. Commercial use, redistribution, or incorporation into a product requires a separate commercial license. Contact [neikiri.dev](https://neikiri.dev) for details.