Bug 1196479 - Fire selectstart and selectionchange events on the input node when the selection in that editor changes. r=ehsan
authorMichael Layzell <michael@thelayzells.com>
Wed, 02 Sep 2015 13:39:39 -0400
changeset 262217 8853d35b1154f25259240308b83ae8d05359ab98
parent 262216 41ffed8cc5d34e954a4c5c9bcd4371a4c84c2847
child 262218 e0b5fec1df4d2c8e52f3543654e2d1c3f5dafd2a
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)
reviewersehsan
bugs1196479
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 1196479 - Fire selectstart and selectionchange events on the input node when the selection in that editor changes. r=ehsan
dom/html/nsTextEditorState.cpp
dom/tests/mochitest/general/frameSelectEvents.html
dom/webidl/Document.webidl
layout/forms/nsTextControlFrame.cpp
layout/generic/nsSelection.cpp
--- a/dom/html/nsTextEditorState.cpp
+++ b/dom/html/nsTextEditorState.cpp
@@ -88,16 +88,19 @@ public:
   {
   }
 
   NS_IMETHOD Run() {
     if (!mTextEditorState) {
       return NS_OK;
     }
 
+    AutoHideSelectionChanges hideSelectionChanges
+      (mFrame->GetConstFrameSelection());
+
     if (mFrame) {
       // SetSelectionRange leads to Selection::AddRange which flushes Layout -
       // need to block script to avoid nested PrepareEditor calls (bug 642800).
       nsAutoScriptBlocker scriptBlocker;
        nsTextEditorState::SelectionProperties& properties =
          mTextEditorState->GetSelectionProperties();
        mFrame->SetSelectionRange(properties.mStart,
                                  properties.mEnd,
@@ -1243,16 +1246,18 @@ nsTextEditorState::PrepareEditor(const n
     return NS_OK;
   }
 
   if (mEditorInitialized) {
     // Do not initialize the editor multiple times.
     return NS_OK;
   }
 
+  AutoHideSelectionChanges hideSelectionChanges(GetConstFrameSelection());
+
   // Don't attempt to initialize recursively!
   InitializationGuard guard(*this);
   if (guard.IsInitializingRecursively()) {
     return NS_ERROR_NOT_INITIALIZED;
   }
 
   // Note that we don't check mEditor here, because we might already have one
   // around, in which case we don't create a new one, and we'll just tie the
--- a/dom/tests/mochitest/general/frameSelectEvents.html
+++ b/dom/tests/mochitest/general/frameSelectEvents.html
@@ -10,16 +10,20 @@
     <div id="normal">
       <span id="inner">A bunch of text in a span inside of a div which should be selected</span>
     </div>
 
     <div id="ce" contenteditable>
       This is a random block of text
     </div>
 
+    <input type="text" id="input" value="XXXXXXXXXXXXXXXXXXX" width="200"> <br>
+
+    <textarea id="textarea" width="200">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</textarea>
+
     <script>
       // Call the testing methods from the parent window
       var is = parent.is;
       var ok = parent.ok;
 
       // spin() spins the event loop for two cycles, giving time for
       // selectionchange events to be fired, and handled by our listeners.
       function spin() {
@@ -31,16 +35,19 @@
       }
 
       // The main test
       parent.add_task(function *() {
         yield spin();
 
         var selectstart = 0;
         var selectionchange = 0;
+        var inputSelectionchange = 0;
+        var textareaSelectionchange = 0;
+
         var cancel = false;
         var selectstartTarget = null;
 
         document.addEventListener('selectstart', function(aEvent) {
           console.log("originaltarget", aEvent.originalTarget, "new", selectstartTarget);
           is(aEvent.originalTarget, selectstartTarget,
              "The original target of selectstart");
           selectstartTarget = null;
@@ -55,44 +62,83 @@
         document.addEventListener('selectionchange', function(aEvent) {
           is(aEvent.originalTarget, document,
              "The original target of selectionchange should be the document");
           console.log(selectionchange);
           selectionchange++;
         });
 
         function elt(aId) { return document.getElementById(aId); }
-        function reset() { selectstart = 0; selectionchange = 0; cancel = false; }
+        function reset() {
+          selectstart = 0;
+          selectionchange = 0;
+          inputSelectionchange = 0;
+          textareaSelectionchange = 0;
+          cancel = false;
+        }
 
-        function* mouseAction(aElement, aOffset, aType, aSelStart, aSelChng) {
+        elt("input").addEventListener('selectionchange', function(aEvent) {
+          is (aEvent.originalTarget, elt("input"),
+              "The original target of selectionchange should be the input");
+          console.log(inputSelectionchange);
+          inputSelectionchange++;
+        });
+        elt("textarea").addEventListener('selectionchange', function(aEvent) {
+          is (aEvent.originalTarget, elt("textarea"),
+              "The original target of selectionchange should be the textarea");
+          console.log(textareaSelectionchange);
+          textareaSelectionchange++;
+        });
+        function* mouseAction(aElement, aOffset, aType,
+                              aSelStart, aSelChng, aISelChng, aTSelChng,
+                              aYOffset)
+        {
           if (aType == "click") { // You can simulate a click event by sending undefined
             aType = undefined;
           }
-          synthesizeMouse(aElement, aOffset, 10, { type: aType });
+          if (!aYOffset) {
+            aYOffset = 10;
+          }
+          synthesizeMouse(aElement, aOffset, aYOffset, { type: aType });
           yield spin();
+
           is(selectstart, aSelStart,
              "SelStart Mouse Action (" + aOffset + " - " + aType + ")");
           is(selectionchange, aSelChng,
              "SelChng Mouse Action (" + aOffset + " - " + aType + ")");
+          is(inputSelectionchange, aISelChng || 0,
+             "ISelChng Mouse Action (" + aOffset + " - " + aType + ")");
+          is(textareaSelectionchange, aTSelChng || 0,
+             "TSelChng Mouse Action (" + aOffset + " - " + aType + ")");
           reset();
         }
 
-        function* keyAction(aKey, aShift, aAccel, aSelStart, aSelChng) {
+        function* keyAction(aKey, aShift, aAccel,
+                            aSelStart, aSelChng, aISelChng, aTSelChng)
+        {
           synthesizeKey(aKey, { shiftKey: aShift, accelKey: aAccel });
           yield spin();
           is(selectstart, aSelStart,
              "SelStart Key Action (" + aKey + " - " + aShift + " - " + aAccel + ")");
           is(selectionchange, aSelChng,
              "SelChng Key Action (" + aKey + " - " + aShift + " - " + aAccel + ")");
+          is(inputSelectionchange, aISelChng || 0,
+             "ISelChng Key Action (" + aKey + " - " + aShift + " - " + aAccel + ")");
+          is(textareaSelectionchange, aTSelChng || 0,
+             "TSelChng Key Action (" + aKey + " - " + aShift + " - " + aAccel + ")");
           reset();
         }
 
         var selection = document.getSelection();
-        function isCollapsed() { is(selection.isCollapsed, true, "Selection is collapsed"); }
-        function isNotCollapsed() { is(selection.isCollapsed, false, "Selection is not collapsed"); }
+        function isCollapsed() {
+          is(selection.isCollapsed, true, "Selection is collapsed");
+        }
+        function isNotCollapsed() {
+          is(selection.isCollapsed, false, "Selection is not collapsed");
+        }
 
         // Focus the contenteditable text
         yield* mouseAction(elt("ce"), 100, "click", 0, 1);
         isCollapsed();
 
         // Move the selection to the right, this should only fire selectstart once
         selectstartTarget = elt("ce").firstChild;
         yield* keyAction("VK_RIGHT", true, false, 1, 1);
@@ -123,17 +169,17 @@
           // Moving it more shouldn't trigger a start (move back to empty)
           yield* mouseAction(aElement, 75, "mousemove", 0, 1);
           isNotCollapsed();
           yield* mouseAction(aElement, 50, "mousemove", 0, 1);
           isCollapsed();
 
           // Wiggling the mouse a little such that it doesn't select any
           // characters shouldn't trigger a selection
-          yield* mouseAction(aElement, 49, "mousemove", 0, 0);
+          yield* mouseAction(aElement, 50, "mousemove", 0, 0, 0, 0, 11);
           isCollapsed();
 
           // Moving the mouse again from an empty selection should trigger a
           // selectstart
           selectstartTarget = aTarget;
           yield* mouseAction(aElement, 25, "mousemove", 1, 1);
           isNotCollapsed();
 
@@ -224,12 +270,145 @@
 
         // Change the range, without replacing
         range.selectNode(elt("ce"));
         yield spin();
         is(selectstart, 0, "Synthesized range mutations shouldn't fire selectstart");
         is(selectionchange, 1, "Synthesized range mutations should change selectionchange");
         reset();
         isNotCollapsed();
+
+        // Remove the range
+        s.removeAllRanges();
+        yield spin();
+        is(selectstart, 0, "Synthesized range removal");
+        is(selectionchange, 1, "Synthesized range removal");
+        reset();
+        isCollapsed();
+
+
+        /*
+           Selection events mouse move on input type=text
+        */
+
+        // Select a region
+
+        yield* mouseAction(elt("input"), 50, "mousedown", 0, 1, 1, 0);
+
+        selectstartTarget = elt("input");
+        yield* mouseAction(elt("input"), 100, "mousemove", 1, 0, 1, 0);
+
+        // Moving it more shouldn't trigger a start (move back to empty)
+        yield* mouseAction(elt("input"), 75, "mousemove", 0, 0, 1, 0);
+        yield* mouseAction(elt("input"), 50, "mousemove", 0, 0, 1, 0);
+
+        // Wiggling the mouse a little such that it doesn't select any
+        // characters shouldn't trigger a selection
+        yield* mouseAction(elt("input"), 50, "mousemove", 0, 0, 0, 0, 11);
+
+        // Moving the mouse again from an empty selection should trigger a
+        // selectstart
+        selectstartTarget = elt("input");
+        yield* mouseAction(elt("input"), 25, "mousemove", 1, 0, 1, 0);
+
+        // Releasing the mouse shouldn't do anything
+        yield* mouseAction(elt("input"), 25, "mouseup", 0, 0, 0, 0);
+
+        // And neither should moving your mouse around when the mouse
+        // button isn't pressed
+        yield* mouseAction(elt("input"), 50, "mousemove", 0, 0, 0, 0);
+
+        // Clicking in an random location should move the selection, but
+        // not perform a selectstart
+        yield* mouseAction(elt("input"), 50, "click", 0, 0, 1, 0);
+
+        // Clicking there again should do nothing
+        yield* mouseAction(elt("input"), 50, "click", 0, 0, 0, 0);
+
+        // Selecting a region, and canceling the selectstart should mean that the
+        // selection remains collapsed
+        yield* mouseAction(elt("input"), 75, "mousedown", 0, 0, 1, 0);
+        cancel = true;
+        selectstartTarget = elt("input");
+        yield* mouseAction(elt("input"), 100, "mousemove", 1, 0, 1, 0);
+        yield* mouseAction(elt("input"), 100, "mouseup", 0, 0, 0, 0);
+
+
+        // Select a region
+        // XXX For some reason we fire 2 selectchange events on the body
+        // when switching from the input to the text area.
+        yield* mouseAction(elt("textarea"), 50, "mousedown", 0, 2, 0, 1);
+
+        selectstartTarget = elt("textarea");
+        yield* mouseAction(elt("textarea"), 100, "mousemove", 1, 0, 0, 1);
+
+        // Moving it more shouldn't trigger a start (move back to empty)
+        yield* mouseAction(elt("textarea"), 75, "mousemove", 0, 0, 0, 1);
+        yield* mouseAction(elt("textarea"), 50, "mousemove", 0, 0, 0, 1);
+
+        // Wiggling the mouse a little such that it doesn't select any
+        // characters shouldn't trigger a selection
+        yield* mouseAction(elt("textarea"), 50, "mousemove", 0, 0, 0, 0, 11);
+
+        // Moving the mouse again from an empty selection should trigger a
+        // selectstart
+        selectstartTarget = elt("textarea");
+        yield* mouseAction(elt("textarea"), 25, "mousemove", 1, 0, 0, 1);
+
+        // Releasing the mouse shouldn't do anything
+        yield* mouseAction(elt("textarea"), 25, "mouseup", 0, 0, 0, 0);
+
+        // And neither should moving your mouse around when the mouse
+        // button isn't pressed
+        yield* mouseAction(elt("textarea"), 50, "mousemove", 0, 0, 0, 0);
+
+        // Clicking in an random location should move the selection, but not perform a
+        // selectstart
+        yield* mouseAction(elt("textarea"), 50, "click", 0, 0, 0, 1);
+
+        // Clicking there again should do nothing
+        yield* mouseAction(elt("textarea"), 50, "click", 0, 0, 0, 0);
+
+        // Selecting a region, and canceling the selectstart should mean that the
+        // selection remains collapsed
+        yield* mouseAction(elt("textarea"), 75, "mousedown", 0, 0, 0, 1);
+        cancel = true;
+        selectstartTarget = elt("textarea");
+        yield* mouseAction(elt("textarea"), 100, "mousemove", 1, 0, 0, 1);
+        yield* mouseAction(elt("textarea"), 100, "mouseup", 0, 0, 0, 0);
+
+        // Marking the input and textarea as display: none and then as visible again
+        // shouldn't trigger any changes, although the nodes will be re-framed
+        elt("input").setAttribute("style", "display: none;");
+        yield spin();
+        is(selectstart, 0, "idn - ss 1");
+        is(selectionchange, 0, "idn - sc 1");
+        is(inputSelectionchange, 0, "idn - isc 1");
+        is(textareaSelectionchange, 0, "idn - tsc 1");
+        reset();
+
+        elt("input").setAttribute("style", "");
+        yield spin();
+        is(selectstart, 0, "idn - ss 2");
+        is(selectionchange, 0, "idn - sc 2");
+        is(inputSelectionchange, 0, "idn - isc 2");
+        is(textareaSelectionchange, 0, "idn - tsc 2");
+        reset();
+
+        elt("textarea").setAttribute("style", "display: none;");
+        yield spin();
+        is(selectstart, 0, "tdn - ss 1");
+        is(selectionchange, 0, "tdn - sc 1");
+        is(inputSelectionchange, 0, "tdn - isc 1");
+        is(textareaSelectionchange, 0, "tdn - tsc 1");
+        reset();
+
+        elt("textarea").setAttribute("style", "");
+        yield spin();
+        is(selectstart, 0, "tdn - ss 2");
+        is(selectionchange, 0, "tdn - sc 2");
+        is(inputSelectionchange, 0, "tdn - isc 2");
+        is(textareaSelectionchange, 0, "tdn - tsc 2");
+        reset();
       });
     </script>
   </body>
 </html>
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -147,18 +147,16 @@ 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/layout/forms/nsTextControlFrame.cpp
+++ b/layout/forms/nsTextControlFrame.cpp
@@ -41,16 +41,17 @@
 #include "nsContentList.h"
 #include "nsAttrValueInlines.h"
 #include "mozilla/dom/Selection.h"
 #include "nsContentUtils.h"
 #include "nsTextNode.h"
 #include "nsStyleSet.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/MathAlgorithms.h"
+#include "nsFrameSelection.h"
 
 #define DEFAULT_COLUMN_WIDTH 20
 
 using namespace mozilla;
 
 nsIFrame*
 NS_NewTextControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
 {
@@ -254,16 +255,23 @@ nsTextControlFrame::EnsureEditorInitiali
   // blockers don't prevent the sink flushing out content and notifying in the
   // process, which can destroy frames.
   doc->FlushPendingNotifications(Flush_ContentAndNotify);
   NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_ERROR_FAILURE);
 
   // Make sure that editor init doesn't do things that would kill us off
   // (especially off the script blockers it'll create for its DOM mutations).
   {
+    nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent());
+    MOZ_ASSERT(txtCtrl, "Content not a text control element");
+
+    // Hide selection changes during the initialization, as webpages should not
+    // be aware of these initializations
+    AutoHideSelectionChanges hideSelectionChanges(txtCtrl->GetConstFrameSelection());
+
     nsAutoScriptBlocker scriptBlocker;
 
     // Time to mess with our security context... See comments in GetValue()
     // for why this is needed.
     mozilla::dom::AutoNoJSAPI nojsapi;
 
     // Make sure that we try to focus the content even if the method fails
     class EnsureSetFocus {
@@ -283,18 +291,16 @@ nsTextControlFrame::EnsureEditorInitiali
     // Make sure we are not being called again until we're finished.
     // If reentrancy happens, just pretend that we don't have an editor.
     const EditorInitializerEntryTracker tracker(*this);
     NS_ASSERTION(!tracker.EnteredMoreThanOnce(),
                  "EnsureEditorInitialized has been called while a previous call was in progress");
 #endif
 
     // Create an editor for the frame, if one doesn't already exist
-    nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent());
-    NS_ASSERTION(txtCtrl, "Content not a text control element");
     nsresult rv = txtCtrl->CreateEditor();
     NS_ENSURE_SUCCESS(rv, rv);
     NS_ENSURE_STATE(weakFrame.IsAlive());
 
     // Set mEditorHasBeenInitialized so that subsequent calls will use the
     // editor.
     mEditorHasBeenInitialized = true;
 
--- a/layout/generic/nsSelection.cpp
+++ b/layout/generic/nsSelection.cpp
@@ -3720,18 +3720,24 @@ Selection::AddItem(nsRange* aItem, int32
         (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(),
+
+        // Get the first element which isn't in a native anonymous subtree
+        nsCOMPtr<nsINode> target = aItem->GetStartParent();
+        while (target && target->IsInNativeAnonymousSubtree()) {
+          target = target->GetParent();
+        }
+
+        nsContentUtils::DispatchTrustedEvent(GetParentObject(), target,
                                              NS_LITERAL_STRING("selectstart"),
                                              true, true, &defaultAction);
 
         if (!defaultAction) {
           return NS_OK;
         }
 
         // As we just dispatched an event to the DOM, something could have
@@ -6372,27 +6378,27 @@ 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()) {
+  // Don't bother checking this if we are hiding changes.
+  if (mOldRanges.Length() == sel->RangeCount() && !sel->IsBlockingSelectionChangeEvents()) {
     bool changed = false;
 
     for (size_t i = 0; i < mOldRanges.Length(); i++) {
       if (!mOldRanges[i].Equals(sel->GetRangeAt(i))) {
         changed = true;
         break;
       }
     }
@@ -6403,18 +6409,46 @@ SelectionChangeListener::NotifySelection
   }
 
   // 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) {
+  // If we are hiding changes, then don't do anything else. We do this after we
+  // update mOldRanges so that changes after the changes stop being hidden don't
+  // incorrectly trigger a change, even though they didn't change anything
+  if (sel->IsBlockingSelectionChangeEvents()) {
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsINode> target;
+
+  // Check if we should be firing this event to a different node than the
+  // document. The limiter of the nsFrameSelection will be within the native
+  // anonymous subtree of the node we want to fire the event on. We need to
+  // climb up the parent chain to escape the native anonymous subtree, and then
+  // fire the event.
+  if (nsFrameSelection* fs = sel->GetFrameSelection()) {
+    if (nsCOMPtr<nsIContent> root = fs->GetLimiter()) {
+      while (root && root->IsInNativeAnonymousSubtree()) {
+        root = root->GetParent();
+      }
+
+      target = root.forget();
+    }
+  }
+
+  // If we didn't get a target before, we can instead fire the event at the document.
+  if (!target) {
+    nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc);
+    target = doc.forget();
+  }
+
+  if (target) {
     nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
-      new AsyncEventDispatcher(doc, NS_LITERAL_STRING("selectionchange"), false);
+      new AsyncEventDispatcher(target, NS_LITERAL_STRING("selectionchange"), false);
     asyncDispatcher->PostDOMEvent();
   }
 
   return NS_OK;
 }