blob: b0f7bbe1c050bd1eff45484f92b5715737bb14e6 [file] [log] [blame]
"use strict";
var URL = require("url");
var CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration;
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
var dom = require("../level1/core");
var NOT_IMPLEMENTED = require("./utils").NOT_IMPLEMENTED;
var createFrom = require("../utils").createFrom;
var History = require("./history");
var VirtualConsole = require("../virtual-console");
var cssSelectorSplitRE = /((?:[^,"']|"[^"]*"|'[^']*')+)/;
function matchesDontThrow(el, selector) {
try {
return el.matches(selector);
} catch (e) {
return false;
}
}
function startTimer(window, startFn, stopFn, callback, ms) {
var res = startFn(callback, ms);
window.__timers.push([res, stopFn]);
return res;
}
function stopTimer(window, id) {
if (typeof id === "undefined") {
return;
}
for (var i in window.__timers) {
if (window.__timers[i][0] === id) {
window.__timers[i][1].call(window, id);
window.__timers.splice(i, 1);
break;
}
}
}
function stopAllTimers(window) {
window.__timers.forEach(function (t) {
t[1].call(window, t[0]);
});
window.__timers = [];
}
function Window(document) {
this.__timers = [];
// TODO: very little of this belongs on the instance; they should be prototype methods instead.
var window = this;
this._document = document;
this.history = new History(this);
this.addEventListener = function () {
dom.Node.prototype.addEventListener.apply(window, arguments);
};
this.removeEventListener = function () {
dom.Node.prototype.removeEventListener.apply(window, arguments);
};
this.dispatchEvent = function () {
dom.Node.prototype.dispatchEvent.apply(window, arguments);
};
this.raise = function () {
dom.Node.prototype.raise.apply(window.document, arguments);
};
this.setTimeout = function (fn, ms) { return startTimer(window, setTimeout, clearTimeout, fn, ms); };
this.setInterval = function (fn, ms) { return startTimer(window, setInterval, clearInterval, fn, ms); };
this.clearInterval = stopTimer.bind(this, window);
this.clearTimeout = stopTimer.bind(this, window);
this.__stopAllTimers = stopAllTimers.bind(this, window);
this.Image = function (width, height) {
var element = window._document.createElement("img");
element.width = width;
element.height = height;
return element;
};
this._virtualConsole = new VirtualConsole();
function wrapConsoleMethod(method) {
return function () {
var args = Array.prototype.slice.call(arguments);
window._virtualConsole.emit.apply(window._virtualConsole, [method].concat(args));
};
}
this.console = {
assert: wrapConsoleMethod("assert"),
clear: wrapConsoleMethod("clear"),
count: wrapConsoleMethod("count"),
debug: wrapConsoleMethod("debug"),
error: wrapConsoleMethod("error"),
group: wrapConsoleMethod("group"),
groupCollapse: wrapConsoleMethod("groupCollapse"),
groupEnd: wrapConsoleMethod("groupEnd"),
info: wrapConsoleMethod("info"),
log: wrapConsoleMethod("log"),
table: wrapConsoleMethod("table"),
time: wrapConsoleMethod("time"),
timeEnd: wrapConsoleMethod("timeEnd"),
trace: wrapConsoleMethod("trace"),
warn: wrapConsoleMethod("warn")
};
this.XMLHttpRequest = function () {
var xhr = new XMLHttpRequest();
var lastUrl = "";
xhr._open = xhr.open;
xhr.open = function (method, url, async, user, password) {
url = URL.resolve(window.document.URL, url);
lastUrl = url;
return xhr._open(method, url, async, user, password);
};
xhr._send = xhr.send;
xhr.send = function (data) {
if (window.document.cookie) {
var cookieDomain = window.document._cookieDomain;
var url = URL.parse(lastUrl);
var host = url.host.split(":")[0];
if (host.indexOf(cookieDomain, host.length - cookieDomain.length) !== -1) {
xhr.setDisableHeaderCheck(true);
xhr.setRequestHeader("cookie", window.document.cookie);
xhr.setDisableHeaderCheck(false);
}
}
return xhr._send(data);
};
return xhr;
};
}
Window.prototype = createFrom(dom || null, {
constructor: Window,
// This implements window.frames.length, since window.frames returns a
// self reference to the window object. This value is incremented in the
// HTMLFrameElement init function (see: level2/html.js).
_length: 0,
get length() {
return this._length;
},
get document() {
return this._document;
},
get location() {
return this._document._location;
},
close: function () {
// Recursively close child frame windows, then ourselves.
var currentWindow = this;
(function windowCleaner(window) {
var i;
// We could call window.frames.length etc, but window.frames just points
// back to window.
if (window.length > 0) {
for (i = 0; i < window.length; i++) {
windowCleaner(window[i]);
}
}
// We"re already in our own window.close().
if (window !== currentWindow) {
window.close();
}
})(this);
if (this._document) {
if (this._document.body) {
this._document.body.innerHTML = "";
}
if (this._document.close) {
// We need to empty out the event listener array because
// document.close() causes "load" event to re-fire.
this._document._listeners = [];
this._document.close();
}
delete this._document;
}
stopAllTimers(currentWindow);
// Clean up the window"s execution context.
// dispose() is added by Contextify.
this.dispose();
},
getComputedStyle: function (node) {
var s = node.style,
cs = new CSSStyleDeclaration(),
forEach = Array.prototype.forEach;
function setPropertiesFromRule(rule) {
if (!rule.selectorText) {
return;
}
var selectors = rule.selectorText.split(cssSelectorSplitRE);
var matched = false;
selectors.forEach(function (selectorText) {
if (selectorText !== "" && selectorText !== "," && !matched && matchesDontThrow(node, selectorText)) {
matched = true;
forEach.call(rule.style, function (property) {
cs.setProperty(property, rule.style.getPropertyValue(property), rule.style.getPropertyPriority(property));
});
}
});
}
forEach.call(node.ownerDocument.styleSheets, function (sheet) {
forEach.call(sheet.cssRules, function (rule) {
if (rule.media) {
if (Array.prototype.indexOf.call(rule.media, "screen") !== -1) {
forEach.call(rule.cssRules, setPropertiesFromRule);
}
} else {
setPropertiesFromRule(rule);
}
});
});
forEach.call(s, function (property) {
cs.setProperty(property, s.getPropertyValue(property), s.getPropertyPriority(property));
});
return cs;
},
// TODO: all of the below data properties should be getters; right now they are shared between Window instances
// which is of course bad.
navigator: {
get userAgent() { return "Node.js (" + process.platform + "; U; rv:" + process.version + ")"; },
get appName() { return "Node.js jsDom"; },
get platform() { return process.platform; },
get appVersion() { return process.version; },
noUI: true,
get cookieEnabled() { return true; }
},
name: "nodejs",
innerWidth: 1024,
innerHeight: 768,
outerWidth: 1024,
outerHeight: 768,
pageXOffset: 0,
pageYOffset: 0,
screenX: 0,
screenY: 0,
screenLeft: 0,
screenTop: 0,
scrollX: 0,
scrollY: 0,
scrollTop: 0,
scrollLeft: 0,
alert: NOT_IMPLEMENTED(null, "window.alert"),
blur: NOT_IMPLEMENTED(null, "window.blur"),
confirm: NOT_IMPLEMENTED(null, "window.confirm"),
createPopup: NOT_IMPLEMENTED(null, "window.createPopup"),
focus: NOT_IMPLEMENTED(null, "window.focus"),
moveBy: NOT_IMPLEMENTED(null, "window.moveBy"),
moveTo: NOT_IMPLEMENTED(null, "window.moveTo"),
open: NOT_IMPLEMENTED(null, "window.open"),
print: NOT_IMPLEMENTED(null, "window.print"),
prompt: NOT_IMPLEMENTED(null, "window.prompt"),
resizeBy: NOT_IMPLEMENTED(null, "window.resizeBy"),
resizeTo: NOT_IMPLEMENTED(null, "window.resizeTo"),
scroll: NOT_IMPLEMENTED(null, "window.scroll"),
scrollBy: NOT_IMPLEMENTED(null, "window.scrollBy"),
scrollTo: NOT_IMPLEMENTED(null, "window.scrollTo"),
screen: {
width: 0,
height: 0
},
// Note: these will not be necessary for newer Node.js versions, which have
// typed arrays in V8 and thus on every global object. (That is, in newer
// versions we"ll get `ArrayBuffer` just as automatically as we get
// `Array`.) But to support older versions, we explicitly set them here.
Int8Array: global.Int8Array,
Int16Array: global.Int16Array,
Int32Array: global.Int32Array,
Float32Array: global.Float32Array,
Float64Array: global.Float64Array,
Uint8Array: global.Uint8Array,
Uint8ClampedArray: global.Uint8ClampedArray,
Uint16Array: global.Uint16Array,
Uint32Array: global.Uint32Array,
ArrayBuffer: global.ArrayBuffer
});
module.exports = Window;