/* -*- 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/. */ var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; var Cr = Components.results; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", "resource://gre/modules/BrowserUtils.jsm"); var global = this; // Lazily load the finder code addMessageListener("Finder:Initialize", function () { let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {}); new RemoteFinderListener(global); }); var ClickEventHandler = { init: function init() { this._scrollable = null; this._scrolldir = ""; this._startX = null; this._startY = null; this._screenX = null; this._screenY = null; this._lastFrame = null; this.autoscrollLoop = this.autoscrollLoop.bind(this); Services.els.addSystemEventListener(global, "mousedown", this, true); addMessageListener("Autoscroll:Stop", this); }, isAutoscrollBlocker: function(node) { let mmPaste = Services.prefs.getBoolPref("middlemouse.paste"); let mmScrollbarPosition = Services.prefs.getBoolPref("middlemouse.scrollbarPosition"); while (node) { if ((node instanceof content.HTMLAnchorElement || node instanceof content.HTMLAreaElement) && node.hasAttribute("href")) { return true; } if (mmPaste && (node instanceof content.HTMLInputElement || node instanceof content.HTMLTextAreaElement)) { return true; } if (node instanceof content.XULElement && mmScrollbarPosition && (node.localName == "scrollbar" || node.localName == "scrollcorner")) { return true; } node = node.parentNode; } return false; }, findNearestScrollableElement: function(aNode) { // this is a list of overflow property values that allow scrolling const scrollingAllowed = ['scroll', 'auto']; // go upward in the DOM and find any parent element that has a overflow // area and can therefore be scrolled for (this._scrollable = aNode; this._scrollable; this._scrollable = this._scrollable.parentNode) { // do not use overflow based autoscroll for and // Elements or non-html elements such as svg or Document nodes // also make sure to skip select elements that are not multiline if (!(this._scrollable instanceof content.HTMLElement) || ((this._scrollable instanceof content.HTMLSelectElement) && !this._scrollable.multiple)) { continue; } var overflowx = this._scrollable.ownerDocument.defaultView .getComputedStyle(this._scrollable, '') .getPropertyValue('overflow-x'); var overflowy = this._scrollable.ownerDocument.defaultView .getComputedStyle(this._scrollable, '') .getPropertyValue('overflow-y'); // we already discarded non-multiline selects so allow vertical // scroll for multiline ones directly without checking for a // overflow property var scrollVert = this._scrollable.scrollTopMax && (this._scrollable instanceof content.HTMLSelectElement || scrollingAllowed.indexOf(overflowy) >= 0); // do not allow horizontal scrolling for select elements, it leads // to visual artifacts and is not the expected behavior anyway if (!(this._scrollable instanceof content.HTMLSelectElement) && this._scrollable.scrollLeftMin != this._scrollable.scrollLeftMax && scrollingAllowed.indexOf(overflowx) >= 0) { this._scrolldir = scrollVert ? "NSEW" : "EW"; break; } else if (scrollVert) { this._scrolldir = "NS"; break; } } if (!this._scrollable) { this._scrollable = aNode.ownerDocument.defaultView; if (this._scrollable.scrollMaxX != this._scrollable.scrollMinX) { this._scrolldir = this._scrollable.scrollMaxY != this._scrollable.scrollMinY ? "NSEW" : "EW"; } else if (this._scrollable.scrollMaxY != this._scrollable.scrollMinY) { this._scrolldir = "NS"; } else if (this._scrollable.frameElement) { this.findNearestScrollableElement(this._scrollable.frameElement); } else { this._scrollable = null; // abort scrolling } } }, startScroll: function(event) { this.findNearestScrollableElement(event.originalTarget); if (!this._scrollable) return; let [enabled] = sendSyncMessage("Autoscroll:Start", {scrolldir: this._scrolldir, screenX: event.screenX, screenY: event.screenY}); if (!enabled) { this._scrollable = null; return; } Services.els.addSystemEventListener(global, "mousemove", this, true); addEventListener("pagehide", this, true); this._ignoreMouseEvents = true; this._startX = event.screenX; this._startY = event.screenY; this._screenX = event.screenX; this._screenY = event.screenY; this._scrollErrorX = 0; this._scrollErrorY = 0; this._lastFrame = content.performance.now(); content.requestAnimationFrame(this.autoscrollLoop); }, stopScroll: function() { if (this._scrollable) { this._scrollable.mozScrollSnap(); this._scrollable = null; Services.els.removeSystemEventListener(global, "mousemove", this, true); removeEventListener("pagehide", this, true); } }, accelerate: function(curr, start) { const speed = 12; var val = (curr - start) / speed; if (val > 1) return val * Math.sqrt(val) - 1; if (val < -1) return val * Math.sqrt(-val) + 1; return 0; }, roundToZero: function(num) { if (num > 0) return Math.floor(num); return Math.ceil(num); }, autoscrollLoop: function(timestamp) { if (!this._scrollable) { // Scrolling has been canceled return; } // avoid long jumps when the browser hangs for more than // |maxTimeDelta| ms const maxTimeDelta = 100; var timeDelta = Math.min(maxTimeDelta, timestamp - this._lastFrame); // we used to scroll |accelerate()| pixels every 20ms (50fps) var timeCompensation = timeDelta / 20; this._lastFrame = timestamp; var actualScrollX = 0; var actualScrollY = 0; // don't bother scrolling vertically when the scrolldir is only horizontal // and the other way around if (this._scrolldir != 'EW') { var y = this.accelerate(this._screenY, this._startY) * timeCompensation; var desiredScrollY = this._scrollErrorY + y; actualScrollY = this.roundToZero(desiredScrollY); this._scrollErrorY = (desiredScrollY - actualScrollY); } if (this._scrolldir != 'NS') { var x = this.accelerate(this._screenX, this._startX) * timeCompensation; var desiredScrollX = this._scrollErrorX + x; actualScrollX = this.roundToZero(desiredScrollX); this._scrollErrorX = (desiredScrollX - actualScrollX); } const kAutoscroll = 15; // defined in mozilla/layers/ScrollInputMethods.h Services.telemetry.getHistogramById("SCROLL_INPUT_METHODS").add(kAutoscroll); this._scrollable.scrollBy({ left: actualScrollX, top: actualScrollY, behavior: "instant" }); content.requestAnimationFrame(this.autoscrollLoop); }, handleEvent: function(event) { if (event.type == "mousemove") { this._screenX = event.screenX; this._screenY = event.screenY; } else if (event.type == "mousedown") { if (event.isTrusted & !event.defaultPrevented && event.button == 1 && !this._scrollable && !this.isAutoscrollBlocker(event.originalTarget)) { this.startScroll(event); } } else if (event.type == "pagehide") { if (this._scrollable) { var doc = this._scrollable.ownerDocument || this._scrollable.document; if (doc == event.target) { sendAsyncMessage("Autoscroll:Cancel"); } } } }, receiveMessage: function(msg) { switch (msg.name) { case "Autoscroll:Stop": { this.stopScroll(); break; } } }, }; ClickEventHandler.init(); var PopupBlocking = { popupData: null, popupDataInternal: null, init: function() { addEventListener("DOMPopupBlocked", this, true); addEventListener("pageshow", this, true); addEventListener("pagehide", this, true); addMessageListener("PopupBlocking:UnblockPopup", this); addMessageListener("PopupBlocking:GetBlockedPopupList", this); }, receiveMessage: function(msg) { switch (msg.name) { case "PopupBlocking:UnblockPopup": { let i = msg.data.index; if (this.popupData && this.popupData[i]) { let data = this.popupData[i]; let internals = this.popupDataInternal[i]; let dwi = internals.requestingWindow; // If we have a requesting window and the requesting document is // still the current document, open the popup. if (dwi && dwi.document == internals.requestingDocument) { dwi.open(data.popupWindowURIspec, data.popupWindowName, data.popupWindowFeatures); } } break; } case "PopupBlocking:GetBlockedPopupList": { let popupData = []; let length = this.popupData ? this.popupData.length : 0; // Limit 15 popup URLs to be reported through the UI length = Math.min(length, 15); for (let i = 0; i < length; i++) { let popupWindowURIspec = this.popupData[i].popupWindowURIspec; if (popupWindowURIspec == global.content.location.href) { popupWindowURIspec = ""; } else { // Limit 500 chars to be sent because the URI will be cropped // by the UI anyway, and data: URIs can be significantly larger. popupWindowURIspec = popupWindowURIspec.substring(0, 500) } popupData.push({popupWindowURIspec}); } sendAsyncMessage("PopupBlocking:ReplyGetBlockedPopupList", {popupData}); break; } } }, handleEvent: function(ev) { switch (ev.type) { case "DOMPopupBlocked": return this.onPopupBlocked(ev); case "pageshow": return this.onPageShow(ev); case "pagehide": return this.onPageHide(ev); } return undefined; }, onPopupBlocked: function(ev) { if (!this.popupData) { this.popupData = new Array(); this.popupDataInternal = new Array(); } let obj = { popupWindowURIspec: ev.popupWindowURI ? ev.popupWindowURI.spec : "about:blank", popupWindowFeatures: ev.popupWindowFeatures, popupWindowName: ev.popupWindowName }; let internals = { requestingWindow: ev.requestingWindow, requestingDocument: ev.requestingWindow.document, }; this.popupData.push(obj); this.popupDataInternal.push(internals); this.updateBlockedPopups(true); }, onPageShow: function(ev) { if (this.popupData) { let i = 0; while (i < this.popupData.length) { // Filter out irrelevant reports. if (this.popupDataInternal[i].requestingWindow && (this.popupDataInternal[i].requestingWindow.document == this.popupDataInternal[i].requestingDocument)) { i++; } else { this.popupData.splice(i, 1); this.popupDataInternal.splice(i, 1); } } if (this.popupData.length == 0) { this.popupData = null; this.popupDataInternal = null; } this.updateBlockedPopups(false); } }, onPageHide: function(ev) { if (this.popupData) { this.popupData = null; this.popupDataInternal = null; this.updateBlockedPopups(false); } }, updateBlockedPopups: function(freshPopup) { sendAsyncMessage("PopupBlocking:UpdateBlockedPopups", { count: this.popupData ? this.popupData.length : 0, freshPopup }); }, }; PopupBlocking.init(); XPCOMUtils.defineLazyGetter(this, "console", () => { // Set up console.* for frame scripts. let Console = Components.utils.import("resource://gre/modules/Console.jsm", {}); return new Console.ConsoleAPI(); }); var Printing = { // Bug 1088061: nsPrintEngine's DoCommonPrint currently expects the // progress listener passed to it to QI to an nsIPrintingPromptService // in order to know that a printing progress dialog has been shown. That's // really all the interface is used for, hence the fact that I don't actually // implement the interface here. Bug 1088061 has been filed to remove // this hackery. QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsIPrintingPromptService]), MESSAGES: [ "Printing:Preview:Enter", "Printing:Preview:Exit", "Printing:Preview:Navigate", "Printing:Preview:ParseDocument", "Printing:Preview:UpdatePageCount", "Printing:Print", ], init() { this.MESSAGES.forEach(msgName => addMessageListener(msgName, this)); addEventListener("PrintingError", this, true); }, get shouldSavePrintSettings() { return Services.prefs.getBoolPref("print.use_global_printsettings", false) && Services.prefs.getBoolPref("print.save_print_settings", false); }, handleEvent(event) { if (event.type == "PrintingError") { let win = event.target.defaultView; let wbp = win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebBrowserPrint); let nsresult = event.detail; sendAsyncMessage("Printing:Error", { isPrinting: wbp.doingPrint, nsresult: nsresult, }); } }, receiveMessage(message) { let objects = message.objects; let data = message.data; switch (message.name) { case "Printing:Preview:Enter": { this.enterPrintPreview(Services.wm.getOuterWindowWithId(data.windowID), data.simplifiedMode); break; } case "Printing:Preview:Exit": { this.exitPrintPreview(); break; } case "Printing:Preview:Navigate": { this.navigate(data.navType, data.pageNum); break; } case "Printing:Preview:ParseDocument": { this.parseDocument(data.URL, Services.wm.getOuterWindowWithId(data.windowID)); break; } case "Printing:Preview:UpdatePageCount": { this.updatePageCount(); break; } case "Printing:Print": { this.print(Services.wm.getOuterWindowWithId(data.windowID), data.simplifiedMode); break; } } }, getPrintSettings() { try { let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"] .getService(Ci.nsIPrintSettingsService); let printSettings = PSSVC.globalPrintSettings; if (!printSettings.printerName) { printSettings.printerName = PSSVC.defaultPrinterName; } // First get any defaults from the printer try { PSSVC.initPrintSettingsFromPrinter(printSettings.printerName, printSettings); } catch(e) { // The printer name specified was invalid or there was an O.S. error. Components.utils.reportError("Invalid printer: " + printSettings.printerName); Services.prefs.clearUserPref("print.print_printer"); // Try again with default printSettings.printerName = PSSVC.defaultPrinterName; PSSVC.initPrintSettingsFromPrinter(printSettings.printerName, printSettings); } // now augment them with any values from last time PSSVC.initPrintSettingsFromPrefs(printSettings, true, printSettings.kInitSaveAll); return printSettings; } catch (e) { Components.utils.reportError(e); } return null; }, parseDocument(URL, contentWindow) { // By using ReaderMode primitives, we parse given document and place the // resulting JS object into the DOM of current browser. let articlePromise = ReaderMode.parseDocument(contentWindow.document).catch(Cu.reportError); articlePromise.then(function (article) { // We make use of a web progress listener in order to know when the content we inject // into the DOM has finished rendering. If our layout engine is still painting, we // will wait for MozAfterPaint event to be fired. let webProgressListener = { onStateChange: function (webProgress, req, flags, status) { if (flags & Ci.nsIWebProgressListener.STATE_STOP) { webProgress.removeProgressListener(webProgressListener); let domUtils = content.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); // Here we tell the parent that we have parsed the document successfully // using ReaderMode primitives and we are able to enter on preview mode. if (domUtils.isMozAfterPaintPending) { addEventListener("MozAfterPaint", function onPaint() { removeEventListener("MozAfterPaint", onPaint); sendAsyncMessage("Printing:Preview:ReaderModeReady"); }); } else { sendAsyncMessage("Printing:Preview:ReaderModeReady"); } } }, QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference, Ci.nsIObserver, ]), }; // Here we QI the docShell into a nsIWebProgress passing our web progress listener in. let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebProgress); webProgress.addProgressListener(webProgressListener, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST); content.document.head.innerHTML = ""; // Set title of document content.document.title = article.title; // Set base URI of document. Print preview code will read this value to // populate the URL field in print settings so that it doesn't show // "about:blank" as its URI. let headBaseElement = content.document.createElement("base"); headBaseElement.setAttribute("href", URL); content.document.head.appendChild(headBaseElement); // Create link element referencing aboutReader.css and append it to head let headStyleElement = content.document.createElement("link"); headStyleElement.setAttribute("rel", "stylesheet"); headStyleElement.setAttribute("href", "chrome://global/skin/aboutReader.css"); headStyleElement.setAttribute("type", "text/css"); content.document.head.appendChild(headStyleElement); content.document.body.innerHTML = ""; // Create container div (main element) and append it to body let containerElement = content.document.createElement("div"); containerElement.setAttribute("id", "container"); content.document.body.appendChild(containerElement); // Create header div and append it to container let headerElement = content.document.createElement("div"); headerElement.setAttribute("id", "reader-header"); headerElement.setAttribute("class", "header"); containerElement.appendChild(headerElement); // Create style element for header div and import simplifyMode.css let controlHeaderStyle = content.document.createElement("style"); controlHeaderStyle.setAttribute("scoped", ""); controlHeaderStyle.textContent = "@import url(\"chrome://global/content/simplifyMode.css\");"; headerElement.appendChild(controlHeaderStyle); // Jam the article's title and byline into header div let titleElement = content.document.createElement("h1"); titleElement.setAttribute("id", "reader-title"); titleElement.textContent = article.title; headerElement.appendChild(titleElement); let bylineElement = content.document.createElement("div"); bylineElement.setAttribute("id", "reader-credits"); bylineElement.setAttribute("class", "credits"); bylineElement.textContent = article.byline; headerElement.appendChild(bylineElement); // Display header element headerElement.style.display = "block"; // Create content div and append it to container let contentElement = content.document.createElement("div"); contentElement.setAttribute("class", "content"); containerElement.appendChild(contentElement); // Jam the article's content into content div let readerContent = content.document.createElement("div"); readerContent.setAttribute("id", "moz-reader-content"); contentElement.appendChild(readerContent); let articleUri = Services.io.newURI(article.url, null, null); let parserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils); let contentFragment = parserUtils.parseFragment(article.content, Ci.nsIParserUtils.SanitizerDropForms | Ci.nsIParserUtils.SanitizerAllowStyle, false, articleUri, readerContent); readerContent.appendChild(contentFragment); // Display reader content element readerContent.style.display = "block"; }); }, enterPrintPreview(contentWindow, simplifiedMode) { // We'll call this whenever we've finished reflowing the document, or if // we errored out while attempting to print preview (in which case, we'll // notify the parent that we've failed). let notifyEntered = (error) => { removeEventListener("printPreviewUpdate", onPrintPreviewReady); sendAsyncMessage("Printing:Preview:Entered", { failed: !!error, }); }; let onPrintPreviewReady = () => { notifyEntered(); }; // We have to wait for the print engine to finish reflowing all of the // documents and subdocuments before we can tell the parent to flip to // the print preview UI - otherwise, the print preview UI might ask for // information (like the number of pages in the document) before we have // our PresShells set up. addEventListener("printPreviewUpdate", onPrintPreviewReady); try { let printSettings = this.getPrintSettings(); // If we happen to be on simplified mode, we need to set docURL in order // to generate header/footer content correctly, since simplified tab has // "about:blank" as its URI. if (printSettings && simplifiedMode) printSettings.docURL = contentWindow.document.baseURI; docShell.printPreview.printPreview(printSettings, contentWindow, this); } catch (error) { // This might fail if we, for example, attempt to print a XUL document. // In that case, we inform the parent to bail out of print preview. Components.utils.reportError(error); notifyEntered(error); } }, exitPrintPreview() { docShell.printPreview.exitPrintPreview(); }, print(contentWindow, simplifiedMode) { let printSettings = this.getPrintSettings(); let rv = Cr.NS_OK; // If we happen to be on simplified mode, we need to set docURL in order // to generate header/footer content correctly, since simplified tab has // "about:blank" as its URI. if (printSettings && simplifiedMode) { printSettings.docURL = contentWindow.document.baseURI; } try { let print = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebBrowserPrint); if (print.doingPrintPreview) { this.logKeyedTelemetry("PRINT_DIALOG_OPENED_COUNT", "FROM_PREVIEW"); } else { this.logKeyedTelemetry("PRINT_DIALOG_OPENED_COUNT", "FROM_PAGE"); } print.print(printSettings, null); if (print.doingPrintPreview) { if (simplifiedMode) { this.logKeyedTelemetry("PRINT_COUNT", "SIMPLIFIED"); } else { this.logKeyedTelemetry("PRINT_COUNT", "WITH_PREVIEW"); } } else { this.logKeyedTelemetry("PRINT_COUNT", "WITHOUT_PREVIEW"); } } catch (e) { // Pressing cancel is expressed as an NS_ERROR_ABORT return value, // causing an exception to be thrown which we catch here. if (e.result != Cr.NS_ERROR_ABORT) { Cu.reportError(`In Printing:Print:Done handler, got unexpected rv ${e.result}.`); sendAsyncMessage("Printing:Error", { isPrinting: true, nsresult: e.result, }); } } if (this.shouldSavePrintSettings) { let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"] .getService(Ci.nsIPrintSettingsService); PSSVC.savePrintSettingsToPrefs(printSettings, true, printSettings.kInitSaveAll); PSSVC.savePrintSettingsToPrefs(printSettings, false, printSettings.kInitSavePrinterName); } }, logKeyedTelemetry(id, key) { let histogram = Services.telemetry.getKeyedHistogramById(id); histogram.add(key); }, updatePageCount() { let numPages = docShell.printPreview.printPreviewNumPages; sendAsyncMessage("Printing:Preview:UpdatePageCount", { numPages: numPages, }); }, navigate(navType, pageNum) { docShell.printPreview.printPreviewNavigate(navType, pageNum); }, /* nsIWebProgressListener for print preview */ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { sendAsyncMessage("Printing:Preview:StateChange", { stateFlags: aStateFlags, status: aStatus, }); }, onProgressChange(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { sendAsyncMessage("Printing:Preview:ProgressChange", { curSelfProgress: aCurSelfProgress, maxSelfProgress: aMaxSelfProgress, curTotalProgress: aCurTotalProgress, maxTotalProgress: aMaxTotalProgress, }); }, onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {}, onSecurityChange(aWebProgress, aRequest, aState) {}, } Printing.init(); function SwitchDocumentDirection(aWindow) { // document.dir can also be "auto", in which case it won't change if (aWindow.document.dir == "ltr" || aWindow.document.dir == "") { aWindow.document.dir = "rtl"; } else if (aWindow.document.dir == "rtl") { aWindow.document.dir = "ltr"; } for (let run = 0; run < aWindow.frames.length; run++) { SwitchDocumentDirection(aWindow.frames[run]); } } addMessageListener("SwitchDocumentDirection", () => { SwitchDocumentDirection(content.window); }); var FindBar = { /* Please keep in sync with toolkit/content/widgets/findbar.xml */ FIND_NORMAL: 0, FIND_TYPEAHEAD: 1, FIND_LINKS: 2, _findMode: 0, init() { addMessageListener("Findbar:UpdateState", this); Services.els.addSystemEventListener(global, "keypress", this, false); Services.els.addSystemEventListener(global, "mouseup", this, false); }, receiveMessage(msg) { switch (msg.name) { case "Findbar:UpdateState": this._findMode = msg.data.findMode; break; } }, handleEvent(event) { switch (event.type) { case "keypress": this._onKeypress(event); break; case "mouseup": this._onMouseup(event); break; } }, /** * Returns whether FAYT can be used for the given event in * the current content state. */ _canAndShouldFastFind() { let should = false; let can = BrowserUtils.canFastFind(content); if (can) { // XXXgijs: why all these shenanigans? Why not use the event's target? let focusedWindow = {}; let elt = Services.focus.getFocusedElementForWindow(content, true, focusedWindow); let win = focusedWindow.value; should = BrowserUtils.shouldFastFind(elt, win); } return { can, should } }, _onKeypress(event) { // Useless keys: if (event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented) { return undefined; } // Check the focused element etc. let fastFind = this._canAndShouldFastFind(); // Can we even use find in this page at all? if (!fastFind.can) { return undefined; } let fakeEvent = {}; for (let k in event) { if (typeof event[k] != "object" && typeof event[k] != "function" && !(k in content.KeyboardEvent)) { fakeEvent[k] = event[k]; } } // sendSyncMessage returns an array of the responses from all listeners let rv = sendSyncMessage("Findbar:Keypress", { fakeEvent: fakeEvent, shouldFastFind: fastFind.should }); if (rv.indexOf(false) !== -1) { event.preventDefault(); return false; } return undefined; }, _onMouseup(event) { if (this._findMode != this.FIND_NORMAL) sendAsyncMessage("Findbar:Mouseup"); }, }; FindBar.init(); let WebChannelMessageToChromeListener = { // Preference containing the list (space separated) of origins that are // allowed to send non-string values through a WebChannel, mainly for // backwards compatability. See bug 1238128 for more information. URL_WHITELIST_PREF: "webchannel.allowObject.urlWhitelist", // Cached list of whitelisted principals, we avoid constructing this if the // value in `_lastWhitelistValue` hasn't changed since we constructed it last. _cachedWhitelist: [], _lastWhitelistValue: "", init() { addEventListener("WebChannelMessageToChrome", e => { this._onMessageToChrome(e); }, true, true); }, _getWhitelistedPrincipals() { let whitelist = Services.prefs.getCharPref(this.URL_WHITELIST_PREF); if (whitelist != this._lastWhitelistValue) { let urls = whitelist.split(/\s+/); this._cachedWhitelist = urls.map(origin => Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(origin)); } return this._cachedWhitelist; }, _onMessageToChrome(e) { // If target is window then we want the document principal, otherwise fallback to target itself. let principal = e.target.nodePrincipal ? e.target.nodePrincipal : e.target.document.nodePrincipal; if (e.detail) { if (typeof e.detail != 'string') { // Check if the principal is one of the ones that's allowed to send // non-string values for e.detail. let objectsAllowed = this._getWhitelistedPrincipals().some(whitelisted => principal.originNoSuffix == whitelisted.originNoSuffix); if (!objectsAllowed) { Cu.reportError("WebChannelMessageToChrome sent with an object from a non-whitelisted principal"); return; } } sendAsyncMessage("WebChannelMessageToChrome", e.detail, { eventTarget: e.target }, principal); } else { Cu.reportError("WebChannel message failed. No message detail."); } } }; WebChannelMessageToChromeListener.init(); // This should be kept in sync with /browser/base/content.js. // Add message listener for "WebChannelMessageToContent" messages from chrome scripts. addMessageListener("WebChannelMessageToContent", function (e) { if (e.data) { // e.objects.eventTarget will be defined if sending a response to // a WebChannelMessageToChrome event. An unsolicited send // may not have an eventTarget defined, in this case send to the // main content window. let eventTarget = e.objects.eventTarget || content; // Use nodePrincipal if available, otherwise fallback to document principal. let targetPrincipal = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget.document.nodePrincipal : eventTarget.nodePrincipal; if (e.principal.subsumes(targetPrincipal)) { // If eventTarget is a window, use it as the targetWindow, otherwise // find the window that owns the eventTarget. let targetWindow = eventTarget instanceof Ci.nsIDOMWindow ? eventTarget : eventTarget.ownerDocument.defaultView; eventTarget.dispatchEvent(new targetWindow.CustomEvent("WebChannelMessageToContent", { detail: Cu.cloneInto({ id: e.data.id, message: e.data.message, }, targetWindow), })); } else { Cu.reportError("WebChannel message failed. Principal mismatch."); } } else { Cu.reportError("WebChannel message failed. No message data."); } }); var AudioPlaybackListener = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), init() { Services.obs.addObserver(this, "audio-playback", false); Services.obs.addObserver(this, "AudioFocusChanged", false); Services.obs.addObserver(this, "MediaControl", false); addMessageListener("AudioPlayback", this); addEventListener("unload", () => { AudioPlaybackListener.uninit(); }); }, uninit() { Services.obs.removeObserver(this, "audio-playback"); Services.obs.removeObserver(this, "AudioFocusChanged"); Services.obs.removeObserver(this, "MediaControl"); removeMessageListener("AudioPlayback", this); }, handleMediaControlMessage(msg) { let utils = global.content.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let suspendTypes = Ci.nsISuspendedTypes; switch (msg) { case "mute": utils.audioMuted = true; break; case "unmute": utils.audioMuted = false; break; case "lostAudioFocus": utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE_DISPOSABLE; break; case "lostAudioFocusTransiently": utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE; break; case "gainAudioFocus": utils.mediaSuspend = suspendTypes.NONE_SUSPENDED; break; case "mediaControlPaused": utils.mediaSuspend = suspendTypes.SUSPENDED_PAUSE_DISPOSABLE; break; case "mediaControlStopped": utils.mediaSuspend = suspendTypes.SUSPENDED_STOP_DISPOSABLE; break; case "blockInactivePageMedia": utils.mediaSuspend = suspendTypes.SUSPENDED_BLOCK; break; case "resumeMedia": utils.mediaSuspend = suspendTypes.NONE_SUSPENDED; break; default: dump("Error : wrong media control msg!\n"); break; } }, observe(subject, topic, data) { if (topic === "audio-playback") { if (subject && subject.top == global.content) { let name = "AudioPlayback:"; if (data === "block") { name += "Block"; } else { name += (data === "active") ? "Start" : "Stop"; } sendAsyncMessage(name); } } else if (topic == "AudioFocusChanged" || topic == "MediaControl") { this.handleMediaControlMessage(data); } }, receiveMessage(msg) { if (msg.name == "AudioPlayback") { this.handleMediaControlMessage(msg.data.type); } }, }; AudioPlaybackListener.init(); addMessageListener("Browser:PurgeSessionHistory", function BrowserPurgeHistory() { let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; if (!sessionHistory) { return; } // place the entry at current index at the end of the history list, so it won't get removed if (sessionHistory.index < sessionHistory.count - 1) { let indexEntry = sessionHistory.getEntryAtIndex(sessionHistory.index, false); sessionHistory.QueryInterface(Components.interfaces.nsISHistoryInternal); indexEntry.QueryInterface(Components.interfaces.nsISHEntry); sessionHistory.addEntry(indexEntry, true); } let purge = sessionHistory.count; if (global.content.location.href != "about:blank") { --purge; // Don't remove the page the user's staring at from shistory } if (purge > 0) { sessionHistory.PurgeHistory(purge); } }); var ViewSelectionSource = { init: function () { addMessageListener("ViewSource:GetSelection", this); }, receiveMessage: function(message) { if (message.name == "ViewSource:GetSelection") { let selectionDetails; try { selectionDetails = message.objects.target ? this.getMathMLSelection(message.objects.target) : this.getSelection(); } finally { sendAsyncMessage("ViewSource:GetSelectionDone", selectionDetails); } } }, /** * A helper to get a path like FIXptr, but with an array instead of the * "tumbler" notation. * See FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm */ getPath: function(ancestor, node) { var n = node; var p = n.parentNode; if (n == ancestor || !p) return null; var path = new Array(); if (!path) return null; do { for (var i = 0; i < p.childNodes.length; i++) { if (p.childNodes.item(i) == n) { path.push(i); break; } } n = p; p = n.parentNode; } while (n != ancestor && p); return path; }, getSelection: function () { // These are markers used to delimit the selection during processing. They // are removed from the final rendering. // We use noncharacter Unicode codepoints to minimize the risk of clashing // with anything that might legitimately be present in the document. // U+FDD0..FDEF const MARK_SELECTION_START = "\uFDD0"; const MARK_SELECTION_END = "\uFDEF"; var focusedWindow = Services.focus.focusedWindow || content; var selection = focusedWindow.getSelection(); var range = selection.getRangeAt(0); var ancestorContainer = range.commonAncestorContainer; var doc = ancestorContainer.ownerDocument; var startContainer = range.startContainer; var endContainer = range.endContainer; var startOffset = range.startOffset; var endOffset = range.endOffset; // let the ancestor be an element var Node = doc.defaultView.Node; if (ancestorContainer.nodeType == Node.TEXT_NODE || ancestorContainer.nodeType == Node.CDATA_SECTION_NODE) ancestorContainer = ancestorContainer.parentNode; // for selectAll, let's use the entire document, including ... // @see nsDocumentViewer::SelectAll() for how selectAll is implemented try { if (ancestorContainer == doc.body) ancestorContainer = doc.documentElement; } catch (e) { } // each path is a "child sequence" (a.k.a. "tumbler") that // descends from the ancestor down to the boundary point var startPath = this.getPath(ancestorContainer, startContainer); var endPath = this.getPath(ancestorContainer, endContainer); // clone the fragment of interest and reset everything to be relative to it // note: it is with the clone that we operate/munge from now on. Also note // that we clone into a data document to prevent images in the fragment from // loading and the like. The use of importNode here, as opposed to adoptNode, // is _very_ important. // XXXbz wish there were a less hacky way to create an untrusted document here var isHTML = (doc.createElement("div").tagName == "DIV"); var dataDoc = isHTML ? ancestorContainer.ownerDocument.implementation.createHTMLDocument("") : ancestorContainer.ownerDocument.implementation.createDocument("", "", null); ancestorContainer = dataDoc.importNode(ancestorContainer, true); startContainer = ancestorContainer; endContainer = ancestorContainer; // Only bother with the selection if it can be remapped. Don't mess with // leaf elements (such as ) that secretly use anynomous content // for their display appearance. var canDrawSelection = ancestorContainer.hasChildNodes(); var tmpNode; if (canDrawSelection) { var i; for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) { startContainer = startContainer.childNodes.item(startPath[i]); } for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) { endContainer = endContainer.childNodes.item(endPath[i]); } // add special markers to record the extent of the selection // note: |startOffset| and |endOffset| are interpreted either as // offsets in the text data or as child indices (see the Range spec) // (here, munging the end point first to keep the start point safe...) if (endContainer.nodeType == Node.TEXT_NODE || endContainer.nodeType == Node.CDATA_SECTION_NODE) { // do some extra tweaks to try to avoid the view-source output to look like // ...]... or ...]... (where ']' marks the end of the selection). // To get a neat output, the idea here is to remap the end point from: // 1. ...]... to ...]... // 2. ...]... to ...]... if ((endOffset > 0 && endOffset < endContainer.data.length) || !endContainer.parentNode || !endContainer.parentNode.parentNode) endContainer.insertData(endOffset, MARK_SELECTION_END); else { tmpNode = dataDoc.createTextNode(MARK_SELECTION_END); endContainer = endContainer.parentNode; if (endOffset === 0) endContainer.parentNode.insertBefore(tmpNode, endContainer); else endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling); } } else { tmpNode = dataDoc.createTextNode(MARK_SELECTION_END); endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset)); } if (startContainer.nodeType == Node.TEXT_NODE || startContainer.nodeType == Node.CDATA_SECTION_NODE) { // do some extra tweaks to try to avoid the view-source output to look like // ...[... or ...[... (where '[' marks the start of the selection). // To get a neat output, the idea here is to remap the start point from: // 1. ...[... to ...[... // 2. ...[... to ...[... if ((startOffset > 0 && startOffset < startContainer.data.length) || !startContainer.parentNode || !startContainer.parentNode.parentNode || startContainer != startContainer.parentNode.lastChild) startContainer.insertData(startOffset, MARK_SELECTION_START); else { tmpNode = dataDoc.createTextNode(MARK_SELECTION_START); startContainer = startContainer.parentNode; if (startOffset === 0) startContainer.parentNode.insertBefore(tmpNode, startContainer); else startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling); } } else { tmpNode = dataDoc.createTextNode(MARK_SELECTION_START); startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset)); } } // now extract and display the syntax highlighted source tmpNode = dataDoc.createElementNS("http://www.w3.org/1999/xhtml", "div"); tmpNode.appendChild(ancestorContainer); return { uri: (isHTML ? "view-source:data:text/html;charset=utf-8," : "view-source:data:application/xml;charset=utf-8,") + encodeURIComponent(tmpNode.innerHTML), drawSelection: canDrawSelection, baseURI: doc.baseURI }; }, /** * Reformat the source of a MathML node to highlight the node that was targetted. * * @param node * Some element within the fragment of interest. */ getMathMLSelection: function(node) { var Node = node.ownerDocument.defaultView.Node; this._lineCount = 0; this._startTargetLine = 0; this._endTargetLine = 0; this._targetNode = node; if (this._targetNode && this._targetNode.nodeType == Node.TEXT_NODE) this._targetNode = this._targetNode.parentNode; // walk up the tree to the top-level element (e.g., , ) var topTag = "math"; var topNode = this._targetNode; while (topNode && topNode.localName != topTag) { topNode = topNode.parentNode; } if (!topNode) return undefined; // serialize const VIEW_SOURCE_CSS = "resource://gre-resources/viewsource.css"; const BUNDLE_URL = "chrome://global/locale/viewSource.properties"; let bundle = Services.strings.createBundle(BUNDLE_URL); var title = bundle.GetStringFromName("viewMathMLSourceTitle"); var wrapClass = this.wrapLongLines ? ' class="wrap"' : ''; var source = '' + '' + '' + title + '' + '' + '' + '' + '' + '
'
    + this.getOuterMarkup(topNode, 0)
    + '
