// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- /* * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Mobile Browser. * * The Initial Developer of the Original Code is * Mozilla Corporation. * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ const kBrowserFormZoomLevelMin = 0.8; const kBrowserFormZoomLevelMax = 2.0; var BrowserSearch = { get _popup() { let popup = document.getElementById("search-engines-popup"); popup.addEventListener("TapSingle", function(aEvent) { popup.hidden = true; BrowserUI.doOpenSearch(aEvent.target.getAttribute("label")); }, false); delete this._popup; return this._popup = popup; }, get _list() { delete this._list; return this._list = document.getElementById("search-engines-list"); }, get _button() { delete this._button; return this._button = document.getElementById("tool-search"); }, toggle: function bs_toggle() { if (this._popup.hidden) this.show(); else this.hide(); }, show: function bs_show() { let popup = this._popup; let list = this._list; while (list.lastChild) list.removeChild(list.lastChild); this.engines.forEach(function(aEngine, aIndex, aArray) { let button = document.createElement("button"); button.className = "action-button"; button.setAttribute("label", aEngine.name); button.setAttribute("crop", "end"); button.setAttribute("pack", "start"); button.setAttribute("image", aEngine.iconURI ? aEngine.iconURI.spec : ""); list.appendChild(button); }); popup.hidden = false; popup.top = BrowserUI.toolbarH - popup.offset; let searchButton = document.getElementById("tool-search"); let anchorPosition = ""; if (Util.isTablet()) anchorPosition = "after_start"; else if (popup.hasAttribute("left")) popup.removeAttribute("left"); popup.anchorTo(searchButton, anchorPosition); document.getElementById("urlbar-icons").setAttribute("open", "true"); BrowserUI.pushPopup(this, [popup, this._button]); }, hide: function bs_hide() { this._popup.hidden = true; document.getElementById("urlbar-icons").removeAttribute("open"); BrowserUI.popPopup(this); }, observe: function bs_observe(aSubject, aTopic, aData) { if (aTopic != "browser-search-engine-modified") return; switch (aData) { case "engine-added": case "engine-removed": // force a rebuild of the prefs list, if needed // XXX this is inefficient, shouldn't have to rebuild the entire list if (ExtensionsView._list) ExtensionsView.getAddonsFromLocal(); // fall through case "engine-changed": // XXX we should probably also update the ExtensionsView list here once // that's efficient, since the icon can change (happen during an async // installs from the web) // blow away our cache this._engines = null; break; case "engine-current": // Not relevant break; } }, get engines() { if (this._engines) return this._engines; this._engines = Services.search.getVisibleEngines({ }); return this._engines; }, updatePageSearchEngines: function updatePageSearchEngines(aNode) { let items = Browser.selectedBrowser.searchEngines.filter(this.isPermanentSearchEngine); if (!items.length) return false; // XXX limit to the first search engine for now let engine = items[0]; aNode.setAttribute("description", engine.title); aNode.onclick = function(aEvent) { BrowserSearch.addPermanentSearchEngine(engine); PageActions.hideItem(aNode); aEvent.stopPropagation(); // Don't hide the site menu. }; return true; }, addPermanentSearchEngine: function addPermanentSearchEngine(aEngine) { let iconURL = BrowserUI._favicon.src; Services.search.addEngine(aEngine.href, Ci.nsISearchEngine.DATA_XML, iconURL, false); this._engines = null; }, isPermanentSearchEngine: function isPermanentSearchEngine(aEngine) { return !BrowserSearch.engines.some(function(item) { return aEngine.title == item.name; }); } }; var NewTabPopup = { _timeout: 0, _tabs: [], init: function init() { Elements.tabs.addEventListener("TabOpen", this, true); }, get box() { delete this.box; return this.box = document.getElementById("newtab-popup"); }, _updateLabel: function nt_updateLabel() { let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened"); let label = PluralForm.get(this._tabs.length, newtabStrings).replace("#1", this._tabs.length); this.box.firstChild.setAttribute("value", label); }, hide: function nt_hide() { if (this._timeout) { clearTimeout(this._timeout); this._timeout = 0; } this._tabs = []; this.box.hidden = true; BrowserUI.popPopup(this); }, show: function nt_show(aTab) { if (Util.isTablet() && TabsPopup.visible) return; BrowserUI.pushPopup(this, this.box); this._tabs.push(aTab); this._updateLabel(); this.box.hidden = false; let tabRect = aTab.getBoundingClientRect(); this.box.top = tabRect.top + (tabRect.height / 2); // wait for layout to resolve the real size of the box setTimeout((function() { let boxRect = this.box.getBoundingClientRect(); this.box.top = tabRect.top + (tabRect.height / 2) - (boxRect.height / 2); // We don't use anchorTo() here because the tab // being anchored to might be overflowing the tabs // scrollbox which confuses the dynamic arrow direction // calculation (see bug 662520). if (Util.isTablet()) { let toolbarbutton = document.getElementById("tool-tabs"); this.box.anchorTo(toolbarbutton, "after_start"); } else if (Elements.tabList.getBoundingClientRect().left < 0) this.box.pointLeftAt(aTab); else this.box.pointRightAt(aTab); }).bind(this), 0); if (this._timeout) clearTimeout(this._timeout); this._timeout = setTimeout(function(self) { self.hide(); }, 2000, this); }, selectTab: function nt_selectTab() { BrowserUI.selectTab(this._tabs.pop()); this.hide(); }, handleEvent: function nt_handleEvent(aEvent) { // Bail early and fast if (!aEvent.detail) return; let [tabsVisibility,,,] = Browser.computeSidebarVisibility(); if (tabsVisibility != 1.0) this.show(aEvent.originalTarget); } }; var FindHelperUI = { type: "find", commands: { next: "cmd_findNext", previous: "cmd_findPrevious", close: "cmd_findClose" }, _open: false, _status: null, get status() { return this._status; }, set status(val) { if (val != this._status) { this._status = val; if (!val) this._textbox.removeAttribute("status"); else this._textbox.setAttribute("status", val); this.updateCommands(this._textbox.value); } }, init: function findHelperInit() { this._textbox = document.getElementById("find-helper-textbox"); this._container = document.getElementById("content-navigator"); this._cmdPrevious = document.getElementById(this.commands.previous); this._cmdNext = document.getElementById(this.commands.next); // Listen for find assistant messages from content messageManager.addMessageListener("FindAssist:Show", this); messageManager.addMessageListener("FindAssist:Hide", this); // Listen for pan events happening on the browsers Elements.browsers.addEventListener("PanBegin", this, false); Elements.browsers.addEventListener("PanFinished", this, false); // Listen for events where form assistant should be closed Elements.tabList.addEventListener("TabSelect", this, true); Elements.browsers.addEventListener("URLChanged", this, true); }, receiveMessage: function findHelperReceiveMessage(aMessage) { let json = aMessage.json; switch(aMessage.name) { case "FindAssist:Show": this.status = json.result; if (json.rect) this._zoom(Rect.fromRect(json.rect)); break; case "FindAssist:Hide": if (this._container.getAttribute("type") == this.type) this.hide(); break; } }, handleEvent: function findHelperHandleEvent(aEvent) { switch (aEvent.type) { case "TabSelect": this.hide(); break; case "URLChanged": if (aEvent.detail && aEvent.target == getBrowser()) this.hide(); break; case "PanBegin": this._container.style.visibility = "hidden"; this._textbox.collapsed = true; break; case "PanFinished": this._container.style.visibility = "visible"; this._textbox.collapsed = false; break; } }, show: function findHelperShow() { this._container.show(this); this.search(this._textbox.value); this._textbox.select(); this._textbox.focus(); this._open = true; // Prevent the view to scroll automatically while searching Browser.selectedBrowser.scrollSync = false; }, hide: function findHelperHide() { if (!this._open) return; this._textbox.value = ""; this.status = null; this._textbox.blur(); this._container.hide(this); this._open = false; // Restore the scroll synchronisation Browser.selectedBrowser.scrollSync = true; }, goToPrevious: function findHelperGoToPrevious() { Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Previous", { }); }, goToNext: function findHelperGoToNext() { Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Next", { }); }, search: function findHelperSearch(aValue) { this.updateCommands(aValue); // Don't bother searching if the value is empty if (aValue == "") { this.status = null; return; } Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Find", { searchString: aValue }); }, updateCommands: function findHelperUpdateCommands(aValue) { let disabled = (this._status == Ci.nsITypeAheadFind.FIND_NOTFOUND) || (aValue == ""); this._cmdPrevious.setAttribute("disabled", disabled); this._cmdNext.setAttribute("disabled", disabled); }, _zoom: function _findHelperZoom(aElementRect) { let autozoomEnabled = Services.prefs.getBoolPref("findhelper.autozoom"); if (!aElementRect || !autozoomEnabled) return; if (Browser.selectedTab.allowZoom) { let zoomLevel = Browser._getZoomLevelForRect(aElementRect); // Clamp the zoom level relatively to the default zoom level of the page let defaultZoomLevel = Browser.selectedTab.getDefaultZoomLevel(); zoomLevel = Util.clamp(zoomLevel, (defaultZoomLevel * kBrowserFormZoomLevelMin), (defaultZoomLevel * kBrowserFormZoomLevelMax)); zoomLevel = Browser.selectedTab.clampZoomLevel(zoomLevel); let zoomRect = Browser._getZoomRectForPoint(aElementRect.center().x, aElementRect.y, zoomLevel); AnimatedZoom.animateTo(zoomRect); } else { // Even if zooming is disabled we could need to reposition the view in // order to keep the element on-screen let zoomRect = Browser._getZoomRectForPoint(aElementRect.center().x, aElementRect.y, getBrowser().scale); AnimatedZoom.animateTo(zoomRect); } } }; /** * Responsible for navigating forms and filling in information. * - Navigating forms is handled by next and previous commands. * - When an element is focused, the browser view zooms in to the control. * - The caret positionning and the view are sync to keep the type * in text into view for input fields (text/textarea). * - Provides autocomplete box for input fields. */ var FormHelperUI = { type: "form", commands: { next: "cmd_formNext", previous: "cmd_formPrevious", close: "cmd_formClose" }, get enabled() { return Services.prefs.getBoolPref("formhelper.enabled"); }, init: function formHelperInit() { this._container = document.getElementById("content-navigator"); this._cmdPrevious = document.getElementById(this.commands.previous); this._cmdNext = document.getElementById(this.commands.next); // Listen for form assistant messages from content messageManager.addMessageListener("FormAssist:Show", this); messageManager.addMessageListener("FormAssist:Hide", this); messageManager.addMessageListener("FormAssist:Update", this); messageManager.addMessageListener("FormAssist:Resize", this); messageManager.addMessageListener("FormAssist:AutoComplete", this); messageManager.addMessageListener("FormAssist:ValidationMessage", this); // Listen for events where form assistant should be closed or updated let tabs = Elements.tabList; tabs.addEventListener("TabSelect", this, true); tabs.addEventListener("TabClose", this, true); Elements.browsers.addEventListener("URLChanged", this, true); Elements.browsers.addEventListener("SizeChanged", this, true); // Listen for modal dialog to show/hide the UI messageManager.addMessageListener("DOMWillOpenModalDialog", this); messageManager.addMessageListener("DOMModalDialogClosed", this); // Listen key events for fields that are non-editable window.addEventListener("keydown", this, true); window.addEventListener("keyup", this, true); window.addEventListener("keypress", this, true); // Listen some events to show/hide arrows Elements.browsers.addEventListener("PanBegin", this, false); Elements.browsers.addEventListener("PanFinished", this, false); // Dynamically enabled/disabled the form helper if needed depending on // the size of the screen let mode = Services.prefs.getIntPref("formhelper.mode"); let state = (mode == 2) ? !Util.isTablet() : !!mode; Services.prefs.setBoolPref("formhelper.enabled", state); }, _currentBrowser: null, show: function formHelperShow(aElement, aHasPrevious, aHasNext) { // Delay the operation until all resize operations generated by the // keyboard apparition are done. This avoid doing unuseful zooming // operations. if (aElement.editable && !ViewableAreaObserver.isKeyboardOpened) { this._waitForKeyboard(aElement, aHasPrevious, aHasNext); return; } this._currentBrowser = Browser.selectedBrowser; this._currentCaretRect = null; #ifndef ANDROID // Update the next/previous commands this._cmdPrevious.setAttribute("disabled", !aHasPrevious); this._cmdNext.setAttribute("disabled", !aHasNext); // If both next and previous are disabled don't bother showing arrows if (aHasNext || aHasPrevious) this._container.removeAttribute("disabled"); else this._container.setAttribute("disabled", "true"); #else // Always hide the arrows on Android this._container.setAttribute("disabled", "true"); #endif this._open = true; let lastElement = this._currentElement || null; this._currentElement = { id: aElement.id, name: aElement.name, title: aElement.title, value: aElement.value, maxLength: aElement.maxLength, type: aElement.type, choices: aElement.choices, isAutocomplete: aElement.isAutocomplete, validationMessage: aElement.validationMessage, list: aElement.list, } this._updateContainerForSelect(lastElement, this._currentElement); this._zoom(Rect.fromRect(aElement.rect), Rect.fromRect(aElement.caretRect)); this._updatePopupsFor(this._currentElement); // Prevent the view to scroll automatically while typing this._currentBrowser.scrollSync = false; }, hide: function formHelperHide() { if (!this._open) return; // Restore the scroll synchonisation this._currentBrowser.scrollSync = true; // reset current Element and Caret Rect this._currentElementRect = null; this._currentCaretRect = null; this._updateContainerForSelect(this._currentElement, null); this._currentBrowser.messageManager.sendAsyncMessage("FormAssist:Closed", { }); this._open = false; }, // for VKB that does not resize the window _currentCaretRect: null, _currentElementRect: null, handleEvent: function formHelperHandleEvent(aEvent) { if (!this._open) return; switch (aEvent.type) { case "TabSelect": case "TabClose": this.hide(); break; case "PanBegin": // The previous/next buttons should be hidden during a manual panning // operation but not doing a zoom operation since this happen on both // manual dblClick and navigation between the fields by clicking the // buttons this._container.style.visibility = "hidden"; break; case "PanFinished": this._container.style.visibility = "visible"; break; case "URLChanged": if (aEvent.detail && aEvent.target == getBrowser()) this.hide(); break; case "keydown": case "keypress": case "keyup": // Ignore event that doesn't have a view, like generated keypress event // from browser.js if (!aEvent.view) { aEvent.preventDefault(); aEvent.stopPropagation(); return; } // If the focus is not on the browser element, the key will not be sent // to the content so do it ourself let focusedElement = gFocusManager.getFocusedElementForWindow(window, true, {}); if (focusedElement && focusedElement.localName == "browser") return; Browser.keyFilter.handleEvent(aEvent); break; case "SizeChanged": setTimeout(function(self) { SelectHelperUI.sizeToContent(); self._zoom(self._currentElementRect, self._currentCaretRect); }, 0, this); break; } }, receiveMessage: function formHelperReceiveMessage(aMessage) { let allowedMessages = ["FormAssist:Show", "FormAssist:Hide", "FormAssist:AutoComplete", "FormAssist:ValidationMessage"]; if (!this._open && allowedMessages.indexOf(aMessage.name) == -1) return; let json = aMessage.json; switch (aMessage.name) { case "FormAssist:Show": // if the user has manually disabled the Form Assistant UI we still // want to show a UI for