Bug 987040 - Part 1: Implement mozbrowserselectionchange. r=vingtetun,ehsan,bugs. sr=bz
authorMorris Tseng <mtseng@mozilla.com>
Mon, 28 Jul 2014 01:21:00 +0200
changeset 218209 fc734b3bbb4215cdd3c9dd59cb39ba6e3b56c21f
parent 218208 e12da3e04bf9dcbbe1bd119e8f2688283834962b
child 218210 c2d5fa0473cb4e6bca75af63e525609fd098e10c
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvingtetun, ehsan, bugs, bz
bugs987040
milestone34.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 987040 - Part 1: Implement mozbrowserselectionchange. r=vingtetun,ehsan,bugs. sr=bz
b2g/chrome/content/shell.js
content/base/src/nsCopySupport.cpp
content/html/content/src/nsTextEditorState.cpp
dom/base/nsFocusManager.cpp
dom/base/nsGlobalWindow.cpp
dom/browser-element/BrowserElementChildPreload.js
dom/browser-element/BrowserElementParent.jsm
dom/interfaces/base/nsIDOMWindow.idl
dom/webidl/SelectionChangeEvent.webidl
dom/webidl/Window.webidl
dom/webidl/moz.build
layout/base/nsDocumentViewer.cpp
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -341,16 +341,17 @@ var shell = {
     chromeEventHandler.addEventListener('keyup', this, true);
 
     window.addEventListener('MozApplicationManifest', this);
     window.addEventListener('mozfullscreenchange', this);
     window.addEventListener('MozAfterPaint', this);
     window.addEventListener('sizemodechange', this);
     window.addEventListener('unload', this);
     this.contentBrowser.addEventListener('mozbrowserloadstart', this, true);
+    this.contentBrowser.addEventListener('mozbrowserselectionchange', this, true);
 
     CustomEventManager.init();
     WebappsHelper.init();
     UserAgentOverrides.init();
     IndexedDBPromptHelper.init();
     CaptivePortalLoginHelper.init();
 
     this.contentBrowser.src = homeURL;
@@ -367,16 +368,17 @@ var shell = {
     window.removeEventListener('unload', this);
     window.removeEventListener('keydown', this, true);
     window.removeEventListener('keypress', this, true);
     window.removeEventListener('keyup', this, true);
     window.removeEventListener('MozApplicationManifest', this);
     window.removeEventListener('mozfullscreenchange', this);
     window.removeEventListener('sizemodechange', this);
     this.contentBrowser.removeEventListener('mozbrowserloadstart', this, true);
+    this.contentBrowser.removeEventListener('mozbrowserselectionchange', this, true);
     ppmm.removeMessageListener("content-handler", this);
 
     UserAgentOverrides.uninit();
     IndexedDBPromptHelper.uninit();
   },
 
   // If this key event actually represents a hardware button, filter it here
   // and send a mozChromeEvent with detail.type set to xxx-button-press or
@@ -509,16 +511,40 @@ var shell = {
       case 'mozbrowserlocationchange':
         if (content.document.location == 'about:blank') {
           return;
         }
 
         this.notifyContentStart();
        break;
 
+      case 'mozbrowserselectionchange':
+        // The mozbrowserselectionchange event, may have crossed the chrome-content boundary.
+        // This event always dispatch to shell.js. But the offset we got from this event is
+        // based on tab's coordinate. So get the actual offsets between shell and evt.target.
+        let elt = evt.target;
+        let win = elt.ownerDocument.defaultView;
+        let offsetX = win.mozInnerScreenX;
+        let offsetY = win.mozInnerScreenY;
+
+        let rect = elt.getBoundingClientRect();
+        offsetX += rect.left;
+        offsetY += rect.top;
+
+        let data = evt.detail;
+        data.offsetX = offsetX;
+        data.offsetY = offsetY;
+
+        DoCommandHelper.setEvent(evt);
+        shell.sendChromeEvent({
+          type: 'selectionchange',
+          detail: data,
+        });
+        break;
+
       case 'MozApplicationManifest':
         try {
           if (!Services.prefs.getBoolPref('browser.cache.offline.enable'))
             return;
 
           let contentWindow = evt.originalTarget.defaultView;
           let documentElement = contentWindow.document.documentElement;
           if (!documentElement)
@@ -708,16 +734,33 @@ var CustomEventManager = {
         RemoteDebugger.handleEvent(detail);
         break;
       case 'captive-portal-login-cancel':
         CaptivePortalLoginHelper.handleEvent(detail);
         break;
       case 'inputmethod-update-layouts':
         KeyboardHelper.handleEvent(detail);
         break;
+      case 'do-command':
+        DoCommandHelper.handleEvent(detail.cmd);
+        break;
+    }
+  }
+}
+
+let DoCommandHelper = {
+  _event: null,
+  setEvent: function docommand_setEvent(evt) {
+    this._event = evt;
+  },
+
+  handleEvent: function docommand_handleEvent(cmd) {
+    if (this._event) {
+      shell.sendEvent(this._event.target, 'mozdocommand', { cmd: cmd });
+      this._event = null;
     }
   }
 }
 
 var WebappsHelper = {
   _installers: {},
   _count: 0,
 
--- a/content/base/src/nsCopySupport.cpp
+++ b/content/base/src/nsCopySupport.cpp
@@ -710,13 +710,13 @@ nsCopySupport::FireClipboardEvent(int32_
         return false;
       }
     }
   }
 
   // Now that we have copied, update the clipboard commands. This should have
   // the effect of updating the enabled state of the paste menu item.
   if (doDefault || count) {
-    piWindow->UpdateCommands(NS_LITERAL_STRING("clipboard"));
+    piWindow->UpdateCommands(NS_LITERAL_STRING("clipboard"), nullptr, 0);
   }
 
   return doDefault;
 }
