diff --git a/components/script/dom/domtokenlist.rs b/components/script/dom/domtokenlist.rs index f74887d0899..3968b2084cb 100644 --- a/components/script/dom/domtokenlist.rs +++ b/components/script/dom/domtokenlist.rs @@ -139,6 +139,32 @@ impl DOMTokenListMethods for DOMTokenList { self.element.set_tokenlist_attribute(&self.local_name, value); } + // https://dom.spec.whatwg.org/#dom-domtokenlist-replace + fn Replace(&self, token: DOMString, new_token: DOMString) -> ErrorResult { + if token.is_empty() || new_token.is_empty() { + // Step 1. + return Err(Error::Syntax); + } + if token.contains(HTML_SPACE_CHARACTERS) || new_token.contains(HTML_SPACE_CHARACTERS) { + // Step 2. + return Err(Error::InvalidCharacter); + } + // Steps 3-4. + let token = Atom::from(token); + let new_token = Atom::from(new_token); + let mut atoms = self.element.get_tokenlist_attribute(&self.local_name); + if let Some(pos) = atoms.iter().position(|atom| *atom == token) { + if !atoms.contains(&new_token) { + atoms[pos] = new_token; + } else { + atoms.remove(pos); + } + } + // Step 5. + self.element.set_atomic_tokenlist_attribute(&self.local_name, atoms); + Ok(()) + } + // https://dom.spec.whatwg.org/#concept-dtl-serialize fn Stringifier(&self) -> DOMString { self.element.get_string_attribute(&self.local_name) diff --git a/components/script/dom/webidls/DOMTokenList.webidl b/components/script/dom/webidls/DOMTokenList.webidl index c9125285f74..21be3590c0a 100644 --- a/components/script/dom/webidls/DOMTokenList.webidl +++ b/components/script/dom/webidls/DOMTokenList.webidl @@ -18,6 +18,8 @@ interface DOMTokenList { void remove(DOMString... tokens); [Throws] boolean toggle(DOMString token, optional boolean force); + [Throws] + void replace(DOMString token, DOMString newToken); [Pure] attribute DOMString value; diff --git a/tests/wpt/metadata/dom/interfaces.html.ini b/tests/wpt/metadata/dom/interfaces.html.ini index d79c09e1674..0ebcd506c2a 100644 --- a/tests/wpt/metadata/dom/interfaces.html.ini +++ b/tests/wpt/metadata/dom/interfaces.html.ini @@ -156,18 +156,9 @@ [DOMSettableTokenList interface object name] expected: FAIL - [DOMTokenList interface: operation replace(DOMString,DOMString)] - expected: FAIL - [DOMTokenList interface: operation supports(DOMString)] expected: FAIL - [DOMTokenList interface: document.body.classList must inherit property "replace" with the proper type (6)] - expected: FAIL - - [DOMTokenList interface: calling replace(DOMString,DOMString) on document.body.classList with too few arguments must throw TypeError] - expected: FAIL - [DOMTokenList interface: document.body.classList must inherit property "supports" with the proper type (7)] expected: FAIL diff --git a/tests/wpt/web-platform-tests/dom/nodes/Element-classlist.html b/tests/wpt/web-platform-tests/dom/nodes/Element-classlist.html index 9c0ff05efd9..cad3669da11 100644 --- a/tests/wpt/web-platform-tests/dom/nodes/Element-classlist.html +++ b/tests/wpt/web-platform-tests/dom/nodes/Element-classlist.html @@ -77,6 +77,13 @@ test(function () { test(function () { assert_throws( 'SYNTAX_ERR', function () { elem.classList.toggle(''); } ); }, '.toggle(empty_string) must throw a SYNTAX_ERR'); +test(function () { + assert_throws( 'SYNTAX_ERR', function () { elem.classList.replace('', 'foo'); } ); + assert_throws( 'SYNTAX_ERR', function () { elem.classList.replace('foo', ''); } ); + assert_throws( 'SYNTAX_ERR', function () { elem.classList.replace('', 'foo bar'); } ); + assert_throws( 'SYNTAX_ERR', function () { elem.classList.replace('foo bar', ''); } ); + assert_throws( 'SYNTAX_ERR', function () { elem.classList.replace('', ''); } ); +}, '.replace with empty_string must throw a SYNTAX_ERR'); test(function () { assert_throws( 'INVALID_CHARACTER_ERR', function () { elem.classList.contains('a b'); } ); }, '.contains(string_with_spaces) must throw an INVALID_CHARACTER_ERR'); @@ -89,6 +96,20 @@ test(function () { test(function () { assert_throws( 'INVALID_CHARACTER_ERR', function () { elem.classList.toggle('a b'); } ); }, '.toggle(string_with_spaces) must throw an INVALID_CHARACTER_ERR'); +test(function () { + assert_throws( 'INVALID_CHARACTER_ERR', function () { elem.classList.replace('z', 'a b'); } ); + assert_throws( 'INVALID_CHARACTER_ERR', function () { elem.classList.replace('a b', 'z'); } ); + assert_throws( 'INVALID_CHARACTER_ERR', function () { elem.classList.replace('a b', 'b c'); } ); +}, '.replace with string_with_spaces must throw a INVALID_CHARACTER_ERR'); +test(function () { + var foo = document.createElement('div'); + foo.className = 'token1 token2 token3' + foo.classList.replace('token1', 'token3'); + assert_equals( foo.classList.length, 2 ); + assert_false( foo.classList.contains('token1') ); + assert_true( foo.classList.contains('token2') ); + assert_true( foo.classList.contains('token3') ); +}, '.replace with an already existing token') elem.className = 'foo'; test(function () { assert_equals( getComputedStyle(elem,null).fontStyle, 'italic', 'critical test; required by the testsuite' ); @@ -224,6 +245,58 @@ test(function () { assert_false( elem.classList.contains('foo') ); assert_false( elem.classList.contains('FOO') ); }, 'classList.toggle must be case-sensitive when removing tokens'); +test(function () { + secondelem.className = 'foo FOO' + secondelem.classList.replace('bar', 'baz'); + assert_equals( secondelem.classList.length, 2 ); + assert_equals( secondelem.classList + '', 'foo FOO', 'implicit' ); + assert_equals( secondelem.classList.toString(), 'foo FOO', 'explicit' ); +}, 'classList.replace replaces arguments passed, if they are present.'); +test(function () { + secondelem.classList.replace('foo', 'bar'); + assert_equals( secondelem.classList.length, 2 ); + assert_equals( secondelem.classList + '', 'bar FOO', 'implicit' ); + assert_equals( secondelem.classList.toString(), 'bar FOO', 'explicit' ); + assert_false( secondelem.classList.contains('foo') ); + assert_true( secondelem.classList.contains('bar') ); + assert_true( secondelem.classList.contains('FOO') ); +}, 'classList.replace must replace existing tokens'); +test(function () { + assert_not_equals( getComputedStyle(secondelem,null).fontStyle, 'italic' ); +}, 'classList.replace must not break case-sensitive CSS selector matching'); +test(function () { + secondelem.className = 'token1 token2 token1' + secondelem.classList.replace('token1', 'token3'); + assert_equals( secondelem.classList.length, 2 ); + assert_false( secondelem.classList.contains('token1') ); + assert_true( secondelem.classList.contains('token2') ); + assert_true( secondelem.classList.contains('token3') ); +}, 'classList.replace must replace duplicated tokens'); +test(function () { + secondelem.className = 'token1 token2 token3'; + secondelem.classList.replace('token2', 'token4'); + assert_equals( secondelem.classList + '', 'token1 token4 token3', 'implicit' ); + assert_equals( secondelem.classList.toString(), 'token1 token4 token3', 'explicit' ); +}, 'classList.replace must collapse whitespace around replaced tokens'); +test(function () { + secondelem.className = ' token1 token2 '; + secondelem.classList.replace('token2', 'token3'); + assert_equals( secondelem.classList.length, 2 ); + assert_equals( secondelem.classList + '', 'token1 token3', 'implicit' ); + assert_equals( secondelem.classList.toString(), 'token1 token3', 'explicit' ); +}, 'classList.replace must collapse whitespaces around each token'); +test(function () { + secondelem.className = ' token1 token2 token1 '; + secondelem.classList.replace('token2', 'token3'); + assert_equals( secondelem.classList + '', 'token1 token3', 'implicit' ); + assert_equals( secondelem.classList.toString(), 'token1 token3', 'explicit' ); +}, 'classList.replace must collapse whitespaces around each token and remove duplicates'); +test(function () { + secondelem.className = ' token1 token2 token1 '; + secondelem.classList.replace('token1', 'token3'); + assert_equals( secondelem.classList + '', 'token3 token2', 'implicit' ); + assert_equals( secondelem.classList.toString(), 'token3 token2', 'explicit' ); +}, 'classList.replace must collapse whitespace when replacing duplicate tokens'); test(function () { assert_not_equals( getComputedStyle(elem,null).fontStyle, 'italic' ); }, 'CSS class selectors must stop matching when all classes have been removed');