Bug 1266322 - Fennec doesn't allow user cut/paste in etherpad
MozReview-Commit-ID: 9Mu7ZtsLE7P
--- 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>