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