Adding Element.closest() API

Implementing Element.closest() API according to specs https://dom.spec.whatwg.org/#dom-element-closest

This API will parse selectors and if fails then throw SyntaxError
else return closest ancestor that matches selectors if there is
no ancestor that matches selectors, it will return nullptr.

Blink-Dev Intent to Implement and ship Link :
https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/yk_Fo6hyvb8

This patch is a merge from webkit http://trac.webkit.org/changeset/174324
by Dhi Aurrahman <diorahman@rockybars.com>".

BUG=422731, 417603
R=habib.virji@samsung.com

Review URL: https://codereview.chromium.org/701723007

git-svn-id: svn://svn.chromium.org/blink/trunk@185180 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent e37e1c97
This test makes sure the closest() API works correctly
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS theTarget.closest("#theTarget") is theTarget
PASS theTarget.closest("ancestor") is ancestor
PASS theTarget.closest("tribe ancestor") is ancestor
PASS theTarget.closest("tribe > ancestor") is ancestor
PASS theTarget.closest("realm + ancestor") is ancestor
PASS theTarget.closest("realm ~ ancestor") is ancestor
PASS theTarget.closest("tribe, ancestor") is ancestor
PASS theTarget.closest("ancestor, tribe") is ancestor
PASS theTarget.closest("tribe realm") is null
PASS theTarget.closest("tribe realm throne") is null
PASS theTarget.closest("tribe realm ancestor") is null
PASS theTarget.closest("realm > ancestor") is null
PASS theTarget.closest("throne + ancestor") is null
PASS theTarget.closest("throne ~ ancestor") is null
PASS theTarget.closest(".classic") is ancestor
PASS theTarget.closest("#john") is ancestor
PASS theTarget.closest("doe") is null
PASS theTarget.closest("ancestor[name=old]") is ancestor
PASS theTarget.closest("ancestor[name=young]") is null
PASS theTarget.closest(null) is null
PASS theTarget.closest(undefined) is null
PASS sour.closest("lemon") is sour
PASS sour.closest("a, b, c, d, e") is e
PASS sour.closest("a, b, c") is c
PASS sour.closest("a, b") is b
PASS sour.closest("e, d, c, b, a") is e
PASS sour.closest("d, c, b, a") is d
PASS sour.closest("c, b, a") is c
PASS sour.closest("b, a") is b
PASS sour.closest("a") is a
PASS document.closest is undefined
PASS document.closest() threw exception TypeError: undefined is not a function.
PASS theTarget.closest() threw exception TypeError: Failed to execute 'closest' on 'Element': 1 argument required, but only 0 present..
PASS theTarget.closest("") threw exception SyntaxError: Failed to execute 'closest' on 'Element': '' is not a valid selector..
PASS theTarget.closest(".123") threw exception SyntaxError: Failed to execute 'closest' on 'Element': '.123' is not a valid selector..
PASS theTarget.closest(" ") threw exception SyntaxError: Failed to execute 'closest' on 'Element': ' ' is not a valid selector..
PASS theTarget.closest(")") threw exception SyntaxError: Failed to execute 'closest' on 'Element': ')' is not a valid selector..
PASS theTarget.closest("(") threw exception SyntaxError: Failed to execute 'closest' on 'Element': '(' is not a valid selector..
PASS theTarget.closest("()") threw exception SyntaxError: Failed to execute 'closest' on 'Element': '()' is not a valid selector..
PASS theTarget.closest("^_^") threw exception SyntaxError: Failed to execute 'closest' on 'Element': '^_^' is not a valid selector..
PASS theTarget.closest("{") threw exception SyntaxError: Failed to execute 'closest' on 'Element': '{' is not a valid selector..
PASS theTarget.closest("}") threw exception SyntaxError: Failed to execute 'closest' on 'Element': '}' is not a valid selector..
PASS theTarget.closest("{}") threw exception SyntaxError: Failed to execute 'closest' on 'Element': '{}' is not a valid selector..
PASS successfullyParsed is true
TEST COMPLETE
<!doctype html>
<html>
<head>
<script src="../../resources/js-test.js"></script>
<script>
if (window.testRunner)
testRunner.dumpAsText();
</script>
</head>
<body>
<tribe>
<realm>
<throne></throne>
<ancestor id="doe" name="young" class="classic">
<target id="anotherTarget"></target>
</ancestor>
</realm>
<ancestor id="john" name="old" class="classic">
<sibling id="sibling"></sibling>
<target id="theTarget" webkit="fast"></target>
</ancestor>
</tribe>
<foo>
<bar>
<a id="a">
<b id="b">
<c id="c">
<d id="d">
<e id="e">
<lemon id="sour"></lemon>
</e>
</d>
</c>
</b>
</a>
</bar>
</foo>
</body>
<script>
description('This test makes sure the closest() API works correctly');
var theTarget = document.getElementById('theTarget');
var ancestor = document.getElementById('john');
var sour = document.getElementById('sour');
var sibling = document.getElementById('sibling');
var a = document.getElementById('a');
var b = document.getElementById('b');
var c = document.getElementById('c');
var d = document.getElementById('d');
var e = document.getElementById('e');
shouldBe('theTarget.closest("#theTarget")', 'theTarget');
shouldBe('theTarget.closest("ancestor")', 'ancestor');
shouldBe('theTarget.closest("tribe ancestor")', 'ancestor');
shouldBe('theTarget.closest("tribe > ancestor")', 'ancestor');
shouldBe('theTarget.closest("realm + ancestor")', 'ancestor');
shouldBe('theTarget.closest("realm ~ ancestor")', 'ancestor');
shouldBe('theTarget.closest("tribe, ancestor")', 'ancestor');
shouldBe('theTarget.closest("ancestor, tribe")', 'ancestor');
shouldBeNull('theTarget.closest("tribe realm")');
shouldBeNull('theTarget.closest("tribe realm throne")');
shouldBeNull('theTarget.closest("tribe realm ancestor")');
shouldBeNull('theTarget.closest("realm > ancestor")');
shouldBeNull('theTarget.closest("throne + ancestor")');
shouldBeNull('theTarget.closest("throne ~ ancestor")');
shouldBe('theTarget.closest(".classic")', 'ancestor');
shouldBe('theTarget.closest("#john")', 'ancestor');
shouldBeNull('theTarget.closest("doe")');
shouldBe('theTarget.closest("ancestor[name=old]")', 'ancestor');
shouldBeNull('theTarget.closest("ancestor[name=young]")');
shouldBeNull('theTarget.closest(null)');
shouldBeNull('theTarget.closest(undefined)');
shouldBe('sour.closest("lemon")', 'sour');
shouldBe('sour.closest("a, b, c, d, e")', 'e');
shouldBe('sour.closest("a, b, c")', 'c');
shouldBe('sour.closest("a, b")', 'b');
shouldBe('sour.closest("e, d, c, b, a")', 'e');
shouldBe('sour.closest("d, c, b, a")', 'd');
shouldBe('sour.closest("c, b, a")', 'c');
shouldBe('sour.closest("b, a")', 'b');
shouldBe('sour.closest("a")', 'a');
shouldBe('document.closest', 'undefined');
shouldThrow('document.closest()');
shouldThrow('theTarget.closest()');
shouldThrow('theTarget.closest("")');
shouldThrow('theTarget.closest(".123")');
shouldThrow('theTarget.closest(" ")');
shouldThrow('theTarget.closest(")")');
shouldThrow('theTarget.closest("(")');
shouldThrow('theTarget.closest("()")');
shouldThrow('theTarget.closest("^_^")');
shouldThrow('theTarget.closest("{")');
shouldThrow('theTarget.closest("}")');
shouldThrow('theTarget.closest("{}")');
</script>
</html>
This test makes sure that :scope works correctly with the closest() API.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS theTarget.closest(":scope") is theTarget
PASS theTarget.closest(":not(:scope)") is body
PASS theTarget.closest("body :scope") is theTarget
PASS theTarget.closest("body > :scope") is theTarget
PASS theTarget.closest("body:scope") is null
PASS theTarget.closest("sibling + :scope") is theTarget
PASS theTarget.closest("sibling ~ :scope") is theTarget
PASS theTarget.closest("#theTarget:scope") is theTarget
PASS theTarget.closest(":scope#theTarget") is theTarget
PASS theTarget.closest("[webkit]:scope#theTarget") is theTarget
PASS theTarget.closest(":not([webkit=fast]):scope#theTarget") is null
PASS theTarget.closest(":scope target") is null
PASS theTarget.closest(":scope > target") is null
PASS theTarget.closest(":scope + target") is null
PASS theTarget.closest(":scope ~ target") is null
PASS theTarget.closest(":scope *") is null
PASS theTarget.closest(":scope > *") is null
PASS theTarget.closest(":scope + *") is null
PASS theTarget.closest(":scope ~ *") is null
PASS successfullyParsed is true
TEST COMPLETE
<!doctype html>
<html>
<head>
<script src="../../resources/js-test.js"></script>
<script>
if (window.testRunner)
testRunner.dumpAsText();
</script>
</head>
<body>
<sibling></sibling>
<target id="theTarget" webkit="fast"></target>
</body>
<script>
description('This test makes sure that :scope works correctly with the closest() API.');
var theTarget = document.getElementById('theTarget');
var body = document.body;
shouldBe('theTarget.closest(":scope")', 'theTarget');
shouldBe('theTarget.closest(":not(:scope)")', 'body');
shouldBe('theTarget.closest("body :scope")', 'theTarget');
shouldBe('theTarget.closest("body > :scope")', 'theTarget');
shouldBeNull('theTarget.closest("body:scope")');
shouldBe('theTarget.closest("sibling + :scope")', 'theTarget');
shouldBe('theTarget.closest("sibling ~ :scope")', 'theTarget');
shouldBe('theTarget.closest("#theTarget:scope")', 'theTarget');
shouldBe('theTarget.closest(":scope#theTarget")', 'theTarget');
shouldBe('theTarget.closest("[webkit]:scope#theTarget")', 'theTarget');
shouldBeNull('theTarget.closest(":not([webkit=fast]):scope#theTarget")');
shouldBeNull('theTarget.closest(":scope target")');
shouldBeNull('theTarget.closest(":scope > target")');
shouldBeNull('theTarget.closest(":scope + target")');
shouldBeNull('theTarget.closest(":scope ~ target")');
shouldBeNull('theTarget.closest(":scope *")');
shouldBeNull('theTarget.closest(":scope > *")');
shouldBeNull('theTarget.closest(":scope + *")');
shouldBeNull('theTarget.closest(":scope ~ *")');
</script>
</html>
......@@ -2533,6 +2533,14 @@ bool Element::matches(const String& selectors, ExceptionState& exceptionState)
return selectorQuery->matches(*this);
}
Element* Element::closest(const String& selectors, ExceptionState& exceptionState)
{
SelectorQuery* selectorQuery = document().selectorQueryCache().add(AtomicString(selectors), document(), exceptionState);
if (!selectorQuery)
return nullptr;
return selectorQuery->closest(*this);
}
DOMTokenList& Element::classList()
{
ElementRareData& rareData = ensureElementRareData();
......
......@@ -411,6 +411,7 @@ public:
virtual bool matchesReadWritePseudoClass() const { return false; }
virtual bool matchesValidityPseudoClasses() const { return false; }
bool matches(const String& selectors, ExceptionState&);
Element* closest(const String& selectors, ExceptionState&);
virtual bool shouldAppearIndeterminate() const { return false; }
DOMTokenList& classList();
......
......@@ -56,6 +56,7 @@ interface Element : Node {
readonly attribute DOMString? localName;
[RaisesException] boolean matches(DOMString selectors);
[RaisesException] Element closest(DOMString selectors);
// Common extensions
......
......@@ -136,6 +136,18 @@ bool SelectorDataList::matches(Element& targetElement) const
return false;
}
Element* SelectorDataList::closest(Element& targetElement) const
{
unsigned selectorCount = m_selectors.size();
for (Element* currentElement = &targetElement; currentElement; currentElement = currentElement->parentElement()) {
for (unsigned i = 0; i < selectorCount; ++i) {
if (selectorMatches(*m_selectors[i], *currentElement, targetElement))
return currentElement;
}
}
return nullptr;
}
PassRefPtrWillBeRawPtr<StaticElementList> SelectorDataList::queryAll(ContainerNode& rootNode) const
{
WillBeHeapVector<RefPtrWillBeMember<Element> > result;
......@@ -481,6 +493,11 @@ bool SelectorQuery::matches(Element& element) const
return m_selectors.matches(element);
}
Element* SelectorQuery::closest(Element& element) const
{
return m_selectors.closest(element);
}
PassRefPtrWillBeRawPtr<StaticElementList> SelectorQuery::queryAll(ContainerNode& rootNode) const
{
return m_selectors.queryAll(rootNode);
......
......@@ -47,6 +47,7 @@ class SelectorDataList {
public:
void initialize(const CSSSelectorList&);
bool matches(Element&) const;
Element* closest(Element&) const;
PassRefPtrWillBeRawPtr<StaticElementList> queryAll(ContainerNode& rootNode) const;
PassRefPtrWillBeRawPtr<Element> queryFirst(ContainerNode& rootNode) const;
......@@ -89,6 +90,7 @@ public:
static PassOwnPtr<SelectorQuery> adopt(CSSSelectorList&);
bool matches(Element&) const;
Element* closest(Element&) const;
PassRefPtrWillBeRawPtr<StaticElementList> queryAll(ContainerNode& rootNode) const;
PassRefPtrWillBeRawPtr<Element> queryFirst(ContainerNode& rootNode) const;
private:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment