tinymce-rails 8.8.1 → 8.9.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/source/tinymce/tinymce.js +270 -68
  3. data/lib/tinymce/rails/version.rb +2 -2
  4. data/vendor/assets/javascripts/tinymce/plugins/accordion/plugin.js +1 -1
  5. data/vendor/assets/javascripts/tinymce/plugins/advlist/plugin.js +1 -1
  6. data/vendor/assets/javascripts/tinymce/plugins/anchor/plugin.js +1 -1
  7. data/vendor/assets/javascripts/tinymce/plugins/autolink/plugin.js +1 -1
  8. data/vendor/assets/javascripts/tinymce/plugins/autoresize/plugin.js +1 -1
  9. data/vendor/assets/javascripts/tinymce/plugins/autosave/plugin.js +1 -1
  10. data/vendor/assets/javascripts/tinymce/plugins/charmap/plugin.js +1 -1
  11. data/vendor/assets/javascripts/tinymce/plugins/code/plugin.js +1 -1
  12. data/vendor/assets/javascripts/tinymce/plugins/codesample/plugin.js +1 -1
  13. data/vendor/assets/javascripts/tinymce/plugins/directionality/plugin.js +1 -1
  14. data/vendor/assets/javascripts/tinymce/plugins/emoticons/plugin.js +1 -1
  15. data/vendor/assets/javascripts/tinymce/plugins/fullscreen/plugin.js +1 -1
  16. data/vendor/assets/javascripts/tinymce/plugins/help/plugin.js +1 -1
  17. data/vendor/assets/javascripts/tinymce/plugins/image/plugin.js +1 -1
  18. data/vendor/assets/javascripts/tinymce/plugins/importcss/plugin.js +1 -1
  19. data/vendor/assets/javascripts/tinymce/plugins/insertdatetime/plugin.js +1 -1
  20. data/vendor/assets/javascripts/tinymce/plugins/link/plugin.js +1 -1
  21. data/vendor/assets/javascripts/tinymce/plugins/lists/plugin.js +1 -1
  22. data/vendor/assets/javascripts/tinymce/plugins/media/plugin.js +1 -1
  23. data/vendor/assets/javascripts/tinymce/plugins/nonbreaking/plugin.js +1 -1
  24. data/vendor/assets/javascripts/tinymce/plugins/pagebreak/plugin.js +1 -1
  25. data/vendor/assets/javascripts/tinymce/plugins/preview/plugin.js +1 -1
  26. data/vendor/assets/javascripts/tinymce/plugins/quickbars/plugin.js +1 -1
  27. data/vendor/assets/javascripts/tinymce/plugins/save/plugin.js +1 -1
  28. data/vendor/assets/javascripts/tinymce/plugins/searchreplace/plugin.js +1 -1
  29. data/vendor/assets/javascripts/tinymce/plugins/table/plugin.js +1 -1
  30. data/vendor/assets/javascripts/tinymce/plugins/visualblocks/plugin.js +1 -1
  31. data/vendor/assets/javascripts/tinymce/plugins/visualchars/plugin.js +1 -1
  32. data/vendor/assets/javascripts/tinymce/plugins/wordcount/plugin.js +1 -1
  33. data/vendor/assets/javascripts/tinymce/skins/ui/oxide/skin.css +1 -1
  34. data/vendor/assets/javascripts/tinymce/skins/ui/oxide/skin.js +1 -1
  35. data/vendor/assets/javascripts/tinymce/skins/ui/oxide/skin.min.css +1 -1
  36. data/vendor/assets/javascripts/tinymce/skins/ui/oxide-dark/skin.css +1 -1
  37. data/vendor/assets/javascripts/tinymce/skins/ui/oxide-dark/skin.js +1 -1
  38. data/vendor/assets/javascripts/tinymce/skins/ui/oxide-dark/skin.min.css +1 -1
  39. data/vendor/assets/javascripts/tinymce/skins/ui/tinymce-5/skin.css +1 -1
  40. data/vendor/assets/javascripts/tinymce/skins/ui/tinymce-5/skin.js +1 -1
  41. data/vendor/assets/javascripts/tinymce/skins/ui/tinymce-5/skin.min.css +1 -1
  42. data/vendor/assets/javascripts/tinymce/skins/ui/tinymce-5-dark/skin.css +1 -1
  43. data/vendor/assets/javascripts/tinymce/skins/ui/tinymce-5-dark/skin.js +1 -1
  44. data/vendor/assets/javascripts/tinymce/skins/ui/tinymce-5-dark/skin.min.css +1 -1
  45. data/vendor/assets/javascripts/tinymce/themes/silver/theme.js +3 -3
  46. data/vendor/assets/javascripts/tinymce/tinymce.d.ts +32 -6
  47. data/vendor/assets/javascripts/tinymce/tinymce.js +4 -4
  48. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a62c7a288ed5696f584343feec82754519985ae2f5b114d512cad2dfbdeee0ad
