// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- var BrowserSearch = { get _popup() { delete this._popup; return this._popup = document.getElementById("search-engines-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) { let button = document.createElement("button"); button.className = "prompt-button"; button.setAttribute("label", aEngine.name); button.setAttribute("crop", "end"); button.setAttribute("pack", "start"); button.setAttribute("image", aEngine.iconURI ? aEngine.iconURI.spec : null); button.onclick = function() { popup.hidden = true; BrowserUI.doOpenSearch(aEngine.name); } list.appendChild(button); }); popup.hidden = false; popup.top = BrowserUI.toolbarH - popup.offset; popup.anchorTo(document.getElementById("tool-search")); 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 PageActions = { init: function init() { document.getElementById("pageactions-container").addEventListener("click", this, true); this.register("pageaction-reset", this.updatePagePermissions, this); this.register("pageaction-password", this.updateForgetPassword, this); #ifdef NS_PRINTING this.register("pageaction-saveas", this.updatePageSaveAs, this); #endif this.register("pageaction-share", this.updateShare, this); this.register("pageaction-search", BrowserSearch.updatePageSearchEngines, BrowserSearch); }, handleEvent: function handleEvent(aEvent) { switch (aEvent.type) { case "click": getIdentityHandler().hide(); break; } }, /** * @param aId id of a pageaction element * @param aCallback function that takes an element and returns true if it should be visible * @param aThisObj (optional) scope object for aCallback */ register: function register(aId, aCallback, aThisObj) { this._handlers.push({id: aId, callback: aCallback, obj: aThisObj}); }, _handlers: [], updateSiteMenu: function updateSiteMenu() { this._handlers.forEach(function(action) { let node = document.getElementById(action.id); node.hidden = !action.callback.call(action.obj, node); }); this._updateAttributes(); }, get _loginManager() { delete this._loginManager; return this._loginManager = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); }, // Permissions we track in Page Actions _permissions: ["popup", "offline-app", "geolocation", "desktop-notification"], _forEachPermissions: function _forEachPermissions(aHost, aCallback) { let pm = Services.perms; for (let i = 0; i < this._permissions.length; i++) { let type = this._permissions[i]; if (!pm.testPermission(aHost, type)) continue; let perms = pm.enumerator; while (perms.hasMoreElements()) { let permission = perms.getNext().QueryInterface(Ci.nsIPermission); if (permission.host == aHost.asciiHost && permission.type == type) aCallback(type); } } }, updatePagePermissions: function updatePagePermissions(aNode) { let host = Browser.selectedBrowser.currentURI; let permissions = []; this._forEachPermissions(host, function(aType) { permissions.push("pageactions." + aType); }); if (!this._loginManager.getLoginSavingEnabled(host.prePath)) { // If rememberSignons is false, then getLoginSavingEnabled returns false // for all pages, so we should just ignore it (Bug 601163). if (Services.prefs.getBoolPref("signon.rememberSignons")) permissions.push("pageactions.password"); } let descriptions = permissions.map(function(s) Strings.browser.GetStringFromName(s)); aNode.setAttribute("description", descriptions.join(", ")); return (permissions.length > 0); }, updateForgetPassword: function updateForgetPassword(aNode) { let host = Browser.selectedBrowser.currentURI; let logins = this._loginManager.findLogins({}, host.prePath, "", ""); return logins.some(function(login) login.hostname == host.prePath); }, forgetPassword: function forgetPassword(aEvent) { let host = Browser.selectedBrowser.currentURI; let lm = this._loginManager; lm.findLogins({}, host.prePath, "", "").forEach(function(login) { if (login.hostname == host.prePath) lm.removeLogin(login); }); this.hideItem(aEvent.target); aEvent.stopPropagation(); // Don't hide the site menu. }, clearPagePermissions: function clearPagePermissions(aEvent) { let pm = Services.perms; let host = Browser.selectedBrowser.currentURI; this._forEachPermissions(host, function(aType) { pm.remove(host.asciiHost, aType); // reset the 'remember' counter for permissions that support it if (["geolocation", "desktop-notification"].indexOf(aType) != -1) Services.contentPrefs.setPref(host.asciiHost, aType + ".request.remember", 0); }); let lm = this._loginManager; if (!lm.getLoginSavingEnabled(host.prePath)) lm.setLoginSavingEnabled(host.prePath, true); this.hideItem(aEvent.target); aEvent.stopPropagation(); // Don't hide the site menu. }, savePageAsPDF: function saveAsPDF() { let browser = Browser.selectedBrowser; let fileName = ContentAreaUtils.getDefaultFileName(browser.contentTitle, browser.documentURI, null, null); fileName = fileName.trim() + ".pdf"; let displayName = fileName; #ifdef MOZ_PLATFORM_MAEMO fileName = fileName.replace(/[\*\:\?]+/g, " "); #endif let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); let downloadsDir = dm.defaultDownloadsDirectory; #ifdef ANDROID // Create the final destination file location // Try the intended filename, and if that doesn't work, try a safer one // (the intended one may have special characters) let file = null; [fileName, 'download.pdf'].forEach(function(potentialName, i) { if (file) return; let attemptedFile = downloadsDir.clone(); attemptedFile.append(potentialName); // The filename is used below to save the file to a temp location in // the content process. Make sure it's up to date. try { attemptedFile.createUnique(attemptedFile.NORMAL_FILE_TYPE, 0666); } catch (e) { // The first try may fail if the filename has special characters. If so // we will try with a safer name. if (i != 0) throw e; return; } file = attemptedFile; fileName = file.leafName; }); #else let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); picker.init(window, Strings.browser.GetStringFromName("pageactions.saveas.pdf"), Ci.nsIFilePicker.modeSave); picker.appendFilter("PDF", "*.pdf"); picker.defaultExtension = "pdf"; picker.defaultString = fileName; picker.displayDirectory = downloadsDir; let rv = picker.show(); if (rv == Ci.nsIFilePicker.returnCancel) return; let file = picker.file; #endif // We must manually add this to the download system let db = dm.DBConnection; let stmt = db.createStatement( "INSERT INTO moz_downloads (name, source, target, startTime, endTime, state, referrer) " + "VALUES (:name, :source, :target, :startTime, :endTime, :state, :referrer)" ); let current = browser.currentURI.spec; stmt.params.name = displayName; stmt.params.source = current; stmt.params.target = Services.io.newFileURI(file).spec; stmt.params.startTime = Date.now() * 1000; stmt.params.endTime = Date.now() * 1000; stmt.params.state = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED; stmt.params.referrer = current; stmt.execute(); stmt.finalize(); let newItemId = db.lastInsertRowID; let download = dm.getDownload(newItemId); try { DownloadsView.downloadStarted(download); } catch(e) {} Services.obs.notifyObservers(download, "dl-start", null); #ifdef ANDROID let tmpDir = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties).get("TmpD", Ci.nsIFile); file = tmpDir.clone(); file.append(fileName); #endif let data = { type: Ci.nsIPrintSettings.kOutputFormatPDF, id: newItemId, referrer: current, filePath: file.path }; Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:SaveAs", data); }, updatePageSaveAs: function updatePageSaveAs(aNode) { // Check for local XUL content let contentWindow = Browser.selectedBrowser.contentWindow; return !(contentWindow && contentWindow.document instanceof XULDocument); }, updateShare: function updateShare(aNode) { return Util.isShareableScheme(Browser.selectedBrowser.currentURI.scheme); }, hideItem: function hideItem(aNode) { aNode.hidden = true; this._updateAttributes(); }, _updateAttributes: function _updateAttributes() { let container = document.getElementById("pageactions-container"); let visibleNodes = container.querySelectorAll("pageaction:not([hidden=true])"); let visibleCount = visibleNodes.length; for (let i = 0; i < visibleCount; i++) visibleNodes[i].classList.remove("odd-last-child"); if (visibleCount % 2) visibleNodes[visibleCount - 1].classList.add("odd-last-child"); } }; 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) { BrowserUI.pushPopup(this, this.box); this._tabs.push(aTab); this._updateLabel(); this.box.top = aTab.getBoundingClientRect().top + (aTab.getBoundingClientRect().height / 3); this.box.hidden = false; 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 document.getElementById("tabs").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); zoomLevel = Math.min(Math.max(kBrowserFormZoomLevelMin, zoomLevel), 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._suggestionsContainer = document.getElementById("form-helper-suggestions-container"); 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); // Listen for events where form assistant should be closed or updated let tabs = document.getElementById("tabs"); 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 the autocomplete box Elements.browsers.addEventListener("PanBegin", this, false); Elements.browsers.addEventListener("PanFinished", this, false); window.addEventListener("AnimatedZoomBegin", this, false); window.addEventListener("AnimatedZoomEnd", this, false); }, _currentBrowser: null, show: function formHelperShow(aElement, aHasPrevious, aHasNext) { this._currentBrowser = Browser.selectedBrowser; this._currentCaretRect = null; // Update the next/previous commands this._cmdPrevious.setAttribute("disabled", !aHasPrevious); this._cmdNext.setAttribute("disabled", !aHasNext); this._hasSuggestions = false; 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, isAutocomplete: aElement.isAutocomplete, list: aElement.choices } this._updateContainerForSelect(lastElement, this._currentElement); this._zoom(Rect.fromRect(aElement.rect), Rect.fromRect(aElement.caretRect)); this._updateSuggestionsFor(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._resetSuggestions(); 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"; case "AnimatedZoomBegin": // Changing the hidden attribute here create bugs with the scrollbox // arrows because the binding will miss some underflow events if (this._hasSuggestions) this._suggestionsContainer.style.visibility = "hidden"; break; case "PanFinished": this._container.style.visibility = "visible"; case "AnimatedZoomEnd": if (this._hasSuggestions) { this._suggestionsContainer.style.visibility = "visible"; this._ensureSuggestionsVisible(); } 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.localName == "browser") return; Browser.keySender.handleEvent(aEvent); break; case "SizeChanged": setTimeout(function(self) { SelectHelperUI.sizeToContent(); self._zoom(self._currentElementRect, self._currentCaretRect); }, 0, this); break; } }, receiveMessage: function formHelperReceiveMessage(aMessage) { if (!this._open && aMessage.name != "FormAssist:Show" && aMessage.name != "FormAssist:Hide") 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