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/. */
6 
7 #include "mozilla/dom/HTMLMenuItemElement.h"
8 
9 #include "mozilla/BasicEvents.h"
10 #include "mozilla/EventDispatcher.h"
11 #include "mozilla/dom/HTMLMenuItemElementBinding.h"
12 #include "nsAttrValueInlines.h"
13 #include "nsContentUtils.h"
14 
15 NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(MenuItem)
16 
17 namespace mozilla {
18 namespace dom {
19 
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))
25 
26 enum CmdType : uint8_t {
27   CMD_TYPE_MENUITEM = 1,
28   CMD_TYPE_CHECKBOX,
29   CMD_TYPE_RADIO
30 };
31 
32 static const nsAttrValue::EnumTable kMenuItemTypeTable[] = {
33     {"menuitem", CMD_TYPE_MENUITEM},
34     {"checkbox", CMD_TYPE_CHECKBOX},
35     {"radio", CMD_TYPE_RADIO},
36     {nullptr, 0}};
37 
38 static const nsAttrValue::EnumTable* kMenuItemDefaultType =
39     &kMenuItemTypeTable[0];
40 
41 // A base class inherited by all radio visitors.
42 class Visitor {
43  public:
Visitor()44   Visitor() {}
~Visitor()45   virtual ~Visitor() {}
46 
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.
51    */
52   virtual bool Visit(HTMLMenuItemElement* aMenuItem) = 0;
53 };
54 
55 // Find the selected radio, see GetSelectedRadio().
56 class GetCheckedVisitor : public Visitor {
57  public:
GetCheckedVisitor(HTMLMenuItemElement ** aResult)58   explicit GetCheckedVisitor(HTMLMenuItemElement** aResult)
59       : mResult(aResult) {}
Visit(HTMLMenuItemElement * aMenuItem)60   virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
61     if (aMenuItem->IsChecked()) {
62       *mResult = aMenuItem;
63       return false;
64     }
65     return true;
66   }
67 
68  protected:
69   HTMLMenuItemElement** mResult;
70 };
71 
72 // Deselect all radios except the one passed to the constructor.
73 class ClearCheckedVisitor : public Visitor {
74  public:
ClearCheckedVisitor(HTMLMenuItemElement * aExcludeMenuItem)75   explicit ClearCheckedVisitor(HTMLMenuItemElement* aExcludeMenuItem)
76       : mExcludeMenuItem(aExcludeMenuItem) {}
Visit(HTMLMenuItemElement * aMenuItem)77   virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
78     if (aMenuItem != mExcludeMenuItem && aMenuItem->IsChecked()) {
79       aMenuItem->ClearChecked();
80     }
81     return true;
82   }
83 
84  protected:
85   HTMLMenuItemElement* mExcludeMenuItem;
86 };
87 
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:
GetCheckedDirtyVisitor(bool * aCheckedDirty,HTMLMenuItemElement * aExcludeMenuItem)92   GetCheckedDirtyVisitor(bool* aCheckedDirty,
93                          HTMLMenuItemElement* aExcludeMenuItem)
94       : mCheckedDirty(aCheckedDirty), mExcludeMenuItem(aExcludeMenuItem) {}
Visit(HTMLMenuItemElement * aMenuItem)95   virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
96     if (aMenuItem == mExcludeMenuItem) {
97       return true;
98     }
99     *mCheckedDirty = aMenuItem->IsCheckedDirty();
100     return false;
101   }
102 
103  protected:
104   bool* mCheckedDirty;
105   HTMLMenuItemElement* mExcludeMenuItem;
106 };
107 
108 // Set checked dirty to true on all radios in the group.
109 class SetCheckedDirtyVisitor : public Visitor {
110  public:
SetCheckedDirtyVisitor()111   SetCheckedDirtyVisitor() {}
Visit(HTMLMenuItemElement * aMenuItem)112   virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
113     aMenuItem->SetCheckedDirty();
114     return true;
115   }
116 };
117 
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:
CombinedVisitor(Visitor * aVisitor1,Visitor * aVisitor2)122   CombinedVisitor(Visitor* aVisitor1, Visitor* aVisitor2)
123       : mVisitor1(aVisitor1),
124         mVisitor2(aVisitor2),
125         mContinue1(true),
126         mContinue2(true) {}
Visit(HTMLMenuItemElement * aMenuItem)127   virtual bool Visit(HTMLMenuItemElement* aMenuItem) override {
128     if (mContinue1) {
129       mContinue1 = mVisitor1->Visit(aMenuItem);
130     }
131     if (mContinue2) {
132       mContinue2 = mVisitor2->Visit(aMenuItem);
133     }
134     return mContinue1 || mContinue2;
135   }
136 
137  protected:
138   Visitor* mVisitor1;
139   Visitor* mVisitor2;
140   bool mContinue1;
141   bool mContinue2;
142 };
143 
HTMLMenuItemElement(already_AddRefed<mozilla::dom::NodeInfo> & aNodeInfo,FromParser aFromParser)144 HTMLMenuItemElement::HTMLMenuItemElement(
145     already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo, FromParser aFromParser)
146     : nsGenericHTMLElement(aNodeInfo),
147       mType(kMenuItemDefaultType->value),
148       mParserCreating(false),
149       mShouldInitChecked(false),
150       mCheckedDirty(false),
151       mChecked(false) {
152   mParserCreating = aFromParser;
153 }
154 
~HTMLMenuItemElement()155 HTMLMenuItemElement::~HTMLMenuItemElement() {}
156 
157 // NS_IMPL_ELEMENT_CLONE(HTMLMenuItemElement)
158 
Clone(mozilla::dom::NodeInfo * aNodeInfo,nsINode ** aResult,bool aPreallocateArrays) const159 nsresult HTMLMenuItemElement::Clone(mozilla::dom::NodeInfo* aNodeInfo,
160                                     nsINode** aResult,
161                                     bool aPreallocateArrays) const {
162   *aResult = nullptr;
163   already_AddRefed<mozilla::dom::NodeInfo> ni =
164       RefPtr<mozilla::dom::NodeInfo>(aNodeInfo).forget();
165   RefPtr<HTMLMenuItemElement> it = new HTMLMenuItemElement(ni, NOT_FROM_PARSER);
166   nsresult rv = const_cast<HTMLMenuItemElement*>(this)->CopyInnerTo(
167       it, aPreallocateArrays);
168   if (NS_SUCCEEDED(rv)) {
169     switch (mType) {
170       case CMD_TYPE_CHECKBOX:
171       case CMD_TYPE_RADIO:
172         if (mCheckedDirty) {
173           // We no longer have our original checked state.  Set our
174           // checked state on the clone.
175           it->mCheckedDirty = true;
176           it->mChecked = mChecked;
177         }
178         break;
179     }
180 
181     it.forget(aResult);
182   }
183 
184   return rv;
185 }
186 
GetType(DOMString & aValue)187 void HTMLMenuItemElement::GetType(DOMString& aValue) {
188   GetEnumAttr(nsGkAtoms::type, kMenuItemDefaultType->tag, aValue);
189 }
190 
SetChecked(bool aChecked)191 void HTMLMenuItemElement::SetChecked(bool aChecked) {
192   bool checkedChanged = mChecked != aChecked;
193 
194   mChecked = aChecked;
195 
196   if (mType == CMD_TYPE_RADIO) {
197     if (checkedChanged) {
198       if (mCheckedDirty) {
199         ClearCheckedVisitor visitor(this);
200         WalkRadioGroup(&visitor);
201       } else {
202         ClearCheckedVisitor visitor1(this);
203         SetCheckedDirtyVisitor visitor2;
204         CombinedVisitor visitor(&visitor1, &visitor2);
205         WalkRadioGroup(&visitor);
206       }
207     } else if (!mCheckedDirty) {
208       SetCheckedDirtyVisitor visitor;
209       WalkRadioGroup(&visitor);
210     }
211   } else {
212     mCheckedDirty = true;
213   }
214 }
215 
GetEventTargetParent(EventChainPreVisitor & aVisitor)216 nsresult HTMLMenuItemElement::GetEventTargetParent(
217     EventChainPreVisitor& aVisitor) {
218   if (aVisitor.mEvent->mMessage == eMouseClick) {
219     bool originalCheckedValue = false;
220     switch (mType) {
221       case CMD_TYPE_CHECKBOX:
222         originalCheckedValue = mChecked;
223         SetChecked(!originalCheckedValue);
224         aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED;
225         break;
226       case CMD_TYPE_RADIO:
227         // casting back to Element* here to resolve nsISupports ambiguity.
228         Element* supports = GetSelectedRadio();
229         aVisitor.mItemData = supports;
230 
231         originalCheckedValue = mChecked;
232         if (!originalCheckedValue) {
233           SetChecked(true);
234           aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED;
235         }
236         break;
237     }
238 
239     if (originalCheckedValue) {
240       aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE;
241     }
242 
243     // We must cache type because mType may change during JS event.
244     aVisitor.mItemFlags |= mType;
245   }
246 
247   return nsGenericHTMLElement::GetEventTargetParent(aVisitor);
248 }
249 
PostHandleEvent(EventChainPostVisitor & aVisitor)250 nsresult HTMLMenuItemElement::PostHandleEvent(EventChainPostVisitor& aVisitor) {
251   // Check to see if the event was cancelled.
252   if (aVisitor.mEvent->mMessage == eMouseClick &&
253       aVisitor.mItemFlags & NS_CHECKED_IS_TOGGLED &&
254       aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) {
255     bool originalCheckedValue =
256         !!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE);
257     uint8_t oldType = NS_MENUITEM_TYPE(aVisitor.mItemFlags);
258 
259     nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mItemData));
260     RefPtr<HTMLMenuItemElement> selectedRadio =
261         HTMLMenuItemElement::FromContentOrNull(content);
262     if (selectedRadio) {
263       selectedRadio->SetChecked(true);
264       if (mType != CMD_TYPE_RADIO) {
265         SetChecked(false);
266       }
267     } else if (oldType == CMD_TYPE_CHECKBOX) {
268       SetChecked(originalCheckedValue);
269     }
270   }
271 
272   return NS_OK;
273 }
274 
BindToTree(nsIDocument * aDocument,nsIContent * aParent,nsIContent * aBindingParent,bool aCompileEventHandlers)275 nsresult HTMLMenuItemElement::BindToTree(nsIDocument* aDocument,
276                                          nsIContent* aParent,
277                                          nsIContent* aBindingParent,
278                                          bool aCompileEventHandlers) {
279   nsresult rv = nsGenericHTMLElement::BindToTree(
280       aDocument, aParent, aBindingParent, aCompileEventHandlers);
281 
282   if (NS_SUCCEEDED(rv) && aDocument && mType == CMD_TYPE_RADIO) {
283     AddedToRadioGroup();
284   }
285 
286   return rv;
287 }
288 
ParseAttribute(int32_t aNamespaceID,nsAtom * aAttribute,const nsAString & aValue,nsIPrincipal * aMaybeScriptedPrincipal,nsAttrValue & aResult)289 bool HTMLMenuItemElement::ParseAttribute(int32_t aNamespaceID,
290                                          nsAtom* aAttribute,
291                                          const nsAString& aValue,
292                                          nsIPrincipal* aMaybeScriptedPrincipal,
293                                          nsAttrValue& aResult) {
294   if (aNamespaceID == kNameSpaceID_None) {
295     if (aAttribute == nsGkAtoms::type) {
296       return aResult.ParseEnumValue(aValue, kMenuItemTypeTable, false,
297                                     kMenuItemDefaultType);
298     }
299 
300     if (aAttribute == nsGkAtoms::radiogroup) {
301       aResult.ParseAtom(aValue);
302       return true;
303     }
304   }
305 
306   return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
307                                               aMaybeScriptedPrincipal, aResult);
308 }
309 
DoneCreatingElement()310 void HTMLMenuItemElement::DoneCreatingElement() {
311   mParserCreating = false;
312 
313   if (mShouldInitChecked) {
314     InitChecked();
315     mShouldInitChecked = false;
316   }
317 }
318 
GetText(nsAString & aText)319 void HTMLMenuItemElement::GetText(nsAString& aText) {
320   nsAutoString text;
321   nsContentUtils::GetNodeTextContent(this, false, text);
322 
323   text.CompressWhitespace(true, true);
324   aText = text;
325 }
326 
AfterSetAttr(int32_t aNameSpaceID,nsAtom * aName,const nsAttrValue * aValue,const nsAttrValue * aOldValue,nsIPrincipal * aSubjectPrincipal,bool aNotify)327 nsresult HTMLMenuItemElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
328                                            const nsAttrValue* aValue,
329                                            const nsAttrValue* aOldValue,
330                                            nsIPrincipal* aSubjectPrincipal,
331                                            bool aNotify) {
332   if (aNameSpaceID == kNameSpaceID_None) {
333     // Handle type changes first, since some of the later conditions in this
334     // method look at mType and want to see the new value.
335     if (aName == nsGkAtoms::type) {
336       if (aValue) {
337         mType = aValue->GetEnumValue();
338       } else {
339         mType = kMenuItemDefaultType->value;
340       }
341     }
342 
343     if ((aName == nsGkAtoms::radiogroup || aName == nsGkAtoms::type) &&
344         mType == CMD_TYPE_RADIO && !mParserCreating) {
345       if (IsInUncomposedDoc() && GetParent()) {
346         AddedToRadioGroup();
347       }
348     }
349 
350     // Checked must be set no matter what type of menuitem it is, since
351     // GetChecked() must reflect the new value
352     if (aName == nsGkAtoms::checked && !mCheckedDirty) {
353       if (mParserCreating) {
354         mShouldInitChecked = true;
355       } else {
356         InitChecked();
357       }
358     }
359   }
360 
361   return nsGenericHTMLElement::AfterSetAttr(
362       aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify);
363 }
364 
WalkRadioGroup(Visitor * aVisitor)365 void HTMLMenuItemElement::WalkRadioGroup(Visitor* aVisitor) {
366   nsIContent* parent = GetParent();
367   if (!parent) {
368     aVisitor->Visit(this);
369     return;
370   }
371 
372   BorrowedAttrInfo info1(GetAttrInfo(kNameSpaceID_None, nsGkAtoms::radiogroup));
373   bool info1Empty = !info1.mValue || info1.mValue->IsEmptyString();
374 
375   for (nsIContent* cur = parent->GetFirstChild(); cur;
376        cur = cur->GetNextSibling()) {
377     HTMLMenuItemElement* menuitem = HTMLMenuItemElement::FromContent(cur);
378 
379     if (!menuitem || menuitem->GetType() != CMD_TYPE_RADIO) {
380       continue;
381     }
382 
383     BorrowedAttrInfo info2(
384         menuitem->GetAttrInfo(kNameSpaceID_None, nsGkAtoms::radiogroup));
385     bool info2Empty = !info2.mValue || info2.mValue->IsEmptyString();
386 
387     if (info1Empty != info2Empty || (info1.mValue && info2.mValue &&
388                                      !info1.mValue->Equals(*info2.mValue))) {
389       continue;
390     }
391 
392     if (!aVisitor->Visit(menuitem)) {
393       break;
394     }
395   }
396 }
397 
GetSelectedRadio()398 HTMLMenuItemElement* HTMLMenuItemElement::GetSelectedRadio() {
399   HTMLMenuItemElement* result = nullptr;
400 
401   GetCheckedVisitor visitor(&result);
402   WalkRadioGroup(&visitor);
403 
404   return result;
405 }
406 
AddedToRadioGroup()407 void HTMLMenuItemElement::AddedToRadioGroup() {
408   bool checkedDirty = mCheckedDirty;
409   if (mChecked) {
410     ClearCheckedVisitor visitor1(this);
411     GetCheckedDirtyVisitor visitor2(&checkedDirty, this);
412     CombinedVisitor visitor(&visitor1, &visitor2);
413     WalkRadioGroup(&visitor);
414   } else {
415     GetCheckedDirtyVisitor visitor(&checkedDirty, this);
416     WalkRadioGroup(&visitor);
417   }
418   mCheckedDirty = checkedDirty;
419 }
420 
InitChecked()421 void HTMLMenuItemElement::InitChecked() {
422   bool defaultChecked = DefaultChecked();
423   mChecked = defaultChecked;
424   if (mType == CMD_TYPE_RADIO) {
425     ClearCheckedVisitor visitor(this);
426     WalkRadioGroup(&visitor);
427   }
428 }
429 
WrapNode(JSContext * aCx,JS::Handle<JSObject * > aGivenProto)430 JSObject* HTMLMenuItemElement::WrapNode(JSContext* aCx,
431                                         JS::Handle<JSObject*> aGivenProto) {
432   return HTMLMenuItemElementBinding::Wrap(aCx, this, aGivenProto);
433 }
434 
435 }  // namespace dom
436 }  // namespace mozilla
437 
438 #undef NS_ORIGINAL_CHECKED_VALUE
439