4
- data.tar.gz: afa1237efdcf85f4b879e2d857ebaf5c6a76a27b11861aae0cf853337da4ac48
3
+ metadata.gz: ac0f6b7996563391a41c7a79784540a1a7e4d6e4c3df8172f45886f909320b38
4
+ data.tar.gz: '018e0b48877e87639b2312a6ba0b673d275e9dfb905c3986e6d93ab8d1d14eec'
5
5
  SHA512:
6
- metadata.gz: 1f34f17bd042bb908ff1551f4db673326eae1b476555866c15c58f837f0141431c3c596d302088763879b3ac34f1d3af77f136189cf8d4dd55a36c012b07c9ec
7
- data.tar.gz: 96a89c0c643a51a2b904de5c4dd4f45c20abb0e3572f3613f44edf54dddc2aa44816b29c900500e9c39ec9b2a30309e2a9364f5265303362feb428b2db5f4c0f
6
+ metadata.gz: 9fa2ae87f866744fb26965a51de6193bf53fef8afbce8baebf7ab2b8d2a19e63923c89b8044e3fe843cdbeb05d580352ca831fc47fec780d51d0e165f19326c0
7
+ data.tar.gz: 66d4cbe492f6247fbc37d64f050863766162ae84403845e953bd3573302c4357378e701fd2153d67fad8bb8810d76159e4633e8e0d080002d50d42a36ad66104
@@ -1,5 +1,5 @@
1
1
  /**
2
- * TinyMCE version 8.8.1 (2026-07-22)
2
+ * TinyMCE version 8.9.0 (2026-08-27)
3
3
  */
4
4
 
5
5
  (function () {
@@ -12153,6 +12153,13 @@
12153
12153
  const selector = isEmpty$5(formatNoneditableSelector) ? baseDataSelector : `${baseDataSelector},${formatNoneditableSelector}`;
12154
12154
  return is$2(SugarElement.fromDom(node), selector);
12155
12155
  };
12156
+ const hasEditableDescendants = (dom, node) => dom.select('[contenteditable="true"]', node).length > 0;
12157
+ const getEditableDescendants = (dom, parent) => foldl(from(parent.childNodes), (editableDescendants, child) => [
12158
+ ...editableDescendants,
12159
+ ...(isElement$7(child) && dom.getContentEditable(child) === 'true'
12160
+ ? [child]
12161
+ : getEditableDescendants(dom, child))
12162
+ ], []);
12156
12163
  // A noneditable element is wrappable if it:
12157
12164
  // - is valid target (has data-mce-cef-wrappable attribute or matches selector from option)
12158
12165
  // - has no editable descendants - removing formats in the editable region can result in the wrapped noneditable being split which is undesirable
@@ -12161,7 +12168,7 @@
12161
12168
  return (isElementNode$1(node) &&
12162
12169
  dom.getContentEditable(node) === 'false' &&
12163
12170
  isWrapNoneditableTarget(editor, node) &&
12164
- dom.select('[contenteditable="true"]', node).length === 0);
12171
+ !hasEditableDescendants(dom, node));
12165
12172
  };
