Bug 571294 - Part 1: Implement selection events behind the dom.select_events.enabled pref, r=smaug
authorMichael Layzell <michael@thelayzells.com>
Wed, 12 Aug 2015 13:26:01 -0400
changeset 262215 24515d221f0a98f7e63e3f31eb9650ffacbbe82a
parent 262214 cd8b8017370cf14c472c107d256bb98ce7a19270
child 262216 41ffed8cc5d34e954a4c5c9bcd4371a4c84c2847
push id29366
push userphilringnalda@gmail.com
push dateSun, 13 Sep 2015 18:58:26 +0000
treeherdermozilla-central@9ed17db42e3e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs571294
milestone43.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 571294 - Part 1: Implement selection events behind the dom.select_events.enabled pref, r=smaug
dom/base/FragmentOrElement.cpp
dom/base/nsGkAtomList.h
dom/base/nsRange.cpp
dom/base/nsRange.h
dom/events/EventNameList.h
dom/webidl/Document.webidl
dom/webidl/EventHandler.webidl
editor/libeditor/nsTextEditRules.cpp
extensions/spellcheck/src/mozInlineSpellChecker.cpp
layout/generic/Selection.h
layout/generic/SelectionChangeListener.h
layout/generic/nsFrameSelection.h
layout/generic/nsSelection.cpp
modules/libpref/init/all.js
widget/EventMessageList.h
--- a/dom/base/FragmentOrElement.cpp
+++ b/dom/base/FragmentOrElement.cpp
@@ -809,31 +809,31 @@ nsIContent::PreHandleEvent(EventChainPre
       case eImageAbort:
       case eLoadError:
       case eFormSelect:
       case eFormChange:
       case eLoad:
       case eFormReset:
       case eResize:
       case eScroll:
+      case NS_SELECT_START:
         stopEvent = true;
         break;
       case eUnidentifiedEvent:
         if (aVisitor.mDOMEvent) {
           nsAutoString eventType;
           aVisitor.mDOMEvent->GetType(eventType);
           if (eventType.EqualsLiteral("abort") ||
               eventType.EqualsLiteral("error") ||
               eventType.EqualsLiteral("select") ||
               eventType.EqualsLiteral("change") ||
               eventType.EqualsLiteral("load") ||
               eventType.EqualsLiteral("reset") ||
               eventType.EqualsLiteral("resize") ||
-              eventType.EqualsLiteral("scroll") ||
-              eventType.EqualsLiteral("selectstart")) {
+              eventType.EqualsLiteral("scroll")) {
             stopEvent = true;
           }
         }
         break;
       default:
         break;
     }
 
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -877,16 +877,18 @@ GK_ATOM(onrequestmediaplaystatus, "onreq
 GK_ATOM(onreset, "onreset")
 GK_ATOM(onresuming, "onresuming")
 GK_ATOM(onresize, "onresize")
 GK_ATOM(onrtchange, "onrtchange")
 GK_ATOM(onscanningstatechanged, "onscanningstatechanged")
 GK_ATOM(onscostatuschanged, "onscostatuschanged")
 GK_ATOM(onscroll, "onscroll")
 GK_ATOM(onselect, "onselect")
+GK_ATOM(onselectionchange, "onselectionchange")
+GK_ATOM(onselectstart, "onselectstart")
 GK_ATOM(onsending, "onsending")
 GK_ATOM(onsent, "onsent")
 GK_ATOM(onset, "onset")
 GK_ATOM(onshow, "onshow")
 GK_ATOM(onshutter, "onshutter")
 GK_ATOM(onstatechange, "onstatechange")
 GK_ATOM(onstatuschanged, "onstatuschanged")
 GK_ATOM(onstkcommand, "onstkcommand")
--- a/dom/base/nsRange.cpp
+++ b/dom/base/nsRange.cpp
@@ -26,16 +26,17 @@
 #include "nsGenericDOMDataNode.h"
 #include "nsTextFrame.h"
 #include "nsFontFaceList.h"
 #include "mozilla/dom/DocumentFragment.h"
 #include "mozilla/dom/DocumentType.h"
 #include "mozilla/dom/RangeBinding.h"
 #include "mozilla/dom/DOMRect.h"
 #include "mozilla/dom/ShadowRoot.h"
+#include "mozilla/dom/Selection.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/Likely.h"
 #include "nsCSSFrameConstructor.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 JSObject*
@@ -189,16 +190,36 @@ nsRange::~nsRange()
 
   // Maybe we can remove Detach() -- bug 702948.
   Telemetry::Accumulate(Telemetry::DOM_RANGE_DETACHED, mIsDetached);
 
   // we want the side effects (releases and list removals)
   DoSetRange(nullptr, 0, nullptr, 0, nullptr);
 }
 
+nsRange::nsRange(nsINode* aNode)
+  : mRoot(nullptr)
+  , mStartOffset(0)
+  , mEndOffset(0)
+  , mIsPositioned(false)
+  , mIsDetached(false)
+  , mMaySpanAnonymousSubtrees(false)
+  , mIsGenerated(false)
+  , mStartOffsetWasIncremented(false)
+  , mEndOffsetWasIncremented(false)
+  , mEnableGravitationOnElementRemoval(true)
+#ifdef DEBUG
+  , mAssertNextInsertOrAppendIndex(-1)
+  , mAssertNextInsertOrAppendNode(nullptr)
+#endif
+{
+  MOZ_ASSERT(aNode, "range isn't in a document!");
+  mOwner = aNode->OwnerDoc();
+}
+
 /* static */
 nsresult
 nsRange::CreateRange(nsINode* aStartParent, int32_t aStartOffset,
                      nsINode* aEndParent, int32_t aEndOffset,
                      nsRange** aRange)
 {
   nsCOMPtr<nsIDOMNode> startDomNode = do_QueryInterface(aStartParent);
   nsCOMPtr<nsIDOMNode> endDomNode = do_QueryInterface(aEndParent);
@@ -264,23 +285,28 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(nsRange)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsRange)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner);
   tmp->Reset();
+
+  // This needs to be unlinked after Reset() is called, as it controls
+  // the result of IsInSelection() which is used by tmp->Reset().
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelection);
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsRange)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStartParent)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndParent)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelection)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsRange)
   NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_TRACE_END
 
 static void MarkDescendants(nsINode* aNode)
