| /* |
| * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| * (C) 2004-2005 Allan Sandfeld Jensen (kde@carewolf.com) |
| * Copyright (C) 2006, 2007 Nicholas Shanks (webkit@nickshanks.com) |
| * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Apple Inc. |
| * All rights reserved. |
| * Copyright (C) 2007 Alexey Proskuryakov <ap@webkit.org> |
| * Copyright (C) 2007, 2008 Eric Seidel <eric@webkit.org> |
| * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved. |
| * (http://www.torchmobile.com/) |
| * Copyright (c) 2011, Code Aurora Forum. All rights reserved. |
| * Copyright (C) Research In Motion Limited 2011. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "core/css/SelectorChecker.h" |
| |
| #include "core/HTMLNames.h" |
| #include "core/css/CSSSelectorList.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/Element.h" |
| #include "core/dom/ElementTraversal.h" |
| #include "core/dom/Fullscreen.h" |
| #include "core/dom/NodeComputedStyle.h" |
| #include "core/dom/NthIndexCache.h" |
| #include "core/dom/StyleEngine.h" |
| #include "core/dom/Text.h" |
| #include "core/dom/shadow/FlatTreeTraversal.h" |
| #include "core/dom/shadow/InsertionPoint.h" |
| #include "core/dom/shadow/ShadowRoot.h" |
| #include "core/editing/FrameSelection.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/html/HTMLDocument.h" |
| #include "core/html/HTMLFrameElementBase.h" |
| #include "core/html/HTMLInputElement.h" |
| #include "core/html/HTMLOptionElement.h" |
| #include "core/html/HTMLSelectElement.h" |
| #include "core/html/HTMLSlotElement.h" |
| #include "core/html/parser/HTMLParserIdioms.h" |
| #include "core/html/track/vtt/VTTElement.h" |
| #include "core/inspector/InspectorInstrumentation.h" |
| #include "core/page/FocusController.h" |
| #include "core/page/Page.h" |
| #include "core/style/ComputedStyle.h" |
| #include "platform/scroll/ScrollableArea.h" |
| #include "platform/scroll/ScrollbarTheme.h" |
| #include "wtf/AutoReset.h" |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| static bool isFrameFocused(const Element& element) { |
| return element.document().frame() && |
| element.document().frame()->selection().isFocusedAndActive(); |
| } |
| |
| static bool matchesSpatialNavigationFocusPseudoClass(const Element& element) { |
| return isHTMLOptionElement(element) && |
| toHTMLOptionElement(element).spatialNavigationFocused() && |
| isFrameFocused(element); |
| } |
| |
| static bool matchesListBoxPseudoClass(const Element& element) { |
| return isHTMLSelectElement(element) && |
| !toHTMLSelectElement(element).usesMenuList(); |
| } |
| |
| static bool matchesTagName(const Element& element, |
| const QualifiedName& tagQName) { |
| if (tagQName == anyQName()) |
| return true; |
| const AtomicString& localName = tagQName.localName(); |
| if (localName != starAtom && localName != element.localName()) { |
| if (element.isHTMLElement() || !element.document().isHTMLDocument()) |
| return false; |
| // Non-html elements in html documents are normalized to their camel-cased |
| // version during parsing if applicable. Yet, type selectors are lower-cased |
| // for selectors in html documents. Compare the upper case converted names |
| // instead to allow matching SVG elements like foreignObject. |
| if (element.tagQName().localNameUpper() != tagQName.localNameUpper()) |
| return false; |
| } |
| const AtomicString& namespaceURI = tagQName.namespaceURI(); |
| return namespaceURI == starAtom || namespaceURI == element.namespaceURI(); |
| } |
| |
| static Element* parentElement( |
| const SelectorChecker::SelectorCheckingContext& context) { |
| // - If context.scope is a shadow root, we should walk up to its shadow host. |
| // - If context.scope is some element in some shadow tree and querySelector |
| // initialized the context, e.g. shadowRoot.querySelector(':host *'), |
| // (a) context.element has the same treescope as context.scope, need to walk |
| // up to its shadow host. |
| // (b) Otherwise, should not walk up from a shadow root to a shadow host. |
| if (context.scope && |
| (context.scope == context.element->containingShadowRoot() || |
| context.scope->treeScope() == context.element->treeScope())) |
| return context.element->parentOrShadowHostElement(); |
| return context.element->parentElement(); |
| } |
| |
| // If context has scope, return slot that matches the scope, otherwise return |
| // the assigned slot for scope-less matching of ::slotted pseudo element. |
| static const HTMLSlotElement* findSlotElementInScope( |
| const SelectorChecker::SelectorCheckingContext& context) { |
| if (!context.scope) |
| return context.element->assignedSlot(); |
| |
| for (const HTMLSlotElement* slot = context.element->assignedSlot(); slot; |
| slot = slot->assignedSlot()) { |
| if (slot->treeScope() == context.scope->treeScope()) |
| return slot; |
| } |
| return nullptr; |
| } |
| |
| static bool scopeContainsLastMatchedElement( |
| const SelectorChecker::SelectorCheckingContext& context) { |
| // If this context isn't scoped, skip checking. |
| if (!context.scope) |
| return true; |
| |
| if (context.scope->treeScope() == context.element->treeScope()) |
| return true; |
| |
| // Because Blink treats a shadow host's TreeScope as a separate one from its |
| // descendent shadow roots, if the last matched element is a shadow host, the |
| // condition above isn't met, even though it should be. |
| return context.element == context.scope->ownerShadowHost() && |
| (!context.previousElement || |
| context.previousElement->isInDescendantTreeOf(context.element)); |
| } |
| |
| static inline bool nextSelectorExceedsScope( |
| const SelectorChecker::SelectorCheckingContext& context) { |
| if (context.scope && context.scope->isInShadowTree()) |
| return context.element == context.scope->ownerShadowHost(); |
| |
| return false; |
| } |
| |
| static bool shouldMatchHoverOrActive( |
| const SelectorChecker::SelectorCheckingContext& context) { |
| // If we're in quirks mode, then :hover and :active should never match anchors |
| // with no href and *:hover and *:active should not match anything. This is |
| // specified in https://quirks.spec.whatwg.org/#the-:active-and-:hover-quirk |
| if (!context.element->document().inQuirksMode()) |
| return true; |
| if (context.isSubSelector) |
| return true; |
| if (context.element->isLink()) |
| return true; |
| const CSSSelector* selector = context.selector; |
| while (selector->relation() == CSSSelector::SubSelector && |
| selector->tagHistory()) { |
| selector = selector->tagHistory(); |
| if (selector->match() != CSSSelector::PseudoClass) |
| return true; |
| if (selector->getPseudoType() != CSSSelector::PseudoHover && |
| selector->getPseudoType() != CSSSelector::PseudoActive) |
| return true; |
| } |
| return false; |
| } |
| |
| static bool isFirstChild(Element& element) { |
| return !ElementTraversal::previousSibling(element); |
| } |
| |
| static bool isLastChild(Element& element) { |
| return !ElementTraversal::nextSibling(element); |
| } |
| |
| static bool isFirstOfType(Element& element, const QualifiedName& type) { |
| return !ElementTraversal::previousSibling(element, HasTagName(type)); |
| } |
| |
| static bool isLastOfType(Element& element, const QualifiedName& type) { |
| return !ElementTraversal::nextSibling(element, HasTagName(type)); |
| } |
| |
| // Recursive check of selectors and combinators |
| // It can return 4 different values: |
| // * SelectorMatches - the selector matches the element e |
| // * SelectorFailsLocally - the selector fails for the element e |
| // * SelectorFailsAllSiblings - the selector fails for e and any sibling of e |
| // * SelectorFailsCompletely - the selector fails for e and any sibling or |
| // ancestor of e |
| SelectorChecker::MatchStatus SelectorChecker::matchSelector( |
| const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| MatchResult subResult; |
| if (!checkOne(context, subResult)) |
| return SelectorFailsLocally; |
| |
| if (subResult.dynamicPseudo != PseudoIdNone) |
| result.dynamicPseudo = subResult.dynamicPseudo; |
| |
| if (context.selector->isLastInTagHistory()) { |
| if (scopeContainsLastMatchedElement(context)) { |
| result.specificity += subResult.specificity; |
| return SelectorMatches; |
| } |
| return SelectorFailsLocally; |
| } |
| |
| MatchStatus match; |
| if (context.selector->relation() != CSSSelector::SubSelector) { |
| if (nextSelectorExceedsScope(context)) |
| return SelectorFailsCompletely; |
| |
| if (context.pseudoId != PseudoIdNone && |
| context.pseudoId != result.dynamicPseudo) |
| return SelectorFailsCompletely; |
| |
| AutoReset<PseudoId> dynamicPseudoScope(&result.dynamicPseudo, PseudoIdNone); |
| match = matchForRelation(context, result); |
| } else { |
| match = matchForSubSelector(context, result); |
| } |
| if (match == SelectorMatches) |
| result.specificity += subResult.specificity; |
| return match; |
| } |
| |
| static inline SelectorChecker::SelectorCheckingContext |
| prepareNextContextForRelation( |
| const SelectorChecker::SelectorCheckingContext& context) { |
| SelectorChecker::SelectorCheckingContext nextContext(context); |
| DCHECK(context.selector->tagHistory()); |
| nextContext.selector = context.selector->tagHistory(); |
| return nextContext; |
| } |
| |
| SelectorChecker::MatchStatus SelectorChecker::matchForSubSelector( |
| const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| SelectorCheckingContext nextContext = prepareNextContextForRelation(context); |
| |
| PseudoId dynamicPseudo = result.dynamicPseudo; |
| nextContext.hasScrollbarPseudo = |
| dynamicPseudo != PseudoIdNone && |
| (m_scrollbar || dynamicPseudo == PseudoIdScrollbarCorner || |
| dynamicPseudo == PseudoIdResizer); |
| nextContext.hasSelectionPseudo = dynamicPseudo == PseudoIdSelection; |
| nextContext.isSubSelector = true; |
| return matchSelector(nextContext, result); |
| } |
| |
| static inline bool isV0ShadowRoot(const Node* node) { |
| return node && node->isShadowRoot() && |
| toShadowRoot(node)->type() == ShadowRootType::V0; |
| } |
| |
| SelectorChecker::MatchStatus SelectorChecker::matchForPseudoShadow( |
| const SelectorCheckingContext& context, |
| const ContainerNode* node, |
| MatchResult& result) const { |
| if (!isV0ShadowRoot(node)) |
| return SelectorFailsCompletely; |
| if (!context.previousElement) |
| return SelectorFailsCompletely; |
| return matchSelector(context, result); |
| } |
| |
| static inline Element* parentOrV0ShadowHostElement(const Element& element) { |
| if (element.parentNode() && element.parentNode()->isShadowRoot()) { |
| if (toShadowRoot(element.parentNode())->type() != ShadowRootType::V0) |
| return nullptr; |
| } |
| return element.parentOrShadowHostElement(); |
| } |
| |
| static inline Element* parentOrOpenShadowHostElement(const Element& element) { |
| if (element.parentNode() && element.parentNode()->isShadowRoot()) { |
| if (toShadowRoot(element.parentNode())->type() != ShadowRootType::Open) |
| return nullptr; |
| } |
| return element.parentOrShadowHostElement(); |
| } |
| |
| SelectorChecker::MatchStatus SelectorChecker::matchForRelation( |
| const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| SelectorCheckingContext nextContext = prepareNextContextForRelation(context); |
| |
| CSSSelector::RelationType relation = context.selector->relation(); |
| |
| // Disable :visited matching when we see the first link or try to match |
| // anything else than an ancestors. |
| if (!context.isSubSelector && |
| (context.element->isLink() || |
| (relation != CSSSelector::Descendant && relation != CSSSelector::Child))) |
| nextContext.visitedMatchType = VisitedMatchDisabled; |
| |
| nextContext.inRightmostCompound = false; |
| nextContext.isSubSelector = false; |
| nextContext.previousElement = context.element; |
| nextContext.pseudoId = PseudoIdNone; |
| |
| switch (relation) { |
| case CSSSelector::Descendant: |
| if (context.selector->relationIsAffectedByPseudoContent()) { |
| for (Element* element = context.element; element; |
| element = element->parentElement()) { |
| if (matchForPseudoContent(nextContext, *element, result) == |
| SelectorMatches) |
| return SelectorMatches; |
| } |
| return SelectorFailsCompletely; |
| } |
| |
| if (nextContext.selector->getPseudoType() == CSSSelector::PseudoShadow) |
| return matchForPseudoShadow( |
| nextContext, context.element->containingShadowRoot(), result); |
| |
| for (nextContext.element = parentElement(context); nextContext.element; |
| nextContext.element = parentElement(nextContext)) { |
| MatchStatus match = matchSelector(nextContext, result); |
| if (match == SelectorMatches || match == SelectorFailsCompletely) |
| return match; |
| if (nextSelectorExceedsScope(nextContext)) |
| return SelectorFailsCompletely; |
| } |
| return SelectorFailsCompletely; |
| case CSSSelector::Child: { |
| if (context.selector->relationIsAffectedByPseudoContent()) |
| return matchForPseudoContent(nextContext, *context.element, result); |
| |
| if (nextContext.selector->getPseudoType() == CSSSelector::PseudoShadow) |
| return matchForPseudoShadow(nextContext, context.element->parentNode(), |
| result); |
| |
| nextContext.element = parentElement(context); |
| if (!nextContext.element) |
| return SelectorFailsCompletely; |
| return matchSelector(nextContext, result); |
| } |
| case CSSSelector::DirectAdjacent: |
| // Shadow roots can't have sibling elements |
| if (nextContext.selector->getPseudoType() == CSSSelector::PseudoShadow) |
| return SelectorFailsCompletely; |
| |
| if (m_mode == ResolvingStyle) { |
| if (ContainerNode* parent = |
| context.element->parentElementOrShadowRoot()) |
| parent->setChildrenAffectedByDirectAdjacentRules(); |
| } |
| nextContext.element = ElementTraversal::previousSibling(*context.element); |
| if (!nextContext.element) |
| return SelectorFailsAllSiblings; |
| return matchSelector(nextContext, result); |
| |
| case CSSSelector::IndirectAdjacent: |
| // Shadow roots can't have sibling elements |
| if (nextContext.selector->getPseudoType() == CSSSelector::PseudoShadow) |
| return SelectorFailsCompletely; |
| |
| if (m_mode == ResolvingStyle) { |
| if (ContainerNode* parent = |
| context.element->parentElementOrShadowRoot()) |
| parent->setChildrenAffectedByIndirectAdjacentRules(); |
| } |
| nextContext.element = ElementTraversal::previousSibling(*context.element); |
| for (; nextContext.element; |
| nextContext.element = |
| ElementTraversal::previousSibling(*nextContext.element)) { |
| MatchStatus match = matchSelector(nextContext, result); |
| if (match == SelectorMatches || match == SelectorFailsAllSiblings || |
| match == SelectorFailsCompletely) |
| return match; |
| } |
| return SelectorFailsAllSiblings; |
| |
| case CSSSelector::ShadowPseudo: { |
| if (!m_isUARule && !m_isQuerySelector && |
| context.selector->getPseudoType() == CSSSelector::PseudoShadow) |
| Deprecation::countDeprecation(context.element->document(), |
| UseCounter::CSSSelectorPseudoShadow); |
| // If we're in the same tree-scope as the scoping element, then following |
| // a shadow descendant combinator would escape that and thus the scope. |
| if (context.scope && context.scope->ownerShadowHost() && |
| context.scope->ownerShadowHost()->treeScope() == |
| context.element->treeScope()) |
| return SelectorFailsCompletely; |
| |
| Element* shadowHost = context.element->ownerShadowHost(); |
| if (!shadowHost) |
| return SelectorFailsCompletely; |
| nextContext.element = shadowHost; |
| return matchSelector(nextContext, result); |
| } |
| |
| case CSSSelector::ShadowDeep: { |
| if (!m_isUARule && !m_isQuerySelector) |
| Deprecation::countDeprecation(context.element->document(), |
| UseCounter::CSSDeepCombinator); |
| if (ShadowRoot* root = context.element->containingShadowRoot()) { |
| if (root->type() == ShadowRootType::UserAgent) |
| return SelectorFailsCompletely; |
| } |
| |
| if (context.selector->relationIsAffectedByPseudoContent()) { |
| // TODO(kochi): closed mode tree should be handled as well for |
| // ::content. |
| for (Element* element = context.element; element; |
| element = element->parentOrShadowHostElement()) { |
| if (matchForPseudoContent(nextContext, *element, result) == |
| SelectorMatches) { |
| if (context.element->isInShadowTree()) |
| UseCounter::count(context.element->document(), |
| UseCounter::CSSDeepCombinatorAndShadow); |
| return SelectorMatches; |
| } |
| } |
| return SelectorFailsCompletely; |
| } |
| |
| for (nextContext.element = parentOrV0ShadowHostElement(*context.element); |
| nextContext.element; |
| nextContext.element = |
| parentOrV0ShadowHostElement(*nextContext.element)) { |
| MatchStatus match = matchSelector(nextContext, result); |
| if (match == SelectorMatches && context.element->isInShadowTree()) |
| UseCounter::count(context.element->document(), |
| UseCounter::CSSDeepCombinatorAndShadow); |
| if (match == SelectorMatches || match == SelectorFailsCompletely) |
| return match; |
| if (nextSelectorExceedsScope(nextContext)) |
| return SelectorFailsCompletely; |
| } |
| return SelectorFailsCompletely; |
| } |
| |
| case CSSSelector::ShadowPiercingDescendant: { |
| DCHECK(m_isQuerySelector); |
| UseCounter::count(context.element->document(), |
| UseCounter::CSSShadowPiercingDescendantCombinator); |
| // TODO(kochi): parentOrOpenShadowHostElement() is necessary because |
| // SelectorQuery can pass V0 shadow roots. All closed shadow roots are |
| // already filtered out, thus once V0 is removed this logic can use |
| // parentOrShadowHostElement() instead. |
| for (nextContext.element = |
| parentOrOpenShadowHostElement(*context.element); |
| nextContext.element; |
| nextContext.element = |
| parentOrOpenShadowHostElement(*nextContext.element)) { |
| MatchStatus match = matchSelector(nextContext, result); |
| if (match == SelectorMatches || match == SelectorFailsCompletely) |
| return match; |
| if (nextSelectorExceedsScope(nextContext)) |
| break; |
| } |
| return SelectorFailsCompletely; |
| } |
| |
| case CSSSelector::ShadowSlot: { |
| const HTMLSlotElement* slot = findSlotElementInScope(context); |
| if (!slot) |
| return SelectorFailsCompletely; |
| |
| nextContext.element = const_cast<HTMLSlotElement*>(slot); |
| return matchSelector(nextContext, result); |
| } |
| |
| case CSSSelector::SubSelector: |
| break; |
| } |
| NOTREACHED(); |
| return SelectorFailsCompletely; |
| } |
| |
| SelectorChecker::MatchStatus SelectorChecker::matchForPseudoContent( |
| const SelectorCheckingContext& context, |
| const Element& element, |
| MatchResult& result) const { |
| HeapVector<Member<InsertionPoint>, 8> insertionPoints; |
| collectDestinationInsertionPoints(element, insertionPoints); |
| SelectorCheckingContext nextContext(context); |
| for (const auto& insertionPoint : insertionPoints) { |
| nextContext.element = insertionPoint; |
| // TODO(esprehn): Why does SharingRules have a special case? |
| if (m_mode == SharingRules) |
| nextContext.scope = insertionPoint->containingShadowRoot(); |
| if (match(nextContext, result)) |
| return SelectorMatches; |
| } |
| return SelectorFailsLocally; |
| } |
| |
| static bool attributeValueMatches(const Attribute& attributeItem, |
| CSSSelector::MatchType match, |
| const AtomicString& selectorValue, |
| TextCaseSensitivity caseSensitivity) { |
| // TODO(esprehn): How do we get here with a null value? |
| const AtomicString& value = attributeItem.value(); |
| if (value.isNull()) |
| return false; |
| |
| switch (match) { |
| case CSSSelector::AttributeExact: |
| if (caseSensitivity == TextCaseSensitive) |
| return selectorValue == value; |
| return equalIgnoringASCIICase(selectorValue, value); |
| case CSSSelector::AttributeSet: |
| return true; |
| case CSSSelector::AttributeList: { |
| // Ignore empty selectors or selectors containing HTML spaces |
| if (selectorValue.isEmpty() || |
| selectorValue.find(&isHTMLSpace<UChar>) != kNotFound) |
| return false; |
| |
| unsigned startSearchAt = 0; |
| while (true) { |
| size_t foundPos = |
| value.find(selectorValue, startSearchAt, caseSensitivity); |
| if (foundPos == kNotFound) |
| return false; |
| if (!foundPos || isHTMLSpace<UChar>(value[foundPos - 1])) { |
| unsigned endStr = foundPos + selectorValue.length(); |
| if (endStr == value.length() || isHTMLSpace<UChar>(value[endStr])) |
| break; // We found a match. |
| } |
| |
| // No match. Keep looking. |
| startSearchAt = foundPos + 1; |
| } |
| return true; |
| } |
| case CSSSelector::AttributeContain: |
| if (selectorValue.isEmpty()) |
| return false; |
| return value.contains(selectorValue, caseSensitivity); |
| case CSSSelector::AttributeBegin: |
| if (selectorValue.isEmpty()) |
| return false; |
| return value.startsWith(selectorValue, caseSensitivity); |
| case CSSSelector::AttributeEnd: |
| if (selectorValue.isEmpty()) |
| return false; |
| return value.endsWith(selectorValue, caseSensitivity); |
| case CSSSelector::AttributeHyphen: |
| if (value.length() < selectorValue.length()) |
| return false; |
| if (!value.startsWith(selectorValue, caseSensitivity)) |
| return false; |
| // It they start the same, check for exact match or following '-': |
| if (value.length() != selectorValue.length() && |
| value[selectorValue.length()] != '-') |
| return false; |
| return true; |
| default: |
| NOTREACHED(); |
| return false; |
| } |
| } |
| |
| static bool anyAttributeMatches(Element& element, |
| CSSSelector::MatchType match, |
| const CSSSelector& selector) { |
| const QualifiedName& selectorAttr = selector.attribute(); |
| // Should not be possible from the CSS grammar. |
| DCHECK_NE(selectorAttr.localName(), starAtom); |
| |
| // Synchronize the attribute in case it is lazy-computed. |
| // Currently all lazy properties have a null namespace, so only pass |
| // localName(). |
| element.synchronizeAttribute(selectorAttr.localName()); |
| |
| const AtomicString& selectorValue = selector.value(); |
| TextCaseSensitivity caseSensitivity = |
| (selector.attributeMatch() == CSSSelector::CaseInsensitive) |
| ? TextCaseASCIIInsensitive |
| : TextCaseSensitive; |
| |
| AttributeCollection attributes = element.attributesWithoutUpdate(); |
| for (const auto& attributeItem : attributes) { |
| if (!attributeItem.matches(selectorAttr)) { |
| if (element.isHTMLElement() || !element.document().isHTMLDocument()) |
| continue; |
| // Non-html attributes in html documents are normalized to their camel- |
| // cased version during parsing if applicable. Yet, attribute selectors |
| // are lower-cased for selectors in html documents. Compare the selector |
| // and the attribute local name insensitively to e.g. allow matching SVG |
| // attributes like viewBox. |
| if (!attributeItem.matchesCaseInsensitive(selectorAttr)) |
| continue; |
| } |
| |
| if (attributeValueMatches(attributeItem, match, selectorValue, |
| caseSensitivity)) |
| return true; |
| |
| if (caseSensitivity == TextCaseASCIIInsensitive) { |
| if (selectorAttr.namespaceURI() != starAtom) |
| return false; |
| continue; |
| } |
| |
| // Legacy dictates that values of some attributes should be compared in |
| // a case-insensitive manner regardless of whether the case insensitive |
| // flag is set or not. |
| bool legacyCaseInsensitive = |
| element.document().isHTMLDocument() && |
| !HTMLDocument::isCaseSensitiveAttribute(selectorAttr); |
| |
| // If case-insensitive, re-check, and count if result differs. |
| // See http://code.google.com/p/chromium/issues/detail?id=327060 |
| if (legacyCaseInsensitive && |
| attributeValueMatches(attributeItem, match, selectorValue, |
| TextCaseASCIIInsensitive)) { |
| UseCounter::count(element.document(), |
| UseCounter::CaseInsensitiveAttrSelectorMatch); |
| return true; |
| } |
| if (selectorAttr.namespaceURI() != starAtom) |
| return false; |
| } |
| |
| return false; |
| } |
| |
| bool SelectorChecker::checkOne(const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| DCHECK(context.element); |
| Element& element = *context.element; |
| DCHECK(context.selector); |
| const CSSSelector& selector = *context.selector; |
| |
| // Only :host and :host-context() should match the host: |
| // http://drafts.csswg.org/css-scoping/#host-element |
| if (context.scope && context.scope->ownerShadowHost() == element && |
| (!selector.isHostPseudoClass() && !context.treatShadowHostAsNormalScope && |
| selector.match() != CSSSelector::PseudoElement)) |
| return false; |
| |
| switch (selector.match()) { |
| case CSSSelector::Tag: |
| return matchesTagName(element, selector.tagQName()); |
| case CSSSelector::Class: |
| return element.hasClass() && |
| element.classNames().contains(selector.value()); |
| case CSSSelector::Id: |
| return element.hasID() && |
| element.idForStyleResolution() == selector.value(); |
| |
| // Attribute selectors |
| case CSSSelector::AttributeExact: |
| case CSSSelector::AttributeSet: |
| case CSSSelector::AttributeHyphen: |
| case CSSSelector::AttributeList: |
| case CSSSelector::AttributeContain: |
| case CSSSelector::AttributeBegin: |
| case CSSSelector::AttributeEnd: |
| return anyAttributeMatches(element, selector.match(), selector); |
| |
| case CSSSelector::PseudoClass: |
| return checkPseudoClass(context, result); |
| case CSSSelector::PseudoElement: |
| return checkPseudoElement(context, result); |
| |
| default: |
| NOTREACHED(); |
| return false; |
| } |
| } |
| |
| bool SelectorChecker::checkPseudoNot(const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| const CSSSelector& selector = *context.selector; |
| |
| SelectorCheckingContext subContext(context); |
| subContext.isSubSelector = true; |
| DCHECK(selector.selectorList()); |
| for (subContext.selector = selector.selectorList()->first(); |
| subContext.selector; |
| subContext.selector = subContext.selector->tagHistory()) { |
| // :not cannot nest. I don't really know why this is a |
| // restriction in CSS3, but it is, so let's honor it. |
| // the parser enforces that this never occurs |
| DCHECK_NE(subContext.selector->getPseudoType(), CSSSelector::PseudoNot); |
| // We select between :visited and :link when applying. We don't know which |
| // one applied (or not) yet. |
| if (subContext.selector->getPseudoType() == CSSSelector::PseudoVisited || |
| (subContext.selector->getPseudoType() == CSSSelector::PseudoLink && |
| subContext.visitedMatchType == VisitedMatchEnabled)) |
| return true; |
| if (m_mode == SharingRules) { |
| // context.scope is not available if m_mode == SharingRules. |
| // We cannot determine whether :host or :scope matches a given element or |
| // not. |
| if (subContext.selector->isHostPseudoClass() || |
| subContext.selector->getPseudoType() == CSSSelector::PseudoScope) |
| return true; |
| // :hover, :active, :focus, :-webkit-drag relies on setting flags on |
| // ComputedStyle even if the whole selector may not match. That |
| // means we cannot share style between elements which may fail |
| // matching the same selector for different reasons. An example is |
| // [attr]:hover which both fail for :hover, but an element without |
| // attr won't reach the :hover selector, hence not setting the bit. |
| if (subContext.selector->isUserActionPseudoClass()) |
| return true; |
| } |
| if (!checkOne(subContext, result)) |
| return true; |
| } |
| return false; |
| } |
| |
| bool SelectorChecker::checkPseudoClass(const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| Element& element = *context.element; |
| const CSSSelector& selector = *context.selector; |
| |
| if (context.hasScrollbarPseudo) { |
| // CSS scrollbars match a specific subset of pseudo classes, and they have |
| // specialized rules for each |
| // (since there are no elements involved). |
| return checkScrollbarPseudoClass(context, result); |
| } |
| |
| switch (selector.getPseudoType()) { |
| case CSSSelector::PseudoNot: |
| return checkPseudoNot(context, result); |
| case CSSSelector::PseudoEmpty: { |
| bool result = true; |
| for (Node* n = element.firstChild(); n; n = n->nextSibling()) { |
| if (n->isElementNode()) { |
| result = false; |
| break; |
| } |
| if (n->isTextNode()) { |
| Text* textNode = toText(n); |
| if (!textNode->data().isEmpty()) { |
| result = false; |
| break; |
| } |
| } |
| } |
| if (m_mode == ResolvingStyle) { |
| element.setStyleAffectedByEmpty(); |
| if (context.inRightmostCompound) |
| m_elementStyle->setEmptyState(result); |
| else if (element.computedStyle() && |
| (element.document().styleEngine().usesSiblingRules() || |
| element.computedStyle()->unique())) |
| element.mutableComputedStyle()->setEmptyState(result); |
| } |
| return result; |
| } |
| case CSSSelector::PseudoFirstChild: |
| if (m_mode == ResolvingStyle) { |
| if (ContainerNode* parent = element.parentElementOrDocumentFragment()) |
| parent->setChildrenAffectedByFirstChildRules(); |
| element.setAffectedByFirstChildRules(); |
| } |
| return isFirstChild(element); |
| case CSSSelector::PseudoFirstOfType: |
| if (m_mode == ResolvingStyle) { |
| if (ContainerNode* parent = element.parentElementOrDocumentFragment()) |
| parent->setChildrenAffectedByForwardPositionalRules(); |
| } |
| return isFirstOfType(element, element.tagQName()); |
| case CSSSelector::PseudoLastChild: { |
| ContainerNode* parent = element.parentElementOrDocumentFragment(); |
| if (m_mode == ResolvingStyle) { |
| if (parent) |
| parent->setChildrenAffectedByLastChildRules(); |
| element.setAffectedByLastChildRules(); |
| } |
| if (!m_isQuerySelector && parent && !parent->isFinishedParsingChildren()) |
| return false; |
| return isLastChild(element); |
| } |
| case CSSSelector::PseudoLastOfType: { |
| ContainerNode* parent = element.parentElementOrDocumentFragment(); |
| if (m_mode == ResolvingStyle) { |
| if (parent) |
| parent->setChildrenAffectedByBackwardPositionalRules(); |
| } |
| if (!m_isQuerySelector && parent && !parent->isFinishedParsingChildren()) |
| return false; |
| return isLastOfType(element, element.tagQName()); |
| } |
| case CSSSelector::PseudoOnlyChild: { |
| ContainerNode* parent = element.parentElementOrDocumentFragment(); |
| if (m_mode == ResolvingStyle) { |
| if (parent) { |
| parent->setChildrenAffectedByFirstChildRules(); |
| parent->setChildrenAffectedByLastChildRules(); |
| } |
| element.setAffectedByFirstChildRules(); |
| element.setAffectedByLastChildRules(); |
| } |
| if (!m_isQuerySelector && parent && !parent->isFinishedParsingChildren()) |
| return false; |
| return isFirstChild(element) && isLastChild(element); |
| } |
| case CSSSelector::PseudoOnlyOfType: { |
| // FIXME: This selector is very slow. |
| ContainerNode* parent = element.parentElementOrDocumentFragment(); |
| if (m_mode == ResolvingStyle && parent) { |
| parent->setChildrenAffectedByForwardPositionalRules(); |
| parent->setChildrenAffectedByBackwardPositionalRules(); |
| } |
| if (!m_isQuerySelector && parent && !parent->isFinishedParsingChildren()) |
| return false; |
| return isFirstOfType(element, element.tagQName()) && |
| isLastOfType(element, element.tagQName()); |
| } |
| case CSSSelector::PseudoPlaceholderShown: |
| if (isTextControlElement(element)) |
| return toTextControlElement(element).isPlaceholderVisible(); |
| break; |
| case CSSSelector::PseudoNthChild: |
| if (m_mode == ResolvingStyle) { |
| if (ContainerNode* parent = element.parentElementOrDocumentFragment()) |
| parent->setChildrenAffectedByForwardPositionalRules(); |
| } |
| return selector.matchNth(NthIndexCache::nthChildIndex(element)); |
| case CSSSelector::PseudoNthOfType: |
| if (m_mode == ResolvingStyle) { |
| if (ContainerNode* parent = element.parentElementOrDocumentFragment()) |
| parent->setChildrenAffectedByForwardPositionalRules(); |
| } |
| return selector.matchNth(NthIndexCache::nthOfTypeIndex(element)); |
| case CSSSelector::PseudoNthLastChild: { |
| ContainerNode* parent = element.parentElementOrDocumentFragment(); |
| if (m_mode == ResolvingStyle && parent) |
| parent->setChildrenAffectedByBackwardPositionalRules(); |
| if (!m_isQuerySelector && parent && !parent->isFinishedParsingChildren()) |
| return false; |
| return selector.matchNth(NthIndexCache::nthLastChildIndex(element)); |
| } |
| case CSSSelector::PseudoNthLastOfType: { |
| ContainerNode* parent = element.parentElementOrDocumentFragment(); |
| if (m_mode == ResolvingStyle && parent) |
| parent->setChildrenAffectedByBackwardPositionalRules(); |
| if (!m_isQuerySelector && parent && !parent->isFinishedParsingChildren()) |
| return false; |
| return selector.matchNth(NthIndexCache::nthLastOfTypeIndex(element)); |
| } |
| case CSSSelector::PseudoTarget: |
| return element == element.document().cssTarget(); |
| case CSSSelector::PseudoAny: { |
| SelectorCheckingContext subContext(context); |
| subContext.isSubSelector = true; |
| DCHECK(selector.selectorList()); |
| for (subContext.selector = selector.selectorList()->first(); |
| subContext.selector; |
| subContext.selector = CSSSelectorList::next(*subContext.selector)) { |
| if (match(subContext)) |
| return true; |
| } |
| } break; |
| case CSSSelector::PseudoAutofill: |
| return element.isFormControlElement() && |
| toHTMLFormControlElement(element).isAutofilled(); |
| case CSSSelector::PseudoAnyLink: |
| case CSSSelector::PseudoLink: |
| return element.isLink(); |
| case CSSSelector::PseudoVisited: |
| return element.isLink() && |
| context.visitedMatchType == VisitedMatchEnabled; |
| case CSSSelector::PseudoDrag: |
| if (m_mode == SharingRules) |
| return true; |
| if (m_mode == ResolvingStyle) { |
| if (context.inRightmostCompound) { |
| m_elementStyle->setAffectedByDrag(); |
| } else { |
| m_elementStyle->setUnique(); |
| element.setChildrenOrSiblingsAffectedByDrag(); |
| } |
| } |
| return element.isDragged(); |
| case CSSSelector::PseudoFocus: |
| if (m_mode == SharingRules) |
| return true; |
| if (m_mode == ResolvingStyle) { |
| if (context.inRightmostCompound) { |
| m_elementStyle->setAffectedByFocus(); |
| } else { |
| m_elementStyle->setUnique(); |
| element.setChildrenOrSiblingsAffectedByFocus(); |
| } |
| } |
| return matchesFocusPseudoClass(element); |
| case CSSSelector::PseudoHover: |
| if (m_mode == SharingRules) |
| return true; |
| if (m_mode == ResolvingStyle) { |
| if (context.inRightmostCompound) { |
| m_elementStyle->setAffectedByHover(); |
| } else { |
| m_elementStyle->setUnique(); |
| element.setChildrenOrSiblingsAffectedByHover(); |
| } |
| } |
| if (!shouldMatchHoverOrActive(context)) |
| return false; |
| if (InspectorInstrumentation::forcePseudoState(&element, |
| CSSSelector::PseudoHover)) |
| return true; |
| return element.isHovered(); |
| case CSSSelector::PseudoActive: |
| if (m_mode == SharingRules) |
| return true; |
| if (m_mode == ResolvingStyle) { |
| if (context.inRightmostCompound) { |
| m_elementStyle->setAffectedByActive(); |
| } else { |
| m_elementStyle->setUnique(); |
| element.setChildrenOrSiblingsAffectedByActive(); |
| } |
| } |
| if (!shouldMatchHoverOrActive(context)) |
| return false; |
| if (InspectorInstrumentation::forcePseudoState(&element, |
| CSSSelector::PseudoActive)) |
| return true; |
| return element.isActive(); |
| case CSSSelector::PseudoEnabled: |
| return element.matchesEnabledPseudoClass(); |
| case CSSSelector::PseudoFullPageMedia: |
| return element.document().isMediaDocument(); |
| case CSSSelector::PseudoDefault: |
| return element.matchesDefaultPseudoClass(); |
| case CSSSelector::PseudoDisabled: |
| return element.isDisabledFormControl(); |
| case CSSSelector::PseudoReadOnly: |
| return element.matchesReadOnlyPseudoClass(); |
| case CSSSelector::PseudoReadWrite: |
| return element.matchesReadWritePseudoClass(); |
| case CSSSelector::PseudoOptional: |
| return element.isOptionalFormControl(); |
| case CSSSelector::PseudoRequired: |
| return element.isRequiredFormControl(); |
| case CSSSelector::PseudoValid: |
| if (m_mode == ResolvingStyle) |
| element.document().setContainsValidityStyleRules(); |
| return element.matchesValidityPseudoClasses() && element.isValidElement(); |
| case CSSSelector::PseudoInvalid: |
| if (m_mode == ResolvingStyle) |
| element.document().setContainsValidityStyleRules(); |
| return element.matchesValidityPseudoClasses() && |
| !element.isValidElement(); |
| case CSSSelector::PseudoChecked: { |
| if (isHTMLInputElement(element)) { |
| HTMLInputElement& inputElement = toHTMLInputElement(element); |
| // Even though WinIE allows checked and indeterminate to |
| // co-exist, the CSS selector spec says that you can't be |
| // both checked and indeterminate. We will behave like WinIE |
| // behind the scenes and just obey the CSS spec here in the |
| // test for matching the pseudo. |
| if (inputElement.shouldAppearChecked() && |
| !inputElement.shouldAppearIndeterminate()) |
| return true; |
| } else if (isHTMLOptionElement(element) && |
| toHTMLOptionElement(element).selected()) { |
| return true; |
| } |
| break; |
| } |
| case CSSSelector::PseudoIndeterminate: |
| return element.shouldAppearIndeterminate(); |
| case CSSSelector::PseudoRoot: |
| return element == element.document().documentElement(); |
| case CSSSelector::PseudoLang: { |
| AtomicString value; |
| if (element.isVTTElement()) |
| value = toVTTElement(element).language(); |
| else |
| value = element.computeInheritedLanguage(); |
| const AtomicString& argument = selector.argument(); |
| if (value.isEmpty() || |
| !value.startsWith(argument, TextCaseASCIIInsensitive)) |
| break; |
| if (value.length() != argument.length() && |
| value[argument.length()] != '-') |
| break; |
| return true; |
| } |
| case CSSSelector::PseudoFullScreen: |
| // While a Document is in the fullscreen state, and the document's current |
| // fullscreen element is an element in the document, the 'full-screen' |
| // pseudoclass applies to that element. Also, an <iframe>, <object> or |
| // <embed> element whose child browsing context's Document is in the |
| // fullscreen state has the 'full-screen' pseudoclass applied. |
| if (isHTMLFrameElementBase(element) && |
| element.containsFullScreenElement()) |
| return true; |
| return Fullscreen::isCurrentFullScreenElement(element); |
| case CSSSelector::PseudoFullScreenAncestor: |
| return element.containsFullScreenElement(); |
| case CSSSelector::PseudoInRange: |
| if (m_mode == ResolvingStyle) |
| element.document().setContainsValidityStyleRules(); |
| return element.isInRange(); |
| case CSSSelector::PseudoOutOfRange: |
| if (m_mode == ResolvingStyle) |
| element.document().setContainsValidityStyleRules(); |
| return element.isOutOfRange(); |
| case CSSSelector::PseudoFutureCue: |
| return element.isVTTElement() && !toVTTElement(element).isPastNode(); |
| case CSSSelector::PseudoPastCue: |
| return element.isVTTElement() && toVTTElement(element).isPastNode(); |
| case CSSSelector::PseudoScope: |
| if (m_mode == SharingRules) |
| return true; |
| if (context.scope == &element.document()) |
| return element == element.document().documentElement(); |
| return context.scope == &element; |
| case CSSSelector::PseudoUnresolved: |
| return element.isUnresolvedV0CustomElement(); |
| case CSSSelector::PseudoDefined: |
| DCHECK(RuntimeEnabledFeatures::customElementsV1Enabled()); |
| return element.isDefined(); |
| case CSSSelector::PseudoHost: |
| case CSSSelector::PseudoHostContext: |
| return checkPseudoHost(context, result); |
| case CSSSelector::PseudoSpatialNavigationFocus: |
| return m_isUARule && matchesSpatialNavigationFocusPseudoClass(element); |
| case CSSSelector::PseudoListBox: |
| return m_isUARule && matchesListBoxPseudoClass(element); |
| case CSSSelector::PseudoHostHasAppearance: |
| if (!m_isUARule) |
| return false; |
| if (ShadowRoot* root = element.containingShadowRoot()) { |
| if (root->type() != ShadowRootType::UserAgent) |
| return false; |
| const ComputedStyle* style = root->host().computedStyle(); |
| return style && style->hasAppearance(); |
| } |
| return false; |
| case CSSSelector::PseudoWindowInactive: |
| if (!context.hasSelectionPseudo) |
| return false; |
| return !element.document().page()->focusController().isActive(); |
| case CSSSelector::PseudoHorizontal: |
| case CSSSelector::PseudoVertical: |
| case CSSSelector::PseudoDecrement: |
| case CSSSelector::PseudoIncrement: |
| case CSSSelector::PseudoStart: |
| case CSSSelector::PseudoEnd: |
| case CSSSelector::PseudoDoubleButton: |
| case CSSSelector::PseudoSingleButton: |
| case CSSSelector::PseudoNoButton: |
| case CSSSelector::PseudoCornerPresent: |
| return false; |
| case CSSSelector::PseudoUnknown: |
| default: |
| NOTREACHED(); |
| break; |
| } |
| return false; |
| } |
| |
| bool SelectorChecker::checkPseudoElement(const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| const CSSSelector& selector = *context.selector; |
| Element& element = *context.element; |
| |
| switch (selector.getPseudoType()) { |
| case CSSSelector::PseudoCue: { |
| SelectorCheckingContext subContext(context); |
| subContext.isSubSelector = true; |
| subContext.scope = nullptr; |
| subContext.treatShadowHostAsNormalScope = false; |
| |
| for (subContext.selector = selector.selectorList()->first(); |
| subContext.selector; |
| subContext.selector = CSSSelectorList::next(*subContext.selector)) { |
| if (match(subContext)) |
| return true; |
| } |
| return false; |
| } |
| case CSSSelector::PseudoPlaceholder: |
| if (ShadowRoot* root = element.containingShadowRoot()) { |
| return root->type() == ShadowRootType::UserAgent && |
| element.shadowPseudoId() == "-webkit-input-placeholder"; |
| } |
| return false; |
| case CSSSelector::PseudoWebKitCustomElement: { |
| if (ShadowRoot* root = element.containingShadowRoot()) |
| return root->type() == ShadowRootType::UserAgent && |
| element.shadowPseudoId() == selector.value(); |
| return false; |
| } |
| case CSSSelector::PseudoBlinkInternalElement: |
| if (!m_isUARule) |
| return false; |
| if (ShadowRoot* root = element.containingShadowRoot()) |
| return root->type() == ShadowRootType::UserAgent && |
| element.shadowPseudoId() == selector.value(); |
| return false; |
| case CSSSelector::PseudoSlotted: { |
| SelectorCheckingContext subContext(context); |
| subContext.isSubSelector = true; |
| subContext.scope = nullptr; |
| subContext.treatShadowHostAsNormalScope = false; |
| |
| // ::slotted() only allows one compound selector. |
| DCHECK(selector.selectorList()->first()); |
| DCHECK(!CSSSelectorList::next(*selector.selectorList()->first())); |
| subContext.selector = selector.selectorList()->first(); |
| return match(subContext); |
| } |
| case CSSSelector::PseudoContent: |
| return element.isInShadowTree() && element.isInsertionPoint(); |
| case CSSSelector::PseudoShadow: |
| return element.isInShadowTree() && context.previousElement; |
| default: |
| if (m_mode == SharingRules) |
| return true; |
| DCHECK_NE(m_mode, QueryingRules); |
| result.dynamicPseudo = CSSSelector::pseudoId(selector.getPseudoType()); |
| DCHECK_NE(result.dynamicPseudo, PseudoIdNone); |
| return true; |
| } |
| } |
| |
| bool SelectorChecker::checkPseudoHost(const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| const CSSSelector& selector = *context.selector; |
| Element& element = *context.element; |
| |
| if (m_mode == SharingRules) |
| return true; |
| // :host only matches a shadow host when :host is in a shadow tree of the |
| // shadow host. |
| if (!context.scope) |
| return false; |
| const ContainerNode* shadowHost = context.scope->ownerShadowHost(); |
| if (!shadowHost || shadowHost != element) |
| return false; |
| DCHECK(element.shadow()); |
| |
| // For the case with no parameters, i.e. just :host. |
| if (!selector.selectorList()) |
| return true; |
| |
| SelectorCheckingContext subContext(context); |
| subContext.isSubSelector = true; |
| |
| bool matched = false; |
| unsigned maxSpecificity = 0; |
| |
| // If one of simple selectors matches an element, returns SelectorMatches. |
| // Just "OR". |
| for (subContext.selector = selector.selectorList()->first(); |
| subContext.selector; |
| subContext.selector = CSSSelectorList::next(*subContext.selector)) { |
| subContext.treatShadowHostAsNormalScope = true; |
| subContext.scope = context.scope; |
| // Use FlatTreeTraversal to traverse a composed ancestor list of a given |
| // element. |
| Element* nextElement = &element; |
| SelectorCheckingContext hostContext(subContext); |
| do { |
| MatchResult subResult; |
| hostContext.element = nextElement; |
| if (match(hostContext, subResult)) { |
| matched = true; |
| // Consider div:host(div:host(div:host(div:host...))). |
| maxSpecificity = |
| std::max(maxSpecificity, hostContext.selector->specificity() + |
| subResult.specificity); |
| break; |
| } |
| hostContext.treatShadowHostAsNormalScope = false; |
| hostContext.scope = nullptr; |
| |
| if (selector.getPseudoType() == CSSSelector::PseudoHost) |
| break; |
| |
| hostContext.inRightmostCompound = false; |
| nextElement = FlatTreeTraversal::parentElement(*nextElement); |
| } while (nextElement); |
| } |
| if (matched) { |
| result.specificity += maxSpecificity; |
| return true; |
| } |
| |
| // FIXME: this was a fallthrough condition. |
| return false; |
| } |
| |
| bool SelectorChecker::checkScrollbarPseudoClass( |
| const SelectorCheckingContext& context, |
| MatchResult& result) const { |
| const CSSSelector& selector = *context.selector; |
| |
| if (selector.getPseudoType() == CSSSelector::PseudoNot) |
| return checkPseudoNot(context, result); |
| |
| // FIXME: This is a temporary hack for resizers and scrollbar corners. |
| // Eventually :window-inactive should become a real |
| // pseudo class and just apply to everything. |
| if (selector.getPseudoType() == CSSSelector::PseudoWindowInactive) |
| return !context.element->document().page()->focusController().isActive(); |
| |
| if (!m_scrollbar) |
| return false; |
| |
| switch (selector.getPseudoType()) { |
| case CSSSelector::PseudoEnabled: |
| return m_scrollbar->enabled(); |
| case CSSSelector::PseudoDisabled: |
| return !m_scrollbar->enabled(); |
| case CSSSelector::PseudoHover: { |
| ScrollbarPart hoveredPart = m_scrollbar->hoveredPart(); |
| if (m_scrollbarPart == ScrollbarBGPart) |
| return hoveredPart != NoPart; |
| if (m_scrollbarPart == TrackBGPart) |
| return hoveredPart == BackTrackPart || |
| hoveredPart == ForwardTrackPart || hoveredPart == ThumbPart; |
| return m_scrollbarPart == hoveredPart; |
| } |
| case CSSSelector::PseudoActive: { |
| ScrollbarPart pressedPart = m_scrollbar->pressedPart(); |
| if (m_scrollbarPart == ScrollbarBGPart) |
| return pressedPart != NoPart; |
| if (m_scrollbarPart == TrackBGPart) |
| return pressedPart == BackTrackPart || |
| pressedPart == ForwardTrackPart || pressedPart == ThumbPart; |
| return m_scrollbarPart == pressedPart; |
| } |
| case CSSSelector::PseudoHorizontal: |
| return m_scrollbar->orientation() == HorizontalScrollbar; |
| case CSSSelector::PseudoVertical: |
| return m_scrollbar->orientation() == VerticalScrollbar; |
| case CSSSelector::PseudoDecrement: |
| return m_scrollbarPart == BackButtonStartPart || |
| m_scrollbarPart == BackButtonEndPart || |
| m_scrollbarPart == BackTrackPart; |
| case CSSSelector::PseudoIncrement: |
| return m_scrollbarPart == ForwardButtonStartPart || |
| m_scrollbarPart == ForwardButtonEndPart || |
| m_scrollbarPart == ForwardTrackPart; |
| case CSSSelector::PseudoStart: |
| return m_scrollbarPart == BackButtonStartPart || |
| m_scrollbarPart == ForwardButtonStartPart || |
| m_scrollbarPart == BackTrackPart; |
| case CSSSelector::PseudoEnd: |
| return m_scrollbarPart == BackButtonEndPart || |
| m_scrollbarPart == ForwardButtonEndPart || |
| m_scrollbarPart == ForwardTrackPart; |
| case CSSSelector::PseudoDoubleButton: { |
| WebScrollbarButtonsPlacement buttonsPlacement = |
| m_scrollbar->theme().buttonsPlacement(); |
| if (m_scrollbarPart == BackButtonStartPart || |
| m_scrollbarPart == ForwardButtonStartPart || |
| m_scrollbarPart == BackTrackPart) |
| return buttonsPlacement == WebScrollbarButtonsPlacementDoubleStart || |
| buttonsPlacement == WebScrollbarButtonsPlacementDoubleBoth; |
| if (m_scrollbarPart == BackButtonEndPart || |
| m_scrollbarPart == ForwardButtonEndPart || |
| m_scrollbarPart == ForwardTrackPart) |
| return buttonsPlacement == WebScrollbarButtonsPlacementDoubleEnd || |
| buttonsPlacement == WebScrollbarButtonsPlacementDoubleBoth; |
| return false; |
| } |
| case CSSSelector::PseudoSingleButton: { |
| WebScrollbarButtonsPlacement buttonsPlacement = |
| m_scrollbar->theme().buttonsPlacement(); |
| if (m_scrollbarPart == BackButtonStartPart || |
| m_scrollbarPart == ForwardButtonEndPart || |
| m_scrollbarPart == BackTrackPart || |
| m_scrollbarPart == ForwardTrackPart) |
| return buttonsPlacement == WebScrollbarButtonsPlacementSingle; |
| return false; |
| } |
| case CSSSelector::PseudoNoButton: { |
| WebScrollbarButtonsPlacement buttonsPlacement = |
| m_scrollbar->theme().buttonsPlacement(); |
| if (m_scrollbarPart == BackTrackPart) |
| return buttonsPlacement == WebScrollbarButtonsPlacementNone || |
| buttonsPlacement == WebScrollbarButtonsPlacementDoubleEnd; |
| if (m_scrollbarPart == ForwardTrackPart) |
| return buttonsPlacement == WebScrollbarButtonsPlacementNone || |
| buttonsPlacement == WebScrollbarButtonsPlacementDoubleStart; |
| return false; |
| } |
| case CSSSelector::PseudoCornerPresent: |
| return m_scrollbar->getScrollableArea() && |
| m_scrollbar->getScrollableArea()->isScrollCornerVisible(); |
| default: |
| return false; |
| } |
| } |
| |
| bool SelectorChecker::matchesFocusPseudoClass(const Element& element) { |
| if (InspectorInstrumentation::forcePseudoState(const_cast<Element*>(&element), |
| CSSSelector::PseudoFocus)) |
| return true; |
| return element.isFocused() && isFrameFocused(element); |
| } |
| |
| } // namespace blink |