12166
12173
  /**
12167
12174
  * Replaces variables in the value. The variable format is %var.
@@ -13399,7 +13406,11 @@
13399
13406
  editor.selection.normalize();
13400
13407
  }, (caretPos) => editor.selection.setRng(caretPos.toRange()));
13401
13408
  };
13402
- const focusBody = (body) => {
13409
+ const focusBody = (body, preventScroll = false) => {
13410
+ if (preventScroll) {
13411
+ body.focus({ preventScroll: true });
13412
+ return;
13413
+ }
13403
13414
  if (body.setActive) {
13404
13415
  // IE 11 sometimes throws "Invalid function" then fallback to focus
13405
13416
  // setActive is better since it doesn't scroll to the element being focused
@@ -13430,7 +13441,7 @@
13430
13441
  };
13431
13442
  const hasFocus = (editor) => editor.inline ? hasInlineFocus(editor) : hasIframeFocus(editor);
13432
13443
  const hasEditorOrUiFocus = (editor) => hasFocus(editor) || hasUiFocus(editor);
13433
- const focusEditor = (editor) => {
13444
+ const focusEditor = (editor, preventScroll) => {
13434
13445
  const selection = editor.selection;
13435
13446
  const body = editor.getBody();
13436
13447
  let rng = selection.getRng();
@@ -13448,9 +13459,9 @@
13448
13459
  const contentEditableHost = getContentEditableHost(editor, selection.getNode());
13449
13460
  if (contentEditableHost && editor.dom.isChildOf(contentEditableHost, body)) {
13450
13461
  if (!hasContentEditableFalseParent$1(editor, contentEditableHost)) {
13451
- focusBody(body);
13462
+ focusBody(body, preventScroll);
13452
13463
  }
13453
- focusBody(contentEditableHost);
13464
+ focusBody(contentEditableHost, preventScroll);
13454
13465
  if (!editor.hasEditableRoot()) {
13455
13466
  restoreBookmark(editor);
13456
13467
  }
@@ -13461,30 +13472,40 @@
13461
13472
  // Focus the window iframe
13462
13473
  if (!editor.inline) {
13463
13474
  // WebKit needs this call to fire focusin event properly see #5948
13464
- // But Opera pre Blink engine will produce an empty selection so skip Opera
13465
- if (!Env.browser.isOpera()) {
13466
- focusBody(body);
13475
+ focusBody(body, preventScroll);
13476
+ if (preventScroll) {
13477
+ if (isNonNullable(editor.iframeElement)) {
13478
+ editor.iframeElement.focus({ preventScroll: true });
13479
+ }
13480
+ }
13481
+ else {
13482
+ editor.getWin().focus();
13467
13483
  }
13468
- editor.getWin().focus();
13469
13484
  }
13470
13485
  // Focus the body as well since it's contentEditable
13471
13486
  if (Env.browser.isFirefox() || editor.inline) {
13472
- focusBody(body);
13487
+ focusBody(body, preventScroll);
13473
13488
  normalizeSelection(editor, rng);
13474
13489
  }
13475
13490
  activateEditor(editor);
13476
13491
  };
13477
13492
  const activateEditor = (editor) => editor.editorManager.setActive(editor);
13478
- const focus = (editor, skipFocus) => {
13493
+ const normalizeFocusOptions = (args) => {
13494
+ if (isNonNullable(args) && !isBoolean(args)) {
13495
+ return args;
13496
+ }
13497
+ return { scrollToSelection: true };
13498
+ };
13499
+ const focus = (editor, args) => {
13479
13500
  if (editor.removed) {
13480
13501
  return;
13481
13502
  }
13482
- if (skipFocus) {
13503
+ if (args === true) {
13483
13504
  activateEditor(editor);
13505
+ return;
13484
13506
  }
13485
- else {
13486
- focusEditor(editor);
13487
- }
13507
+ const options = normalizeFocusOptions(args);
13508
+ focusEditor(editor, options.scrollToSelection === false);
13488
13509
  };
13489
13510
 
13490
13511
  /**
@@ -16769,14 +16790,23 @@
16769
16790
  }
16770
16791
  return undefined;
16771
16792
  };
16793
+ const containsFormatDeep = (editor, root, name, vars, similar) => isNonNullable(matchNode$1(editor, root, name, vars, similar)) ||
16794
+ exists(from(root.childNodes), (child) => containsFormatDeep(editor, child, name, vars, similar));
16795
+ const matchEditableDescendants = (editor, node, name, vars, similar) => {
16796
+ const dom = editor.dom;
16797
+ if (!isElement$7(node) || dom.getContentEditable(node) !== 'false' || isWrappableNoneditable(editor, node)) {
16798
+ return false;
16799
+ }
16800
+ return exists(getEditableDescendants(dom, node), (descendant) => containsFormatDeep(editor, descendant, name, vars, similar));
16801
+ };
16772
16802
  const match$2 = (editor, name, vars, node, similar) => {
16773
16803
  // Check specified node
16774
16804
  if (node) {
16775
- return matchParents(editor, node, name, vars, similar);
16805
+ return matchParents(editor, node, name, vars, similar) || matchEditableDescendants(editor, node, name, vars, similar);
16776
16806
  }
16777
16807
  // Check selected node
16778
16808
  node = editor.selection.getNode();
16779
- if (matchParents(editor, node, name, vars, similar)) {
16809
+ if (matchParents(editor, node, name, vars, similar) || matchEditableDescendants(editor, node, name, vars, similar)) {
16780
16810
  return true;
16781
16811
  }
16782
16812
  // Check start node if it's different
@@ -18647,7 +18677,7 @@
18647
18677
  }
18648
18678
  };
18649
18679
 
18650
- /*! @license DOMPurify 3.4.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.11/LICENSE */
18680
+ /*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE */
18651
18681
 
