Bug 1266322 - Fennec doesn't allow user cut/paste in etherpad draft
authorMark Capella <markcapella@twcny.rr.com>
Sat, 23 Apr 2016 05:42:52 -0400
changeset 355645 7a7b0bbe08df52fe60c588b0780c4dbd018818ab
parent 355634 37f04460ddb76d6ef4e7c32a8a6b2fbc44cb8776
child 519249 2539b548f2d8492df9fa5bd3a35043dec6c6eb94
push id16346
push usermarkcapella@twcny.rr.com
push dateSat, 23 Apr 2016 10:01:55 +0000
bugs1266322
milestone48.0a1
Bug 1266322 - Fennec doesn't allow user cut/paste in etherpad MozReview-Commit-ID: 9Mu7ZtsLE7P
mobile/android/chrome/content/ActionBarHandler.js
mobile/android/tests/browser/robocop/testAccessibleCarets.js
mobile/android/tests/browser/robocop/testAccessibleCarets2.html
--- a/mobile/android/chrome/content/ActionBarHandler.js
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -58,26 +58,26 @@ var ActionBarHandler = {
     // Open a closed ActionBar if carets actually visible.
     if (!this._selectionID && e.caretVisuallyVisible) {
       this._init(e.boundingClientRect);
       return;
     }
 
     // Else, update an open ActionBar.
     if (this._selectionID) {
-      let [element, win] = this._getSelectionTargets();
-      if (this._targetElement === element && this._contentWindow === win) {
+      if (!this._selectionHasChanged()) {
+        // Still the same active selection.
         if (e.reason == 'visibilitychange' || e.reason == 'presscaret') {
           this._updateVisibility();
         } else {
           let forceUpdate = e.reason == 'updateposition' || e.reason == 'releasecaret';
           this._sendActionBarActions(forceUpdate, e.boundingClientRect);
         }
       } else {
-        // We have a new focused window/element pair.
+        // We've started a new selection entirely.
         this._uninit(false);
         this._init(e.boundingClientRect);
       }
     }
   },
 
   /**
    * ActionBarHandler notification observers.
@@ -173,16 +173,27 @@ var ActionBarHandler = {
       return [element, win];
     }
 
     // Focused element can't contain text.
     return [null, win];
   },
 
   /**
+   * The active Selection has changed, if the current focused element / win,
+   * pair, or state of the win's designMode changes.
+   */
+  _selectionHasChanged: function() {
+    let [element, win] = this._getSelectionTargets();
+    return (this._targetElement !== element ||
+            this._contentWindow !== win ||
+            this._isInDesignMode() !== this._isInDesignMode(win));
+  },
+
+  /**
    * Called when Gecko AccessibleCaret becomes hidden,
    * ActionBar is closed by user "close" request, or as a result of object
    * methods such as SELECT_ALL, PASTE, etc.
    */
   _uninit: function(clearSelection = true) {
     // Bail if there's no active selection.
     if (!this._selectionID) {
       return;
@@ -343,27 +354,27 @@ var ActionBarHandler = {
       id: "cut_action",
       label: Strings.browser.GetStringFromName("contextmenu.cut"),
       icon: "drawable://ab_cut",
       order: 4,
       floatingOrder: 1,
 
       selector: {
         matches: function(element, win) {
-          // Can't cut from non-editable.
-          if (!element) {
+          // Can cut from editable, or design-mode document.
+          if (!element && !ActionBarHandler._isInDesignMode(win)) {
             return false;
           }
           // Don't allow "cut" from password fields.
           if (element instanceof Ci.nsIDOMHTMLInputElement &&
               !element.mozIsTextField(true)) {
             return false;
           }
           // Don't allow "cut" from disabled/readonly fields.
-          if (element.disabled || element.readOnly) {
+          if (element && (element.disabled || element.readOnly)) {
             return false;
           }
           // Allow if selected text exists.
           return (ActionBarHandler._getSelectedText().length > 0);
         },
       },
 
       action: function(element, win) {
@@ -421,22 +432,22 @@ var ActionBarHandler = {
       id: "paste_action",
       label: Strings.browser.GetStringFromName("contextmenu.paste"),
       icon: "drawable://ab_paste",
       order: 2,
       floatingOrder: 3,
 
       selector: {
         matches: function(element, win) {
-          // Can't paste into non-editable.
-          if (!element) {
+          // Can paste to editable, or design-mode document.
+          if (!element && !ActionBarHandler._isInDesignMode(win)) {
             return false;
           }
           // Can't paste into disabled/readonly fields.
-          if (element.disabled || element.readOnly) {
+          if (element && (element.disabled || element.readOnly)) {
             return false;
           }
           // Can't paste if Clipboard empty.
           let flavors = ["text/unicode"];
           return Services.clipboard.hasDataMatchingFlavors(flavors, flavors.length,
             Ci.nsIClipboard.kGlobalClipboard);
         },
       },
@@ -597,16 +608,23 @@ var ActionBarHandler = {
     return null;
   },
 
   set _contentWindow(aContentWindow) {
     this._contentWindowRef = Cu.getWeakReference(aContentWindow);
   },
 
   /**
+   * If we have an active selection, is it part of a designMode document?
+   */
+  _isInDesignMode: function(win = this._contentWindow) {
+    return this._selectionID && (win.document.designMode === "on");
+  },
+
+  /**
    * Provides the currently selected text, for either an editable,
    * or for the default contentWindow.
    */
   _getSelectedText: function() {
     // Can be called from FindInPageBar "TextSelection:Get", when there
     // is no active selection.
     if (!this._selectionID) {
       return "";
--- a/mobile/android/tests/browser/robocop/testAccessibleCarets.js
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets.js
@@ -5,17 +5,18 @@
 "use strict";
 
 var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Messaging.jsm");
 Cu.import('resource://gre/modules/Geometry.jsm');
 
 const ACCESSIBLECARET_PREF = "layout.accessiblecaret.enabled";
-const TEST_URL = "http://mochi.test:8888/tests/robocop/testAccessibleCarets.html";
+const BASE_TEST_URL = "http://mochi.test:8888/tests/robocop/testAccessibleCarets.html";
+const DESIGNMODE_TEST_URL = "http://mochi.test:8888/tests/robocop/testAccessibleCarets2.html";
 
 // Ensures Tabs are completely loaded, viewport and zoom constraints updated, etc.
 const TAB_CHANGE_EVENT = "testAccessibleCarets:TabChange";
 const TAB_STOP_EVENT = "STOP";
 
 const gChromeWin = Services.wm.getMostRecentWindow("navigator:browser");
 
 /**
@@ -99,19 +100,17 @@ function getCharPressPoint(doc, element,
   return r;
 }
 
 /**
  * Long press an element (RTL/LTR) at its calculated first character
  * position, and return the result.
  *
  * @param midPoint, The screen coord for the longpress.
- * @return {Promise}
- * @resolves The ActionBar status, including its target focused element, and
- *           the selected text that it sees.
+ * @return Selection state helper-result object.
  */
 function getLongPressResult(browser, midPoint) {
   let domWinUtils = browser.contentWindow.
     QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
 
   // AccessibleCarets expect longtap between touchstart/end.
   domWinUtils.sendTouchEventToWindow("touchstart", [0], [midPoint.x], [midPoint.y],
                                      [1], [1], [0], [1], 1, 0);
@@ -123,30 +122,52 @@ function getLongPressResult(browser, mid
   let ActionBarHandler = gChromeWin.ActionBarHandler;
   return { focusedElement: ActionBarHandler._targetElement,
            text: ActionBarHandler._getSelectedText(),
            selectionID: ActionBarHandler._selectionID,
   };
 }
 
 /**
+ * Checks the Selection UI (ActionBar or FloatingToolbar) 
+ * for the availability of an expected action.
+ *
+ * @param expectedActionID, The Selection UI action we expect to be available.
+ * @return Result boolean.
+ */
+function UIhasActionByID(expectedActionID) {
+  let actions = gChromeWin.ActionBarHandler._actionBarActions;
+  return actions.some(action => {
+    return action.id === expectedActionID;
+  });
+}
+
+/**
+ * Messages the ActionBarHandler to close the Selection UI.
+ */
+function closeSelectionUI() {
+  Services.obs.notifyObservers(null, "TextSelection:End",
+    JSON.stringify({selectionID: gChromeWin.ActionBarHandler._selectionID}));
+}
+
+/**
  * Main test method.
  */
 add_task(function* testAccessibleCarets() {
   // Wait to start loading our test page until after the initial browser tab is
   // completely loaded. This allows each tab to complete its layer initialization,
   // importantly, its viewport and zoomContraints info.
   let BrowserApp = gChromeWin.BrowserApp;
   yield do_promiseTabChangeEvent(BrowserApp.selectedTab.id, TAB_STOP_EVENT);
 
   // Ensure Gecko Selection and Touch carets are enabled.
   Services.prefs.setBoolPref(ACCESSIBLECARET_PREF, true);
 
   // Load test page, wait for load completion, register cleanup.
-  let browser = BrowserApp.addTab(TEST_URL).browser;
+  let browser = BrowserApp.addTab(BASE_TEST_URL).browser;
   let tab = BrowserApp.getTabForBrowser(browser);
   yield do_promiseTabChangeEvent(tab.id, TAB_STOP_EVENT);
 
   do_register_cleanup(function cleanup() {
     BrowserApp.closeTab(tab);
     Services.prefs.clearUserPref(ACCESSIBLECARET_PREF);
   });
 
@@ -229,12 +250,65 @@ add_task(function* testAccessibleCarets(
   is(result.focusedElement, ta_RTL_elem, "Focused element should match expected.");
   is(result.text, "הספר", "Selected text should match expected text.");
 
   result = getLongPressResult(browser, ip_RTL_midPoint);
   is(result.focusedElement, ip_RTL_elem, "Focused element should match expected.");
   is(result.text, "+972 3 7347514 ",
     "Selected phone number should match expected text.");
 
-  ok(true, "Finished all tests.");
+  // Close Selection UI (ActionBar or FloatingToolbar) and complete test.
+  closeSelectionUI();
+  ok(true, "Finished testAccessibleCarets tests.");
 });
 
+/**
+ * DesignMode test method.
+ */
+add_task(function* testAccessibleCarets_designMode() {
+  let BrowserApp = gChromeWin.BrowserApp;
+
+  // Load test page, wait for load completion.
+  let browser = BrowserApp.addTab(DESIGNMODE_TEST_URL).browser;
+  let tab = BrowserApp.getTabForBrowser(browser, { selected: true });
+  yield do_promiseTabChangeEvent(tab.id, TAB_STOP_EVENT);
+
+  // References to test document elements, ActionBarHandler.
+  let doc = browser.contentDocument;
+  let tc_LTR_elem = doc.getElementById("LTRtextContent");
+  let tc_RTL_elem = doc.getElementById("RTLtextContent");
+
+  // Locate longpress midpoints for test elements, ensure expactations.
+  let tc_LTR_midPoint = getCharPressPoint(doc, tc_LTR_elem, 5, "x");
+  let tc_RTL_midPoint = getCharPressPoint(doc, tc_RTL_elem, 9, "ת");
+
+  // Toggle designMode on/off/on, check UI expectations.
+  ["on", "off"].forEach(designMode => {
+    doc.designMode = designMode;
+
+    // Text content in a document, whether in designMode or not, never receives focus.
+    // Available ActionBar/FloatingToolbar UI actions should vary depending on mode.
+
+    let result = getLongPressResult(browser, tc_LTR_midPoint);
+    is(result.focusedElement, null, "No focused element is expected.");
+    is(result.text, "existence", "Selected text should match expected text.");
+    is(UIhasActionByID("cut_action"), (designMode === "on"),
+      "CUT action UI Visibility should match designMode state.");
+    is(UIhasActionByID("paste_action"), (designMode === "on"),
+      "PASTE action UI Visibility should match designMode state.");
+
+    result = getLongPressResult(browser, tc_RTL_midPoint);
+    is(result.focusedElement, null, "No focused element is expected.");
+    is(result.text, "אותו", "Selected text should match expected text.");
+    is(UIhasActionByID("cut_action"), (designMode === "on"),
+      "CUT action UI Visibility should match designMode state.");
+    is(UIhasActionByID("paste_action"), (designMode === "on"),
+      "PASTE action UI Visibility should match designMode state.");
+  });
+
+  // Close Selection UI (ActionBar or FloatingToolbar) and complete test.
+  closeSelectionUI();
+  ok(true, "Finished testAccessibleCarets_designMode tests.");
+});
+
+
+// Start all the test tasks.
 run_next_test();
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets2.html
@@ -0,0 +1,23 @@
+<html>
+  <head>
+    <title>ActionBar Handler and AccessibleCarets tests for DesignMode</title>
+    <meta name="viewport"
+      content="initial-scale=1, allowZoom=no, maximum-scale=1,
+               user-scalable=no, width=device-width">
+  </head>
+
+  <body>
+    <div id="LTRtextContent" style="direction: ltr;">The existence of right-handed
+        neutrinos is theoretically well-motivated, as all other known fermions have
+        been observed with left and right chirality, and they can explain the
+        observed active neutrino masses in a natural way.
+    </div>
+    <br><br><br> <!-- Rule out caret overlay on next field -->
+
+    <div id="RTLtextContent" style="direction: rtl;">זהו לא אותו הטקסט כפי למבחן שמאל לימין,
+      אבל מה לעזאזל? הסוקר שלי לעולם לא לתפוס אותי. אני רק תורם נחות מנסה להשתעשע קצת.
+    </div>
+    <br><br><br>
+
+  </body>
+</html>