1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2026-06-03 19:22:27 +00:00

Fix RSOD when $tw.utils.addClass receives a class string with whitespace

PR #9251 replaced the manual setAttribute("class", ...) implementation of
$tw.utils.addClass/removeClass/toggleClass with direct Element.classList
calls. Unlike setAttribute, classList.add/remove/toggle throws
InvalidCharacterError on any token containing whitespace, so callers that
pass a whole class string (e.g. modal.js passing tiddler.fields.class)
now crash.

Manual repro on tw5-com: open SampleWizard, set the `class` field to
"aaa bbb", Done, open popup -> OK -> open nested popup -> RSOD.

Fix: split the className argument on whitespace in deprecated.js and feed
individual tokens to classList. A small splitClasses() helper keeps the
three functions symmetrical.

Adds adversarial regression tests in test-utils.js covering:
- ASCII whitespace variants (space, tab, CR, LF, mixed runs, padding)
- Unicode whitespace (U+00A0 non-breaking space)
- de-duplication across single and multiple calls
- remove/toggle no-op on missing tokens
- toggle with status undefined / true / false
- silent no-op for whitespace-only / empty / non-string / null input
- silent no-op when the element has no classList
This commit is contained in:
pmario
2026-04-22 17:15:35 +02:00
parent ea84baa5a3
commit 005e175378
2 changed files with 109 additions and 3 deletions
+11 -3
View File
@@ -43,16 +43,24 @@ exports.domMatchesSelector = (node,selector) => node.matches(selector);
exports.hasClass = (el,className) => el.classList && el.classList.contains(className);
// classList.add/remove/toggle reject whitespace, but the legacy API accepts "aaa bbb".
function splitClasses(className) {
return (typeof className === "string" && className.match(/\S+/g)) || [];
}
exports.addClass = function(el,className) {
el.classList && className && el.classList.add(className);
if(!el.classList) return;
splitClasses(className).forEach(function(c) { el.classList.add(c); });
};
exports.removeClass = function(el,className) {
el.classList && className && el.classList.remove(className);
if(!el.classList) return;
splitClasses(className).forEach(function(c) { el.classList.remove(c); });
};
exports.toggleClass = function(el,className,status) {
el.classList && className && el.classList.toggle(className, status);
if(!el.classList) return;
splitClasses(className).forEach(function(c) { el.classList.toggle(c,status); });
};
exports.getLocationPath = () => window.location.origin + window.location.pathname;