--- a/content/html/content/src/nsTextEditorState.cpp
+++ b/content/html/content/src/nsTextEditorState.cpp
@@ -671,17 +671,19 @@ public:
 
   NS_DECL_NSIEDITOROBSERVER
 
 protected:
   /** the default destructor. virtual due to the possibility of derivation.
    */
   virtual ~nsTextInputListener();
 
-  nsresult  UpdateTextInputCommands(const nsAString& commandsToUpdate);
+  nsresult  UpdateTextInputCommands(const nsAString& commandsToUpdate,
+                                    nsISelection* sel = nullptr,
+                                    int16_t reason = 0);
 
 protected:
 
   nsIFrame* mFrame;
 
   nsITextControlElement* const mTxtCtrlElement;
 
   bool            mSelectionWasCollapsed;
@@ -775,26 +777,28 @@ nsTextInputListener::NotifySelectionChan
           WidgetEvent event(true, NS_FORM_SELECTED);
 
           presShell->HandleEventWithTarget(&event, mFrame, content, &status);
         }
       }
     }
   }
 
+  UpdateTextInputCommands(NS_LITERAL_STRING("selectionchange"), aSel, aReason);
+
   // if the collapsed state did not change, don't fire notifications
   if (collapsed == mSelectionWasCollapsed)
     return NS_OK;
-  
+
   mSelectionWasCollapsed = collapsed;
 
   if (!weakFrame.IsAlive() || !nsContentUtils::IsFocusedContent(mFrame->GetContent()))
     return NS_OK;
 
-  return UpdateTextInputCommands(NS_LITERAL_STRING("select"));
+  return UpdateTextInputCommands(NS_LITERAL_STRING("select"), aSel, aReason);
 }
 
 // END nsIDOMSelectionListener
 
 static void
 DoCommandCallback(Command aCommand, void* aData)
 {
   nsTextControlFrame *frame = static_cast<nsTextControlFrame*>(aData);
@@ -925,28 +929,30 @@ nsTextInputListener::EditAction()
 
   return NS_OK;
 }
 
 // END nsIEditorObserver
 
 
 nsresult
-nsTextInputListener::UpdateTextInputCommands(const nsAString& commandsToUpdate)
+nsTextInputListener::UpdateTextInputCommands(const nsAString& commandsToUpdate,
+                                             nsISelection* sel,
+                                             int16_t reason)
 {
   nsIContent* content = mFrame->GetContent();
   NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
   
   nsCOMPtr<nsIDocument> doc = content->GetDocument();
   NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
 
   nsPIDOMWindow *domWindow = doc->GetWindow();
   NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE);
 
