diff options
Diffstat (limited to '')
-rw-r--r-- | dom/html/nsGenericHTMLElement.cpp | 3623 |
1 files changed, 3623 insertions, 0 deletions
diff --git a/dom/html/nsGenericHTMLElement.cpp b/dom/html/nsGenericHTMLElement.cpp new file mode 100644 index 0000000000..de29276fdc --- /dev/null +++ b/dom/html/nsGenericHTMLElement.cpp @@ -0,0 +1,3623 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/EditorBase.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/IMEContentObserver.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/Maybe.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/TextEditor.h" +#include "mozilla/TextEvents.h" +#include "mozilla/StaticPrefs_html5.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "mozilla/dom/FetchPriority.h" +#include "mozilla/dom/FormData.h" +#include "nscore.h" +#include "nsGenericHTMLElement.h" +#include "nsCOMPtr.h" +#include "nsAtom.h" +#include "nsQueryObject.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsIWidget.h" +#include "nsRange.h" +#include "nsPresContext.h" +#include "nsError.h" +#include "nsIPrincipal.h" +#include "nsContainerFrame.h" +#include "nsStyleUtil.h" +#include "ReferrerInfo.h" + +#include "mozilla/PresState.h" +#include "nsILayoutHistoryState.h" + +#include "nsHTMLParts.h" +#include "nsContentUtils.h" +#include "mozilla/dom/DirectionalityUtils.h" +#include "mozilla/dom/DocumentOrShadowRoot.h" +#include "nsString.h" +#include "nsGkAtoms.h" +#include "nsDOMCSSDeclaration.h" +#include "nsITextControlFrame.h" +#include "nsIFormControl.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "nsFocusManager.h" + +#include "nsDOMStringMap.h" +#include "nsDOMString.h" + +#include "nsLayoutUtils.h" +#include "mozilla/dom/DocumentInlines.h" +#include "HTMLFieldSetElement.h" +#include "nsTextNode.h" +#include "HTMLBRElement.h" +#include "nsDOMMutationObserver.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/FromParser.h" +#include "mozilla/dom/Link.h" +#include "mozilla/dom/ScriptLoader.h" + +#include "nsDOMTokenList.h" +#include "nsThreadUtils.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/ToggleEvent.h" +#include "mozilla/dom/TouchEvent.h" +#include "mozilla/dom/InputEvent.h" +#include "mozilla/dom/InvokeEvent.h" +#include "mozilla/ErrorResult.h" +#include "nsHTMLDocument.h" +#include "nsGlobalWindowInner.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "imgIContainer.h" +#include "nsComputedDOMStyle.h" +#include "mozilla/dom/HTMLDialogElement.h" +#include "mozilla/dom/HTMLLabelElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/CustomElementRegistry.h" +#include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/ElementInternals.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static const uint8_t NS_INPUTMODE_NONE = 1; +static const uint8_t NS_INPUTMODE_TEXT = 2; +static const uint8_t NS_INPUTMODE_TEL = 3; +static const uint8_t NS_INPUTMODE_URL = 4; +static const uint8_t NS_INPUTMODE_EMAIL = 5; +static const uint8_t NS_INPUTMODE_NUMERIC = 6; +static const uint8_t NS_INPUTMODE_DECIMAL = 7; +static const uint8_t NS_INPUTMODE_SEARCH = 8; + +static const nsAttrValue::EnumTable kInputmodeTable[] = { + {"none", NS_INPUTMODE_NONE}, + {"text", NS_INPUTMODE_TEXT}, + {"tel", NS_INPUTMODE_TEL}, + {"url", NS_INPUTMODE_URL}, + {"email", NS_INPUTMODE_EMAIL}, + {"numeric", NS_INPUTMODE_NUMERIC}, + {"decimal", NS_INPUTMODE_DECIMAL}, + {"search", NS_INPUTMODE_SEARCH}, + {nullptr, 0}}; + +static const uint8_t NS_ENTERKEYHINT_ENTER = 1; +static const uint8_t NS_ENTERKEYHINT_DONE = 2; +static const uint8_t NS_ENTERKEYHINT_GO = 3; +static const uint8_t NS_ENTERKEYHINT_NEXT = 4; +static const uint8_t NS_ENTERKEYHINT_PREVIOUS = 5; +static const uint8_t NS_ENTERKEYHINT_SEARCH = 6; +static const uint8_t NS_ENTERKEYHINT_SEND = 7; + +static const nsAttrValue::EnumTable kEnterKeyHintTable[] = { + {"enter", NS_ENTERKEYHINT_ENTER}, + {"done", NS_ENTERKEYHINT_DONE}, + {"go", NS_ENTERKEYHINT_GO}, + {"next", NS_ENTERKEYHINT_NEXT}, + {"previous", NS_ENTERKEYHINT_PREVIOUS}, + {"search", NS_ENTERKEYHINT_SEARCH}, + {"send", NS_ENTERKEYHINT_SEND}, + {nullptr, 0}}; + +static const uint8_t NS_AUTOCAPITALIZE_NONE = 1; +static const uint8_t NS_AUTOCAPITALIZE_SENTENCES = 2; +static const uint8_t NS_AUTOCAPITALIZE_WORDS = 3; +static const uint8_t NS_AUTOCAPITALIZE_CHARACTERS = 4; + +static const nsAttrValue::EnumTable kAutocapitalizeTable[] = { + {"none", NS_AUTOCAPITALIZE_NONE}, + {"sentences", NS_AUTOCAPITALIZE_SENTENCES}, + {"words", NS_AUTOCAPITALIZE_WORDS}, + {"characters", NS_AUTOCAPITALIZE_CHARACTERS}, + {"off", NS_AUTOCAPITALIZE_NONE}, + {"on", NS_AUTOCAPITALIZE_SENTENCES}, + {"", 0}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable* kDefaultAutocapitalize = + &kAutocapitalizeTable[1]; + +nsresult nsGenericHTMLElement::CopyInnerTo(Element* aDst) { + MOZ_ASSERT(!aDst->GetUncomposedDoc(), + "Should not CopyInnerTo an Element in a document"); + + auto reparse = aDst->OwnerDoc() == OwnerDoc() ? ReparseAttributes::No + : ReparseAttributes::Yes; + nsresult rv = Element::CopyInnerTo(aDst, reparse); + NS_ENSURE_SUCCESS(rv, rv); + + // cloning a node must retain its internal nonce slot + nsString* nonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce)); + if (nonce) { + static_cast<nsGenericHTMLElement*>(aDst)->SetNonce(*nonce); + } + return NS_OK; +} + +static const nsAttrValue::EnumTable kDirTable[] = { + {"ltr", Directionality::Ltr}, + {"rtl", Directionality::Rtl}, + {"auto", Directionality::Auto}, + {nullptr, 0}, +}; + +namespace { +// See <https://html.spec.whatwg.org/#the-popover-attribute>. +enum class PopoverAttributeKeyword : uint8_t { Auto, EmptyString, Manual }; + +static const char* kPopoverAttributeValueAuto = "auto"; +static const char* kPopoverAttributeValueEmptyString = ""; +static const char* kPopoverAttributeValueManual = "manual"; + +static const nsAttrValue::EnumTable kPopoverTable[] = { + {kPopoverAttributeValueAuto, PopoverAttributeKeyword::Auto}, + {kPopoverAttributeValueEmptyString, PopoverAttributeKeyword::EmptyString}, + {kPopoverAttributeValueManual, PopoverAttributeKeyword::Manual}, + {nullptr, 0}}; + +// See <https://html.spec.whatwg.org/#the-popover-attribute>. +static const nsAttrValue::EnumTable* kPopoverTableInvalidValueDefault = + &kPopoverTable[2]; +} // namespace + +void nsGenericHTMLElement::GetFetchPriority(nsAString& aFetchPriority) const { + // <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>. + GetEnumAttr(nsGkAtoms::fetchpriority, kFetchPriorityAttributeValueAuto, + aFetchPriority); +} + +/* static */ +FetchPriority nsGenericHTMLElement::ToFetchPriority(const nsAString& aValue) { + nsAttrValue attrValue; + ParseFetchPriority(aValue, attrValue); + MOZ_ASSERT(attrValue.Type() == nsAttrValue::eEnum); + return FetchPriority(attrValue.GetEnumValue()); +} + +namespace { +// <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>. +static const nsAttrValue::EnumTable kFetchPriorityEnumTable[] = { + {kFetchPriorityAttributeValueHigh, FetchPriority::High}, + {kFetchPriorityAttributeValueLow, FetchPriority::Low}, + {kFetchPriorityAttributeValueAuto, FetchPriority::Auto}, + {nullptr, 0}}; + +// <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>. +static const nsAttrValue::EnumTable* + kFetchPriorityEnumTableInvalidValueDefault = &kFetchPriorityEnumTable[2]; +} // namespace + +FetchPriority nsGenericHTMLElement::GetFetchPriority() const { + const nsAttrValue* fetchpriorityAttribute = + GetParsedAttr(nsGkAtoms::fetchpriority); + if (fetchpriorityAttribute) { + MOZ_ASSERT(fetchpriorityAttribute->Type() == nsAttrValue::eEnum); + return FetchPriority(fetchpriorityAttribute->GetEnumValue()); + } + + return FetchPriority::Auto; +} + +/* static */ +void nsGenericHTMLElement::ParseFetchPriority(const nsAString& aValue, + nsAttrValue& aResult) { + aResult.ParseEnumValue(aValue, kFetchPriorityEnumTable, + false /* aCaseSensitive */, + kFetchPriorityEnumTableInvalidValueDefault); +} + +void nsGenericHTMLElement::AddToNameTable(nsAtom* aName) { + MOZ_ASSERT(HasName(), "Node doesn't have name?"); + Document* doc = GetUncomposedDoc(); + if (doc && !IsInNativeAnonymousSubtree()) { + doc->AddToNameTable(this, aName); + } +} + +void nsGenericHTMLElement::RemoveFromNameTable() { + if (HasName() && CanHaveName(NodeInfo()->NameAtom())) { + if (Document* doc = GetUncomposedDoc()) { + doc->RemoveFromNameTable(this, + GetParsedAttr(nsGkAtoms::name)->GetAtomValue()); + } + } +} + +void nsGenericHTMLElement::GetAccessKeyLabel(nsString& aLabel) { + nsAutoString suffix; + GetAccessKey(suffix); + if (!suffix.IsEmpty()) { + EventStateManager::GetAccessKeyLabelPrefix(this, aLabel); + aLabel.Append(suffix); + } +} + +static bool IsOffsetParent(nsIFrame* aFrame) { + LayoutFrameType frameType = aFrame->Type(); + + if (frameType == LayoutFrameType::TableCell || + frameType == LayoutFrameType::TableWrapper) { + // Per the IDL for Element, only td, th, and table are acceptable + // offsetParents apart from body or positioned elements; we need to check + // the content type as well as the frame type so we ignore anonymous tables + // created by an element with display: table-cell with no actual table + nsIContent* content = aFrame->GetContent(); + + return content->IsAnyOfHTMLElements(nsGkAtoms::table, nsGkAtoms::td, + nsGkAtoms::th); + } + return false; +} + +struct OffsetResult { + Element* mParent = nullptr; + CSSIntRect mRect; +}; + +static OffsetResult GetUnretargetedOffsetsFor(const Element& aElement) { + nsIFrame* frame = aElement.GetPrimaryFrame(); + if (!frame) { + return {}; + } + + nsIFrame* styleFrame = nsLayoutUtils::GetStyleFrame(frame); + + nsIFrame* parent = frame->GetParent(); + nsPoint origin(0, 0); + + nsIContent* offsetParent = nullptr; + Element* docElement = aElement.GetComposedDoc()->GetRootElement(); + nsIContent* content = frame->GetContent(); + + if (content && + (content->IsHTMLElement(nsGkAtoms::body) || content == docElement)) { + parent = frame; + } else { + const bool isPositioned = styleFrame->IsAbsPosContainingBlock(); + const bool isAbsolutelyPositioned = frame->IsAbsolutelyPositioned(); + origin += frame->GetPositionIgnoringScrolling(); + + for (; parent; parent = parent->GetParent()) { + content = parent->GetContent(); + + // Stop at the first ancestor that is positioned. + if (parent->IsAbsPosContainingBlock()) { + offsetParent = content; + break; + } + + // Add the parent's origin to our own to get to the + // right coordinate system. + const bool isOffsetParent = !isPositioned && IsOffsetParent(parent); + if (!isOffsetParent) { + origin += parent->GetPositionIgnoringScrolling(); + } + + if (content) { + // If we've hit the document element, break here. + if (content == docElement) { + break; + } + + // Break if the ancestor frame type makes it suitable as offset parent + // and this element is *not* positioned or if we found the body element. + if (isOffsetParent || content->IsHTMLElement(nsGkAtoms::body)) { + offsetParent = content; + break; + } + } + } + + if (isAbsolutelyPositioned && !offsetParent) { + // If this element is absolutely positioned, but we don't have + // an offset parent it means this element is an absolutely + // positioned child that's not nested inside another positioned + // element, in this case the element's frame's parent is the + // frame for the HTML element so we fail to find the body in the + // parent chain. We want the offset parent in this case to be + // the body, so we just get the body element from the document. + // + // We use GetBodyElement() here, not GetBody(), because we don't want to + // end up with framesets here. + offsetParent = aElement.GetComposedDoc()->GetBodyElement(); + } + } + + // Make the position relative to the padding edge. + if (parent) { + const nsStyleBorder* border = parent->StyleBorder(); + origin.x -= border->GetComputedBorderWidth(eSideLeft); + origin.y -= border->GetComputedBorderWidth(eSideTop); + } + + // Get the union of all rectangles in this and continuation frames. + // It doesn't really matter what we use as aRelativeTo here, since + // we only care about the size. We just have to use something non-null. + nsRect rcFrame = nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame); + rcFrame.MoveTo(origin); + return {Element::FromNodeOrNull(offsetParent), + CSSIntRect::FromAppUnitsRounded(rcFrame)}; +} + +static bool ShouldBeRetargeted(const Element& aReferenceElement, + const Element& aElementToMaybeRetarget) { + ShadowRoot* shadow = aElementToMaybeRetarget.GetContainingShadow(); + if (!shadow) { + return false; + } + for (ShadowRoot* scope = aReferenceElement.GetContainingShadow(); scope; + scope = scope->Host()->GetContainingShadow()) { + if (scope == shadow) { + return false; + } + } + + return true; +} + +Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) { + aRect = CSSIntRect(); + + if (!GetPrimaryFrame(FlushType::Layout)) { + return nullptr; + } + + OffsetResult thisResult = GetUnretargetedOffsetsFor(*this); + aRect = thisResult.mRect; + + Element* parent = thisResult.mParent; + while (parent && ShouldBeRetargeted(*this, *parent)) { + OffsetResult result = GetUnretargetedOffsetsFor(*parent); + aRect += result.mRect.TopLeft(); + parent = result.mParent; + } + + return parent; +} + +bool nsGenericHTMLElement::Spellcheck() { + // Has the state has been explicitly set? + nsIContent* node; + for (node = this; node; node = node->GetParent()) { + if (node->IsHTMLElement()) { + static Element::AttrValuesArray strings[] = {nsGkAtoms::_true, + nsGkAtoms::_false, nullptr}; + switch (node->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::spellcheck, strings, eCaseMatters)) { + case 0: // spellcheck = "true" + return true; + case 1: // spellcheck = "false" + return false; + } + } + } + + // contenteditable/designMode are spellchecked by default + if (IsEditable()) { + return true; + } + + // Is this a chrome element? + if (nsContentUtils::IsChromeDoc(OwnerDoc())) { + return false; // Not spellchecked by default + } + + // Anything else that's not a form control is not spellchecked by default + nsCOMPtr<nsIFormControl> formControl = do_QueryObject(this); + if (!formControl) { + return false; // Not spellchecked by default + } + + // Is this a multiline plaintext input? + auto controlType = formControl->ControlType(); + if (controlType == FormControlType::Textarea) { + return true; // Spellchecked by default + } + + // Is this anything other than an input text? + // Other inputs are not spellchecked. + if (controlType != FormControlType::InputText) { + return false; // Not spellchecked by default + } + + // Does the user want input text spellchecked by default? + // NOTE: Do not reflect a pref value of 0 back to the DOM getter. + // The web page should not know if the user has disabled spellchecking. + // We'll catch this in the editor itself. + int32_t spellcheckLevel = Preferences::GetInt("layout.spellcheckDefault", 1); + return spellcheckLevel == 2; // "Spellcheck multi- and single-line" +} + +bool nsGenericHTMLElement::InNavQuirksMode(Document* aDoc) { + return aDoc && aDoc->GetCompatibilityMode() == eCompatibility_NavQuirks; +} + +void nsGenericHTMLElement::UpdateEditableState(bool aNotify) { + // XXX Should we do this only when in a document? + ContentEditableTristate value = GetContentEditableValue(); + if (value != eInherit) { + SetEditableFlag(!!value); + UpdateReadOnlyState(aNotify); + return; + } + nsStyledElement::UpdateEditableState(aNotify); +} + +nsresult nsGenericHTMLElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElementBase::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInComposedDoc()) { + RegUnRegAccessKey(true); + } + + if (IsInUncomposedDoc()) { + if (HasName() && CanHaveName(NodeInfo()->NameAtom())) { + aContext.OwnerDoc().AddToNameTable( + this, GetParsedAttr(nsGkAtoms::name)->GetAtomValue()); + } + } + + if (HasFlag(NODE_IS_EDITABLE) && GetContentEditableValue() == eTrue && + IsInComposedDoc()) { + aContext.OwnerDoc().ChangeContentEditableCount(this, +1); + } + + // Hide any nonce from the DOM, but keep the internal value of the + // nonce by copying and resetting the internal nonce value. + if (HasFlag(NODE_HAS_NONCE_AND_HEADER_CSP) && IsInComposedDoc() && + OwnerDoc()->GetBrowsingContext()) { + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "nsGenericHTMLElement::ResetNonce::Runnable", + [self = RefPtr<nsGenericHTMLElement>(this)]() { + nsAutoString nonce; + self->GetNonce(nonce); + self->SetAttr(kNameSpaceID_None, nsGkAtoms::nonce, u""_ns, true); + self->SetNonce(nonce); + })); + } + + // We need to consider a labels element is moved to another subtree + // with different root, it needs to update labels list and its root + // as well. + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (slots && slots->mLabelsList) { + slots->mLabelsList->MaybeResetRoot(SubtreeRoot()); + } + + return rv; +} + +void nsGenericHTMLElement::UnbindFromTree(bool aNullParent) { + if (IsInComposedDoc()) { + // https://html.spec.whatwg.org/#dom-trees:hide-popover-algorithm + // If removedNode's popover attribute is not in the no popover state, then + // run the hide popover algorithm given removedNode, false, false, and + // false. + if (GetPopoverData()) { + HidePopoverWithoutRunningScript(); + } + RegUnRegAccessKey(false); + } + + RemoveFromNameTable(); + + if (GetContentEditableValue() == eTrue) { + if (Document* doc = GetComposedDoc()) { + doc->ChangeContentEditableCount(this, -1); + } + } + + nsStyledElement::UnbindFromTree(aNullParent); + + // Invalidate .labels list. It will be repopulated when used the next time. + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (slots && slots->mLabelsList) { + slots->mLabelsList->MaybeResetRoot(SubtreeRoot()); + } +} + +HTMLFormElement* nsGenericHTMLElement::FindAncestorForm( + HTMLFormElement* aCurrentForm) { + NS_ASSERTION(!HasAttr(nsGkAtoms::form) || IsHTMLElement(nsGkAtoms::img), + "FindAncestorForm should not be called if @form is set!"); + if (IsInNativeAnonymousSubtree()) { + return nullptr; + } + + nsIContent* content = this; + while (content) { + // If the current ancestor is a form, return it as our form + if (content->IsHTMLElement(nsGkAtoms::form)) { +#ifdef DEBUG + if (!nsContentUtils::IsInSameAnonymousTree(this, content)) { + // It's possible that we started unbinding at |content| or + // some ancestor of it, and |content| and |this| used to all be + // anonymous. Check for this the hard way. + for (nsIContent* child = this; child != content; + child = child->GetParent()) { + NS_ASSERTION(child->ComputeIndexInParentContent().isSome(), + "Walked too far?"); + } + } +#endif + return static_cast<HTMLFormElement*>(content); + } + + nsIContent* prevContent = content; + content = prevContent->GetParent(); + + if (!content && aCurrentForm) { + // We got to the root of the subtree we're in, and we're being removed + // from the DOM (the only time we get into this method with a non-null + // aCurrentForm). Check whether aCurrentForm is in the same subtree. If + // it is, we want to return aCurrentForm, since this case means that + // we're one of those inputs-in-a-table that have a hacked mForm pointer + // and a subtree containing both us and the form got removed from the + // DOM. + if (aCurrentForm->IsInclusiveDescendantOf(prevContent)) { + return aCurrentForm; + } + } + } + + return nullptr; +} + +bool nsGenericHTMLElement::CheckHandleEventForAnchorsPreconditions( + EventChainVisitor& aVisitor) { + MOZ_ASSERT(nsCOMPtr<Link>(do_QueryObject(this)), + "should be called only when |this| implements |Link|"); + // When disconnected, only <a> should navigate away per + // https://html.spec.whatwg.org/#cannot-navigate + return IsInComposedDoc() || IsHTMLElement(nsGkAtoms::a); +} + +void nsGenericHTMLElement::GetEventTargetParentForAnchors( + EventChainPreVisitor& aVisitor) { + nsGenericHTMLElementBase::GetEventTargetParent(aVisitor); + + if (!CheckHandleEventForAnchorsPreconditions(aVisitor)) { + return; + } + + GetEventTargetParentForLinks(aVisitor); +} + +nsresult nsGenericHTMLElement::PostHandleEventForAnchors( + EventChainPostVisitor& aVisitor) { + if (!CheckHandleEventForAnchorsPreconditions(aVisitor)) { + return NS_OK; + } + + return PostHandleEventForLinks(aVisitor); +} + +bool nsGenericHTMLElement::IsHTMLLink(nsIURI** aURI) const { + MOZ_ASSERT(aURI, "Must provide aURI out param"); + + *aURI = GetHrefURIForAnchors().take(); + // We promise out param is non-null if we return true, so base rv on it + return *aURI != nullptr; +} + +already_AddRefed<nsIURI> nsGenericHTMLElement::GetHrefURIForAnchors() const { + // This is used by the three Link implementations and + // nsHTMLStyleElement. + + // Get href= attribute (relative URI). + + // We use the nsAttrValue's copy of the URI string to avoid copying. + nsCOMPtr<nsIURI> uri; + GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(uri)); + + return uri.forget(); +} + +void nsGenericHTMLElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::accesskey) { + // Have to unregister before clearing flag. See UnregAccessKey + RegUnRegAccessKey(false); + if (!aValue) { + UnsetFlags(NODE_HAS_ACCESSKEY); + } + } else if (aName == nsGkAtoms::name) { + // Have to do this before clearing flag. See RemoveFromNameTable + RemoveFromNameTable(); + if (!aValue || aValue->IsEmptyString()) { + ClearHasName(); + } + } else if (aName == nsGkAtoms::contenteditable) { + if (aValue) { + // Set this before the attribute is set so that any subclass code that + // runs before the attribute is set won't think we're missing a + // contenteditable attr when we actually have one. + SetMayHaveContentEditableAttr(); + } + } + if (!aValue && IsEventAttributeName(aName)) { + if (EventListenerManager* manager = GetExistingListenerManager()) { + manager->RemoveEventHandler(GetEventNameForAttr(aName)); + } + } + } + + return nsGenericHTMLElementBase::BeforeSetAttr(aNamespaceID, aName, aValue, + aNotify); +} + +namespace { +constexpr PopoverAttributeState ToPopoverAttributeState( + PopoverAttributeKeyword aPopoverAttributeKeyword) { + // See <https://html.spec.whatwg.org/#the-popover-attribute>. + switch (aPopoverAttributeKeyword) { + case PopoverAttributeKeyword::Auto: + return PopoverAttributeState::Auto; + case PopoverAttributeKeyword::EmptyString: + return PopoverAttributeState::Auto; + case PopoverAttributeKeyword::Manual: + return PopoverAttributeState::Manual; + default: { + MOZ_ASSERT_UNREACHABLE(); + return PopoverAttributeState::None; + } + } +} +} // namespace + +void nsGenericHTMLElement::AfterSetPopoverAttr() { + auto mapPopoverState = [](const nsAttrValue* value) -> PopoverAttributeState { + if (value) { + MOZ_ASSERT(value->Type() == nsAttrValue::eEnum); + const auto popoverAttributeKeyword = + static_cast<PopoverAttributeKeyword>(value->GetEnumValue()); + return ToPopoverAttributeState(popoverAttributeKeyword); + } + + // The missing value default is the no popover state, see + // <https://html.spec.whatwg.org/multipage/popover.html#attr-popover>. + return PopoverAttributeState::None; + }; + + PopoverAttributeState newState = + mapPopoverState(GetParsedAttr(nsGkAtoms::popover)); + + const PopoverAttributeState oldState = GetPopoverAttributeState(); + + if (newState != oldState) { + PopoverPseudoStateUpdate(false, true); + + if (IsPopoverOpen()) { + HidePopoverInternal(/* aFocusPreviousElement = */ true, + /* aFireEvents = */ true, IgnoreErrors()); + // Event handlers could have removed the popover attribute, or changed + // its value. + // https://github.com/whatwg/html/issues/9034 + newState = mapPopoverState(GetParsedAttr(nsGkAtoms::popover)); + } + + if (newState == PopoverAttributeState::None) { + ClearPopoverData(); + RemoveStates(ElementState::POPOVER_OPEN); + } else { + // TODO: what if `HidePopoverInternal` called `ShowPopup()`? + EnsurePopoverData().SetPopoverAttributeState(newState); + } + } +} + +void nsGenericHTMLElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (IsEventAttributeName(aName) && aValue) { + MOZ_ASSERT(aValue->Type() == nsAttrValue::eString, + "Expected string value for script body"); + SetEventHandler(GetEventNameForAttr(aName), aValue->GetStringValue()); + } else if (aNotify && aName == nsGkAtoms::spellcheck) { + SyncEditorsOnSubtree(this); + } else if (aName == nsGkAtoms::popover && + StaticPrefs::dom_element_popover_enabled()) { + nsContentUtils::AddScriptRunner( + NewRunnableMethod("nsGenericHTMLElement::AfterSetPopoverAttr", this, + &nsGenericHTMLElement::AfterSetPopoverAttr)); + } else if (aName == nsGkAtoms::popovertarget) { + ClearExplicitlySetAttrElement(nsGkAtoms::popovertarget); + } else if (aName == nsGkAtoms::dir) { + auto dir = Directionality::Ltr; + // A boolean tracking whether we need to recompute our directionality. + // This needs to happen after we update our internal "dir" attribute + // state but before we call SetDirectionalityOnDescendants. + bool recomputeDirectionality = false; + ElementState dirStates; + if (aValue && aValue->Type() == nsAttrValue::eEnum) { + SetHasValidDir(); + dirStates |= ElementState::HAS_DIR_ATTR; + auto dirValue = Directionality(aValue->GetEnumValue()); + if (dirValue == Directionality::Auto) { + dirStates |= ElementState::HAS_DIR_ATTR_LIKE_AUTO; + } else { + dir = dirValue; + SetDirectionality(dir, aNotify); + if (dirValue == Directionality::Ltr) { + dirStates |= ElementState::HAS_DIR_ATTR_LTR; + } else { + MOZ_ASSERT(dirValue == Directionality::Rtl); + dirStates |= ElementState::HAS_DIR_ATTR_RTL; + } + } + } else { + if (aValue) { + // We have a value, just not a valid one. + dirStates |= ElementState::HAS_DIR_ATTR; + } + ClearHasValidDir(); + if (NodeInfo()->Equals(nsGkAtoms::bdi)) { + dirStates |= ElementState::HAS_DIR_ATTR_LIKE_AUTO; + } else { + recomputeDirectionality = true; + } + } + // Now figure out what's changed about our dir states. + ElementState oldDirStates = State() & ElementState::DIR_ATTR_STATES; + ElementState changedStates = dirStates ^ oldDirStates; + if (!changedStates.IsEmpty()) { + ToggleStates(changedStates, aNotify); + } + if (recomputeDirectionality) { + dir = RecomputeDirectionality(this, aNotify); + } + SetDirectionalityOnDescendants(this, dir, aNotify); + } else if (aName == nsGkAtoms::contenteditable) { + int32_t editableCountDelta = 0; + if (aOldValue && (aOldValue->Equals(u"true"_ns, eIgnoreCase) || + aOldValue->Equals(u""_ns, eIgnoreCase))) { + editableCountDelta = -1; + } + if (aValue && (aValue->Equals(u"true"_ns, eIgnoreCase) || + aValue->Equals(u""_ns, eIgnoreCase))) { + ++editableCountDelta; + } + ChangeEditableState(editableCountDelta); + } else if (aName == nsGkAtoms::accesskey) { + if (aValue && !aValue->Equals(u""_ns, eIgnoreCase)) { + SetFlags(NODE_HAS_ACCESSKEY); + RegUnRegAccessKey(true); + } + } else if (aName == nsGkAtoms::inert) { + if (aValue) { + AddStates(ElementState::INERT); + } else { + RemoveStates(ElementState::INERT); + } + } else if (aName == nsGkAtoms::name) { + if (aValue && !aValue->Equals(u""_ns, eIgnoreCase)) { + // This may not be quite right because we can have subclass code run + // before here. But in practice subclasses don't care about this flag, + // and in particular selector matching does not care. Otherwise we'd + // want to handle it like we handle id attributes (in PreIdMaybeChange + // and PostIdMaybeChange). + SetHasName(); + if (CanHaveName(NodeInfo()->NameAtom())) { + AddToNameTable(aValue->GetAtomValue()); + } + } + } else if (aName == nsGkAtoms::inputmode || + aName == nsGkAtoms::enterkeyhint) { + if (nsFocusManager::GetFocusedElementStatic() == this) { + if (const nsPresContext* presContext = + GetPresContext(eForComposedDoc)) { + IMEContentObserver* observer = + IMEStateManager::GetActiveContentObserver(); + if (observer && observer->IsObserving(*presContext, this)) { + if (RefPtr<EditorBase> editorBase = GetEditorWithoutCreation()) { + IMEState newState; + editorBase->GetPreferredIMEState(&newState); + OwningNonNull<nsGenericHTMLElement> kungFuDeathGrip(*this); + IMEStateManager::UpdateIMEState( + newState, kungFuDeathGrip, *editorBase, + {IMEStateManager::UpdateIMEStateOption::ForceUpdate, + IMEStateManager::UpdateIMEStateOption:: + DontCommitComposition}); + } + } + } + } + } + + // The nonce will be copied over to an internal slot and cleared from the + // Element within BindToTree to avoid CSS Selector nonce exfiltration if + // the CSP list contains a header-delivered CSP. + if (nsGkAtoms::nonce == aName) { + if (aValue) { + SetNonce(aValue->GetStringValue()); + if (OwnerDoc()->GetHasCSPDeliveredThroughHeader()) { + SetFlags(NODE_HAS_NONCE_AND_HEADER_CSP); + } + } else { + RemoveNonce(); + } + } + } + + return nsGenericHTMLElementBase::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +EventListenerManager* nsGenericHTMLElement::GetEventListenerManagerForAttr( + nsAtom* aAttrName, bool* aDefer) { + // Attributes on the body and frameset tags get set on the global object + if ((mNodeInfo->Equals(nsGkAtoms::body) || + mNodeInfo->Equals(nsGkAtoms::frameset)) && + // We only forward some event attributes from body/frameset to window + (0 +#define EVENT(name_, id_, type_, struct_) /* nothing */ +#define FORWARDED_EVENT(name_, id_, type_, struct_) \ + || nsGkAtoms::on##name_ == aAttrName +#define WINDOW_EVENT FORWARDED_EVENT +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef WINDOW_EVENT +#undef FORWARDED_EVENT +#undef EVENT + )) { + nsPIDOMWindowInner* win; + + // If we have a document, and it has a window, add the event + // listener on the window (the inner window). If not, proceed as + // normal. + // XXXbz sXBL/XBL2 issue: should we instead use GetComposedDoc() here, + // override BindToTree for those classes and munge event listeners there? + Document* document = OwnerDoc(); + + *aDefer = false; + if ((win = document->GetInnerWindow())) { + nsCOMPtr<EventTarget> piTarget(do_QueryInterface(win)); + + return piTarget->GetOrCreateListenerManager(); + } + + return nullptr; + } + + return nsGenericHTMLElementBase::GetEventListenerManagerForAttr(aAttrName, + aDefer); +} + +#define EVENT(name_, id_, type_, struct_) /* nothing; handled by nsINode */ +#define FORWARDED_EVENT(name_, id_, type_, struct_) \ + EventHandlerNonNull* nsGenericHTMLElement::GetOn##name_() { \ + if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \ + /* XXXbz note to self: add tests for this! */ \ + if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + return globalWin->GetOn##name_(); \ + } \ + return nullptr; \ + } \ + \ + return nsINode::GetOn##name_(); \ + } \ + void nsGenericHTMLElement::SetOn##name_(EventHandlerNonNull* handler) { \ + if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \ + if (!win) { \ + return; \ + } \ + \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + return globalWin->SetOn##name_(handler); \ + } \ + \ + return nsINode::SetOn##name_(handler); \ + } +#define ERROR_EVENT(name_, id_, type_, struct_) \ + already_AddRefed<EventHandlerNonNull> nsGenericHTMLElement::GetOn##name_() { \ + if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \ + /* XXXbz note to self: add tests for this! */ \ + if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + OnErrorEventHandlerNonNull* errorHandler = globalWin->GetOn##name_(); \ + if (errorHandler) { \ + RefPtr<EventHandlerNonNull> handler = \ + new EventHandlerNonNull(errorHandler); \ + return handler.forget(); \ + } \ + } \ + return nullptr; \ + } \ + \ + RefPtr<EventHandlerNonNull> handler = nsINode::GetOn##name_(); \ + return handler.forget(); \ + } \ + void nsGenericHTMLElement::SetOn##name_(EventHandlerNonNull* handler) { \ + if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \ + if (!win) { \ + return; \ + } \ + \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + RefPtr<OnErrorEventHandlerNonNull> errorHandler; \ + if (handler) { \ + errorHandler = new OnErrorEventHandlerNonNull(handler); \ + } \ + return globalWin->SetOn##name_(errorHandler); \ + } \ + \ + return nsINode::SetOn##name_(handler); \ + } +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef ERROR_EVENT +#undef FORWARDED_EVENT +#undef EVENT + +void nsGenericHTMLElement::GetBaseTarget(nsAString& aBaseTarget) const { + OwnerDoc()->GetBaseTarget(aBaseTarget); +} + +//---------------------------------------------------------------------- + +bool nsGenericHTMLElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::dir) { + return aResult.ParseEnumValue(aValue, kDirTable, false); + } + + if (aAttribute == nsGkAtoms::popover && + StaticPrefs::dom_element_popover_enabled()) { + return aResult.ParseEnumValue(aValue, kPopoverTable, false, + kPopoverTableInvalidValueDefault); + } + + if (aAttribute == nsGkAtoms::tabindex) { + return aResult.ParseIntValue(aValue); + } + + if (aAttribute == nsGkAtoms::referrerpolicy) { + return ParseReferrerAttribute(aValue, aResult); + } + + if (aAttribute == nsGkAtoms::name) { + // Store name as an atom. name="" means that the element has no name, + // not that it has an empty string as the name. + if (aValue.IsEmpty()) { + return false; + } + aResult.ParseAtom(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::contenteditable || + aAttribute == nsGkAtoms::translate) { + aResult.ParseAtom(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::rel) { + aResult.ParseAtomArray(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::inputmode) { + return aResult.ParseEnumValue(aValue, kInputmodeTable, false); + } + + if (aAttribute == nsGkAtoms::enterkeyhint) { + return aResult.ParseEnumValue(aValue, kEnterKeyHintTable, false); + } + + if (aAttribute == nsGkAtoms::autocapitalize) { + return aResult.ParseEnumValue(aValue, kAutocapitalizeTable, false); + } + } + + return nsGenericHTMLElementBase::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +bool nsGenericHTMLElement::ParseBackgroundAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && + aAttribute == nsGkAtoms::background && !aValue.IsEmpty()) { + // Resolve url to an absolute url + Document* doc = OwnerDoc(); + nsCOMPtr<nsIURI> uri; + nsresult rv = nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(uri), aValue, doc, GetBaseURI()); + if (NS_FAILED(rv)) { + return false; + } + aResult.SetTo(uri, &aValue); + return true; + } + + return false; +} + +bool nsGenericHTMLElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = {sCommonAttributeMap}; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc nsGenericHTMLElement::GetAttributeMappingFunction() + const { + return &MapCommonAttributesInto; +} + +nsIFormControlFrame* nsGenericHTMLElement::GetFormControlFrame( + bool aFlushFrames) { + auto flushType = aFlushFrames ? FlushType::Frames : FlushType::None; + nsIFrame* frame = GetPrimaryFrame(flushType); + if (!frame) { + return nullptr; + } + + if (nsIFormControlFrame* f = do_QueryFrame(frame)) { + return f; + } + + // If we have generated content, the primary frame will be a wrapper frame... + // Our real frame will be in its child list. + // + // FIXME(emilio): I don't think that's true... See bug 155957 for test-cases + // though, we should figure out whether this is still needed. + for (nsIFrame* kid : frame->PrincipalChildList()) { + if (nsIFormControlFrame* f = do_QueryFrame(kid)) { + return f; + } + } + + return nullptr; +} + +static const nsAttrValue::EnumTable kDivAlignTable[] = { + {"left", StyleTextAlign::MozLeft}, + {"right", StyleTextAlign::MozRight}, + {"center", StyleTextAlign::MozCenter}, + {"middle", StyleTextAlign::MozCenter}, + {"justify", StyleTextAlign::Justify}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable kFrameborderTable[] = { + {"yes", FrameBorderProperty::Yes}, + {"no", FrameBorderProperty::No}, + {"1", FrameBorderProperty::One}, + {"0", FrameBorderProperty::Zero}, + {nullptr, 0}}; + +// TODO(emilio): Nobody uses the parsed attribute here. +static const nsAttrValue::EnumTable kScrollingTable[] = { + {"yes", ScrollingAttribute::Yes}, + {"no", ScrollingAttribute::No}, + {"on", ScrollingAttribute::On}, + {"off", ScrollingAttribute::Off}, + {"scroll", ScrollingAttribute::Scroll}, + {"noscroll", ScrollingAttribute::Noscroll}, + {"auto", ScrollingAttribute::Auto}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable kTableVAlignTable[] = { + {"top", StyleVerticalAlignKeyword::Top}, + {"middle", StyleVerticalAlignKeyword::Middle}, + {"bottom", StyleVerticalAlignKeyword::Bottom}, + {"baseline", StyleVerticalAlignKeyword::Baseline}, + {nullptr, 0}}; + +bool nsGenericHTMLElement::ParseAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + static const nsAttrValue::EnumTable kAlignTable[] = { + {"left", StyleTextAlign::Left}, + {"right", StyleTextAlign::Right}, + + {"top", StyleVerticalAlignKeyword::Top}, + {"middle", StyleVerticalAlignKeyword::MozMiddleWithBaseline}, + + // Intentionally not bottom. + {"bottom", StyleVerticalAlignKeyword::Baseline}, + + {"center", StyleVerticalAlignKeyword::MozMiddleWithBaseline}, + {"baseline", StyleVerticalAlignKeyword::Baseline}, + + {"texttop", StyleVerticalAlignKeyword::TextTop}, + {"absmiddle", StyleVerticalAlignKeyword::Middle}, + {"abscenter", StyleVerticalAlignKeyword::Middle}, + {"absbottom", StyleVerticalAlignKeyword::Bottom}, + {nullptr, 0}}; + + static_assert(uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::Top) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::MozMiddleWithBaseline) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::Baseline) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::TextTop) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::Middle) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::Bottom)); + + static_assert(uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::Top) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::MozMiddleWithBaseline) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::Baseline) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::TextTop) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::Middle) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::Bottom)); + + return aResult.ParseEnumValue(aString, kAlignTable, false); +} + +//---------------------------------------- + +static const nsAttrValue::EnumTable kTableHAlignTable[] = { + {"left", StyleTextAlign::Left}, {"right", StyleTextAlign::Right}, + {"center", StyleTextAlign::Center}, {"char", StyleTextAlign::Char}, + {"justify", StyleTextAlign::Justify}, {nullptr, 0}}; + +bool nsGenericHTMLElement::ParseTableHAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kTableHAlignTable, false); +} + +//---------------------------------------- + +// This table is used for td, th, tr, col, thead, tbody and tfoot. +static const nsAttrValue::EnumTable kTableCellHAlignTable[] = { + {"left", StyleTextAlign::MozLeft}, + {"right", StyleTextAlign::MozRight}, + {"center", StyleTextAlign::MozCenter}, + {"char", StyleTextAlign::Char}, + {"justify", StyleTextAlign::Justify}, + {"middle", StyleTextAlign::MozCenter}, + {"absmiddle", StyleTextAlign::Center}, + {nullptr, 0}}; + +bool nsGenericHTMLElement::ParseTableCellHAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kTableCellHAlignTable, false); +} + +//---------------------------------------- + +bool nsGenericHTMLElement::ParseTableVAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kTableVAlignTable, false); +} + +bool nsGenericHTMLElement::ParseDivAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kDivAlignTable, false); +} + +bool nsGenericHTMLElement::ParseImageAttribute(nsAtom* aAttribute, + const nsAString& aString, + nsAttrValue& aResult) { + if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::hspace || aAttribute == nsGkAtoms::vspace) { + return aResult.ParseHTMLDimension(aString); + } + if (aAttribute == nsGkAtoms::border) { + return aResult.ParseNonNegativeIntValue(aString); + } + return false; +} + +bool nsGenericHTMLElement::ParseReferrerAttribute(const nsAString& aString, + nsAttrValue& aResult) { + using mozilla::dom::ReferrerInfo; + static const nsAttrValue::EnumTable kReferrerPolicyTable[] = { + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::No_referrer), + static_cast<int16_t>(ReferrerPolicy::No_referrer)}, + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Origin), + static_cast<int16_t>(ReferrerPolicy::Origin)}, + {ReferrerInfo::ReferrerPolicyToString( + ReferrerPolicy::Origin_when_cross_origin), + static_cast<int16_t>(ReferrerPolicy::Origin_when_cross_origin)}, + {ReferrerInfo::ReferrerPolicyToString( + ReferrerPolicy::No_referrer_when_downgrade), + static_cast<int16_t>(ReferrerPolicy::No_referrer_when_downgrade)}, + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Unsafe_url), + static_cast<int16_t>(ReferrerPolicy::Unsafe_url)}, + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Strict_origin), + static_cast<int16_t>(ReferrerPolicy::Strict_origin)}, + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Same_origin), + static_cast<int16_t>(ReferrerPolicy::Same_origin)}, + {ReferrerInfo::ReferrerPolicyToString( + ReferrerPolicy::Strict_origin_when_cross_origin), + static_cast<int16_t>(ReferrerPolicy::Strict_origin_when_cross_origin)}, + {nullptr, ReferrerPolicy::_empty}}; + return aResult.ParseEnumValue(aString, kReferrerPolicyTable, false); +} + +bool nsGenericHTMLElement::ParseFrameborderValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kFrameborderTable, false); +} + +bool nsGenericHTMLElement::ParseScrollingValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kScrollingTable, false); +} + +static inline void MapLangAttributeInto(MappedDeclarationsBuilder& aBuilder) { + const nsAttrValue* langValue = aBuilder.GetAttr(nsGkAtoms::lang); + if (!langValue) { + return; + } + MOZ_ASSERT(langValue->Type() == nsAttrValue::eAtom); + aBuilder.SetIdentAtomValueIfUnset(eCSSProperty__x_lang, + langValue->GetAtomValue()); + if (!aBuilder.PropertyIsSet(eCSSProperty_text_emphasis_position)) { + const nsAtom* lang = langValue->GetAtomValue(); + if (nsStyleUtil::MatchesLanguagePrefix(lang, u"zh")) { + aBuilder.SetKeywordValue(eCSSProperty_text_emphasis_position, + StyleTextEmphasisPosition::UNDER._0); + } else if (nsStyleUtil::MatchesLanguagePrefix(lang, u"ja") || + nsStyleUtil::MatchesLanguagePrefix(lang, u"mn")) { + // This branch is currently no part of the spec. + // See bug 1040668 comment 69 and comment 75. + aBuilder.SetKeywordValue(eCSSProperty_text_emphasis_position, + StyleTextEmphasisPosition::OVER._0); + } + } +} + +/** + * Handle attributes common to all html elements + */ +void nsGenericHTMLElement::MapCommonAttributesIntoExceptHidden( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty__moz_user_modify)) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::contenteditable); + if (value) { + if (value->Equals(nsGkAtoms::_empty, eCaseMatters) || + value->Equals(nsGkAtoms::_true, eIgnoreCase)) { + aBuilder.SetKeywordValue(eCSSProperty__moz_user_modify, + StyleUserModify::ReadWrite); + } else if (value->Equals(nsGkAtoms::_false, eIgnoreCase)) { + aBuilder.SetKeywordValue(eCSSProperty__moz_user_modify, + StyleUserModify::ReadOnly); + } + } + } + + MapLangAttributeInto(aBuilder); +} + +void nsGenericHTMLElement::MapCommonAttributesInto( + MappedDeclarationsBuilder& aBuilder) { + MapCommonAttributesIntoExceptHidden(aBuilder); + if (!aBuilder.PropertyIsSet(eCSSProperty_display)) { + if (aBuilder.GetAttr(nsGkAtoms::hidden)) { + aBuilder.SetKeywordValue(eCSSProperty_display, StyleDisplay::None._0); + } + } +} + +/* static */ +const nsGenericHTMLElement::MappedAttributeEntry + nsGenericHTMLElement::sCommonAttributeMap[] = {{nsGkAtoms::contenteditable}, + {nsGkAtoms::lang}, + {nsGkAtoms::hidden}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sImageMarginSizeAttributeMap[] = {{nsGkAtoms::width}, + {nsGkAtoms::height}, + {nsGkAtoms::hspace}, + {nsGkAtoms::vspace}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sImageAlignAttributeMap[] = {{nsGkAtoms::align}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sDivAlignAttributeMap[] = {{nsGkAtoms::align}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sImageBorderAttributeMap[] = {{nsGkAtoms::border}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sBackgroundAttributeMap[] = { + {nsGkAtoms::background}, {nsGkAtoms::bgcolor}, {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sBackgroundColorAttributeMap[] = { + {nsGkAtoms::bgcolor}, {nullptr}}; + +void nsGenericHTMLElement::MapImageAlignAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align); + if (value && value->Type() == nsAttrValue::eEnum) { + int32_t align = value->GetEnumValue(); + if (!aBuilder.PropertyIsSet(eCSSProperty_float)) { + if (align == uint8_t(StyleTextAlign::Left)) { + aBuilder.SetKeywordValue(eCSSProperty_float, StyleFloat::Left); + } else if (align == uint8_t(StyleTextAlign::Right)) { + aBuilder.SetKeywordValue(eCSSProperty_float, StyleFloat::Right); + } + } + if (!aBuilder.PropertyIsSet(eCSSProperty_vertical_align)) { + switch (align) { + case uint8_t(StyleTextAlign::Left): + case uint8_t(StyleTextAlign::Right): + break; + default: + aBuilder.SetKeywordValue(eCSSProperty_vertical_align, align); + break; + } + } + } +} + +void nsGenericHTMLElement::MapDivAlignAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_text_align)) { + // align: enum + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align); + if (value && value->Type() == nsAttrValue::eEnum) + aBuilder.SetKeywordValue(eCSSProperty_text_align, value->GetEnumValue()); + } +} + +void nsGenericHTMLElement::MapVAlignAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_vertical_align)) { + // align: enum + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::valign); + if (value && value->Type() == nsAttrValue::eEnum) + aBuilder.SetKeywordValue(eCSSProperty_vertical_align, + value->GetEnumValue()); + } +} + +void nsGenericHTMLElement::MapDimensionAttributeInto( + MappedDeclarationsBuilder& aBuilder, nsCSSPropertyID aProp, + const nsAttrValue& aValue) { + MOZ_ASSERT(!aBuilder.PropertyIsSet(aProp), + "Why mapping the same property twice?"); + if (aValue.Type() == nsAttrValue::eInteger) { + return aBuilder.SetPixelValue(aProp, aValue.GetIntegerValue()); + } + if (aValue.Type() == nsAttrValue::ePercent) { + return aBuilder.SetPercentValue(aProp, aValue.GetPercentValue()); + } + if (aValue.Type() == nsAttrValue::eDoubleValue) { + return aBuilder.SetPixelValue(aProp, aValue.GetDoubleValue()); + } +} + +void nsGenericHTMLElement::MapImageMarginAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + // hspace: value + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::hspace)) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_left, *value); + MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_right, *value); + } + + // vspace: value + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::vspace)) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_top, *value); + MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_bottom, *value); + } +} + +void nsGenericHTMLElement::MapWidthAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::width)) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_width, *value); + } +} + +void nsGenericHTMLElement::MapHeightAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::height)) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_height, *value); + } +} + +void nsGenericHTMLElement::DoMapAspectRatio( + const nsAttrValue& aWidth, const nsAttrValue& aHeight, + MappedDeclarationsBuilder& aBuilder) { + Maybe<double> w; + if (aWidth.Type() == nsAttrValue::eInteger) { + w.emplace(aWidth.GetIntegerValue()); + } else if (aWidth.Type() == nsAttrValue::eDoubleValue) { + w.emplace(aWidth.GetDoubleValue()); + } + + Maybe<double> h; + if (aHeight.Type() == nsAttrValue::eInteger) { + h.emplace(aHeight.GetIntegerValue()); + } else if (aHeight.Type() == nsAttrValue::eDoubleValue) { + h.emplace(aHeight.GetDoubleValue()); + } + + if (w && h) { + aBuilder.SetAspectRatio(*w, *h); + } +} + +void nsGenericHTMLElement::MapImageSizeAttributesInto( + MappedDeclarationsBuilder& aBuilder, MapAspectRatio aMapAspectRatio) { + auto* width = aBuilder.GetAttr(nsGkAtoms::width); + auto* height = aBuilder.GetAttr(nsGkAtoms::height); + if (width) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_width, *width); + } + if (height) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_height, *height); + } + if (aMapAspectRatio == MapAspectRatio::Yes && width && height) { + DoMapAspectRatio(*width, *height, aBuilder); + } +} + +void nsGenericHTMLElement::MapAspectRatioInto( + MappedDeclarationsBuilder& aBuilder) { + auto* width = aBuilder.GetAttr(nsGkAtoms::width); + auto* height = aBuilder.GetAttr(nsGkAtoms::height); + if (width && height) { + DoMapAspectRatio(*width, *height, aBuilder); + } +} + +void nsGenericHTMLElement::MapImageBorderAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + // border: pixels + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::border); + if (!value) return; + + nscoord val = 0; + if (value->Type() == nsAttrValue::eInteger) val = value->GetIntegerValue(); + + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width, (float)val); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width, (float)val); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width, (float)val); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width, (float)val); + + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_top_style, + StyleBorderStyle::Solid); + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_right_style, + StyleBorderStyle::Solid); + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_bottom_style, + StyleBorderStyle::Solid); + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_left_style, + StyleBorderStyle::Solid); + + aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_top_color); + aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_right_color); + aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_bottom_color); + aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_left_color); +} + +void nsGenericHTMLElement::MapBackgroundInto( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_background_image)) { + // background + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::background)) { + aBuilder.SetBackgroundImage(*value); + } + } +} + +void nsGenericHTMLElement::MapBGColorInto(MappedDeclarationsBuilder& aBuilder) { + if (aBuilder.PropertyIsSet(eCSSProperty_background_color)) { + return; + } + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::bgcolor); + nscolor color; + if (value && value->GetColorValue(color)) { + aBuilder.SetColorValue(eCSSProperty_background_color, color); + } +} + +void nsGenericHTMLElement::MapBackgroundAttributesInto( + MappedDeclarationsBuilder& aBuilder) { + MapBackgroundInto(aBuilder); + MapBGColorInto(aBuilder); +} + +//---------------------------------------------------------------------- + +int32_t nsGenericHTMLElement::GetIntAttr(nsAtom* aAttr, + int32_t aDefault) const { + const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr); + if (attrVal && attrVal->Type() == nsAttrValue::eInteger) { + return attrVal->GetIntegerValue(); + } + return aDefault; +} + +nsresult nsGenericHTMLElement::SetIntAttr(nsAtom* aAttr, int32_t aValue) { + nsAutoString value; + value.AppendInt(aValue); + + return SetAttr(kNameSpaceID_None, aAttr, value, true); +} + +uint32_t nsGenericHTMLElement::GetUnsignedIntAttr(nsAtom* aAttr, + uint32_t aDefault) const { + const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr); + if (!attrVal || attrVal->Type() != nsAttrValue::eInteger) { + return aDefault; + } + + return attrVal->GetIntegerValue(); +} + +uint32_t nsGenericHTMLElement::GetDimensionAttrAsUnsignedInt( + nsAtom* aAttr, uint32_t aDefault) const { + const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr); + if (!attrVal) { + return aDefault; + } + + if (attrVal->Type() == nsAttrValue::eInteger) { + return attrVal->GetIntegerValue(); + } + + if (attrVal->Type() == nsAttrValue::ePercent) { + // This is a nasty hack. When we parsed the value, we stored it as an + // ePercent, not eInteger, because there was a '%' after it in the string. + // But the spec says to basically re-parse the string as an integer. + // Luckily, we can just return the value we have stored. But + // GetPercentValue() divides it by 100, so we need to multiply it back. + return uint32_t(attrVal->GetPercentValue() * 100.0f); + } + + if (attrVal->Type() == nsAttrValue::eDoubleValue) { + return uint32_t(attrVal->GetDoubleValue()); + } + + // Unfortunately, the set of values that are valid dimensions is not a + // superset of values that are valid unsigned ints. In particular "+100" is + // not a valid dimension, but should parse as the unsigned int "100". So if + // we got here and we don't have a valid dimension value, just try re-parsing + // the string we have as an integer. + nsAutoString val; + attrVal->ToString(val); + nsContentUtils::ParseHTMLIntegerResultFlags result; + int32_t parsedInt = nsContentUtils::ParseHTMLInteger(val, &result); + if ((result & nsContentUtils::eParseHTMLInteger_Error) || parsedInt < 0) { + return aDefault; + } + + return parsedInt; +} + +void nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, + nsAString& aResult) const { + nsCOMPtr<nsIURI> uri; + bool hadAttr = GetURIAttr(aAttr, aBaseAttr, getter_AddRefs(uri)); + if (!hadAttr) { + aResult.Truncate(); + return; + } + + if (!uri) { + // Just return the attr value + GetAttr(aAttr, aResult); + return; + } + + nsAutoCString spec; + uri->GetSpec(spec); + CopyUTF8toUTF16(spec, aResult); +} + +bool nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, + nsIURI** aURI) const { + *aURI = nullptr; + + const nsAttrValue* attr = mAttrs.GetAttr(aAttr); + if (!attr) { + return false; + } + + nsCOMPtr<nsIURI> baseURI = GetBaseURI(); + + if (aBaseAttr) { + nsAutoString baseAttrValue; + if (GetAttr(aBaseAttr, baseAttrValue)) { + nsCOMPtr<nsIURI> baseAttrURI; + nsresult rv = nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(baseAttrURI), baseAttrValue, OwnerDoc(), baseURI); + if (NS_FAILED(rv)) { + return true; + } + baseURI.swap(baseAttrURI); + } + } + + // Don't care about return value. If it fails, we still want to + // return true, and *aURI will be null. + nsContentUtils::NewURIWithDocumentCharset(aURI, attr->GetStringValue(), + OwnerDoc(), baseURI); + return true; +} + +bool nsGenericHTMLElement::IsLabelable() const { + return IsAnyOfHTMLElements(nsGkAtoms::progress, nsGkAtoms::meter); +} + +/* static */ +bool nsGenericHTMLElement::MatchLabelsElement(Element* aElement, + int32_t aNamespaceID, + nsAtom* aAtom, void* aData) { + HTMLLabelElement* element = HTMLLabelElement::FromNode(aElement); + return element && element->GetControl() == aData; +} + +already_AddRefed<nsINodeList> nsGenericHTMLElement::Labels() { + MOZ_ASSERT(IsLabelable(), + "Labels() only allow labelable elements to use it."); + nsExtendedDOMSlots* slots = ExtendedDOMSlots(); + + if (!slots->mLabelsList) { + slots->mLabelsList = + new nsLabelsNodeList(SubtreeRoot(), MatchLabelsElement, nullptr, this); + } + + RefPtr<nsLabelsNodeList> labels = slots->mLabelsList; + return labels.forget(); +} + +// static +bool nsGenericHTMLElement::LegacyTouchAPIEnabled(JSContext* aCx, + JSObject* aGlobal) { + return TouchEvent::LegacyAPIEnabled(aCx, aGlobal); +} + +bool nsGenericHTMLElement::IsFormControlDefaultFocusable( + bool aWithMouse) const { + if (!aWithMouse) { + return true; + } + switch (StaticPrefs::accessibility_mouse_focuses_formcontrol()) { + case 0: + return false; + case 1: + return true; + default: + return !IsInChromeDocument(); + } +} + +//---------------------------------------------------------------------- + +nsGenericHTMLFormElement::nsGenericHTMLFormElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + // We should add the ElementState::ENABLED bit here as needed, but that + // depends on our type, which is not initialized yet. So we have to do this + // in subclasses. Same for a couple other bits. +} + +void nsGenericHTMLFormElement::ClearForm(bool aRemoveFromForm, + bool aUnbindOrDelete) { + MOZ_ASSERT(IsFormAssociatedElement()); + + HTMLFormElement* form = GetFormInternal(); + NS_ASSERTION((form != nullptr) == HasFlag(ADDED_TO_FORM), + "Form control should have had flag set correctly"); + + if (!form) { + return; + } + + if (aRemoveFromForm) { + nsAutoString nameVal, idVal; + GetAttr(nsGkAtoms::name, nameVal); + GetAttr(nsGkAtoms::id, idVal); + + form->RemoveElement(this, true); + + if (!nameVal.IsEmpty()) { + form->RemoveElementFromTable(this, nameVal); + } + + if (!idVal.IsEmpty()) { + form->RemoveElementFromTable(this, idVal); + } + } + + UnsetFlags(ADDED_TO_FORM); + SetFormInternal(nullptr, false); + AfterClearForm(aUnbindOrDelete); +} + +nsresult nsGenericHTMLFormElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsFormAssociatedElement()) { + // If @form is set, the element *has* to be in a composed document, + // otherwise it wouldn't be possible to find an element with the + // corresponding id. If @form isn't set, the element *has* to have a parent, + // otherwise it wouldn't be possible to find a form ancestor. We should not + // call UpdateFormOwner if none of these conditions are fulfilled. + if (HasAttr(nsGkAtoms::form) ? IsInComposedDoc() : aParent.IsContent()) { + UpdateFormOwner(true, nullptr); + } + } + + // Set parent fieldset which should be used for the disabled state. + UpdateFieldSet(false); + return NS_OK; +} + +void nsGenericHTMLFormElement::UnbindFromTree(bool aNullParent) { + // Save state before doing anything else. + SaveState(); + + if (IsFormAssociatedElement()) { + if (HTMLFormElement* form = GetFormInternal()) { + // Might need to unset form + if (aNullParent) { + // No more parent means no more form + ClearForm(true, true); + } else { + // Recheck whether we should still have an form. + if (HasAttr(nsGkAtoms::form) || !FindAncestorForm(form)) { + ClearForm(true, true); + } else { + UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT); + } + } + } + + // We have to remove the form id observer if there was one. + // We will re-add one later if needed (during bind to tree). + if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, + nsGkAtoms::form)) { + RemoveFormIdObserver(); + } + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + // The element might not have a fieldset anymore. + UpdateFieldSet(false); +} + +void nsGenericHTMLFormElement::BeforeSetAttr(int32_t aNameSpaceID, + nsAtom* aName, + const nsAttrValue* aValue, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && IsFormAssociatedElement()) { + nsAutoString tmp; + HTMLFormElement* form = GetFormInternal(); + + // remove the control from the hashtable as needed + + if (form && (aName == nsGkAtoms::name || aName == nsGkAtoms::id)) { + GetAttr(aName, tmp); + + if (!tmp.IsEmpty()) { + form->RemoveElementFromTable(this, tmp); + } + } + + if (form && aName == nsGkAtoms::type) { + GetAttr(nsGkAtoms::name, tmp); + + if (!tmp.IsEmpty()) { + form->RemoveElementFromTable(this, tmp); + } + + GetAttr(nsGkAtoms::id, tmp); + + if (!tmp.IsEmpty()) { + form->RemoveElementFromTable(this, tmp); + } + + form->RemoveElement(this, false); + } + + if (aName == nsGkAtoms::form) { + // If @form isn't set or set to the empty string, there were no observer + // so we don't have to remove it. + if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, + nsGkAtoms::form)) { + // The current form id observer is no longer needed. + // A new one may be added in AfterSetAttr. + RemoveFormIdObserver(); + } + } + } + + return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, + aNotify); +} + +void nsGenericHTMLFormElement::AfterSetAttr( + int32_t aNameSpaceID, nsAtom* aName, const nsAttrValue* aValue, + const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && IsFormAssociatedElement()) { + HTMLFormElement* form = GetFormInternal(); + + // add the control to the hashtable as needed + if (form && (aName == nsGkAtoms::name || aName == nsGkAtoms::id) && + aValue && !aValue->IsEmptyString()) { + MOZ_ASSERT(aValue->Type() == nsAttrValue::eAtom, + "Expected atom value for name/id"); + form->AddElementToTable(this, + nsDependentAtomString(aValue->GetAtomValue())); + } + + if (form && aName == nsGkAtoms::type) { + nsAutoString tmp; + + GetAttr(nsGkAtoms::name, tmp); + + if (!tmp.IsEmpty()) { + form->AddElementToTable(this, tmp); + } + + GetAttr(nsGkAtoms::id, tmp); + + if (!tmp.IsEmpty()) { + form->AddElementToTable(this, tmp); + } + + form->AddElement(this, false, aNotify); + } + + if (aName == nsGkAtoms::form) { + // We need a new form id observer. + DocumentOrShadowRoot* docOrShadow = + GetUncomposedDocOrConnectedShadowRoot(); + if (docOrShadow) { + Element* formIdElement = nullptr; + if (aValue && !aValue->IsEmptyString()) { + formIdElement = AddFormIdObserver(); + } + + // Because we have a new @form value (or no more @form), we have to + // update our form owner. + UpdateFormOwner(false, formIdElement); + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void nsGenericHTMLFormElement::ForgetFieldSet(nsIContent* aFieldset) { + MOZ_DIAGNOSTIC_ASSERT(IsFormAssociatedElement()); + if (GetFieldSetInternal() == aFieldset) { + SetFieldSetInternal(nullptr); + } +} + +Element* nsGenericHTMLFormElement::AddFormIdObserver() { + MOZ_ASSERT(IsFormAssociatedElement()); + + nsAutoString formId; + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + GetAttr(nsGkAtoms::form, formId); + NS_ASSERTION(!formId.IsEmpty(), + "@form value should not be the empty string!"); + RefPtr<nsAtom> atom = NS_Atomize(formId); + + return docOrShadow->AddIDTargetObserver(atom, FormIdUpdated, this, false); +} + +void nsGenericHTMLFormElement::RemoveFormIdObserver() { + MOZ_ASSERT(IsFormAssociatedElement()); + + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + if (!docOrShadow) { + return; + } + + nsAutoString formId; + GetAttr(nsGkAtoms::form, formId); + NS_ASSERTION(!formId.IsEmpty(), + "@form value should not be the empty string!"); + RefPtr<nsAtom> atom = NS_Atomize(formId); + + docOrShadow->RemoveIDTargetObserver(atom, FormIdUpdated, this, false); +} + +/* static */ +bool nsGenericHTMLFormElement::FormIdUpdated(Element* aOldElement, + Element* aNewElement, + void* aData) { + nsGenericHTMLFormElement* element = + static_cast<nsGenericHTMLFormElement*>(aData); + + NS_ASSERTION(element->IsHTMLElement(), "aData should be an HTML element"); + + element->UpdateFormOwner(false, aNewElement); + + return true; +} + +bool nsGenericHTMLFormElement::IsElementDisabledForEvents(WidgetEvent* aEvent, + nsIFrame* aFrame) { + MOZ_ASSERT(aEvent); + + // Allow dispatch of CustomEvent and untrusted Events. + if (!aEvent->IsTrusted()) { + return false; + } + + switch (aEvent->mMessage) { + case eAnimationStart: + case eAnimationEnd: + case eAnimationIteration: + case eAnimationCancel: + case eFormChange: + case eMouseMove: + case eMouseOver: + case eMouseOut: + case eMouseEnter: + case eMouseLeave: + case ePointerMove: + case ePointerOver: + case ePointerOut: + case ePointerEnter: + case ePointerLeave: + case eTransitionCancel: + case eTransitionEnd: + case eTransitionRun: + case eTransitionStart: + case eWheel: + case eLegacyMouseLineOrPageScroll: + case eLegacyMousePixelScroll: + return false; + case eFocus: + case eBlur: + case eFocusIn: + case eFocusOut: + case eKeyPress: + case eKeyUp: + case eKeyDown: + if (StaticPrefs::dom_forms_always_allow_key_and_focus_events_enabled()) { + return false; + } + [[fallthrough]]; + case ePointerDown: + case ePointerUp: + case ePointerCancel: + case ePointerGotCapture: + case ePointerLostCapture: + if (StaticPrefs::dom_forms_always_allow_pointer_events_enabled()) { + return false; + } + [[fallthrough]]; + default: + break; + } + + if (aEvent->mSpecifiedEventType == nsGkAtoms::oninput) { + return false; + } + + // FIXME(emilio): This poking at the style of the frame is slightly bogus + // unless we flush before every event, which we don't really want to do. + if (aFrame && aFrame->StyleUI()->UserInput() == StyleUserInput::None) { + return true; + } + + return IsDisabled(); +} + +void nsGenericHTMLFormElement::UpdateFormOwner(bool aBindToTree, + Element* aFormIdElement) { + MOZ_ASSERT(IsFormAssociatedElement()); + MOZ_ASSERT(!aBindToTree || !aFormIdElement, + "aFormIdElement shouldn't be set if aBindToTree is true!"); + + HTMLFormElement* form = GetFormInternal(); + if (!aBindToTree) { + ClearForm(true, false); + form = nullptr; + } + + HTMLFormElement* oldForm = form; + if (!form) { + // If @form is set, we have to use that to find the form. + nsAutoString formId; + if (GetAttr(nsGkAtoms::form, formId)) { + if (!formId.IsEmpty()) { + Element* element = nullptr; + + if (aBindToTree) { + element = AddFormIdObserver(); + } else { + element = aFormIdElement; + } + + NS_ASSERTION(!IsInComposedDoc() || + element == GetUncomposedDocOrConnectedShadowRoot() + ->GetElementById(formId), + "element should be equals to the current element " + "associated with the id in @form!"); + + if (element && element->IsHTMLElement(nsGkAtoms::form) && + nsContentUtils::IsInSameAnonymousTree(this, element)) { + form = static_cast<HTMLFormElement*>(element); + SetFormInternal(form, aBindToTree); + } + } + } else { + // We now have a parent, so we may have picked up an ancestor form. Search + // for it. Note that if form is already set we don't want to do this, + // because that means someone (probably the content sink) has already set + // it to the right value. Also note that even if being bound here didn't + // change our parent, we still need to search, since our parent chain + // probably changed _somewhere_. + form = FindAncestorForm(); + SetFormInternal(form, aBindToTree); + } + } + + if (form && !HasFlag(ADDED_TO_FORM)) { + // Now we need to add ourselves to the form + nsAutoString nameVal, idVal; + GetAttr(nsGkAtoms::name, nameVal); + GetAttr(nsGkAtoms::id, idVal); + + SetFlags(ADDED_TO_FORM); + + // Notify only if we just found this form. + form->AddElement(this, true, oldForm == nullptr); + + if (!nameVal.IsEmpty()) { + form->AddElementToTable(this, nameVal); + } + + if (!idVal.IsEmpty()) { + form->AddElementToTable(this, idVal); + } + } +} + +void nsGenericHTMLFormElement::UpdateFieldSet(bool aNotify) { + if (IsInNativeAnonymousSubtree() || !IsFormAssociatedElement()) { + MOZ_ASSERT_IF(IsFormAssociatedElement(), !GetFieldSetInternal()); + return; + } + + nsIContent* parent = nullptr; + nsIContent* prev = nullptr; + HTMLFieldSetElement* fieldset = GetFieldSetInternal(); + + for (parent = GetParent(); parent; + prev = parent, parent = parent->GetParent()) { + HTMLFieldSetElement* parentFieldset = HTMLFieldSetElement::FromNode(parent); + if (parentFieldset && (!prev || parentFieldset->GetFirstLegend() != prev)) { + if (fieldset == parentFieldset) { + // We already have the right fieldset; + return; + } + + if (fieldset) { + fieldset->RemoveElement(this); + } + SetFieldSetInternal(parentFieldset); + parentFieldset->AddElement(this); + + // The disabled state may have changed + FieldSetDisabledChanged(aNotify); + return; + } + } + + // No fieldset found. + if (fieldset) { + fieldset->RemoveElement(this); + SetFieldSetInternal(nullptr); + // The disabled state may have changed + FieldSetDisabledChanged(aNotify); + } +} + +void nsGenericHTMLFormElement::UpdateDisabledState(bool aNotify) { + if (!CanBeDisabled()) { + return; + } + + HTMLFieldSetElement* fieldset = GetFieldSetInternal(); + const bool isDisabled = + HasAttr(nsGkAtoms::disabled) || (fieldset && fieldset->IsDisabled()); + + const ElementState disabledStates = + isDisabled ? ElementState::DISABLED : ElementState::ENABLED; + + ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES; + ElementState changedStates = disabledStates ^ oldDisabledStates; + + if (!changedStates.IsEmpty()) { + ToggleStates(changedStates, aNotify); + if (DoesReadOnlyApply()) { + // :disabled influences :read-only / :read-write. + UpdateReadOnlyState(aNotify); + } + } +} + +bool nsGenericHTMLFormElement::IsReadOnlyInternal() const { + if (DoesReadOnlyApply()) { + return IsDisabled() || GetBoolAttr(nsGkAtoms::readonly); + } + return nsGenericHTMLElement::IsReadOnlyInternal(); +} + +void nsGenericHTMLFormElement::FieldSetDisabledChanged(bool aNotify) { + UpdateDisabledState(aNotify); +} + +void nsGenericHTMLFormElement::SaveSubtreeState() { + SaveState(); + + nsGenericHTMLElement::SaveSubtreeState(); +} + +//---------------------------------------------------------------------- + +void nsGenericHTMLElement::Click(CallerType aCallerType) { + if (HandlingClick()) { + return; + } + + // There are two notions of disabled. + // "disabled": + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled + // "actually disabled": + // https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled + // click() reads the former but IsDisabled() is for the latter. <fieldset> is + // included only in the latter, so we exclude it here. + // XXX(krosylight): What about <optgroup>? And should we add a separate method + // for this? + if (IsDisabled() && + !(mNodeInfo->Equals(nsGkAtoms::fieldset) && + StaticPrefs::dom_forms_fieldset_disable_only_descendants_enabled())) { + return; + } + + // Strong in case the event kills it + nsCOMPtr<Document> doc = GetComposedDoc(); + + RefPtr<nsPresContext> context; + if (doc) { + PresShell* presShell = doc->GetPresShell(); + if (!presShell) { + // We need the nsPresContext for dispatching the click event. In some + // rare cases we need to flush notifications to force creation of the + // nsPresContext here (for example when a script calls button.click() + // from script early during page load). We only flush the notifications + // if the PresShell hasn't been created yet, to limit the performance + // impact. + doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames); + presShell = doc->GetPresShell(); + } + if (presShell) { + context = presShell->GetPresContext(); + } + } + + SetHandlingClick(); + + // Mark this event trusted if Click() is called from system code. + WidgetMouseEvent event(aCallerType == CallerType::System, eMouseClick, + nullptr, WidgetMouseEvent::eReal); + event.mFlags.mIsPositionless = true; + event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + + EventDispatcher::Dispatch(this, context, &event); + + ClearHandlingClick(); +} + +bool nsGenericHTMLElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + MOZ_ASSERT(aIsFocusable); + MOZ_ASSERT(aTabIndex); + if (ShadowRoot* root = GetShadowRoot()) { + if (root->DelegatesFocus()) { + *aIsFocusable = false; + return true; + } + } + + if (!IsInComposedDoc() || IsInDesignMode()) { + // In designMode documents we only allow focusing the document. + *aTabIndex = -1; + *aIsFocusable = false; + return true; + } + + *aTabIndex = TabIndex(); + bool disabled = false; + bool disallowOverridingFocusability = true; + Maybe<int32_t> attrVal = GetTabIndexAttrValue(); + if (IsEditingHost()) { + // Editable roots should always be focusable. + disallowOverridingFocusability = true; + + // Ignore the disabled attribute in editable contentEditable/designMode + // roots. + if (attrVal.isNothing()) { + // The default value for tabindex should be 0 for editable + // contentEditable roots. + *aTabIndex = 0; + } + } else { + disallowOverridingFocusability = false; + + // Just check for disabled attribute on form controls + disabled = IsDisabled(); + if (disabled) { + *aTabIndex = -1; + } + } + + // If a tabindex is specified at all, or the default tabindex is 0, we're + // focusable. + *aIsFocusable = (*aTabIndex >= 0 || (!disabled && attrVal.isSome())); + return disallowOverridingFocusability; +} + +Result<bool, nsresult> nsGenericHTMLElement::PerformAccesskey( + bool aKeyCausesActivation, bool aIsTrustedEvent) { + RefPtr<nsPresContext> presContext = GetPresContext(eForComposedDoc); + if (!presContext) { + return Err(NS_ERROR_UNEXPECTED); + } + + // It's hard to say what HTML4 wants us to do in all cases. + bool focused = true; + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + fm->SetFocus(this, nsIFocusManager::FLAG_BYKEY); + + // Return true if the element became the current focus within its window. + nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow(); + focused = window && window->GetFocusedElement() == this; + } + + if (aKeyCausesActivation) { + // Click on it if the users prefs indicate to do so. + AutoHandlingUserInputStatePusher userInputStatePusher(aIsTrustedEvent); + AutoPopupStatePusher popupStatePusher( + aIsTrustedEvent ? PopupBlocker::openAllowed : PopupBlocker::openAbused); + DispatchSimulatedClick(this, aIsTrustedEvent, presContext); + return focused; + } + + // If the accesskey won't cause the activation and the focus isn't changed, + // either. Return error so EventStateManager would try to find next element + // to handle the accesskey. + return focused ? Result<bool, nsresult>{focused} : Err(NS_ERROR_ABORT); +} + +void nsGenericHTMLElement::HandleKeyboardActivation( + EventChainPostVisitor& aVisitor) { + MOZ_ASSERT(aVisitor.mEvent->HasKeyEventMessage()); + MOZ_ASSERT(aVisitor.mEvent->IsTrusted()); + + // If focused element is different from this element, it may be editable. + // In that case, associated editor for the element should handle the keyboard + // instead. Therefore, if this is not the focused element, we should not + // handle the event here. Note that this element may be an editing host, + // i.e., focused and editable. In the case, keyboard events should be + // handled by the focused element instead of associated editor because + // Chrome handles the case so. For compatibility with Chrome, we follow them. + if (nsFocusManager::GetFocusedElementStatic() != this) { + return; + } + + const auto message = aVisitor.mEvent->mMessage; + const WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent(); + if (nsEventStatus_eIgnore != aVisitor.mEventStatus) { + if (message == eKeyUp && keyEvent->mKeyCode == NS_VK_SPACE) { + // Unset the flag even if the event is default-prevented or something. + UnsetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD); + } + return; + } + + bool shouldActivate = false; + switch (message) { + case eKeyDown: + if (keyEvent->ShouldWorkAsSpaceKey()) { + SetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD); + } + return; + case eKeyPress: + shouldActivate = keyEvent->mKeyCode == NS_VK_RETURN; + if (keyEvent->ShouldWorkAsSpaceKey()) { + // Consume 'space' key to prevent scrolling the page down. + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + break; + case eKeyUp: + shouldActivate = keyEvent->ShouldWorkAsSpaceKey() && + HasFlag(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD); + if (shouldActivate) { + UnsetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD); + } + break; + default: + MOZ_ASSERT_UNREACHABLE("why didn't we bail out earlier?"); + break; + } + + if (!shouldActivate) { + return; + } + + RefPtr<nsPresContext> presContext = aVisitor.mPresContext; + DispatchSimulatedClick(this, aVisitor.mEvent->IsTrusted(), presContext); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; +} + +nsresult nsGenericHTMLElement::DispatchSimulatedClick( + nsGenericHTMLElement* aElement, bool aIsTrusted, + nsPresContext* aPresContext) { + WidgetMouseEvent event(aIsTrusted, eMouseClick, nullptr, + WidgetMouseEvent::eReal); + event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_KEYBOARD; + event.mFlags.mIsPositionless = true; + return EventDispatcher::Dispatch(aElement, aPresContext, &event); +} + +already_AddRefed<EditorBase> nsGenericHTMLElement::GetAssociatedEditor() { + // If contenteditable is ever implemented, it might need to do something + // different here? + + RefPtr<TextEditor> textEditor = GetTextEditorInternal(); + return textEditor.forget(); +} + +// static +void nsGenericHTMLElement::SyncEditorsOnSubtree(nsIContent* content) { + /* Sync this node */ + nsGenericHTMLElement* element = FromNode(content); + if (element) { + if (RefPtr<EditorBase> editorBase = element->GetAssociatedEditor()) { + editorBase->SyncRealTimeSpell(); + } + } + + /* Sync all children */ + for (nsIContent* child = content->GetFirstChild(); child; + child = child->GetNextSibling()) { + SyncEditorsOnSubtree(child); + } +} + +static void MakeContentDescendantsEditable(nsIContent* aContent) { + // If aContent is not an element, we just need to update its + // internal editable state and don't need to notify anyone about + // that. For elements, we need to send a ElementStateChanged + // notification. + if (!aContent->IsElement()) { + aContent->UpdateEditableState(false); + return; + } + + Element* element = aContent->AsElement(); + + element->UpdateEditableState(true); + + for (nsIContent* child = aContent->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (!child->IsElement() || + !child->AsElement()->HasAttr(nsGkAtoms::contenteditable)) { + MakeContentDescendantsEditable(child); + } + } +} + +void nsGenericHTMLElement::ChangeEditableState(int32_t aChange) { + Document* document = GetComposedDoc(); + if (!document) { + return; + } + + Document::EditingState previousEditingState = Document::EditingState::eOff; + if (aChange != 0) { + document->ChangeContentEditableCount(this, aChange); + previousEditingState = document->GetEditingState(); + } + + // MakeContentDescendantsEditable is going to call ElementStateChanged for + // this element and all descendants if editable state has changed. + // We might as well wrap it all in one script blocker. + nsAutoScriptBlocker scriptBlocker; + MakeContentDescendantsEditable(this); + + // If the document already had contenteditable and JS adds new + // contenteditable, that might cause changing editing host to current editing + // host's ancestor. In such case, HTMLEditor needs to know that + // synchronously to update selection limitter. + // Additionally, elements in shadow DOM is not editable in the normal cases, + // but if its content has `contenteditable`, only in it can be ediable. + // So we don't need to notify HTMLEditor of this change only when we're not + // in shadow DOM and the composed document is in design mode. + if (IsInDesignMode() && !IsInShadowTree() && aChange > 0 && + previousEditingState == Document::EditingState::eContentEditable) { + if (HTMLEditor* htmlEditor = + nsContentUtils::GetHTMLEditor(document->GetPresContext())) { + htmlEditor->NotifyEditingHostMaybeChanged(); + } + } +} + +//---------------------------------------------------------------------- + +nsGenericHTMLFormControlElement::nsGenericHTMLFormControlElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, FormControlType aType) + : nsGenericHTMLFormElement(std::move(aNodeInfo)), + nsIFormControl(aType), + mForm(nullptr), + mFieldSet(nullptr) {} + +nsGenericHTMLFormControlElement::~nsGenericHTMLFormControlElement() { + if (mFieldSet) { + mFieldSet->RemoveElement(this); + } + + // Check that this element doesn't know anything about its form at this point. + NS_ASSERTION(!mForm, "mForm should be null at this point!"); +} + +NS_IMPL_ISUPPORTS_INHERITED(nsGenericHTMLFormControlElement, + nsGenericHTMLFormElement, nsIFormControl) + +nsINode* nsGenericHTMLFormControlElement::GetScopeChainParent() const { + return mForm ? mForm : nsGenericHTMLElement::GetScopeChainParent(); +} + +nsIContent::IMEState nsGenericHTMLFormControlElement::GetDesiredIMEState() { + TextEditor* textEditor = GetTextEditorInternal(); + if (!textEditor) { + return nsGenericHTMLFormElement::GetDesiredIMEState(); + } + IMEState state; + nsresult rv = textEditor->GetPreferredIMEState(&state); + if (NS_FAILED(rv)) { + return nsGenericHTMLFormElement::GetDesiredIMEState(); + } + return state; +} + +void nsGenericHTMLFormControlElement::GetAutocapitalize( + nsAString& aValue) const { + if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, + nsGkAtoms::autocapitalize)) { + nsGenericHTMLFormElement::GetAutocapitalize(aValue); + return; + } + + if (mForm && IsAutocapitalizeInheriting()) { + mForm->GetAutocapitalize(aValue); + } +} + +bool nsGenericHTMLFormControlElement::IsHTMLFocusable(bool aWithMouse, + bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLFormElement::IsHTMLFocusable(aWithMouse, aIsFocusable, + aTabIndex)) { + return true; + } + + *aIsFocusable = *aIsFocusable && IsFormControlDefaultFocusable(aWithMouse); + return false; +} + +void nsGenericHTMLFormControlElement::GetEventTargetParent( + EventChainPreVisitor& aVisitor) { + if (aVisitor.mEvent->IsTrusted() && (aVisitor.mEvent->mMessage == eFocus || + aVisitor.mEvent->mMessage == eBlur)) { + // We have to handle focus/blur event to change focus states in + // PreHandleEvent to prevent it breaks event target chain creation. + aVisitor.mWantsPreHandleEvent = true; + } + nsGenericHTMLFormElement::GetEventTargetParent(aVisitor); +} + +nsresult nsGenericHTMLFormControlElement::PreHandleEvent( + EventChainVisitor& aVisitor) { + if (aVisitor.mEvent->IsTrusted()) { + switch (aVisitor.mEvent->mMessage) { + case eFocus: { + // Check to see if focus has bubbled up from a form control's + // child textfield or button. If that's the case, don't focus + // this parent file control -- leave focus on the child. + nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); + if (formControlFrame && + aVisitor.mEvent->mOriginalTarget == static_cast<nsINode*>(this)) { + formControlFrame->SetFocus(true, true); + } + break; + } + case eBlur: { + nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); + if (formControlFrame) { + formControlFrame->SetFocus(false, false); + } + break; + } + default: + break; + } + } + return nsGenericHTMLFormElement::PreHandleEvent(aVisitor); +} + +HTMLFieldSetElement* nsGenericHTMLFormControlElement::GetFieldSet() { + return GetFieldSetInternal(); +} + +void nsGenericHTMLFormControlElement::SetForm(HTMLFormElement* aForm) { + MOZ_ASSERT(aForm, "Don't pass null here"); + NS_ASSERTION(!mForm, + "We don't support switching from one non-null form to another."); + + SetFormInternal(aForm, false); +} + +void nsGenericHTMLFormControlElement::ClearForm(bool aRemoveFromForm, + bool aUnbindOrDelete) { + nsGenericHTMLFormElement::ClearForm(aRemoveFromForm, aUnbindOrDelete); +} + +bool nsGenericHTMLFormControlElement::IsLabelable() const { + auto type = ControlType(); + return (IsInputElement(type) && type != FormControlType::InputHidden) || + IsButtonElement(type) || type == FormControlType::Output || + type == FormControlType::Select || type == FormControlType::Textarea; +} + +bool nsGenericHTMLFormControlElement::CanBeDisabled() const { + auto type = ControlType(); + // It's easier to test the types that _cannot_ be disabled + return type != FormControlType::Object && type != FormControlType::Output; +} + +bool nsGenericHTMLFormControlElement::DoesReadOnlyApply() const { + auto type = ControlType(); + if (!IsInputElement(type) && type != FormControlType::Textarea) { + return false; + } + + switch (type) { + case FormControlType::InputHidden: + case FormControlType::InputButton: + case FormControlType::InputImage: + case FormControlType::InputReset: + case FormControlType::InputSubmit: + case FormControlType::InputRadio: + case FormControlType::InputFile: + case FormControlType::InputCheckbox: + case FormControlType::InputRange: + case FormControlType::InputColor: + return false; +#ifdef DEBUG + case FormControlType::Textarea: + case FormControlType::InputText: + case FormControlType::InputPassword: + case FormControlType::InputSearch: + case FormControlType::InputTel: + case FormControlType::InputEmail: + case FormControlType::InputUrl: + case FormControlType::InputNumber: + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputDatetimeLocal: + return true; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesReadOnlyApply()"); + return true; +#else // DEBUG + default: + return true; +#endif // DEBUG + } +} + +void nsGenericHTMLFormControlElement::SetFormInternal(HTMLFormElement* aForm, + bool aBindToTree) { + if (aForm) { + BeforeSetForm(aForm, aBindToTree); + } + + // keep a *weak* ref to the form here + mForm = aForm; +} + +HTMLFormElement* nsGenericHTMLFormControlElement::GetFormInternal() const { + return mForm; +} + +HTMLFieldSetElement* nsGenericHTMLFormControlElement::GetFieldSetInternal() + const { + return mFieldSet; +} + +void nsGenericHTMLFormControlElement::SetFieldSetInternal( + HTMLFieldSetElement* aFieldset) { + mFieldSet = aFieldset; +} + +void nsGenericHTMLFormControlElement::UpdateRequiredState(bool aIsRequired, + bool aNotify) { +#ifdef DEBUG + auto type = ControlType(); +#endif + MOZ_ASSERT(IsInputElement(type) || type == FormControlType::Select || + type == FormControlType::Textarea, + "This should be called only on types that @required applies"); + +#ifdef DEBUG + if (HTMLInputElement* input = HTMLInputElement::FromNode(this)) { + MOZ_ASSERT( + input->DoesRequiredApply(), + "This should be called only on input types that @required applies"); + } +#endif + + ElementState requiredStates; + if (aIsRequired) { + requiredStates |= ElementState::REQUIRED; + } else { + requiredStates |= ElementState::OPTIONAL_; + } + + ElementState oldRequiredStates = State() & ElementState::REQUIRED_STATES; + ElementState changedStates = requiredStates ^ oldRequiredStates; + + if (!changedStates.IsEmpty()) { + ToggleStates(changedStates, aNotify); + } +} + +bool nsGenericHTMLFormControlElement::IsAutocapitalizeInheriting() const { + auto type = ControlType(); + return IsInputElement(type) || IsButtonElement(type) || + type == FormControlType::Fieldset || type == FormControlType::Output || + type == FormControlType::Select || type == FormControlType::Textarea; +} + +nsresult nsGenericHTMLFormControlElement::SubmitDirnameDir( + FormData* aFormData) { + // Submit dirname=dir if element has non-empty dirname attribute + if (HasAttr(nsGkAtoms::dirname)) { + nsAutoString dirname; + GetAttr(nsGkAtoms::dirname, dirname); + if (!dirname.IsEmpty()) { + const Directionality dir = GetDirectionality(); + MOZ_ASSERT(dir == Directionality::Ltr || dir == Directionality::Rtl, + "The directionality of an element is either ltr or rtl"); + return aFormData->AddNameValuePair( + dirname, dir == Directionality::Ltr ? u"ltr"_ns : u"rtl"_ns); + } + } + return NS_OK; +} + +//---------------------------------------------------------------------- + +static const nsAttrValue::EnumTable kPopoverTargetActionTable[] = { + {"toggle", PopoverTargetAction::Toggle}, + {"show", PopoverTargetAction::Show}, + {"hide", PopoverTargetAction::Hide}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable* kPopoverTargetActionDefault = + &kPopoverTargetActionTable[0]; + +nsGenericHTMLFormControlElementWithState:: + nsGenericHTMLFormControlElementWithState( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser, FormControlType aType) + : nsGenericHTMLFormControlElement(std::move(aNodeInfo), aType), + mControlNumber(!!(aFromParser & FROM_PARSER_NETWORK) + ? OwnerDoc()->GetNextControlNumber() + : -1) { + mStateKey.SetIsVoid(true); +} + +bool nsGenericHTMLFormControlElementWithState::ParseAttribute( + int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (StaticPrefs::dom_element_popover_enabled()) { + if (aAttribute == nsGkAtoms::popovertargetaction) { + return aResult.ParseEnumValue(aValue, kPopoverTargetActionTable, false, + kPopoverTargetActionDefault); + } + if (aAttribute == nsGkAtoms::popovertarget) { + aResult.ParseAtom(aValue); + return true; + } + } + + if (StaticPrefs::dom_element_invokers_enabled()) { + if (aAttribute == nsGkAtoms::invokeaction) { + aResult.ParseAtom(aValue); + return true; + } + if (aAttribute == nsGkAtoms::invoketarget) { + aResult.ParseAtom(aValue); + return true; + } + } + } + + return nsGenericHTMLFormControlElement::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +mozilla::dom::Element* +nsGenericHTMLFormControlElementWithState::GetPopoverTargetElement() const { + return GetAttrAssociatedElement(nsGkAtoms::popovertarget); +} + +void nsGenericHTMLFormControlElementWithState::SetPopoverTargetElement( + mozilla::dom::Element* aElement) { + ExplicitlySetAttrElement(nsGkAtoms::popovertarget, aElement); +} + +void nsGenericHTMLFormControlElementWithState::HandlePopoverTargetAction() { + RefPtr<nsGenericHTMLElement> target = GetEffectivePopoverTargetElement(); + if (!target) { + return; + } + + auto action = PopoverTargetAction::Toggle; + if (const nsAttrValue* value = + GetParsedAttr(nsGkAtoms::popovertargetaction)) { + MOZ_ASSERT(value->Type() == nsAttrValue::eEnum); + action = static_cast<PopoverTargetAction>(value->GetEnumValue()); + } + + bool canHide = action == PopoverTargetAction::Hide || + action == PopoverTargetAction::Toggle; + bool shouldHide = canHide && target->IsPopoverOpen(); + bool canShow = action == PopoverTargetAction::Show || + action == PopoverTargetAction::Toggle; + bool shouldShow = canShow && !target->IsPopoverOpen(); + + if (shouldHide) { + target->HidePopover(IgnoreErrors()); + } else if (shouldShow) { + target->ShowPopoverInternal(this, IgnoreErrors()); + } +} + +void nsGenericHTMLFormControlElementWithState::GetInvokeAction( + nsAString& aValue) const { + GetInvokeAction()->ToString(aValue); +} + +nsAtom* nsGenericHTMLFormControlElementWithState::GetInvokeAction() const { + const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::invokeaction); + if (attr && attr->GetAtomValue() != nsGkAtoms::_empty) { + return attr->GetAtomValue(); + } + return nsGkAtoms::_auto; +} + +mozilla::dom::Element* +nsGenericHTMLFormControlElementWithState::GetInvokeTargetElement() const { + if (StaticPrefs::dom_element_invokers_enabled()) { + return GetAttrAssociatedElement(nsGkAtoms::invoketarget); + } + return nullptr; +} + +void nsGenericHTMLFormControlElementWithState::SetInvokeTargetElement( + mozilla::dom::Element* aElement) { + ExplicitlySetAttrElement(nsGkAtoms::invoketarget, aElement); +} + +void nsGenericHTMLFormControlElementWithState::HandleInvokeTargetAction() { + // 1. Let invokee be node's invoke target element. + RefPtr<Element> invokee = GetInvokeTargetElement(); + + // 2. If invokee is null, then return. + if (!invokee) { + return; + } + + // 3. Let action be node's invokeaction attribute + // 4. If action is null or empty, then let action be the string "auto". + RefPtr<nsAtom> aAction = GetInvokeAction(); + MOZ_ASSERT(!aAction->IsEmpty(), "Action should not be empty"); + + // 5. Let notCancelled be the result of firing an event named invoke at + // invokee with its action set to action, its invoker set to node, + // and its cancelable attribute initialized to true. + InvokeEventInit init; + aAction->ToString(init.mAction); + init.mInvoker = this; + init.mCancelable = true; + init.mComposed = true; + RefPtr<Event> event = InvokeEvent::Constructor(this, u"invoke"_ns, init); + event->SetTrusted(true); + event->SetTarget(invokee); + + EventDispatcher::DispatchDOMEvent(invokee, nullptr, event, nullptr, nullptr); + + // 6. If notCancelled is true and invokee has an associated invocation action + // algorithm then run the invokee's invocation action algorithm given action. + if (event->DefaultPrevented()) { + return; + } + + invokee->HandleInvokeInternal(aAction, IgnoreErrors()); +} + +void nsGenericHTMLFormControlElementWithState::GenerateStateKey() { + // Keep the key if already computed + if (!mStateKey.IsVoid()) { + return; + } + + Document* doc = GetUncomposedDoc(); + if (!doc) { + mStateKey.Truncate(); + return; + } + + // Generate the state key + nsContentUtils::GenerateStateKey(this, doc, mStateKey); + + // If the state key is blank, this is anonymous content or for whatever + // reason we are not supposed to save/restore state: keep it as such. + if (!mStateKey.IsEmpty()) { + // Add something unique to content so layout doesn't muck us up. + mStateKey += "-C"; + } +} + +PresState* nsGenericHTMLFormControlElementWithState::GetPrimaryPresState() { + if (mStateKey.IsEmpty()) { + return nullptr; + } + + nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(false); + + if (!history) { + return nullptr; + } + + // Get the pres state for this key, if it doesn't exist, create one. + PresState* result = history->GetState(mStateKey); + if (!result) { + UniquePtr<PresState> newState = NewPresState(); + result = newState.get(); + history->AddState(mStateKey, std::move(newState)); + } + + return result; +} + +already_AddRefed<nsILayoutHistoryState> +nsGenericHTMLFormElement::GetLayoutHistory(bool aRead) { + nsCOMPtr<Document> doc = GetUncomposedDoc(); + if (!doc) { + return nullptr; + } + + // + // Get the history + // + nsCOMPtr<nsILayoutHistoryState> history = doc->GetLayoutHistoryState(); + if (!history) { + return nullptr; + } + + if (aRead && !history->HasStates()) { + return nullptr; + } + + return history.forget(); +} + +bool nsGenericHTMLFormControlElementWithState::RestoreFormControlState() { + MOZ_ASSERT(!mStateKey.IsVoid(), + "GenerateStateKey must already have been called"); + + if (mStateKey.IsEmpty()) { + return false; + } + + nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(true); + if (!history) { + return false; + } + + // Get the pres state for this key + PresState* state = history->GetState(mStateKey); + if (state) { + bool result = RestoreState(state); + history->RemoveState(mStateKey); + return result; + } + + return false; +} + +void nsGenericHTMLFormControlElementWithState::NodeInfoChanged( + Document* aOldDoc) { + nsGenericHTMLFormControlElement::NodeInfoChanged(aOldDoc); + + // We need to regenerate the state key now we're in a new document. Clearing + // mControlNumber means we stop considering this control to be parser + // inserted, and we'll generate a state key based on its position in the + // document rather than the order it was inserted into the document. + mControlNumber = -1; + mStateKey.SetIsVoid(true); +} + +void nsGenericHTMLFormControlElementWithState::GetFormAction(nsString& aValue) { + auto type = ControlType(); + if (!IsInputElement(type) && !IsButtonElement(type)) { + return; + } + + if (!GetAttr(nsGkAtoms::formaction, aValue) || aValue.IsEmpty()) { + Document* document = OwnerDoc(); + nsIURI* docURI = document->GetDocumentURI(); + if (docURI) { + nsAutoCString spec; + nsresult rv = docURI->GetSpec(spec); + if (NS_FAILED(rv)) { + return; + } + + CopyUTF8toUTF16(spec, aValue); + } + } else { + GetURIAttr(nsGkAtoms::formaction, nullptr, aValue); + } +} + +bool nsGenericHTMLElement::IsEventAttributeNameInternal(nsAtom* aName) { + return nsContentUtils::IsEventAttributeName(aName, EventNameType_HTML); +} + +/** + * Construct a URI from a string, as an element.src attribute + * would be set to. Helper for the media elements. + */ +nsresult nsGenericHTMLElement::NewURIFromString(const nsAString& aURISpec, + nsIURI** aURI) { + NS_ENSURE_ARG_POINTER(aURI); + + *aURI = nullptr; + + nsCOMPtr<Document> doc = OwnerDoc(); + + nsresult rv = nsContentUtils::NewURIWithDocumentCharset(aURI, aURISpec, doc, + GetBaseURI()); + NS_ENSURE_SUCCESS(rv, rv); + + bool equal; + if (aURISpec.IsEmpty() && doc->GetDocumentURI() && + NS_SUCCEEDED(doc->GetDocumentURI()->Equals(*aURI, &equal)) && equal) { + // Assume an element can't point to a fragment of its embedding + // document. Fail here instead of returning the recursive URI + // and waiting for the subsequent load to fail. + NS_RELEASE(*aURI); + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + return NS_OK; +} + +void nsGenericHTMLElement::GetInnerText(mozilla::dom::DOMString& aValue, + mozilla::ErrorResult& aError) { + // innerText depends on layout. For example, white space processing is + // something that happens during reflow and which must be reflected by + // innerText. So for: + // + // <div style="white-space:normal"> A B C </div> + // + // innerText should give "A B C". + // + // The approach taken here to avoid the expense of reflow is to flush style + // and then see whether it's necessary to flush layout afterwards. Flushing + // layout can be skipped if we can detect that the element or its descendants + // are not dirty. + + // Obtain the composed doc to handle elements in Shadow DOM. + Document* doc = GetComposedDoc(); + if (doc) { + doc->FlushPendingNotifications(FlushType::Style); + } + + // Elements with `display: content` will not have a frame. To handle Shadow + // DOM, walk the flattened tree looking for parent frame. + nsIFrame* frame = GetPrimaryFrame(); + if (IsDisplayContents()) { + for (Element* parent = GetFlattenedTreeParentElement(); parent; + parent = parent->GetFlattenedTreeParentElement()) { + frame = parent->GetPrimaryFrame(); + if (frame) { + break; + } + } + } + + // Check for dirty reflow roots in the subtree from targetFrame; this requires + // a reflow flush. + bool dirty = frame && frame->PresShell()->FrameIsAncestorOfDirtyRoot(frame); + + // The way we do that is by checking whether the element has either of the two + // dirty bits (NS_FRAME_IS_DIRTY or NS_FRAME_HAS_DIRTY_DESCENDANTS) or if any + // ancestor has NS_FRAME_IS_DIRTY. We need to check for NS_FRAME_IS_DIRTY on + // ancestors since that is something that implies NS_FRAME_IS_DIRTY on all + // descendants. + dirty |= frame && frame->HasAnyStateBits(NS_FRAME_HAS_DIRTY_CHILDREN); + while (!dirty && frame) { + dirty |= frame->HasAnyStateBits(NS_FRAME_IS_DIRTY); + frame = frame->GetInFlowParent(); + } + + // Flush layout if we determined a reflow is required. + if (dirty && doc) { + doc->FlushPendingNotifications(FlushType::Layout); + } + + if (!IsRendered()) { + GetTextContentInternal(aValue, aError); + } else { + nsRange::GetInnerTextNoFlush(aValue, aError, this); + } +} + +static already_AddRefed<nsINode> TextToNode(const nsAString& aString, + nsNodeInfoManager* aNim) { + nsString str; + const char16_t* s = aString.BeginReading(); + const char16_t* end = aString.EndReading(); + RefPtr<DocumentFragment> fragment; + while (true) { + if (s != end && *s == '\r' && s + 1 != end && s[1] == '\n') { + // a \r\n pair should only generate one <br>, so just skip the \r + ++s; + } + if (s == end || *s == '\r' || *s == '\n') { + if (!str.IsEmpty()) { + RefPtr<nsTextNode> textContent = new (aNim) nsTextNode(aNim); + textContent->SetText(str, true); + if (!fragment) { + if (s == end) { + return textContent.forget(); + } + fragment = new (aNim) DocumentFragment(aNim); + } + fragment->AppendChildTo(textContent, true, IgnoreErrors()); + } + if (s == end) { + break; + } + str.Truncate(); + RefPtr<NodeInfo> ni = aNim->GetNodeInfo( + nsGkAtoms::br, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + auto* nim = ni->NodeInfoManager(); + RefPtr<HTMLBRElement> br = new (nim) HTMLBRElement(ni.forget()); + if (!fragment) { + if (s + 1 == end) { + return br.forget(); + } + fragment = new (aNim) DocumentFragment(aNim); + } + fragment->AppendChildTo(br, true, IgnoreErrors()); + } else { + str.Append(*s); + } + ++s; + } + return fragment.forget(); +} + +void nsGenericHTMLElement::SetInnerText(const nsAString& aValue) { + RefPtr<nsINode> node = TextToNode(aValue, NodeInfo()->NodeInfoManager()); + ReplaceChildren(node, IgnoreErrors()); +} + +// https://html.spec.whatwg.org/#merge-with-the-next-text-node +static void MergeWithNextTextNode(Text& aText, ErrorResult& aRv) { + RefPtr<Text> nextSibling = Text::FromNodeOrNull(aText.GetNextSibling()); + if (!nextSibling) { + return; + } + nsAutoString data; + nextSibling->GetData(data); + aText.AppendData(data, aRv); + nextSibling->Remove(); +} + +// https://html.spec.whatwg.org/#dom-outertext +void nsGenericHTMLElement::SetOuterText(const nsAString& aValue, + ErrorResult& aRv) { + nsCOMPtr<nsINode> parent = GetParentNode(); + if (!parent) { + return aRv.ThrowNoModificationAllowedError("Element has no parent"); + } + + RefPtr<nsINode> next = GetNextSibling(); + RefPtr<nsINode> previous = GetPreviousSibling(); + + // Batch possible DOMSubtreeModified events. + mozAutoSubtreeModified subtree(OwnerDoc(), nullptr); + + nsNodeInfoManager* nim = NodeInfo()->NodeInfoManager(); + RefPtr<nsINode> node = TextToNode(aValue, nim); + if (!node) { + // This doesn't match the spec, see + // https://github.com/whatwg/html/issues/7508 + node = new (nim) nsTextNode(nim); + } + parent->ReplaceChild(*node, *this, aRv); + if (aRv.Failed()) { + return; + } + + if (next) { + if (RefPtr<Text> text = Text::FromNodeOrNull(next->GetPreviousSibling())) { + MergeWithNextTextNode(*text, aRv); + if (aRv.Failed()) { + return; + } + } + } + if (auto* text = Text::FromNodeOrNull(previous)) { + MergeWithNextTextNode(*text, aRv); + } +} + +// This should be true when `:open` should match. +bool nsGenericHTMLElement::PopoverOpen() const { + if (PopoverData* popoverData = GetPopoverData()) { + return popoverData->GetPopoverVisibilityState() == + PopoverVisibilityState::Showing; + } + return false; +} + +// https://html.spec.whatwg.org/#check-popover-validity +bool nsGenericHTMLElement::CheckPopoverValidity( + PopoverVisibilityState aExpectedState, Document* aExpectedDocument, + ErrorResult& aRv) { + if (GetPopoverAttributeState() == PopoverAttributeState::None) { + aRv.ThrowNotSupportedError("Element is in the no popover state"); + return false; + } + + if (GetPopoverData()->GetPopoverVisibilityState() != aExpectedState) { + return false; + } + + if (!IsInComposedDoc()) { + aRv.ThrowInvalidStateError("Element is not connected"); + return false; + } + + if (aExpectedDocument && aExpectedDocument != OwnerDoc()) { + aRv.ThrowInvalidStateError("Element is moved to other document"); + return false; + } + + if (auto* dialog = HTMLDialogElement::FromNode(this)) { + if (dialog->IsInTopLayer()) { + aRv.ThrowInvalidStateError("Element is a modal <dialog> element"); + return false; + } + } + + if (State().HasState(ElementState::FULLSCREEN)) { + aRv.ThrowInvalidStateError("Element is fullscreen"); + return false; + } + + return true; +} + +PopoverAttributeState nsGenericHTMLElement::GetPopoverAttributeState() const { + return GetPopoverData() ? GetPopoverData()->GetPopoverAttributeState() + : PopoverAttributeState::None; +} + +void nsGenericHTMLElement::PopoverPseudoStateUpdate(bool aOpen, bool aNotify) { + SetStates(ElementState::POPOVER_OPEN, aOpen, aNotify); +} + +already_AddRefed<ToggleEvent> nsGenericHTMLElement::CreateToggleEvent( + const nsAString& aEventType, const nsAString& aOldState, + const nsAString& aNewState, Cancelable aCancelable) { + ToggleEventInit init; + init.mBubbles = false; + init.mOldState = aOldState; + init.mNewState = aNewState; + init.mCancelable = aCancelable == Cancelable::eYes; + RefPtr<ToggleEvent> event = ToggleEvent::Constructor(this, aEventType, init); + event->SetTrusted(true); + event->SetTarget(this); + return event.forget(); +} + +bool nsGenericHTMLElement::FireToggleEvent(PopoverVisibilityState aOldState, + PopoverVisibilityState aNewState, + const nsAString& aType) { + auto stringForState = [](PopoverVisibilityState state) { + return state == PopoverVisibilityState::Hidden ? u"closed"_ns : u"open"_ns; + }; + const auto cancelable = aType == u"beforetoggle"_ns && + aNewState == PopoverVisibilityState::Showing + ? Cancelable::eYes + : Cancelable::eNo; + RefPtr event = CreateToggleEvent(aType, stringForState(aOldState), + stringForState(aNewState), cancelable); + EventDispatcher::DispatchDOMEvent(this, nullptr, event, nullptr, nullptr); + return event->DefaultPrevented(); +} + +// https://html.spec.whatwg.org/#queue-a-popover-toggle-event-task +void nsGenericHTMLElement::QueuePopoverEventTask( + PopoverVisibilityState aOldState) { + auto* data = GetPopoverData(); + MOZ_ASSERT(data, "Should have popover data"); + + if (auto* queuedToggleEventTask = data->GetToggleEventTask()) { + aOldState = queuedToggleEventTask->GetOldState(); + } + + auto task = + MakeRefPtr<PopoverToggleEventTask>(do_GetWeakReference(this), aOldState); + data->SetToggleEventTask(task); + OwnerDoc()->Dispatch(task.forget()); +} + +void nsGenericHTMLElement::RunPopoverToggleEventTask( + PopoverToggleEventTask* aTask, PopoverVisibilityState aOldState) { + auto* data = GetPopoverData(); + if (!data) { + return; + } + + auto* popoverToggleEventTask = data->GetToggleEventTask(); + if (!popoverToggleEventTask || aTask != popoverToggleEventTask) { + return; + } + data->ClearToggleEventTask(); + // Intentionally ignore the return value here as only on open event the + // cancelable attribute is initialized to true for beforetoggle event. + FireToggleEvent(aOldState, data->GetPopoverVisibilityState(), u"toggle"_ns); +} + +// https://html.spec.whatwg.org/#dom-showpopover +void nsGenericHTMLElement::ShowPopover(ErrorResult& aRv) { + return ShowPopoverInternal(nullptr, aRv); +} +void nsGenericHTMLElement::ShowPopoverInternal(Element* aInvoker, + ErrorResult& aRv) { + if (!CheckPopoverValidity(PopoverVisibilityState::Hidden, nullptr, aRv)) { + return; + } + RefPtr<Document> document = OwnerDoc(); + + MOZ_ASSERT(!GetPopoverData() || !GetPopoverData()->GetInvoker()); + MOZ_ASSERT(!OwnerDoc()->TopLayerContains(*this)); + + bool wasShowingOrHiding = GetPopoverData()->IsShowingOrHiding(); + GetPopoverData()->SetIsShowingOrHiding(true); + auto cleanupShowingFlag = MakeScopeExit([&]() { + if (auto* popoverData = GetPopoverData()) { + popoverData->SetIsShowingOrHiding(wasShowingOrHiding); + } + }); + + // Fire beforetoggle event and re-check popover validity. + if (FireToggleEvent(PopoverVisibilityState::Hidden, + PopoverVisibilityState::Showing, u"beforetoggle"_ns)) { + return; + } + if (!CheckPopoverValidity(PopoverVisibilityState::Hidden, document, aRv)) { + return; + } + + bool shouldRestoreFocus = false; + nsWeakPtr originallyFocusedElement; + if (IsAutoPopover()) { + auto originalState = GetPopoverAttributeState(); + RefPtr<nsINode> ancestor = GetTopmostPopoverAncestor(aInvoker, true); + if (!ancestor) { + ancestor = document; + } + document->HideAllPopoversUntil(*ancestor, false, + /* aFireEvents = */ !wasShowingOrHiding); + if (GetPopoverAttributeState() != originalState) { + aRv.ThrowInvalidStateError( + "The value of the popover attribute was changed while hiding the " + "popover."); + return; + } + + // TODO: Handle if document changes, see + // https://github.com/whatwg/html/issues/9177 + if (!IsAutoPopover() || + !CheckPopoverValidity(PopoverVisibilityState::Hidden, document, aRv)) { + return; + } + + shouldRestoreFocus = !document->GetTopmostAutoPopover(); + // Let originallyFocusedElement be document's focused area of the document's + // DOM anchor. + if (nsIContent* unretargetedFocus = + document->GetUnretargetedFocusedContent()) { + originallyFocusedElement = + do_GetWeakReference(unretargetedFocus->AsElement()); + } + } + + document->AddPopoverToTopLayer(*this); + + PopoverPseudoStateUpdate(true, true); + + { + auto* popoverData = GetPopoverData(); + popoverData->SetPopoverVisibilityState(PopoverVisibilityState::Showing); + popoverData->SetInvoker(aInvoker); + } + + // Run the popover focusing steps given element. + FocusPopover(); + if (shouldRestoreFocus && + GetPopoverAttributeState() != PopoverAttributeState::None) { + GetPopoverData()->SetPreviouslyFocusedElement(originallyFocusedElement); + } + + // Queue popover toggle event task. + QueuePopoverEventTask(PopoverVisibilityState::Hidden); +} + +void nsGenericHTMLElement::HidePopoverWithoutRunningScript() { + HidePopoverInternal(/* aFocusPreviousElement = */ false, + /* aFireEvents = */ false, IgnoreErrors()); +} + +// https://html.spec.whatwg.org/#dom-hidepopover +void nsGenericHTMLElement::HidePopover(ErrorResult& aRv) { + HidePopoverInternal(/* aFocusPreviousElement = */ true, + /* aFireEvents = */ true, aRv); +} + +void nsGenericHTMLElement::HidePopoverInternal(bool aFocusPreviousElement, + bool aFireEvents, + ErrorResult& aRv) { + OwnerDoc()->HidePopover(*this, aFocusPreviousElement, aFireEvents, aRv); +} + +void nsGenericHTMLElement::ForgetPreviouslyFocusedElementAfterHidingPopover() { + auto* data = GetPopoverData(); + MOZ_ASSERT(data, "Should have popover data"); + data->SetPreviouslyFocusedElement(nullptr); +} + +void nsGenericHTMLElement::FocusPreviousElementAfterHidingPopover() { + auto* data = GetPopoverData(); + MOZ_ASSERT(data, "Should have popover data"); + + RefPtr<Element> control = + do_QueryReferent(data->GetPreviouslyFocusedElement().get()); + data->SetPreviouslyFocusedElement(nullptr); + + if (!control) { + return; + } + + // Step 14.2 at + // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm + // If focusPreviousElement is true and document's focused area of the + // document's DOM anchor is a shadow-including inclusive descendant of + // element, then run the focusing steps for previouslyFocusedElement; + nsIContent* currentFocus = OwnerDoc()->GetUnretargetedFocusedContent(); + if (currentFocus && + currentFocus->IsShadowIncludingInclusiveDescendantOf(this)) { + FocusOptions options; + options.mPreventScroll = true; + control->Focus(options, CallerType::NonSystem, IgnoreErrors()); + } +} + +// https://html.spec.whatwg.org/multipage/popover.html#dom-togglepopover +bool nsGenericHTMLElement::TogglePopover(const Optional<bool>& aForce, + ErrorResult& aRv) { + if (PopoverOpen() && (!aForce.WasPassed() || !aForce.Value())) { + HidePopover(aRv); + } else if (!aForce.WasPassed() || aForce.Value()) { + ShowPopover(aRv); + } else { + CheckPopoverValidity(GetPopoverData() + ? GetPopoverData()->GetPopoverVisibilityState() + : PopoverVisibilityState::Showing, + nullptr, aRv); + } + + return PopoverOpen(); +} + +// https://html.spec.whatwg.org/multipage/popover.html#popover-focusing-steps +void nsGenericHTMLElement::FocusPopover() { + if (auto* dialog = HTMLDialogElement::FromNode(this)) { + return MOZ_KnownLive(dialog)->FocusDialog(); + } + + if (RefPtr<Document> doc = GetComposedDoc()) { + doc->FlushPendingNotifications(FlushType::Frames); + } + + RefPtr<Element> control = GetBoolAttr(nsGkAtoms::autofocus) + ? this + : GetAutofocusDelegate(false /* aWithMouse */); + + if (!control) { + return; + } + FocusCandidate(control, false /* aClearUpFocus */); +} + +void nsGenericHTMLElement::FocusCandidate(Element* aControl, + bool aClearUpFocus) { + // 1) Run the focusing steps given control. + IgnoredErrorResult rv; + if (RefPtr<Element> elementToFocus = nsFocusManager::GetTheFocusableArea( + aControl, nsFocusManager::ProgrammaticFocusFlags(FocusOptions()))) { + elementToFocus->Focus(FocusOptions(), CallerType::NonSystem, rv); + if (rv.Failed()) { + return; + } + } else if (aClearUpFocus) { + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + // Clear the focus which ends up making the body gets focused + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = OwnerDoc()->GetWindow(); + fm->ClearFocus(outerWindow); + } + } + + // 2) Let topDocument be the active document of control's node document's + // browsing context's top-level browsing context. + // 3) If control's node document's origin is not the same as the origin of + // topDocument, then return. + BrowsingContext* bc = aControl->OwnerDoc()->GetBrowsingContext(); + if (bc && bc->IsInProcess() && bc->SameOriginWithTop()) { + if (nsCOMPtr<nsIDocShell> docShell = bc->Top()->GetDocShell()) { + if (Document* topDocument = docShell->GetExtantDocument()) { + // 4) Empty topDocument's autofocus candidates. + // 5) Set topDocument's autofocus processed flag to true. + topDocument->SetAutoFocusFired(); + } + } + } +} + +already_AddRefed<ElementInternals> nsGenericHTMLElement::AttachInternals( + ErrorResult& aRv) { + // ElementInternals is only available on autonomous custom element, so throws + // an error by default. The spec steps are implemented in HTMLElement because + // ElementInternals needs to hold a pointer to HTMLElement in order to forward + // form operation to it. + aRv.ThrowNotSupportedError(nsPrintfCString( + "Cannot attach ElementInternals to a customized built-in or non-custom " + "element " + "'%s'", + NS_ConvertUTF16toUTF8(NodeInfo()->NameAtom()->GetUTF16String()).get())); + return nullptr; +} + +ElementInternals* nsGenericHTMLElement::GetInternals() const { + if (CustomElementData* data = GetCustomElementData()) { + return data->GetElementInternals(); + } + return nullptr; +} + +bool nsGenericHTMLElement::IsFormAssociatedCustomElements() const { + if (CustomElementData* data = GetCustomElementData()) { + return data->IsFormAssociated(); + } + return false; +} + +void nsGenericHTMLElement::GetAutocapitalize(nsAString& aValue) const { + GetEnumAttr(nsGkAtoms::autocapitalize, nullptr, kDefaultAutocapitalize->tag, + aValue); +} + +bool nsGenericHTMLElement::Translate() const { + if (const nsAttrValue* attr = mAttrs.GetAttr(nsGkAtoms::translate)) { + if (attr->IsEmptyString() || attr->Equals(nsGkAtoms::yes, eIgnoreCase)) { + return true; + } + if (attr->Equals(nsGkAtoms::no, eIgnoreCase)) { + return false; + } + } + return nsGenericHTMLElementBase::Translate(); +} + +void nsGenericHTMLElement::GetPopover(nsString& aPopover) const { + GetHTMLEnumAttr(nsGkAtoms::popover, aPopover); + if (aPopover.IsEmpty() && !DOMStringIsNull(aPopover)) { + aPopover.Assign(NS_ConvertUTF8toUTF16(kPopoverAttributeValueAuto)); + } +} |