Bug 596681 - Implement HTMLSelectElement.selectedOptions attribute. r=smaug
authorReuben Morais <reuben.morais@gmail.com>
Mon, 02 Sep 2013 14:23:27 -0300
changeset 159067 499f3c4fbd98f5440ed73288ca8c0153480370a0
parent 159066 24ffdfbf55d8d8abf8257a4ad9acdb6f0232f5e9
child 159068 1604454f8d214b06225e7118a6864064131bfedb
push id407
push userlsblakk@mozilla.com
push dateTue, 03 Dec 2013 03:32:50 +0000
treeherdermozilla-release@babf8c9ebc52 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs596681
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 596681 - Implement HTMLSelectElement.selectedOptions attribute. r=smaug
content/base/src/nsContentList.h
content/html/content/src/HTMLSelectElement.cpp
content/html/content/src/HTMLSelectElement.h
content/html/content/test/forms/Makefile.in
content/html/content/test/forms/test_select_selectedOptions.html
dom/interfaces/html/nsIDOMHTMLSelectElement.idl
dom/webidl/HTMLSelectElement.webidl
--- a/content/base/src/nsContentList.h
+++ b/content/base/src/nsContentList.h
@@ -294,16 +294,26 @@ public:
     NS_PRECONDITION(mXMLMatchAtom,
                     "How did we get here with a null match atom on our list?");
     return
       mXMLMatchAtom->Equals(aKey.mTagname) &&
       mRootNode == aKey.mRootNode &&
       mMatchNameSpaceId == aKey.mMatchNameSpaceId;
   }
 
+  /**
+   * Sets the state to LIST_DIRTY and clears mElements array.
+   * @note This is the only acceptable way to set state to LIST_DIRTY.
+   */
+  void SetDirty()
+  {
+    mState = LIST_DIRTY;
+    Reset();
+  }
+
 protected:
   /**
    * Returns whether the element matches our criterion
    *
    * @param  aElement the element to attempt to match
    * @return whether we match
    */
   bool Match(mozilla::dom::Element *aElement);
@@ -346,26 +356,16 @@ protected:
   void RemoveFromHashtable();
   /**
    * If state is not LIST_UP_TO_DATE, fully populate ourselves with
    * all the nodes we can find.
    */
   inline void BringSelfUpToDate(bool aDoFlush);
 
   /**
-   * Sets the state to LIST_DIRTY and clears mElements array.
-   * @note This is the only acceptable way to set state to LIST_DIRTY.
-   */
-  void SetDirty()
-  {
-    mState = LIST_DIRTY;
-    Reset();
-  }
-
-  /**
    * To be called from non-destructor locations that want to remove from caches.
    * Needed because if subclasses want to have cache behavior they can't just
    * override RemoveFromHashtable(), since we call that in our destructor.
    */
   virtual void RemoveFromCaches() {
     RemoveFromHashtable();
   }
 
--- a/content/html/content/src/HTMLSelectElement.cpp
+++ b/content/html/content/src/HTMLSelectElement.cpp
@@ -8,16 +8,17 @@
 #include "mozAutoDocUpdate.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/HTMLOptGroupElement.h"
 #include "mozilla/dom/HTMLOptionElement.h"
 #include "mozilla/dom/HTMLSelectElementBinding.h"
 #include "mozilla/Util.h"
 #include "nsContentCreatorFunctions.h"
+#include "nsContentList.h"
 #include "nsError.h"
 #include "nsEventDispatcher.h"
 #include "nsEventStates.h"
 #include "nsFormSubmission.h"
 #include "nsGkAtoms.h"
 #include "nsGUIEvent.h"
 #include "nsIComboboxControlFrame.h"
 #include "nsIDocument.h"
@@ -134,20 +135,22 @@ HTMLSelectElement::~HTMLSelectElement()
 // ISupports
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLSelectElement)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLSelectElement,
                                                   nsGenericHTMLFormElementWithState)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOptions)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedOptions)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLSelectElement,
                                                 nsGenericHTMLFormElementWithState)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedOptions)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_ADDREF_INHERITED(HTMLSelectElement, Element)
 NS_IMPL_RELEASE_INHERITED(HTMLSelectElement, Element)
 
 // QueryInterface implementation for HTMLSelectElement
 NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLSelectElement)
   NS_INTERFACE_TABLE_INHERITED2(HTMLSelectElement,
@@ -764,16 +767,44 @@ HTMLSelectElement::SetLength(uint32_t aL
           return;
         }
         MOZ_ASSERT(node);
       }
     }
   }
 }
 
