jsdom-se
Version:
jsdom fork for silent errors - A JavaScript implementation of the DOM and HTML standards
1,965 lines (1,809 loc) • 64.1 kB
JavaScript
"use strict";
var resourceLoader = require('../browser/resource-loader'),
core = require("../level1/core"),
applyDocumentFeatures = require('../browser/documentfeatures').applyDocumentFeatures,
defineGetter = require('../utils').defineGetter,
defineSetter = require('../utils').defineSetter,
inheritFrom = require("../utils").inheritFrom,
URL = require("../utils").URL,
Window = require("../browser/Window"),
documentBaseURL = require("../living/helpers/document-base-url").documentBaseURL,
fallbackBaseURL = require("../living/helpers/document-base-url").fallbackBaseURL,
mapper = require("../utils").mapper,
addConstants = require("../utils").addConstants,
whatwgUrl = require("whatwg-url-compat"),
mixinURLUtils = whatwgUrl.mixinURLUtils;
const DOMException = require ('../web-idl/DOMException');
const getAttributeValue = require("../living/attributes").getAttributeValue;
const cloningSteps = require("../living/helpers/internal-constants").cloningSteps;
const clone = require("../living/node").clone;
const notImplemented = require("../browser/not-implemented");
const proxiedWindowEventHandlers = require("../living/helpers/proxied-window-event-handlers");
const internalConstants = require("../living/helpers/internal-constants");
const domSymbolTree = internalConstants.domSymbolTree;
const accept = internalConstants.accept;
const createHTMLCollection = require("../living/html-collection").create;
const NODE_TYPE = require("../living/node-type");
// Setup the javascript language processor
core.languageProcessors = {
javascript : require("./languages/javascript").javascript
};
function define(elementClass, def) {
var tagName = def.tagName,
tagNames = def.tagNames || (tagName? [tagName] : []),
parentClass = def.parentClass || core.HTMLElement,
attrs = def.attributes || [],
proto = def.proto || {};
proto.toString = function() {
return '[object ' + elementClass + ']';
};
var elem = core[elementClass] = function(document, name) {
parentClass.call(this, document, name || tagName.toUpperCase());
if (elem._init) {
elem._init.call(this);
}
};
elem._init = def.init;
inheritFrom(parentClass, elem, proto);
attrs.forEach(function(n) {
var prop = n.prop || n,
attr = n.attr || prop.toLowerCase();
if (!n.prop || n.read !== false) {
defineGetter(elem.prototype, prop, function() {
var s = this.getAttribute(attr);
if (n.type && n.type === 'boolean') {
return s !== null;
}
if (n.type && n.type === 'long') {
return +s;
}
if (typeof n === 'object' && n.normalize) { // see GH-491
return n.normalize(s);
}
if (s === null) {
s = '';
}
return s;
});
}
if (!n.prop || n.write !== false) {
defineSetter(elem.prototype, prop, function(val) {
if (!val) {
this.removeAttribute(attr);
}
else {
var s = val.toString();
if (typeof n === 'object' && n.normalize) {
s = n.normalize(s);
}
this.setAttribute(attr, s);
}
});
}
});
tagNames.forEach(function(tag) {
core.Document.prototype._elementBuilders[tag.toLowerCase()] = function(doc, s) {
return new elem(doc, s);
};
});
}
function closest(e, tagName) {
tagName = tagName.toUpperCase();
while (e) {
if (e.nodeName.toUpperCase() === tagName ||
(e.tagName && e.tagName.toUpperCase() === tagName))
{
return e;
}
e = domSymbolTree.parent(e);
}
return null;
}
function descendants(e, tagName, recursive) {
var owner = recursive ? e._ownerDocument || e : e;
return createHTMLCollection(owner, mapper(e, function(n) {
return n.tagName === tagName;
}, recursive));
}
function firstChild(e, tagName) {
if (!e) {
return null;
}
var c = descendants(e, tagName, false);
return c.length > 0 ? c[0] : null;
}
function ResourceQueue(paused) {
this.paused = !!paused;
}
ResourceQueue.prototype = {
push: function(callback) {
var q = this;
var item = {
prev: q.tail,
check: function() {
if (!q.paused && !this.prev && this.fired){
callback(this.err, this.data);
if (this.next) {
this.next.prev = null;
this.next.check();
}else{//q.tail===this
q.tail = null;
}
}
}
};
if (q.tail) {
q.tail.next = item;
}
q.tail = item;
return function(err, data) {
item.fired = 1;
item.err = err;
item.data = data;
item.check();
};
},
resume: function() {
if(!this.paused){
return;
}
this.paused = false;
var head = this.tail;
while(head && head.prev){
head = head.prev;
}
if(head){
head.check();
}
}
};
class RequestManager {
constructor() {
this.openedRequests = [];
}
add(req) {
this.openedRequests.push(req);
}
remove(req) {
var idx = this.openedRequests.indexOf(req);
if(idx !== -1) {
this.openedRequests.splice(idx, 1);
}
}
close() {
for (const openedRequest of this.openedRequests) {
openedRequest.abort();
}
this.openedRequests = [];
}
size() {
return this.openedRequests.length;
}
}
core.HTMLDocument = function HTMLDocument(options) {
core.Document.call(this, options);
this._referrer = options.referrer;
this._queue = new ResourceQueue(options.deferClose);
this._customResourceLoader = options.resourceLoader;
this[internalConstants.pool] = options.pool;
this[internalConstants.agentOptions] = options.agentOptions;
this[internalConstants.requestManager] = new RequestManager();
this.readyState = 'loading';
// Add level2 features
this.implementation._addFeature('core' , '2.0');
this.implementation._addFeature('html' , '2.0');
this.implementation._addFeature('xhtml' , '2.0');
this.implementation._addFeature('xml' , '2.0');
};
var nonInheritedTags = new Set([
'article', 'section', 'nav', 'aside', 'hgroup', 'header', 'footer', 'address', 'dt',
'dd', 'figure', 'figcaption', 'main', 'em', 'strong', 'small', 's', 'cite', 'dfn', 'abbr',
'ruby', 'rt', 'rp', 'code', 'var', 'samp', 'kbd', 'i', 'b', 'u', 'mark', 'bdi', 'bdo', 'wbr'
]);
inheritFrom(core.Document, core.HTMLDocument, {
_defaultElementBuilder: function (document, tagName) {
if (nonInheritedTags.has(tagName.toLowerCase())) {
return new core.HTMLElement(document, tagName);
} else {
return new core.HTMLUnknownElement(document, tagName);
}
},
_referrer : "",
get referrer() {
return this._referrer || '';
},
get domain() {
return "";
},
get images() {
return this.getElementsByTagName('IMG');
},
get applets() {
return createHTMLCollection(this, mapper(this, function(el) {
if (el && el.tagName) {
var upper = el.tagName.toUpperCase();
if (upper === "APPLET") {
return true;
} else if (upper === "OBJECT" &&
el.getElementsByTagName('APPLET').length > 0)
{
return true;
}
}
}));
},
get links() {
return createHTMLCollection(this, mapper(this, function(el) {
if (el && el.tagName) {
var upper = el.tagName.toUpperCase();
if (upper === "AREA" || (upper === "A" && el.href)) {
return true;
}
}
}));
},
get forms() {
return this.getElementsByTagName('FORM');
},
get anchors() {
return this.getElementsByTagName('A');
},
open : function() {
for (let child = null; child = domSymbolTree.firstChild(this);) {
this.removeChild(child);
}
this._documentElement = null;
this._modified();
return this;
},
close : function() {
this._queue.resume();
// Set the readyState to 'complete' once all resources are loaded.
// As a side-effect the document's load-event will be dispatched.
resourceLoader.enqueue(this, null, function() {
this.readyState = 'complete';
var ev = this.createEvent('HTMLEvents');
ev.initEvent('DOMContentLoaded', false, false);
this.dispatchEvent(ev);
})(null, true);
},
getElementsByName : function(elementName) {
return createHTMLCollection(this, mapper(this, function(el) {
return (el.getAttribute && el.getAttribute("name") === elementName);
}));
},
get title() {
var head = this.head,
title = head ? firstChild(head, 'TITLE') : null;
return title ? title.textContent : '';
},
set title(val) {
var title = firstChild(this.head, 'TITLE');
if (!title) {
title = this.createElement('TITLE');
var head = this.head;
if (!head) {
head = this.createElement('HEAD');
this.documentElement.insertBefore(head, this.documentElement.firstChild);
}
head.appendChild(title);
}
title.textContent = val;
},
get head() {
return firstChild(this.documentElement, 'HEAD');
},
set head(unused) { /* noop */ },
get body() {
var body = firstChild(this.documentElement, 'BODY');
if (!body) {
body = firstChild(this.documentElement, 'FRAMESET');
}
return body;
}
});
define('HTMLElement', {
parentClass: core.Element,
init: function () {
this._settingCssText = false;
var that = this;
this._style = new core.CSSStyleDeclaration(function onCssTextChange(newCssText) {
if (!that._settingCssText) {
that.setAttribute('style', newCssText);
}
});
},
proto : {
// Add default event behavior (click link to navigate, click button to submit
// form, etc). We start by wrapping dispatchEvent so we can forward events to
// the element's _eventDefault function (only events that did not incur
// preventDefault).
dispatchEvent : function (event) {
var outcome = core.Node.prototype.dispatchEvent.call(this, event)
if (!event.defaultPrevented &&
event.target._eventDefaults[event.type] &&
typeof event.target._eventDefaults[event.type] === 'function')
{
event.target._eventDefaults[event.type](event)
}
return outcome;
},
getBoundingClientRect: function () {
return {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0
};
},
focus : function() {
this._ownerDocument.activeElement = this;
},
blur : function() {
this._ownerDocument.activeElement = this._ownerDocument.body;
},
click: function() {
// https://html.spec.whatwg.org/multipage/interaction.html#dom-click
// https://html.spec.whatwg.org/multipage/interaction.html#run-synthetic-click-activation-steps
// Not completely spec compliant due to e.g. incomplete implementations of disabled for form controls, or no
// implementation at all of isTrusted.
if (this.disabled) {
return false;
}
var event = new core.MouseEvent("click", { bubbles: true, cancelable: true });
this.dispatchEvent(event);
},
get style() {
return this._style;
},
set style(value) {
this._style.cssText = value;
},
_eventDefaults : {},
_attrModified: function (name, value, oldValue) {
if (name === 'style' && value !== oldValue && !this._settingCssText) {
this._settingCssText = true;
this._style.cssText = value;
this._settingCssText = false;
}
core.Element.prototype._attrModified.apply(this, arguments);
}
},
attributes: [
'id',
'title',
'lang',
'dir',
{prop: 'className', attr: 'class', normalize: function(s) { return s || ''; }}
]
});
define('HTMLUnknownElement', {
// no additional properties & attributes
});
// http://www.whatwg.org/specs/web-apps/current-work/#category-listed
var listedElements = /button|fieldset|input|keygen|object|select|textarea/i;
define('HTMLFormElement', {
tagName: 'FORM',
proto: {
_descendantAdded: function(parent, child) {
var form = this;
for (const el of domSymbolTree.treeIterator(child)) {
if (typeof el._changedFormOwner === 'function') {
el._changedFormOwner(form);
}
};
core.HTMLElement.prototype._descendantAdded.apply(this, arguments);
},
_descendantRemoved: function(parent, child) {
for (const el of domSymbolTree.treeIterator(child)) {
if (typeof el._changedFormOwner === 'function') {
el._changedFormOwner(null);
}
}
core.HTMLElement.prototype._descendantRemoved.apply(this, arguments);
},
get elements() {
return createHTMLCollection(this._ownerDocument, mapper(this, function(e) {
return listedElements.test(e.nodeName) ; // TODO exclude <input type="image">
}));
},
get length() {
return this.elements.length;
},
_dispatchSubmitEvent: function() {
var ev = this._ownerDocument.createEvent('HTMLEvents');
ev.initEvent('submit', true, true);
if (!this.dispatchEvent(ev)) {
this.submit();
};
},
submit: function() {
},
reset: function() {
Array.prototype.forEach.call(this.elements, function(el) {
if (typeof el._formReset === 'function') {
el._formReset();
}
});
}
},
attributes: [
'name',
{prop: 'acceptCharset', attr: 'accept-charset'},
'action',
'enctype',
'method',
'target'
]
});
define('HTMLLinkElement', {
tagName: 'LINK',
proto: {
get [accept]() {
return "text/css,*/*;q=0.1";
},
get href() {
return resourceLoader.resolveResourceUrl(this._ownerDocument, this.getAttribute('href'));
}
},
attributes: [
{prop: 'disabled', type: 'boolean'},
'charset',
'href',
'hreflang',
'media',
'rel',
'rev',
'target',
'type'
]
});
define('HTMLMetaElement', {
tagName: 'META',
attributes: [
'content',
{prop: 'httpEquiv', attr: 'http-equiv'},
'name',
'scheme'
]
});
define('HTMLHtmlElement', {
tagName: 'HTML',
attributes: [
'version'
]
});
define('HTMLHeadElement', {
tagName: 'HEAD',
attributes: [
'profile'
]
});
define('HTMLTitleElement', {
tagName: 'TITLE',
proto: {
get text() {
return this.innerHTML;
},
set text(s) {
this.innerHTML = s;
}
}
});
function reparseAnchors(doc) {
var anchors = doc.getElementsByTagName("a");
for (var i = 0; i < anchors.length; ++i) {
whatwgUrl.reparse(anchors[i]);
}
}
define('HTMLBaseElement', {
tagName: 'BASE',
attributes: [
'target'
],
proto: {
get href() {
if (!this.hasAttribute("href")) {
return documentBaseURL(this._ownerDocument);
}
var fbbu = fallbackBaseURL(this._ownerDocument);
var url = this.getAttribute("href");
try {
return new URL(url, fbbu).href;
} catch (e) {
return "";
}
},
set href(value) {
this.setAttribute("href", String(value));
},
_attrModified: function (name, value, oldVal) {
core.HTMLElement.prototype._attrModified.call(this, name, value, oldVal);
if (name === 'href') {
reparseAnchors(this._ownerDocument);
}
},
_detach: function () {
core.HTMLElement.prototype._detach.call(this);
reparseAnchors(this._ownerDocument);
},
_attach: function () {
core.HTMLElement.prototype._attach.call(this);
reparseAnchors(this._ownerDocument);
}
}
});
define('HTMLStyleElement', {
tagName: 'STYLE',
attributes: [
{prop: 'disabled', type: 'boolean'},
'media',
'type',
]
});
define('HTMLBodyElement', {
proto: (function() {
var proto = {};
proxiedWindowEventHandlers.forEach(function (name) {
defineSetter(proto, name, function (handler) {
const window = this._ownerDocument._defaultView;
if (window) {
window[name] = handler;
}
});
defineGetter(proto, name, function () {
const window = this._ownerDocument._defaultView;
return window ? window[name] : null;
});
});
return proto;
})(),
tagName: 'BODY',
attributes: [
'aLink',
'background',
'bgColor',
'link',
'text',
'vLink'
]
});
define('HTMLSelectElement', {
tagName: 'SELECT',
proto: {
_formReset: function() {
Array.prototype.forEach.call(this.options, function(option) {
option._selectedness = option.defaultSelected;
option._dirtyness = false;
});
this._askedForAReset();
},
_askedForAReset: function() {
if (this.hasAttribute('multiple')) {
return;
}
var selected = Array.prototype.filter.call(this.options, function(option){
return option._selectedness;
});
// size = 1 is default if not multiple
if ((!this.size || this.size === 1) && !selected.length) {
// select the first option that is not disabled
for (var i = 0; i < this.options.length; ++i) {
var option = this.options[i];
var disabled = option.disabled;
const parentNode = domSymbolTree.parent(option);
if (parentNode &&
parentNode.nodeName.toUpperCase() === 'OPTGROUP' &&
parentNode.disabled) {
disabled = true;
}
if (!disabled) {
// (do not set dirty)
option._selectedness = true;
break;
}
}
} else if (selected.length >= 2) {
// select the last selected option
selected.forEach(function(option, index) {
option._selectedness = index === selected.length - 1;
});
}
},
_descendantAdded: function(parent, child) {
if (child.nodeType === NODE_TYPE.ELEMENT_NODE) {
this._askedForAReset();
}
core.HTMLElement.prototype._descendantAdded.apply(this, arguments);
},
_descendantRemoved: function(parent, child) {
if (child.nodeType === NODE_TYPE.ELEMENT_NODE) {
this._askedForAReset();
}
core.HTMLElement.prototype._descendantRemoved.apply(this, arguments);
},
_attrModified: function(name, value) {
if (name === 'multiple' || name === 'size') {
this._askedForAReset();
}
core.HTMLElement.prototype._attrModified.apply(this, arguments);
},
get options() {
// TODO: implement HTMLOptionsCollection
return createHTMLCollection(this, mapper(this, function(n) {
return n.nodeName === 'OPTION';
}));
},
get length() {
return this.options.length;
},
get selectedIndex() {
return Array.prototype.reduceRight.call(this.options, function(prev, option, i) {
return option.selected ? i : prev;
}, -1);
},
set selectedIndex(index) {
Array.prototype.forEach.call(this.options, function(option, i) {
option.selected = i === index;
});
},
get value() {
var i = this.selectedIndex;
if (this.options.length && (i === -1)) {
i = 0;
}
if (i === -1) {
return '';
}
return this.options[i].value;
},
set value(val) {
var self = this;
Array.prototype.forEach.call(this.options, function(option) {
if (option.value === val) {
option.selected = true;
} else {
if (!self.hasAttribute('multiple')) {
// Remove the selected bit from all other options in this group
// if the multiple attr is not present on the select
option.selected = false;
}
}
});
},
get form() {
return closest(this, 'FORM');
},
get type() {
return this.multiple ? 'select-multiple' : 'select-one';
},
add: function(opt, before) {
if (before) {
this.insertBefore(opt, before);
}
else {
this.appendChild(opt);
}
},
remove: function(index) {
var opts = this.options;
if (index >= 0 && index < opts.length) {
var el = opts[index];
domSymbolTree.parent(el).removeChild(el);
}
}
},
attributes: [
{prop: 'disabled', type: 'boolean'},
{prop: 'multiple', type: 'boolean'},
'name',
{prop: 'size', type: 'long'},
{prop: 'tabIndex', type: 'long'},
]
});
define('HTMLOptGroupElement', {
tagName: 'OPTGROUP',
attributes: [
{prop: 'disabled', type: 'boolean'},
'label'
]
});
define('HTMLOptionElement', {
tagName: 'OPTION',
proto: {
// whenever selectedness is set to true, make sure all
// other options set selectedness to false
_selectedness: false,
_dirtyness: false,
_removeOtherSelectedness: function() {
//Remove the selectedness flag from all other options in this select
var select = this._selectNode;
if (select && !select.multiple) {
var o = select.options;
for (var i = 0; i < o.length; i++) {
if (o[i] !== this) {
o[i]._selectedness = false;
}
}
}
},
_askForAReset: function() {
var select = this._selectNode;
if (select) {
select._askedForAReset();
}
},
_attrModified: function(name, value) {
if (!this._dirtyness && name === 'selected') {
this._selectedness = this.defaultSelected;
if (this._selectedness) {
this._removeOtherSelectedness();
}
this._askForAReset();
}
core.HTMLElement.prototype._attrModified.apply(this, arguments);
},
get _selectNode() {
var select = domSymbolTree.parent(this);
if (!select) return null;
if (select.nodeName.toUpperCase() !== 'SELECT') {
select = domSymbolTree.parent(select);
if (!select) return null;
if (select.nodeName.toUpperCase() !== 'SELECT') return null;
}
return select;
},
get form() {
return closest(this, 'FORM');
},
get defaultSelected() {
return this.getAttribute('selected') !== null;
},
set defaultSelected(s) {
if (s) this.setAttribute('selected', 'selected');
else this.removeAttribute('selected');
},
get text() {
return this.innerHTML;
},
get value() {
return (this.hasAttribute('value')) ? this.getAttribute('value') : this.innerHTML;
},
set value(val) {
this.setAttribute('value', val);
},
get index() {
return Array.prototype.indexOf.call(closest(this, 'SELECT').options, this);
},
get selected() {
return this._selectedness;
},
set selected(s) {
this._dirtyness = true;
this._selectedness = !!s;
if (this._selectedness) {
this._removeOtherSelectedness();
}
this._askForAReset();
}
},
attributes: [
{prop: 'disabled', type: 'boolean'},
'label'
]
});
const filesSymbol = Symbol("files");
define('HTMLInputElement', {
tagName: 'INPUT',
init: function() {
if (!this.type) {
this.type = 'text';
}
this._selectionStart = this._selectionEnd = 0;
this._selectionDirection = "none";
},
proto: {
_value: null,
_dirtyValue: false,
_checkedness: false,
_dirtyCheckedness: false,
_attrModified: function(name, value) {
if (!this._dirtyValue && name === 'value') {
this._value = this.defaultValue;
}
if (!this._dirtyCheckedness && name === 'checked') {
this._checkedness = this.defaultChecked;
if (this._checkedness) {
this._removeOtherRadioCheckedness();
}
}
if (name === 'name' || name === 'type') {
if (this._checkedness) {
this._removeOtherRadioCheckedness();
}
}
core.HTMLElement.prototype._attrModified.apply(this, arguments);
},
_formReset: function() {
this._value = this.defaultValue;
this._dirtyValue = false;
this._checkedness = this.defaultChecked;
this._dirtyCheckedness = false;
if (this._checkedness) {
this._removeOtherRadioCheckedness();
}
},
_changedFormOwner: function(newForm) {
if (this._checkedness) {
this._removeOtherRadioCheckedness();
}
},
_removeOtherRadioCheckedness: function() {
var root = this._radioButtonGroupRoot;
if (!root) {
return;
}
var name = this.name.toLowerCase();
var radios = createHTMLCollection(this, mapper(root, function(el) {
return el.type === 'radio' &&
el.name &&
el.name.toLowerCase() === name &&
el._radioButtonGroupRoot === root;
}));
Array.prototype.forEach.call(radios, function(radio) {
if (radio !== this) {
radio._checkedness = false;
}
}, this);
},
get _radioButtonGroupRoot() {
if (this.type !== 'radio' || !this.name) {
return null;
}
var e = domSymbolTree.parent(this);
while (e) {
// root node of this home sub tree
// or the form element we belong to
if (!domSymbolTree.parent(e) || e.nodeName.toUpperCase() === 'FORM') {
return e;
}
e = domSymbolTree.parent(e);
}
return null;
},
get form() {
return closest(this, 'FORM');
},
get defaultValue() {
var val = this.getAttribute('value');
return val !== null ? val : "";
},
set defaultValue(val) {
this.setAttribute('value', String(val));
},
get defaultChecked() {
return this.getAttribute('checked') !== null;
},
set defaultChecked(s) {
if (s) this.setAttribute('checked', 'checked');
else this.removeAttribute('checked');
},
get checked() {
return this._checkedness;
},
set checked(checked) {
this._checkedness = !!checked;
this._dirtyCheckedness = true;
if (this._checkedness) {
this._removeOtherRadioCheckedness();
}
},
get value() {
if (this._value === null) {
return '';
}
return this._value;
},
set value(val) {
this._dirtyValue = true;
if (val === null) {
this._value = null;
} else {
this._value = String(val);
}
if (this._allowSelection()) {
this._selectionStart = 0;
this._selectionEnd = 0;
this._selectionDirection = 'none';
}
},
get files() {
if (this.type === "file") {
this[filesSymbol] = this[filesSymbol] || new core.FileList();
} else {
this[filesSymbol] = null;
}
return this[filesSymbol];
},
get type() {
var type = this.getAttribute('type');
return type ? type : 'text';
},
set type(type) {
this.setAttribute('type', type);
},
blur: function() {
this._ownerDocument.activeElement = this._ownerDocument.body;
},
focus: function() {
this._ownerDocument.activeElement = this;
},
//LIVING
_dispatchSelectEvent: function() {
var event = this._ownerDocument.createEvent("HTMLEvents");
event.initEvent("select", true, true);
this.dispatchEvent(event);
},
_getValueLength: function() {
return typeof this.value === "string" ? this.value.length : 0;
},
_allowSelection: function() {
var type = this.type.toLowerCase();
return type === "text" || type === "search" || type === "tel" ||
type === "url" || type === "password";
},
select: function() {
if (!this._allowSelection()) {
throw new DOMException(DOMException.INVALID_STATE_ERR);
}
this._selectionStart = 0;
this._selectionEnd = this._getValueLength();
this._selectionDirection = "none";
this._dispatchSelectEvent();
},
get selectionStart() {
if (!this._allowSelection()) {
throw new DOMException(DOMException.INVALID_STATE_ERR);
}
return this._selectionStart;
},
set selectionStart(start) {
this.setSelectionRange(start, Math.max(start, this._selectionEnd), this._selectionDirection);
},
get selectionEnd() {
if(!this._allowSelection()) {
throw new DOMException(DOMException.INVALID_STATE_ERR);
}
return this._selectionEnd;
},
set selectionEnd(end) {
this.setSelectionRange(this._selectionStart, end, this._selectionDirection);
},
get selectionDirection() {
if(!this._allowSelection()) {
throw new DOMException(DOMException.INVALID_STATE_ERR);
}
return this._selectionDirection;
},
set selectionDirection(dir) {
this.setSelectionRange(this._selectionStart, this._selectionEnd, dir);
},
setSelectionRange: function(start, end, dir) {
if (!this._allowSelection()) {
throw new DOMException(DOMException.INVALID_STATE_ERR);
}
this._selectionEnd = Math.min(end, this._getValueLength());
this._selectionStart = Math.min(start, this._selectionEnd);
this._selectionDirection = ((dir == "forward") || (dir == "backward")) ? dir : "none";
this._dispatchSelectEvent();
},
setRangeText: function(repl, start, end, selectionMode) {
if (arguments.length < 2) {
start = this._selectionStart;
end = this._selectionEnd;
} else if (start > end) {
throw new core.DOMException(core.INDEX_SIZE_ERR);
}
start = Math.min(start, this._getValueLength());
end = Math.min(end, this._getValueLength());
var val = this.value;
var selStart = this._selectionStart;
var selEnd = this._selectionEnd;
this.value = val.slice(0, start) + repl + val.slice(end);
var newEnd = start + this.value.length;
if (selectionMode == "select") {
this.setSelectionRange(start, newEnd);
} else if(selectionMode === "start") {
this.setSelectionRange(start, start);
} else if(selectionMode === "end") {
this.setSelectionRange(newEnd, newEnd);
} else {//preserve
var delta = repl.length - (end - start);
if (selStart > end) {
selStart += delta;
} else if (selStart > start) {
selStart = start;
}
if (selEnd > end) {
selEnd += delta;
} else if (selEnd > start) {
selEnd = newEnd;
}
this.setSelectionRange(selStart, selEnd);
}
},
_eventDefaults: {
click: function (event) {
var target = event.target;
if (target.type === 'checkbox') {
target.checked = !target.checked;
} else if (target.type === 'radio') {
target.checked = true;
} else if (target.type === 'submit') {
var form = this.form;
if (form) {
form._dispatchSubmitEvent();
}
}
}
}
},
attributes: [
'accept',
'accessKey',
'align',
'alt',
{prop: 'disabled', type: 'boolean'},
{prop: 'maxLength', type: 'long'},
'name',
{prop: 'readOnly', type: 'boolean'},
{prop: 'size', type: 'long'},
'src',
{prop: 'tabIndex', type: 'long'},
{prop: 'type', normalize: function(val) {
return val ? val.toLowerCase() : 'text';
}},
'useMap'
]
});
define('HTMLTextAreaElement', {
tagName: 'TEXTAREA',
init: function() {
this._selectionStart = this._selectionEnd = 0;
this._selectionDirection = "none";
},
proto: {
_apiValue: null,
_dirtyValue: false,
// "raw value" and "value" are not used here because jsdom has no GUI
_formReset: function() {
this._apiValue = null;
this._dirtyValue = false;
},
get form() {
return closest(this, 'FORM');
},
get defaultValue() {
return this.textContent;
},
set defaultValue(val) {
this.textContent = val;
},
get value() {
// The WHATWG specifies that when "textContent" changes, the "raw value"
// (just the API value in jsdom) must also be updated.
// This slightly different solution has identical results, but is a lot less complex.
if (this._dirtyValue) {
if (this._apiValue === null) {
return '';
}
return this._apiValue;
}
var val = this.defaultValue;
val = val.replace(/\r\n|\r/g, '\n'); // API value normalizes line breaks per WHATWG
return val;
},
set value(val) {
if (val) {
val = val.replace(/\r\n|\r/g, '\n'); // API value normalizes line breaks per WHATWG
}
this._dirtyValue = true;
this._apiValue = val;
this._selectionStart = 0;
this._selectionEnd = 0;
this._selectionDirection = 'none';
},
get textLength() {
return this.value.length; // code unit length (16 bit)
},
get type() {
return 'textarea';
},
blur : function() {
this._ownerDocument.activeElement = this._ownerDocument.body;
},
focus : function() {
this._ownerDocument.activeElement = this;
},
//LIVING
_dispatchSelectEvent: function() {
var event = this._ownerDocument.createEvent("HTMLEvents");
event.initEvent("select", true, true);
this.dispatchEvent(event);
},
_getValueLength: function() {
return typeof this.value == "string" ? this.value.length : 0;
},
select: function() {
this._selectionStart = 0;
this._selectionEnd = this._getValueLength();
this._selectionDirection = "none";
this._dispatchSelectEvent();
},
get selectionStart() {
return this._selectionStart;
},
set selectionStart(start) {
this.setSelectionRange(start, Math.max(start, this._selectionEnd), this._selectionDirection);
},
get selectionEnd() {
return this._selectionEnd;
},
set selectionEnd(end) {
this.setSelectionRange(this._selectionStart, end, this._selectionDirection);
},
get selectionDirection() {
return this._selectionDirection;
},
set selectionDirection(dir) {
this.setSelectionRange(this._selectionStart, this._selectionEnd, dir);
},
setSelectionRange: function(start, end, dir) {
this._selectionEnd = Math.min(end, this._getValueLength());
this._selectionStart = Math.min(start, this._selectionEnd);
this._selectionDirection = ((dir == "forward") || (dir == "backward")) ? dir : "none";
this._dispatchSelectEvent();
},
setRangeText: function(repl, start, end, selectionMode) {
if (arguments.length < 2) {
start = this._selectionStart;
end = this._selectionEnd;
} else if (start > end) {
throw new core.DOMException(core.INDEX_SIZE_ERR);
}
start = Math.min(start, this._getValueLength());
end = Math.min(end, this._getValueLength());
var val = this.value;
var selStart = this._selectionStart;
var selEnd = this._selectionEnd;
this.value = val.slice(0, start) + repl + val.slice(end);
var newEnd = start + this.value.length;
if (selectionMode === "select") {
this.setSelectionRange(start, newEnd);
} else if (selectionMode === "start") {
this.setSelectionRange(start, start);
} else if (selectionMode === "end") {
this.setSelectionRange(newEnd, newEnd);
} else {//preserve
var delta = repl.length - (end-start);
if(selStart > end) {
selStart += delta;
} else if(selStart > start) {
selStart = start;
}
if (selEnd > end) {
selEnd += delta;
} else if (selEnd > start) {
selEnd = newEnd;
}
this.setSelectionRange(selStart, selEnd);
}
}
},
attributes: [
'accessKey',
{prop: 'cols', type: 'long'},
{prop: 'disabled', type: 'boolean'},
{prop: 'maxLength', type: 'long'},
'name',
{prop: 'readOnly', type: 'boolean'},
{prop: 'rows', type: 'long'},
{prop: 'tabIndex', type: 'long'}
]
});
define('HTMLButtonElement', {
tagName: 'BUTTON',
proto: {
get form() {
return closest(this, 'FORM');
},
_eventDefaults: {
click: function (event) {
var target = event.target;
var form = target.form;
if (form) {
if (target.type === 'submit') {
form._dispatchSubmitEvent();
}
}
}
},
get type() {
const typeAttr = (this.getAttribute('type') || '').toLowerCase();
switch (typeAttr) {
case 'submit':
case 'reset':
case 'button':
case 'menu':
return typeAttr;
default:
return 'submit';
}
},
set type(v) {
v = String(v).toLowerCase();
switch (v) {
case 'submit':
case 'reset':
case 'button':
case 'menu':
this.setAttribute('type', v);
break;
default:
this.setAttribute('type', 'submit');
break;
}
}
},
attributes: [
'accessKey',
{prop: 'disabled', type: 'boolean'},
'name',
{prop: 'tabIndex', type: 'long'},
'value'
]
});
define('HTMLLabelElement', {
tagName: 'LABEL',
proto: {
get form() {
return closest(this, 'FORM');
}
},
attributes: [
'accessKey',
{prop: 'htmlFor', attr: 'for'}
]
});
define('HTMLFieldSetElement', {
tagName: 'FIELDSET',
proto: {
get form() {
return closest(this, 'FORM');
}
}
});
define('HTMLLegendElement', {
tagName: 'LEGEND',
proto: {
get form() {
return closest(this, 'FORM');
}
},
attributes: [
'accessKey',
'align'
]
});
define('HTMLUListElement', {
tagName: 'UL',
attributes: [
{prop: 'compact', type: 'boolean'},
'type'
]
});
define('HTMLOListElement', {
tagName: 'OL',
attributes: [
{prop: 'compact', type: 'boolean'},
{prop: 'start', type: 'long'},
'type'
]
});
define('HTMLDListElement', {
tagName: 'DL',
attributes: [
{prop: 'compact', type: 'boolean'}
]
});
define('HTMLDirectoryElement', {
tagName: 'DIR',
attributes: [
{prop: 'compact', type: 'boolean'}
]
});
define('HTMLMenuElement', {
tagName: 'MENU',
attributes: [
{prop: 'compact', type: 'boolean'}
]
});
define('HTMLLIElement', {
tagName: 'LI',
attributes: [
'type',
{prop: 'value', type: 'long'}
]
});
define('HTMLCanvasElement', {
tagName: 'CANVAS',
init() {
let Canvas;
try {
Canvas = require("canvas");
} catch (e) {}
if (typeof Canvas === "function") { // in browserify, the require will succeed but return an empty object
this._nodeCanvas = new Canvas(this.width, this.height);
}
},
proto: {
_attrModified(name, value, oldValue) {
if ((name == "width" || name === "height") && this._nodeCanvas) {
const Canvas = require("canvas");
this._nodeCanvas = new Canvas(this.width, this.height);
}
return core.HTMLElement.prototype._attrModified.apply(this, arguments);
},
getContext(contextId) {
if (this._nodeCanvas) {
return this._nodeCanvas.getContext(contextId) || null;
}
notImplemented("HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)",
this._ownerDocument._defaultView);
},
probablySupportsContext(contextId) {
if (this._nodeCanvas) {
return contextId === "2d";
}
return false;
},
setContext(context) {
notImplemented("HTMLCanvasElement.prototype.setContext");
},
toDataURL() {
if (this._nodeCanvas) {
return this._nodeCanvas.toDataURL.apply(this._nodeCanvas, arguments);
}
notImplemented("HTMLCanvasElement.prototype.toDataURL (without installing the canvas npm package)",
this._ownerDocument._defaultView);
},
get width() {
const parsed = parseInt(this.getAttribute("width"));
return (parsed < 0 || Number.isNaN(parsed)) ? 300 : parsed;
},
set width(v) {
v = parseInt(v);
v = (Number.isNaN(v) || v < 0) ? 300 : v;
this.setAttribute("width", v);
},
get height() {
const parsed = parseInt(this.getAttribute("height"));
return (parsed < 0 || Number.isNaN(parsed)) ? 150 : parsed;
},
set height(v) {
v = parseInt(v);
v = (Number.isNaN(v) || v < 0) ? 150 : v;
this.setAttribute("height", v);
}
}
});
define('HTMLDivElement', {
tagName: 'DIV',
attributes: [
'align'
]
});
define('HTMLParagraphElement', {
tagName: 'P',
attributes: [
'align'
]
});
define('HTMLHeadingElement', {
tagNames: ['H1','H2','H3','H4','H5','H6'],
attributes: [
'align'
]
});
define('HTMLQuoteElement', {
tagNames: ['Q','BLOCKQUOTE'],
attributes: [
'cite'
]
});
define('HTMLPreElement', {
tagName: 'PRE',
attributes: [
{prop: 'width', type: 'long'}
]
});
define('HTMLBRElement', {
tagName: 'BR',
attributes: [
'clear'
]
});
define('HTMLBaseFontElement', {
tagName: 'BASEFONT',
attributes: [
'color',
'face',
{prop: 'size', type: 'long'}
]
});
define('HTMLFontElement', {
tagName: 'FONT',
attributes: [
'color',
'face',
'size'
]
});
define('HTMLHRElement', {
tagName: 'HR',
attributes: [
'align',
{prop: 'noShade', type: 'boolean'},
'size',
'width'
]
});
define('HTMLModElement', {
tagNames: ['INS', 'DEL'],
attributes: [
'cite',
'dateTime'
]
});
define('HTMLAnchorElement', {
tagName: 'A',
init: function () {
mixinURLUtils(this, function getTheBase() {
return documentBaseURL(this._ownerDocument);
}, function updateSteps(value) {
this.setAttribute("href", value);
});
},
proto: {
_attrModified: function(name, value, oldVal) {
if (name === 'href') {
whatwgUrl.setTheInput(this, value);
}
core.HTMLElement.prototype._attrModified.call(this, name, value, oldVal);
},
},
attributes: [
'accessKey',
'charset',
'coords',
'hreflang',
'name',
'rel',
'rev',
'shape',
{prop: 'tabIndex', type: 'long'},
'target',
'type'
]
});
define('HTMLImageElement', {
tagName: 'IMG',
proto: {
_attrModified: function(name, value, oldVal) {
if (name == 'src' && value !== oldVal) {
resourceLoader.enqueue(this, null, function () { })();
}
core.HTMLElement.prototype._attrModified.call(this, name, value, oldVal);
},
get [accept]() {
return "image/png,image/*;q=0.8,*/*;q=0.5";
},
get src() {
return resourceLoader.resolveResourceUrl(this._ownerDocument, this.getAttribute('src'));
}
},
attributes: [
'name',
'align',
'alt',
'border',
{prop: 'height', type: 'long'},
{prop: 'hspace', type: 'long'},
{prop: 'isMap', type: 'boolean'},
'longDesc',
{prop: 'src', type: 'string', read: false},
'useMap',
{prop: 'vspace', type: 'long'},
{prop: 'width', type: 'long'}
]
});
define('HTMLObjectElement', {
tagName: 'OBJECT',
proto: {
get form() {
return closest(this, 'FORM');
},
get contentDocument() {
return null;
}
},
attributes: [
'code',
'align',
'archive',
'border',
'codeBase',
'codeType',
'data',
{prop: 'declare', type: 'boolean'},
{prop: 'height', type: 'long'},
{prop: 'hspace', type: 'long'},
'name',
'standby',
{prop: 'tabIndex', type: 'long'},
'type',
'useMap',
{prop: 'vspace', type: 'long'},
{prop: 'width', type: 'long'}
]
});
define('HTMLParamElement', {
tagName: 'PARAM',
attributes: [
'name',
'type',
'value',
'valueType'
]
});
define('HTMLAppletElement', {
tagName: 'APPLET',
attributes: [
'align',
'alt',
'archive',
'code',
'codeBase',
'height',
{prop: 'hspace', type: 'long'},
'name',
'object',
{prop: 'vspace', type: 'long'},
'width'
]
});
define('HTMLMapElement', {
tagName: 'MAP',
proto: {
get areas() {
return this.getElementsByTagName("AREA");
}
},
attributes: [
'name'
]
});
define('HTMLAreaElement', {
tagName: 'AREA',
attributes: [
'accessKey',
'alt',
'coords',
'href',
{prop: 'noHref', type: 'boolean'},
'shape',
{prop: 'tabIndex', type: 'long'},
'target'
]
});
define('HTMLScriptElement', {
tagName: 'SCRIPT',
init: function() {
this.addEventListener('DOMNodeInsertedIntoDocument', function() {
if (this.src) {
resourceLoader.load(this, this.src, this._eval);
}
else {
resourceLoader.enqueue(this, this._ownerDocument.URL, this._eval)(null, this.text);
}
});
},
proto: {
_eval: function(text, filename) {
if (this._ownerDocument.implementation._hasFeature("ProcessExternalResources", "script") &&
this.language &&
core.languageProcessors[this.language])
{
this._ownerDocument._writeAfterElement = this;
core.languageProcessors[this.language](this, text, filename);
delete this._ownerDocument._writeAfterElement;
}
},
get language() {
var type = this.type || "text/javascript";
return type.split("/").pop().toLowerCase();
},
get text() {
let text = '';
for (const child of domSymbolTree.childrenIterator(this)) {
text += child.nodeValue;
}
return text;
},
set text(text) {
for (let child = null; child = domSymbolTree.firstChild(this);) {
this.removeChild(child);
}
this.appendChild(this._ownerDocument.createTextNode(text));
}
},
attributes : [
{prop: 'defer', 'type': 'boolean'},
'htmlFor',
'event',
'charset',
'type',
'src'
]
})
define('HTMLTableElement', {
tagName: 'TABLE',
proto: {
get caption() {
return firstChild(this, 'CAPTION');
},
get tHead() {
return firstChild(this, 'THEAD');
},
get tFoot() {
return firstChild(this, 'TFOOT');
},
get rows() {
if (!this._rows) {
var table = this;
this._rows = createHTMLCollection(this._ownerDocument, function() {
const sections = [];
if (table.tHead) {
sections.push(table.tHead);
}
sections.push.apply(sections, table.tBodies);
if (table.tFoot) {
sections.push(table.tFoot);
}
if (sections.length === 0) {
return domSymbolTree.childrenToArray(table, {filter: function(el) {
return el.tagName === 'TR';
}});
}
const rows = [];
for (const s of sections) {
rows.push.apply(rows, s.rows);
}
return rows;
});
}
return this._rows;
},
get tBodies() {
if (!this._tBodies) {
this._tBodies = descendants(this, 'TBODY', false);
}
return this._tBodies;
},
createTHead: function() {
var el = this.tHead;
if (!el) {
el = this._ownerDocument.createElement('THEAD');
this.appendChild(el);
}
return el;
},
deleteTHead: function() {
var el = this.tHead;
if (el) {
domSymbolTree.parent(el).removeChild(el);
}
},
createTFoot: function() {
var el = this.tFoot;
if (!el) {
el = this._ownerDocument.createElement('TFOOT');
this.appendChild(el);
}
return el;
},
deleteTFoot: function() {
var el = this.tFoot;
if (el) {
domSymbolTree.parent(el).removeChild(el);
}
},
createCaption: function() {
var el = this.caption;
if (!el) {
el = this._ownerDocument.createElement('CAPTION');
this.appendChild(el);
}
return el;
},
deleteCaption: function() {
var c = this.caption;
if (c) {
domSymbolTree.parent(c).removeChild(c);
}
},
insertRow: function(index) {
var tr = this._ownerDocument.createElement('TR');
if (!domSymbolTree.hasChildren(this)) {
this.appendChild(this._ownerDocument.createElement('TBODY'));
}
var rows = this.rows;
if (index < -1 || index > rows.length) {
throw new core.DOMException(core.DOMException.INDEX_SIZE_ERR);
}
if (index === -1 || (index === 0 && rows.length === 0)) {
this.tBodies.item(0).appendChild(tr);
}
else if (index === rows.length) {
var ref = rows[index-1];
domSymbolTree.parent(ref).appendChild(tr);
}
else {
var ref = rows[index];
domSymbolTree.parent(ref).insertBefore(tr, ref);
}
return tr;
},
deleteRow: function(index) {
var rows = this.rows, l = rows.length;
if (index === -1) {
index = l-1;
}
if (index < 0 || index >= l) {
throw new core.DOMException(core.DOMException.INDEX_SIZE_ERR);
}
var tr = rows[index];
domSymbolTree.parent(tr).removeChild(tr);
}
},
attributes: [
'align',
'bgColor',
'border',
'cellPadding',
'cellSpacing',
'frame',
'rules',
'summary',
'width'
]
});
define('HTMLTableCaptionElement', {
tagName: 'CAPTION',
attributes: [
'align'
]
});
define('HTMLTableColElement', {
tagNames: ['COL','COLGROUP'],
attributes: [
{ prop: 'span', type: 'long' },
'align',
{ prop: 'ch', attr: 'char' },
{ prop: 'chOff', attr: 'charoff' },
'vAlign',
'width'
]
});