-  return domWindow->UpdateCommands(commandsToUpdate);
+  return domWindow->UpdateCommands(commandsToUpdate, sel, reason);
 }
 
 // END nsTextInputListener
 
 // nsTextEditorState
 
 nsTextEditorState::nsTextEditorState(nsITextControlElement* aOwningElement)
   : mTextCtrlElement(aOwningElement),
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -918,17 +918,17 @@ nsFocusManager::WindowHidden(nsIDOMWindo
 
   nsCOMPtr<nsIDocShell> focusedDocShell = mFocusedWindow->GetDocShell();
   nsCOMPtr<nsIPresShell> presShell = focusedDocShell->GetPresShell();
 
   if (oldFocusedContent && oldFocusedContent->IsInDoc()) {
     NotifyFocusStateChange(oldFocusedContent,
                            mFocusedWindow->ShouldShowFocusRing(),
                            false);
-    window->UpdateCommands(NS_LITERAL_STRING("focus"));
+    window->UpdateCommands(NS_LITERAL_STRING("focus"), nullptr, 0);
 
     if (presShell) {
       SendFocusOrBlurEvent(NS_BLUR_CONTENT, presShell,
                            oldFocusedContent->GetCurrentDoc(),
                            oldFocusedContent, 1, false);
     }
   }
 
@@ -1261,17 +1261,17 @@ nsFocusManager::SetFocusInner(nsIContent
       nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
       if (presShell)
         ScrollIntoView(presShell, contentToFocus, aFlags);
     }
 
     // update the commands even when inactive so that the attributes for that
     // window are up to date.
     if (allowFrameSwitch)
-      newWindow->UpdateCommands(NS_LITERAL_STRING("focus"));
+      newWindow->UpdateCommands(NS_LITERAL_STRING("focus"), nullptr, 0);
 
     if (aFlags & FLAG_RAISE)
       RaiseWindow(newRootWindow);
   }
 }
 
 bool
 nsFocusManager::IsSameOrAncestor(nsPIDOMWindow* aPossibleAncestor,
@@ -1581,17 +1581,17 @@ nsFocusManager::Blur(nsPIDOMWindow* aWin
   }
 
   bool result = true;
   if (sendBlurEvent) {
     // if there is an active window, update commands. If there isn't an active
     // window, then this was a blur caused by the active window being lowered,
     // so there is no need to update the commands
     if (mActiveWindow)
-      window->UpdateCommands(NS_LITERAL_STRING("focus"));
+      window->UpdateCommands(NS_LITERAL_STRING("focus"), nullptr, 0);
 
     SendFocusOrBlurEvent(NS_BLUR_CONTENT, presShell,
                          content->GetCurrentDoc(), content, 1, false);
   }
 
   // if we are leaving the document or the window was lowered, make the caret
   // invisible.
   if (aIsLeavingDocument || !mActiveWindow)
@@ -1802,27 +1802,27 @@ nsFocusManager::Focus(nsPIDOMWindow* aWi
 
       IMEStateManager::OnChangeFocus(presContext, aContent,
                                      GetFocusMoveActionCause(aFlags));
 
       // as long as this focus wasn't because a window was raised, update the
       // commands
       // XXXndeakin P2 someone could adjust the focus during the update
       if (!aWindowRaised)
-        aWindow->UpdateCommands(NS_LITERAL_STRING("focus"));
+        aWindow->UpdateCommands(NS_LITERAL_STRING("focus"), nullptr, 0);
 
       SendFocusOrBlurEvent(NS_FOCUS_CONTENT, presShell,
                            aContent->GetCurrentDoc(),
                            aContent, aFlags & FOCUSMETHOD_MASK,
                            aWindowRaised, isRefocus);
     } else {
       IMEStateManager::OnChangeFocus(presContext, nullptr,
                                      GetFocusMoveActionCause(aFlags));
       if (!aWindowRaised) {
-        aWindow->UpdateCommands(NS_LITERAL_STRING("focus"));
+        aWindow->UpdateCommands(NS_LITERAL_STRING("focus"), nullptr, 0);
       }
     }
   }
   else {
     // If the window focus event (fired above when aIsNewDocument) caused
     // the plugin not to be focusable, update the system focus by focusing
     // the root widget.
     if (aAdjustWidgets && objectFrameWidget &&
@@ -1837,17 +1837,17 @@ nsFocusManager::Focus(nsPIDOMWindow* aWi
       }
     }
 
     nsPresContext* presContext = presShell->GetPresContext();
     IMEStateManager::OnChangeFocus(presContext, nullptr,
                                    GetFocusMoveActionCause(aFlags));
 
     if (!aWindowRaised)
-      aWindow->UpdateCommands(NS_LITERAL_STRING("focus"));
+      aWindow->UpdateCommands(NS_LITERAL_STRING("focus"), nullptr, 0);
   }
 
   // update the caret visibility and position to match the newly focused
   // element. However, don't update the position if this was a focus due to a
   // mouse click as the selection code would already have moved the caret as
   // needed. If this is a different document than was focused before, also
   // update the caret's visibility. If this is the same document, the caret
   // visibility should be the same as before so there is no need to update it.
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -194,16 +194,18 @@
 #include "mozilla/dom/StructuredCloneTags.h"
 
 #ifdef MOZ_GAMEPAD
 #include "mozilla/dom/GamepadService.h"
 #endif
 
 #include "nsRefreshDriver.h"
 