@@ -874,34 +900,62 @@ nsRange::DoSetRange(nsINode* aStartN, in
     if (newCommonAncestor != oldCommonAncestor) {
       if (oldCommonAncestor) {
         UnregisterCommonAncestor(oldCommonAncestor);
       }
       if (newCommonAncestor) {
         RegisterCommonAncestor(newCommonAncestor);
       } else {
         NS_ASSERTION(!mIsPositioned, "unexpected disconnected nodes");
-        mInSelection = false;
+        mSelection = nullptr;
       }
     }
   }
 
-  // This needs to be the last thing this function does.  See comment
-  // in ParentChainChanged.
+  // This needs to be the last thing this function does, other than notifying
+  // selection listeners. See comment in ParentChainChanged.
   mRoot = aRoot;
+
+  // Notify any selection listeners. This has to occur last because otherwise the world
+  // could be observed by a selection listener while the range was in an invalid state.
+  if (mSelection) {
+    mSelection->NotifySelectionListeners();
+  }
 }
 
 static int32_t
 IndexOf(nsINode* aChild)
 {
   nsINode* parent = aChild->GetParentNode();
 
   return parent ? parent->IndexOf(aChild) : -1;
 }
 
+void
+nsRange::SetSelection(mozilla::dom::Selection* aSelection)
+{
+  if (mSelection == aSelection) {
+    return;
+  }
+  // At least one of aSelection and mSelection must be null
+  // aSelection will be null when we are removing from a selection
+  // and a range can't be in more than one selection at a time,
+  // thus mSelection must be null too.
+  MOZ_ASSERT(!aSelection || !mSelection);
+
+  mSelection = aSelection;
+  nsINode* commonAncestor = GetCommonAncestor();
+  NS_ASSERTION(commonAncestor, "unexpected disconnected nodes");
+  if (mSelection) {
+    RegisterCommonAncestor(commonAncestor);
+  } else {
+    UnregisterCommonAncestor(commonAncestor);
+  }
+}
+
 nsINode*
 nsRange::GetCommonAncestor() const
 {
   return mIsPositioned ?
     nsContentUtils::GetCommonAncestor(mStartParent, mEndParent) :
     nullptr;
 }
 