18652
18682
  function _arrayLikeToArray(r, a) {
18653
18683
  (null == a || a > r.length) && (a = r.length);
@@ -18960,7 +18990,7 @@
18960
18990
  const text = freeze(['#text']);
18961
18991
 
18962
18992
  const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'command', 'commandfor', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns']);
18963
- const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
18993
+ const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dominant-baseline', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-orientation', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
18964
18994
  const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lquote', 'lspace', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
18965
18995
  const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
18966
18996
 
@@ -19073,7 +19103,7 @@
19073
19103
  function createDOMPurify() {
19074
19104
  let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
19075
19105
  const DOMPurify = root => createDOMPurify(root);
19076
- DOMPurify.version = '3.4.11';
19106
+ DOMPurify.version = '3.4.12';
19077
19107
  DOMPurify.removed = [];
19078
19108
  if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
19079
19109
  // Not running in a browser, provide a factory function
@@ -19755,6 +19785,13 @@
19755
19785
  * @param root the in-place root to empty
19756
19786
  */
19757
19787
  const _neutralizeRoot = function _neutralizeRoot(root) {
19788
+ /* Strip every disallowed attribute (on* handlers included) off the whole
19789
+ subtree BEFORE detaching anything. Detaching first would hand back
19790
+ handler-bearing originals (e.g. an already-loading `<img onerror>`)
19791
+ whose queued resource event still fires in page scope after we throw.
19792
+ Clobber-safe reads; a doomed clobbered node's own attributes are
19793
+ irrelevant while its non-clobbered descendants are reached and scrubbed. */
19794
+ _neutralizeSubtree(root);
19758
19795
  const childNodes = getChildNodes(root);
19759
19796
  if (childNodes) {
19760
19797
  const snapshot = [];
@@ -19882,6 +19919,82 @@
19882
19919
  }
19883
19920
  }
19884
19921
  };
19922
+ /**
19923
+ * _neutralizePatchLinkage
19924
+ *
19925
+ * IN_PLACE entry pre-pass (declarative-partial-updates / streaming
19926
+ * hardening, https://github.com/WICG/declarative-partial-updates).
19927
+ *
19928
+ * The main walk strips patch linkage (`for`/`patchsrc`) and removes range
19929
+ * markers (PIs / markup comments) node-by-node, in document order, AS it
19930
+ * reaches each node. On a live in-place root that leaves a window: from the
19931
+ * moment the root is connected until the walk arrives at a given node, that
19932
+ * node's linkage is live. A patch applied on connection/stream can fire as
19933
+ * a microtask during the walk and inject or teleport an unsanitized DOM
19934
+ * range into a region the iterator has already passed and will not revisit,
19935
+ * so the post-return "tree is sanitized" contract is violated. Sweep the
19936
+ * whole tree once up front and sever every linkage before the walk begins,
19937
+ * closing that window.
19938
+ *
19939
+ * This CANNOT undo a patch that already fired before sanitize ran — that is
19940
+ * the irreducible "do not IN_PLACE a live-connected attacker tree" caveat —
19941
+ * but it closes everything from sanitize-start onward. Gated on SAFE_FOR_XML
19942
+ * to group with the rest of the declarative-partial-updates handling and
19943
+ * stay overridable, consistent with the codebase.
19944
+ *
19945
+ * Clobber-safe traversal (cached childNodes getter); per-node try/catch so a
19946
+ * clobbered root cannot defeat the sweep of its non-clobbered descendants.
19947
+ *
19948
+ * NOTE (pending real-Chrome confirmation, see test/declarative-patch-probe
19949
+ * .html Q1): this mirrors the existing policy of keeping `for` on
19950
+ * <label>/<output>. If the shipping feature can drive a patch through a
19951
+ * surviving `for`-on-label/output + `id` pair, this pre-pass and the
19952
+ * attribute check at _isBasicCustomElement's caller must additionally drop
19953
+ * that pair on the IN_PLACE path. Left as-is until the taxonomy is verified.
19954
+ *
19955
+ * @param root the in-place root to sweep
19956
+ */
19957
+ const _neutralizePatchLinkage = function _neutralizePatchLinkage(root) {
19958
+ if (!SAFE_FOR_XML) {
19959
+ return;
19960
+ }
19961
+ const stack = [root];
19962
+ while (stack.length > 0) {
19963
+ const node = stack.pop();
19964
+ const nodeType = getNodeType ? getNodeType(node) : node.nodeType;
19965
+ /* Remove range markers (the target side of a patch linkage): every
19966
+ processing instruction, and any markup-bearing comment. */
19967
+ if (nodeType === NODE_TYPE.processingInstruction || nodeType === NODE_TYPE.comment && regExpTest(COMMENT_MARKUP_PROBE, node.data)) {
19968
+ try {
19969
+ remove(node);
19970
+ } catch (_) {
19971
+ /* Best-effort */
19972
+ }
19973
+ continue;
19974
+ }
19975
+ /* Strip patch-source attributes (the source side) off elements. */
19976
+ if (nodeType === NODE_TYPE.element) {
19977
+ const element = node;
19978
+ const lcTag = transformCaseFunc(getNodeName ? getNodeName(node) : node.nodeName);
19979
+ try {
19980
+ if (element.hasAttribute && element.hasAttribute('patchsrc')) {
19981
+ element.removeAttribute('patchsrc');
19982
+ }
19983
+ if (element.hasAttribute && element.hasAttribute('for') && lcTag !== 'label' && lcTag !== 'output') {
19984
+ element.removeAttribute('for');
19985
+ }
19986
+ } catch (_) {
19987
+ /* Clobbered removeAttribute/hasAttribute on a doomed node — ignore */
19988
+ }
19989
+ }
19990
+ const childNodes = getChildNodes(node);
19991
+ if (childNodes) {
19992
+ for (let i = childNodes.length - 1; i >= 0; --i) {
19993
+ stack.push(childNodes[i]);
19994
+ }
19995
+ }
19996
+ }
19997
+ };
19885
19998
  /**
19886
19999
  * _initDocument
19887
20000
  *
@@ -20132,9 +20245,15 @@
20132
20245
  /**
20133
20246
  * Handle a node whose tag is forbidden or not allowlisted: keep
20134
20247
  * allowed custom elements (false return exits _sanitizeElements
20135
- * early - namespace/fallback checks and the afterSanitizeElements
20136
- * hook are intentionally skipped for kept custom elements), else
20137
- * hoist content per KEEP_CONTENT and remove.
20248
+ * early - the namespace and fallback-tag removal checks are
20249
+ * intentionally skipped for kept custom elements), else hoist
20250
+ * content per KEEP_CONTENT and remove.
20251
+ *
20252
+ * A kept custom element is the ONLY case in which this function
20253
+ * returns false, so the caller uses that return value to run the
20254
+ * afterSanitizeElements hook on the kept element and keep the
20255
+ * element-hook lifecycle consistent with normal allowlisted
20256
+ * elements (GHSA-c2j3-45gr-mqc4).
20138
20257
  *
20139
20258
  * @param currentNode the disallowed node
20140
20259
  * @param tagName the node's transformCaseFunc'd tag name
@@ -20200,9 +20319,15 @@
20200
20319
  * @param currentNode to check for permission to exist
20201
20320
  * @return true if node was killed, false if left alive
20202
20321
  */
20203
- const _sanitizeElements = function _sanitizeElements(currentNode) {
20322
+ // eslint-disable-next-line complexity
20323
+ const _sanitizeElements = function _sanitizeElements(currentNode, root) {
20204
20324
  /* Execute a hook if present */
20205
20325
  _executeHooks(hooks.beforeSanitizeElements, currentNode, null);
20326
+ /* A hook may have detached the node — treat it as removed (see the
20327
+ detached-node comment after the uponSanitizeElement hook below). */
20328
+ if (currentNode !== root && getParentNode(currentNode) === null) {
20329
+ return true;
20330
+ }
20206
20331
  /* Check if element is clobbered or can clobber */
20207
20332
  if (_isClobbered(currentNode)) {
20208
20333
  _forceRemove(currentNode);
@@ -20215,6 +20340,24 @@
20215
20340
  tagName,
20216
20341
  allowedTags: ALLOWED_TAGS
20217
20342
  });
20343
+ /* A hook may have detached the node from the tree — a long-standing
20344
+ user pattern (issue #469; draw.io-style foreignObject filtering).
20345
+ Per the cached, unclobberable parentNode getter the node is
20346
+ genuinely out of the tree, so it can reach neither the serialized
20347
+ output nor an IN_PLACE live tree; treat it as removed and stop
20348
+ processing it. Without this guard, the unsafe-node / namespace
20349
+ checks below would call _forceRemove on a parentless node and hit
20350
+ the REPORT-3 fail-closed throw — which exists for nodes DOMPurify
20351
+ wants gone but *cannot* detach (clobbered / parentless roots), the
20352
+ opposite of a node that is already safely gone. The walk root is
20353
+ exempt: a detached IN_PLACE root is legitimate input and must still
20354
+ be fully sanitized, and a kill-decision on it must keep hitting the
20355
+ REPORT-3 throw. Nodes detached by hooks are the hook's
20356
+ responsibility: they are not recorded in DOMPurify.removed and are
20357
+ not neutralized by the post-walk IN_PLACE pass. */
20358
+ if (currentNode !== root && getParentNode(currentNode) === null) {
20359
+ return true;
20360
+ }
20218
20361
  /* Remove mXSS vectors, processing instructions and risky comments */
20219
20362
  if (_isUnsafeNode(currentNode, tagName)) {
20220
20363
  _forceRemove(currentNode);
@@ -20222,7 +20365,22 @@
20222
20365
  }
20223
20366
  /* Remove element if anything forbids its presence */
20224
20367
  if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS[tagName]) {
20225
- return _sanitizeDisallowedNode(currentNode, tagName);
20368
+ const removed = _sanitizeDisallowedNode(currentNode, tagName);
20369
+ /* A false return means the node is a custom element kept via
20370
+ CUSTOM_ELEMENT_HANDLING - the only keep path through
20371
+ _sanitizeDisallowedNode. Run afterSanitizeElements on it so the
20372
+ element-hook lifecycle matches normal allowlisted elements: a
20373
+ security policy applied in this hook (e.g. stripping an attribute
20374
+ from every surviving element) must not silently skip kept custom
20375
+ elements (GHSA-c2j3-45gr-mqc4). This mirrors the normal-element
20376
+ tail below - the hook runs, then the walker's subsequent
20377
+ _sanitizeAttributes pass sanitizes the element's attributes. The
20378
+ deliberately skipped namespace and fallback-tag removal checks stay
20379
+ skipped; they are removal decisions, not the hook contract. */
20380
+ if (removed === false) {
20381
+ _executeHooks(hooks.afterSanitizeElements, currentNode, null);
20382
+ }
20383
+ return removed;
20226
20384
  }
20227
20385
  /* Check whether element has a valid namespace.
20228
20386
  Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
@@ -20269,6 +20427,33 @@
20269
20427
  if (FORBID_ATTR[lcName]) {
20270
20428
  return false;
20271
20429
  }
20430
+ /* Reject declarative-partial-updates patch-linkage attributes
20431
+ (https://github.com/WICG/declarative-partial-updates).
20432
+ Empirical note (Chrome 150, verified — see
20433
+ test/declarative-patch-probe-v3.html): expansion is NOT applied after
20434
+ sanitization. For the string path it fires during sanitize()'s own
20435
+ parse, so the walk sees and sanitizes the fully materialized expanded
20436
+ tree — teleports into MathML/SVG integration points included; a
20437
+ weaponized `<template for>`->`<img onerror>` comes back with the handler
20438
+ stripped. For the IN_PLACE path it fires on connection, before the walk.
20439
+ Either way DOMPurify is NOT blind to the patch.
20440
+ This removal is therefore defense-in-depth rather than the sole barrier:
20441
+ it prevents live linkage from surviving into the OUTPUT and re-expanding
20442
+ in the caller's context, and keeps behaviour deterministic if a future
20443
+ engine defers expansion. `for` is legitimate only on <label>/<output>;
20444
+ anywhere else (notably <template for>) it links the element to a patch
20445
+ target and teleports or removes an arbitrary DOM range by id/marker name.
20446
+ `patchsrc` fetches remote markup and is treated as a script-loading
20447
+ mechanism (CSP). Gated on SAFE_FOR_XML so the removal groups with the
20448
+ other structural-threat checks and stays overridable, consistent with
20449
+ the rest of the codebase. PI range markers are already removed by
20450
+ _isUnsafeNode. */
20451
+ if (SAFE_FOR_XML && lcName === 'patchsrc') {
20452
+ return false;
20453
+ }
20454
+ if (SAFE_FOR_XML && lcName === 'for' && lcTag !== 'label' && lcTag !== 'output') {
20455
+ return false;
20456
+ }
20272
20457
  /* Make sure attribute cannot clobber */
20273
20458
  if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
20274
20459
  return false;
@@ -20478,7 +20663,7 @@
20478
20663
  /* Execute a hook if present */
20479
20664
  _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
20480
20665
  /* Sanitize tags and elements */
20481
- _sanitizeElements(shadowNode);
20666
+ _sanitizeElements(shadowNode, fragment);
20482
20667
  /* Check attributes next */
20483
20668
  _sanitizeAttributes(shadowNode);
20484
20669
  /* Deep shadow DOM detected.
@@ -20669,6 +20854,11 @@
20669
20854
  keep using and whose return value they ignore — unsanitized. REPORT-2. */
20670
20855
  const inPlace = IN_PLACE && typeof dirty !== 'string' && _isNode(dirty);
20671
20856
  if (inPlace) {
20857
+ /* Declarative-partial-updates / streaming pre-pass: sever every patch
20858
+ linkage across the live tree BEFORE the walk, so no patch can fire
20859
+ mid-walk and inject into an already-processed region. Runs first, so
20860
+ it also covers the forbidden/clobbered roots that throw below. */
20861
+ _neutralizePatchLinkage(dirty);
20672
20862
  /* Do some early pre-sanitization to avoid unsafe root nodes.
20673
20863
  Read nodeName through the cached prototype getter — a clobbering
20674
20864
  child named "nodeName" on the form root would otherwise shadow
@@ -20678,6 +20868,9 @@
20678
20868
  if (typeof nn === 'string') {
20679
20869
  const tagName = transformCaseFunc(nn);
20680
20870
  if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
20871
+ /* Fail closed on a live root: neutralize handlers/children before
20872
+ throwing, exactly as the mid-walk abort path does. */
20873
+ _neutralizeRoot(dirty);
20681
20874
  throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
20682
20875
  }
20683
20876
  }
