tosijs-styled-editor 0.4.3 → 0.4.4
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/CHANGELOG.md +275 -0
- package/README.md +61 -6
- package/dist/commands.d.ts +11 -1
- package/dist/dom-utils.d.ts +24 -0
- package/dist/index.js +5 -5
- package/dist/module.js +170 -34
- package/dist/tosijs-styled-editor.d.ts +58 -2
- package/dist/version.d.ts +1 -1
- package/package.json +3 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.4] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
First release since 0.4.3 to reach npm. 0.4.4 and 0.4.5 were versioned in the
|
|
13
|
+
repo during development and **never published**, so this is numbered 0.4.4:
|
|
14
|
+
semver describes what consumers observe between releases, and consumers
|
|
15
|
+
observed none of it.
|
|
16
|
+
|
|
17
|
+
Everything below through the 0.2.0 heading shipped across 0.4.2–0.4.4. The
|
|
18
|
+
earlier 0.4.x releases went out without changelog sections of their own, so
|
|
19
|
+
they are collected here rather than reconstructed inaccurately.
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- **Pasted and dropped HTML is now sanitized** before it enters the document.
|
|
24
|
+
The editor replaced `contentEditable` but not the sanitization the browser
|
|
25
|
+
was doing on its behalf: clipboard and drop HTML went in through `innerHTML`
|
|
26
|
+
verbatim, so `<img onerror>`, `<svg onload>`, `javascript:` URLs and
|
|
27
|
+
`<script>` reached the live document — and from there `value`,
|
|
28
|
+
`internals.setFormValue` and every undo snapshot, meaning a host storing
|
|
29
|
+
`value` stored the payload. Handlers, executing elements and unsafe URL
|
|
30
|
+
schemes are now stripped at the single shared paste/drop choke point.
|
|
31
|
+
Ordinary formatting and unregistered custom elements are preserved.
|
|
32
|
+
`editor.sanitize` is a swappable hook if you would rather supply your own
|
|
33
|
+
(DOMPurify drops in; see the README).
|
|
34
|
+
*This path was unreachable in 0.4.2–0.4.3 only because `insertionPoint()` was
|
|
35
|
+
broken; fixing that is what made it live again.*
|
|
36
|
+
- **Ctrl/Cmd-clicking a link checks the URL scheme** and always opens a new
|
|
37
|
+
context. `javascript:` executes in the embedding page's origin and `noopener`
|
|
38
|
+
does not prevent it; `_self`/`_top` are resolved before `noopener` is
|
|
39
|
+
consulted, so a document-supplied `target` could run it same-origin.
|
|
40
|
+
`setLink` validates the scheme too.
|
|
41
|
+
- **Commands built from runtime values no longer go through the string form.**
|
|
42
|
+
`executeCommand` splits on `;`, and a data URI contains `;` by spec — so
|
|
43
|
+
every dropped image produced `<img src="data:image/png">` plus a bogus second
|
|
44
|
+
command, and a crafted filename could inject one. `doCommandWith(name, ...args)`
|
|
45
|
+
passes arguments without parsing.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Mobile browsers no longer zoom when you select text.** Two independent
|
|
50
|
+
causes: the caret is a real `<input>` (that is what raises the mobile
|
|
51
|
+
keyboard) and inherited the UA default form-control font size of 11px, and
|
|
52
|
+
iOS Safari zooms the page on focus below 16px; and double-tap selects a word
|
|
53
|
+
here, which a touch browser reads as zoom. Fixed by sizing the caret at 16px
|
|
54
|
+
and setting `touch-action: manipulation` on the document, which keeps panning
|
|
55
|
+
and pinch-zoom. Deliberately NOT fixed with `user-scalable=no`, which would
|
|
56
|
+
fail WCAG 1.4.4.
|
|
57
|
+
- **The caret no longer strands itself when the document scrolls.** The overlay
|
|
58
|
+
is positioned in viewport coordinates but only repainted when the selection
|
|
59
|
+
bounds changed, so scrolling left it where the text used to be. Scroll, window
|
|
60
|
+
resize and a `ResizeObserver` on the document now repaint it; measured drift is
|
|
61
|
+
0 at every scroll offset, and it hides correctly once its line scrolls out of
|
|
62
|
+
view.
|
|
63
|
+
- **Commands that insert at the caret worked again.** `insertionPoint()` selected
|
|
64
|
+
`input.caret`, which has matched nothing since the bound markers stopped being
|
|
65
|
+
`<input>` elements — so `insertFootnote`, `insertTable`, `insertImage` and
|
|
66
|
+
`setLink` all bailed out silently. Its unit test passed throughout because it
|
|
67
|
+
built the `<input>` itself and asserted the selector found it; the test now
|
|
68
|
+
uses the real `createBounds()`.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- Both `setTimeout` calls replaced with the signals they were approximating: a
|
|
73
|
+
`ResizeObserver` tracks the touch-affordance padding transition continuously
|
|
74
|
+
instead of waiting a guessed 160ms (and no longer leaves the affordances
|
|
75
|
+
invisible if the transition never runs), and the touch menu's dismiss handler
|
|
76
|
+
ignores the event that opened it by identity instead of deferring its own
|
|
77
|
+
subscription.
|
|
78
|
+
- The drop-in `dist/index.js` build is minified: 83.5kB → 70.8kB gzipped.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **Drag and drop editing.** Selected text is a real draggable object, offering
|
|
84
|
+
`text/html` and `text/plain` so the receiver picks — which means dragging works
|
|
85
|
+
between windows, between browsers, and to and from the desktop. Move within
|
|
86
|
+
the editor, Alt to copy, and leaving the editor is always a copy. Dropped image
|
|
87
|
+
files come in as data URIs; dropped HTML runs through the same `pastemode`
|
|
88
|
+
path as a paste.
|
|
89
|
+
|
|
90
|
+
- **Links**: `setLink <url> [target]` and `removeLink`. Defaults to
|
|
91
|
+
`target="_blank"` with `rel="noopener"`. Clicking a link in the editor places
|
|
92
|
+
the caret; Ctrl/Cmd-click follows it.
|
|
93
|
+
- **Images**: `insertImage <url> [alt…]`. An `<img>` is already a leaf node, so
|
|
94
|
+
selection and deletion treat it as one thing with no special casing.
|
|
95
|
+
- **Footnotes**: `insertFootnote [text…]` and `renumberFootnotes`. Numbers are
|
|
96
|
+
derived from document order rather than stored, so inserting in the middle
|
|
97
|
+
renumbers the rest and reorders the list. Note that renumbering happens at
|
|
98
|
+
INSERTION time only — deleting a reference with Backspace currently leaves its
|
|
99
|
+
entry orphaned in the list and does not renumber the survivors. Tracked in
|
|
100
|
+
`TODO.md`; `EXTENSIBILITY.md` covers why the fix is a lifecycle change rather
|
|
101
|
+
than another call to `renumberFootnotes`.
|
|
102
|
+
- An **Insert** menu carrying all three.
|
|
103
|
+
|
|
104
|
+
- **Localization**, following tosijs-ui's conventions rather than a private
|
|
105
|
+
scheme: `localized` on the element translates the built-in widgets and adds a
|
|
106
|
+
flag-only language picker. Toolbar buttons carry `data-tosi-localized` (a JSON
|
|
107
|
+
attribute-to-key map, re-applied on locale change), menus set `localized`, and
|
|
108
|
+
menu labels are `<tosi-localized>`, so custom widgets get the same treatment.
|
|
109
|
+
- `localized-strings.tsv` — a sample table in English and Suomi covering all 51
|
|
110
|
+
UI strings. Adding a language is adding a column. Column 0 is both the lookup
|
|
111
|
+
key and the English text, so missing cells fall back to English and a
|
|
112
|
+
half-translated column is safe to ship.
|
|
113
|
+
- README is pinned to the top of the doc-site nav.
|
|
114
|
+
- A Right-to-Left doc page with live examples: RTL blocks in Arabic and Hebrew,
|
|
115
|
+
LTR-with-embedded-RTL, and RTL blocks with embedded LTR runs (inline code,
|
|
116
|
+
URLs, version numbers) — the cases where visual and logical order disagree and
|
|
117
|
+
a DOM-only selection has to earn its keep.
|
|
118
|
+
- Menu dropdowns are compacted (30px rows, 16px horizontal padding) and scoped
|
|
119
|
+
to this editor's own menus.
|
|
120
|
+
|
|
121
|
+
- `setList ul | ol | none` — bulleted and numbered list formatting, exposed as
|
|
122
|
+
toolbar buttons and Style menu entries. Adjacent selected blocks become one
|
|
123
|
+
list, a converted block merges into an adjacent list of the same type (so
|
|
124
|
+
`<ol>` numbering continues), and re-applying the current type toggles it off.
|
|
125
|
+
- Live behaviour tests that run in a real browser, covering the click
|
|
126
|
+
positioning that happy-dom cannot see.
|
|
127
|
+
- `llms.txt`, a sitemap, and prerendered doc pages, generated by the doc system.
|
|
128
|
+
- `.haltija.json` pinning agent browser commands to this project's dev origin.
|
|
129
|
+
|
|
130
|
+
### Fixed
|
|
131
|
+
|
|
132
|
+
- **The caret no longer reshapes the text it sits in.** It was an `<input>`
|
|
133
|
+
between characters, and a replaced element breaks the shaping run: measured on
|
|
134
|
+
Arabic, a neighbouring glyph's advance moved 6.2 → 6.7 even though the caret's
|
|
135
|
+
box was already width-neutral, and absolute positioning did not rescue it. The
|
|
136
|
+
in-text anchors are now plain spans — bit-identical to no markup at all — and
|
|
137
|
+
the focusable caret is positioned over the text instead of inside it. Placing
|
|
138
|
+
the caret in an Arabic paragraph now leaves its width unchanged at 565.7px.
|
|
139
|
+
A collapsed selection shows an ordinary caret; an expanded one keeps its two
|
|
140
|
+
edges distinguishable.
|
|
141
|
+
|
|
142
|
+
- **Arabic text jittered when you moused over it.** Not a shaping problem, as it
|
|
143
|
+
appeared: spanification preserves shaping, ligatures and per-glyph positions
|
|
144
|
+
exactly (measured). The cause was wrapping each SPACE in its own span, which
|
|
145
|
+
changes which spaces CSS collapses — the same number collapse, but not the
|
|
146
|
+
same ones, so words merge and gaps open mid-word. Whitespace now stays a text
|
|
147
|
+
node, which reproduces the original layout space for space.
|
|
148
|
+
|
|
149
|
+
- **Live examples now fill their preview instead of taking a fixed height.**
|
|
150
|
+
`tosi-example` is `height: var(--tosi-example-height)` (320px) and becomes
|
|
151
|
+
`100vh` when maximized, so the editor's hardcoded 340px both overflowed the
|
|
152
|
+
normal case — the EXAMPLE scrolled rather than the document — and ignored the
|
|
153
|
+
space when maximized. The sizing and the preview's padding reset now live in
|
|
154
|
+
the site config's `headExtra`, so every doc page gets them, including the RTL
|
|
155
|
+
page which had no CSS block of its own.
|
|
156
|
+
|
|
157
|
+
- **The selection was unreadable in dark mode.** `.selected` was a hardcoded
|
|
158
|
+
`rgba(0,0,255,0.3)` and `.selected-block` a hardcoded `#ddf` — a pale blue
|
|
159
|
+
that light text disappears into — and the caret/bounds were `background: black`,
|
|
160
|
+
invisible on a dark page. All three now derive from `--editor-ink` mixed into
|
|
161
|
+
`--editor-surface` (and `currentColor` for the bounds), so they tint whichever
|
|
162
|
+
way the page is themed. The band is lifted toward white BEFORE the ink is
|
|
163
|
+
mixed in — nearly a no-op on an already-white page, but it raises a dark one
|
|
164
|
+
clear of the background, which a plain ink-into-surface mix cannot do because
|
|
165
|
+
the surface dominates. Dark is deliberately given MORE measured separation
|
|
166
|
+
than light, because light-on-dark halates and reads as less contrast at the
|
|
167
|
+
same numbers. Measured light 0.75 against a 0.99 page (separation 0.24), dark
|
|
168
|
+
0.44 against 0.11 (separation 0.33).
|
|
169
|
+
|
|
170
|
+
- **Emoji were torn in half.** `spanify` split text with `split('')`, which
|
|
171
|
+
splits by UTF-16 code UNIT, so an emoji's surrogate pair became two lone
|
|
172
|
+
surrogates rendering as `?`. Splitting is now by grapheme cluster via
|
|
173
|
+
`Intl.Segmenter`, which also keeps flags (two regional indicators), skin-tone
|
|
174
|
+
modifiers, ZWJ sequences and combining marks whole — every one of those is one
|
|
175
|
+
thing a user clicks on or deletes.
|
|
176
|
+
- **Dark mode: the document was black text on a near-black page.** The surface
|
|
177
|
+
followed the page theme but `color` was the system `CanvasText`, which does
|
|
178
|
+
not. Text and surface are now a paired `--editor-text` / `--editor-surface`,
|
|
179
|
+
and a consumer that themes one must theme both.
|
|
180
|
+
- Added **Cut** to the touch selection menu, which had Copy and Paste but no Cut.
|
|
181
|
+
|
|
182
|
+
- **The caret sat on the wrong side of the line when typing LTR into an RTL
|
|
183
|
+
block** (and vice versa). The caret is an element, and bidi treats an empty
|
|
184
|
+
inline as a NEUTRAL, so it resolved against the block's base direction instead
|
|
185
|
+
of the run being typed. Typing across a direction boundary now wraps the run
|
|
186
|
+
and the caret in a `<span dir>` isolate, extending one isolate rather than
|
|
187
|
+
creating one per keystroke. Neutral characters take whichever run they land in.
|
|
188
|
+
|
|
189
|
+
- **Typing over a selection deleted it and inserted nothing.** A regression from
|
|
190
|
+
the double-click fix: `resetBounds()` derives bounds from `.selected`, which
|
|
191
|
+
lands the caret INSIDE the last selected character, and `deleteSelection()`
|
|
192
|
+
then removed the caret along with the selection — leaving no insertion point,
|
|
193
|
+
so every keystroke was silently swallowed. The caret is now moved out of the
|
|
194
|
+
way before the selected chains are deleted.
|
|
195
|
+
|
|
196
|
+
- Left-to-right runs inside right-to-left paragraphs — `<code>`, `<kbd>`,
|
|
197
|
+
`<samp>` — inherited the paragraph's base direction, so a URL's slashes or a
|
|
198
|
+
trailing period resolved to the wrong end. They now get their own
|
|
199
|
+
`direction: ltr; unicode-bidi: isolate`, which the new RTL page surfaced.
|
|
200
|
+
|
|
201
|
+
- Double-clicking left the caret blinking where the click landed instead of at
|
|
202
|
+
the end of the selected word: word and block gestures expanded the marked
|
|
203
|
+
range without moving the `.sel-start`/`.sel-end` elements.
|
|
204
|
+
- Clicking in the dead space to the right of a line did nothing, and
|
|
205
|
+
double-clicking there selected a word around stale bounds. Click position now
|
|
206
|
+
resolves to the nearest character on the clicked line.
|
|
207
|
+
- `editable.commands` was never consulted — `executeCommand` resolved names
|
|
208
|
+
against the module-level registry, so the documented way to add a custom
|
|
209
|
+
command had no effect. The registry now travels on `EditableContext`.
|
|
210
|
+
- README and the component doc comment advertised `<tosi-editable>`, which was
|
|
211
|
+
never the registered tag. It is `<tosijs-styled-editor>`.
|
|
212
|
+
|
|
213
|
+
### Changed
|
|
214
|
+
|
|
215
|
+
- The Highlight button now uses Lucide's `highlighter` icon, registered through
|
|
216
|
+
`defineIcons`. The previous `penTool` read as a fountain pen — a different
|
|
217
|
+
tool. Stored following tosijs-ui's own convention: no `xmlns`, `width`,
|
|
218
|
+
`height`, `fill`, `stroke` or `stroke-*`, since the host supplies all of that
|
|
219
|
+
from `--tosi-icon-*` and a hardwired stroke would ignore the current colour.
|
|
220
|
+
|
|
221
|
+
- **Renamed to one name everywhere: `tosijs-styled-editor`.** The element is now
|
|
222
|
+
`<tosijs-styled-editor>` (was `<tosi-styled-editor>`), the class is
|
|
223
|
+
`TosijsStyledEditor`, the creator is `tosijsStyledEditor()`, and the source is
|
|
224
|
+
`src/tosijs-styled-editor.ts`. Only the repo directory stays `tosijs-editor`.
|
|
225
|
+
Breaking, and free to take now because the package is unpublished.
|
|
226
|
+
- Doc pages have distinct titles — "A Rich Text Editor Component" (README) and
|
|
227
|
+
"Editor Component" — so the site nav no longer shows two near-identical entries.
|
|
228
|
+
- Site icon, header mark and social image now use `static/tosijs-editor.svg`.
|
|
229
|
+
- Chrome restyled around a pen-ink-blue accent (`#27488c`), shared by the
|
|
230
|
+
component and the doc site. The menubar, toolbar and document now read as
|
|
231
|
+
three distinct surfaces, mixed from a single `--editor-ink` custom property so
|
|
232
|
+
a consumer can re-theme the whole thing by setting one value. Toolbar buttons
|
|
233
|
+
are compact 26px squares, styled by the component rather than left to each
|
|
234
|
+
consumer to re-invent. Those styles ship as `lightStyleSpec`, not
|
|
235
|
+
`::slotted()`: slotted content is light DOM, so the host page's own `button`
|
|
236
|
+
rules win the cascade — which had left the buttons as white chips on the
|
|
237
|
+
tinted bars.
|
|
238
|
+
|
|
239
|
+
- **Build**: replaced the bespoke `dev.ts` with `bin/site.ts`, a thin wrapper
|
|
240
|
+
over tosijs-ui's doc system (`buildSite`/`devServer`). The full build is
|
|
241
|
+
`bun run make` — there is deliberately no `build` script, because `bun build`
|
|
242
|
+
is a Bun builtin and the two would differ.
|
|
243
|
+
- Bundling moved out of the long-lived watch process into child processes;
|
|
244
|
+
Bun's bundler never returns its native arena (oven-sh/bun#34053).
|
|
245
|
+
- `docs/` is now the generated Pages web root and is committed.
|
|
246
|
+
- Peer floors raised to `tosijs ^1.10.1` / `tosijs-ui ^1.13.0`.
|
|
247
|
+
- Migrated off `elementCreator({ tag })` and `static styleSpec`, both deprecated
|
|
248
|
+
in the upgrade, to `static preferredTagName` and `static shadowStyleSpec`.
|
|
249
|
+
|
|
250
|
+
### Security
|
|
251
|
+
|
|
252
|
+
- `happy-dom` → 20.14.0 (GHSA-w4gp-fjgq-3q4g, GHSA-6q6h-j7hj-3r64) and a minimal
|
|
253
|
+
`ws` override → `^8.21.0` (GHSA-96hv-2xvq-fx4p), both surfaced by the build's
|
|
254
|
+
dependency audit gate.
|
|
255
|
+
- `tls/` is gitignored as an allowlist so a dev TLS private key cannot be staged.
|
|
256
|
+
|
|
257
|
+
### Removed
|
|
258
|
+
|
|
259
|
+
- The legacy jQuery implementation (`edx-*.js`, `lib/jquery-2.1.4.js`, and the
|
|
260
|
+
orphaned HTML pages) — 336K that nothing in the build referenced. Recover from
|
|
261
|
+
git history if ever needed: `git show 298bf16 -- edx-editable.js`.
|
|
262
|
+
|
|
263
|
+
## [0.2.0]
|
|
264
|
+
|
|
265
|
+
### Added
|
|
266
|
+
|
|
267
|
+
- Touch selection affordances and context menu; component renamed to
|
|
268
|
+
`<tosijs-styled-editor>`.
|
|
269
|
+
|
|
270
|
+
## [0.1.0]
|
|
271
|
+
|
|
272
|
+
### Added
|
|
273
|
+
|
|
274
|
+
- Initial rewrite as a tosijs web component: custom selection via spanification,
|
|
275
|
+
command-based editing, grid tables, and toolbar/menubar factories.
|
package/README.md
CHANGED
|
@@ -27,7 +27,11 @@ A pure web-component. What it does **not** use:
|
|
|
27
27
|
|
|
28
28
|
- No `document.execCommand`
|
|
29
29
|
- No `contentEditable`
|
|
30
|
-
- No
|
|
30
|
+
- No `getSelection`, no browser selection model, no `execCommand`-era APIs
|
|
31
|
+
|
|
32
|
+
Range is used, but only as a **measuring tape** — `getBoundingClientRect()` to
|
|
33
|
+
ask the layout engine where a character is. It is never a selection model, and
|
|
34
|
+
nothing is handed back to the browser to edit.
|
|
31
35
|
|
|
32
36
|
What you get instead:
|
|
33
37
|
|
|
@@ -110,11 +114,62 @@ for (const widget of defaultToolbar()) {
|
|
|
110
114
|
The editor uses three layers:
|
|
111
115
|
|
|
112
116
|
1. **DOM utilities** (`dom-utils.ts`) — leaf-node traversal; nearly all operations work with leaf nodes
|
|
113
|
-
2. **Selection** (`selection.ts`) — custom selection
|
|
117
|
+
2. **Selection** (`selection.ts`) — a custom selection model. Character positions are found by MEASURING with a Range, which does not touch the document; wrapping characters in spans to measure them changes the thing being measured (it breaks shaping, so cursive scripts come apart and lines re-wrap)
|
|
114
118
|
3. **Commands** (`commands.ts`) — extensible command system for formatting and editing
|
|
115
119
|
|
|
116
120
|
The caret is an `<input>` element, so mobile browsers show their keyboard automatically.
|
|
117
121
|
|
|
122
|
+
## Security: what is sanitized, and what is not
|
|
123
|
+
|
|
124
|
+
Replacing `contentEditable` also means replacing the sanitization the browser
|
|
125
|
+
was doing on your behalf. As of 0.4.4:
|
|
126
|
+
|
|
127
|
+
**Sanitized** — content arriving from outside the document, which is the path an
|
|
128
|
+
attacker controls:
|
|
129
|
+
|
|
130
|
+
- **paste** and **drop** (both go through one shared choke point)
|
|
131
|
+
- inline event handlers (`onerror`, `onload`, …) are removed
|
|
132
|
+
- `script`, `iframe`, `object`, `embed`, `link`, `meta`, `base`, `style`,
|
|
133
|
+
`form` and the SVG animation elements are removed — in **any** namespace, so
|
|
134
|
+
`<svg><script>` and `<svg><style>` are caught too
|
|
135
|
+
- `href`/`src`/`xlink:href` are scheme-checked: `http(s)`, `mailto`, `tel` and
|
|
136
|
+
relative URLs are kept, `javascript:` is dropped, and `data:` is allowed only
|
|
137
|
+
for raster images (never for a link, never `data:image/svg+xml`)
|
|
138
|
+
- ordinary formatting and **unregistered custom elements survive** — plugin
|
|
139
|
+
markup is content, not a threat
|
|
140
|
+
|
|
141
|
+
**NOT sanitized** — content you supply, which is inside your own trust boundary:
|
|
142
|
+
|
|
143
|
+
- `editor.value = html`
|
|
144
|
+
- initial light-DOM content
|
|
145
|
+
|
|
146
|
+
**Using a different sanitizer.** `editor.sanitize` is the hook — it receives a
|
|
147
|
+
detached element and mutates it:
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
editor.sanitize = (root) => {
|
|
151
|
+
DOMPurify.sanitize(root, {
|
|
152
|
+
IN_PLACE: true,
|
|
153
|
+
FORBID_TAGS: ['style'],
|
|
154
|
+
CUSTOM_ELEMENT_HANDLING: {
|
|
155
|
+
tagNameCheck: /^[a-z][a-z0-9]*-[a-z0-9-]*$/,
|
|
156
|
+
attributeNameCheck: /^data-|^slot$|^dir$/,
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
It takes an element rather than an HTML string on purpose: a string signature
|
|
163
|
+
would force a serialize-and-reparse round trip, and that round trip is where
|
|
164
|
+
mutation XSS lives. Note the `CUSTOM_ELEMENT_HANDLING` block — DOMPurify
|
|
165
|
+
unwraps unknown custom elements by default, which would discard plugin markup
|
|
166
|
+
the built-in sanitizer preserves.
|
|
167
|
+
|
|
168
|
+
**If you are upgrading from 0.4.3 or earlier, read this:** documents your users
|
|
169
|
+
created before 0.4.4 may already contain a payload that was pasted in, and the
|
|
170
|
+
component cannot fix that for you — setting `value` does not filter. Sanitize
|
|
171
|
+
your stored corpus as part of the upgrade.
|
|
172
|
+
|
|
118
173
|
## Keyboard Behavior
|
|
119
174
|
|
|
120
175
|
### General editing
|
|
@@ -216,10 +271,10 @@ setBlocks line-height 2.5
|
|
|
216
271
|
|
|
217
272
|
### Drag and drop
|
|
218
273
|
|
|
219
|
-
Selected text becomes a real draggable object —
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
274
|
+
Selected text becomes a real draggable object — the selection is marked on
|
|
275
|
+
elements, which is exactly what HTML5 drag and drop wants, so dragging works
|
|
276
|
+
between windows, between browsers, and to and from the desktop with no extra
|
|
277
|
+
machinery.
|
|
223
278
|
|
|
224
279
|
Every drag offers **both representations**, and the receiver picks:
|
|
225
280
|
|
package/dist/commands.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface EditableContext {
|
|
|
21
21
|
findAll(selector: string): Element[];
|
|
22
22
|
selectedLeafNodes(): Node[];
|
|
23
23
|
selectedBlocks(): Element[];
|
|
24
|
-
insertionPoint():
|
|
24
|
+
insertionPoint(): HTMLElement | null;
|
|
25
25
|
block(node: Node): Element | null;
|
|
26
26
|
normalize(): void;
|
|
27
27
|
focus(): void;
|
|
@@ -44,4 +44,14 @@ export declare const commands: Record<string, Command>;
|
|
|
44
44
|
* Supports chained commands separated by semicolons.
|
|
45
45
|
* Example: "setText font-weight bold; setBlockType h2"
|
|
46
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* Run a single command with already-separated arguments.
|
|
49
|
+
*
|
|
50
|
+
* The string form splits on `;` and whitespace, so any runtime value
|
|
51
|
+
* interpolated into it can inject a second command — and a data URI contains
|
|
52
|
+
* `;` BY SPEC (`data:image/png;base64,…`), which silently broke every dropped
|
|
53
|
+
* image. Call sites that build a command from a URL, a filename or anything
|
|
54
|
+
* else the user did not type must use this form instead.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runCommand(ctx: EditableContext, name: string, ...args: string[]): void;
|
|
47
57
|
export declare function executeCommand(ctx: EditableContext, commandString: string): void;
|
package/dist/dom-utils.d.ts
CHANGED
|
@@ -83,3 +83,27 @@ export declare function caretGeometryAt(marker: Element, root: Element): {
|
|
|
83
83
|
top: number;
|
|
84
84
|
height: number;
|
|
85
85
|
} | null;
|
|
86
|
+
/**
|
|
87
|
+
* Strip executable content from a subtree, IN PLACE.
|
|
88
|
+
*
|
|
89
|
+
* The editor replaced `contentEditable` but not the sanitization the browser
|
|
90
|
+
* was doing on its behalf: pasted and dropped HTML is written into the live
|
|
91
|
+
* document, and from there into `value`, `internals.setFormValue` and every
|
|
92
|
+
* undo snapshot — so an unsanitized payload is stored, re-served, and re-fired
|
|
93
|
+
* on undo. Must run BEFORE any node enters the document.
|
|
94
|
+
*
|
|
95
|
+
* This is deliberately a denylist for elements and an allowlist for URL
|
|
96
|
+
* schemes: unknown ELEMENTS are content (including a plugin's custom elements,
|
|
97
|
+
* which must survive — see EXTENSIBILITY.md), whereas unknown SCHEMES are not.
|
|
98
|
+
*/
|
|
99
|
+
export declare function sanitizeInPlace(root: Element | DocumentFragment): void;
|
|
100
|
+
/**
|
|
101
|
+
* Is this URL safe to NAVIGATE to, or to write into an href?
|
|
102
|
+
*
|
|
103
|
+
* Stricter than `isSafeUrl`: that one allows raster `data:image/*` because an
|
|
104
|
+
* `<img src>` may legitimately carry one, while a link must never — so this
|
|
105
|
+
* rejects every `data:` URL. Both must normalize identically, or the stricter
|
|
106
|
+
* check is the one that gets bypassed: `da	ta:image/png;…` passed here while
|
|
107
|
+
* `data:image/png;…` was correctly rejected.
|
|
108
|
+
*/
|
|
109
|
+
export declare function isSafeNavigationUrl(value: string): boolean;
|