1 : /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* ***** BEGIN LICENSE BLOCK *****
3 : * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 : *
5 : * The contents of this file are subject to the Mozilla Public License Version
6 : * 1.1 (the "License"); you may not use this file except in compliance with
7 : * the License. You may obtain a copy of the License at
8 : * http://www.mozilla.org/MPL/
9 : *
10 : * Software distributed under the License is distributed on an "AS IS" basis,
11 : * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 : * for the specific language governing rights and limitations under the
13 : * License.
14 : *
15 : * The Original Code is Mozilla.
16 : *
17 : * The Initial Developer of the Original Code is Mozilla Foundation
18 : * Portions created by the Initial Developer are Copyright (C) 2011
19 : * the Initial Developer. All Rights Reserved.
20 : *
21 : * Contributor(s):
22 : *
23 : * Alternatively, the contents of this file may be used under the terms of
24 : * either of the GNU General Public License Version 2 or later (the "GPL"),
25 : * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 : * in which case the provisions of the GPL or the LGPL are applicable instead
27 : * of those above. If you wish to allow use of your version of this file only
28 : * under the terms of either the GPL or the LGPL, and not to allow others to
29 : * use your version of this file under the terms of the MPL, indicate your
30 : * decision by deleting the provisions above and replace them with the notice
31 : * and other provisions required by the GPL or the LGPL. If you do not delete
32 : * the provisions above, a recipient may use your version of this file under
33 : * the terms of any one of the MPL, the GPL or the LGPL.
34 : *
35 : * ***** END LICENSE BLOCK ***** */
36 :
37 : #include "nsGUIEvent.h"
38 : #include "nsEventDispatcher.h"
39 : #include "nsHTMLMenuItemElement.h"
40 : #include "nsContentUtils.h"
41 :
42 : using namespace mozilla::dom;
43 :
44 : // First bits are needed for the menuitem type.
45 : #define NS_CHECKED_IS_TOGGLED (1 << 2)
46 : #define NS_ORIGINAL_CHECKED_VALUE (1 << 3)
47 : #define NS_MENUITEM_TYPE(bits) ((bits) & ~( \
48 : NS_CHECKED_IS_TOGGLED | NS_ORIGINAL_CHECKED_VALUE))
49 :
50 : enum CmdType
51 : {
52 : CMD_TYPE_MENUITEM = 1,
53 : CMD_TYPE_CHECKBOX,
54 : CMD_TYPE_RADIO
55 : };
56 :
57 : static const nsAttrValue::EnumTable kMenuItemTypeTable[] = {
58 : { "menuitem", CMD_TYPE_MENUITEM },
59 : { "checkbox", CMD_TYPE_CHECKBOX },
60 : { "radio", CMD_TYPE_RADIO },
61 : { 0 }
62 : };
63 :
64 : static const nsAttrValue::EnumTable* kMenuItemDefaultType =
65 : &kMenuItemTypeTable[0];
66 :
67 : // A base class inherited by all radio visitors.
68 : class Visitor
69 : {
70 : public:
71 0 : Visitor() { }
72 0 : virtual ~Visitor() { }
73 :
74 : /**
75 : * Visit a node in the tree. This is meant to be called on all radios in a
76 : * group, sequentially. If the method returns false then the iteration is
77 : * stopped.
78 : */
79 : virtual bool Visit(nsHTMLMenuItemElement* aMenuItem) = 0;
80 : };
81 :
82 : // Find the selected radio, see GetSelectedRadio().
83 : class GetCheckedVisitor : public Visitor
84 0 : {
85 : public:
86 0 : GetCheckedVisitor(nsHTMLMenuItemElement** aResult)
87 0 : : mResult(aResult)
88 0 : { }
89 0 : virtual bool Visit(nsHTMLMenuItemElement* aMenuItem)
90 : {
91 0 : if (aMenuItem->IsChecked()) {
92 0 : *mResult = aMenuItem;
93 0 : return false;
94 : }
95 0 : return true;
96 : }
97 : protected:
98 : nsHTMLMenuItemElement** mResult;
99 : };
100 :
101 : // Deselect all radios except the one passed to the constructor.
102 : class ClearCheckedVisitor : public Visitor
103 0 : {
104 : public:
105 0 : ClearCheckedVisitor(nsHTMLMenuItemElement* aExcludeMenuItem)
106 0 : : mExcludeMenuItem(aExcludeMenuItem)
107 0 : { }
108 0 : virtual bool Visit(nsHTMLMenuItemElement* aMenuItem)
109 : {
110 0 : if (aMenuItem != mExcludeMenuItem && aMenuItem->IsChecked()) {
111 0 : aMenuItem->ClearChecked();
112 : }
113 0 : return true;
114 : }
115 : protected:
116 : nsHTMLMenuItemElement* mExcludeMenuItem;
117 : };
118 :
119 : // Get current value of the checked dirty flag. The same value is stored on all
120 : // radios in the group, so we need to check only the first one.
121 : class GetCheckedDirtyVisitor : public Visitor
122 0 : {
123 : public:
124 0 : GetCheckedDirtyVisitor(bool* aCheckedDirty,
125 : nsHTMLMenuItemElement* aExcludeMenuItem)
126 : : mCheckedDirty(aCheckedDirty),
127 0 : mExcludeMenuItem(aExcludeMenuItem)
128 0 : { }
129 0 : virtual bool Visit(nsHTMLMenuItemElement* aMenuItem)
130 : {
131 0 : if (aMenuItem == mExcludeMenuItem) {
132 0 : return true;
133 : }
134 0 : *mCheckedDirty = aMenuItem->IsCheckedDirty();
135 0 : return false;
136 : }
137 : protected:
138 : bool* mCheckedDirty;
139 : nsHTMLMenuItemElement* mExcludeMenuItem;
140 : };
141 :
142 : // Set checked dirty to true on all radios in the group.
143 : class SetCheckedDirtyVisitor : public Visitor
144 0 : {
145 : public:
146 0 : SetCheckedDirtyVisitor()
147 0 : { }
148 0 : virtual bool Visit(nsHTMLMenuItemElement* aMenuItem)
149 : {
150 0 : aMenuItem->SetCheckedDirty();
151 0 : return true;
152 : }
153 : };
154 :
155 : // A helper visitor that is used to combine two operations (visitors) to avoid
156 : // iterating over radios twice.
157 : class CombinedVisitor : public Visitor
158 0 : {
159 : public:
160 0 : CombinedVisitor(Visitor* aVisitor1, Visitor* aVisitor2)
161 : : mVisitor1(aVisitor1), mVisitor2(aVisitor2),
162 0 : mContinue1(true), mContinue2(true)
163 0 : { }
164 0 : virtual bool Visit(nsHTMLMenuItemElement* aMenuItem)
165 : {
166 0 : if (mContinue1) {
167 0 : mContinue1 = mVisitor1->Visit(aMenuItem);
168 : }
169 0 : if (mContinue2) {
170 0 : mContinue2 = mVisitor2->Visit(aMenuItem);
171 : }
172 0 : return mContinue1 || mContinue2;
173 : }
174 : protected:
175 : Visitor* mVisitor1;
176 : Visitor* mVisitor2;
177 : bool mContinue1;
178 : bool mContinue2;
179 : };
180 :
181 :
182 0 : NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(MenuItem)
183 :
184 0 : nsHTMLMenuItemElement::nsHTMLMenuItemElement(
185 : already_AddRefed<nsINodeInfo> aNodeInfo, FromParser aFromParser)
186 : : nsGenericHTMLElement(aNodeInfo),
187 : mType(kMenuItemDefaultType->value),
188 : mParserCreating(false),
189 : mShouldInitChecked(false),
190 : mCheckedDirty(false),
191 0 : mChecked(false)
192 : {
193 0 : mParserCreating = aFromParser;
194 0 : }
195 :
196 0 : nsHTMLMenuItemElement::~nsHTMLMenuItemElement()
197 : {
198 0 : }
199 :
200 :
201 0 : NS_IMPL_ADDREF_INHERITED(nsHTMLMenuItemElement, nsGenericElement)
202 0 : NS_IMPL_RELEASE_INHERITED(nsHTMLMenuItemElement, nsGenericElement)
203 :
204 :
205 0 : DOMCI_NODE_DATA(HTMLMenuItemElement, nsHTMLMenuItemElement)
206 :
207 : // QueryInterface implementation for nsHTMLMenuItemElement
208 0 : NS_INTERFACE_TABLE_HEAD(nsHTMLMenuItemElement)
209 0 : NS_HTML_CONTENT_INTERFACE_TABLE2(nsHTMLMenuItemElement,
210 : nsIDOMHTMLCommandElement,
211 : nsIDOMHTMLMenuItemElement)
212 0 : NS_HTML_CONTENT_INTERFACE_TABLE_TO_MAP_SEGUE(nsHTMLMenuItemElement,
213 : nsGenericHTMLElement)
214 0 : NS_HTML_CONTENT_INTERFACE_TABLE_TAIL_CLASSINFO(HTMLMenuItemElement)
215 :
216 : //NS_IMPL_ELEMENT_CLONE(nsHTMLMenuItemElement)
217 : nsresult
218 0 : nsHTMLMenuItemElement::Clone(nsINodeInfo *aNodeInfo, nsINode **aResult) const
219 : {
220 0 : *aResult = nsnull;
221 0 : nsCOMPtr<nsINodeInfo> ni = aNodeInfo;
222 : nsRefPtr<nsHTMLMenuItemElement> it =
223 0 : new nsHTMLMenuItemElement(ni.forget(), NOT_FROM_PARSER);
224 0 : nsresult rv = CopyInnerTo(it);
225 0 : if (NS_SUCCEEDED(rv)) {
226 0 : switch (mType) {
227 : case CMD_TYPE_CHECKBOX:
228 : case CMD_TYPE_RADIO:
229 0 : if (mCheckedDirty) {
230 : // We no longer have our original checked state. Set our
231 : // checked state on the clone.
232 0 : it->mCheckedDirty = true;
233 0 : it->mChecked = mChecked;
234 : }
235 0 : break;
236 : }
237 :
238 0 : it.forget(aResult);
239 : }
240 :
241 0 : return rv;
242 : }
243 :
244 :
245 0 : NS_IMPL_ENUM_ATTR_DEFAULT_VALUE(nsHTMLMenuItemElement, Type, type,
246 : kMenuItemDefaultType->tag)
247 : // GetText returns a whitespace compressed .textContent value.
248 0 : NS_IMPL_STRING_ATTR_WITH_FALLBACK(nsHTMLMenuItemElement, Label, label, GetText)
249 0 : NS_IMPL_URI_ATTR(nsHTMLMenuItemElement, Icon, icon)
250 0 : NS_IMPL_BOOL_ATTR(nsHTMLMenuItemElement, Disabled, disabled)
251 0 : NS_IMPL_BOOL_ATTR(nsHTMLMenuItemElement, DefaultChecked, checked)
252 : //NS_IMPL_BOOL_ATTR(nsHTMLMenuItemElement, Checked, checked)
253 0 : NS_IMPL_STRING_ATTR(nsHTMLMenuItemElement, Radiogroup, radiogroup)
254 :
255 : NS_IMETHODIMP
256 0 : nsHTMLMenuItemElement::GetChecked(bool* aChecked)
257 : {
258 0 : *aChecked = mChecked;
259 0 : return NS_OK;
260 : }
261 :
262 : NS_IMETHODIMP
263 0 : nsHTMLMenuItemElement::SetChecked(bool aChecked)
264 : {
265 0 : bool checkedChanged = mChecked != aChecked;
266 :
267 0 : mChecked = aChecked;
268 :
269 0 : if (mType == CMD_TYPE_RADIO) {
270 0 : if (checkedChanged) {
271 0 : if (mCheckedDirty) {
272 0 : ClearCheckedVisitor visitor(this);
273 0 : WalkRadioGroup(&visitor);
274 : } else {
275 0 : ClearCheckedVisitor visitor1(this);
276 0 : SetCheckedDirtyVisitor visitor2;
277 0 : CombinedVisitor visitor(&visitor1, &visitor2);
278 0 : WalkRadioGroup(&visitor);
279 : }
280 0 : } else if (!mCheckedDirty) {
281 0 : SetCheckedDirtyVisitor visitor;
282 0 : WalkRadioGroup(&visitor);
283 : }
284 : } else {
285 0 : mCheckedDirty = true;
286 : }
287 :
288 0 : return NS_OK;
289 : }
290 :
291 : nsresult
292 0 : nsHTMLMenuItemElement::PreHandleEvent(nsEventChainPreVisitor& aVisitor)
293 : {
294 0 : if (aVisitor.mEvent->message == NS_MOUSE_CLICK) {
295 :
296 0 : bool originalCheckedValue = false;
297 0 : switch (mType) {
298 : case CMD_TYPE_CHECKBOX:
299 0 : originalCheckedValue = mChecked;
300 0 : SetChecked(!originalCheckedValue);
301 0 : aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED;
302 0 : break;
303 : case CMD_TYPE_RADIO:
304 0 : nsCOMPtr<nsIDOMHTMLMenuItemElement> selectedRadio = GetSelectedRadio();
305 0 : aVisitor.mItemData = selectedRadio;
306 :
307 0 : originalCheckedValue = mChecked;
308 0 : if (!originalCheckedValue) {
309 0 : SetChecked(true);
310 0 : aVisitor.mItemFlags |= NS_CHECKED_IS_TOGGLED;
311 : }
312 : break;
313 : }
314 :
315 0 : if (originalCheckedValue) {
316 0 : aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE;
317 : }
318 :
319 : // We must cache type because mType may change during JS event.
320 0 : aVisitor.mItemFlags |= mType;
321 : }
322 :
323 0 : return nsGenericHTMLElement::PreHandleEvent(aVisitor);
324 : }
325 :
326 : nsresult
327 0 : nsHTMLMenuItemElement::PostHandleEvent(nsEventChainPostVisitor& aVisitor)
328 : {
329 : // Check to see if the event was cancelled.
330 0 : if (aVisitor.mEvent->message == NS_MOUSE_CLICK &&
331 : aVisitor.mItemFlags & NS_CHECKED_IS_TOGGLED &&
332 : aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) {
333 : bool originalCheckedValue =
334 0 : !!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE);
335 0 : PRUint8 oldType = NS_MENUITEM_TYPE(aVisitor.mItemFlags);
336 :
337 : nsCOMPtr<nsIDOMHTMLMenuItemElement> selectedRadio =
338 0 : do_QueryInterface(aVisitor.mItemData);
339 0 : if (selectedRadio) {
340 0 : selectedRadio->SetChecked(true);
341 0 : if (mType != CMD_TYPE_RADIO) {
342 0 : SetChecked(false);
343 : }
344 0 : } else if (oldType == CMD_TYPE_CHECKBOX) {
345 0 : SetChecked(originalCheckedValue);
346 : }
347 : }
348 :
349 0 : return NS_OK;
350 : }
351 :
352 : nsresult
353 0 : nsHTMLMenuItemElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
354 : nsIContent* aBindingParent,
355 : bool aCompileEventHandlers)
356 : {
357 : nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent,
358 : aBindingParent,
359 0 : aCompileEventHandlers);
360 :
361 0 : if (NS_SUCCEEDED(rv) && aDocument && mType == CMD_TYPE_RADIO) {
362 0 : AddedToRadioGroup();
363 : }
364 :
365 0 : return rv;
366 : }
367 :
368 : bool
369 0 : nsHTMLMenuItemElement::ParseAttribute(PRInt32 aNamespaceID,
370 : nsIAtom* aAttribute,
371 : const nsAString& aValue,
372 : nsAttrValue& aResult)
373 : {
374 0 : if (aNamespaceID == kNameSpaceID_None) {
375 0 : if (aAttribute == nsGkAtoms::type) {
376 : bool success = aResult.ParseEnumValue(aValue, kMenuItemTypeTable,
377 0 : false);
378 0 : if (success) {
379 0 : mType = aResult.GetEnumValue();
380 : } else {
381 0 : mType = kMenuItemDefaultType->value;
382 : }
383 :
384 0 : return success;
385 : }
386 :
387 0 : if (aAttribute == nsGkAtoms::radiogroup) {
388 0 : aResult.ParseAtom(aValue);
389 0 : return true;
390 : }
391 : }
392 :
393 : return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
394 0 : aResult);
395 : }
396 :
397 : void
398 0 : nsHTMLMenuItemElement::DoneCreatingElement()
399 : {
400 0 : mParserCreating = false;
401 :
402 0 : if (mShouldInitChecked) {
403 0 : InitChecked();
404 0 : mShouldInitChecked = false;
405 : }
406 0 : }
407 :
408 : void
409 0 : nsHTMLMenuItemElement::GetText(nsAString& aText)
410 : {
411 0 : nsAutoString text;
412 0 : nsContentUtils::GetNodeTextContent(this, false, text);
413 :
414 0 : text.CompressWhitespace(true, true);
415 0 : aText = text;
416 0 : }
417 :
418 : nsresult
419 0 : nsHTMLMenuItemElement::AfterSetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
420 : const nsAttrValue* aValue, bool aNotify)
421 : {
422 0 : if (aNameSpaceID == kNameSpaceID_None) {
423 0 : if ((aName == nsGkAtoms::radiogroup || aName == nsGkAtoms::type) &&
424 : mType == CMD_TYPE_RADIO &&
425 0 : !mParserCreating) {
426 0 : if (IsInDoc() && GetParent()) {
427 0 : AddedToRadioGroup();
428 : }
429 : }
430 :
431 : // Checked must be set no matter what type of menuitem it is, since
432 : // GetChecked() must reflect the new value
433 0 : if (aName == nsGkAtoms::checked &&
434 0 : !mCheckedDirty) {
435 0 : if (mParserCreating) {
436 0 : mShouldInitChecked = true;
437 : } else {
438 0 : InitChecked();
439 : }
440 : }
441 : }
442 :
443 : return nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue,
444 0 : aNotify);
445 : }
446 :
447 : void
448 0 : nsHTMLMenuItemElement::WalkRadioGroup(Visitor* aVisitor)
449 : {
450 0 : nsIContent* parent = GetParent();
451 0 : if (!parent) {
452 0 : aVisitor->Visit(this);
453 0 : return;
454 : }
455 :
456 : nsAttrInfo info1(GetAttrInfo(kNameSpaceID_None,
457 0 : nsGkAtoms::radiogroup));
458 0 : bool info1Empty = !info1.mValue || info1.mValue->IsEmptyString();
459 :
460 0 : for (nsIContent* cur = parent->GetFirstChild();
461 : cur;
462 0 : cur = cur->GetNextSibling()) {
463 0 : nsHTMLMenuItemElement* menuitem = nsHTMLMenuItemElement::FromContent(cur);
464 :
465 0 : if (!menuitem || menuitem->GetType() != CMD_TYPE_RADIO) {
466 0 : continue;
467 : }
468 :
469 : nsAttrInfo info2(menuitem->GetAttrInfo(kNameSpaceID_None,
470 0 : nsGkAtoms::radiogroup));
471 0 : bool info2Empty = !info2.mValue || info2.mValue->IsEmptyString();
472 :
473 0 : if (info1Empty != info2Empty ||
474 0 : (info1.mValue && info2.mValue && !info1.mValue->Equals(*info2.mValue))) {
475 0 : continue;
476 : }
477 :
478 0 : if (!aVisitor->Visit(menuitem)) {
479 0 : break;
480 : }
481 : }
482 : }
483 :
484 : nsHTMLMenuItemElement*
485 0 : nsHTMLMenuItemElement::GetSelectedRadio()
486 : {
487 0 : nsHTMLMenuItemElement* result = nsnull;
488 :
489 0 : GetCheckedVisitor visitor(&result);
490 0 : WalkRadioGroup(&visitor);
491 :
492 0 : return result;
493 : }
494 :
495 : void
496 0 : nsHTMLMenuItemElement::AddedToRadioGroup()
497 : {
498 0 : bool checkedDirty = mCheckedDirty;
499 0 : if (mChecked) {
500 0 : ClearCheckedVisitor visitor1(this);
501 0 : GetCheckedDirtyVisitor visitor2(&checkedDirty, this);
502 0 : CombinedVisitor visitor(&visitor1, &visitor2);
503 0 : WalkRadioGroup(&visitor);
504 : } else {
505 0 : GetCheckedDirtyVisitor visitor(&checkedDirty, this);
506 0 : WalkRadioGroup(&visitor);
507 : }
508 0 : mCheckedDirty = checkedDirty;
509 0 : }
510 :
511 : void
512 0 : nsHTMLMenuItemElement::InitChecked()
513 : {
514 : bool defaultChecked;
515 0 : GetDefaultChecked(&defaultChecked);
516 0 : mChecked = defaultChecked;
517 0 : if (mType == CMD_TYPE_RADIO) {
518 0 : ClearCheckedVisitor visitor(this);
519 0 : WalkRadioGroup(&visitor);
520 : }
521 0 : }
|