Bug 1560374 - Set testharness and reftest web-platform-tests to Tier-1; r=jmaher...
[gecko.git] / dom / html / HTMLMenuItemElement.cpp
blobf48e5735c55ce6624c97dd8d4d3932a2e24c9f86
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/dom/HTMLMenuItemElement.h"
9 #include "mozilla/BasicEvents.h"
10 #include "mozilla/EventDispatcher.h"
11 #include "mozilla/dom/HTMLMenuItemElementBinding.h"
12 #include "nsAttrValueInlines.h"
13 #include "nsContentUtils.h"
15 NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(MenuItem)
17 namespace mozilla {
18 namespace dom {
20 // First bits are needed for the menuitem type.
21 #define NS_CHECKED_IS_TOGGLED (1 << 2)
22 #define NS_ORIGINAL_CHECKED_VALUE (1 << 3)
23 #define NS_MENUITEM_TYPE(bits) \
24 ((bits) & ~(NS_CHECKED_IS_TOGGLED | NS_ORIGINAL_CHECKED_VALUE))
26 enum CmdType : uint8_t {
27 CMD_TYPE_MENUITEM = 1,
28 CMD_TYPE_CHECKBOX,
29 CMD_TYPE_RADIO
32 static const nsAttrValue::EnumTable kMenuItemTypeTable[] = {
33 {"menuitem", CMD_TYPE_MENUITEM},
34 {"checkbox", CMD_TYPE_CHECKBOX},
35 {"radio", CMD_TYPE_RADIO},
36 {nullptr, 0}};
38 static const nsAttrValue::EnumTable* kMenuItemDefaultType =
39 &kMenuItemTypeTable[0];
41 // A base class inherited by all radio visitors.
42 class Visitor {
43 public:
44 Visitor() {}
45 virtual ~Visitor() {}
47 /**
48 * Visit a node in the tree. This is meant to be called on all radios in a
49 * group, sequentially. If the method returns false then the iteration is
50 * stopped.
52 virtual bool Visit(HTMLMenuItemElement* aMenuItem) = 0;
55 // Find the selected radio, see GetSelectedRadio().
56 class GetCheckedVisitor : public Visitor {
57 public:
58 explicit GetCheckedVisitor(HTMLMenuItemElement** aResult)
59 : mResult(aResult) {}
60 virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
61 if (aMenuItem->IsChecked()) {
62 *mResult = aMenuItem;
63 return false;
65 return true;
68 protected:
69 HTMLMenuItemElement** mResult;
72 // Deselect all radios except the one passed to the constructor.
73 class ClearCheckedVisitor : public Visitor {
74 public:
75 explicit ClearCheckedVisitor(HTMLMenuItemElement* aExcludeMenuItem)
76 : mExcludeMenuItem(aExcludeMenuItem) {}
77 virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
78 if (aMenuItem != mExcludeMenuItem && aMenuItem->IsChecked()) {
79 aMenuItem->ClearChecked();
81 return true;
84 protected:
85 HTMLMenuItemElement* mExcludeMenuItem;
88 // Get current value of the checked dirty flag. The same value is stored on all
89 // radios in the group, so we need to check only the first one.
90 class GetCheckedDirtyVisitor : public Visitor {
91 public:
92 GetCheckedDirtyVisitor(bool* aCheckedDirty,
93 HTMLMenuItemElement* aExcludeMenuItem)
94 : mCheckedDirty(aCheckedDirty), mExcludeMenuItem(aExcludeMenuItem) {}
95 virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
96 if (aMenuItem == mExcludeMenuItem) {
97 return true;
99 *mCheckedDirty = aMenuItem->IsCheckedDirty();
100 return false;
103 protected:
104 bool* mCheckedDirty;
105 HTMLMenuItemElement* mExcludeMenuItem;
108 // Set checked dirty to true on all radios in the group.
109 class SetCheckedDirtyVisitor : public Visitor {
110 public:
111 SetCheckedDirtyVisitor() {}
112 virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
113 aMenuItem->SetCheckedDirty();
114 return true;
118 // A helper visitor that is used to combine two operations (visitors) to avoid
119 // iterating over radios twice.
120 class CombinedVisitor : public Visitor {
121 public:
122 CombinedVisitor(Visitor* aVisitor1, Visitor* aVisitor2)
123 : mVisitor1(aVisitor1),
124 mVisitor2(aVisitor2),
125 mContinue1(true),
126 mContinue2(true) {}
127 virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
128 if (mContinue1) {
129 mContinue1 = mVisitor1->Visit(aMenuItem);
131 if (mContinue2) {
132 mContinue2 = mVisitor2->Visit(aMenuItem);
134 return mContinue1 || mContinue2;
137 protected:
138 Visitor* mVisitor1;
139 Visitor* mVisitor2;
140 bool mContinue1;
141 bool mContinue2;
144 HTMLMenuItemElement::HTMLMenuItemElement(
145 already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo,
146 FromParser aFromParser)
147 : nsGenericHTMLElement(std::move(aNodeInfo)),
148 mType(kMenuItemDefaultType->value),
149 mParserCreating(false),
150 mShouldInitChecked(false),
151 mCheckedDirty(false),
152 mChecked(false) {
153 mParserCreating = aFromParser;
156 HTMLMenuItemElement::~HTMLMenuItemElement() {}
158 // NS_IMPL_ELEMENT_CLONE(HTMLMenuItemElement)
160 nsresult HTMLMenuItemElement::Clone(dom::NodeInfo* aNodeInfo,
161 nsINode** aResult) const {
162 *aResult = nullptr;
163 RefPtr<HTMLMenuItemElement> it =
164 new HTMLMenuItemElement(do_AddRef(aNodeInfo), NOT_FROM_PARSER);
165 nsresult rv = const_cast<HTMLMenuItemElement*>(this)->CopyInnerTo(it);
166 if (NS_SUCCEEDED(rv)) {
167 switch (mType) {
168 case CMD_TYPE_CHECKBOX:
169 case CMD_TYPE_RADIO:
170 if (mCheckedDirty) {
171 // We no longer have our original checked state. Set our
172 // checked state on the clone.
173 it->mCheckedDirty = true;
174 it->mChecked = mChecked;
176 break;
179 it.forget(aResult);
182 return rv;
185 void HTMLMenuItemElement::GetType(DOMString& aValue) {
186 GetEnumAttr(nsGkAtoms::type, kMenuItemDefaultType->tag, aValue);
189 void HTMLMenuItemElement::SetChecked(bool aChecked) {
190 bool checkedChanged = mChecked != aChecked;
192 mChecked = aChecked;
194 if (mType == CMD_TYPE_RADIO) {
195 if (checkedChanged) {
196 if (mCheckedDirty) {
197 ClearCheckedVisitor visitor(this);
198 WalkRadioGroup(&visitor);
199 } else {
200 ClearCheckedVisitor visitor1(this);
201 SetCheckedDirtyVisitor visitor2;
202 CombinedVisitor visitor(&visitor1, &visitor2);
203 WalkRadioGroup(&visitor);
205 } else if (!mCheckedDirty) {
206 SetCheckedDirtyVisitor visitor;
207 WalkRadioGroup(&visitor);
209 } else {
210 mCheckedDirty = true;
214 void HTMLMenuItemElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) {
215 if (aVisitor.mEvent->mMessage == eMouseClick) {
216 bool originalCheckedValue = false;
217 switch (mType) {
218 case CMD_TYPE_CHECKBOX:
219 originalCheckedValue = mChecked;
220 SetChecked(!originalCheckedValue);
221 aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED;
222 break;
223 case CMD_TYPE_RADIO:
224 // casting back to Element* here to resolve nsISupports ambiguity.
225 Element* supports = GetSelectedRadio();
226 aVisitor.mItemData = supports;
228 originalCheckedValue = mChecked;
229 if (!originalCheckedValue) {
230 SetChecked(true);
231 aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED;
233 break;
236 if (originalCheckedValue) {
237 aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE;
240 // We must cache type because mType may change during JS event.
241 aVisitor.mItemFlags |= mType;
244 nsGenericHTMLElement::GetEventTargetParent(aVisitor);
247 nsresult HTMLMenuItemElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
248 // Check to see if the event was cancelled.
249 if (aVisitor.mEvent->mMessage == eMouseClick &&
250 aVisitor.mItemFlags & NS_CHECKED_IS_TOGGLED &&
251 aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) {
252 bool originalCheckedValue =
253 !!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE);
254 uint8_t oldType = NS_MENUITEM_TYPE(aVisitor.mItemFlags);
256 nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mItemData));
257 RefPtr<HTMLMenuItemElement> selectedRadio =
258 HTMLMenuItemElement::FromNodeOrNull(content);
259 if (selectedRadio) {
260 selectedRadio->SetChecked(true);
261 if (mType != CMD_TYPE_RADIO) {
262 SetChecked(false);
264 } else if (oldType == CMD_TYPE_CHECKBOX) {
265 SetChecked(originalCheckedValue);
269 return NS_OK;
272 nsresult HTMLMenuItemElement::BindToTree(BindContext& aContext,
273 nsINode& aParent) {
274 nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent);
275 NS_ENSURE_SUCCESS(rv, rv);
277 if (IsInUncomposedDoc() && mType == CMD_TYPE_RADIO) {
278 AddedToRadioGroup();
281 return rv;
284 bool HTMLMenuItemElement::ParseAttribute(int32_t aNamespaceID,
285 nsAtom* aAttribute,
286 const nsAString& aValue,
287 nsIPrincipal* aMaybeScriptedPrincipal,
288 nsAttrValue& aResult) {
289 if (aNamespaceID == kNameSpaceID_None) {
290 if (aAttribute == nsGkAtoms::type) {
291 return aResult.ParseEnumValue(aValue, kMenuItemTypeTable, false,
292 kMenuItemDefaultType);
295 if (aAttribute == nsGkAtoms::radiogroup) {
296 aResult.ParseAtom(aValue);
297 return true;
301 return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
302 aMaybeScriptedPrincipal, aResult);
305 void HTMLMenuItemElement::DoneCreatingElement() {
306 mParserCreating = false;
308 if (mShouldInitChecked) {
309 InitChecked();
310 mShouldInitChecked = false;
314 void HTMLMenuItemElement::GetText(nsAString& aText) {
315 nsAutoString text;
316 nsContentUtils::GetNodeTextContent(this, false, text);
318 text.CompressWhitespace(true, true);
319 aText = text;
322 nsresult HTMLMenuItemElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
323 const nsAttrValue* aValue,
324 const nsAttrValue* aOldValue,
325 nsIPrincipal* aSubjectPrincipal,
326 bool aNotify) {
327 if (aNameSpaceID == kNameSpaceID_None) {
328 // Handle type changes first, since some of the later conditions in this
329 // method look at mType and want to see the new value.
330 if (aName == nsGkAtoms::type) {
331 if (aValue) {
332 mType = aValue->GetEnumValue();
333 } else {
334 mType = kMenuItemDefaultType->value;
338 if ((aName == nsGkAtoms::radiogroup || aName == nsGkAtoms::type) &&
339 mType == CMD_TYPE_RADIO && !mParserCreating) {
340 if (IsInUncomposedDoc() && GetParent()) {
341 AddedToRadioGroup();
345 // Checked must be set no matter what type of menuitem it is, since
346 // GetChecked() must reflect the new value
347 if (aName == nsGkAtoms::checked && !mCheckedDirty) {
348 if (mParserCreating) {
349 mShouldInitChecked = true;
350 } else {
351 InitChecked();
356 return nsGenericHTMLElement::AfterSetAttr(
357 aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
360 void HTMLMenuItemElement::WalkRadioGroup(Visitor* aVisitor) {
361 nsIContent* parent = GetParent();
362 if (!parent) {
363 aVisitor->Visit(this);
364 return;
367 BorrowedAttrInfo info1(GetAttrInfo(kNameSpaceID_None, nsGkAtoms::radiogroup));
368 bool info1Empty = !info1.mValue || info1.mValue->IsEmptyString();
370 for (nsIContent* cur = parent->GetFirstChild(); cur;
371 cur = cur->GetNextSibling()) {
372 HTMLMenuItemElement* menuitem = HTMLMenuItemElement::FromNode(cur);
374 if (!menuitem || menuitem->GetType() != CMD_TYPE_RADIO) {
375 continue;
378 BorrowedAttrInfo info2(
379 menuitem->GetAttrInfo(kNameSpaceID_None, nsGkAtoms::radiogroup));
380 bool info2Empty = !info2.mValue || info2.mValue->IsEmptyString();
382 if (info1Empty != info2Empty || (info1.mValue && info2.mValue &&
383 !info1.mValue->Equals(*info2.mValue))) {
384 continue;
387 if (!aVisitor->Visit(menuitem)) {
388 break;
393 HTMLMenuItemElement* HTMLMenuItemElement::GetSelectedRadio() {
394 HTMLMenuItemElement* result = nullptr;
396 GetCheckedVisitor visitor(&result);
397 WalkRadioGroup(&visitor);
399 return result;
402 void HTMLMenuItemElement::AddedToRadioGroup() {
403 bool checkedDirty = mCheckedDirty;
404 if (mChecked) {
405 ClearCheckedVisitor visitor1(this);
406 GetCheckedDirtyVisitor visitor2(&checkedDirty, this);
407 CombinedVisitor visitor(&visitor1, &visitor2);
408 WalkRadioGroup(&visitor);
409 } else {
410 GetCheckedDirtyVisitor visitor(&checkedDirty, this);
411 WalkRadioGroup(&visitor);
413 mCheckedDirty = checkedDirty;
416 void HTMLMenuItemElement::InitChecked() {
417 bool defaultChecked = DefaultChecked();
418 mChecked = defaultChecked;
419 if (mType == CMD_TYPE_RADIO) {
420 ClearCheckedVisitor visitor(this);
421 WalkRadioGroup(&visitor);
425 JSObject* HTMLMenuItemElement::WrapNode(JSContext* aCx,
426 JS::Handle<JSObject*> aGivenProto) {
427 return HTMLMenuItemElement_Binding::Wrap(aCx, this, aGivenProto);
430 } // namespace dom
431 } // namespace mozilla
433 #undef NS_ORIGINAL_CHECKED_VALUE