@@ -20692,6 +20885,10 @@
20692
20885
  the application unsanitized. Refuse to sanitize such a root
20693
20886
  the same way we refuse a forbidden tag. GHSA-r47g-fvhr-h676. */
20694
20887
  if (_isClobbered(dirty)) {
20888
+ /* Fail closed on a live clobbered root before throwing.
20889
+ _neutralizeRoot's reads are clobber-safe (cached getters); the
20890
+ form's non-clobbered descendants, e.g. an armed <img>, are scrubbed. */
20891
+ _neutralizeRoot(dirty);
20695
20892
  throw typeErrorCreate('root node is clobbered and cannot be sanitized in-place');
20696
20893
  }
20697
20894
  /* Sanitize attached shadow roots before the main iterator runs.
@@ -20744,7 +20941,8 @@
20744
20941
  _forceRemove(body.firstChild);
20745
20942
  }
20746
20943
  /* Get node iterator */
20747
- const nodeIterator = _createNodeIterator(inPlace ? dirty : body);
20944
+ const walkRoot = inPlace ? dirty : body;
20945
+ const nodeIterator = _createNodeIterator(walkRoot);
20748
20946
  /* Now start iterating over the created document.
20749
20947
  The walk runs inside an exception barrier (campaign-3 F2): a re-entrant
20750
20948
  engine/custom-element mutation can detach a node mid-walk so
@@ -20757,7 +20955,7 @@
20757
20955
  try {
20758
20956
  while (currentNode = nodeIterator.nextNode()) {
20759
20957
  /* Sanitize tags and elements */
