summaryrefslogtreecommitdiff
path: root/dom/html/HTMLMenuItemElement.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/html/HTMLMenuItemElement.cpp')
-rw-r--r--dom/html/HTMLMenuItemElement.cpp495
1 files changed, 495 insertions, 0 deletions
diff --git a/dom/html/HTMLMenuItemElement.cpp b/dom/html/HTMLMenuItemElement.cpp
new file mode 100644
index 0000000000..ca8bb4feb5
--- /dev/null
+++ b/dom/html/HTMLMenuItemElement.cpp
@@ -0,0 +1,495 @@
+/* -*- 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 "nsAttrValueInlines.h"
+#include "nsContentUtils.h"
+
+
+NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(MenuItem)
+
+namespace mozilla {
+namespace 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() { }
+ virtual ~Visitor() { }
+
+ /**
+ * 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)
+ {
+ 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)
+ {
+ 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)
+ {
+ 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()
+ { }
+ virtual bool Visit(HTMLMenuItemElement* aMenuItem)
+ {
+ 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)
+ {
+ 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(aNodeInfo),
+ mType(kMenuItemDefaultType->value),
+ mParserCreating(false),
+ mShouldInitChecked(false),
+ mCheckedDirty(false),
+ mChecked(false)
+{
+ mParserCreating = aFromParser;
+}
+
+HTMLMenuItemElement::~HTMLMenuItemElement()
+{
+}
+
+
+NS_IMPL_ISUPPORTS_INHERITED(HTMLMenuItemElement, nsGenericHTMLElement,
+ nsIDOMHTMLMenuItemElement)
+
+//NS_IMPL_ELEMENT_CLONE(HTMLMenuItemElement)
+nsresult
+HTMLMenuItemElement::Clone(mozilla::dom::NodeInfo *aNodeInfo, nsINode **aResult) const
+{
+ *aResult = nullptr;
+ already_AddRefed<mozilla::dom::NodeInfo> ni = RefPtr<mozilla::dom::NodeInfo>(aNodeInfo).forget();
+ RefPtr<HTMLMenuItemElement> it =
+ new HTMLMenuItemElement(ni, 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;
+}
+
+
+NS_IMPL_ENUM_ATTR_DEFAULT_VALUE(HTMLMenuItemElement, Type, type,
+ kMenuItemDefaultType->tag)
+// GetText returns a whitespace compressed .textContent value.
+NS_IMPL_STRING_ATTR_WITH_FALLBACK(HTMLMenuItemElement, Label, label, GetText)
+NS_IMPL_URI_ATTR(HTMLMenuItemElement, Icon, icon)
+NS_IMPL_BOOL_ATTR(HTMLMenuItemElement, Disabled, disabled)
+NS_IMPL_BOOL_ATTR(HTMLMenuItemElement, DefaultChecked, checked)
+//NS_IMPL_BOOL_ATTR(HTMLMenuItemElement, Checked, checked)
+NS_IMPL_STRING_ATTR(HTMLMenuItemElement, Radiogroup, radiogroup)
+
+NS_IMETHODIMP
+HTMLMenuItemElement::GetChecked(bool* aChecked)
+{
+ *aChecked = mChecked;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+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;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+HTMLMenuItemElement::PreHandleEvent(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:
+ nsCOMPtr<nsIDOMHTMLMenuItemElement> selectedRadio = GetSelectedRadio();
+ aVisitor.mItemData = selectedRadio;
+
+ 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;
+ }
+
+ return nsGenericHTMLElement::PreHandleEvent(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<nsIDOMHTMLMenuItemElement> selectedRadio =
+ do_QueryInterface(aVisitor.mItemData);
+ 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(nsIDocument* aDocument, nsIContent* aParent,
+ nsIContent* aBindingParent,
+ bool aCompileEventHandlers)
+{
+ nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent,
+ aBindingParent,
+ aCompileEventHandlers);
+
+ if (NS_SUCCEEDED(rv) && aDocument && mType == CMD_TYPE_RADIO) {
+ AddedToRadioGroup();
+ }
+
+ return rv;
+}
+
+bool
+HTMLMenuItemElement::ParseAttribute(int32_t aNamespaceID,
+ nsIAtom* aAttribute,
+ const nsAString& aValue,
+ nsAttrValue& aResult)
+{
+ if (aNamespaceID == kNameSpaceID_None) {
+ if (aAttribute == nsGkAtoms::type) {
+ bool success = aResult.ParseEnumValue(aValue, kMenuItemTypeTable,
+ false);
+ if (success) {
+ mType = aResult.GetEnumValue();
+ } else {
+ mType = kMenuItemDefaultType->value;
+ }
+
+ return success;
+ }
+
+ if (aAttribute == nsGkAtoms::radiogroup) {
+ aResult.ParseAtom(aValue);
+ return true;
+ }
+ }
+
+ return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
+ 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, nsIAtom* aName,
+ const nsAttrValue* aValue, bool aNotify)
+{
+ if (aNameSpaceID == kNameSpaceID_None) {
+ 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,
+ 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::FromContent(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;
+ GetDefaultChecked(&defaultChecked);
+ mChecked = defaultChecked;
+ if (mType == CMD_TYPE_RADIO) {
+ ClearCheckedVisitor visitor(this);
+ WalkRadioGroup(&visitor);
+ }
+}
+
+JSObject*
+HTMLMenuItemElement::WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return HTMLMenuItemElementBinding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace dom
+} // namespace mozilla
+
+#undef NS_ORIGINAL_CHECKED_VALUE