+#include "mozilla/dom/SelectionChangeEvent.h"
+
 #include "mozilla/Services.h"
 #include "mozilla/Telemetry.h"
 #include "nsLocation.h"
 #include "nsHTMLDocument.h"
 #include "nsWrapperCacheInlines.h"
 #include "mozilla/DOMEventTargetHelper.h"
 #include "prrng.h"
 #include "nsSandboxFlags.h"
@@ -9243,35 +9245,60 @@ public:
     return mDispatcher->UpdateCommands(mAction);
   }
 
   nsCOMPtr<nsIDOMXULCommandDispatcher> mDispatcher;
   nsString                             mAction;
 };
 
 NS_IMETHODIMP
-nsGlobalWindow::UpdateCommands(const nsAString& anAction)
+nsGlobalWindow::UpdateCommands(const nsAString& anAction, nsISelection* aSel, int16_t aReason)
 {
   nsPIDOMWindow *rootWindow = nsGlobalWindow::GetPrivateRoot();
   if (!rootWindow)
     return NS_OK;
 
   nsCOMPtr<nsIDOMXULDocument> xulDoc =
     do_QueryInterface(rootWindow->GetExtantDoc());
   // See if we contain a XUL document.
-  if (xulDoc) {
+  // selectionchange action is only used for mozbrowser, not for XUL. So we bypass
+  // XUL command dispatch if anAction is "selectionchange".
+  if (xulDoc && !anAction.EqualsLiteral("selectionchange")) {
     // Retrieve the command dispatcher and call updateCommands on it.
     nsCOMPtr<nsIDOMXULCommandDispatcher> xulCommandDispatcher;
     xulDoc->GetCommandDispatcher(getter_AddRefs(xulCommandDispatcher));
     if (xulCommandDispatcher) {
       nsContentUtils::AddScriptRunner(new CommandDispatcher(xulCommandDispatcher,
                                                             anAction));
     }
   }
 
+  if (mDoc && anAction.EqualsLiteral("selectionchange")) {
+    SelectionChangeEventInit init;
+    init.mBubbles = true;
+    if (aSel) {
+      nsCOMPtr<nsIDOMRange> range;
+      nsresult rv = aSel->GetRangeAt(0, getter_AddRefs(range));
+      if (NS_SUCCEEDED(rv) && range) {
+        nsRefPtr<nsRange> nsrange = static_cast<nsRange*>(range.get());
+        init.mBoundingClientRect = nsrange->GetBoundingClientRect(true);
+        range->ToString(init.mSelectedText);
+        init.mReason = aReason;
+      }
+
+      nsRefPtr<SelectionChangeEvent> event =
+        SelectionChangeEvent::Constructor(mDoc, NS_LITERAL_STRING("mozselectionchange"), init);
+
+      event->SetTrusted(true);
+      event->GetInternalNSEvent()->mFlags.mOnlyChromeDispatch = true;
+      bool ret;
+      mDoc->DispatchEvent(event, &ret);
+    }
+  }
+
   return NS_OK;
 }
 
 Selection*
 nsGlobalWindow::GetSelection(ErrorResult& aError)
 {
   FORWARD_TO_OUTER_OR_THROW(GetSelection, (aError), aError, nullptr);
 
--- a/dom/browser-element/BrowserElementChildPreload.js
+++ b/dom/browser-element/BrowserElementChildPreload.js
@@ -103,16 +103,23 @@ function getErrorClass(errorCode) {
 const OBSERVED_EVENTS = [
   'fullscreen-origin-change',
   'ask-parent-to-exit-fullscreen',
   'ask-parent-to-rollback-fullscreen',
   'xpcom-shutdown',
   'activity-done'
 ];
 
+const COMMAND_MAP = {
+  'cut': 'cmd_cut',
+  'copy': 'cmd_copy',
+  'paste': 'cmd_paste',
+  'selectall': 'cmd_selectAll'
+};
+
 /**
  * The BrowserElementChild implements one half of <iframe mozbrowser>.
  * (The other half is, unsurprisingly, BrowserElementParent.)
  *
  * This script is injected into an <iframe mozbrowser> via
  * nsIMessageManager::LoadFrameScript().
  *
  * Our job here is to listen for events within this frame and bubble them up to
@@ -195,16 +202,20 @@ BrowserElementChild.prototype = {
                      /* useCapture = */ true,
                      /* wantsUntrusted = */ false);
 
     addEventListener('DOMMetaRemoved',
                      this._metaChangedHandler.bind(this),
                      /* useCapture = */ true,
                      /* wantsUntrusted = */ false);
 
+    addEventListener('mozselectionchange',
+                     this._selectionChangeHandler.bind(this),
+                     /* useCapture = */ false,
+                     /* wantsUntrusted = */ false);
 
     // This listens to unload events from our message manager, but /not/ from
     // the |content| window.  That's because the window's unload event doesn't
     // bubble, and we're not using a capturing listener.  If we'd used
     // useCapture == true, we /would/ hear unload events from the window, which
     // is not what we want!
     addEventListener('unload',
                      this._unloadHandler.bind(this),
@@ -232,17 +243,18 @@ BrowserElementChild.prototype = {
       "reload": this._recvReload,
       "stop": this._recvStop,
       "unblock-modal-prompt": this._recvStopWaiting,
       "fire-ctx-callback": this._recvFireCtxCallback,
       "owner-visibility-change": this._recvOwnerVisibilityChange,
       "exit-fullscreen": this._recvExitFullscreen.bind(this),
       "activate-next-paint-listener": this._activateNextPaintListener.bind(this),
       "set-input-method-active": this._recvSetInputMethodActive.bind(this),
-      "deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this)
+      "deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this),
+      "do-command": this._recvDoCommand
     }
 
     addMessageListener("browser-element-api:call", function(aMessage) {
       if (aMessage.data.msg_name in mmCalls) {
         return mmCalls[aMessage.data.msg_name].apply(self, arguments);
       }
     });
 
@@ -346,16 +358,25 @@ BrowserElementChild.prototype = {
 
     if (args.promptType == 'prompt' ||
         args.promptType == 'confirm' ||
         args.promptType == 'custom-prompt') {
       return returnValue;
     }
   },
 
+  _isCommandEnabled: function(cmd) {
+    let command = COMMAND_MAP[cmd];
+    if (!command) {
+      return false;
+    }
+
+    return docShell.isCommandEnabled(command);
+  },
+
   /**
    * Spin in a nested event loop until we receive a unblock-modal-prompt message for
    * this window.
    */
   _waitForResult: function(win) {
     debug("_waitForResult(" + win + ")");
     let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindowUtils);
@@ -584,16 +605,63 @@ BrowserElementChild.prototype = {
 
     if (lang) {
       meta.lang = lang;
     }
 
     sendAsyncMsg('metachange', meta);
   },
 
+  _selectionChangeHandler: function(e) {
+    let isMouseUp = e.reason & Ci.nsISelectionListener.MOUSEUP_REASON;
+    let isSelectAll = e.reason & Ci.nsISelectionListener.SELECTALL_REASON;
+    // When selectall happened, gecko will first collapse the range then
+    // select all. So we will receive two selection change events with
+    // SELECTALL_REASON. We filter first event by check the length of
+    // selectedText.
+    if (!(isMouseUp || (isSelectAll && e.selectedText.length > 0))) {
+      return;
+    }
+
+    e.stopPropagation();
+    let boundingClientRect = e.boundingClientRect;
+    let zoomFactor = content.screen.width / content.innerWidth;
+
+    let detail = {
+      rect: {
+        width: boundingClientRect.width,
+        height: boundingClientRect.height,
+        top: boundingClientRect.top,
+        bottom: boundingClientRect.bottom,
+        left: boundingClientRect.left,
+        right: boundingClientRect.right,
+      },
+      commands: {
+        canSelectAll: this._isCommandEnabled("selectall"),
+        canCut: this._isCommandEnabled("cut"),
+        canCopy: this._isCommandEnabled("copy"),
+        canPaste: this._isCommandEnabled("paste"),
+      },
+      zoomFactor: zoomFactor,
+    };
+
+    // Get correct geometry information if we have nested <iframe mozbrowser>
+    let currentWindow = e.target.defaultView;
+    while (currentWindow.realFrameElement) {
+      let currentRect = currentWindow.realFrameElement.getBoundingClientRect();
+      detail.rect.top += currentRect.top;
+      detail.rect.bottom += currentRect.top;
+      detail.rect.left += currentRect.left;
+      detail.rect.right += currentRect.left;
+      currentWindow = currentWindow.realFrameElement.ownerDocument.defaultView;
+    }
+
+    sendAsyncMsg("selectionchange", detail);
+  },
+
   _themeColorChangedHandler: function(eventType, target) {
     let meta = {
       name: 'theme-color',
       content: target.content,
       type: eventType.replace('DOMMeta', '').toLowerCase()
     };
     sendAsyncMsg('metachange', meta);
   },
@@ -1026,16 +1094,22 @@ BrowserElementChild.prototype = {
     }
   },
 
   _recvStop: function(data) {
     let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
     webNav.stop(webNav.STOP_NETWORK);
   },
 
+  _recvDoCommand: function(data) {
+    if (this._isCommandEnabled(data.json.command)) {
+      docShell.doCommand(COMMAND_MAP[data.json.command]);
+    }
+  },
+
   _recvSetInputMethodActive: function(data) {
     let msgData = { id: data.json.id };
     if (!this._isContentWindowCreated) {
       if (data.json.args.isActive) {
         // To activate the input method, we should wait before the content
         // window is ready.
         this._pendingSetInputMethodActive.push(data);
         return;
--- a/dom/browser-element/BrowserElementParent.jsm
+++ b/dom/browser-element/BrowserElementParent.jsm
@@ -154,16 +154,21 @@ function BrowserElementParent(frameLoade
   if (!this._window._browserElementParents) {
     this._window._browserElementParents = new WeakMap();
     this._window.addEventListener('visibilitychange',
                                   visibilityChangeHandler,
                                   /* useCapture = */ false,
                                   /* wantsUntrusted = */ false);
   }
 
+  this._frameElement.addEventListener('mozdocommand',
+                                      this._doCommandHandler.bind(this),
+                                      /* useCapture = */ false,
+                                      /* wantsUntrusted = */ false);
+
   this._window._browserElementParents.set(this, null);
 
   // Insert ourself into the prompt service.
   BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this);
   if (!isPendingFrame) {
     this._setupMessageListener();
     this._registerAppManifest();
   } else {
@@ -242,17 +247,18 @@ BrowserElementParent.prototype = {
       "got-screenshot": this._gotDOMRequestResult,
       "got-can-go-back": this._gotDOMRequestResult,
       "got-can-go-forward": this._gotDOMRequestResult,
       "fullscreen-origin-change": this._remoteFullscreenOriginChange,
       "rollback-fullscreen": this._remoteFrameFullscreenReverted,
       "exit-fullscreen": this._exitFullscreen,
       "got-visible": this._gotDOMRequestResult,
       "visibilitychange": this._childVisibilityChange,
-      "got-set-input-method-active": this._gotDOMRequestResult
+      "got-set-input-method-active": this._gotDOMRequestResult,
+      "selectionchange": this._handleSelectionChange
     };
 
     this._mm.addMessageListener('browser-element-api:call', function(aMsg) {
       if (self._isAlive() && (aMsg.data.msg_name in mmCalls)) {
         return mmCalls[aMsg.data.msg_name].apply(self, arguments);
       }
     });
   },
@@ -444,16 +450,27 @@ BrowserElementParent.prototype = {
 
     if (!evt.defaultPrevented) {
       // Unblock the inner frame immediately.  Otherwise we'll unblock upon
       // evt.detail.unblock().
       sendUnblockMsg();
     }
   },
 
+  _handleSelectionChange: function(data) {
+    let evt = this._createEvent('selectionchange', data.json,
+                                /* cancelable = */ false);
+    this._frameElement.dispatchEvent(evt);
+  },
+
+  _doCommandHandler: function(e) {
+    e.stopPropagation();
+    this._sendAsyncMsg('do-command', { command: e.detail.cmd });
+  },
+
   _createEvent: function(evtName, detail, cancelable) {
     // This will have to change if we ever want to send a CustomEvent with null
     // detail.  For now, it's OK.
     if (detail !== undefined && detail !== null) {
       detail = Cu.cloneInto(detail, this._window);
       return new this._window.CustomEvent('mozbrowser' + evtName,
                                           { bubbles: true,
                                             cancelable: cancelable,
--- a/dom/interfaces/base/nsIDOMWindow.idl
+++ b/dom/interfaces/base/nsIDOMWindow.idl
@@ -18,17 +18,17 @@ interface nsIVariant;
  * The nsIDOMWindow interface is the primary interface for a DOM
  * window object. It represents a single window object that may
  * contain child windows if the document in the window contains a
  * HTML frameset document or if the document contains iframe elements.
  *
  * @see <http://www.whatwg.org/html/#window>
  */
 
-[scriptable, uuid(c3ff0328-6c47-4e64-a22f-ac221959e258)]
+[scriptable, uuid(ed7cc4e4-cf5b-42af-9c2e-8df074a01470)]
 interface nsIDOMWindow : nsISupports
 {
   // the current browsing context
   readonly attribute nsIDOMWindow                       window;
 
   /* [replaceable] self */
   readonly attribute nsIDOMWindow                       self;
 
@@ -421,17 +421,19 @@ interface nsIDOMWindow : nsISupports
    * nsISupports (nsISupportsPrimitives) types are converted to native
    * JS types when possible.
    */
   [noscript] nsIDOMWindow   openDialog(in DOMString url, in DOMString name,
                                        in DOMString options,
                                        in nsISupports aExtraArgument);
 
   // XXX Should this be in nsIDOMChromeWindow?
-  void                      updateCommands(in DOMString action);
+  void                      updateCommands(in DOMString action,
+                                           [optional] in nsISelection sel,
+                                           [optional] in short reason);
 
   /* Find in page.
    * @param str: the search pattern
    * @param caseSensitive: is the search caseSensitive
    * @param backwards: should we search backwards
    * @param wrapAround: should we wrap the search
    * @param wholeWord: should we search only for whole words
    * @param searchInFrames: should we search through all frames
new file mode 100644
--- /dev/null
+++ b/dom/webidl/SelectionChangeEvent.webidl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; 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/.
+ */
+
+dictionary SelectionChangeEventInit : EventInit {
+  DOMString selectedText = "";
+  DOMRectReadOnly? boundingClientRect = null;
+  short reason = 0;
+};
+
+[Constructor(DOMString type, optional SelectionChangeEventInit eventInit),
+ ChromeOnly]
+interface SelectionChangeEvent : Event {
+  readonly attribute DOMString selectedText;
+  readonly attribute DOMRectReadOnly? boundingClientRect;
+  readonly attribute short reason;
+};
--- a/dom/webidl/Window.webidl
+++ b/dom/webidl/Window.webidl
@@ -298,17 +298,19 @@ partial interface Window {
 
   [Throws] attribute boolean                            fullScreen;
 
   [Throws, ChromeOnly] void             back();
   [Throws, ChromeOnly] void             forward();
   [Throws, ChromeOnly] void             home();
 
   // XXX Should this be in nsIDOMChromeWindow?
-  void                      updateCommands(DOMString action);
+  void                      updateCommands(DOMString action,
+                                           optional Selection? sel = null,
+                                           optional short reason = 0);
 
   /* Find in page.
    * @param str: the search pattern
    * @param caseSensitive: is the search caseSensitive
    * @param backwards: should we search backwards
    * @param wrapAround: should we wrap the search
    * @param wholeWord: should we search only for whole words
    * @param searchInFrames: should we search through all frames
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -663,16 +663,17 @@ GENERATED_EVENTS_WEBIDL_FILES = [
     'PopStateEvent.webidl',
     'PopupBlockedEvent.webidl',
     'ProgressEvent.webidl',
     'RecordErrorEvent.webidl',
     'RTCDataChannelEvent.webidl',
     'RTCPeerConnectionIceEvent.webidl',
     'RTCPeerConnectionIdentityErrorEvent.webidl',
     'RTCPeerConnectionIdentityEvent.webidl',
+    'SelectionChangeEvent.webidl',
     'SmartCardEvent.webidl',
     'StyleRuleChangeEvent.webidl',
     'StyleSheetApplicableStateChangeEvent.webidl',
     'StyleSheetChangeEvent.webidl',
     'TrackEvent.webidl',
     'UserProximityEvent.webidl',
     'USSDReceivedEvent.webidl',
 ]
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -3524,43 +3524,45 @@ NS_IMETHODIMP nsDocumentViewer::GetInIma
   if (NS_FAILED(rv)) return rv;
   NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
 
   // if we made it here, we're in an image
   *aInImage = true;
   return NS_OK;
 }
 
-NS_IMETHODIMP nsDocViewerSelectionListener::NotifySelectionChanged(nsIDOMDocument *, nsISelection *, int16_t)
+NS_IMETHODIMP nsDocViewerSelectionListener::NotifySelectionChanged(nsIDOMDocument *, nsISelection *, int16_t aReason)
 {
   NS_ASSERTION(mDocViewer, "Should have doc viewer!");
 
   // get the selection state
   nsCOMPtr<nsISelection> selection;
   nsresult rv = mDocViewer->GetDocumentSelection(getter_AddRefs(selection));
   if (NS_FAILED(rv)) return rv;
 
+  nsIDocument* theDoc = mDocViewer->GetDocument();
+  if (!theDoc) return NS_ERROR_FAILURE;
+
+  nsCOMPtr<nsPIDOMWindow> domWindow = theDoc->GetWindow();
+  if (!domWindow) return NS_ERROR_FAILURE;
+
   bool selectionCollapsed;
   selection->GetIsCollapsed(&selectionCollapsed);
   // we only call UpdateCommands when the selection changes from collapsed
   // to non-collapsed or vice versa. We might need another update string
   // for simple selection changes, but that would be expenseive.
   if (!mGotSelectionState || mSelectionWasCollapsed != selectionCollapsed)
   {
-    nsIDocument* theDoc = mDocViewer->GetDocument();
-    if (!theDoc) return NS_ERROR_FAILURE;
-
-    nsPIDOMWindow *domWindow = theDoc->GetWindow();
-    if (!domWindow) return NS_ERROR_FAILURE;
-
-    domWindow->UpdateCommands(NS_LITERAL_STRING("select"));
+    domWindow->UpdateCommands(NS_LITERAL_STRING("select"), selection, aReason);
     mGotSelectionState = true;
     mSelectionWasCollapsed = selectionCollapsed;
   }
 
+  domWindow->UpdateCommands(NS_LITERAL_STRING("selectionchange"), selection, aReason);
+
   return NS_OK;
 }
 
 //nsDocViewerFocusListener
 NS_IMPL_ISUPPORTS(nsDocViewerFocusListener,
                   nsIDOMEventListener)
 
 nsDocViewerFocusListener::nsDocViewerFocusListener()