20760
- _sanitizeElements(currentNode);
20958
+ _sanitizeElements(currentNode, walkRoot);
20761
20959
  /* Check attributes next */
20762
20960
  _sanitizeAttributes(currentNode);
20763
20961
  /* Shadow DOM detected, sanitize it.
@@ -20771,6 +20969,14 @@
20771
20969
  } catch (error) {
20772
20970
  if (inPlace) {
20773
20971
  _neutralizeRoot(dirty);
20972
+ /* Nodes _forceRemove'd earlier in the aborted walk are already
20973
+ detached from the root, so _neutralizeRoot's subtree pass does not
20974
+ reach them. Defuse them too, mirroring the success-path loop below. */
20975
+ arrayForEach(DOMPurify.removed, entry => {
20976
+ if (entry.element) {
20977
+ _neutralizeSubtree(entry.element);
20978
+ }
20979
+ });
20774
20980
  }
20775
20981
  throw error;
20776
20982
  }
@@ -21368,44 +21574,22 @@
21368
21574
  const bogus = get$a(element, 'data-mce-bogus');
21369
21575
  if (!isInternalElement && isString(bogus)) {
21370
21576
  if (bogus === 'all') {
21371
- // Empty before removing so a detached raw-text element (e.g. script/style) can't carry
21372
- // markup-like content into DOMPurify's unsafe-node check, which would then throw trying
21373
- // to _forceRemove the now-parentless node.
21374
- empty(element);
21375
21577
  remove$8(element);
21376
21578
  }
21377
21579
  else {
21378
21580
  unwrap(element);
21379
21581
  }
21380
- // We detach the element above; DOMPurify throws if it then tries to _forceRemove the
21381
- // now-parentless node, so mark the tag allowed to stop that second removal.
21382
- if (isNonNullable(evt)) {
21383
- evt.allowedTags[lcTagName] = true;
21384
- }
21385
21582
  return;
21386
21583
  }
