diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/html/HTMLMenuItemElement.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/html/HTMLMenuItemElement.cpp')
-rw-r--r-- | dom/html/HTMLMenuItemElement.cpp | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/dom/html/HTMLMenuItemElement.cpp b/dom/html/HTMLMenuItemElement.cpp new file mode 100644 index 0000000000..2787b1a3f0 --- /dev/null +++ b/dom/html/HTMLMenuItemElement.cpp @@ -0,0 +1,443 @@ +/* -*- 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/dom/HTMLMenuItemElement.h" + +#include "mozilla/BasicEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/HTMLMenuItemElementBinding.h" +#include "mozilla/dom/HTMLUnknownElement.h" +#include "nsAttrValueInlines.h" +#include "nsContentUtils.h" + +nsGenericHTMLElement* NS_NewHTMLMenuItemElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + MOZ_ASSERT(nim); + if (mozilla::StaticPrefs::dom_menuitem_enabled()) { + return new (nim) + mozilla::dom::HTMLMenuItemElement(nodeInfo.forget(), aFromParser); + } + return new (nim) mozilla::dom::HTMLUnknownElement(nodeInfo.forget()); +} + +namespace mozilla::dom { + +// First bits are needed for the menuitem type. +#define NS_CHECKED_IS_TOGGLED (1 << 2) +#define NS_ORIGINAL_CHECKED_VALUE (1 << 3) +#define NS_MENUITEM_TYPE(bits) \ + ((bits) & ~(NS_CHECKED_IS_TOGGLED | NS_ORIGINAL_CHECKED_VALUE)) + +enum CmdType : uint8_t { + CMD_TYPE_MENUITEM = 1, + CMD_TYPE_CHECKBOX, + CMD_TYPE_RADIO +}; + +static const nsAttrValue::EnumTable kMenuItemTypeTable[] = { + {"menuitem", CMD_TYPE_MENUITEM}, + {"checkbox", CMD_TYPE_CHECKBOX}, + {"radio", CMD_TYPE_RADIO}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable* kMenuItemDefaultType = + &kMenuItemTypeTable[0]; + +// A base class inherited by all radio visitors. +class Visitor { + public: + Visitor() = default; + virtual ~Visitor() = default; + + /** + * Visit a node in the tree. This is meant to be called on all radios in a + * group, sequentially. If the method returns false then the iteration is + * stopped. + */ + virtual bool Visit(HTMLMenuItemElement* aMenuItem) = 0; +}; + +// Find the selected radio, see GetSelectedRadio(). +class GetCheckedVisitor : public Visitor { + public: + explicit GetCheckedVisitor(HTMLMenuItemElement** aResult) + : mResult(aResult) {} + virtual bool Visit(HTMLMenuItemElement* aMenuItem) override { + if (aMenuItem->IsChecked()) { + *mResult = aMenuItem; + return false; + } + return true; + } + + protected: + HTMLMenuItemElement** mResult; +}; + +// Deselect all radios except the one passed to the constructor. +class ClearCheckedVisitor : public Visitor { + public: + explicit ClearCheckedVisitor(HTMLMenuItemElement* aExcludeMenuItem) + : mExcludeMenuItem(aExcludeMenuItem) {} + virtual bool Visit(HTMLMenuItemElement* aMenuItem) override { + if (aMenuItem != mExcludeMenuItem && aMenuItem->IsChecked()) { + aMenuItem->ClearChecked(); + } + return true; + } + + protected: + HTMLMenuItemElement* mExcludeMenuItem; +}; + +// Get current value of the checked dirty flag. The same value is stored on all +// radios in the group, so we need to check only the first one. +class GetCheckedDirtyVisitor : public Visitor { + public: + GetCheckedDirtyVisitor(bool* aCheckedDirty, + HTMLMenuItemElement* aExcludeMenuItem) + : mCheckedDirty(aCheckedDirty), mExcludeMenuItem(aExcludeMenuItem) {} + virtual bool Visit(HTMLMenuItemElement* aMenuItem) override { + if (aMenuItem == mExcludeMenuItem) { + return true; + } + *mCheckedDirty = aMenuItem->IsCheckedDirty(); + return false; + } + + protected: + bool* mCheckedDirty; + HTMLMenuItemElement* mExcludeMenuItem; +}; + +// Set checked dirty to true on all radios in the group. +class SetCheckedDirtyVisitor : public Visitor { + public: + SetCheckedDirtyVisitor() = default; + virtual bool Visit(HTMLMenuItemElement* aMenuItem) override { + aMenuItem->SetCheckedDirty(); + return true; + } +}; + +// A helper visitor that is used to combine two operations (visitors) to avoid +// iterating over radios twice. +class CombinedVisitor : public Visitor { + public: + CombinedVisitor(Visitor* aVisitor1, Visitor* aVisitor2) + : mVisitor1(aVisitor1), + mVisitor2(aVisitor2), + mContinue1(true), + mContinue2(true) {} + virtual bool Visit(HTMLMenuItemElement* aMenuItem) override { + if (mContinue1) { + mContinue1 = mVisitor1->Visit(aMenuItem); + } + if (mContinue2) { + mContinue2 = mVisitor2->Visit(aMenuItem); + } + return mContinue1 || mContinue2; + } + + protected: + Visitor* mVisitor1; + Visitor* mVisitor2; + bool mContinue1; + bool mContinue2; +}; + +HTMLMenuItemElement::HTMLMenuItemElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mType(kMenuItemDefaultType->value), + mParserCreating(false), + mShouldInitChecked(false), + mCheckedDirty(false), + mChecked(false) { + mParserCreating = aFromParser; +} + +HTMLMenuItemElement::~HTMLMenuItemElement() = default; + +// NS_IMPL_ELEMENT_CLONE(HTMLMenuItemElement) + +nsresult HTMLMenuItemElement::Clone(dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + *aResult = nullptr; + RefPtr<HTMLMenuItemElement> it = new (aNodeInfo->NodeInfoManager()) + HTMLMenuItemElement(do_AddRef(aNodeInfo), NOT_FROM_PARSER); + nsresult rv = const_cast<HTMLMenuItemElement*>(this)->CopyInnerTo(it); + if (NS_SUCCEEDED(rv)) { + switch (mType) { + case CMD_TYPE_CHECKBOX: + case CMD_TYPE_RADIO: + if (mCheckedDirty) { + // We no longer have our original checked state. Set our + // checked state on the clone. + it->mCheckedDirty = true; + it->mChecked = mChecked; + } + break; + } + + it.forget(aResult); + } + + return rv; +} + +void HTMLMenuItemElement::GetType(DOMString& aValue) { + GetEnumAttr(nsGkAtoms::type, kMenuItemDefaultType->tag, aValue); +} + +void HTMLMenuItemElement::SetChecked(bool aChecked) { + bool checkedChanged = mChecked != aChecked; + + mChecked = aChecked; + + if (mType == CMD_TYPE_RADIO) { + if (checkedChanged) { + if (mCheckedDirty) { + ClearCheckedVisitor visitor(this); + WalkRadioGroup(&visitor); + } else { + ClearCheckedVisitor visitor1(this); + SetCheckedDirtyVisitor visitor2; + CombinedVisitor visitor(&visitor1, &visitor2); + WalkRadioGroup(&visitor); + } + } else if (!mCheckedDirty) { + SetCheckedDirtyVisitor visitor; + WalkRadioGroup(&visitor); + } + } else { + mCheckedDirty = true; + } +} + +void HTMLMenuItemElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + if (aVisitor.mEvent->mMessage == eMouseClick) { + bool originalCheckedValue = false; + switch (mType) { + case CMD_TYPE_CHECKBOX: + originalCheckedValue = mChecked; + SetChecked(!originalCheckedValue); + aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED; + break; + case CMD_TYPE_RADIO: + // casting back to Element* here to resolve nsISupports ambiguity. + Element* supports = GetSelectedRadio(); + aVisitor.mItemData = supports; + + originalCheckedValue = mChecked; + if (!originalCheckedValue) { + SetChecked(true); + aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED; + } + break; + } + + if (originalCheckedValue) { + aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE; + } + + // We must cache type because mType may change during JS event. + aVisitor.mItemFlags |= mType; + } + + nsGenericHTMLElement::GetEventTargetParent(aVisitor); +} + +nsresult HTMLMenuItemElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + // Check to see if the event was cancelled. + if (aVisitor.mEvent->mMessage == eMouseClick && + aVisitor.mItemFlags & NS_CHECKED_IS_TOGGLED && + aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) { + bool originalCheckedValue = + !!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE); + uint8_t oldType = NS_MENUITEM_TYPE(aVisitor.mItemFlags); + + nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mItemData)); + RefPtr<HTMLMenuItemElement> selectedRadio = + HTMLMenuItemElement::FromNodeOrNull(content); + if (selectedRadio) { + selectedRadio->SetChecked(true); + if (mType != CMD_TYPE_RADIO) { + SetChecked(false); + } + } else if (oldType == CMD_TYPE_CHECKBOX) { + SetChecked(originalCheckedValue); + } + } + + return NS_OK; +} + +nsresult HTMLMenuItemElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInUncomposedDoc() && mType == CMD_TYPE_RADIO) { + AddedToRadioGroup(); + } + + return rv; +} + +bool HTMLMenuItemElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::type) { + return aResult.ParseEnumValue(aValue, kMenuItemTypeTable, false, + kMenuItemDefaultType); + } + + if (aAttribute == nsGkAtoms::radiogroup) { + aResult.ParseAtom(aValue); + return true; + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLMenuItemElement::DoneCreatingElement() { + mParserCreating = false; + + if (mShouldInitChecked) { + InitChecked(); + mShouldInitChecked = false; + } +} + +void HTMLMenuItemElement::GetText(nsAString& aText) { + nsAutoString text; + nsContentUtils::GetNodeTextContent(this, false, text); + + text.CompressWhitespace(true, true); + aText = text; +} + +nsresult HTMLMenuItemElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + // Handle type changes first, since some of the later conditions in this + // method look at mType and want to see the new value. + if (aName == nsGkAtoms::type) { + if (aValue) { + mType = aValue->GetEnumValue(); + } else { + mType = kMenuItemDefaultType->value; + } + } + + if ((aName == nsGkAtoms::radiogroup || aName == nsGkAtoms::type) && + mType == CMD_TYPE_RADIO && !mParserCreating) { + if (IsInUncomposedDoc() && GetParent()) { + AddedToRadioGroup(); + } + } + + // Checked must be set no matter what type of menuitem it is, since + // GetChecked() must reflect the new value + if (aName == nsGkAtoms::checked && !mCheckedDirty) { + if (mParserCreating) { + mShouldInitChecked = true; + } else { + InitChecked(); + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLMenuItemElement::WalkRadioGroup(Visitor* aVisitor) { + nsIContent* parent = GetParent(); + if (!parent) { + aVisitor->Visit(this); + return; + } + + BorrowedAttrInfo info1(GetAttrInfo(kNameSpaceID_None, nsGkAtoms::radiogroup)); + bool info1Empty = !info1.mValue || info1.mValue->IsEmptyString(); + + for (nsIContent* cur = parent->GetFirstChild(); cur; + cur = cur->GetNextSibling()) { + HTMLMenuItemElement* menuitem = HTMLMenuItemElement::FromNode(cur); + + if (!menuitem || menuitem->GetType() != CMD_TYPE_RADIO) { + continue; + } + + BorrowedAttrInfo info2( + menuitem->GetAttrInfo(kNameSpaceID_None, nsGkAtoms::radiogroup)); + bool info2Empty = !info2.mValue || info2.mValue->IsEmptyString(); + + if (info1Empty != info2Empty || (info1.mValue && info2.mValue && + !info1.mValue->Equals(*info2.mValue))) { + continue; + } + + if (!aVisitor->Visit(menuitem)) { + break; + } + } +} + +HTMLMenuItemElement* HTMLMenuItemElement::GetSelectedRadio() { + HTMLMenuItemElement* result = nullptr; + + GetCheckedVisitor visitor(&result); + WalkRadioGroup(&visitor); + + return result; +} + +void HTMLMenuItemElement::AddedToRadioGroup() { + bool checkedDirty = mCheckedDirty; + if (mChecked) { + ClearCheckedVisitor visitor1(this); + GetCheckedDirtyVisitor visitor2(&checkedDirty, this); + CombinedVisitor visitor(&visitor1, &visitor2); + WalkRadioGroup(&visitor); + } else { + GetCheckedDirtyVisitor visitor(&checkedDirty, this); + WalkRadioGroup(&visitor); + } + mCheckedDirty = checkedDirty; +} + +void HTMLMenuItemElement::InitChecked() { + bool defaultChecked = DefaultChecked(); + mChecked = defaultChecked; + if (mType == CMD_TYPE_RADIO) { + ClearCheckedVisitor visitor(this); + WalkRadioGroup(&visitor); + } +} + +JSObject* HTMLMenuItemElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLMenuItemElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom + +#undef NS_ORIGINAL_CHECKED_VALUE |