@mgdis/mg-components 6.23.0 → 6.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cjs/mg-action-more_36.cjs.entry.js +20 -12
  2. package/dist/cjs/mg-input-combobox.cjs.entry.js +7 -6
  3. package/dist/cjs/mg-input-rich-text-editor.cjs.entry.js +808 -135
  4. package/dist/cjs/mg-progress.cjs.entry.js +1 -1
  5. package/dist/collection/components/molecules/inputs/mg-input-combobox/mg-input-combobox.js +7 -6
  6. package/dist/collection/components/molecules/mg-message/mg-message.js +2 -2
  7. package/dist/collection/components/molecules/mg-modal/mg-modal.js +2 -2
  8. package/dist/collection/components/molecules/mg-panel/mg-panel.js +1 -1
  9. package/dist/collection/components/molecules/mg-popover/mg-popover-content/mg-popover-content.js +1 -1
  10. package/dist/collection/components/molecules/mg-popover/mg-popover.js +10 -2
  11. package/dist/collection/components/molecules/mg-progress/mg-progress.js +1 -1
  12. package/dist/collection/components/molecules/mg-skip-links/mg-skip-links.js +1 -1
  13. package/dist/collection/components/molecules/mg-tabs/mg-tabs.js +1 -1
  14. package/dist/components/mg-illustrated-message.js +1 -1
  15. package/dist/components/mg-input-combobox.js +1 -1
  16. package/dist/components/mg-input-rich-text-editor.js +238 -239
  17. package/dist/components/mg-message2.js +1 -1
  18. package/dist/components/mg-modal.js +1 -1
  19. package/dist/components/mg-panel.js +1 -1
  20. package/dist/components/mg-popover-content2.js +1 -1
  21. package/dist/components/mg-popover2.js +1 -1
  22. package/dist/components/mg-progress.js +1 -1
  23. package/dist/components/mg-skip-links.js +1 -1
  24. package/dist/components/mg-tabs2.js +1 -1
  25. package/dist/esm/mg-action-more_36.entry.js +20 -12
  26. package/dist/esm/mg-input-combobox.entry.js +7 -6
  27. package/dist/esm/mg-input-rich-text-editor.entry.js +808 -135
  28. package/dist/esm/mg-progress.entry.js +1 -1
  29. package/dist/mg-components/mg-components.esm.js +1 -1
  30. package/dist/mg-components/{p-236bb1d2.entry.js → p-08d3d957.entry.js} +1 -1
  31. package/dist/mg-components/p-9953b835.entry.js +1 -0
  32. package/dist/mg-components/{p-d698fa67.entry.js → p-9c0a9fc7.entry.js} +203 -204
  33. package/dist/mg-components/p-a338088e.entry.js +1 -0
  34. package/ide/intellij/web-types.json +1 -1
  35. package/package.json +18 -18
  36. package/dist/mg-components/p-79265ad3.entry.js +0 -1
  37. package/dist/mg-components/p-a0e2c886.entry.js +0 -1
@@ -6865,10 +6865,10 @@ var e = Object.create, t = Object.defineProperty, n = Object.getOwnPropertyDescr
6865
6865
  return;
6866
6866
  }
6867
6867
  }
