UNPKG

jodit

Version:

Jodit is an awesome and useful wysiwyg editor with filebrowser

281 lines (176 loc) 233 kB
# Changelog > **Tags:** > > - :boom: [Breaking Change] > - :rocket: [New Feature] > - :bug: [Bug Fix] > - :memo: [Documentation] > - :house: [Internal] > - :nail_care: [Polish] ## 4.13.10 #### :house: Internal - **Repository**: added a `funding` field (and a GitHub Sponsor button) pointing to Jodit PRO, refreshed the README header, pinned all GitHub Actions to commit SHAs, and added Dependabot (actions + npm), a PR path-labeler, a first-interaction greeting, a surgical stale-issue bot and release-note categories. No runtime changes. ## 4.13.9 #### :bug: Bug Fix - **Mobile / adaptive toolbar**: with a custom `buttons` list and the default `buttonsMD`/`buttonsSM`/`buttonsXS`, resizing the editor narrower surfaced buttons that were never requested — the breakpoint sets are group-based defaults independent of `buttons`. The responsive breakpoint set is now constrained to `buttons`: resizing only ever drops buttons, never adds ones outside `buttons`. Set `buttonsMD`/`buttonsSM`/`buttonsXS` explicitly if you want a different per-breakpoint set. The default configuration (where `buttons` is the full superset) is unchanged. Fixes [#1389](https://github.com/xdan/jodit/issues/1389). ## 4.13.8 #### :house: Internal - **Dependencies**: security/maintenance bumps — `axios` 1.18.0, `webpack-dev-server` 5.2.6, `tar` 7.5.21, `svgo` 4.0.2, `fast-uri` 3.1.4 (Dependabot). All dev/build-only. (#1384, #1385, #1386, #1387, #1388) ## 4.13.7 #### :bug: Bug Fix - **Dialog**: event handlers passed via `options.events` are now attached to the dialog's emitter (as `FileBrowser` already does). A dialog fires `afterOpen`/`beforeClose` on its own emitter — unlike popups, which fire on the editor emitter — so those events were previously unreachable from the editor. Plugins that open dialogs (e.g. the PRO Button Generator) can now forward the editor's `events` config so `afterOpen`/`beforeClose` can be observed. Reported by Ralf Pichler (uniquare.com, Jodit OEM). ## 4.13.6 #### :bug: Bug Fix - **Security / clean-html**: a `<script>` nested directly inside `<svg>` (or MathML) is now removed by `denyTags`. The deny/allow tag filter matched `node.nodeName` against an upper-cased hash, but foreign-namespaced elements keep their original case (an SVG `<script>` reports `"script"`, not `"SCRIPT"`), so the script slipped through the default `denyTags` and executed when the value was loaded into the editor. The lookup now normalises the tag name to upper case, so namespace can't bypass the filter — while still honouring `allowTags`. Fixes GHSA-45qg-252v-3f7p. Thanks to Roman Kis for the report. - **Toolbar**: fixed `TypeError: Cannot redefine property: component` thrown when the editor is created with `cache: false`. `ToolbarButton.createContainer` (decorated with `@cacheHTML`) defined the `component` back-reference on the button element without `configurable`, i.e. non-configurable. With caching enabled the container is cloned (which drops the property), hiding the problem; with `cache: false` the container isn't cloned, so a button built after an editor became ready reached the constructor still carrying that non-configurable property and the constructor's redefine threw. The property is now defined as configurable, matching the base `UIElement`. ## 4.13.5 #### :house: Internal - **Build**: pin the ESM build to the project's TypeScript (`node_modules/.bin/tsc`) instead of a bare `tsc` resolved from `PATH`. The `4.13.4` publish build broke after the GitHub runner image began shipping a newer global TypeScript that rejects options (`baseUrl`, `downlevelIteration`, `moduleResolution: "node"`) which the project's pinned TypeScript 6.0.2 still accepts. This re-releases the 4.13.4 security fixes, which never reached npm. ## 4.13.4 #### :bug: Bug Fix - **Security / clean-html**: HTML smuggled into MathML/SVG (e.g. `<math><mglyph><html><body onload>`) is now stripped even when clean-html parses the fragment in a disconnected sandbox (`useIframeSandbox: false`). The guard used `isConnected`, which is `false` for a detached parse box, so the smuggled node slipped through; it now uses `box.contains()`. Fixes GHSA-rxcw-mc6f-6hr3 (mutation XSS via MathML/style rawtext carrier). Thanks @MatrixNeoKozak (#1380). - **safeHTML**: guard `location` access when neutralising `javascript:` links so `sanitizeHTMLElement` no longer throws a `ReferenceError` in SSR / Node environments where `location` is undefined (#1380). #### :house: Internal - **Dependencies**: security bumps for `tar` 7.5.16, `form-data` 4.0.6, `js-yaml` 4.3.0, `launch-editor` 2.14.1, `webpack-dev-server` 5.2.5 (+ `http-proxy-middleware` 2.0.9), `ws` 8.21.1 / `engine.io` 6.6.9 / `socket.io-adapter` 2.5.8, and `websocket-driver` 0.7.5 (Dependabot alerts #189–#197). All dev/test-only. (#1373, #1374, #1375, #1376, #1377, #1382, #1383) ## 4.13.3 #### :rocket: New Feature - **AsyncStorage**: `AsyncStorage.makeStorage(persistentOrStrategy, suffix, options?)` now accepts a third `options` argument with a `defaultProvider` field that overrides which provider backs the storage — `'local'` (localStorage), `'memory'`, or a custom `IAsyncStorage` implementation. When omitted the behaviour is unchanged (persistent IndexedDB with an in-memory fallback). The same option is exposed on the editor as the `asyncStorage` config option (`Jodit.make('#editor', { asyncStorage: { defaultProvider: 'local' } })`), so `jodit.asyncStorage` can be pointed at localStorage, memory, or your own backend without subclassing. - **Slots**: a new `above` workplace slot (`editor.currentPlace.slots.above`) that always stays **above the toolbar** — the spot Google-Docs-style presence bars and banners live in. The toolbar box used to re-pin itself as the container's first child on every `toolbarContainer` access; it now keeps itself below any container children flagged with the `data-jodit-above-toolbar` attribute (the new slot carries the flag). The slot renders as nothing while empty and gets the standard border-bottom once filled, like the other slots. #### :bug: Bug Fix - **Storage**: `LocalStorageProvider.delete(key)` removed the entire storage scope (every key sharing the same `rootKey`/suffix) instead of just the requested key — `delete` behaved identically to `clear`. It now reads the JSON blob, drops only that key and writes the rest back. Affects `Jodit.modules.Storage`/`buffer`/`storage` and the `@persistent` decorator when a single key is deleted. #### :house: Internal - **Selection**: `Select.wrapInTagGen` no longer relies on the browser's `document.execCommand('fontSize', false, '7')` to split a non-collapsed selection into wrappable inline fragments. It now uses a pure-DOM implementation that splits the boundary text nodes and wraps every contiguous run of selected inline content (grouped per block) into a `<font>` element. This removes one more dependency on the deprecated `execCommand` API and makes selection wrapping (`commitStyle`, bold/italic/font/color, `wrapInTag`) behave identically across Chrome and Firefox. No public API or output change; covered by new `wrapInTag` tests. ## 4.12.42 #### :bug: Bug Fix - **Insert YouTube/Vimeo video**: an inserted video was shown for a moment and then immediately removed. Since 4.11.2 `cleanHTML.denyTags` includes `iframe` by default, and the plugin's async cleanup walker (which runs ~300 ms after every change) stripped the freshly inserted embed — so the built-in Video button produced nothing that survived. Recognized YouTube/Vimeo player iframes (the output of the Video button / `convertMediaUrlToVideoEmbed`) are now exempt from the `denyTags` sweep and from the automatic empty `sandbox=""` (which would have blocked playback anyway); arbitrary and bare `<iframe>` elements are still removed as before. Fixes [#1381](https://github.com/xdan/jodit/issues/1381). ## 4.12.40 #### :nail_care: Polish - **Insert image / file popup**: the tabs of the insert-image / insert-file popup now size to their labels, so long localized captions — e.g. the German "Datei hochladen" / "Durchsuchen" — are no longer clipped and the popup widens to fit. Previously each tab was forced to an equal column width, which cut off longer translations. The change is scoped to a new `jodit-file-selector` class, so other tab popups (link, video …) keep their fixed equal-column layout. ## 4.12.39 #### :bug: Bug Fix - **Fullsize / global fullscreen**: exiting fullsize scrolled the whole page back to the top. Entering global fullsize puts `<html>` into `position: fixed`, which makes the browser reset the page scroll to `0`; that position was never restored on exit. The scroll offset is now saved when entering fullsize and restored with `window.scrollTo()` when leaving it. Fixes [#1255](https://github.com/xdan/jodit/issues/1255). - **Scroll into view / Search**: `inView` (used by `scrollIntoViewIfNeeded`) only checked that an element wasn't below the viewport bottom, not that it wasn't above the top. An element scrolled **above** the visible area was therefore reported as visible, so navigating to it never scrolled — most visibly, cycling through search results and wrapping around to a match near the top of the page did not scroll up to it. `inView` now also requires the element's bottom to be at or below the viewport top. Fixes [#1279](https://github.com/xdan/jodit/issues/1279). #### :rocket: New Feature - **Link dialog / `link.deriveUrlFromText`**: new opt-in option (default `false`). When the link dialog is opened for a new link with an empty URL field, the URL is pre-filled from the selected text when it looks like a URL or email — a bare domain gets `https://` (`example.com` → `https://example.com`), an email gets `mailto:`, an already-schemed URL is kept as is, and plain text is left untouched. Requested in [#1248](https://github.com/xdan/jodit/issues/1248) / [#1323](https://github.com/xdan/jodit/issues/1323). - **CSP `nonce` support**: added a `nonce` option that is applied to every `<style>`, `<script>` and `<link>` element Jodit injects at runtime (plugin styles, CDN scripts for ACE/beautify, downloaded stylesheets). Setting it to the nonce from your `Content-Security-Policy` header lets the editor run under a strict `style-src`/`script-src` policy without `'unsafe-inline'`. Requested in [#1276](https://github.com/xdan/jodit/issues/1276). ```js Jodit.make('#editor', { nonce: 'r4nd0m' }); ``` #### :memo: Documentation - **Getting started / CDN**: bumped the pinned Jodit version in the cdnjs/unpkg snippets in `README.md` and `docs/getting-started.md` from the stale `4.7.6` to `4.12.37`. Refs [#1208](https://github.com/xdan/jodit/issues/1208). #### :house: Internal - **Enter tests**: added a regression test asserting that pressing Enter with the caret at the bottom of a scrollable editing area scrolls to follow the cursor (and does not scroll when the caret line is already visible). Covers the scenario from [#1300](https://github.com/xdan/jodit/issues/1300), which works on the current version. ## 4.12.37 #### :bug: Bug Fix - **Source mode / ACE options**: only a fixed set of `sourceEditorNativeOptions` (`theme`, `mode`, `wrap`, `showGutter`, `highlightActiveLine`) was applied to the ACE editor — any other native option, such as `fontSize`, `tabSize` or `useSoftTabs`, was silently dropped, so `sourceEditorNativeOptions: { fontSize: '30px' }` had no effect. The whole options object is now forwarded to ACE's `editor.setOptions()`, and the type accepts arbitrary native keys. Fixes [#1285](https://github.com/xdan/jodit/issues/1285). ## 4.12.36 #### :bug: Bug Fix - **Lists / `enter: 'br'` mode**: converting a list type (`ul ↔ ol`, or applying a class/style that forces the REPLACE path) destroyed the list — `unwrapList` extracted the `li` content into a `DocumentFragment` that was dissolved on insertion, so `wrapList` received an empty detached node and the new `<ol>`/`<ul>` was never inserted into the document; the content was left as bare inline nodes. In the REPLACE path the `li` now stays in the document and is re-wrapped directly. - **commitStyle / attributes**: a `number`/`boolean` attribute value (e.g. `commitStyle({ element: 'a', attributes: { tabindex: 2 } })`) could never be toggled off — the `attr()` getter returns a string, so the strict comparison against the raw value always failed and every re-apply took the CHANGE branch instead of UNSET. The value is normalized to a string before comparison now. - **Selection**: `cursorInTheEdge()` (and its `cursorOnTheLeft`/`cursorOnTheRight` wrappers) threw `IndexSizeError` when the document had no selection ranges — `getRangeAt(0)` was called without checking `rangeCount`. It returns `null` now, as documented. - **Selection**: `eachSelection()` threw `TypeError` when the selection root was an empty editor (`childNodes[-1]` produced an `undefined` start node). - **Dom.isFragment**: fragments belonging to an inert document (`template.content`, `DOMParser` output) were not recognized because of a stale `defaultView` requirement — same class of problem already fixed for `Dom.isElement` earlier. - **Dom.between**: when `end` was an ancestor of `start`, the ascent skipped over it and the callback kept firing for nodes far outside the intended range, up to the end of the tree. - **Dom.replaceTemporaryFromString**: the regular expression hardcoded the `data-jodit-temp` attribute name instead of using the `TEMP_ATTR` constant and did not match a temporary element whose marker attribute has no value (`<span data-jodit-temp>`). - **LazyWalker**: stopping a walk did not really cancel it — `stop()` called `cancelIdleCallback` with an id that was never assigned (dead code left from the pre-`scheduler.postTask` implementation) and the scheduler's `AbortController` was never aborted. Also, `setWork()` called before the first chunk had started did not cancel the previously scheduled pass. Repeated `setWork()` calls (the `clean-html` plugin does this on every change) piled up concurrent loops pumping the same generator, defeating the chunked-walk throttling. The pending task is now aborted in `stop()`. - **LazyWalker**: the `affect` flag was not reset when a pass was interrupted via `break()`, so the next pass reported `end(affect = true)` even if it had not changed anything (a false `synchronizeValues` trigger for `clean-html`). It also processed `timeoutChunkSize + 1` nodes per chunk instead of the configured size. #### :house: Internal - **Dom**: the `Dom.replace(elm, 'p')` overload without a `create` instance is removed from the type surface — it always failed at runtime on an assertion (`Need create instance for new tag`); `create` is now required whenever the replacement is defined by a tag name or an HTML string. `Dom.isList` is honestly typed as `HTMLUListElement | HTMLOListElement`, `Dom.isComment` accepts `Nullable<Node>`, `isSameAttributes` no longer pretends to be a type predicate, `CommitStyle.isApplied` returns a real boolean. - **Dom**: `nextGen` builds its sibling stack with `push` + `reverse` instead of `unshift` in a loop (was O(n²) on wide sibling lists); `isTag` computes the upper-case tag name lazily; the temporary-element regexp is compiled once at the module level. - **Docs**: fixed the misleading/inverted JSDoc for `Dom.between`, `Dom.up` (root-check asymmetry), `Dom.safeInsertNode`, `LazyWalker` options and events, `cursorInTheEdgeOfString`, `isSuitElement` (`strictStyle` description was inverted), the `LazyWalker` example in the DOM module README (options object, not a number), and the `Dom.replace` example (missing `create` argument). - **Tests**: new coverage for `Dom.between`, `Dom.isFragment` (inert documents), `Dom.replace`, `Dom.replaceTemporaryFromString`, wide sibling traversal order in `Dom.find`, `LazyWalker` restart/affect-flag reset, `cursorInTheEdge` without a selection, `eachSelection` on an empty editor, `ul → ol` conversion in `enter: 'br'` mode, and toggling a numeric attribute via `commitStyle`. - **Tests / hermetic images**: the resize, image-editor and image-properties karma tests that wait for a real image load no longer fetch `https://xdsoft.net/jodit/files/artio.jpg`/`th.jpg` from the live server — they use the byte-identical `tests/artio.jpg` already served by karma, so a slow or unreachable xdsoft.net can't time these tests out anymore (~17 tests were flaking on degraded networks). The `onLoadImage` test helper now also rejects with a clear `failed to load "<src>"` message on an image `error` event instead of hanging until the mocha timeout. ## 4.12.35 #### :bug: Bug Fix - **Ajax / GET requests**: when a request URL already contained a query string (e.g. `filebrowser.ajax.url: '/connector/index.php?uid=123'`), `prepareRequest` corrupted it — `parseQuery` received the whole URL instead of just its query part, so the URL path was glued into the first parameter name (`index.php?connector%2Findex.php%3Fuid=123&...`) and the baked-in parameters were duplicated instead of being merged with (and overridden by) the request data. The query part is now extracted before parsing, so URL parameters merge cleanly and `data` keys take priority. Reported by a customer (EventSystemPro, Jodit Multi PRO). ## 4.12.34 #### :rocket: New Feature - **Image editor / `openImageEditor` save contract**: `ImageEditorActionBox.action` now also accepts `'saved'` (plus an optional `newPath`). A replacement image editor (e.g. the Jodit PRO one, which uploads the fully edited blob itself through the connector's `imageSave` action) can report "the file is already persisted" — the core then skips the server `resize`/`crop` request and only runs the success wiring (file browser refresh, in-content `<img>` swap via `onSuccess(newPath)`). Previously every save was forced through `dataProvider.resize`/`crop`, which produced a redundant request that could 404 ("Source not found") when the editor was opened from the Image properties dialog. #### :bug: Bug Fix - **Dialog**: destructing a dialog from its own `afterClose` handler (a common pattern for one-shot dialogs, e.g. the finder preview lightbox) made `close()` throw `TypeError: Need subject` — after the handler ran, `close()` still fired `joditCloseDialog` on `this.ow`, which the destructor had already cleared. `close()` now skips that fire when the dialog is being destructed. ## 4.12.33 #### :bug: Bug Fix - **Image properties / edit remote image**: when downloading an external image to the host failed, the alert displayed the raw template `There was an error loading %s` — the error message was passed as `alert`'s second argument (the callback slot) instead of being substituted into the placeholder. The message is now formatted through `i18n('There was an error loading %s', error.message)`, so the alert shows the actual (translated) error. #### :house: Internal - **ESM build**: `@jodit/image-editor` added to the `allowPackages` allowlist of the alias-imports resolver, so the ESM build keeps the package as an external import — groundwork for the Jodit PRO image editor built on it. - **Image editor tests**: expanded crop/resize scenarios (with and without aspect ratio) and new acceptance tests for the `afterImageEditorSave` event (correct `action`/`box` payload on Save / Save as). ## 4.12.32 #### :bug: Bug Fix - **Editor size / external toolbar**: when the `toolbar` option points to an external element (the toolbar renders outside the editor container), the workplace height was still computed as `container height − toolbar height`, so the editing area was wrongly shrunk by the height of a toolbar that wasn't inside the container. The toolbar height is now subtracted only when the toolbar actually lives inside the container. Reported in [#920](https://github.com/xdan/jodit/discussions/920). ## 4.12.31 #### :bug: Bug Fix - **Security / sanitizeHTMLElement (`javascript:` link XSS, CWE-79/83)**: the `href` safety check used a bare `href.trim().indexOf('javascript') === 0` — unlike the `isDangerousUrl` normalization applied to every other URL attribute, it was case-sensitive and did not strip control bytes, tabs or newlines. So a dangerous `href` survived sanitization (and persisted in the stored `editor.value`) when the scheme was upper/mixed-case (`JAVASCRIPT:`), prefixed by a C0 control byte (`\x01javascript:`), or split by an embedded tab/newline (`java\tscript:`) — all of which a browser still resolves to `javascript:` on click, executing attacker script in any page that renders the stored value. `href` is now routed through the same `isDangerousUrl` normalization (strips control characters and lowercases before matching the scheme), so these obfuscations are neutralized like every other URL attribute. Affected all versions through 4.12.30. Responsibly reported by Younghun Ko of AhnLab ([@koyokr](https://github.com/koyokr)) (GHSA-j839-gqq4-gf9j). ## 4.12.30 #### :bug: Bug Fix - **Paste from Excel / "Keep" formatting**: content copied from MS Excel (and other apps that wrap the clipboard in a bare `<html>` tag) lost all styling — table cells styled by class in a `<style>` block (e.g. `.xl31 { background:#FCE4D6 }`, the typical Excel export) came in with no background, fonts or alignment. `applyStyles` (the *Paste as HTML → Keep* path) only inlined `<style>` rules when the opening tag carried attributes (`<html …>`, as Word emits); Excel emits a bare `<html>`, so the rules were never inlined and were dropped together with the `<style>` block. The opening `<html>` tag is now matched with or without attributes, so class-based styling is inlined onto the cells and survives. Reported in [#1362](https://github.com/xdan/jodit/issues/1362). ## 4.12.29 #### :bug: Bug Fix - **Event emitter (memory hygiene in SPA / Shadow DOM)**: `EventEmitter` stores its per-subject handler namespaces under a `__JoditEventEmitterNamespaces<timestamp>` property, but on `off()`/`destruct()` it only set that property to `undefined` instead of removing it. On long-lived subjects such as `window`, repeatedly creating and destroying editors in a single-page application left a growing pile of leftover `undefined` keys on the object. The property is now `delete`d on cleanup, so nothing lingers after `destruct()`. Reported by Ralf Pichler (uniquare.com, Jodit OEM). ## 4.12.28 #### :bug: Bug Fix - **Security / clean-html (mutation XSS, CWE-79)**: the HTML sanitizer (`safeHTML`) walked the parsed value as elements, but a handler smuggled as `<style>` rawtext inside a MathML/SVG foreign-content carrier (e.g. `math > mtext > table > mglyph > style` hiding an `<img onload=…>`) was never an element during that walk. A later serialize-reparse then hoisted the `<img>` out of `<style>` into a live HTML node with its `on*` handler intact, so an application that re-rendered `editor.value` could execute attacker script with no user interaction — a stored XSS in the default config affecting all 3.x/4.x through 4.12.27. The fix drops the smuggled HTML at the source: any HTML-namespace element the parser placed inside `<math>`/`<svg>` outside an integration point (`foreignObject`/`annotation-xml`/`desc`/`title`) is removed before the walk, which also covers carriers nested one level deeper without a re-parse loop. Legitimate MathML/SVG content and top-level `<style>`/`<script>` are preserved. Responsibly reported by Younghun Ko of AhnLab ([@koyokr](https://github.com/koyokr)) (GHSA-rxcw-mc6f-6hr3). ## 4.12.26 #### :bug: Bug Fix - **Security / Helpers (prototype pollution)**: `Jodit.modules.Helpers.set(chain, value, obj)` walked the dot-separated `chain` and created/followed each segment without filtering prototype-mutating keys, so a chain such as `__proto__.polluted` (e.g. `set('__proto__.polluted', 'yes', {})`) could reach and mutate `Object.prototype` (CWE-1321). `set` now bails out when any segment is `__proto__`, `constructor`, or `prototype`, reusing the same guard added for `Jodit.configure()` in 4.12.18. Responsibly reported by Junming Wu. ## 4.12.25 #### :bug: Bug Fix - **Source mode / complex scripts (Thai, Arabic, Hindi, Hebrew, Persian)**: the ACE source editor was loaded from CDN at version **1.4.2 (2018)**, which predates working bidirectional-text and combining-character support — complex-script text rendered misaligned with the underlying code ("invisible characters" after lines), the caret/selection landed on the wrong characters and copying lost as many characters as there were combining marks. The default `sourceEditorCDNUrlsJS` now points to **ACE 1.43.3**, which ships an automatic per-line bidi handler and years of complex-script rendering fixes; all existing source-mode integration tests pass against the new build, plus new regression tests for the Thai round-trip and the bidi layer. ## 4.12.24 #### :rocket: New Feature - **Link dialog**: new opt-in option `link.ariaLabelInput` (default `false`) adds an **Aria label** text field to the Insert/Edit link form. It reads the existing `aria-label` when editing and writes it to the `<a>` on submit (clearing it when empty) — useful for accessibility when several links share the same visible text (e.g. multiple "here" links a screen reader can't tell apart). Addresses [#1204](https://github.com/xdan/jodit/issues/1204). - **Image editor**: new `afterImageEditorSave` event, fired when the user clicks **Save** / **Save as** in the image editor (crop/resize). The handler receives the action box `{ action: 'resize' | 'crop', box }` (and the new name for *Save as*), so you can react to the applied crop/resize — e.g. update the image's aspect ratio or set its real size. The existing `afterImageEditor` only fired when the editor *opened*. Addresses [#820](https://github.com/xdan/jodit/issues/820). - **Uploader**: new `uploader.beforeUpload(files)` hook, called with the file list right before upload (or base64 read). Return `false` to abort — useful for client-side validation of size/type/count; `this` is the uploader (so `this.j` is the editor). Addresses [#1329](https://github.com/xdan/jodit/issues/1329). - **Table / Select cells**: **Ctrl/Cmd + click** now toggles individual cells into a non-contiguous selection (clicking an already-selected cell removes it), in addition to the existing click-and-drag rectangular selection. The cell properties popup opens for the accumulated selection, and toolbar actions (background color, etc.) apply to every selected cell. Previously each Ctrl+click reset the selection to the single clicked cell. Fixes [#1163](https://github.com/xdan/jodit/issues/1163). - **Backspace/Delete**: new option `delete.disableCases` (a `Set<string>`) lets you turn off individual Backspace/Delete cleanup behaviors that the plugin applies after the native deletion — e.g. `delete: { disableCases: new Set(['join-neighbors']) }` stops Backspace at the start of a paragraph from merging it into the previous one. Available keys: `remove-unbreakable`, `remove-not-editable`, `remove-char`, `table-cell`, `remove-empty-parent`, `remove-empty-neighbor`, `join-two-lists`, `join-neighbors`, `unwrap-first-list-item`. Addresses [#1060](https://github.com/xdan/jodit/issues/1060). - **Clean HTML**: new opt-in option `cleanHTML.collapseEmptyValueToEmptyString` (default `false`). When the editor holds only a single empty block — e.g. `<p><br></p>` left in the DOM after the user deletes all content (contenteditable keeps that caret container) — `editor.value` and the synced source element now return an empty string `''` instead of `<p><br></p>`, which is what forms usually expect on submit. Real content (including a `<p><br></p>` followed by other blocks) is never collapsed. Addresses [#1149](https://github.com/xdan/jodit/issues/1149). #### :bug: Bug Fix - **Font select button**: with the font control rendered as a select (`controls: { font: { component: 'select' } }`), unstyled text showed the editor's raw default font stack (e.g. `-apple-system`) on the button instead of `Default`. The font control's `value` now returns an empty value when the computed font-family equals the editor's own default, so the button shows the `Default` list entry. Fixes [#1370](https://github.com/xdan/jodit/issues/1370). ## 4.12.23 #### :bug: Bug Fix - **Search (Ctrl+F)**: pressing Enter in the search input **submitted the parent `<form>`** instead of jumping to the next match — the `preventDefault()` for Enter lived inside a debounced handler, so it ran only after the browser had already dispatched the form submission. Enter is now canceled synchronously, while the search logic itself stays debounced. Fixes [#918](https://github.com/xdan/jodit/issues/918). - **i18n / language bundles**: translations for keys that were missing from `ar.js` never reached the build — the master i18n key list was generated **only from `ar.js`**, and every other language is serialized as an array indexed by that list, so existing translations for the ordered-list menu items (*Lower Alpha*, *Lower Greek*, *Lower Roman*, *Upper Alpha*, *Upper Roman* — translated in de/es/fi/it/ja/pt_br) and a dozen other keys were silently dropped from every bundle. The key list is now built as the **union of all language files**. Fixes [#997](https://github.com/xdan/jodit/issues/997). - **Custom `ownerWindow` (editor created inside an iframe)**: UI components (toolbar collection, popups, dialogs) always kept the **global** `window` as their owner window instead of the configured one. Popup/dropdown outside-click close handlers were therefore bound to the wrong window — clicks inside the iframe never reached them, so toolbar dropdowns did not close on an outside click. `ViewComponent` now inherits the owner window from its parent view. Fixes [#965](https://github.com/xdan/jodit/issues/965). - **Toolbar content controls (`getContent`)**: controls rendered through `getContent` (e.g. the FileBrowser **Upload** button) never ran the status calculation, so their `isDisabled`/`isActive`/`update` callbacks were silently ignored — the Upload button stayed enabled even when the backend permissions (`canI('FileUpload')`) said otherwise. `ToolbarContent.update()` now evaluates the control's status like regular toolbar buttons and also propagates the disabled state to nested form controls (the file `<input>` of the Upload button). Thanks @WatchfulEyeOfZod for the diagnosis and the patch draft. Fixes [#1094](https://github.com/xdan/jodit/issues/1094). - **Inline toolbar / Iframe mode**: with `iframe: true` the selection toolbar (`toolbarInlineForSelection`) opened far away from the selected text — the popup was positioned by `range.getBoundingClientRect()`, whose coordinates are **iframe-local**, while the popup itself lives in the host document. The iframe offset is now added to the selection bound. Fixes [#1058](https://github.com/xdan/jodit/issues/1058). - **Paste toolbar button**: the *Paste from clipboard* toolbar button inserted only the **plain text** flavor of the clipboard, while Ctrl+V pasted the full HTML — the button explicitly requested `text/plain` from `navigator.clipboard.read()` even when `text/html` was available. The button now prefers the HTML flavor (falling back to plain text), so both paste paths behave the same. Fixes [#1061](https://github.com/xdan/jodit/issues/1061). - **Format block (H1–H6 / paragraph)**: applying a heading to a block pasted from an external source seemed to do nothing — the tag actually changed, but leftover inline styles like `font-weight: normal; font-size: 24px` visually overrode the new format, so an `<h2>` looked exactly like the old text. When a block format is applied, the conflicting `font-size`/`font-weight` inline styles are now removed from the block (other styles like color stay). Fixes [#1063](https://github.com/xdan/jodit/issues/1063). - **Backspace/Delete next to a table**: pressing Backspace at the start of a paragraph that follows a table (or Delete at the end of a paragraph before one) merged the paragraph's text **into the `<table>` element itself** — it landed after `</tbody>`, which is invalid HTML; on the next parse the browser foster-parents such content out of the table, corrupting the document and causing follow-up DOM errors (the `Failed to execute 'appendChild'` console spam from the report). The join logic now merges the text into the edge table **cell** (last cell for Backspace, first for Delete). Covers [#1064](https://github.com/xdan/jodit/issues/1064). - **Paste from Word (`askBeforePasteFromWord`)**: office content was frequently mis-detected as plain HTML, so the generic *Paste as HTML* dialog appeared instead of the Word one. The detector only matched a `Microsoft Word N` meta generator or `mso-` styles in **double** quotes combined with `<font>` tags — while the raw Word clipboard fragment uses **single-quoted** `style='mso-…'`, unquoted `class=MsoNormal`, an unquoted `ProgId` meta and office XML namespaces (and modern Word emits no `<font>` at all); LibreOffice/OpenOffice content was never detected. The detector now also recognizes all of these markers. Fixes [#1078](https://github.com/xdan/jodit/issues/1078). - **Drag and drop (`enableDragAndDropFileToEditor: false`)**: dropping a file (e.g. a JPG from another window) into the editor still embedded it — with the option off no handler was bound to the `drop` event, nothing called `preventDefault`, and Firefox inserted the dropped image **natively**. With the option disabled the editor now cancels file drops, so they do nothing — as the option promises. Fixes [#1077](https://github.com/xdan/jodit/issues/1077). - **AI Assistant**: the *Insert After* button placed the generated text after the **first** node of the selection — with several selected paragraphs the result landed inside/after the first paragraph instead of after the whole selection. The insertion point is now the collapsed **end** of the selection, so the result follows everything that was selected; single-word/sentence behavior is unchanged. Fixes [#1263](https://github.com/xdan/jodit/issues/1263). - **Color / Select cells**: applying a text or background color from the main toolbar to table cells selected with the `select-cells` plugin (click or drag-select a cell — typical for tables pasted from Word) silently did nothing: the cell selection drops or collapses the native range, so the color was committed as a pending caret format in an empty `<span>` **outside** the table. When cells are selected and the native selection is collapsed, the `forecolor`/`background` commands now recolor the content of every selected cell (including nested spans carrying their own Word colors). Fixes [#1250](https://github.com/xdan/jodit/issues/1250). - **Shadow DOM / Table**: clicking a table cell inside a Shadow DOM editor (the `shadowRoot` option, e.g. a Stencil/web component integration) never opened the cell properties popup (background, vertical align, split/merge, add/remove row & column). `document.elementFromPoint` does not pierce shadow boundaries — it returns the shadow **host** — so the `select-cells` plugin could not find the clicked cell on `mouseup`. The hit-testing now starts from the configured shadow root in `select-cells` (cell selection on click and drag-select), `add-new-line` (the *Break* line over tables/images) and `Popup.getKeepBound`. Fixes [#1312](https://github.com/xdan/jodit/issues/1312). ## 4.12.22 #### :bug: Bug Fix - **Clipboard (copy/cut inside the editor)**: copying or cutting a selection that sat **entirely inside the text** of a formatted element (e.g. selecting part of a bold-italic word with the mouse) put the bare text into the clipboard — the `<strong>`/`<em>`/… context was lost, so pasting it elsewhere dropped the formatting. Native browser copy keeps that context, but Jodit intercepts copy/cut and serialized only the range contents. The copied fragment is now wrapped in shallow clones of the selection's inline ancestors, like browsers do. Fixes [#1202](https://github.com/xdan/jodit/issues/1202). - **Source mode / multiple editors**: switching an editor into source mode focused its code mirror **unconditionally** — when the switch happened programmatically (e.g. a Vue/React wrapper re-initializing the editor) it pulled the focus out of the editor the user was typing in. The mirror is now focused only when the focus is not already inside another editor or control. Fixes [#1356](https://github.com/xdan/jodit/issues/1356) (follow-up to [#1313](https://github.com/xdan/jodit/issues/1313)). - **Limit (`limitChars` + `countTextSpaces`)**: the character limiter never counted spaces — even with `countTextSpaces: true` the limit applied to non-space characters only (`limitChars: 10` allowed `I am only 8` because only 8 visible characters were counted), while the status-bar counter showed the space-inclusive number. With `countTextSpaces: true` the limiter now counts characters the same way as the counter — including spaces. Fixes [#1144](https://github.com/xdan/jodit/issues/1144). - **Value assignment re-requested images (jodit-react)**: every `editor.value = …` assignment parsed the HTML into a helper element of the **live** document to sanitize it, so the browser issued a network request for every `<img>` in the value — with wrappers that sync the value on each change (e.g. jodit-react `onChange`), the image URLs were re-requested on every click/keystroke. The sanitize sandbox is now an **inert document** (`implementation.createHTMLDocument`), which never loads sub-resources; `Dom.isElement`/`Dom.isHTMLElement` no longer require a browsing context (`defaultView`), so the sanitizer keeps working on inert nodes. The internal `safeHTML` hook is now fired as an event for extensibility. Fixes [#1237](https://github.com/xdan/jodit/issues/1237). - **Inline toolbar (`toolbarInlineForSelection`)**: clicking a button in the selection toolbar (e.g. *Bold*) applied the command but closed the toolbar, even though the selection was still active — applying several formats required re-selecting the same text again and again. After a command fired from inside the popup, the selection toolbar is now reopened while the selection is not collapsed. Fixes [#1238](https://github.com/xdan/jodit/issues/1238). - **Clean HTML (`allowTags`)**: when `cleanHTML.allowTags` was configured as a map with per-tag attribute rules (e.g. `{p: true, a: {href: true}}`) and the content contained a tag **outside** the list that had attributes (e.g. `<meta charset="utf-8">` from pasted e-mail HTML), the cleanup worker threw `TypeError: Cannot read properties of undefined` on every pass — the editor "crashed" on the first click and disallowed tags were never removed. The attribute filter now skips tags that are not in the allow list (the tags filter removes them). Fixes [#1224](https://github.com/xdan/jodit/issues/1224). - **Paste (plain text)**: pasting plain text containing a stray `<` (e.g. `TD}gy{<KMj3ScW1[efbi`) lost everything after the bracket — the clipboard text was inserted through the HTML path, so `<KMj3…` was parsed as an unclosed tag and swallowed the rest of the string. When the clipboard contains **only** `text/plain` (and it is not actual HTML), special characters are now escaped before insertion, so `<`, `>` and `&` survive as literal text; the `nl2brInPlainText` line-break conversion still applies after escaping. Fixes [#1227](https://github.com/xdan/jodit/issues/1227). - **i18n (Turkish)**: the Word/Excel paste prompt ("The pasted content is coming from a Microsoft Word/Excel document…") was shown in **German** when the editor language was set to Turkish — `langs/tr.js` contained a copy of the German translation for that key. Replaced with the proper Turkish text. Fixes [#1245](https://github.com/xdan/jodit/issues/1245). - **Dialog**: a dialog (e.g. *Paste as HTML*) could be resized by its corner handle to a size smaller than its own header, content and footer, so the footer buttons ended up rendered outside the dialog box. Interactive resizing is now clamped to a minimum size: `dialog.minWidth`/`dialog.minHeight` options when set, otherwise the sum of the header, the content's CSS `min-height` and the footer (and the footer buttons' row width). Fixes [#1262](https://github.com/xdan/jodit/issues/1262). - **Resize handler / Iframe**: resizing the **editor itself** by the statusbar handle was jerky in `iframe` mode — while dragging, the editor height jumped between two values. Same root cause as the image-resizer fix below: `mousemove` events proxied from the editor's iframe carry iframe-viewport coordinates and were mixed as-is with host-window ones; they are now shifted into the host coordinate space (checked via `e.view`). Fixes [#1287](https://github.com/xdan/jodit/issues/1287). - **Resizer / Iframe**: resizing an image (or table) by its handles was jerky in `iframe` mode (e.g. with `editHTMLDocumentMode`) — the size jumped while dragging, making it impossible to set precisely. During a drag, `mousemove` events arrive both from the host window (pointer over the resize frame) and proxied from the editor's iframe (pointer over the content), but the iframe-viewport coordinate correction was applied to **all** of them, so host-window events were shifted by the workplace offset. The correction is now applied only to events that originate from the iframe window (`e.view`). Fixes [#1264](https://github.com/xdan/jodit/issues/1264). #### :house: Internal - **Popup tests**: added a regression test asserting that with `allowTabNavigation: true` an inner popup (a dropdown opened from a button inside another popup — e.g. the image *Horizontal align* list) is positioned next to its trigger button. With that option the inner popup is appended into the button inside the outer popup, whose CSS `transform` shifts `position: fixed` coordinates; covers [#1265](https://github.com/xdan/jodit/issues/1265) (already fixed in 4.12.8 by the `getFixedPositionOffset` containing-block compensation from [#1350](https://github.com/xdan/jodit/issues/1350)). - **Shadow DOM tests**: added regression tests asserting that with the `shadowRoot` option the image **resizer frame**, the **image properties dialog** (double click) and the **inline popup** all open correctly inside the shadow root. Covers the scenario from [#1109](https://github.com/xdan/jodit/issues/1109), which does not reproduce with a vanilla Shadow DOM. - **Tooltip tests**: added a regression test asserting that buttons inside the table-cell inline popup show their tooltip on hover. Covers [#1141](https://github.com/xdan/jodit/issues/1141), fixed back in 4.2.40. - **Image-processor tests**: added regression tests asserting that a base64 (`data:`) image is replaced with a `blob:` URL **only in the view** and never leaks into `editor.value`, the source element, the controlled-value loop (value re-assigned on every change, as jodit-react does) or the value left after `destruct()` (React remount). Covers the scenario from [#1244](https://github.com/xdan/jodit/issues/1244), which does not reproduce in the core editor. - **Selection tests**: added a regression test asserting that `editor.selection.insertHTML()` called after the editor lost focus (e.g. from a click handler on a non-focusable page element) inserts at the previously saved caret position instead of the start of the editor. Covers the scenario from [#1239](https://github.com/xdan/jodit/issues/1239), which does not reproduce on the current version in Chrome or Firefox. - **Color tests**: added a regression test asserting that applying a font color over a selection that includes a whole table recolors the text in **every** cell — including cells whose color sits on the `<td>` itself (as Word produces) rather than on inner spans. Covers the scenario from [#1221](https://github.com/xdan/jodit/issues/1221), which does not reproduce on the current version. - **Select-cells tests**: added a regression test asserting that the table cells popup stays open after a fast drag-select — including a pending trailing throttled `mousemove`, the Firefox redraw-hack cleanup, debounced history changes and the browser's `click` dispatched on the common `TR` ancestor. Covers the scenario from [#1174](https://github.com/xdan/jodit/issues/1174), which does not reproduce on the current version in Chrome or Firefox. - **Paste tests**: added regression tests asserting that Word's inter-word spaces placed in their own spans (`<span style="letter-spacing:-.5pt"> </span>`) survive pasting in every mode (*keep*, *clean*, *text*). Covers the scenario from [#1230](https://github.com/xdan/jodit/issues/1230), which does not reproduce on the current version (verified with the full table HTML from the issue). - **Resizer tests**: added a regression test asserting that in `iframe: true` + `editHTMLDocumentMode: true` mode the image resize frame is hidden when the iframe content is scrolled (the `iframe` plugin proxies the inner window's `scroll` event into the main window, where the `resizer` plugin listens). Covers the scenario from [#1266](https://github.com/xdan/jodit/issues/1266), which does not reproduce on the current version. ## 4.12.21 #### :bug: Bug Fix - **Security (stored XSS)**: the HTML sanitizer (`safeHTML`) stripped `on*` event handlers and `javascript:` links but left several executable constructs in the serialized editor value, so an application that re-rendered `editor.value` as trusted HTML could execute attacker script. It now also: drops `iframe[srcdoc]`; removes `data:text/html` / `data:application/xhtml` (and SVG `data:` URLs in `iframe`/`object`/`embed`/`frame`) sources; and strips `javascript:`/`vbscript:`/`livescript:`/`mocha:` from every URL-bearing attribute (`src`, `data`, `action`, `formaction`, `poster`, `background`, `xlink:href`), not just `<a href>`. Safe `data:image/*` sources (e.g. base64 PNG/SVG used in `<img>`) are preserved. Responsibly reported by Yuji Tounai ([@yousukezan](https://x.com/yousukezan)). - **Paste (Insert as Text)**: pasting multi-line content with the *Insert as Text* option lost its line breaks — the escaped text kept raw newlines that collapse to spaces when rendered. The *Insert as Text* path now converts newlines to `<br>` (gated on `nl2brInPlainText`, like the plain-text paste path). Fixes [#1093](https://github.com/xdan/jodit/issues/1093). - **Hotkeys**: the default keyboard shortcut for *Insert Unordered List* (`Ctrl/Cmd+Shift+8`) never fired — both variants were written as a single comma-joined string `'ctrl+shift+8, cmd+shift+8'` instead of two separate array entries, so the combined value never matched a real keypress. Fixes [#1079](https://github.com/xdan/jodit/issues/1079). - **Color / Brush button**: the brush (text/background color) button never reflected the color under the caret. Its `update` handler computed the current color into the icon fill but then **unconditionally** reset `icon.fill` to an empty string on every toolbar update, discarding it — which also made the icon render invisibly against some themes (`editorCssClass`). The computed fill is now kept when a color is present (and only cleared when there is none). Fixes [#195](https://github.com/xdan/jodit/issues/195) and [#182](https://github.com/xdan/jodit/issues/182). - **Toolbar / Selection**: when a text selection was started inside the editor and the mouse button was released **outside** of the editable area (a drag-select that ends over the page), the editor never received the `mouseup` event, so the active state of toolbar buttons (Bold, Italic, etc.) was not recalculated and stayed stale. A document-level `mouseup` listener now re-fires `changeSelection` when the selection still belongs to the editor, so the toolbar updates correctly. Fixes [#1251](https://github.com/xdan/jodit/issues/1251). - **Media embed (YouTube & Vimeo)**: real-world share URLs were not converted to an embedded player. For YouTube, short links of the form `https://youtu.be/<id>?si=…` (the *Share* button format) and `?t=` timestamp links were inserted as plain text — the video id sits in the path but was only read when there was no query string — and `/shorts/<id>` / `/live/<id>` produced a broken `embed/shorts/<id>` src. For Vimeo, `?share=copy` tracking params and `channels/<name>/<id>` / `groups/<name>/videos/<id>` URLs produced a broken `video/channels/…` src. `convertMediaUrlToVideoEmbed` now extracts the video id (and the unlisted Vimeo hash) from the URL path, ignoring tracking params and channel/group/`embed`/`shorts`/`live` prefixes, and also recognises the `m.youtube.com` (mobile) and `music.youtube.com` hosts. Fixes [#1209](https://github.com/xdan/jodit/issues/1209). - **Indent / Lists**: the *Decrease Indent* (outdent) toolbar button stayed disabled when the cursor was inside a nested list item, even though `Shift+Tab` could un-nest it. The button's enabled state only considered an inline indent margin and ignored list nesting. It is now also enabled when the cursor sits in a list whose parent is another list item (matching the `tab` plugin's outdent behaviour). Fixes [#1247](https://github.com/xdan/jodit/issues/1247). - **Uploader (base64)**: with `uploader.insertImageAsBase64URI = true`, dropping or pasting images in formats outside the default `imagesExtensions` list — notably `.svg`, `.bmp`, `.webp` — failed with `Need Url for Ajax Request` instead of being inlined as a data URI (the file fell through to the server-upload path). The default `imagesExtensions` now also includes `webp`, `bmp`, `svg`, `tiff` and `avif`. Fixes [#1228](https://github.com/xdan/jodit/issues/1228). - **Paste (Insert only Text)**: pasting multi-paragraph HTML with the *Insert only Text* option collapsed everything into a single paragraph — block boundaries became spaces, so the text could no longer be split into list items or separate blocks. `stripTags` gained an opt-in `blockBr` mode and the *Insert only Text* path now uses it (gated on `nl2brInPlainText`), so paragraphs