21387
21584
  // Determine if the schema allows the element and either add it or remove it
21388
21585
  const rule = schema.getElementRule(lcTagName);
21389
21586
  if (validate && !rule) {
21390
- // Don't detach invalid special / non-HTML-namespace elements: DOMPurify throws if it later
21391
- // _forceRemoves a parentless node (its unsafe-content and namespace checks do this regardless
21392
- // of allowedTags). Empty and leave attached instead, so DOMPurify removes it without hoisting content.
21393
- if (settings.sanitize && (has$2(specialElements, lcTagName) || scope !== 'html')) {
21394
- empty(element);
21587
+ // If a special element is invalid, then remove the entire element instead of unwrapping
21588
+ if (has$2(specialElements, lcTagName)) {
21589
+ remove$8(element);
21395
21590
  }
21396
21591
  else {
21397
- // No DOMPurify to remove it (sanitize disabled), or a plain invalid HTML element: detach it ourselves.
21398
- if (has$2(specialElements, lcTagName)) {
21399
- remove$8(element);
21400
- }
21401
- else {
21402
- unwrap(element);
21403
- }
21404
- // The element is now detached; mark the tag allowed so DOMPurify won't _forceRemove the
21405
- // parentless node and throw. Safe here: it's empty and HTML-namespaced, so no earlier check fires.
21406
- if (isNonNullable(evt)) {
21407
- evt.allowedTags[lcTagName] = true;
21408
- }
21592
+ unwrap(element);
21409
21593
  }
21410
21594
  return;
21411
21595
  }
@@ -21588,9 +21772,7 @@
21588
21772
  evt.allowedTags[lcTagName] = keepElement;
21589
21773
  if (!keepElement && settings.sanitize) {
21590
21774
  if (isElement$7(node)) {
21591
- // Empty the node and leave it attached so DOMPurify removes it (allowedTags is false).
21592
- // Detaching it would make DOMPurify throw when it _forceRemoves the parentless node.
21593
- empty(SugarElement.fromDom(node));
21775
+ node.remove();
21594
21776
  }
21595
21777
  }
21596
21778
  });
@@ -23212,14 +23394,21 @@
23212
23394
  }
