mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-07-07 10:29:03 +00:00
633 lines
19 KiB
JavaScript
633 lines
19 KiB
JavaScript
// -*- indent-tabs-mode: nil; js-indent-level: 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/. */
|
|
"use strict";
|
|
|
|
const PHONE_REGEX = /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/; // Are we a phone #?
|
|
|
|
|
|
/**
|
|
* ActionBarHandler Object and methods. Interface between Gecko Text Selection code
|
|
* (TouchCaret, SelectionCarets, etc) and the Mobile ActionBar UI.
|
|
*/
|
|
var ActionBarHandler = {
|
|
// Error codes returned from _init().
|
|
START_TOUCH_ERROR: {
|
|
NO_CONTENT_WINDOW: "No valid content Window found.",
|
|
NONE: "",
|
|
},
|
|
|
|
_nextSelectionID: 1, // Next available.
|
|
_selectionID: null, // Unique Selection ID, assigned each time we _init().
|
|
_actionBarActions: null, // Most-recent set of actions sent to ActionBar.
|
|
|
|
/**
|
|
* Receive and act on AccessibleCarets caret state-change
|
|
* (mozcaretstatechanged) events.
|
|
*/
|
|
caretStateChangedHandler: function(e) {
|
|
// Close an open ActionBar, if carets no longer logically visible.
|
|
if (this._selectionID && !e.caretVisible) {
|
|
this._uninit(false);
|
|
return;
|
|
}
|
|
|
|
// Open a closed ActionBar if carets actually visible.
|
|
if (!this._selectionID && e.caretVisuallyVisible) {
|
|
this._init();
|
|
return;
|
|
}
|
|
|
|
// Else, update an open ActionBar.
|
|
if (this._selectionID) {
|
|
if ([this._targetElement, this._contentWindow] ===
|
|
[Services.focus.focusedElement, Services.focus.focusedWindow]) {
|
|
// We have the same focused window/element as before. Trigger "TextSelection:ActionbarStatus"
|
|
// message only if available actions differ from when last we checked.
|
|
this._sendActionBarActions();
|
|
} else {
|
|
// We have a new focused window/element pair.
|
|
this._uninit(false);
|
|
this._init();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* ActionBarHandler notification observers.
|
|
*/
|
|
observe: function(subject, topic, data) {
|
|
switch (topic) {
|
|
// User click an ActionBar button.
|
|
case "TextSelection:Action": {
|
|
if (!this._selectionID) {
|
|
break;
|
|
}
|
|
for (let type in this.actions) {
|
|
let action = this.actions[type];
|
|
if (action.id == data) {
|
|
action.action(this._targetElement, this._contentWindow);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Provide selected text to FindInPageBar on request.
|
|
case "TextSelection:Get": {
|
|
Messaging.sendRequest({
|
|
type: "TextSelection:Data",
|
|
requestId: data,
|
|
text: this._getSelectedText(),
|
|
});
|
|
|
|
this._uninit();
|
|
break;
|
|
}
|
|
|
|
// User closed ActionBar by clicking "checkmark" button.
|
|
case "TextSelection:End": {
|
|
// End the requested selection only.
|
|
if (this._selectionID == JSON.parse(data).selectionID) {
|
|
this._uninit();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when Gecko TouchCaret or SelectionCarets become visible.
|
|
*/
|
|
_init: function() {
|
|
let [element, win] = this._getSelectionTargets();
|
|
if (!win) {
|
|
return this.START_TOUCH_ERROR.NO_CONTENT_WINDOW;
|
|
}
|
|
|
|
// Hold the ActionBar ID provided by Gecko.
|
|
this._selectionID = this._nextSelectionID++;
|
|
[this._targetElement, this._contentWindow] = [element, win];
|
|
|
|
// Open the ActionBar, send it's actions list.
|
|
Messaging.sendRequest({
|
|
type: "TextSelection:ActionbarInit",
|
|
selectionID: this._selectionID,
|
|
});
|
|
this._sendActionBarActions(true);
|
|
|
|
return this.START_TOUCH_ERROR.NONE;
|
|
},
|
|
|
|
/**
|
|
* Determines the window containing the selection, and its
|
|
* editable element if present.
|
|
*/
|
|
_getSelectionTargets: function() {
|
|
let [element, win] = [Services.focus.focusedElement, Services.focus.focusedWindow];
|
|
if (!element) {
|
|
// No focused editable.
|
|
return [null, win];
|
|
}
|
|
|
|
// Return focused editable text element and its window.
|
|
if (((element instanceof HTMLInputElement) && element.mozIsTextField(false)) ||
|
|
(element instanceof HTMLTextAreaElement) ||
|
|
element.isContentEditable) {
|
|
return [element, win];
|
|
}
|
|
|
|
// Focused element can't contain text.
|
|
return [null, win];
|
|
},
|
|
|
|
/**
|
|
* Called when Gecko TouchCaret or SelectionCarets become 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;
|
|
}
|
|
|
|
// Close the ActionBar.
|
|
Messaging.sendRequest({
|
|
type: "TextSelection:ActionbarUninit",
|
|
});
|
|
|
|
// Clear the selection ID to complete the uninit(), but leave our reference
|
|
// to selectionTargets (_targetElement, _contentWindow) in case we need
|
|
// a final clearSelection().
|
|
this._selectionID = null;
|
|
|
|
// Clear selection required if triggered by self, or TextSelection icon
|
|
// actions. If called by Gecko TouchCaret/SelectionCarets state change,
|
|
// visibility state is already correct.
|
|
if (clearSelection) {
|
|
this._clearSelection();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Final UI cleanup when Actionbar is closed by icon click, or where
|
|
* we terminate selection state after before/after actionbar actions
|
|
* (Cut, Copy, Paste, Search, Share, Call).
|
|
*/
|
|
_clearSelection: function(element = this._targetElement, win = this._contentWindow) {
|
|
// Commit edit compositions, and clear focus from editables.
|
|
if (element) {
|
|
let imeSupport = this._getEditor(element, win).QueryInterface(Ci.nsIEditorIMESupport);
|
|
if (imeSupport.composing) {
|
|
imeSupport.forceCompositionEnd();
|
|
}
|
|
element.blur();
|
|
}
|
|
|
|
// Remove Selection from non-editables and now-unfocused contentEditables.
|
|
if (!element || element.isContentEditable) {
|
|
this._getSelection().removeAllRanges();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called to determine current ActionBar actions and send to TextSelection
|
|
* handler. By default we only send if current action state differs from
|
|
* the previous.
|
|
* @param By default we only send an ActionBarStatus update message if
|
|
* there is a change from the previous state. sendAlways can be
|
|
* set by init() for example, where we want to always send the
|
|
* current state.
|
|
*/
|
|
_sendActionBarActions: function(sendAlways) {
|
|
let actions = this._getActionBarActions();
|
|
|
|
if (sendAlways || actions !== this._actionBarActions) {
|
|
Messaging.sendRequest({
|
|
type: "TextSelection:ActionbarStatus",
|
|
actions: actions,
|
|
});
|
|
}
|
|
|
|
this._actionBarActions = actions;
|
|
},
|
|
|
|
/**
|
|
* Determine and return current ActionBar state.
|
|
*/
|
|
_getActionBarActions: function(element = this._targetElement, win = this._contentWindow) {
|
|
let actions = [];
|
|
|
|
for (let type in this.actions) {
|
|
let action = this.actions[type];
|
|
if (action.selector.matches(element, win)) {
|
|
let a = {
|
|
id: action.id,
|
|
label: this._getActionValue(action, "label", "", element),
|
|
icon: this._getActionValue(action, "icon", "drawable://ic_status_logo", element),
|
|
order: this._getActionValue(action, "order", 0, element),
|
|
showAsAction: this._getActionValue(action, "showAsAction", true, element),
|
|
};
|
|
actions.push(a);
|
|
}
|
|
}
|
|
actions.sort((a, b) => b.order - a.order);
|
|
|
|
return actions;
|
|
},
|
|
|
|
/**
|
|
* Provides a value from an action. If the action defines the value as a function,
|
|
* we return the result of calling the function. Otherwise, we return the value
|
|
* itself. If the value isn't defined for this action, will return a default.
|
|
*/
|
|
_getActionValue: function(obj, name, defaultValue, element) {
|
|
if (!(name in obj))
|
|
return defaultValue;
|
|
|
|
if (typeof obj[name] == "function")
|
|
return obj[name](element);
|
|
|
|
return obj[name];
|
|
},
|
|
|
|
/**
|
|
* Actionbar callback methods.
|
|
*/
|
|
actions: {
|
|
|
|
SELECT_ALL: {
|
|
id: "selectall_action",
|
|
label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
|
|
icon: "drawable://ab_select_all",
|
|
order: 5,
|
|
|
|
selector: {
|
|
matches: function(element, win) {
|
|
// For editable, check its length. For default contentWindow, assume
|
|
// true, else there'd been nothing to long-press to open ActionBar.
|
|
return (element) ? element.textLength != 0 : true;
|
|
},
|
|
},
|
|
|
|
action: function(element, win) {
|
|
// Some Mobile keyboards such as SwiftKeyboard, provide auto-suggest
|
|
// style highlights via composition selections in editables.
|
|
if (element) {
|
|
// If we have an active composition string, commit it, and
|
|
// ensure proper element focus.
|
|
let imeSupport = ActionBarHandler._getEditor(element, win).
|
|
QueryInterface(Ci.nsIEditorIMESupport);
|
|
if (imeSupport.composing) {
|
|
element.blur();
|
|
element.focus();
|
|
}
|
|
}
|
|
|
|
// Close ActionBarHandler, then selectAll, and display handles.
|
|
ActionBarHandler._getSelectAllController(element, win).selectAll();
|
|
UITelemetry.addEvent("action.1", "actionbar", null, "select_all");
|
|
},
|
|
},
|
|
|
|
CUT: {
|
|
id: "cut_action",
|
|
label: Strings.browser.GetStringFromName("contextmenu.cut"),
|
|
icon: "drawable://ab_cut",
|
|
order: 4,
|
|
|
|
selector: {
|
|
matches: function(element, win) {
|
|
// Can't cut from non-editable.
|
|
if (!element) {
|
|
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) {
|
|
return false;
|
|
}
|
|
// Allow if selected text exists.
|
|
return (ActionBarHandler._getSelectedText().length > 0);
|
|
},
|
|
},
|
|
|
|
action: function(element, win) {
|
|
// First copy the selection text to the clipboard.
|
|
let selectedText = ActionBarHandler._getSelectedText();
|
|
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
|
|
getService(Ci.nsIClipboardHelper);
|
|
clipboard.copyString(selectedText);
|
|
|
|
let msg = Strings.browser.GetStringFromName("selectionHelper.textCopied");
|
|
NativeWindow.toast.show(msg, "long");
|
|
|
|
// Then cut the selection text.
|
|
ActionBarHandler._getSelection(element, win).deleteFromDocument();
|
|
|
|
ActionBarHandler._uninit();
|
|
UITelemetry.addEvent("action.1", "actionbar", null, "cut");
|
|
},
|
|
},
|
|
|
|
COPY: {
|
|
id: "copy_action",
|
|
label: Strings.browser.GetStringFromName("contextmenu.copy"),
|
|
icon: "drawable://ab_copy",
|
|
order: 3,
|
|
|
|
selector: {
|
|
matches: function(element, win) {
|
|
// Don't allow "copy" from password fields.
|
|
if (element instanceof Ci.nsIDOMHTMLInputElement &&
|
|
!element.mozIsTextField(true)) {
|
|
return false;
|
|
}
|
|
// Allow if selected text exists.
|
|
return (ActionBarHandler._getSelectedText().length > 0);
|
|
},
|
|
},
|
|
|
|
action: function(element, win) {
|
|
let selectedText = ActionBarHandler._getSelectedText();
|
|
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
|
|
getService(Ci.nsIClipboardHelper);
|
|
clipboard.copyString(selectedText);
|
|
|
|
let msg = Strings.browser.GetStringFromName("selectionHelper.textCopied");
|
|
NativeWindow.toast.show(msg, "long");
|
|
|
|
ActionBarHandler._uninit();
|
|
UITelemetry.addEvent("action.1", "actionbar", null, "copy");
|
|
},
|
|
},
|
|
|
|
PASTE: {
|
|
id: "paste_action",
|
|
label: Strings.browser.GetStringFromName("contextmenu.paste"),
|
|
icon: "drawable://ab_paste",
|
|
order: 2,
|
|
|
|
selector: {
|
|
matches: function(element, win) {
|
|
// Can't paste into non-editable.
|
|
if (!element) {
|
|
return false;
|
|
}
|
|
// Can't paste into disabled/readonly fields.
|
|
if (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);
|
|
},
|
|
},
|
|
|
|
action: function(element, win) {
|
|
// Paste the clipboard, then close the ActionBarHandler and ActionBar.
|
|
ActionBarHandler._getEditor(element, win).
|
|
paste(Ci.nsIClipboard.kGlobalClipboard);
|
|
ActionBarHandler._uninit();
|
|
UITelemetry.addEvent("action.1", "actionbar", null, "paste");
|
|
},
|
|
},
|
|
|
|
CALL: {
|
|
id: "call_action",
|
|
label: Strings.browser.GetStringFromName("contextmenu.call"),
|
|
icon: "drawable://phone",
|
|
order: 1,
|
|
|
|
selector: {
|
|
matches: function(element, win) {
|
|
return (ActionBarHandler._getSelectedPhoneNumber() != null);
|
|
},
|
|
},
|
|
|
|
action: function(element, win) {
|
|
BrowserApp.loadURI("tel:" +
|
|
ActionBarHandler._getSelectedPhoneNumber());
|
|
|
|
ActionBarHandler._uninit();
|
|
UITelemetry.addEvent("action.1", "actionbar", null, "call");
|
|
},
|
|
},
|
|
|
|
SEARCH: {
|
|
id: "search_action",
|
|
label: Strings.browser.formatStringFromName("contextmenu.search",
|
|
[Services.search.defaultEngine.name], 1),
|
|
icon: "drawable://ab_search",
|
|
order: 1,
|
|
|
|
selector: {
|
|
matches: function(element, win) {
|
|
// Allow if selected text exists.
|
|
return (ActionBarHandler._getSelectedText().length > 0);
|
|
},
|
|
},
|
|
|
|
action: function(element, win) {
|
|
let selectedText = ActionBarHandler._getSelectedText();
|
|
ActionBarHandler._uninit();
|
|
|
|
// Set current tab as parent of new tab,
|
|
// and set new tab as private if the parent is.
|
|
let searchSubmission = Services.search.defaultEngine.getSubmission(selectedText);
|
|
let parent = BrowserApp.selectedTab;
|
|
let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
|
|
BrowserApp.addTab(searchSubmission.uri.spec,
|
|
{ parentId: parent.id,
|
|
selected: true,
|
|
isPrivate: isPrivate,
|
|
}
|
|
);
|
|
|
|
UITelemetry.addEvent("action.1", "actionbar", null, "search");
|
|
},
|
|
},
|
|
|
|
SEARCH_ADD: {
|
|
id: "search_add_action",
|
|
label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine2"),
|
|
icon: "drawable://ab_add_search_engine",
|
|
order: 0,
|
|
|
|
selector: {
|
|
matches: function(element, win) {
|
|
if(!(element instanceof HTMLInputElement)) {
|
|
return false;
|
|
}
|
|
let form = element.form;
|
|
if (!form || element.type == "password") {
|
|
return false;
|
|
}
|
|
let method = form.method.toUpperCase();
|
|
return (method == "GET" || method == "") ||
|
|
(form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
|
|
},
|
|
},
|
|
|
|
action: function(element, win) {
|
|
UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
|
|
SearchEngines.addEngine(element);
|
|
},
|
|
},
|
|
|
|
SHARE: {
|
|
id: "share_action",
|
|
label: Strings.browser.GetStringFromName("contextmenu.share"),
|
|
icon: "drawable://ic_menu_share",
|
|
order: 0,
|
|
|
|
selector: {
|
|
matches: function(element, win) {
|
|
if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
|
|
return false;
|
|
}
|
|
// Allow if selected text exists.
|
|
return (ActionBarHandler._getSelectedText().length > 0);
|
|
},
|
|
},
|
|
|
|
action: function(element, win) {
|
|
Messaging.sendRequest({
|
|
type: "Share:Text",
|
|
text: ActionBarHandler._getSelectedText(),
|
|
});
|
|
|
|
ActionBarHandler._uninit();
|
|
UITelemetry.addEvent("action.1", "actionbar", null, "share");
|
|
},
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Provides UUID service for generating action ID's.
|
|
*/
|
|
get _idService() {
|
|
delete this._idService;
|
|
return this._idService = Cc["@mozilla.org/uuid-generator;1"].
|
|
getService(Ci.nsIUUIDGenerator);
|
|
},
|
|
|
|
/**
|
|
* The targetElement holds an editable element containing a
|
|
* selection or a caret.
|
|
*/
|
|
get _targetElement() {
|
|
if (this._targetElementRef)
|
|
return this._targetElementRef.get();
|
|
return null;
|
|
},
|
|
|
|
set _targetElement(element) {
|
|
this._targetElementRef = Cu.getWeakReference(element);
|
|
},
|
|
|
|
/**
|
|
* The contentWindow holds the selection, or the targetElement
|
|
* if it's an editable.
|
|
*/
|
|
get _contentWindow() {
|
|
if (this._contentWindowRef)
|
|
return this._contentWindowRef.get();
|
|
return null;
|
|
},
|
|
|
|
set _contentWindow(aContentWindow) {
|
|
this._contentWindowRef = Cu.getWeakReference(aContentWindow);
|
|
},
|
|
|
|
/**
|
|
* 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 "";
|
|
}
|
|
|
|
let selection = this._getSelection();
|
|
|
|
// Textarea can contain LF, etc.
|
|
if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
|
|
let flags = Ci.nsIDocumentEncoder.OutputPreformatted |
|
|
Ci.nsIDocumentEncoder.OutputRaw;
|
|
return selection.QueryInterface(Ci.nsISelectionPrivate).
|
|
toStringWithFormat("text/plain", flags, 0);
|
|
}
|
|
|
|
// Selection text gets trimmed up.
|
|
return selection.toString().trim();
|
|
},
|
|
|
|
/**
|
|
* Provides the nsISelection for either an editor, or from the
|
|
* default window.
|
|
*/
|
|
_getSelection: function(element = this._targetElement, win = this._contentWindow) {
|
|
return (element instanceof Ci.nsIDOMNSEditableElement) ?
|
|
this._getEditor(element).selection :
|
|
win.getSelection();
|
|
},
|
|
|
|
/**
|
|
* Returns an nsEditor or nsHTMLEditor.
|
|
*/
|
|
_getEditor: function(element = this._targetElement, win = this._contentWindow) {
|
|
if (element instanceof Ci.nsIDOMNSEditableElement) {
|
|
return element.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
|
|
}
|
|
|
|
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
|
|
QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession).
|
|
getEditorForWindow(win);
|
|
},
|
|
|
|
/**
|
|
* Returns a selection controller.
|
|
*/
|
|
_getSelectionController: function(element = this._targetElement, win = this._contentWindow) {
|
|
if (element instanceof Ci.nsIDOMNSEditableElement) {
|
|
return this._getEditor(element, win).selectionController;
|
|
}
|
|
|
|
return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation).
|
|
QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay).
|
|
QueryInterface(Ci.nsISelectionController);
|
|
},
|
|
|
|
/**
|
|
* For selectAll(), provides the editor, or the default window selection Controller.
|
|
*/
|
|
_getSelectAllController: function(element = this._targetElement, win = this._contentWindow) {
|
|
let editor = this._getEditor(element, win);
|
|
return (editor) ?
|
|
editor : this._getSelectionController(element, win);
|
|
},
|
|
|
|
/**
|
|
* Call / Phone Helper methods.
|
|
*/
|
|
_getSelectedPhoneNumber: function() {
|
|
let selectedText = this._getSelectedText().trim();
|
|
return this._isPhoneNumber(selectedText) ?
|
|
selectedText : null;
|
|
},
|
|
|
|
_isPhoneNumber: function(selectedText) {
|
|
return (PHONE_REGEX.test(selectedText));
|
|
},
|
|
};
|