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