+/* static */
+bool
+HTMLSelectElement::MatchSelectedOptions(nsIContent* aContent,
+                                        int32_t /* unused */,
+                                        nsIAtom* /* unused */,
+                                        void* /* unused*/)
+{
+  HTMLOptionElement* option = HTMLOptionElement::FromContent(aContent);
+  return option && option->Selected();
+}
+
+nsIHTMLCollection*
+HTMLSelectElement::SelectedOptions()
+{
+  if (!mSelectedOptions) {
+    mSelectedOptions = new nsContentList(this, MatchSelectedOptions, nullptr,
+                                         nullptr, /* deep */ true);
+  }
+  return mSelectedOptions;
+}
+
+NS_IMETHODIMP
+HTMLSelectElement::GetSelectedOptions(nsIDOMHTMLCollection** aSelectedOptions)
+{
+  NS_ADDREF(*aSelectedOptions = SelectedOptions());
+  return NS_OK;
+}
+
 //NS_IMPL_INT_ATTR(HTMLSelectElement, SelectedIndex, selectedindex)
 
 NS_IMETHODIMP
 HTMLSelectElement::GetSelectedIndex(int32_t* aValue)
 {
   *aValue = SelectedIndex();
 
   return NS_OK;
@@ -842,16 +873,17 @@ HTMLSelectElement::OnOptionSelected(nsIS
     }
   }
 
   // Let the frame know too
   if (aSelectFrame) {
     aSelectFrame->OnOptionSelected(aIndex, aSelected);
   }
 
