diff options
Diffstat (limited to 'dom/html/HTMLMenuItemElement.cpp')
-rw-r--r-- | dom/html/HTMLMenuItemElement.cpp | 495 |
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 |