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