summaryrefslogtreecommitdiffstats
path: root/dom/html/HTMLMenuItemElement.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/html/HTMLMenuItemElement.cpp
parentInitial commit. (diff)
downloadfirefox-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.cpp443
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