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/LICENSE +511 -0
- package/README.md +997 -0
- package/dist/neiki-page-editor.css +1580 -0
- package/dist/neiki-page-editor.esm.js +10545 -0
- package/dist/neiki-page-editor.js +10564 -0
- package/dist/neiki-page-editor.min.js +1637 -0
- package/package.json +48 -0
- package/php/NeikiPageEditorSanitizer.php +24 -0
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 & 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.
|