' ; // end return { uri: "data:text/html;charset=utf-8," + encodeURIComponent(source), drawSelection: false, baseURI: node.ownerDocument.baseURI }; }, get wrapLongLines() { return Services.prefs.getBoolPref("view_source.wrap_long_lines"); }, getInnerMarkup: function(node, indent) { var str = ''; for (var i = 0; i < node.childNodes.length; i++) { str += this.getOuterMarkup(node.childNodes.item(i), indent); } return str; }, getOuterMarkup: function(node, indent) { var Node = node.ownerDocument.defaultView.Node; var newline = ""; var padding = ""; var str = ""; if (node == this._targetNode) { this._startTargetLine = this._lineCount; str += '
';
    }

    switch (node.nodeType) {
    case Node.ELEMENT_NODE: // Element
      // to avoid the wide gap problem, '\n' is not emitted on the first
      // line and the lines before & after the 
...
if (this._lineCount > 0 && this._lineCount != this._startTargetLine && this._lineCount != this._endTargetLine) { newline = "\n"; } this._lineCount++; for (var k = 0; k < indent; k++) { padding += " "; } str += newline + padding + '<' + node.nodeName + ''; for (var i = 0; i < node.attributes.length; i++) { var attr = node.attributes.item(i); if (attr.nodeName.match(/^[-_]moz/)) { continue; } str += ' ' + attr.nodeName + '="' + this.unicodeToEntity(attr.nodeValue) + '"'; } if (!node.hasChildNodes()) { str += "/>"; } else { str += ">"; var oldLine = this._lineCount; str += this.getInnerMarkup(node, indent + 2); if (oldLine == this._lineCount) { newline = ""; padding = ""; } else { newline = (this._lineCount == this._endTargetLine) ? "" : "\n"; this._lineCount++; } str += newline + padding + '</' + node.nodeName + '>'; } break; case Node.TEXT_NODE: // Text var tmp = node.nodeValue; tmp = tmp.replace(/(\n|\r|\t)+/g, " "); tmp = tmp.replace(/^ +/, ""); tmp = tmp.replace(/ +$/, ""); if (tmp.length != 0) { str += '' + this.unicodeToEntity(tmp) + ''; } break; default: break; } if (node == this._targetNode) { this._endTargetLine = this._lineCount; str += '
';
    }
    return str;
  },

  unicodeToEntity: function(text) {
    const charTable = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;'
    };

    function charTableLookup(letter) {
      return charTable[letter];
    }

    function convertEntity(letter) {
      try {
        var unichar = this._entityConverter
                          .ConvertToEntity(letter, entityVersion);
        var entity = unichar.substring(1); // extract '&'
        return '&' + entity + '';
      } catch (ex) {
        return letter;
      }
    }

    if (!this._entityConverter) {
      try {
        this._entityConverter = Cc["@mozilla.org/intl/entityconverter;1"]
                                  .createInstance(Ci.nsIEntityConverter);
      } catch (e) { }
    }

    const entityVersion = Ci.nsIEntityConverter.entityW3C;

    var str = text;

    // replace chars in our charTable
    str = str.replace(/[<>&"]/g, charTableLookup);

    // replace chars > 0x7f via nsIEntityConverter
    str = str.replace(/[^\0-\u007f]/g, convertEntity);

    return str;
  }
};