+  UpdateSelectedOptions();
   UpdateValueMissingValidityState();
   UpdateState(aNotify);
 }
 
 void
 HTMLSelectElement::FindSelectedIndex(int32_t aStartIndex, bool aNotify)
 {
   mSelectedIndex = -1;
@@ -1843,24 +1875,34 @@ HTMLSelectElement::FieldSetDisabledChang
 
 void
 HTMLSelectElement::SetSelectionChanged(bool aValue, bool aNotify)
 {
   if (!mDefaultSelectionSet) {
     return;
   }
 
+  UpdateSelectedOptions();
+
   bool previousSelectionChangedValue = mSelectionHasChanged;
   mSelectionHasChanged = aValue;
 
   if (mSelectionHasChanged != previousSelectionChangedValue) {
     UpdateState(aNotify);
   }
 }
 
+void
+HTMLSelectElement::UpdateSelectedOptions()
+{
+  if (mSelectedOptions) {
+    mSelectedOptions->SetDirty();
+  }
+}
+
 JSObject*
 HTMLSelectElement::WrapNode(JSContext* aCx, JS::Handle<JSObject*> aScope)
 {
   return HTMLSelectElementBinding::Wrap(aCx, aScope, this);
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/content/html/content/src/HTMLSelectElement.h
+++ b/content/html/content/src/HTMLSelectElement.h
@@ -13,17 +13,19 @@
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/HTMLOptionsCollection.h"
 #include "mozilla/ErrorResult.h"
 #include "nsCheapSets.h"
 #include "nsCOMPtr.h"
 #include "nsError.h"
 #include "mozilla/dom/HTMLFormElement.h"
 
+class nsContentList;
 class nsIDOMHTMLOptionElement;
+class nsIHTMLCollection;
 class nsISelectControlFrame;
 class nsPresState;
 
 namespace mozilla {
 namespace dom {
 
 class HTMLSelectElement;
 
@@ -203,16 +205,21 @@ public:
            ErrorResult& aRv);
   // Uses XPCOM Remove.
   void IndexedSetter(uint32_t aIndex, HTMLOptionElement* aOption,
                      ErrorResult& aRv)
   {
     mOptions->IndexedSetter(aIndex, aOption, aRv);
   }
 
+  static bool MatchSelectedOptions(nsIContent* aContent, int32_t, nsIAtom*,
+                                   void*);
+
+  nsIHTMLCollection* SelectedOptions();
+
   int32_t SelectedIndex() const
   {
     return mSelectedIndex;
   }
   void SetSelectedIndex(int32_t aIdx, ErrorResult& aRv)
   {
     aRv = SetSelectedIndexInternal(aIdx, true);
   }
@@ -549,16 +556,22 @@ protected:
   void VerifyOptionsArray();
 #endif
 
   nsresult SetSelectedIndexInternal(int32_t aIndex, bool aNotify);
 
   void SetSelectionChanged(bool aValue, bool aNotify);
 
   /**
+   * Marks the selectedOptions list as dirty, so that it'll populate itself
+   * again.
+   */
+  void UpdateSelectedOptions();
+
+  /**
    * Return whether an element should have a validity UI.
    * (with :-moz-ui-invalid and :-moz-ui-valid pseudo-classes).
    *
    * @return Whether the element should have a validity UI.
    */
   bool ShouldShowValidityUI() const {
     /**
      * Always show the validity UI if the form has already tried to be submitted
@@ -613,14 +626,19 @@ protected:
    * index if multiple are selected)
    */
   int32_t   mSelectedIndex;
   /**
    * The temporary restore state in case we try to restore before parser is
    * done adding options
    */
   nsCOMPtr<SelectState> mRestoreState;
+
+  /**
+   * The live list of selected options.
+  */
+  nsRefPtr<nsContentList> mSelectedOptions;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_HTMLSelectElement_h
--- a/content/html/content/test/forms/Makefile.in
+++ b/content/html/content/test/forms/Makefile.in
@@ -62,12 +62,13 @@ MOCHITEST_FILES = \
 		test_input_event.html \
 		test_input_number_rounding.html \
 		test_valueAsDate_pref.html \
 		test_input_color_picker_initial.html \
 		test_input_color_picker_update.html \
 		test_input_color_picker_popup.html \
 		test_input_color_input_change_events.html \
 		test_restore_form_elements.html \
+		test_select_selectedOptions.html \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
new file mode 100644
--- /dev/null
+++ b/content/html/content/test/forms/test_select_selectedOptions.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=596681
+-->
+<head>
+  <title>Test for HTMLSelectElement.selectedOptions</title>
+  <script type="application/javascript" src="/MochiKit/packed.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596681">Mozilla Bug 596681</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript;version=1.7">
+
+/** Test for HTMLSelectElement's selectedOptions attribute.
+ *
+ * selectedOptions is a live list of the options that have selectedness of true
+ * (not the selected content attribute).
+ *
+ * See http://www.whatwg.org/html/#dom-select-selectedoptions
+ **/
+
+function checkSelectedOptions(size, elements)
+{
+  is(selectedOptions.length, size,
+     "select should have " + size + " selected options");
+  for (let i = 0; i < size; ++i) {
+    ok(selectedOptions[i], "selected option is valid");
+    if (selectedOptions[i]) {
+      is(selectedOptions[i].value, elements[i].value, "selected options are correct");
+    }
+  }
+}
+
+let select = document.createElement("select");
+document.body.appendChild(select);
+let selectedOptions = select.selectedOptions;
+
+ok("selectedOptions" in select,
+   "select element should have a selectedOptions IDL attribute");
+
+ok(select.selectedOptions instanceof HTMLCollection,
+   "selectedOptions should be an HTMLCollection instance");
+
+let option1 = document.createElement("option");
+let option2 = document.createElement("option");
+let option3 = document.createElement("option");
+option1.id = "option1";
+option1.value = "option1";
+option2.value = "option2";
+option3.value = "option3";
+
+checkSelectedOptions(0, null);
+
+select.add(option1, null);
+is(selectedOptions.namedItem("option1").value, "option1", "named getter works");
+checkSelectedOptions(1, [option1]);
+
+select.add(option2, null);
+checkSelectedOptions(1, [option1]);
+
+select.options[1].selected = true;
+checkSelectedOptions(1, [option2]);
+
+select.multiple = true;
+checkSelectedOptions(1, [option2]);
+
+select.options[0].selected = true;
+checkSelectedOptions(2, [option1, option2]);
+
+option1.selected = false;
+// Usinig selected directly on the option should work.
+checkSelectedOptions(1, [option2]);
+
+select.remove(1);
+select.add(option2, 0);
+select.options[0].selected = true;
+select.options[1].selected = true;
+// Should be in tree order.
+checkSelectedOptions(2, [option2, option1]);
+
+select.add(option3, null);
+checkSelectedOptions(2, [option2, option1]);
+
+select.options[2].selected = true;
+checkSelectedOptions(3, [option2, option1, option3]);
+
+select.length = 0;
+option1.selected = false;
+option2.selected = false;
+option3.selected = false;
+var optgroup1 = document.createElement("optgroup");
+optgroup1.appendChild(option1);
+optgroup1.appendChild(option2);
+select.add(optgroup1)
+var optgroup2 = document.createElement("optgroup");
+optgroup2.appendChild(option3);
+select.add(optgroup2);
+
+checkSelectedOptions(0, null);
+
+option2.selected = true;
+checkSelectedOptions(1, [option2]);
+
+option3.selected = true;
+checkSelectedOptions(2, [option2, option3]);
+
+optgroup1.removeChild(option2);
+checkSelectedOptions(1, [option3]);
+
+document.body.removeChild(select);
+option1.selected = true;
+checkSelectedOptions(2, [option1, option3]);
+</script>
+</pre>
+</body>
+</html>
--- a/dom/interfaces/html/nsIDOMHTMLSelectElement.idl
+++ b/dom/interfaces/html/nsIDOMHTMLSelectElement.idl
@@ -14,17 +14,17 @@
  * http://www.w3.org/TR/DOM-Level-2-HTML/
  *
  * with changes from the work-in-progress WHATWG HTML specification:
  * http://www.whatwg.org/specs/web-apps/current-work/
  */
 
 interface nsIDOMValidityState;
 
-[scriptable, uuid(846578b2-6d4f-4399-86cc-2c05f19469d0)]
+[scriptable, uuid(d8914a2d-3556-4b66-911c-a84c4394e7fa)]
 interface nsIDOMHTMLSelectElement : nsISupports
 {
            attribute boolean                     autofocus;
            attribute boolean                     disabled;
   readonly attribute nsIDOMHTMLFormElement       form;
            attribute boolean                     multiple;
            attribute DOMString                   name;
            attribute unsigned long               size;
@@ -39,16 +39,17 @@ interface nsIDOMHTMLSelectElement : nsIS
   // since IDL doesn't support overfload.
   //   void add(in nsIDOMHTMLElement, [optional] in nsIDOMHTMLElement)
   //   void add(in nsIDOMHTMLElement, in long)
   void                      add(in nsIDOMHTMLElement element, 
                                 [optional] in nsIVariant before)
                                                      raises(DOMException);   
   void                      remove(in long index);
 
+  readonly attribute nsIDOMHTMLCollection  selectedOptions;
            attribute long                  selectedIndex;
            attribute DOMString             value;
 
   // The following lines are part of the constraint validation API, see:
   // http://www.whatwg.org/specs/web-apps/current-work/#the-constraint-validation-api
   readonly attribute boolean             willValidate;
   readonly attribute nsIDOMValidityState validity;
   readonly attribute DOMString           validationMessage;
--- a/dom/webidl/HTMLSelectElement.webidl
+++ b/dom/webidl/HTMLSelectElement.webidl
@@ -33,17 +33,17 @@ interface HTMLSelectElement : HTMLElemen
   getter Element? item(unsigned long index);
   HTMLOptionElement? namedItem(DOMString name);
   [Throws]
   void add((HTMLOptionElement or HTMLOptGroupElement) element, optional (HTMLElement or long)? before = null);
   void remove(long index);
   [Throws]
   setter creator void (unsigned long index, HTMLOptionElement? option);
 
-// NYI:  readonly attribute HTMLCollection selectedOptions;
+  readonly attribute HTMLCollection selectedOptions;
   [SetterThrows, Pure]
            attribute long selectedIndex;
   [Pure]
            attribute DOMString value;
 
   readonly attribute boolean willValidate;
   readonly attribute ValidityState validity;
   readonly attribute DOMString validationMessage;