--- a/dom/base/nsRange.h
+++ b/dom/base/nsRange.h
@@ -23,50 +23,32 @@
 #include "mozilla/Attributes.h"
 
 namespace mozilla {
 class ErrorResult;
 namespace dom {
 class DocumentFragment;
 class DOMRect;
 class DOMRectList;
+class Selection;
 } // namespace dom
 } // namespace mozilla
 
 class nsRange final : public nsIDOMRange,
                       public nsStubMutationObserver,
                       public nsWrapperCache
 {
   typedef mozilla::ErrorResult ErrorResult;
   typedef mozilla::dom::DOMRect DOMRect;
   typedef mozilla::dom::DOMRectList DOMRectList;
 
   virtual ~nsRange();
 
 public:
-  explicit nsRange(nsINode* aNode)
-    : mRoot(nullptr)
-    , mStartOffset(0)
-    , mEndOffset(0)
-    , mIsPositioned(false)
-    , mIsDetached(false)
-    , mMaySpanAnonymousSubtrees(false)
-    , mInSelection(false)
-    , mIsGenerated(false)
-    , mStartOffsetWasIncremented(false)
-    , mEndOffsetWasIncremented(false)
-    , mEnableGravitationOnElementRemoval(true)
-#ifdef DEBUG
-    , mAssertNextInsertOrAppendIndex(-1)
-    , mAssertNextInsertOrAppendNode(nullptr)
-#endif
-  {
-    MOZ_ASSERT(aNode, "range isn't in a document!");
-    mOwner = aNode->OwnerDoc();
-  }
+  explicit nsRange(nsINode* aNode);
 
   static nsresult CreateRange(nsIDOMNode* aStartParent, int32_t aStartOffset,
                               nsIDOMNode* aEndParent, int32_t aEndOffset,
                               nsRange** aRange);
   static nsresult CreateRange(nsIDOMNode* aStartParent, int32_t aStartOffset,
                               nsIDOMNode* aEndParent, int32_t aEndOffset,
                               nsIDOMRange** aRange);
   static nsresult CreateRange(nsINode* aStartParent, int32_t aStartOffset,
@@ -124,41 +106,28 @@ public:
   }
 
   void SetMaySpanAnonymousSubtrees(bool aMaySpanAnonymousSubtrees)
   {
     mMaySpanAnonymousSubtrees = aMaySpanAnonymousSubtrees;
   }
   
   /**
-   * Return true iff this range is part of at least one Selection object
+   * Return true iff this range is part of a Selection object
    * and isn't detached.
    */
   bool IsInSelection() const
   {
-    return mInSelection;
+    return !!mSelection;
   }
 
   /**
    * Called when the range is added/removed from a Selection.
    */
-  void SetInSelection(bool aInSelection)
-  {
-    if (mInSelection == aInSelection) {
-      return;
-    }
-    mInSelection = aInSelection;
-    nsINode* commonAncestor = GetCommonAncestor();
-    NS_ASSERTION(commonAncestor, "unexpected disconnected nodes");
-    if (mInSelection) {
-      RegisterCommonAncestor(commonAncestor);
-    } else {
-      UnregisterCommonAncestor(commonAncestor);
-    }
-  }
+  void SetSelection(mozilla::dom::Selection* aSelection);
 
   /**
    * Return true if this range was generated.
    * @see SetIsGenerated
    */
   bool IsGenerated() const
   {
     return mIsGenerated;
@@ -344,23 +313,23 @@ protected:
 #endif
     static bool mIsNested;
   };
 
   nsCOMPtr<nsIDocument> mOwner;
   nsCOMPtr<nsINode> mRoot;
   nsCOMPtr<nsINode> mStartParent;
   nsCOMPtr<nsINode> mEndParent;
+  nsRefPtr<mozilla::dom::Selection> mSelection;
   int32_t mStartOffset;
   int32_t mEndOffset;
 
   bool mIsPositioned : 1;
   bool mIsDetached : 1;
   bool mMaySpanAnonymousSubtrees : 1;
-  bool mInSelection : 1;
   bool mIsGenerated : 1;
   bool mStartOffsetWasIncremented : 1;
   bool mEndOffsetWasIncremented : 1;
   bool mEnableGravitationOnElementRemoval : 1;
 #ifdef DEBUG
   int32_t  mAssertNextInsertOrAppendIndex;
   nsINode* mAssertNextInsertOrAppendNode;
 #endif
--- a/dom/events/EventNameList.h
+++ b/dom/events/EventNameList.h
@@ -345,16 +345,20 @@ EVENT(pointerleave,
 EVENT(gotpointercapture,
       ePointerGotCapture,
       EventNameType_All,
       ePointerEventClass)
 EVENT(lostpointercapture,
       ePointerLostCapture,
       EventNameType_All,
       ePointerEventClass)
+EVENT(selectstart,
+      NS_SELECT_START,
+      EventNameType_HTMLXUL,
+      eBasicEventClass)
 
 // Not supported yet; probably never because "wheel" is a better idea.
 // EVENT(mousewheel)
 EVENT(pause,
       ePause,
       EventNameType_HTML,
       eBasicEventClass)
 EVENT(play,
@@ -579,16 +583,20 @@ TOUCH_EVENT(touchcancel,
             NS_TOUCH_CANCEL,
             EventNameType_All,
             eTouchEventClass)
 
 DOCUMENT_ONLY_EVENT(readystatechange,
                     eReadyStateChange,
                     EventNameType_HTMLXUL,
                     eBasicEventClass)
+DOCUMENT_ONLY_EVENT(selectionchange,
+                    NS_SELECTION_CHANGE,
+                    EventNameType_HTMLXUL,
+                    eBasicEventClass)
 
 NON_IDL_EVENT(MozMouseHittest,
               eMouseHitTest,
               EventNameType_None,
               eMouseEventClass)
 
 NON_IDL_EVENT(DOMAttrModified,
               eLegacyAttrModified,
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -146,16 +146,19 @@ partial interface Document {
 
   // Gecko extensions?
                 attribute EventHandler onwheel;
                 attribute EventHandler oncopy;
                 attribute EventHandler oncut;
                 attribute EventHandler onpaste;
                 attribute EventHandler onbeforescriptexecute;
                 attribute EventHandler onafterscriptexecute;
+
+                [Pref="dom.select_events.enabled"]
+                attribute EventHandler onselectionchange;
   /**
    * True if this document is synthetic : stand alone image, video, audio file,
    * etc.
    */
   [Func="IsChromeOrXBL"] readonly attribute boolean mozSyntheticDocument;
   /**
    * Returns the script element whose script is currently being processed.
    *
--- a/dom/webidl/EventHandler.webidl
+++ b/dom/webidl/EventHandler.webidl
@@ -84,16 +84,19 @@ interface GlobalEventHandlers {
            //(Not implemented)attribute EventHandler onsort;
            attribute EventHandler onstalled;
            attribute EventHandler onsubmit;
            attribute EventHandler onsuspend;
            attribute EventHandler ontimeupdate;
            attribute EventHandler onvolumechange;
            attribute EventHandler onwaiting;
 
+           [Pref="dom.select_events.enabled"]
+           attribute EventHandler onselectstart;
+
            // Pointer events handlers
            [Pref="dom.w3c_pointer_events.enabled"]
            attribute EventHandler onpointercancel;
            [Pref="dom.w3c_pointer_events.enabled"]
            attribute EventHandler onpointerdown;
            [Pref="dom.w3c_pointer_events.enabled"]
            attribute EventHandler onpointerup;
            [Pref="dom.w3c_pointer_events.enabled"]
--- a/editor/libeditor/nsTextEditRules.cpp
+++ b/editor/libeditor/nsTextEditRules.cpp
@@ -826,16 +826,24 @@ nsTextEditRules::WillDeleteSelection(Sel
 
   // if there is only bogus content, cancel the operation
   if (mBogusNode) {
     *aCancel = true;
     return NS_OK;
   }
 
   nsresult res = NS_OK;
+  // If the current selection is empty (e.g the user presses backspace with
+  // a collapsed selection), then we want to avoid sending the selectstart
+  // event to the user, so we hide selection changes. However, we still
+  // want to send a single selectionchange event to the document, so we
+  // batch the selectionchange events, such that a single event fires after
+  // the AutoHideSelectionChanges destructor has been run.
+  SelectionBatcher selectionBatcher(aSelection);
+  AutoHideSelectionChanges hideSelection(aSelection);
   nsAutoScriptBlocker scriptBlocker;
 
   if (IsPasswordEditor())
   {
     NS_ENSURE_STATE(mEditor);
     res = mEditor->ExtendSelectionForDelete(aSelection, &aCollapsedAction);
     NS_ENSURE_SUCCESS(res, res);
 
--- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp
+++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp
@@ -941,23 +941,30 @@ mozInlineSpellChecker::ReplaceWord(nsIDO
   NS_ENSURE_TRUE(newword.Length() != 0, NS_ERROR_FAILURE);
 
   nsCOMPtr<nsIDOMRange> range;
   nsresult res = GetMisspelledWord(aNode, aOffset, getter_AddRefs(range));
   NS_ENSURE_SUCCESS(res, res); 
 
   if (range)
   {
+    // This range was retrieved from the spellchecker selection. As
+    // ranges cannot be shared between selections, we must clone it
+    // before adding it to the editor's selection.
+    nsCOMPtr<nsIDOMRange> editorRange;
+    res = range->CloneRange(getter_AddRefs(editorRange));
+    NS_ENSURE_SUCCESS(res, res);
+
     nsAutoPlaceHolderBatch phb(editor, nullptr);
   
     nsCOMPtr<nsISelection> selection;
     res = editor->GetSelection(getter_AddRefs(selection));
     NS_ENSURE_SUCCESS(res, res);
     selection->RemoveAllRanges();
-    selection->AddRange(range);
+    selection->AddRange(editorRange);
     editor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
 
     nsCOMPtr<nsIPlaintextEditor> textEditor(do_QueryReferent(mEditor));
     if (textEditor)
       textEditor->InsertText(newword);
   }
 
   return NS_OK;
--- a/layout/generic/Selection.h
+++ b/layout/generic/Selection.h
@@ -103,17 +103,17 @@ public:
   nsresult      SubtractRange(RangeData* aRange, nsRange* aSubtract,
                               nsTArray<RangeData>* aOutput);
   /**
    * AddItem adds aRange to this Selection.  If mApplyUserSelectStyle is true,
    * then aRange is first scanned for -moz-user-select:none nodes and split up
    * into multiple ranges to exclude those before adding the resulting ranges
    * to this Selection.
    */
-  nsresult      AddItem(nsRange* aRange, int32_t* aOutIndex);
+  nsresult      AddItem(nsRange* aRange, int32_t* aOutIndex, bool aNoStartSelect = false);
   nsresult      RemoveItem(nsRange* aRange);
   nsresult      RemoveCollapsedRanges();
   nsresult      Clear(nsPresContext* aPresContext);
   nsresult      Collapse(nsINode* aParentNode, int32_t aOffset);
   nsresult      Extend(nsINode* aParentNode, int32_t aOffset);
   nsRange*      GetRangeAt(int32_t aIndex);
 
   // Get the anchor-to-focus range if we don't care which end is
@@ -199,16 +199,19 @@ public:
                             bool aAllowAdjacent,
                             nsTArray<nsRefPtr<nsRange>>& aReturn,
                             mozilla::ErrorResult& aRv);
 
   void ScrollIntoView(int16_t aRegion, bool aIsSynchronous,
                       int16_t aVPercent, int16_t aHPercent,
                       mozilla::ErrorResult& aRv);
 
+  void AddSelectionChangeBlocker();
+  void RemoveSelectionChangeBlocker();
+  bool IsBlockingSelectionChangeEvents() const;
 private:
   friend class ::nsAutoScrollTimer;
 
   // Note: DoAutoScroll might destroy arbitrary frames etc.
   nsresult DoAutoScroll(nsIFrame *aFrame, nsPoint& aPoint);
 
 public:
   SelectionType GetType(){return mType;}
@@ -224,16 +227,17 @@ public:
       : mSavedValue(aSelection->mApplyUserSelectStyle)
     {
       MOZ_GUARD_OBJECT_NOTIFIER_INIT;
       aSelection->mApplyUserSelectStyle = true;
     }
     AutoRestore<bool> mSavedValue;
     MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
   };
+
 private:
   friend struct mozilla::AutoPrepareFocusRange;
   class ScrollSelectionIntoViewEvent;
   friend class ScrollSelectionIntoViewEvent;
 
   class ScrollSelectionIntoViewEvent : public nsRunnable {
   public:
     NS_DECL_NSIRUNNABLE
@@ -277,16 +281,18 @@ private:
                             nsINode* aEndNode, int32_t aEndOffset,
                             int32_t aRangeIndex);
   nsresult GetIndicesForInterval(nsINode* aBeginNode, int32_t aBeginOffset,
                                  nsINode* aEndNode, int32_t aEndOffset,
                                  bool aAllowAdjacent,
                                  int32_t* aStartIndex, int32_t* aEndIndex);
   RangeData* FindRangeData(nsIDOMRange* aRange);
 
+  void UserSelectRangesToAdd(nsRange* aItem, nsTArray<nsRefPtr<nsRange> >& rangesToAdd);
+
   /**
    * Helper method for AddItem.
    */
   nsresult AddItemInternal(nsRange* aRange, int32_t* aOutIndex);
 
   // These are the ranges inside this selection. They are kept sorted in order
   // of DOM start position.
   //
@@ -307,19 +313,24 @@ private:
   nsRefPtr<nsAutoScrollTimer> mAutoScrollTimer;
   nsCOMArray<nsISelectionListener> mSelectionListeners;
   nsRevocableEventPtr<ScrollSelectionIntoViewEvent> mScrollEvent;
   CachedOffsetForFrame *mCachedOffsetForFrame;
   nsDirection mDirection;
   SelectionType mType;
   /**
    * True if the current selection operation was initiated by user action.
-   * It determines whether we exclude -moz-user-select:none nodes or not.
+   * It determines whether we exclude -moz-user-select:none nodes or not,
+   * as well as whether selectstart events will be fired.
    */
   bool mApplyUserSelectStyle;
+
+  // Non-zero if we don't want any changes we make to the selection to be
+  // visible to content. If non-zero, content won't be notified about changes.
+  uint32_t mSelectionChangeBlockerCount;
 };
 
 // Stack-class to turn on/off selection batching.
 class MOZ_STACK_CLASS SelectionBatcher final
 {
 private:
   nsRefPtr<Selection> mSelection;
 public:
@@ -334,12 +345,39 @@ public:
   ~SelectionBatcher()
   {
     if (mSelection) {
       mSelection->EndBatchChanges();
     }
   }
 };
 
+class MOZ_STACK_CLASS AutoHideSelectionChanges final
+{
+private:
+  nsRefPtr<Selection> mSelection;
+  MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
+public:
+  explicit AutoHideSelectionChanges(const nsFrameSelection* aFrame);
+
+  explicit AutoHideSelectionChanges(Selection* aSelection
+                                    MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
+    : mSelection(aSelection)
+  {
+    MOZ_GUARD_OBJECT_NOTIFIER_INIT;
+    mSelection = aSelection;
+    if (mSelection) {
+      mSelection->AddSelectionChangeBlocker();
+    }
+  }
+
+  ~AutoHideSelectionChanges()
+  {
+    if (mSelection) {
+      mSelection->RemoveSelectionChangeBlocker();
+    }
+  }
+};
+
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_Selection_h__
new file mode 100644
--- /dev/null
+++ b/layout/generic/SelectionChangeListener.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_SelectionChangeListener_h_
+#define mozilla_SelectionChangeListener_h_
+
+#include "nsISelectionListener.h"
+#include "nsISelectionPrivate.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+namespace dom {
+
+class SelectionChangeListener final : public nsISelectionListener
+{
+public:
+  // SelectionChangeListener has to participate in cycle collection because
+  // it holds strong references to nsINodes in its mOldRanges array.
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS(SelectionChangeListener)
+  NS_DECL_NSISELECTIONLISTENER
+
+  // This field is used to keep track of the ranges which were present in the
+  // selection when the selectionchange event was previously fired. This allows
+  // for the selectionchange event to only be fired when a selection is actually
+  // changed.
+  struct RawRangeData
+  {
+    // These properties are not void*s to avoid the potential situation where the
+    // nsINode is freed, and a new nsINode is allocated with the same address, which
+    // could potentially break the comparison logic. In reality, this is extremely
+    // unlikely to occur (potentially impossible), but these nsCOMPtrs are safer.
+    // They are never dereferenced.
+    nsCOMPtr<nsINode> mStartParent;
+    nsCOMPtr<nsINode> mEndParent;
+
+    // XXX These are int32_ts on nsRange, but uint32_ts in the return value
+    // of GetStart_, so I use uint32_ts here. See bug 1194256.
+    uint32_t mStartOffset;
+    uint32_t mEndOffset;
+
+    explicit RawRangeData(const nsRange* aRange);
+    bool Equals(const nsRange* aRange);
+  };
+
+private:
+  nsTArray<RawRangeData> mOldRanges;
+
+  ~SelectionChangeListener() {}
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_SelectionChangeListener_h_
--- a/layout/generic/nsFrameSelection.h
+++ b/layout/generic/nsFrameSelection.h
@@ -747,11 +747,13 @@ private:
   bool mChangesDuringBatching;
   bool mNotifyFrames;
   bool mDragSelectingCells;
   bool mDragState;   //for drag purposes
   bool mMouseDoubleDownState; //has the doubleclick down happened
   bool mDesiredPosSet;
 
   int8_t mCaretMovementStyle;
+
+  static bool sSelectionEventsEnabled;
 };
 
 #endif /* nsFrameSelection_h___ */
--- a/layout/generic/nsSelection.cpp
+++ b/layout/generic/nsSelection.cpp
@@ -63,27 +63,29 @@ static NS_DEFINE_CID(kFrameTraversalCID,
 #include "nsITimer.h"
 #include "nsFrameManager.h"
 // notifications
 #include "nsIDOMDocument.h"
 #include "nsIDocument.h"
 
 #include "nsISelectionController.h"//for the enums
 #include "nsAutoCopyListener.h"
+#include "SelectionChangeListener.h"
 #include "nsCopySupport.h"
 #include "nsIClipboard.h"
 #include "nsIFrameInlines.h"
 
 #include "nsIBidiKeyboard.h"
 
 #include "nsError.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/ShadowRoot.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/SelectionBinding.h"
+#include "mozilla/AsyncEventDispatcher.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 //#define DEBUG_TABLE 1
 
 static bool IsValidSelectionPoint(nsFrameSelection *aFrameSel, nsINode *aNode);
 
@@ -407,17 +409,17 @@ struct MOZ_RAII AutoPrepareFocusRange
     aSelection->mAnchorFocusRange = range;
 
     // Remove all generated ranges (including the old mAnchorFocusRange).
     nsRefPtr<nsPresContext> presContext = aSelection->GetPresContext();
     size_t i = len;
     while (i--) {
       range = aSelection->mRanges[i].mRange;
       if (range->IsGenerated()) {
-        range->SetInSelection(false);
+        range->SetSelection(nullptr);
         aSelection->selectFrames(presContext, range, false);
         aSelection->mRanges.RemoveElementAt(i);
       }
     }
     if (aSelection->mFrameSelection) {
       aSelection->mFrameSelection->InvalidateDesiredPos();
     }
   }
@@ -809,16 +811,26 @@ void
 nsFrameSelection::Init(nsIPresShell *aShell, nsIContent *aLimiter)
 {
   mShell = aShell;
   mDragState = false;
   mDesiredPosSet = false;
   mLimiter = aLimiter;
   mCaretMovementStyle =
     Preferences::GetInt("bidi.edit.caret_movement_style", 2);
+
+  // This should only ever be initialized on the main thread, so we are OK here.
+  static bool prefCachesInitialized = false;
+  if (!prefCachesInitialized) {
+    prefCachesInitialized = true;
+
+    Preferences::AddBoolVarCache(&sSelectionEventsEnabled,
+                                 "dom.select_events.enabled", false);
+  }
+
   // Set touch caret as selection listener
   nsRefPtr<TouchCaret> touchCaret = mShell->GetTouchCaret();
   if (touchCaret) {
     int8_t index = GetIndexFromSelectionType(nsISelectionController::SELECTION_NORMAL);
     if (mDomSelections[index]) {
       mDomSelections[index]->AddSelectionListener(touchCaret);
     }
   }
@@ -834,17 +846,30 @@ nsFrameSelection::Init(nsIPresShell *aSh
 
   nsRefPtr<AccessibleCaretEventHub> eventHub = mShell->GetAccessibleCaretEventHub();
   if (eventHub) {
     int8_t index = GetIndexFromSelectionType(nsISelectionController::SELECTION_NORMAL);
     if (mDomSelections[index]) {
       mDomSelections[index]->AddSelectionListener(eventHub);
     }
   }
-}
+
+  if (sSelectionEventsEnabled) {
+    int8_t index =
+      GetIndexFromSelectionType(nsISelectionController::SELECTION_NORMAL);
+    if (mDomSelections[index]) {
+      // The Selection instance will hold a strong reference to its selectionchangelistener
+      // so we don't have to worry about that!
+      nsRefPtr<SelectionChangeListener> listener = new SelectionChangeListener;
+      mDomSelections[index]->AddSelectionListener(listener);
+    }
+  }
+}
+
+bool nsFrameSelection::sSelectionEventsEnabled = false;
 
 nsresult
 nsFrameSelection::MoveCaret(nsDirection       aDirection,
                             bool              aContinueSelection,
                             nsSelectionAmount aAmount,
                             CaretMovementStyle aMovementStyle)
 {
   bool visualMovement = aMovementStyle == eVisual ||
@@ -3302,35 +3327,37 @@ nsFrameSelection::DisconnectFromPresShel
 
 // note: this can return a nil anchor node
 
 Selection::Selection()
   : mCachedOffsetForFrame(nullptr)
   , mDirection(eDirNext)
   , mType(nsISelectionController::SELECTION_NORMAL)
   , mApplyUserSelectStyle(false)
+  , mSelectionChangeBlockerCount(0)
 {
 }
 
 Selection::Selection(nsFrameSelection* aList)
   : mFrameSelection(aList)
   , mCachedOffsetForFrame(nullptr)
   , mDirection(eDirNext)
   , mType(nsISelectionController::SELECTION_NORMAL)
   , mApplyUserSelectStyle(false)
+  , mSelectionChangeBlockerCount(0)
 {
 }
 
 Selection::~Selection()
 {
   setAnchorFocusRange(-1);
 
   uint32_t count = mRanges.Length();
   for (uint32_t i = 0; i < count; ++i) {
-    mRanges[i].mRange->SetInSelection(false);
+    mRanges[i].mRange->SetSelection(nullptr);
   }
 
   if (mAutoScrollTimer) {
     mAutoScrollTimer->Stop();
     mAutoScrollTimer = nullptr;
   }
 
   mScrollEvent.Revoke();
@@ -3642,40 +3669,90 @@ Selection::SubtractRange(RangeData* aRan
         return NS_ERROR_OUT_OF_MEMORY;
       (*aOutput)[0].mTextRangeStyle = aRange->mTextRangeStyle;
     }
   }
 
   return NS_OK;
 }
 
+void
+Selection::UserSelectRangesToAdd(nsRange* aItem, nsTArray<nsRefPtr<nsRange>>& aRangesToAdd)
+{
+  aItem->ExcludeNonSelectableNodes(&aRangesToAdd);
+  if (aRangesToAdd.IsEmpty()) {
+    ErrorResult err;
+    nsINode* node = aItem->GetStartContainer(err);
+    if (node && node->IsContent() && node->AsContent()->GetEditingHost()) {
+      // A contenteditable node with user-select:none, for example.
+      // Allow it to have a collapsed selection (for the caret).
+      aItem->Collapse(GetDirection() == eDirPrevious);
+      aRangesToAdd.AppendElement(aItem);
+    }
+  }
+}
+
 nsresult
-Selection::AddItem(nsRange* aItem, int32_t* aOutIndex)
+Selection::AddItem(nsRange* aItem, int32_t* aOutIndex, bool aNoStartSelect)
 {
   if (!aItem)
     return NS_ERROR_NULL_POINTER;
   if (!aItem->IsPositioned())
     return NS_ERROR_UNEXPECTED;
 
   NS_ASSERTION(aOutIndex, "aOutIndex can't be null");
 
+  // XXX Rename mApplyUserSelectStyle? Not the best name (as it is also being
+  // used to detect here whether the event is user initiated for the purposes of
+  // dispatching the selectstart event).
   if (mApplyUserSelectStyle) {
     nsAutoTArray<nsRefPtr<nsRange>, 4> rangesToAdd;
-    aItem->ExcludeNonSelectableNodes(&rangesToAdd);
-    if (rangesToAdd.IsEmpty()) {
-      ErrorResult err;
-      nsINode* node = aItem->GetStartContainer(err);
-      if (node && node->IsContent() && node->AsContent()->GetEditingHost()) {
-        // A contenteditable node with user-select:none, for example.
-        // Allow it to have a collapsed selection (for the caret).
-        aItem->Collapse(GetDirection() == eDirPrevious);
-        rangesToAdd.AppendElement(aItem);
+    *aOutIndex = -1;
+
+    if (!aNoStartSelect && mType == nsISelectionController::SELECTION_NORMAL &&
+        nsFrameSelection::sSelectionEventsEnabled && Collapsed() &&
+        !IsBlockingSelectionChangeEvents()) {
+      // First, we generate the ranges to add with a scratch range, which is a
+      // clone of the original range passed in. We do this seperately, because the
+      // selectstart event could have caused the world to change, and required
+      // ranges to be re-generated
+      nsRefPtr<nsRange> scratchRange = aItem->CloneRange();
+      UserSelectRangesToAdd(scratchRange, rangesToAdd);
+      bool newRangesNonEmpty = rangesToAdd.Length() > 1 ||
+        (rangesToAdd.Length() == 1 && !rangesToAdd[0]->Collapsed());
+
+      MOZ_ASSERT(!newRangesNonEmpty || nsContentUtils::IsSafeToRunScript());
+      if (newRangesNonEmpty && nsContentUtils::IsSafeToRunScript()) {
+        // We consider a selection to be starting if we are currently collapsed,
+        // and the selection is becoming uncollapsed, and this is caused by a user
+        // initiated event.
+        bool defaultAction = true;
+        nsContentUtils::DispatchTrustedEvent(GetParentObject(),
+                                             aItem->GetStartParent(),
+                                             NS_LITERAL_STRING("selectstart"),
+                                             true, true, &defaultAction);
+
+        if (!defaultAction) {
+          return NS_OK;
+        }
+
+        // As we just dispatched an event to the DOM, something could have
+        // changed under our feet. Re-generate the rangesToAdd array, and ensure
+        // that the range we are about to add is still valid.
+        if (!aItem->IsPositioned()) {
+          return NS_ERROR_UNEXPECTED;
+        }
       }
+
+      // The scratch ranges we generated may be invalid now, throw them out
+      rangesToAdd.ClearAndRetainStorage();
     }
-    *aOutIndex = -1;
+
+    // Generate the ranges to add
+    UserSelectRangesToAdd(aItem, rangesToAdd);
     size_t newAnchorFocusIndex =
       GetDirection() == eDirPrevious ? 0 : rangesToAdd.Length() - 1;
     for (size_t i = 0; i < rangesToAdd.Length(); ++i) {
       int32_t index;
       nsresult rv = AddItemInternal(rangesToAdd[i], &index);
       NS_ENSURE_SUCCESS(rv, rv);
       if (i == newAnchorFocusIndex) {
         *aOutIndex = index;
@@ -3697,17 +3774,17 @@ Selection::AddItemInternal(nsRange* aIte
   MOZ_ASSERT(aOutIndex);
 
   *aOutIndex = -1;
 
   // a common case is that we have no ranges yet
   if (mRanges.Length() == 0) {
     if (!mRanges.AppendElement(RangeData(aItem)))
       return NS_ERROR_OUT_OF_MEMORY;
-    aItem->SetInSelection(true);
+    aItem->SetSelection(this);
 
     *aOutIndex = 0;
     return NS_OK;
   }
 
   int32_t startIndex, endIndex;
   nsresult rv = GetIndicesForInterval(aItem->GetStartParent(),
                                       aItem->StartOffset(),
@@ -3737,17 +3814,17 @@ Selection::AddItemInternal(nsRange* aIte
     *aOutIndex = startIndex;
     return NS_OK;
   }
 
   if (startIndex == endIndex) {
     // The new range doesn't overlap any existing ranges
     if (!mRanges.InsertElementAt(startIndex, RangeData(aItem)))
       return NS_ERROR_OUT_OF_MEMORY;
-    aItem->SetInSelection(true);
+    aItem->SetSelection(this);
     *aOutIndex = startIndex;
     return NS_OK;
   }
 
   // We now know that at least 1 existing range overlaps with the range that
   // we are trying to add. In fact, the only ranges of interest are those at
   // the two end points, startIndex and endIndex - 1 (which may point to the
   // same range) as these may partially overlap the new range. Any ranges
@@ -3759,17 +3836,17 @@ Selection::AddItemInternal(nsRange* aIte
 
   if (endIndex - 1 != startIndex) {
     if (!overlaps.InsertElementAt(1, mRanges[endIndex - 1]))
       return NS_ERROR_OUT_OF_MEMORY;
   }
 
   // Remove all the overlapping ranges
   for (int32_t i = startIndex; i < endIndex; ++i) {
-    mRanges[i].mRange->SetInSelection(false);
+    mRanges[i].mRange->SetSelection(nullptr);
   }
   mRanges.RemoveElementsAt(startIndex, endIndex - startIndex);
 
   nsTArray<RangeData> temp;
   for (int32_t i = overlaps.Length() - 1; i >= 0; i--) {
     nsresult rv = SubtractRange(&overlaps[i], aItem, &temp);
     NS_ENSURE_SUCCESS(rv, rv);
   }
@@ -3784,17 +3861,17 @@ Selection::AddItemInternal(nsRange* aIte
   if (!temp.InsertElementAt(insertionPoint, RangeData(aItem)))
     return NS_ERROR_OUT_OF_MEMORY;
 
   // Merge the leftovers back in to mRanges
   if (!mRanges.InsertElementsAt(startIndex, temp))
     return NS_ERROR_OUT_OF_MEMORY;
 
   for (uint32_t i = 0; i < temp.Length(); ++i) {
-    temp[i].mRange->SetInSelection(true);
+    temp[i].mRange->SetSelection(this);
   }
 
   *aOutIndex = startIndex + insertionPoint;
   return NS_OK;
 }
 
 nsresult
 Selection::RemoveItem(nsRange* aItem)
@@ -3813,17 +3890,17 @@ Selection::RemoveItem(nsRange* aItem)
       idx = (int32_t)i;
       break;
     }
   }
   if (idx < 0)
     return NS_ERROR_INVALID_ARG;
 
   mRanges.RemoveElementAt(idx);
-  aItem->SetInSelection(false);
+  aItem->SetSelection(nullptr);
   return NS_OK;
 }
 
 nsresult
 Selection::RemoveCollapsedRanges()
 {
   uint32_t i = 0;
   while (i < mRanges.Length()) {
@@ -3838,17 +3915,17 @@ Selection::RemoveCollapsedRanges()
 }
 
 nsresult
 Selection::Clear(nsPresContext* aPresContext)
 {
   setAnchorFocusRange(-1);
 
   for (uint32_t i = 0; i < mRanges.Length(); ++i) {
-    mRanges[i].mRange->SetInSelection(false);
+    mRanges[i].mRange->SetSelection(nullptr);
     selectFrames(aPresContext, mRanges[i].mRange, false);
   }
   mRanges.Clear();
 
   // Reset direction so for more dependable table selection range handling
   SetDirection(eDirNext);
 
   // If this was an ATTENTION selection, change it back to normal now
@@ -5059,22 +5136,24 @@ Selection::GetRangeAt(int32_t aIndex)
 /*
 utility function
 */
 nsresult
 Selection::SetAnchorFocusToRange(nsRange* aRange)
 {
   NS_ENSURE_STATE(mAnchorFocusRange);
 
+  bool collapsed = Collapsed();
+
   nsresult res = RemoveItem(mAnchorFocusRange);
   if (NS_FAILED(res))
     return res;
 
   int32_t aOutIndex = -1;
-  res = AddItem(aRange, &aOutIndex);
+  res = AddItem(aRange, &aOutIndex, !collapsed);
   if (NS_FAILED(res))
     return res;
   setAnchorFocusRange(aOutIndex);
 
   return NS_OK;
 }
 
 void
@@ -5466,29 +5545,26 @@ Selection::SelectAllChildren(nsIDOMNode*
   NS_ENSURE_TRUE(node, NS_ERROR_INVALID_ARG);
   SelectAllChildren(*node, result);
   return result.StealNSResult();
 }
 
 void
 Selection::SelectAllChildren(nsINode& aNode, ErrorResult& aRv)
 {
-  if (mFrameSelection)
-  {
+  if (mFrameSelection) {
     mFrameSelection->PostReason(nsISelectionListener::SELECTALL_REASON);
   }
+  SelectionBatcher batch(this);
+
   Collapse(aNode, 0, aRv);
   if (aRv.Failed()) {
     return;
   }
 
-  if (mFrameSelection)
-  {
-    mFrameSelection->PostReason(nsISelectionListener::SELECTALL_REASON);
-  }
   Extend(aNode, aNode.GetChildCount(), aRv);
 }
 
 NS_IMETHODIMP
 Selection::ContainsNode(nsIDOMNode* aNode, bool aAllowPartial, bool* aYes)
 {
   if (!aYes) {
     return NS_ERROR_NULL_POINTER;
@@ -5921,17 +5997,36 @@ NS_IMETHODIMP
 Selection::EndBatchChanges()
 {
   if (mFrameSelection)
     mFrameSelection->EndBatchChanges();
 
   return NS_OK;
 }
 
-
+void
+Selection::AddSelectionChangeBlocker()
+{
+  mSelectionChangeBlockerCount++;
+}
+
+void
+Selection::RemoveSelectionChangeBlocker()
+{
+  MOZ_ASSERT(mSelectionChangeBlockerCount > 0,
+             "mSelectionChangeBlockerCount has an invalid value - "
+             "maybe you have a mismatched RemoveSelectionChangeBlocker?");
+  mSelectionChangeBlockerCount--;
+}
+
+bool
+Selection::IsBlockingSelectionChangeEvents() const
+{
+  return mSelectionChangeBlockerCount > 0;
+}
 
 NS_IMETHODIMP
 Selection::DeleteFromDocument()
 {
   ErrorResult result;
   DeleteFromDocument(result);
   return result.StealNSResult();
 }
@@ -6154,16 +6249,21 @@ Selection::SetSelectionDirection(nsDirec
 }
 
 JSObject*
 Selection::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return mozilla::dom::SelectionBinding::Wrap(aCx, this, aGivenProto);
 }
 
+// AutoHideSelectionChanges
+AutoHideSelectionChanges::AutoHideSelectionChanges(const nsFrameSelection* aFrame)
+  : AutoHideSelectionChanges(aFrame->GetSelection(nsISelectionController::SELECTION_NORMAL))
+{}
+
 // nsAutoCopyListener
 
 nsAutoCopyListener* nsAutoCopyListener::sInstance = nullptr;
 
 NS_IMPL_ISUPPORTS(nsAutoCopyListener, nsISelectionListener)
 
 /*
  * What we do now:
@@ -6213,8 +6313,108 @@ nsAutoCopyListener::NotifySelectionChang
 
   nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc);
   NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
 
   // call the copy code
   return nsCopySupport::HTMLCopy(aSel, doc,
                                  nsIClipboard::kSelectionClipboard, false);
 }
+
+// SelectionChangeListener
+
+SelectionChangeListener::RawRangeData::RawRangeData(const nsRange* aRange)
+{
+  mozilla::ErrorResult rv;
+  mStartParent = aRange->GetStartContainer(rv);
+  rv.SuppressException();
+  mEndParent = aRange->GetEndContainer(rv);
+  rv.SuppressException();
+  mStartOffset = aRange->GetStartOffset(rv);
+  rv.SuppressException();
+  mEndOffset = aRange->GetEndOffset(rv);
+  rv.SuppressException();
+}
+
+bool
+SelectionChangeListener::RawRangeData::Equals(const nsRange* aRange)
+{
+  mozilla::ErrorResult rv;
+  bool eq = mStartParent == aRange->GetStartContainer(rv);
+  rv.SuppressException();
+  eq = eq && mEndParent == aRange->GetEndContainer(rv);
+  rv.SuppressException();
+  eq = eq && mStartOffset == aRange->GetStartOffset(rv);
+  rv.SuppressException();
+  eq = eq && mEndOffset == aRange->GetEndOffset(rv);
+  rv.SuppressException();
+  return eq;
+}
+
+inline void
+ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
+                            SelectionChangeListener::RawRangeData& aField,
+                            const char* aName,
+                            uint32_t aFlags = 0)
+{
+  ImplCycleCollectionTraverse(aCallback, aField.mStartParent, "mStartParent", aFlags);
+  ImplCycleCollectionTraverse(aCallback, aField.mEndParent, "mEndParent", aFlags);
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(SelectionChangeListener)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SelectionChangeListener)
+  tmp->mOldRanges.Clear();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SelectionChangeListener)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOldRanges);
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SelectionChangeListener)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+  NS_INTERFACE_MAP_ENTRY(nsISelectionListener)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SelectionChangeListener)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SelectionChangeListener)
+
+
+NS_IMETHODIMP
+SelectionChangeListener::NotifySelectionChanged(nsIDOMDocument* aDoc,
+                                                nsISelection* aSel, int16_t aReason)
+{
+  // This cast is valid as nsISelection is a builtinclass which is only
+  // implemented by Selection.
+  nsRefPtr<Selection> sel = static_cast<Selection*>(aSel);
+
+  // Check if the ranges have actually changed
+  if (mOldRanges.Length() == sel->RangeCount()) {
+    bool changed = false;
+
+    for (size_t i = 0; i < mOldRanges.Length(); i++) {
+      if (!mOldRanges[i].Equals(sel->GetRangeAt(i))) {
+        changed = true;
+        break;
+      }
+    }
+
+    if (!changed) {
+      return NS_OK;
+    }
+  }
+
+  // The ranges have actually changed, update the mOldRanges array
+  mOldRanges.ClearAndRetainStorage();
+  for (size_t i = 0; i < sel->RangeCount(); i++) {
+    mOldRanges.AppendElement(RawRangeData(sel->GetRangeAt(i)));
+  }
+
+  // Actually fire off the event
+  nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc);
+  if (doc) {
+    nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
+      new AsyncEventDispatcher(doc, NS_LITERAL_STRING("selectionchange"), false);
+    asyncDispatcher->PostDOMEvent();
+  }
+
+  return NS_OK;
+}
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -129,16 +129,23 @@ pref("dom.fileHandle.enabled", true);
 
 // Whether or not the Permissions API is enabled.
 #ifdef NIGHTLY_BUILD
 pref("dom.permissions.enabled", true);
 #else
 pref("dom.permissions.enabled", false);
 #endif
 
+// Whether or not selection events are enabled
+#ifdef NIGHTLY_BUILD
+pref("dom.select_events.enabled", true);
+#else
+pref("dom.select_events.enabled", false);
+#endif
+
 // Whether or not Web Workers are enabled.
 pref("dom.workers.enabled", true);
 // The number of workers per domain allowed to run concurrently.
 pref("dom.workers.maxPerDomain", 20);
 
 // Whether or not Shared Web Workers are enabled.
 pref("dom.workers.sharedWorkers.enabled", true);
 
--- a/widget/EventMessageList.h
+++ b/widget/EventMessageList.h
@@ -419,8 +419,13 @@ NS_EVENT_MESSAGE(eGamepadAxisMove,      
 NS_EVENT_MESSAGE(eGamepadConnected,      eGamepadEventFirst + 3)
 NS_EVENT_MESSAGE(eGamepadDisconnected,   eGamepadEventFirst + 4)
 NS_EVENT_MESSAGE(eGamepadEventLast,      eGamepadDisconnected)
 #endif
 
 // input and beforeinput events.
 NS_EVENT_MESSAGE(NS_EDITOR_EVENT_START,   6100)
 NS_EVENT_MESSAGE(NS_EDITOR_INPUT,         NS_EDITOR_EVENT_START)
+
+// selection events
+NS_EVENT_MESSAGE(NS_SELECT_EVENT_START,   6200)
+NS_EVENT_MESSAGE(NS_SELECT_START,         NS_SELECT_EVENT_START)
+NS_EVENT_MESSAGE(NS_SELECTION_CHANGE,     NS_SELECT_EVENT_START + 1)