ViewSelectionSource.init();

addEventListener("MozApplicationManifest", function(e) {
  let doc = e.target;
  let info = {
    uri: doc.documentURI,
    characterSet: doc.characterSet,
    manifest: doc.documentElement.getAttribute("manifest"),
    principal: doc.nodePrincipal,
  };
  sendAsyncMessage("MozApplicationManifest", info);
}, false);

let AutoCompletePopup = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]),

  _connected: false,

  MESSAGES: [
    "FormAutoComplete:HandleEnter",
    "FormAutoComplete:PopupClosed",
    "FormAutoComplete:PopupOpened",
    "FormAutoComplete:RequestFocus",
  ],

  init: function() {
    addEventListener("unload", this);
    addEventListener("DOMContentLoaded", this);

    for (let messageName of this.MESSAGES) {
      addMessageListener(messageName, this);
    }

    this._input = null;
    this._popupOpen = false;
  },

  destroy: function() {
    if (this._connected) {
      let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"]
                         .getService(Ci.nsIFormFillController);
      controller.detachFromBrowser(docShell);
      this._connected = false;
    }

    removeEventListener("unload", this);
    removeEventListener("DOMContentLoaded", this);

    for (let messageName of this.MESSAGES) {
      removeMessageListener(messageName, this);
    }
  },

  handleEvent(event) {
    switch (event.type) {
      case "DOMContentLoaded": {
        removeEventListener("DOMContentLoaded", this);

        // We need to wait for a content viewer to be available
        // before we can attach our AutoCompletePopup handler,
        // since nsFormFillController assumes one will exist
        // when we call attachToBrowser.

        // Hook up the form fill autocomplete controller.
        let controller = Cc["@mozilla.org/satchel/form-fill-controller;1"]
                           .getService(Ci.nsIFormFillController);
        controller.attachToBrowser(docShell,
                                   this.QueryInterface(Ci.nsIAutoCompletePopup));
        this._connected = true;
        break;
      }

      case "unload": {
        this.destroy();
        break;
      }
    }
  },

  receiveMessage(message) {
    switch (message.name) {
      case "FormAutoComplete:HandleEnter": {
        this.selectedIndex = message.data.selectedIndex;

        let controller = Cc["@mozilla.org/autocomplete/controller;1"]
                           .getService(Ci.nsIAutoCompleteController);
        controller.handleEnter(message.data.isPopupSelection);
        break;
      }

      case "FormAutoComplete:PopupClosed": {
        this._popupOpen = false;
        break;
      }

      case "FormAutoComplete:PopupOpened": {
        this._popupOpen = true;
        break;
      }

      case "FormAutoComplete:RequestFocus": {
        if (this._input) {
          this._input.focus();
        }
        break;
      }
    }
  },

  get input () { return this._input; },
  get overrideValue () { return null; },
  set selectedIndex (index) {
    sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index });
  },
  get selectedIndex () {
    // selectedIndex getter must be synchronous because we need the
    // correct value when the controller is in controller::HandleEnter.
    // We can't easily just let the parent inform us the new value every
    // time it changes because not every action that can change the
    // selectedIndex is trivial to catch (e.g. moving the mouse over the
    // list).
    return sendSyncMessage("FormAutoComplete:GetSelectedIndex", {});
  },
  get popupOpen () {
    return this._popupOpen;
  },

  openAutocompletePopup: function (input, element) {
    if (this._popupOpen || !input) {
      return;
    }

    let rect = BrowserUtils.getElementBoundingScreenRect(element);
    let window = element.ownerDocument.defaultView;
    let dir = window.getComputedStyle(element).direction;
    let results = this.getResultsFromController(input);

    sendAsyncMessage("FormAutoComplete:MaybeOpenPopup",
                     { results, rect, dir });
    this._input = input;
  },

  closePopup: function () {
    // We set this here instead of just waiting for the
    // PopupClosed message to do it so that we don't end
    // up in a state where the content thinks that a popup
    // is open when it isn't (or soon won't be).
    this._popupOpen = false;
    sendAsyncMessage("FormAutoComplete:ClosePopup", {});
  },

  invalidate: function () {
    if (this._popupOpen) {
      let results = this.getResultsFromController(this._input);
      sendAsyncMessage("FormAutoComplete:Invalidate", { results });
    }
  },

  selectBy: function(reverse, page) {
    this._index = sendSyncMessage("FormAutoComplete:SelectBy", {
      reverse: reverse,
      page: page
    });
  },

  getResultsFromController(inputField) {
    let results = [];

    if (!inputField) {
      return results;
    }

    let controller = inputField.controller;
    if (!(controller instanceof Ci.nsIAutoCompleteController)) {
      return results;
    }

    for (let i = 0; i < controller.matchCount; ++i) {
      let result = {};
      result.value = controller.getValueAt(i);
      result.label = controller.getLabelAt(i);
      result.comment = controller.getCommentAt(i);
      result.style = controller.getStyleAt(i);
      result.image = controller.getImageAt(i);
      results.push(result);
    }

    return results;
  },
}