6868
- if (c === "srcset") try {
6868
+ if (c === "srcset" || c === "imagesrcset") try {
6869
6869
  let e = o(n);
6870
6870
  if (e.forEach(function(e) {
6871
- H("srcset", e.url) && (e.evil = true);
6871
+ H(c, e.url) && (e.evil = true);
6872
6872
  }), e = p(e, function(e) {
6873
6873
  return !e.evil;
6874
6874
  }), e.length) n = h(p(e, function(e) {
@@ -6911,8 +6911,8 @@ var e = Object.create, t = Object.defineProperty, n = Object.getOwnPropertyDescr
6911
6911
  if (I) return;
6912
6912
  let n = N[N.length - 1], r;
6913
6913
  if (n && (r = n.tag, e = n.innerText === void 0 ? e : n.innerText), t.disallowedTagsMode === "completelyDiscard" && !C(r)) e = "";
6914
- else if ((t.disallowedTagsMode === "discard" || t.disallowedTagsMode === "completelyDiscard") && (r === "script" || r === "style")) b += e;
6915
- else if ((t.disallowedTagsMode === "discard" || t.disallowedTagsMode === "completelyDiscard") && (r === "textarea" || r === "xmp")) b += e;
6914
+ else if (r && C(r) && (t.disallowedTagsMode === "discard" || t.disallowedTagsMode === "completelyDiscard") && (r === "script" || r === "style")) b += e;
6915
+ else if (r && C(r) && (t.disallowedTagsMode === "discard" || t.disallowedTagsMode === "completelyDiscard") && (r === "textarea" || r === "xmp")) b += e;
6916
6916
  else if (!R) {
6917
6917
  let n = V(e, false);
6918
6918
  t.textFilter ? b += t.textFilter(n, r) : b += n;
@@ -7056,7 +7056,24 @@ var e = Object.create, t = Object.defineProperty, n = Object.getOwnPropertyDescr
7056
7056
  allowedSchemesAppliedToAttributes: [
7057
7057
  "href",
7058
7058
  "src",
7059
- "cite"
7059
+ "cite",
7060
+ "action",
7061
+ "formaction",
7062
+ "data",
7063
+ "xlink:href",
7064
+ "poster",
7065
+ "background",
7066
+ "ping",
7067
+ "longdesc",
7068
+ "usemap",
7069
+ "codebase",
7070
+ "classid",
7071
+ "archive",
7072
+ "profile",
7073
+ "manifest",
7074
+ "itemid",
7075
+ "dynsrc",
7076
+ "lowsrc"
7060
7077
  ],
7061
7078
  allowProtocolRelative: true,
7062
7079
  enforceHtmlBoundary: false,
@@ -7121,7 +7138,7 @@ var e = Object.create, t = Object.defineProperty, n = Object.getOwnPropertyDescr
7121
7138
  * Released under MIT see LICENSE.txt in the project root for license information.
7122
7139
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
7123
7140
  */
7124
- const APP_VERSION = "4.12.4";
7141
+ const APP_VERSION = "4.12.25";
7125
7142
  // prettier-ignore
7126
7143
  const ES = "es2020";
7127
7144
  const IS_ES_MODERN = true;
@@ -8568,12 +8585,12 @@ class Dom {
8568
8585
  * Check if element is element node
8569
8586
  */
8570
8587
  static isElement(node) {
8571
- var _a;
8572
8588
  if (!Dom.isNode(node)) {
8573
8589
  return false;
8574
8590
  }
8575
- const win = (_a = node.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
8576
- return Boolean(win && node.nodeType === Node.ELEMENT_NODE);
8591
+ // no `defaultView` requirement nodes of an inert document
8592
+ // (`DOMParser`, `implementation.createHTMLDocument`) are still elements
8593
+ return node.nodeType === Node.ELEMENT_NODE;
8577
8594
  }
8578
8595
  /**
8579
8596
  * Check if element is document fragment
@@ -8594,8 +8611,10 @@ class Dom {
8594
8611
  if (!Dom.isNode(node)) {
8595
8612
  return false;
8596
8613
  }
8614
+ // an inert document has no browsing context (`defaultView` is null),
8615
+ // but its nodes are same-realm HTMLElements
8597
8616
  const win = (_a = node.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
8598
- return Boolean(win && node instanceof win.HTMLElement);
8617
+ return node instanceof (win ? win.HTMLElement : HTMLElement);
8599
8618
  }
8600
8619
  /**
8601
8620
  * Check element is inline block
@@ -9162,11 +9181,22 @@ function isFastEqual(a, b) {
9162
9181
  * @module helpers/checker
9163
9182
  */
9164
9183
  /**
9165
- * Detect if string is HTML from MS Word or Excel
9184
+ * Detect if string is HTML from MS Word, Excel or LibreOffice/OpenOffice
9166
9185
  */
9167
9186
  function isHtmlFromWord(data) {
9168
9187
  return (data.search(/<meta.*?Microsoft Excel\s[\d].*?>/) !== -1 ||
9169
9188
  data.search(/<meta.*?Microsoft Word\s[\d].*?>/) !== -1 ||
9189
+ // `<meta name=ProgId content=Word.Document>` — attribute values are
9190
+ // unquoted in the raw Word clipboard fragment
9191
+ data.search(/<meta[^>]*?ProgId[^>]*?(Word|Excel)\./i) !== -1 ||
9192
+ // LibreOffice/OpenOffice Writer & Calc
9193
+ data.search(/<meta[^>]*?(LibreOffice|OpenOffice)/i) !== -1 ||
9194
+ // Office namespaces on the root element of the clipboard fragment
9195
+ data.search(/urn:schemas-microsoft-com:office:(word|excel)/) !== -1 ||
9196
+ // `class=MsoNormal` and friends (unquoted/quoted)
9197
+ data.search(/<\w[^>]*\sclass=("|')?Mso/) !== -1 ||
9198
+ // the raw Word clipboard uses SINGLE quotes: style='mso-…'
9199
+ data.search(/style='[^']*mso-/) !== -1 ||
9170
9200
  (data.search(/style="[^"]*mso-/) !== -1 && data.search(/<font/) !== -1));
9171
9201
  }
9172
9202
 
@@ -11235,6 +11265,17 @@ function applyStyles(html) {
11235
11265
  iframeDoc.open();
11236
11266
  iframeDoc.write(html);
11237
11267
  iframeDoc.close();
11268
+ // Word marks its auto-generated list markers (the literal
11269
+ // bullet/number, e.g. `1.` or `·`) with `mso-list:Ignore`.
11270
+ // They are display-only and must not be imported, otherwise
11271
+ // the marker text leaks into the content. Drop them before any
11272
+ // style normalization strips the `mso-list` hint. See #948
11273
+ Dom.each(iframeDoc.body, (node) => {
11274
+ if (Dom.isElement(node) &&
11275
+ /mso-list:\s*ignore/i.test(node.getAttribute('style') || '')) {
11276
+ Dom.safeRemove(node);
11277
+ }
11278
+ });
11238
11279
  try {
11239
11280
  for (let i = 0; i < iframeDoc.styleSheets.length; i += 1) {
11240
11281
  const rules = iframeDoc.styleSheets[i].cssRules;
@@ -11325,6 +11366,15 @@ function cleanFromWord(html) {
11325
11366
  Dom.unwrap(node);
11326
11367
  break;
11327
11368
  default:
11369
+ // Word marks its auto-generated list markers
11370
+ // (the literal bullet/number, e.g. `1.` or `·`)
11371
+ // with `mso-list:Ignore`. They are display-only
11372
+ // and must not be imported, otherwise the marker
11373
+ // text leaks into the content. See #948
11374
+ if (/mso-list:\s*ignore/i.test(node.getAttribute('style') || '')) {
11375
+ marks.push(node);
11376
+ break;
11377
+ }
11328
11378
  toArray(node.attributes).forEach((attr) => {
11329
11379
  if ([
11330
11380
  'src',
@@ -12591,6 +12641,15 @@ function fastClone(object) {
12591
12641
  * Released under MIT see LICENSE.txt in the project root for license information.
12592
12642
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
12593
12643
  */
12644
+ /**
12645
+ * Keys that must never be copied from a (potentially untrusted) config object —
12646
+ * assigning them during a recursive merge can reach and mutate
12647
+ * `Object.prototype` (prototype pollution, CWE-1321).
12648
+ */
12649
+ const UNSAFE_PROTO_KEYS = ['__proto__', 'constructor', 'prototype'];
12650
+ function isUnsafeProtoKey(key) {
12651
+ return UNSAFE_PROTO_KEYS.indexOf(key) !== -1;
12652
+ }
12594
12653
  /**
12595
12654
  * @example
12596
12655
  * ```js
@@ -12640,6 +12699,9 @@ function ConfigProto(options, proto, deep = 0) {
12640
12699
  }
12641
12700
  const newOpt = {};
12642
12701
  Object.keys(options).forEach(key => {
12702
+ if (isUnsafeProtoKey(key)) {
12703
+ return;
12704
+ }
12643
12705
  const opt = options[key], protoKey = proto ? proto[key] : null;
12644
12706
  if (isPlainObject(opt) && isPlainObject(protoKey) && !isAtom(opt)) {
12645
12707
  newOpt[key] = ConfigProto(opt, protoKey, deep + 1);
@@ -12700,6 +12762,9 @@ function ConfigFlatten(obj) {
12700
12762
  */
12701
12763
  function ConfigMerge(target, source) {
12702
12764
  Object.keys(source).forEach(key => {
12765
+ if (isUnsafeProtoKey(key)) {
12766
+ return;
12767
+ }
12703
12768
  const srcVal = source[key];
12704
12769
  const tgtVal = target[key];
12705
12770
  if (isPlainObject(srcVal) && isPlainObject(tgtVal) && !isAtom(srcVal)) {
@@ -12752,7 +12817,6 @@ const convertMediaUrlToVideoEmbed = (url, { width = 400, height = 345 } = {}) =>
12752
12817
  return url;
12753
12818
  }
12754
12819
  const parser = globalDocument.createElement('a');
12755
- const pattern1 = /(?:http?s?:\/\/)?(?:www\.)?(?:vimeo\.com)\/?(.+)/g;
12756
12820
  parser.href = url;
12757
12821
  if (!width) {
12758
12822
  width = 400;
@@ -12763,27 +12827,50 @@ const convertMediaUrlToVideoEmbed = (url, { width = 400, height = 345 } = {}) =>
12763
12827
  const protocol = parser.protocol || '';
12764
12828
  switch (parser.hostname) {
12765
12829
  case 'www.vimeo.com':
12766
- case 'vimeo.com':
12767
- return pattern1.test(url)
12768
- ? url.replace(pattern1, '<iframe width="' +
12769
- width +
12770
- '" height="' +
12771
- height +
12772
- '" src="' +
12773
- protocol +
12774
- '//player.vimeo.com/video/$1" frameborder="0" allowfullscreen></iframe>')
12775
- : url;
12830
+ case 'vimeo.com': {
12831
+ // The numeric video id can be preceded by `channels/<name>/` or
12832
+ // `groups/<name>/videos/` and followed by tracking params (e.g.
12833
+ // `?share=copy`). Unlisted videos keep a hash right after the id
12834
+ // (`vimeo.com/<id>/<hash>`). Extract the id (+ hash) from the path
12835
+ // so all of those forms produce a valid embed. See #1209
12836
+ const segments = parser.pathname.split('/').filter(Boolean);
12837
+ const idIndex = segments.findIndex(s => /^\d+$/.test(s));
12838
+ if (idIndex === -1) {
12839
+ return url;
12840
+ }
12841
+ let path = segments[idIndex];
12842
+ const hash = segments[idIndex + 1];
12843
+ if (hash && idIndex === 0) {
12844
+ path += '/' + hash;
12845
+ }
12846
+ return ('<iframe width="' +
12847
+ width +
12848
+ '" height="' +
12849
+ height +
12850
+ '" src="' +
12851
+ protocol +
12852
+ '//player.vimeo.com/video/' +
12853
+ path +
12854
+ '" frameborder="0" allowfullscreen></iframe>');
12855
+ }
12776
12856
  case 'youtube.com':
12777
12857
  case 'www.youtube.com':
12858
+ case 'm.youtube.com':
12859
+ case 'music.youtube.com':
12778
12860
  case 'youtu.be':
12779
12861
  case 'www.youtu.be': {
12780
- const query = parser.search
12781
- ? parseQuery(parser.search)
12782
- : { v: parser.pathname.substring(1) };
12783
- if (/^embed\/.*/.test(query.v)) {
12784
- query.v = query.v.substring(6);
12785
- }
12786
- return query.v
12862
+ const query = parser.search ? parseQuery(parser.search) : {};
12863
+ // `youtube.com/watch` keeps the video id in the `v` query
12864
+ // parameter, while the short `youtu.be/<id>` links and the
12865
+ // `/embed/`, `/shorts/`, `/live/` paths keep it in the pathname.
12866
+ // Modern share urls add tracking params (e.g. `?si=`, `?t=`), so
12867
+ // the pathname must still be used as a fallback when there is no
12868
+ // `v`. See #1209
12869
+ let v = query.v || parser.pathname.substring(1);
12870
+ v = v
12871
+ .replace(/^(watch|embed|shorts|live|v)\//, '')
12872
+ .replace(/\/$/, '');
12873
+ return v
12787
12874
  ? '<iframe width="' +
12788
12875
  width +
12789
12876
  '" height="' +
@@ -12791,7 +12878,7 @@ const convertMediaUrlToVideoEmbed = (url, { width = 400, height = 345 } = {}) =>
12791
12878
  '" src="' +
12792
12879
  protocol +
12793
12880
  '//www.youtube.com/embed/' +
12794
- query.v +
12881
+ v +
12795
12882
  '" frameborder="0" allowfullscreen></iframe>'
12796
12883
  : url;
12797
12884
  }
@@ -13177,21 +13264,22 @@ function safeHTML(box, options) {
13177
13264
  return;
13178
13265
  }
13179
13266
  const removeEvents = (_a = options.removeEventAttributes) !== null && _a !== void 0 ? _a : options.removeOnError;
13180
- if (removeEvents) {
13181
- removeAllEventAttributes(box);
13182
- $$('*', box).forEach(elm => removeAllEventAttributes(elm));
13183
- }
13184
- else if (options.removeOnError) {
13185
- sanitizeHTMLElement(box, options);
13186
- $$('[onerror]', box).forEach(elm => sanitizeHTMLElement(elm, options));
13187
- }
13188
- if (options.safeJavaScriptLink) {
13189
- sanitizeHTMLElement(box, options);
13190
- $$('a[href^="javascript"]', box).forEach(elm => sanitizeHTMLElement(elm, options));
13191
- }
13192
- if (options.safeLinksTarget) {
13193
- $$('a[target="_blank"]', box).forEach(elm => {
13194
- const rel = elm.getAttribute('rel') || '';
13267
+ // Single synchronous traversal of the subtree. Besides removing event
13268
+ // handlers and `javascript:` links, `sanitizeHTMLElement` neutralises
13269
+ // executable `iframe[srcdoc]`, `data:text/html` / SVG `data:` document
13270
+ // sources and dangerous schemes in every URL-bearing attribute.
13271
+ const process = (node) => {
13272
+ if (!Dom.isElement(node)) {
13273
+ return;
13274
+ }
13275
+ if (removeEvents) {
13276
+ removeAllEventAttributes(node);
13277
+ }
13278
+ sanitizeHTMLElement(node, options);
13279
+ if (options.safeLinksTarget &&
13280
+ node.nodeName === 'A' &&
13281
+ node.getAttribute('target') === '_blank') {
13282
+ const rel = node.getAttribute('rel') || '';
13195
13283
  const parts = rel.split(/\s+/).filter(Boolean);
13196
13284
  if (!parts.includes('noopener')) {
13197
13285
  parts.push('noopener');
@@ -13199,9 +13287,11 @@ function safeHTML(box, options) {
13199
13287
  if (!parts.includes('noreferrer')) {
13200
13288
  parts.push('noreferrer');
13201
13289
  }
13202
- attr(elm, 'rel', parts.join(' '));
13203
- });
13204
- }
13290
+ attr(node, 'rel', parts.join(' '));
13291
+ }
13292
+ };
13293
+ process(box);
13294
+ Dom.each(box, process);
13205
13295
  }
13206
13296
  /**
13207
13297
  * Remove all on* event handler attributes from an element
@@ -13223,6 +13313,39 @@ function removeAllEventAttributes(elm) {
13223
13313
  }
13224
13314
  return effected;
13225
13315
  }
13316
+ /**
13317
+ * URL-bearing attributes (besides `href`) that can load or execute content.
13318
+ */
13319
+ const URL_ATTRIBUTES = [
13320
+ 'src',
13321
+ 'data',
13322
+ 'action',
13323
+ 'formaction',
13324
+ 'poster',
13325
+ 'background',
13326
+ 'xlink:href'
13327
+ ];
13328
+ /**
13329
+ * Tags that load their URL as a *document* (scripts inside run). An SVG data
13330
+ * URL is only an XSS vector here — as an `<img>` source it renders inertly.
13331
+ */
13332
+ const DOCUMENT_EMBED_TAGS = new Set(['iframe', 'frame', 'object', 'embed']);
13333
+ /**
13334
+ * Detects executable / script-bearing URL schemes. The attribute value is
13335
+ * already HTML-entity-decoded by `getAttribute`, so only whitespace and
13336
+ * control characters (which browsers ignore inside a scheme) need stripping.
13337
+ */
13338
+ function isDangerousUrl(value, tagName) {
13339
+ // eslint-disable-next-line no-control-regex
13340
+ const normalized = value.replace(/[\u0000-\u0020]+/g, '').toLowerCase();
13341
+ if (/^(?:javascript|vbscript|livescript|mocha):/.test(normalized)) {
13342
+ return true;
13343
+ }
13344
+ if (/^data:(?:text\/html|application\/xhtml)/.test(normalized)) {
13345
+ return true;
13346
+ }
13347
+ return (/^data:image\/svg/.test(normalized) && DOCUMENT_EMBED_TAGS.has(tagName));
13348
+ }
13226
13349
  function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError } = {
13227
13350
  safeJavaScriptLink: true,
13228
13351
  removeOnError: true
@@ -13240,6 +13363,22 @@ function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError } = {
13240
13363
  attr(elm, 'href', location.protocol + '//' + href);
13241
13364
  effected = true;
13242
13365
  }
13366
+ if (safeJavaScriptLink) {
13367
+ // `srcdoc` runs its content as a full HTML document — drop it entirely.
13368
+ if (elm.hasAttribute('srcdoc')) {
13369
+ attr(elm, 'srcdoc', null);
13370
+ effected = true;
13371
+ }
13372
+ // Strip executable schemes from any other URL-bearing attribute.
13373
+ const tagName = elm.nodeName.toLowerCase();
13374
+ for (const name of URL_ATTRIBUTES) {
13375
+ const value = elm.getAttribute(name);
13376
+ if (value && isDangerousUrl(value, tagName)) {
13377
+ attr(elm, name, null);
13378
+ effected = true;
13379
+ }
13380
+ }
13381
+ }
13243
13382
  return effected;
13244
13383
  }
13245
13384
 
@@ -13265,7 +13404,7 @@ const ALONE_TAGS = new Set(['br', 'hr', 'input']);
13265
13404
  /**
13266
13405
  * Extract plain text from HTML text
13267
13406
  */
13268
- function stripTags(html, doc = document, exclude = null) {
13407
+ function stripTags(html, doc = document, exclude = null, blockBr = false) {
13269
13408
  const tmp = doc.createElement('div');
13270
13409
  if (isString(html)) {
13271
13410
  tmp.innerHTML = html;
@@ -13281,7 +13420,7 @@ function stripTags(html, doc = document, exclude = null) {
13281
13420
  if (exclude && Dom.isTag(p, exclude)) {
13282
13421
  const tag = p.nodeName.toLowerCase();
13283
13422
  const text = !Dom.isTag(p, ALONE_TAGS)
13284
- ? `%%%jodit-${tag}%%%${stripTags(p.innerHTML, doc, exclude)}%%%/jodit-${tag}%%%`
13423
+ ? `%%%jodit-${tag}%%%${stripTags(p.innerHTML, doc, exclude, blockBr)}%%%/jodit-${tag}%%%`
13285
13424
  : `%%%jodit-single-${tag}%%%`;
13286
13425
  Dom.before(p, doc.createTextNode(text));
13287
13426
  Dom.safeRemove(p);
@@ -13299,7 +13438,11 @@ function stripTags(html, doc = document, exclude = null) {
13299
13438
  return;
13300
13439
  }
13301
13440
  if (nx) {
13302
- pr.insertBefore(doc.createTextNode(' '), nx);
13441
+ // By default blocks are joined with a single space (single-line
13442
+ // plain text). When `blockBr` is set, separate them with a line
13443
+ // break instead, so paragraph structure survives — e.g. the
13444
+ // "Insert only Text" paste option. See #1232
13445
+ pr.insertBefore(doc.createTextNode(blockBr ? '%%%jodit-single-br%%%' : ' '), nx);
13303
13446
  }
13304
13447
  });
13305
13448
  return restoreTags(trim(tmp.innerText));
@@ -13506,6 +13649,53 @@ const getContentWidth = (element, win) => {
13506
13649
  return width - paddingLeft - paddingRight;
13507
13650
  };
13508
13651
 
13652
+ /*!
13653
+ * Jodit Editor (https://xdsoft.net/jodit/)
13654
+ * Released under MIT see LICENSE.txt in the project root for license information.
13655
+ * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
13656
+ */
13657
+ /**
13658
+ * Whether the element establishes a containing block for its
13659
+ * `position: fixed` descendants, so their coordinates become relative to it
13660
+ * instead of the viewport.
13661
+ */
13662
+ function isContainingBlockForFixed(style) {
13663
+ return ((style.transform !== '' && style.transform !== 'none') ||
13664
+ (style.perspective !== '' && style.perspective !== 'none') ||
13665
+ (style.filter !== '' && style.filter !== 'none') ||
13666
+ style.willChange === 'transform' ||
13667
+ style.willChange === 'perspective' ||
13668
+ style.willChange === 'filter' ||
13669
+ style.contain === 'paint' ||
13670
+ style.contain === 'layout' ||
13671
+ style.contain === 'strict' ||
13672
+ style.contain === 'content');
13673
+ }
13674
+ /**
13675
+ * Returns the viewport offset of the containing block of a `position: fixed`
13676
+ * descendant of `elm`. A fixed element is normally positioned relative to the
13677
+ * viewport, but an ancestor with `transform`, `filter`, `perspective`, etc.
13678
+ * establishes a new containing block, shifting the fixed element by that
13679
+ * ancestor's top-left corner. The returned offset should be subtracted from
13680
+ * the desired viewport coordinates before applying them.
13681
+ *
13682
+ * Returns `{ x: 0, y: 0 }` when no such ancestor exists (the common case), so
13683
+ * call sites keep their previous behaviour unchanged.
13684
+ */
13685
+ function getFixedPositionOffset(elm) {
13686
+ var _a;
13687
+ const win = (_a = elm.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
13688
+ let node = elm.parentElement;
13689
+ while (win && node) {
13690
+ if (isContainingBlockForFixed(win.getComputedStyle(node))) {
13691
+ const rect = node.getBoundingClientRect();
13692
+ return { x: rect.left, y: rect.top };
13693
+ }
13694
+ node = node.parentElement;
13695
+ }
13696
+ return { x: 0, y: 0 };
13697
+ }
13698
+
13509
13699
  /*!
13510
13700
  * Jodit Editor (https://xdsoft.net/jodit/)
13511
13701
  * Released under MIT see LICENSE.txt in the project root for license information.
@@ -13854,6 +14044,7 @@ var index = /*#__PURE__*/Object.freeze({
13854
14044
  getClassName: getClassName,
13855
14045
  getContentWidth: getContentWidth,
13856
14046
  getDataTransfer: getDataTransfer,
14047
+ getFixedPositionOffset: getFixedPositionOffset,
13857
14048
  getPropertyDescriptor: getPropertyDescriptor,
13858
14049
  getScrollParent: getScrollParent,
13859
14050
  getXPathByElement: getXPathByElement,
@@ -14148,6 +14339,14 @@ class ViewComponent extends Component {
14148
14339
  */
14149
14340
  setParentView(jodit) {
14150
14341
  this.jodit = jodit;
14342
+ // Inherit the owner window from the parent view — for an editor
14343
+ // created with a custom `ownerWindow` (e.g. inside an iframe) the
14344
+ // component default (the global `window`) is wrong: outside-click
14345
+ // handlers of dropdowns/popups listened to the wrong window. See
14346
+ // https://github.com/xdan/jodit/issues/965
14347
+ if (jodit.ow) {
14348
+ this.ownerWindow = jodit.ow;
14349
+ }
14151
14350
  jodit.components.add(this);
14152
14351
  return this;
14153
14352
  }
@@ -15900,9 +16099,14 @@ let UITooltip = UITooltip_1 = class UITooltip extends UIElement {
15900
16099
  this.setMod('above', false);
15901
16100
  this.getElm('content').innerHTML = content;
15902
16101
  const point = getPoint();
16102
+ // The tooltip is `position: fixed`. If it is rendered inside an ancestor
16103
+ // with a `transform` (e.g. an editor placed in a modal/flyout), that
16104
+ // ancestor becomes the containing block, so viewport coordinates must be
16105
+ // shifted by its offset. Returns `{0, 0}` when there is no such ancestor.
16106
+ const offset = getFixedPositionOffset(this.container);
15903
16107
  css(this.container, {
15904
- left: point.x,
15905
- top: point.y
16108
+ left: point.x - offset.x,
16109
+ top: point.y - offset.y
15906
16110
  });
15907
16111
  const tooltipPos = position(this.container);
15908
16112
  const viewHeight = this.j.ow.innerHeight;
@@ -15911,7 +16115,7 @@ let UITooltip = UITooltip_1 = class UITooltip extends UIElement {
15911
16115
  const targetPos = position(this.__currentTarget);
15912
16116
  this.setMod('above', true);
15913
16117
  css(this.container, {
15914
- top: targetPos.top - tooltipPos.height
16118
+ top: targetPos.top - tooltipPos.height - offset.y
15915
16119
  });
15916
16120
  }
15917
16121
  }
@@ -17056,8 +17260,11 @@ class Popup extends UIGroup {
17056
17260
  * Calculate static bound for point
17057
17261
  */
17058
17262
  getKeepBound(getBound) {
17263
+ var _a;
17059
17264
  const oldBound = getBound();
17060
- const elmUnderCursor = this.od.elementFromPoint(oldBound.left, oldBound.top);
17265
+ // Inside Shadow DOM `document.elementFromPoint` returns the shadow
17266
+ // host, so the lookup must start from the shadow root
17267
+ const elmUnderCursor = ((_a = this.j.o.shadowRoot) !== null && _a !== void 0 ? _a : this.od).elementFromPoint(oldBound.left, oldBound.top);
17061
17268
  if (!elmUnderCursor) {
17062
17269
  return getBound;
17063
17270
  }
@@ -17084,9 +17291,13 @@ class Popup extends UIGroup {
17084
17291
  }
17085
17292
  const [pos, strategy] = this.__calculatePosition(this.__targetBound(), this.viewBound(), position(this.container, this.j));
17086
17293
  this.setMod('strategy', strategy);
17294
+ // The popup is `position: fixed`; shift by the containing-block offset
17295
+ // when it is rendered inside a transformed ancestor (e.g. a modal),
17296
+ // otherwise `{0, 0}` keeps the viewport coordinates unchanged.
17297
+ const offset = getFixedPositionOffset(this.container);
17087
17298
  css(this.container, {
17088
- left: pos.left,
17089
- top: pos.top
17299
+ left: pos.left - offset.x,
17300
+ top: pos.top - offset.y
17090
17301
  });
17091
17302
  this.__childrenPopups.forEach(popup => popup.updatePosition());
17092
17303
  return this;
@@ -18689,7 +18900,7 @@ let Dialog = Dialog_1 = class Dialog extends ViewWithToolbar {
18689
18900
  e.stopImmediatePropagation();
18690
18901
  }
18691
18902
  if (this.resizable && this.o.resizable) {
18692
- this.setSize(this.startPoint.w + e.clientX - this.startX, this.startPoint.h + e.clientY - this.startY);
18903
+ this.setSize(Math.max(this.startPoint.w + e.clientX - this.startX, this.minSize.w), Math.max(this.startPoint.h + e.clientY - this.startY, this.minSize.h));
18693
18904
  if (this.e) {
18694
18905
  /**
18695
18906
  * Fired when dialog box is resized
@@ -18716,11 +18927,29 @@ let Dialog = Dialog_1 = class Dialog extends ViewWithToolbar {
18716
18927
  }
18717
18928
  }
18718
18929
  __onResizerMouseDown(e) {
18930
+ var _a, _b, _c;
18719
18931
  this.resizable = true;
18720
18932
  this.startX = e.clientX;
18721
18933
  this.startY = e.clientY;
18722
18934
  this.startPoint.w = this.dialog.offsetWidth;
18723
18935
  this.startPoint.h = this.dialog.offsetHeight;
18936
+ const header = this.getElm('header');
18937
+ const footer = this.getElm('footer');
18938
+ const content = this.getElm('content');
18939
+ // the content area does not shrink below its CSS `min-height`,
18940
+ // so it is part of the smallest height the panel can take
18941
+ const contentMinHeight = content
18942
+ ? parseFloat(this.ow.getComputedStyle(content).minHeight) || 0
18943
+ : 0;
18944
+ this.minSize.w = isNumber(this.o.minWidth)
18945
+ ? this.o.minWidth
18946
+ : Math.max(100, (_a = footer === null || footer === void 0 ? void 0 : footer.scrollWidth) !== null && _a !== void 0 ? _a : 0);
18947
+ this.minSize.h = isNumber(this.o.minHeight)
18948
+ ? this.o.minHeight
18949
+ : ((_b = header === null || header === void 0 ? void 0 : header.offsetHeight) !== null && _b !== void 0 ? _b : 0) +
18950
+ ((_c = footer === null || footer === void 0 ? void 0 : footer.offsetHeight) !== null && _c !== void 0 ? _c : 0) +
18951
+ contentMinHeight +
18952
+ this.resizer.offsetHeight;
18724
18953
  this.lockSelect();
18725
18954
  this.__addGlobalResizeListeners();
18726
18955
  if (this.e) {
@@ -19032,6 +19261,11 @@ let Dialog = Dialog_1 = class Dialog extends ViewWithToolbar {
19032
19261
  this.setPosition();
19033
19262
  }
19034
19263
  };
19264
+ /**
19265
+ * Minimal size the dialog can be resized to — the header and the footer
19266
+ * (with its buttons) must always stay inside the panel
19267
+ */
19268
+ this.minSize = { w: 0, h: 0 };
19035
19269
  this.isModal = false;
19036
19270
  /**
19037
19271
  * True, if dialog was opened
@@ -20908,12 +21142,17 @@ let ImageEditor = ImageEditor_1 = class ImageEditor extends ViewComponent {
20908
21142
  self.j.alert('The name should not be empty');
20909
21143
  return false;
20910
21144
  }
21145
+ self.j.e.fire('afterImageEditorSave', data, name);
20911
21146
  self.onSave(name, data, self.hide, (e) => {
20912
21147
  self.j.alert(e.message);
20913
21148
  });
20914
21149
  });
20915
21150
  break;
20916
21151
  case self.buttons.save:
21152
+ // Notify listeners that a crop/resize was applied,
21153
+ // passing the action box (action + box dimensions).
21154
+ // See https://github.com/xdan/jodit/issues/820
21155
+ self.j.e.fire('afterImageEditorSave', data);
20917
21156
  self.onSave(undefined, data, self.hide, (e) => {
20918
21157
  self.j.alert(e.message);
20919
21158
  });
@@ -22776,8 +23015,22 @@ class Table extends ViewComponent {
22776
23015
  }
22777
23016
  const nextRow = Dom.next(cell.parentNode, elm => Dom.isTag(elm, 'tr'), table);
22778
23017
  if (nextRow) {
22779
- if (box[rowIndex + 1][nextCell]) {
22780
- nextRow.insertBefore(cell, box[rowIndex + 1][nextCell]);
23018
+ // `box[rowIndex + 1][nextCell]` is the cell that
23019
+ // logically follows the moved cell in the next row, but
23020
+ // it may physically belong to an earlier `<tr>` (when it
23021
+ // spans down from a row above). Inserting before such a
23022
+ // cell throws `NotFoundError`, so look for the first
23023
+ // following cell that is actually a child of `nextRow`.
23024
+ let referenceCell = null;
23025
+ for (let nextColumn = nextCell; nextColumn < box[rowIndex + 1].length; nextColumn += 1) {
23026
+ const candidate = box[rowIndex + 1][nextColumn];
23027
+ if (candidate && candidate.parentNode === nextRow) {
23028
+ referenceCell = candidate;
23029
+ break;
23030
+ }
23031
+ }
23032
+ if (referenceCell) {
23033
+ nextRow.insertBefore(cell, referenceCell);
22781
23034
  }
22782
23035
  else {
22783
23036
  nextRow.appendChild(cell);
@@ -23682,13 +23935,38 @@ let ToolbarContent = class ToolbarContent extends UIButton {
23682
23935
  }
23683
23936
  /** @override */
23684
23937
  update() {
23685
- const content = this.control.getContent(this.j, this);
23938
+ var _a, _b, _c;
23939
+ const { control } = this;
23940
+ const content = control.getContent(this.j, this);
23686
23941
  if (isString(content) || content.parentNode !== this.container) {
23687
23942
  Dom.detach(this.container);
23688
23943
  this.container.appendChild(isString(content) ? this.j.create.fromHTML(content) : content);
23689
23944
  }
23945
+ // Content controls never went through the ToolbarButton status
23946
+ // calculation, so `isDisabled`/`isActive`/`update` declared on the
23947
+ // control were silently ignored (e.g. the FileBrowser Upload button
23948
+ // ignored the backend permissions). See
23949
+ // https://github.com/xdan/jodit/issues/1094
23950
+ this.state.disabled = Boolean((_a = control.isDisabled) === null || _a === void 0 ? void 0 : _a.call(control, this.j, this));
23951
+ this.state.activated = Boolean((_b = control.isActive) === null || _b === void 0 ? void 0 : _b.call(control, this.j, this));
23952
+ (_c = control.update) === null || _c === void 0 ? void 0 : _c.call(control, this.j, this);
23953
+ // The first update() runs before the state watchers are attached, so
23954
+ // apply the calculated state explicitly (the calls are idempotent)
23955
+ this.onChangeDisabled();
23956
+ this.onChangeActivated();
23690
23957
  super.update();
23691
23958
  }
23959
+ /**
23960
+ * The content is arbitrary HTML — propagate the disabled state to the
23961
+ * nested form controls (e.g. the file input of the Upload button),
23962
+ * otherwise they stay interactive.
23963
+ */
23964
+ onChangeDisabled() {
23965
+ super.onChangeDisabled();
23966
+ this.container
23967
+ .querySelectorAll('input,button,select,textarea')
23968
+ .forEach(elm => attr(elm, 'disabled', this.state.disabled || null));
23969
+ }
23692
23970
  /** @override */
23693
23971
  createContainer() {
23694
23972
  return this.j.c.span(this.componentName);
@@ -24127,6 +24405,15 @@ function sendFiles(uploader, files, handlerSuccess, handlerError, process) {
24127
24405
  if (!fileList.length) {
24128
24406
  return Promise.reject(error('Need files'));
24129
24407
  }
24408
+ // Client-side validation hook — returning `false` aborts the upload.
24409
+ // See https://github.com/xdan/jodit/issues/1329
24410
+ if (isFunction(o.beforeUpload)) {
24411
+ if (o.beforeUpload.call(uploader, fileList) === false) {
24412
+ const err = error('Upload canceled');
24413
+ (handlerError || o.defaultHandlerError).call(uploader, err);
24414
+ return Promise.reject(err);
24415
+ }
24416
+ }
24130
24417
  const promises = [];
24131
24418
  if (o.insertImageAsBase64URI) {
24132
24419
  readImagesWithReader(fileList, o.imagesExtensions, promises, uploader, handlerSuccess, o.defaultHandlerSuccess);
@@ -24246,7 +24533,17 @@ Config.prototype.enableDragAndDropFileToEditor = true;
24246
24533
  Config.prototype.uploader = {
24247
24534
  url: '',
24248
24535
  insertImageAsBase64URI: false,
24249
- imagesExtensions: ['jpg', 'png', 'jpeg', 'gif'],
24536
+ imagesExtensions: [
24537
+ 'jpg',
24538
+ 'jpeg',
24539
+ 'png',
24540
+ 'gif',
24541
+ 'webp',
24542
+ 'bmp',
24543
+ 'svg',
24544
+ 'tiff',
24545
+ 'avif'
24546
+ ],
24250
24547
  headers: null,
24251
24548
  data: null,
24252
24549
  filesVariableName(i) {
@@ -25028,12 +25325,19 @@ function getShadowRoot(jodit) {
25028
25325
  * `strong -> fontWeight 700`
25029
25326
  */
25030
25327
  function getNativeCSSValue(jodit, elm, key) {
25328
+ const root = getShadowRoot(jodit);
25329
+ // Reproduce the editor's inherited text color inside the isolated probe so
25330
+ // that a value equal to the editor's *effective* default (not only the
25331
+ // browser default black) is recognised. Without this, applying black after
25332
+ // the default font color was changed was wrongly treated as a no-op (#1311).
25333
+ const wrapper = jodit.create.element('div');
25334
+ wrapper.style.color = css(jodit.editor, 'color');
25031
25335
  const newElm = jodit.create.element(elm.tagName.toLowerCase());
25032
25336
  newElm.style.cssText = elm.style.cssText;
25033
- const root = getShadowRoot(jodit);
25034
- root.appendChild(newElm);
25337
+ wrapper.appendChild(newElm);
25338
+ root.appendChild(wrapper);
25035
25339
  const result = css(newElm, key);
25036
- Dom.safeRemove(newElm);
25340
+ Dom.safeRemove(wrapper);
25037
25341
  return result;
25038
25342
  }
25039
25343
 
@@ -25247,9 +25551,23 @@ function wrapUnwrappedText(style, elm, jodit) {
25247
25551
  */
25248
25552
  function wrap(commitStyle, font, jodit) {
25249
25553
  const wrapper = findOrCreateWrapper(commitStyle, font, jodit);
25250
- return commitStyle.elementIsList
25251
- ? wrapList(commitStyle, wrapper, jodit)
25252
- : Dom.replace(wrapper, commitStyle.element, jodit.createInside, true);
25554
+ if (commitStyle.elementIsList) {
25555
+ return wrapList(commitStyle, wrapper, jodit);
25556
+ }
25557
+ const newWrapper = Dom.replace(wrapper, commitStyle.element, jodit.createInside, true);
25558
+ if (commitStyle.elementIsBlock) {
25559
+ // Inline font styles left over from pasted content visually override
25560
+ // the new block format — e.g. an h2 with `font-weight: normal;
25561
+ // font-size: 24px` does not look like a heading at all, so the
25562
+ // command seems to do nothing. See
25563
+ // https://github.com/xdan/jodit/issues/1063
25564
+ css(newWrapper, 'fontSize', null);
25565
+ css(newWrapper, 'fontWeight', null);
25566
+ if (!attr(newWrapper, 'style')) {
25567
+ attr(newWrapper, 'style', null);
25568
+ }
25569
+ }
25570
+ return newWrapper;
25253
25571
  }
25254
25572
  const WRAP_NODES = new Set([
25255
25573
  'td',
@@ -27930,6 +28248,18 @@ let Jodit$1 = Jodit_1 = class Jodit extends ViewWithToolbar {
27930
28248
  (opt.uploader.url || opt.uploader.insertImageAsBase64URI)) {
27931
28249
  this.uploader.bind(this.editor);
27932
28250
  }
28251
+ else if (!opt.enableDragAndDropFileToEditor) {
28252
+ // Without a bound uploader nobody cancels a file drop, and
28253
+ // Firefox inserts the dropped image natively — the option
28254
+ // must mean "do nothing". See
28255
+ // https://github.com/xdan/jodit/issues/1077
28256
+ this.e.on(editor, 'drop', (e) => {
28257
+ var _a, _b;
28258
+ if ((_b = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b.length) {
28259
+ e.preventDefault();
28260
+ }
28261
+ });
28262
+ }
27933
28263
  // in initEditor - the editor could change
27934
28264
  if (!this.__elementToPlace.get(this.editor)) {
27935
28265
  this.__elementToPlace.set(this.editor, currentPlace);
@@ -28067,6 +28397,26 @@ let Jodit$1 = Jodit_1 = class Jodit extends ViewWithToolbar {
28067
28397
  }
28068
28398
  this.synchronizeValues();
28069
28399
  }
28400
+ })
28401
+ .on(this.ow, 'mouseup', (event) => {
28402
+ if (this.o.readonly || this.__isSilentChange) {
28403
+ return;
28404
+ }
28405
+ // When a selection is started inside the editor and the
28406
+ // mouse button is released outside of it, the editable
28407
+ // area never receives the `mouseup` event, so the toolbar
28408
+ // state (active buttons) is not recalculated. Re-fire the
28409
+ // event manually for that case while the selection still
28410
+ // belongs to the editor. See #1251
28411
+ const target = event.target;
28412
+ const insideEditor = Boolean(target &&
28413
+ isNumber(target.nodeType) &&
28414
+ editor.contains(target));
28415
+ if (insideEditor || !this.s.isInsideArea) {
28416
+ return;
28417
+ }
28418
+ this.e.fire('changeSelection');
28419
+ this.synchronizeValues();
28070
28420
  });
28071
28421
  }
28072
28422
  fetch(url, options) {
@@ -32560,7 +32910,7 @@ var tr = {
32560
32910
  'Insert as Text': 'Yazı olarak ekle',
32561
32911
  'Word Paste Detected': 'Word biçiminde yapıştırma algılandı',
32562
32912
  'The pasted content is coming from a Microsoft Word/Excel document. Do you want to keep the format or clean it up?':
32563
- 'Der Inhalt, den Sie einfügen, stammt aus einem Microsoft Word / Excel-Dokument. Möchten Sie das Format erhalten oder löschen?',
32913
+ 'Yapıştırılan içerik bir Microsoft Word/Excel belgesinden geliyor. Formatı korumak yoksa temizlemek mi istiyorsunuz?',
32564
32914
  'Insert only Text': 'Sadece yazıyı ekle',
32565
32915
  'File Browser': 'Dosya Listeleyici',
32566
32916
  'Error on load list': 'Liste yüklenirken hata oluştu',
@@ -33579,15 +33929,37 @@ function checkJoinNeighbors(jodit, fakeNode, backspace) {
33579
33929
  if (Dom.isElement(mainClosestBox) &&
33580
33930
  Dom.isContentEditable(mainClosestBox, jodit.editor)) {
33581
33931
  const sibling = Dom.findNotEmptySibling(mainClosestBox, backspace);
33932
+ // A line break before the cursor is a single unit: remove it instead of
33933
+ // merging the current element into the (void) `<br>`, which used to move
33934
+ // the whole element's content into the `<br>` and delete it (#1282).
33935
+ if (Dom.isTag(sibling, 'br')) {
33936
+ Dom.safeRemove(sibling);
33937
+ jodit.s.setCursorBefore(fakeNode);
33938
+ return true;
33939
+ }
33582
33940
  if (sibling &&
33583
33941
  (checkMoveListContent(jodit, mainClosestBox, sibling, backspace) ||
33584
- moveContentAndRemoveEmpty(jodit, mainClosestBox, sibling, backspace))) {
33942
+ moveContentAndRemoveEmpty(jodit, mainClosestBox, resolveTableSibling(sibling, backspace), backspace))) {
33585
33943
  jodit.s.setCursorBefore(fakeNode);
33586
33944
  return true;
33587
33945
  }
33588
33946
  }
33589
33947
  return false;
33590
33948
  }
33949
+ /**
33950
+ * Content cannot be merged into the `<table>` element itself — it would land
33951
+ * between the table sections (after `</tbody>`), which is invalid HTML and
33952
+ * gets foster-parented out of the table on the next parse. Merge into the
33953
+ * edge cell instead. See https://github.com/xdan/jodit/issues/1064
33954
+ * @private
33955
+ */
33956
+ function resolveTableSibling(sibling, backspace) {
33957
+ if (!Dom.isTag(sibling, 'table')) {
33958
+ return sibling;
33959
+ }
33960
+ const cells = sibling.querySelectorAll('td,th');
33961
+ return cells.length ? cells[backspace ? cells.length - 1 : 0] : null;
33962
+ }
33591
33963
  function checkMoveListContent(jodit, mainClosestBox, sibling, backspace) {
33592
33964
  // Process UL/LI/OL cases
33593
33965
  const siblingIsList = Dom.isTag(sibling, LIST_TAGS);
@@ -33733,6 +34105,14 @@ function getNextInlineSibling(sibling, backspace, root) {
33733
34105
  let nextSibling = Dom.sibling(sibling, backspace);
33734
34106
  if (!nextSibling && sibling.parentNode && sibling.parentNode !== root) {
33735
34107
  nextSibling = findMostNestedNeighbor(sibling, !backspace, root, true);
34108
+ // Do not cross a list-item boundary. At the start of a list item the
34109
+ // neighbor can be the text of the parent/previous item; deleting a char
34110
+ // there is wrong — the list cases must handle the outdent/merge (#1277).
34111
+ if (nextSibling &&
34112
+ Dom.closest(sibling, 'li', root) !==
34113
+ Dom.closest(nextSibling, 'li', root)) {
34114
+ return null;
34115
+ }
33736
34116
  }
33737
34117
  return nextSibling;
33738
34118
  }
@@ -34034,21 +34414,21 @@ function checkUnwrapFirstListItem(jodit, fakeNode, backspace) {
34034
34414
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
34035
34415
  */
34036
34416
  /**
34037
- * @module plugins/backspace
34038
- */
34039
- /**
34417
+ * Ordered delete/backspace cases with stable keys. The first one returning
34418
+ * `true` wins. The keys are stable across minified builds (function names are
34419
+ * mangled by terser) so they can be referenced by `delete.disableCases`.
34040
34420
  * @private
34041
34421
  */
34042
- const cases = [
34043
- checkRemoveUnbreakableElement,
34044
- checkRemoveContentNotEditable,
34045
- checkRemoveChar,
34046
- checkTableCell,
34047
- checkRemoveEmptyParent,
34048
- checkRemoveEmptyNeighbor,
34049
- checkJoinTwoLists,
34050
- checkJoinNeighbors,
34051
- checkUnwrapFirstListItem
34422
+ const casesMap = [
34423
+ ['remove-unbreakable', checkRemoveUnbreakableElement],
34424
+ ['remove-not-editable', checkRemoveContentNotEditable],
34425
+ ['remove-char', checkRemoveChar],
34426
+ ['table-cell', checkTableCell],
34427
+ ['remove-empty-parent', checkRemoveEmptyParent],
34428
+ ['remove-empty-neighbor', checkRemoveEmptyNeighbor],
34429
+ ['join-two-lists', checkJoinTwoLists],
34430
+ ['join-neighbors', checkJoinNeighbors],
34431
+ ['unwrap-first-list-item', checkUnwrapFirstListItem]
34052
34432
  ];
34053
34433
 
34054
34434
  /*!
@@ -34115,7 +34495,11 @@ class backspace extends Plugin {
34115
34495
  return false;
34116
34496
  }
34117
34497
  moveNodeInsideStart(jodit, fakeNode, backspace);
34118
- if (cases.some((func) => {
34498
+ const disabled = jodit.o.delete.disableCases;
34499
+ if (casesMap.some(([key, func]) => {
34500
+ if (disabled && disabled.has(key)) {
34501
+ return;
34502
+ }
34119
34503
  if (isFunction(func) &&
34120
34504
  func(jodit, fakeNode, backspace, mode)) {
34121
34505
  return true;
@@ -34798,8 +35182,9 @@ Config.prototype.controls.brush = {
34798
35182
  const update = (key, value) => {
34799
35183
  if (value && value !== css(editor.editor, key).toString()) {
34800
35184
  button.state.icon.fill = value;
34801
- return;
35185
+ return true;
34802
35186
  }
35187
+ return false;
34803
35188
  };
34804
35189
  if (color) {
34805
35190
  const mode = dataBind(button, 'color');
@@ -34809,8 +35194,16 @@ Config.prototype.controls.brush = {
34809
35194
  const current = editor.s.current();
34810
35195
  if (current && !button.state.disabled) {
34811
35196
  const currentBpx = Dom.closest(current, Dom.isElement, editor.editor) || editor.editor;
34812
- update('color', css(currentBpx, 'color').toString());
34813
- update('background-color', css(currentBpx, 'background-color').toString());
35197
+ // The icon's fill mirrors the current text/background color so the
35198
+ // button reflects the formatting under the caret. Both calls run so
35199
+ // that a background color (the second call) wins over the text color
35200
+ // when both are set. Keep the computed fill instead of resetting it
35201
+ // below. See #195, #182
35202
+ const hasColor = update('color', css(currentBpx, 'color').toString());
35203
+ const hasBackground = update('background-color', css(currentBpx, 'background-color').toString());
35204
+ if (hasColor || hasBackground) {
35205
+ return;
35206
+ }
34814
35207
  }
34815
35208
  button.state.icon.fill = '';
34816
35209
  button.state.activated = false;
@@ -34913,28 +35306,28 @@ function color(editor) {
34913
35306
  group: 'color'
34914
35307
  });
34915
35308
  const callback = (command, second, third) => {
35309
+ var _a;
34916
35310
  const colorHEX = normalizeColor(third);
34917
- switch (command) {
34918
- case 'background':
34919
- editor.s.commitStyle({
34920
- attributes: {
34921
- style: {
34922
- backgroundColor: !colorHEX
34923
- ? ''
34924
- : colorHEX
34925
- }
34926
- }
34927
- });
34928
- break;
34929
- case 'forecolor':
34930
- editor.s.commitStyle({
34931
- attributes: {
34932
- style: {
34933
- color: !colorHEX ? '' : colorHEX
34934
- }
34935
- }
34936
- });
34937
- break;
35311
+ const value = !colorHEX ? '' : colorHEX;
35312
+ const style = command === 'background'
35313
+ ? { backgroundColor: value }
35314
+ : { color: value };
35315
+ // Cells selected with the `select-cells` plugin drop or collapse the
35316
+ // native range, so `commitStyle` would paint a pending caret format
35317
+ // outside the table instead of the selection the user sees. Apply the
35318
+ // style to the content of every selected cell instead. See #1250
35319
+ const selectedCells = editor
35320
+ .getInstance('Table', editor.o)
35321
+ .getAllSelectedCells();
35322
+ if (selectedCells.length && editor.s.isCollapsed()) {
35323
+ selectedCells.forEach(cell => {
35324
+ editor.s.select(cell, true);
35325
+ editor.s.commitStyle({ attributes: { style } });
35326
+ });
35327
+ (_a = editor.s.sel) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
35328
+ }
35329
+ else {
35330
+ editor.s.commitStyle({ attributes: { style } });
34938
35331
  }
34939
35332
  editor.synchronizeValues();
34940
35333
  return false;
@@ -35236,12 +35629,17 @@ class enter extends Plugin {
35236
35629
  if (beforeEnter !== undefined) {
35237
35630
  return beforeEnter;
35238
35631
  }
35239
- if (!editor.s.isCollapsed()) {
35240
- editor.execCommand('Delete');
35241
- }
35242
- editor.s.focus();
35243
- this.onEnter(event);
35244
- editor.e.fire('afterEnter', event);
35632
+ // Delete-of-selection + new block must be a single history step,
35633
+ // otherwise pressing Enter over a selection needs two Ctrl+Z to undo
35634
+ // (the first only reverts to the intermediate empty state). #1292
35635
+ editor.history.snapshot.transaction(() => {
35636
+ if (!editor.s.isCollapsed()) {
35637
+ editor.execCommand('Delete');
35638
+ }
35639
+ editor.s.focus();
35640
+ this.onEnter(event);
35641
+ editor.e.fire('afterEnter', event);
35642
+ });
35245
35643
  editor.synchronizeValues(); // fire change
35246
35644
  return false;
35247
35645
  }
@@ -35411,6 +35809,25 @@ Config.prototype.controls.font = {
35411
35809
  .replace(/[^a-z0-9-]+/g, ',');
35412
35810
  }
35413
35811
  },
35812
+ // When no font is explicitly set, the computed font-family equals the
35813
+ // editor's own default (e.g. `-apple-system, …`). Return '' so the
35814
+ // button shows the `Default` list entry instead of that raw stack.
35815
+ // See #1370
35816
+ value: (editor) => {
35817
+ const current = editor.s.current();
35818
+ if (!current) {
35819
+ return;
35820
+ }
35821
+ const box = Dom.closest(current, Dom.isElement, editor.editor);
35822
+ if (!box) {
35823
+ return;
35824
+ }
35825
+ const value = css(box, 'font-family').toString();
35826
+ if (value === css(editor.editor, 'font-family').toString()) {
35827
+ return '';
35828
+ }
35829
+ return value;
35830
+ },
35414
35831
  tooltip: 'Font family'
35415
35832
  };
35416
35833
 
@@ -35547,7 +35964,7 @@ pluginSystem.add('formatBlock', formatBlock);
35547
35964
  Config.prototype.commandToHotkeys = {
35548
35965
  removeFormat: ['ctrl+shift+m', 'cmd+shift+m'],
35549
35966
  insertOrderedList: ['ctrl+shift+7', 'cmd+shift+7'],
35550
- insertUnorderedList: ['ctrl+shift+8, cmd+shift+8'],
35967
+ insertUnorderedList: ['ctrl+shift+8', 'cmd+shift+8'],
35551
35968
  selectall: ['ctrl+a', 'cmd+a']
35552
35969
  };
35553
35970
 
@@ -36012,13 +36429,15 @@ pluginSystem.add('image', image);
36012
36429
  * Released under MIT see LICENSE.txt in the project root for license information.
36013
36430
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
36014
36431
  */
36432
+ const isSafeHref = (href) => /^(https?:|mailto:|tel:)/i.test(href.trim()) &&
36433
+ !/^(javascript:|data:|vbscript:)/i.test(href.trim());
36015
36434
  var a = [
36016
36435
  {
36017
36436
  name: 'eye',
36018
36437
  tooltip: 'Open link',
36019
36438
  exec: (editor, current) => {
36020
36439
  const href = attr(current, 'href');
36021
- if (current && href) {
36440
+ if (current && href && isSafeHref(href)) {
36022
36441
  editor.ow.open(href);
36023
36442
  }
36024
36443
  }
@@ -36345,6 +36764,12 @@ class inlinePopup extends Plugin {
36345
36764
  constructor() {
36346
36765
  super(...arguments);
36347
36766
  this.type = null;
36767
+ /**
36768
+ * The user pressed a button inside the selection toolbar — after the
36769
+ * command fires `closeAllPopups`, the toolbar should be shown again
36770
+ * while the selection is still there. See #1238
36771
+ */
36772
+ this.__reopenSelectionPopup = false;
36348
36773
  this.snapRange = null;
36349
36774
  this.elmsList = keys$2(this.j.o.popup, false).filter(s => !this.isExcludedTarget(s));
36350
36775
  }
@@ -36460,9 +36885,52 @@ class inlinePopup extends Plugin {
36460
36885
  this.previousTarget = undefined;
36461
36886
  }
36462
36887
  })
36463
- .on([this.j.ew, this.j.ow], 'mouseup keyup', this.onSelectionEnd);
36888
+ .on([this.j.ew, this.j.ow], 'mouseup keyup', this.onSelectionEnd)
36889
+ .on([this.j.ew, this.j.ow], 'mousedown touchstart', this.__onDocumentMouseDown)
36890
+ .on('closeAllPopups', this.__onCloseAllPopups);
36464
36891
  this.addListenersForElements();
36465
36892
  }
36893
+ __onDocumentMouseDown(e) {
36894
+ if (this.popup.isOpened &&
36895
+ this.type === 'selection' &&
36896
+ e.target &&
36897
+ UIElement.closestElement(e.target, Popup)) {
36898
+ this.__reopenSelectionPopup = true;
36899
+ }
36900
+ }
36901
+ __onCloseAllPopups() {
36902
+ if (!this.__reopenSelectionPopup) {
36903
+ return;
36904
+ }
36905
+ this.__reopenSelectionPopup = false;
36906
+ if (!this.j.o.toolbarInlineForSelection) {
36907
+ return;
36908
+ }
36909
+ // a zero timeout would run synchronously — before the popup's own
36910
+ // `closeAllPopups` handler closes it; defer to the next macrotask
36911
+ this.j.async.setTimeout(() => {
36912
+ const sel = this.j.s.sel;
36913
+ if (sel && !sel.isCollapsed) {
36914
+ this.showPopup(() => this.__selectionBound(), 'selection');
36915
+ }
36916
+ }, 1);
36917
+ }
36918
+ /**
36919
+ * The selection rect comes from the editor document — in iframe mode its
36920
+ * coordinates are iframe-local, while the popup lives in the host
36921
+ * document, so the iframe offset must be added. See
36922
+ * https://github.com/xdan/jodit/issues/1058
36923
+ */
36924
+ __selectionBound() {
36925
+ const rect = this.j.s.range.getBoundingClientRect();
36926
+ let { left, top } = rect;
36927
+ if (this.j.iframe) {
36928
+ const offset = position(this.j.iframe, this.j, true);
36929
+ left += offset.left;
36930
+ top += offset.top;
36931
+ }
36932
+ return { left, top, width: rect.width, height: rect.height };
36933
+ }
36466
36934
  onSelectionStart() {
36467
36935
  this.snapRange = this.j.s.range.cloneRange();
36468
36936
  }
@@ -36502,7 +36970,7 @@ class inlinePopup extends Plugin {
36502
36970
  if (!node) {
36503
36971
  return;
36504
36972
  }
36505
- this.showPopup(() => range.getBoundingClientRect(), type);
36973
+ this.showPopup(() => this.__selectionBound(), type);
36506
36974
  }
36507
36975
  /**
36508
36976
  * In not collapsed selection - only one image
@@ -36524,7 +36992,9 @@ class inlinePopup extends Plugin {
36524
36992
  beforeDestruct(jodit) {
36525
36993
  jodit.e
36526
36994
  .off('showPopup')
36527
- .off([this.j.ew, this.j.ow], 'mouseup keyup', this.onSelectionEnd);
36995
+ .off([this.j.ew, this.j.ow], 'mouseup keyup', this.onSelectionEnd)
36996
+ .off([this.j.ew, this.j.ow], 'mousedown touchstart', this.__onDocumentMouseDown)
36997
+ .off('closeAllPopups', this.__onCloseAllPopups);
36528
36998
  this.removeListenersForElements();
36529
36999
  }
36530
37000
  _eventsList() {
@@ -36573,6 +37043,12 @@ __decorate$c([
36573
37043
  __decorate$c([
36574
37044
  watch(':outsideClick')
36575
37045
  ], inlinePopup.prototype, "onOutsideClick", null);
37046
+ __decorate$c([
37047
+ autobind
37048
+ ], inlinePopup.prototype, "__onDocumentMouseDown", null);
37049
+ __decorate$c([
37050
+ autobind
37051
+ ], inlinePopup.prototype, "__onCloseAllPopups", null);
36576
37052
  __decorate$c([
36577
37053
  autobind
36578
37054
  ], inlinePopup.prototype, "onSelectionStart", null);
@@ -36597,7 +37073,7 @@ var unlinkIcon = "<svg xmlns='http://www.w3.org/2000/svg' viewBox=\"0 0 1792 179
36597
37073
  * Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
36598
37074
  */
36599
37075
  const formTemplate = (editor) => {
36600
- const { openInNewTabCheckbox, noFollowCheckbox, modeClassName, selectSizeClassName, selectMultipleClassName, selectOptionsClassName } = editor.o.link;
37076
+ const { openInNewTabCheckbox, noFollowCheckbox, ariaLabelInput, modeClassName, selectSizeClassName, selectMultipleClassName, selectOptionsClassName } = editor.o.link;
36601
37077
  return new UIForm(editor, [
36602
37078
  new UIBlock(editor, [
36603
37079
  new UIInput(editor, {
@@ -36609,6 +37085,15 @@ const formTemplate = (editor) => {
36609
37085
  required: true
36610
37086
  })
36611
37087
  ]),
37088
+ ariaLabelInput
37089
+ ? new UIBlock(editor, [
37090
+ new UIInput(editor, {
37091
+ name: 'ariaLabel',
37092
+ ref: 'aria_label_input',
37093
+ label: 'Aria label'
37094
+ })
37095
+ ])
37096
+ : null,
36612
37097
  new UIBlock(editor, [
36613
37098
  new UIInput(editor, {
36614
37099
  name: 'content',
@@ -36687,6 +37172,7 @@ Config.prototype.link = {
36687
37172
  noFollowCheckbox: true,
36688
37173
  openInNewTabCheckbox: true,
36689
37174
  openInNewTabCheckboxDefaultChecked: false,
37175
+ ariaLabelInput: false,
36690
37176
  modeClassName: 'input',
36691
37177
  selectMultipleClassName: true,
36692
37178
  preventReadOnlyNavigation: true,
@@ -36835,7 +37321,7 @@ class link extends Plugin {
36835
37321
  const currentElement = current;
36836
37322
  const isImageContent = Dom.isImage(currentElement);
36837
37323
  let { content_input } = elements;
36838
- const { className_input } = elements, { className_select } = elements;
37324
+ const { className_input } = elements, { className_select } = elements, { aria_label_input } = elements;
36839
37325
  if (!content_input) {
36840
37326
  content_input = jodit.c.element('input', {
36841
37327
  type: 'hidden',
@@ -36861,6 +37347,9 @@ class link extends Plugin {
36861
37347
  if (!isImageContent && current) {
36862
37348
  content_input.value = getSelectionText();
36863
37349
  }
37350
+ if (aria_label_input) {
37351
+ aria_label_input.value = link ? attr(link, 'aria-label') || '' : '';
37352
+ }
36864
37353
  if (link) {
36865
37354
  url_input.value = attr(link, 'href') || '';
36866
37355
  if (modeClassName) {
@@ -36951,6 +37440,10 @@ class link extends Plugin {
36951
37440
  }
36952
37441
  attr(a, 'rel', relParts.length ? relParts.join(' ') : null);
36953
37442
  }
37443
+ if (aria_label_input) {
37444
+ const ariaLabel = aria_label_input.value.trim();
37445
+ attr(a, 'aria-label', ariaLabel || null);
37446
+ }
36954
37447
  jodit.e.fire('applyLink', jodit, a, form);
36955
37448
  });
36956
37449
  jodit.synchronizeValues();
@@ -38139,6 +38632,12 @@ function removeExtraBr(jodit, node) {
38139
38632
  if (!Dom.isTag(br, 'br')) {
38140
38633
  return;
38141
38634
  }
38635
+ // Only a trailing `<br>` is "extra". If meaningful content follows it,
38636
+ // the `<br>` is a real line break and removing it would merge two lines
38637
+ // (e.g. toggling a style on an empty line), so keep it (#1302).
38638
+ if (Dom.findNotEmptySibling(br, false)) {
38639
+ return;
38640
+ }
38142
38641
  jodit.s.setCursorBefore(br);
38143
38642
  Dom.safeRemove(br);
38144
38643
  }
@@ -38406,6 +38905,68 @@ class select extends Plugin {
38406
38905
  s.expandSelection();
38407
38906
  return false;
38408
38907
  }
38908
+ /**
38909
+ * Fix caret position when clicking to the right of a list item that has a
38910
+ * nested list. Blink/WebKit place the caret at the start of the line instead
38911
+ * of the end (#1296); move it to the end of the item's own text.
38912
+ */
38913
+ onClickRightOfNestedListItem(e) {
38914
+ const { s } = this.j;
38915
+ const range = s.range;
38916
+ if (!range.collapsed ||
38917
+ range.startOffset !== 0 ||
38918
+ !Dom.isText(range.startContainer)) {
38919
+ return;
38920
+ }
38921
+ const text = range.startContainer;
38922
+ const li = text.parentNode;
38923
+ // The text must be the direct content of a list item that has a nested
38924
+ // list (the last level has no nested list and behaves correctly).
38925
+ if (!Dom.isTag(li, 'li') ||
38926
+ !(li.querySelector('ul') || li.querySelector('ol'))) {
38927
+ return;
38928
+ }
38929
+ const measure = this.j.ed.createRange();
38930
+ measure.selectNodeContents(text);
38931
+ const rect = measure.getBoundingClientRect();
38932
+ // Only when the click happened to the right of the text.
38933
+ if (e.clientX > rect.right) {
38934
+ s.setCursorAfter(text);
38935
+ }
38936
+ }
38937
+ /**
38938
+ * Keep pending inline formatting after a click. Toggling Bold/Italic/etc. on
38939
+ * a collapsed cursor leaves empty marker elements with the caret inside; a
38940
+ * click puts the caret right before them (outside), so they get cleaned up
38941
+ * and the formatting is lost. Move the caret back into the innermost marker
38942
+ * so the next typed character keeps every pending format (#1291).
38943
+ */
38944
+ onClickKeepPendingFormat() {
38945
+ var _a, _b;
38946
+ const { s } = this.j;
38947
+ const range = s.range;
38948
+ if (!range.collapsed || !Dom.isText(range.startContainer)) {
38949
+ return;
38950
+ }
38951
+ const text = range.startContainer;
38952
+ // Caret must sit at the very end of the text, right before the markers.
38953
+ if (range.startOffset !== ((_b = (_a = text.nodeValue) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0)) {
38954
+ return;
38955
+ }
38956
+ const marker = text.nextSibling;
38957
+ if (!Dom.isElement(marker) ||
38958
+ !marker.nodeName.match(IS_INLINE) ||
38959
+ !Dom.isEmpty(marker)) {
38960
+ return;
38961
+ }
38962
+ let inner = marker;
38963
+ // Descend into the innermost empty formatting marker.
38964
+ while (Dom.isElement(inner.firstElementChild) &&
38965
+ inner.firstElementChild.nodeName.match(IS_INLINE)) {
38966
+ inner = inner.firstElementChild;
38967
+ }
38968
+ s.setCursorIn(inner);
38969
+ }
38409
38970
  /**
38410
38971
  * Normalize selection after triple click
38411
38972
  */
@@ -38446,6 +39007,12 @@ __decorate$5([
38446
39007
  __decorate$5([
38447
39008
  watch([':beforeCommandSelectall'])
38448
39009
  ], select.prototype, "beforeCommandSelectAll", null);
39010
+ __decorate$5([
39011
+ watch([':click'])
39012
+ ], select.prototype, "onClickRightOfNestedListItem", null);
39013
+ __decorate$5([
39014
+ watch([':click'])
39015
+ ], select.prototype, "onClickKeepPendingFormat", null);
38449
39016
  __decorate$5([
38450
39017
  watch([':click'])
38451
39018
  ], select.prototype, "onTripleClickNormalizeSelection", null);
@@ -38665,7 +39232,11 @@ class resizer extends Plugin {
38665
39232
  this.pointerX = e.clientX;
38666
39233
  this.pointerY = e.clientY;
38667
39234
  let diff_x, diff_y;
38668
- if (this.j.options.iframe) {
39235
+ if (this.j.options.iframe && e.view === this.j.ew) {
39236
+ // the event was proxied from the editor's iframe — its client
39237
+ // coordinates are relative to the iframe viewport, while
39238
+ // `startX/startY` were captured on a host-document handle;
39239
+ // shift them into the host coordinate space
38669
39240
  const workplacePosition = this.getWorkplacePosition();
38670
39241
  diff_x = e.clientX + workplacePosition.left - this.startX;
38671
39242
  diff_y = e.clientY + workplacePosition.top - this.startY;
@@ -39005,15 +39576,42 @@ class selectCells extends Plugin {
39005
39576
  /**
39006
39577
  * Mouse click inside the table
39007
39578
  */
39008
- onStartSelection(cell) {
39579
+ onStartSelection(cell, event) {
39580
+ var _a;
39009
39581
  if (this.j.o.readonly) {
39010
39582
  return;
39011
39583
  }
39584
+ const table = Dom.closest(cell, 'table', this.j.editor);
39585
+ // Ctrl/Cmd + click toggles a single cell into the existing selection
39586
+ // instead of resetting it — non-contiguous multi-cell selection.
39587
+ // See https://github.com/xdan/jodit/issues/1163
39588
+ if (((event === null || event === void 0 ? void 0 : event.ctrlKey) || (event === null || event === void 0 ? void 0 : event.metaKey)) &&
39589
+ cell !== this.j.editor &&
39590
+ table &&
39591
+ Dom.isCell(cell)) {
39592
+ if (this.__tableModule.getAllSelectedCells().includes(cell)) {
39593
+ this.__tableModule.removeSelection(cell);
39594
+ }
39595
+ else {
39596
+ if (!cell.firstChild) {
39597
+ cell.appendChild(this.j.createInside.element('br'));
39598
+ }
39599
+ this.__tableModule.addSelection(cell);
39600
+ }
39601
+ this.__selectedCell = cell;
39602
+ (_a = this.j.s.sel) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
39603
+ if (this.__tableModule.getAllSelectedCells().length) {
39604
+ this.j.e.fire('showPopup', table, () => position(cell, this.j), 'cells');
39605
+ }
39606
+ else {
39607
+ this.j.e.fire('hidePopup', 'cells');
39608
+ }
39609
+ return false;
39610
+ }
39012
39611
  this.unselectCells();
39013
39612
  if (cell === this.j.editor) {
39014
39613
  return;
39015
39614
  }
39016
- const table = Dom.closest(cell, 'table', this.j.editor);
39017
39615
  if (!cell || !table) {
39018
39616
  return;
39019
39617
  }
@@ -39046,14 +39644,16 @@ class selectCells extends Plugin {
39046
39644
  * Mouse move inside the table
39047
39645
  */
39048
39646
  __onMove(table, e) {
39049
- var _a;
39647
+ var _a, _b;
39050
39648
  if (this.j.o.readonly && !this.j.isLocked) {
39051
39649
  return;
39052
39650
  }
39053
39651
  if (this.j.isLockedNotBy(key$1)) {
39054
39652
  return;
39055
39653
  }
39056
- const node = this.j.ed.elementFromPoint(e.clientX, e.clientY);
39654
+ // Inside Shadow DOM `document.elementFromPoint` returns the shadow
39655
+ // host, so the lookup must start from the shadow root
39656
+ const node = ((_a = this.j.o.shadowRoot) !== null && _a !== void 0 ? _a : this.j.ed).elementFromPoint(e.clientX, e.clientY);
39057
39657
  if (!node) {
39058
39658
  return;
39059
39659
  }
@@ -39076,7 +39676,7 @@ class selectCells extends Plugin {
39076
39676
  }
39077
39677
  const cellsCount = this.__tableModule.getAllSelectedCells().length;
39078
39678
  if (cellsCount > 1) {
39079
- (_a = this.j.s.sel) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
39679
+ (_b = this.j.s.sel) === null || _b === void 0 ? void 0 : _b.removeAllRanges();
39080
39680
  }
39081
39681
  this.j.e.fire('hidePopup');
39082
39682
  e.stopPropagation();
@@ -39110,12 +39710,15 @@ class selectCells extends Plugin {
39110
39710
  * Stop a selection process
39111
39711
  */
39112
39712
  __onStopSelection(table, e) {
39713
+ var _a, _b, _c;
39113
39714
  if (!this.__selectedCell) {
39114
39715
  return;
39115
39716
  }
39116
39717
  this.__isSelectionMode = false;
39117
39718
  this.j.unlock();
39118
- const node = this.j.ed.elementFromPoint(e.clientX, e.clientY);
39719
+ // Inside Shadow DOM `document.elementFromPoint` returns the shadow
39720
+ // host, so the lookup must start from the shadow root
39721
+ const node = ((_a = this.j.o.shadowRoot) !== null && _a !== void 0 ? _a : this.j.ed).elementFromPoint(e.clientX, e.clientY);
39119
39722
  if (!node) {
39120
39723
  return;
39121
39724
  }
@@ -39131,7 +39734,16 @@ class selectCells extends Plugin {
39131
39734
  cell,
39132
39735
  this.__selectedCell
39133
39736
  ]), box = this.__tableModule.formalMatrix(table);
39134
- const max = box[bound[1][0]][bound[1][1]], min = box[bound[0][0]][bound[0][1]];
39737
+ const max = (_b = box[bound[1][0]]) === null || _b === void 0 ? void 0 : _b[bound[1][1]], min = (_c = box[bound[0][0]]) === null || _c === void 0 ? void 0 : _c[bound[0][1]];
39738
+ // `getSelectedBound` keeps its `Infinity` sentinel when none of the
39739
+ // selected cells belong to this table's matrix — e.g. after a
39740
+ // drag-and-drop that moved/removed the cells, leaving a stale anchor and
39741
+ // a drop target outside the table. Bail out instead of dereferencing an
39742
+ // out-of-range matrix slot (which threw `Cannot read properties of
39743
+ // undefined`).
39744
+ if (!min || !max) {
39745
+ return;
39746
+ }
39135
39747
  this.j.e.fire('showPopup', table, () => {
39136
39748
  const minOffset = position(min, this.j), maxOffset = position(max, this.j);
39137
39749
  return {
@@ -39164,6 +39776,21 @@ class selectCells extends Plugin {
39164
39776
  * Execute custom commands for table
39165
39777
  */
39166
39778
  onExecCommand(command) {
39779
+ // When several cells are selected, Backspace/Delete must clear the
39780
+ // selected cells instead of running the normal delete (whose collapsed
39781
+ // document-level range threw `insertNode ... #document`). See #1273.
39782
+ if (/^(delete|backspace)(word|sentence)?button$/.test(command) &&
39783
+ this.__tableModule.getAllSelectedCells().length > 1) {
39784
+ const cells = this.__tableModule.getAllSelectedCells();
39785
+ cells.forEach(td => {
39786
+ Dom.detach(td);
39787
+ td.appendChild(this.j.createInside.element('br'));
39788
+ });
39789
+ this.unselectCells();
39790
+ this.j.s.setCursorIn(cells[0]);
39791
+ this.j.synchronizeValues();
39792
+ return false;
39793
+ }
39167
39794
  if (/table(splitv|splitg|merge|empty|bin|binrow|bincolumn|addcolumn|addrow)/.test(command)) {
39168
39795
  command = command.replace('table', '');
39169
39796
  const cells = this.__tableModule.getAllSelectedCells();
@@ -39710,6 +40337,7 @@ Config.prototype.cleanHTML = {
39710
40337
  timeout: 300,
39711
40338
  removeEmptyElements: true,
39712
40339
  fillEmptyParagraph: true,
40340
+ collapseEmptyValueToEmptyString: false,
39713
40341
  replaceNBSP: true,
39714
40342
  replaceOldTags: {
39715
40343
  i: 'em',
@@ -39885,12 +40513,19 @@ function removeFormatForSelection(jodit) {
39885
40513
  * @private
39886
40514
  */
39887
40515
  function allowAttributes(jodit, nodeElm, hadEffect, allow) {
39888
- if (allow && Dom.isElement(nodeElm) && allow[nodeElm.nodeName] !== true) {
40516
+ const allowedForTag = allow && Dom.isElement(nodeElm) && allow[nodeElm.nodeName];
40517
+ if (allow && Dom.isElement(nodeElm) && allowedForTag !== true) {
40518
+ // the tag is not in the allow list at all — attributes do not matter,
40519
+ // the element itself will be removed by the tags filter. Without this
40520
+ // check `allow[nodeName][attr]` threw on e.g. `<meta charset>`. See #1224
40521
+ if (!allowedForTag) {
40522
+ return hadEffect;
40523
+ }
39889
40524
  const attrs = nodeElm.attributes;
39890
40525
  if (attrs && attrs.length) {
39891
40526
  const removeAttrs = [];
39892
40527
  for (let i = 0; i < attrs.length; i += 1) {
39893
- const attr = allow[nodeElm.nodeName][attrs[i].name];
40528
+ const attr = allowedForTag[attrs[i].name];
39894
40529
  if (!attr || (attr !== true && attr !== attrs[i].value)) {
39895
40530
  removeAttrs.push(attrs[i].name);
39896
40531
  }
@@ -40228,11 +40863,18 @@ function isRemovableNode(jodit, node, current, allow, deny) {
40228
40863
  if (!jodit.o.cleanHTML.removeEmptyElements) {
40229
40864
  return false;
40230
40865
  }
40866
+ // Never drop an empty inline element that currently holds the caret — it is
40867
+ // a pending-format marker the user is about to type into (#1291). `current`
40868
+ // is captured before a click moves the caret, so also check the live caret.
40869
+ const liveCaret = jodit.s.isCollapsed()
40870
+ ? jodit.s.range.startContainer
40871
+ : null;
40231
40872
  return (Dom.isElement(node) &&
40232
40873
  node.nodeName.match(IS_INLINE) != null &&
40233
40874
  !Dom.isTemporary(node) &&
40234
40875
  trimInv(node.innerHTML).length === 0 &&
40235
- (current == null || !Dom.isOrContains(node, current)));
40876
+ (current == null || !Dom.isOrContains(node, current)) &&
40877
+ (liveCaret == null || !Dom.isOrContains(node, liveCaret)));
40236
40878
  }
40237
40879
 
40238
40880
  /*!
@@ -40367,14 +41009,33 @@ class cleanHtml extends Plugin {
40367
41009
  onBeforeSetNativeEditorValue(data) {
40368
41010
  const [sandBox, iframe] = this.j.o.cleanHTML.useIframeSandbox
40369
41011
  ? this.j.createInside.sandbox()
40370
- : [this.j.createInside.div()];
41012
+ : [
41013
+ // an inert document never loads sub-resources, so the
41014
+ // images in the value are not re-requested from the
41015
+ // server on every assignment (e.g. on each change in
41016
+ // jodit-react). See #1237
41017
+ this.j.od.implementation.createHTMLDocument('').body
41018
+ ];
40371
41019
  sandBox.innerHTML = data.value;
40372
- this.onSafeHTML(sandBox);
41020
+ this.j.e.fire('safeHTML', sandBox);
40373
41021
  data.value = sandBox.innerHTML;
40374
41022
  safeHTML(sandBox, { safeJavaScriptLink: true, removeOnError: true });
40375
41023
  Dom.safeRemove(iframe);
40376
41024
  return false;
40377
41025
  }
41026
+ /**
41027
+ * Collapse a value that holds only a single empty block (e.g.
41028
+ * `<p><br></p>` left after deleting all content) to an empty string —
41029
+ * opt-in via `cleanHTML.collapseEmptyValueToEmptyString`. See #1149
41030
+ */
41031
+ onAfterGetValueFromEditor(data) {
41032
+ if (!this.j.o.cleanHTML.collapseEmptyValueToEmptyString) {
41033
+ return;
41034
+ }
41035
+ if (/^<([a-z][a-z0-9]*)\b[^>]*>(?:<br\/?>)?<\/\1>$/i.test(data.value.trim())) {
41036
+ data.value = '';
41037
+ }
41038
+ }
40378
41039
  onSafeHTML(sandBox) {
40379
41040
  const sanitizer = this.j.o.cleanHTML.sanitizer;
40380
41041
  if (sanitizer) {
@@ -40399,6 +41060,9 @@ __decorate$1([
40399
41060
  __decorate$1([
40400
41061
  watch(':beforeSetNativeEditorValue')
40401
41062
  ], cleanHtml.prototype, "onBeforeSetNativeEditorValue", null);
41063
+ __decorate$1([
41064
+ watch(':afterGetValueFromEditor')
41065
+ ], cleanHtml.prototype, "onAfterGetValueFromEditor", null);
40402
41066
  __decorate$1([
40403
41067
  watch(':safeHTML')
40404
41068
  ], cleanHtml.prototype, "onSafeHTML", null);
@@ -40649,7 +41313,7 @@ Config.prototype.sourceEditorNativeOptions = {
40649
41313
  highlightActiveLine: true
40650
41314
  };
40651
41315
  Config.prototype.sourceEditorCDNUrlsJS = [
40652
- 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.2/ace.js'
41316
+ 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.43.3/ace.js'
40653
41317
  ];
40654
41318
  Config.prototype.beautifyHTMLCDNUrlsJS = [
40655
41319
  'https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.4/beautify.min.js',
@@ -41171,6 +41835,15 @@ class source extends Plugin {
41171
41835
  }
41172
41836
  setFocusToMirror() {
41173
41837
  var _a;
41838
+ const active = this.j.od.activeElement;
41839
+ // do not steal focus from another editor or control — e.g. when the
41840
+ // mode is switched programmatically (a Vue/React wrapper re-render)
41841
+ // while the user is already typing elsewhere. See #1356
41842
+ if (active &&
41843
+ active !== this.j.od.body &&
41844
+ !this.j.container.contains(active)) {
41845
+ return;
41846
+ }
41174
41847
  (_a = this.sourceEditor) === null || _a === void 0 ? void 0 : _a.focus();
41175
41848
  }
41176
41849
  saveSelection() {