23213
23395
  });
23214
23396
  };
23215
- // TODO: TINY-9142: Remove this to make nested noneditable formatting work
23216
23397
  const targetNode = isNode(node) ? node : selection.getNode();
23217
23398
  if (dom.getContentEditable(targetNode) === 'false' && !isWrappableNoneditable(ed, targetNode)) {
23218
23399
  // node variable is used by other functions above in the same scope so need to set it here
23219
23400
  node = targetNode;
23220
23401
  applyNodeStyle(formatList, node);
23221
- if (isBlockFormat(format) && !dom.isBlock(targetNode)) {
23222
- const parentBlock = dom.getParent(targetNode, dom.isBlock);
23402
+ if (hasEditableDescendants(dom, node)) {
23403
+ each$e(getEditableDescendants(dom, node), (island) => {
23404
+ const rng = dom.createRng();
23405
+ rng.selectNodeContents(island);
23406
+ applyRngStyle(dom, rng, true);
23407
+ });
23408
+ return;
23409
+ }
23410
+ if (isBlockFormat(format) && !dom.isBlock(node)) {
23411
+ const parentBlock = dom.getParent(node, dom.isBlock);
23223
23412
  if (dom.isEditable(parentBlock)) {
23224
23413
  const wrapperElementName = format.block;
23225
23414
  if (parentBlock.nodeName.toLowerCase() === wrapperElementName.toLowerCase()) {
@@ -23231,6 +23420,12 @@
23231
23420
  }
23232
23421
  }
23233
23422
  }
23423
+ else {
23424
+ const selectorFormats = filter$5(formatList, isSelectorFormat);
23425
+ Optional.from(dom.getParent(node, (candidate) => exists(selectorFormats, (fmt) => dom.is(candidate, fmt.selector))))
23426
+ .filter(dom.isEditable)
23427
+ .each((parent) => applyNodeStyle(formatList, parent));
23428
+ }
23234
23429
  fireFormatApply(ed, name, node, vars);
23235
23430
  return;
23236
23431
  }
@@ -38309,9 +38504,11 @@
38309
38504
  const initTooltipClosing = (editor) => {
38310
38505
  const closeTooltipsListener = (event) => {
38311
38506
  if (event.keyCode === VK.ESC && !event.defaultPrevented) {
38312
- if (fireCloseTooltips(editor).isDefaultPrevented()) {
38313
- event.preventDefault();
38314
- }
38507
+ each$e(editor.editorManager.get(), (ed) => {
38508
+ if (fireCloseTooltips(ed).isDefaultPrevented()) {
38509
+ event.preventDefault();
38510
+ }
38511
+ });
38315
38512
  }
38316
38513
  };
38317
38514
  document.addEventListener('keyup', closeTooltipsListener);
@@ -39479,7 +39676,7 @@
39479
39676
  editor.getWin().print();
39480
39677
  },
39481
39678
  mceFocus: (_command, _ui, value) => {
39482
- focus(editor, value === true);
39679
+ focus(editor, value);
39483
39680
  },
39484
39681
  mceToggleVisualAid: () => {
39485
39682
  editor.hasVisual = !editor.hasVisual;
@@ -41205,7 +41402,12 @@
41205
41402
  * it will also place DOM focus inside the editor.
41206
41403
  *
41207
41404
  * @method focus
41208
- * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
41405
+ * @param {Boolean/Object} skipFocus If true, skip DOM focus and only set this editor as the active editor. If an object, focus the editor and optionally control scrolling.
41406
+ * @param {Boolean} skipFocus.scrollToSelection If false, focuses the editor without scrolling the selection into view. Defaults to true.
41407
+ * @example
41408
+ * tinymce.activeEditor.focus();
41409
+ * tinymce.activeEditor.focus(true);
41410
+ * tinymce.activeEditor.focus({ scrollToSelection: false });
41209
41411
  */
41210
41412
  focus(skipFocus) {
41211
41413
  this.execCommand('mceFocus', false, skipFocus);
@@ -41901,14 +42103,14 @@
41901
42103
  * @property minorVersion
41902
42104
  * @type String
41903
42105
  */
41904
- minorVersion: '8.1',
42106
+ minorVersion: '9.0',
41905
42107
  /**
41906
42108
  * Release date of TinyMCE build.
41907
42109
  *
41908
42110
  * @property releaseDate
41909
42111
  * @type String
41910
42112
  */
41911
- releaseDate: '2026-07-22',
42113
+ releaseDate: '2026-08-27',
41912
42114
  /**
41913
42115
  * Collection of language pack data.
41914
42116
  *
@@ -1,6 +1,6 @@
1
1
  module TinyMCE
2
2
  module Rails
3
- VERSION = "8.8.1"
4
- TINYMCE_VERSION = "8.8.1"
3
+ VERSION = "8.9.0"
4
+ TINYMCE_VERSION = "8.9.0"
5
5
  end
6
6
  end