AutoCompletePopup.init();

/**
 * DateTimePickerListener is the communication channel between the input box
 * (content) for date/time input types and its picker (chrome).
 */
let DateTimePickerListener = {
  /**
   * On init, just listen for the event to open the picker, once the picker is
   * opened, we'll listen for update and close events.
   */
  init: function() {
    addEventListener("MozOpenDateTimePicker", this);
    this._inputElement = null;

    addEventListener("unload", () => {
      this.uninit();
    });
  },

  uninit: function() {
    removeEventListener("MozOpenDateTimePicker", this);
    this._inputElement = null;
  },

  /**
   * Cleanup function called when picker is closed.
   */
  close: function() {
    this.removeListeners();
    this._inputElement.setDateTimePickerState(false);
    this._inputElement = null;
  },

  /**
   * Called after picker is opened to start listening for input box update
   * events.
   */
  addListeners: function() {
    addEventListener("MozUpdateDateTimePicker", this);
    addEventListener("MozCloseDateTimePicker", this);
    addEventListener("pagehide", this);

    addMessageListener("FormDateTime:PickerValueChanged", this);
    addMessageListener("FormDateTime:PickerClosed", this);
  },

  /**
   * Stop listeneing for events when picker is closed.
   */
  removeListeners: function() {
    removeEventListener("MozUpdateDateTimePicker", this);
    removeEventListener("MozCloseDateTimePicker", this);
    removeEventListener("pagehide", this);

    removeMessageListener("FormDateTime:PickerValueChanged", this);
    removeMessageListener("FormDateTime:PickerClosed", this);
  },

  /**
   * Helper function that returns the CSS direction property of the element.
   */
  getComputedDirection: function(aElement) {
    return aElement.ownerDocument.defaultView.getComputedStyle(aElement)
      .getPropertyValue("direction");
  },

  /**
   * Helper function that returns the rect of the element, which is the position
   * relative to the left/top of the content area.
   */
  getBoundingContentRect: function(aElement) {
    return BrowserUtils.getElementBoundingRect(aElement);
  },

  getTimePickerPref: function() {
    return Services.prefs.getBoolPref("dom.forms.datetime.timepicker");
  },

  /**
   * nsIMessageListener.
   */
  receiveMessage: function(aMessage) {
    switch (aMessage.name) {
      case "FormDateTime:PickerClosed": {
        this.close();
        break;
      }
      case "FormDateTime:PickerValueChanged": {
        this._inputElement.updateDateTimeInputBox(aMessage.data);
        break;
      }
      default:
        break;
    }
  },

  /**
   * nsIDOMEventListener, for chrome events sent by the input element and other
   * DOM events.
   */
  handleEvent: function(aEvent) {
    switch (aEvent.type) {
      case "MozOpenDateTimePicker": {
        // Time picker is disabled when preffed off
        if (!(aEvent.originalTarget instanceof content.HTMLInputElement) ||
            (aEvent.originalTarget.type == "time" && !this.getTimePickerPref())) {
          return;
        }

        if (this._inputElement) {
          // This happens when we're trying to open a picker when another picker
          // is still open. We ignore this request to let the first picker
          // close gracefully.
          return;
        }

        this._inputElement = aEvent.originalTarget;
        this._inputElement.setDateTimePickerState(true);
        this.addListeners();

        let value = this._inputElement.getDateTimeInputBoxValue();
        sendAsyncMessage("FormDateTime:OpenPicker", {
          rect: this.getBoundingContentRect(this._inputElement),
          dir: this.getComputedDirection(this._inputElement),
          type: this._inputElement.type,
          detail: {
            // Pass partial value if it's available, otherwise pass input
            // element's value.
            value: Object.keys(value).length > 0 ? value
                                                 : this._inputElement.value,
            min: this._inputElement.getMinimum(),
            max: this._inputElement.getMaximum(),
            step: this._inputElement.getStep(),
            stepBase: this._inputElement.getStepBase(),
          },
        });
        break;
      }
      case "MozUpdateDateTimePicker": {
        let value = this._inputElement.getDateTimeInputBoxValue();
        value.type = this._inputElement.type;
        sendAsyncMessage("FormDateTime:UpdatePicker", { value });
        break;
      }
      case "MozCloseDateTimePicker": {
        sendAsyncMessage("FormDateTime:ClosePicker");
        this.close();
        break;
      }
      case "pagehide": {
        if (this._inputElement &&
            this._inputElement.ownerDocument == aEvent.target) {
          sendAsyncMessage("FormDateTime:ClosePicker");
          this.close();
        }
        break;
      }
      default:
        break;
    }
  },
}

DateTimePickerListener.init();