gecko-dev/toolkit/content/browser-content.js

1128 строки
37 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/. */
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");
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 <html> and <body>
// 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.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 > 0) {
this._scrolldir = this._scrollable.scrollMaxY > 0 ? "NSEW" : "EW";
} else if (this._scrollable.scrollMaxY > 0) {
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);
}
if (this._scrollable instanceof content.Window) {
this._scrollable.scrollBy(actualScrollX, actualScrollY);
} else { // an element with overflow
this._scrollable.scrollLeft += actualScrollX;
this._scrollable.scrollTop += actualScrollY;
}
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);
},
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.popupWindowURI, data.popupWindowName, data.popupWindowFeatures);
}
}
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);
}
},
onPopupBlocked: function(ev) {
if (!this.popupData) {
this.popupData = new Array();
this.popupDataInternal = new Array();
}
let obj = {
popupWindowURI: ev.popupWindowURI.spec,
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",
{blockedPopups: this.popupData, freshPopup: 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: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));
break;
}
case "Printing:Preview:Exit": {
this.exitPrintPreview();
break;
}
case "Printing:Preview:Navigate": {
this.navigate(data.navType, data.pageNum);
break;
}
case "Printing:Preview:UpdatePageCount": {
this.updatePageCount();
break;
}
case "Printing:Print": {
this.print(Services.wm.getOuterWindowWithId(data.windowID));
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
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;
},
enterPrintPreview(contentWindow) {
// 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();
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) {
let printSettings = this.getPrintSettings();
let rv = Cr.NS_OK;
try {
let print = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebBrowserPrint);
print.print(printSettings, null);
} 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);
}
},
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,
FAYT_LINKS_KEY: "'".charCodeAt(0),
FAYT_TEXT_KEY: "/".charCodeAt(0),
_findMode: 0,
_findAsYouType: false,
init() {
this._findAsYouType =
Services.prefs.getBoolPref("accessibility.typeaheadfind");
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;
this._findAsYouType = msg.data.findAsYouType;
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.
*/
_shouldFastFind() {
//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;
let {BrowserUtils} = Cu.import("resource://gre/modules/BrowserUtils.jsm", {});
return BrowserUtils.shouldFastFind(elt, win);
},
_onKeypress(event) {
// Useless keys:
if (event.ctrlKey || event.altKey || event.metaKey || event.defaultPrevented) {
return;
}
// Not interested in random keypresses most of the time:
if (this._findMode == this.FIND_NORMAL && !this._findAsYouType &&
event.charCode != this.FAYT_LINKS_KEY && event.charCode != this.FAYT_TEXT_KEY) {
return;
}
// Check the focused element etc.
if (!this._shouldFastFind()) {
return;
}
let fakeEvent = {};
for (let k in event) {
if (typeof event[k] != "object" && typeof event[k] != "function") {
fakeEvent[k] = event[k];
}
}
// sendSyncMessage returns an array of the responses from all listeners
let rv = sendSyncMessage("Findbar:Keypress", fakeEvent);
if (rv.indexOf(false) !== -1) {
event.preventDefault();
return false;
}
},
_onMouseup(event) {
if (this._findMode != this.FIND_NORMAL)
sendAsyncMessage("Findbar:Mouseup");
},
};
FindBar.init();
// An event listener for custom "WebChannelMessageToChrome" events on pages.
addEventListener("WebChannelMessageToChrome", function (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) {
sendAsyncMessage("WebChannelMessageToChrome", e.detail, { eventTarget: e.target }, principal);
} else {
Cu.reportError("WebChannel message failed. No message detail.");
}
}, true, true);
// 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);
addMessageListener("AudioPlaybackMute", this);
addEventListener("unload", () => {
AudioPlaybackListener.uninit();
});
},
uninit() {
Services.obs.removeObserver(this, "audio-playback");
removeMessageListener("AudioPlaybackMute", this);
},
observe(subject, topic, data) {
if (topic === "audio-playback") {
if (subject && subject.top == global.content) {
let name = "AudioPlayback:";
name += (data === "active") ? "Start" : "Stop";
sendAsyncMessage(name);
}
}
},
receiveMessage(msg) {
if (msg.name == "AudioPlaybackMute") {
let utils = global.content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
utils.audioMuted = msg.data.type === "mute";
}
},
};
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 <noncharacters>
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 <html>...</html>
// @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 <isindex>) 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
// ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
// To get a neat output, the idea here is to remap the end point from:
// 1. ...<tag>]... to ...]<tag>...
// 2. ...]</tag>... to ...</tag>]...
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
// ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
// To get a neat output, the idea here is to remap the start point from:
// 1. ...<tag>[... to ...[<tag>...
// 2. ...[</tag>... to ...</tag>[...
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., <math>, <svg>)
var topTag = "math";
var topNode = this._targetNode;
while (topNode && topNode.localName != topTag) {
topNode = topNode.parentNode;
}
if (!topNode)
return;
// 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 =
'<!DOCTYPE html>'
+ '<html>'
+ '<head><title>' + title + '</title>'
+ '<link rel="stylesheet" type="text/css" href="' + VIEW_SOURCE_CSS + '">'
+ '<style type="text/css">'
+ '#target { border: dashed 1px; background-color: lightyellow; }'
+ '</style>'
+ '</head>'
+ '<body id="viewsource"' + wrapClass
+ ' onload="document.title=\''+title+'\'; document.getElementById(\'target\').scrollIntoView(true)">'
+ '<pre>'
+ this.getOuterMarkup(topNode, 0)
+ '</pre></body></html>'
; // 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 += '</pre><pre id="target">';
}
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 <pre id="target">...</pre>
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
+ '&lt;<span class="start-tag">' + node.nodeName + '</span>';
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes.item(i);
if (attr.nodeName.match(/^[-_]moz/)) {
continue;
}
str += ' <span class="attribute-name">'
+ attr.nodeName
+ '</span>=<span class="attribute-value">"'
+ this.unicodeToEntity(attr.nodeValue)
+ '"</span>';
}
if (!node.hasChildNodes()) {
str += "/&gt;";
}
else {
str += "&gt;";
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
+ '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
}
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 += '<span class="text">' + this.unicodeToEntity(tmp) + '</span>';
}
break;
default:
break;
}
if (node == this._targetNode) {
this._endTargetLine = this._lineCount;
str += '</pre><pre>';
}
return str;
},
unicodeToEntity: function(text) {
const charTable = {
'&': '&amp;<span class="entity">amp;</span>',
'<': '&amp;<span class="entity">lt;</span>',
'>': '&amp;<span class="entity">gt;</span>',
'"': '&amp;<span class="entity">quot;</span>'
};
function charTableLookup(letter) {
return charTable[letter];
}
function convertEntity(letter) {
try {
var unichar = this._entityConverter
.ConvertToEntity(letter, entityVersion);
var entity = unichar.substring(1); // extract '&'
return '&amp;<span class="entity">' + entity + '</span>';
} 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();