Coverage Report

Created: 2018-09-25 14:53

/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