| var core = require("../level1/core"), |
| applyDocumentFeatures = require('../browser/documentfeatures').applyDocumentFeatures, |
| defineGetter = require('../utils').defineGetter, |
| defineSetter = require('../utils').defineSetter, |
| inheritFrom = require("../utils").inheritFrom, |
| resolveHref = require("../utils").resolveHref, |
| URL = require("url"), |
| Path = require('path'), |
| fs = require("fs"), |
| http = require('http'), |
| https = require('https'); |
| |
| // Setup the javascript language processor |
| core.languageProcessors = { |
| javascript : require("./languages/javascript").javascript |
| }; |
| |
| core.resourceLoader = { |
| load: function(element, href, callback) { |
| var ownerDoc = element._ownerDocument; |
| var ownerImplementation = ownerDoc.implementation; |
| |
| if (ownerImplementation._hasFeature('FetchExternalResources', element.tagName.toLowerCase())) { |
| var full = this.resolve(ownerDoc, href); |
| var url = URL.parse(full); |
| if (ownerImplementation._hasFeature('SkipExternalResources', full)) { |
| return false; |
| } |
| |
| var cookie = ownerDoc.cookie; |
| var cookieDomain = ownerDoc._cookieDomain; |
| var baseUrl = this.baseUrl(ownerDoc); |
| var enqueued = this.enqueue(element, callback, full); |
| |
| if (typeof ownerDoc._resourceLoader == 'function') { |
| var fetch = this.fetch.bind(this); |
| ownerDoc._resourceLoader.call(null, { |
| url: url, |
| cookie: cookie, |
| cookieDomain: cookieDomain, |
| baseUrl: baseUrl, |
| defaultFetch: function(callback) { |
| fetch(this.url, this.cookie, this.cookieDomain, this.baseUrl, callback); |
| } |
| }, enqueued); |
| } else { |
| this.fetch(url, cookie, cookieDomain, baseUrl, enqueued); |
| } |
| } |
| }, |
| enqueue: function(element, callback, filename) { |
| var loader = this, |
| doc = element.nodeType === core.Node.DOCUMENT_NODE ? |
| element : |
| element._ownerDocument; |
| |
| if (!doc._queue) { |
| return function() {}; |
| } |
| |
| return doc._queue.push(function(err, data) { |
| var ev = doc.createEvent('HTMLEvents'); |
| |
| if (!err) { |
| try { |
| callback.call(element, data, filename || doc.URL); |
| ev.initEvent('load', false, false); |
| } |
| catch(e) { |
| err = e; |
| } |
| } |
| |
| if (err) { |
| ev.initEvent('error', false, false); |
| ev.error = err; |
| } |
| |
| element.dispatchEvent(ev); |
| }); |
| }, |
| |
| baseUrl: function(document) { |
| var baseElements = document.getElementsByTagName('base'); |
| var baseUrl = document.URL; |
| |
| if (baseElements.length > 0) { |
| var baseHref = baseElements.item(0).href; |
| if (baseHref) { |
| baseUrl = resolveHref(baseUrl, baseHref); |
| } |
| } |
| |
| return baseUrl; |
| }, |
| resolve: function(document, href) { |
| // if getAttribute returns null, there is no href |
| // lets resolve to an empty string (nulls are not expected farther up) |
| if (href === null) { |
| return ''; |
| } |
| |
| var baseUrl = this.baseUrl(document); |
| |
| return resolveHref(baseUrl, href); |
| }, |
| fetch: function(url, cookie, cookieDomain, referrer, callback) { |
| if (url.hostname) { |
| this.download(url, cookie, cookieDomain, referrer, callback); |
| } else { |
| this.readFile(url.pathname, callback); |
| } |
| }, |
| download: function(url, cookie, cookieDomain, referrer, callback) { |
| var path = url.pathname + (url.search || ''), |
| options = {'method': 'GET', 'host': url.hostname, 'path': path}, |
| request; |
| if (url.protocol === 'https:') { |
| options.port = url.port || 443; |
| request = https.request(options); |
| } else { |
| options.port = url.port || 80; |
| request = http.request(options); |
| } |
| |
| // set header. |
| if (referrer) { |
| request.setHeader('Referer', referrer); |
| } |
| if (cookie) { |
| var host = url.host.split(':')[0]; |
| if (host.indexOf(cookieDomain, host.length - cookieDomain.length) !== -1) { |
| request.setHeader('cookie', cookie); |
| } |
| } |
| |
| request.on('response', function (response) { |
| var data = ''; |
| function success () { |
| if ([301, 302, 303, 307].indexOf(response.statusCode) > -1) { |
| var redirect = URL.resolve(url, response.headers["location"]); |
| core.resourceLoader.download(URL.parse(redirect), cookie, cookieDomain, referrer, callback); |
| } else { |
| callback(null, data); |
| } |
| } |
| response.setEncoding('utf8'); |
| response.on('data', function (chunk) { |
| data += chunk.toString(); |
| }); |
| response.on('end', function() { |
| // According to node docs, 'close' can fire after 'end', but not |
| // vice versa. Remove 'close' listener so we don't call success twice. |
| response.removeAllListeners('close'); |
| success(); |
| }); |
| response.on('close', function (err) { |
| if (err) { |
| callback(err); |
| } else { |
| success(); |
| } |
| }); |
| }); |
| |
| request.on('error', callback); |
| request.end(); |
| }, |
| readFile: function(url, callback) { |
| fs.readFile(url.replace(/^file:\/\//, "").replace(/^\/([a-z]):\//i, '$1:/').replace(/%20/g, ' '), 'utf8', callback); |
| } |
| }; |
| |
| function define(elementClass, def) { |
| var tagName = def.tagName, |
| tagNames = def.tagNames || (tagName? [tagName] : []), |
| parentClass = def.parentClass || core.HTMLElement, |
| attrs = def.attributes || [], |
| proto = def.proto || {}; |
| |
| 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) { |
| var el = new elem(doc, s); |
| |
| if (def.elementBuilder) { |
| return def.elementBuilder(el, doc, s); |
| } |
| |
| return el; |
| }; |
| }); |
| } |
| |
| |
| |
| core.HTMLCollection = function HTMLCollection(element, query) { |
| this._keys = []; |
| core.NodeList.call(this, element, query); |
| }; |
| inheritFrom(core.NodeList, core.HTMLCollection, { |
| namedItem: function(name) { |
| // Try property shortcut; should work in most cases |
| if (Object.prototype.hasOwnProperty.call(this, name)) { |
| return this[name]; |
| } |
| |
| var results = this._toArray(), |
| l = results.length, |
| node, |
| matchingName = null; |
| |
| for (var i=0; i<l; i++) { |
| node = results[i]; |
| if (node.getAttribute('id') === name) { |
| return node; |
| } else if (node.getAttribute('name') === name) { |
| matchingName = node; |
| } |
| } |
| return matchingName; |
| }, |
| toString: function() { |
| return '[ jsdom HTMLCollection ]: contains ' + this.length + ' items'; |
| }, |
| _resetTo: function(array) { |
| var i, _this = this; |
| |
| for (i = 0; i < this._keys.length; ++i) { |
| delete this[this._keys[i]]; |
| } |
| this._keys = []; |
| |
| core.NodeList.prototype._resetTo.apply(this, arguments); |
| |
| function testAttr(node, attr) { |
| var val = node.getAttribute(attr); |
| if (val && !Object.prototype.hasOwnProperty.call(_this, val)) { |
| _this[val] = node; |
| _this._keys.push(val); |
| } |
| } |
| for (i = 0; i < array.length; ++i) { |
| testAttr(array[i], 'id'); |
| } |
| for (i = 0; i < array.length; ++i) { |
| testAttr(array[i], 'name'); |
| } |
| } |
| }); |
| Object.defineProperty(core.HTMLCollection.prototype, 'constructor', { |
| value: core.NodeList, |
| writable: true, |
| configurable: true |
| }); |
| |
| core.HTMLOptionsCollection = core.HTMLCollection; |
| |
| function closest(e, tagName) { |
| tagName = tagName.toUpperCase(); |
| while (e) { |
| if (e.nodeName.toUpperCase() === tagName || |
| (e.tagName && e.tagName.toUpperCase() === tagName)) |
| { |
| return e; |
| } |
| e = e._parentNode; |
| } |
| return null; |
| } |
| |
| function descendants(e, tagName, recursive) { |
| var owner = recursive ? e._ownerDocument || e : e; |
| return new core.HTMLCollection(owner, core.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(); |
| } |
| } |
| }; |
| |
| core.HTMLDocument = function HTMLDocument(options) { |
| core.Document.call(this, options); |
| this._referrer = options.referrer; |
| this._cookie = options.cookie; |
| this._cookieDomain = options.cookieDomain || '127.0.0.1'; |
| this._documentRoot = options.documentRoot || Path.dirname(this._URL); |
| this._queue = new ResourceQueue(options.deferClose); |
| this._resourceLoader = options.resourceLoader; |
| 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'); |
| }; |
| |
| inheritFrom(core.Document, core.HTMLDocument, { |
| _referrer : "", |
| get referrer() { |
| return this._referrer || ''; |
| }, |
| get domain() { |
| return ""; |
| }, |
| get images() { |
| return this.getElementsByTagName('IMG'); |
| }, |
| get applets() { |
| return new core.HTMLCollection(this, core.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 new core.HTMLCollection(this, core.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() { |
| this._childNodes = []; |
| this._documentElement = null; |
| this._modified(); |
| }, |
| 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. |
| core.resourceLoader.enqueue(this, function() { |
| this.readyState = 'complete'; |
| var ev = this.createEvent('HTMLEvents'); |
| ev.initEvent('DOMContentLoaded', false, false); |
| this.dispatchEvent(ev); |
| })(null, true); |
| }, |
| |
| // document.write is defined in browser/index.js. |
| |
| writeln : function(text) { |
| this.write(text + '\n'); |
| }, |
| |
| getElementsByName : function(elementName) { |
| return new core.HTMLCollection(this, core.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; |
| }, |
| |
| _cookie : "", |
| get cookie() { |
| var cookies = Array.isArray(this._cookie) ? |
| this._cookie : |
| (this._cookie && this._cookie.length > 0 ? [this._cookie] : []); |
| |
| return cookies.map(function (x) { |
| return x.split(';')[0]; |
| }).join('; '); |
| }, |
| set cookie(val) { |
| if (val == null) return val; |
| var key = val.split('=')[0]; |
| var cookies = Array.isArray(this._cookie) ? |
| this._cookie : |
| (this._cookie && this._cookie.length > 0 ? [this._cookie] : []); |
| for (var i = 0; i < cookies.length; i++) { |
| if (cookies[i].lastIndexOf(key + '=', 0) === 0) { |
| cookies[i] = val; |
| key = null; |
| break; |
| } |
| } |
| if (key) { |
| cookies.push(val); |
| } |
| if (cookies.length === 1) { |
| this._cookie = cookies[0]; |
| } else { |
| this._cookie = cookies; |
| } |
| return val; |
| } |
| }); |
| |
| define('HTMLElement', { |
| parentClass: core.Element, |
| 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._preventDefault && |
| 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; |
| }, |
| _eventDefaults : {} |
| }, |
| attributes: [ |
| 'id', |
| 'title', |
| 'lang', |
| 'dir', |
| {prop: 'className', attr: 'class', normalize: function(s) { return s || ''; }} |
| ] |
| }); |
| |
| core.Document.prototype._defaultElementBuilder = function(document, tagName) { |
| return new core.HTMLElement(document, tagName); |
| }; |
| |
| // 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; |
| core.visitTree(child, function(el) { |
| if (typeof el._changedFormOwner === 'function') { |
| el._changedFormOwner(form); |
| } |
| }); |
| |
| core.HTMLElement.prototype._descendantAdded.apply(this, arguments); |
| }, |
| _descendantRemoved: function(parent, child) { |
| core.visitTree(child, function(el) { |
| if (typeof el._changedFormOwner === 'function') { |
| el._changedFormOwner(null); |
| } |
| }); |
| |
| core.HTMLElement.prototype._descendantRemoved.apply(this, arguments); |
| }, |
| get elements() { |
| return new core.HTMLCollection(this._ownerDocument, core.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() { |
| this.elements._toArray().forEach(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 href() { |
| return core.resourceLoader.resolve(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; |
| } |
| } |
| }); |
| |
| define('HTMLBaseElement', { |
| tagName: 'BASE', |
| attributes: [ |
| 'href', |
| 'target' |
| ] |
| }); |
| |
| |
| //**Deprecated** |
| define('HTMLIsIndexElement', { |
| tagName : 'ISINDEX', |
| parentClass : core.Element, |
| proto : { |
| get form() { |
| return closest(this, 'FORM'); |
| } |
| }, |
| attributes : [ |
| 'prompt' |
| ] |
| }); |
| |
| |
| define('HTMLStyleElement', { |
| tagName: 'STYLE', |
| attributes: [ |
| {prop: 'disabled', type: 'boolean'}, |
| 'media', |
| 'type', |
| ] |
| }); |
| |
| define('HTMLBodyElement', { |
| proto: (function() { |
| var proto = {}; |
| // The body element's "traditional" event handlers are proxied to the |
| // window object. |
| // See: http://www.whatwg.org/specs/web-apps/current-work/#the-body-element |
| ['onafterprint', 'onbeforeprint', 'onbeforeunload', 'onblur', 'onerror', |
| 'onfocus', 'onhashchange', 'onload', 'onmessage', 'onoffline', 'ononline', |
| 'onpagehide', 'onpageshow', 'onpopstate', 'onresize', 'onscroll', |
| 'onstorage', 'onunload'].forEach(function (name) { |
| defineSetter(proto, name, function (handler) { |
| this._ownerDocument.parentWindow[name] = handler; |
| }); |
| defineGetter(proto, name, function () { |
| return this._ownerDocument.parentWindow[name]; |
| }); |
| }); |
| return proto; |
| })(), |
| tagName: 'BODY', |
| attributes: [ |
| 'aLink', |
| 'background', |
| 'bgColor', |
| 'link', |
| 'text', |
| 'vLink' |
| ] |
| }); |
| |
| define('HTMLSelectElement', { |
| tagName: 'SELECT', |
| proto: { |
| _formReset: function() { |
| this.options._toArray().forEach(function(option, i) { |
| option._selectedness = option.defaultSelected; |
| option._dirtyness = false; |
| }); |
| this._askedForAReset(); |
| }, |
| _askedForAReset: function() { |
| if (this.hasAttribute('multiple')) { |
| return; |
| } |
| |
| var options = this.options._toArray(); |
| var selected = options.filter(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 < options.length; ++i) { |
| var option = options[i]; |
| var disabled = option.disabled; |
| if (option._parentNode && |
| option._parentNode.nodeName.toUpperCase() === 'OPTGROUP' && |
| option._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 === core.Node.ELEMENT_NODE) { |
| this._askedForAReset(); |
| } |
| |
| core.HTMLElement.prototype._descendantAdded.apply(this, arguments); |
| }, |
| _descendantRemoved: function(parent, child) { |
| if (child.nodeType === core.Node.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() { |
| return new core.HTMLOptionsCollection(this, core.mapper(this, function(n) { |
| return n.nodeName === 'OPTION'; |
| })); |
| }, |
| |
| get length() { |
| return this.options.length; |
| }, |
| |
| get selectedIndex() { |
| return this.options._toArray().reduceRight(function(prev, option, i) { |
| return option.selected ? i : prev; |
| }, -1); |
| }, |
| |
| set selectedIndex(index) { |
| this.options._toArray().forEach(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; |
| this.options._toArray().forEach(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._toArray(); |
| if (index >= 0 && index < opts.length) { |
| var el = opts[index]; |
| el._parentNode.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 = this._parentNode; |
| if (!select) return null; |
| if (select.nodeName.toUpperCase() !== 'SELECT') { |
| select = select._parentNode; |
| 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 closest(this, 'SELECT').options._toArray().indexOf(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' |
| ] |
| }); |
| |
| define('HTMLInputElement', { |
| tagName: 'INPUT', |
| init: function() { |
| if (!this.type) { |
| this.type = 'text'; |
| } |
| }, |
| 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 = new core.HTMLCollection(this, core.mapper(root, function(el) { |
| return el.type === 'radio' && |
| el.name && |
| el.name.toLowerCase() === name && |
| el._radioButtonGroupRoot === root; |
| })); |
| |
| radios._toArray().forEach(function(radio) { |
| if (radio !== this) { |
| radio._checkedness = false; |
| } |
| }, this); |
| }, |
| get _radioButtonGroupRoot() { |
| if (this.type !== 'radio' || !this.name) { |
| return null; |
| } |
| |
| var e = this._parentNode; |
| while (e) { |
| // root node of this home sub tree |
| // or the form element we belong to |
| if (!e._parentNode || e.nodeName.toUpperCase() === 'FORM') { |
| return e; |
| } |
| e = e._parentNode; |
| } |
| 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); |
| } |
| }, |
| get type() { |
| var type = this.getAttribute('type'); |
| return type ? type : 'text'; |
| }, |
| set type(type) { |
| this.setAttribute('type', type); |
| }, |
| select: function() { |
| }, |
| |
| _dispatchClickEvent: function() { |
| var event = this._ownerDocument.createEvent("HTMLEvents"); |
| event.initEvent("click", true, true); |
| this.dispatchEvent(event); |
| }, |
| |
| click: function() { |
| if (this.type === 'checkbox') { |
| this.checked = !this.checked; |
| } |
| else if (this.type === 'radio') { |
| this.checked = true; |
| } |
| else if (this.type === 'submit') { |
| var form = this.form; |
| if (form) { |
| form._dispatchSubmitEvent(); |
| } |
| } |
| this._dispatchClickEvent(); |
| } |
| }, |
| 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', |
| 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; |
| }, |
| get textLength() { |
| return this.value.length; // code unit length (16 bit) |
| }, |
| get type() { |
| return 'textarea'; |
| }, |
| select: function() { |
| } |
| }, |
| 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'); |
| } |
| }, |
| attributes: [ |
| 'accessKey', |
| {prop: 'disabled', type: 'boolean'}, |
| 'name', |
| {prop: 'tabIndex', type: 'long'}, |
| 'type', |
| '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', |
| attributes: [ |
| 'align' |
| ], |
| elementBuilder: function (element) { |
| // require node-canvas and catch the error if it blows up |
| try { |
| var canvas = new (require('canvas'))(0,0); |
| for (var attr in element) { |
| if (!canvas[attr]) { |
| canvas[attr] = element[attr]; |
| } |
| } |
| return canvas; |
| } catch (e) { |
| return element; |
| } |
| } |
| }); |
| |
| define('HTMLDivElement', { |
| tagName: 'DIV', |
| attributes: [ |
| 'align' |
| ], |
| proto: { |
| toString: function() { return '[object HTMLDivElement]'; } |
| } |
| }); |
| |
| 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', |
| |
| proto: { |
| get href() { |
| return core.resourceLoader.resolve(this._ownerDocument, this.getAttribute('href')); |
| }, |
| get hostname() { |
| return URL.parse(this.href).hostname || ''; |
| }, |
| get host() { |
| return URL.parse(this.href).host || ''; |
| }, |
| get origin() { |
| var proto = URL.parse(this.href).protocol; |
| |
| if (proto !== undefined && proto !== null) { |
| proto += '//'; |
| } |
| |
| return proto + URL.parse(this.href).host || ''; |
| }, |
| get port() { |
| return URL.parse(this.href).port || ''; |
| }, |
| get protocol() { |
| var protocol = URL.parse(this.href).protocol; |
| return (protocol == null) ? ':' : protocol; |
| }, |
| get password() { |
| var auth = URL.parse(this.href).auth; |
| return auth.substr(auth.indexOf(':') + 1); |
| }, |
| get pathname() { |
| return URL.parse(this.href).pathname || ''; |
| }, |
| get username() { |
| var auth = URL.parse(this.href).auth; |
| return auth.substr(0, auth.indexOf(':')); |
| }, |
| get search() { |
| return URL.parse(this.href).search || ''; |
| }, |
| get hash() { |
| return URL.parse(this.href).hash || ''; |
| } |
| }, |
| attributes: [ |
| 'accessKey', |
| 'charset', |
| 'coords', |
| {prop: 'href', type: 'string', read: false}, |
| '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) { |
| core.resourceLoader.enqueue(this, function() {})(); |
| } |
| }, |
| get src() { |
| return core.resourceLoader.resolve(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) { |
| core.resourceLoader.load(this, this.src, this._eval); |
| } |
| else { |
| var src = this.sourceLocation || {}, |
| filename = src.file || this._ownerDocument.URL; |
| |
| if (src) { |
| filename += ':' + src.line + ':' + src.col; |
| } |
| filename += '<script>'; |
| |
| core.resourceLoader.enqueue(this, this._eval, filename)(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() { |
| var i=0, children = this._childNodes, l = children.length, ret = []; |
| |
| for (i; i<l; i++) { |
| ret.push(children[i].nodeValue); |
| } |
| |
| return ret.join(""); |
| }, |
| set text(text) { |
| while (this._childNodes.length) { |
| this.removeChild(this._childNodes[this._childNodes.length-1]); |
| } |
| 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 = new core.HTMLCollection(this._ownerDocument, function() { |
| var sections = [table.tHead].concat(table.tBodies._toArray(), table.tFoot).filter(function(s) { return !!s }); |
| |
| if (sections.length === 0) { |
| return core.mapDOMNodes(table, false, function(el) { |
| return el.tagName === 'TR'; |
| }); |
| } |
| |
| return sections.reduce(function(prev, s) { |
| return prev.concat(s.rows._toArray()); |
| }, []); |
| |
| }); |
| } |
| 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) { |
| el._parentNode.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) { |
| el._parentNode.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) { |
| c._parentNode.removeChild(c); |
| } |
| }, |
| insertRow: function(index) { |
| var tr = this._ownerDocument.createElement('TR'); |
| if (this._childNodes.length === 0) { |
| this.appendChild(this._ownerDocument.createElement('TBODY')); |
| } |
| var rows = this.rows._toArray(); |
| if (index < -1 || index > rows.length) { |
| throw new core.DOMException(core.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]; |
| ref._parentNode.appendChild(tr); |
| } |
| else { |
| var ref = rows[index]; |
| ref._parentNode.insertBefore(tr, ref); |
| } |
| return tr; |
| }, |
| deleteRow: function(index) { |
| var rows = this.rows._toArray(), l = rows.length; |
| if (index === -1) { |
| index = l-1; |
| } |
| if (index < 0 || index >= l) { |
| throw new core.DOMException(core.INDEX_SIZE_ERR); |
| } |
| var tr = rows[index]; |
| tr._parentNode.removeChild(tr); |
| } |
| }, |
| attributes: [ |
| 'align', |
| 'bgColor', |
| 'border', |
| 'cellPadding', |
| 'cellSpacing', |
| 'frame', |
| 'rules', |
| 'summary', |
| 'width' |
| ] |
| }); |
| |
| define('HTMLTableCaptionElement', { |
| tagName: 'CAPTION', |
| attributes: [ |
| 'align' |
| ] |
| }); |
| |
| define('HTMLTableColElement', { |
| tagNames: ['COL','COLGROUP'], |
| attributes: [ |
| 'align', |
| {prop: 'ch', attr: 'char'}, |
| {prop: 'chOff', attr: 'charoff'}, |
| {prop: 'span', type: 'long'}, |
| 'vAlign', |
| 'width', |
| ] |
| }); |
| |
| define('HTMLTableSectionElement', { |
| tagNames: ['THEAD','TBODY','TFOOT'], |
| proto: { |
| get rows() { |
| if (!this._rows) { |
| this._rows = descendants(this, 'TR', false); |
| } |
| return this._rows; |
| }, |
| insertRow: function(index) { |
| var tr = this._ownerDocument.createElement('TR'); |
| var rows = this.rows._toArray(); |
| if (index < -1 || index > rows.length) { |
| throw new core.DOMException(core.INDEX_SIZE_ERR); |
| } |
| if (index === -1 || index === rows.length) { |
| this.appendChild(tr); |
| } |
| else { |
| var ref = rows[index]; |
| this.insertBefore(tr, ref); |
| } |
| return tr; |
| }, |
| deleteRow: function(index) { |
| var rows = this.rows._toArray(); |
| if (index === -1) { |
| index = rows.length-1; |
| } |
| if (index < 0 || index >= rows.length) { |
| throw new core.DOMException(core.INDEX_SIZE_ERR); |
| } |
| var tr = this.rows[index]; |
| this.removeChild(tr); |
| } |
| }, |
| attributes: [ |
| 'align', |
| {prop: 'ch', attr: 'char'}, |
| {prop: 'chOff', attr: 'charoff'}, |
| {prop: 'span', type: 'long'}, |
| 'vAlign', |
| 'width', |
| ] |
| }); |
| |
| define('HTMLTableRowElement', { |
| tagName: 'TR', |
| proto: { |
| get cells() { |
| if (!this._cells) { |
| this._cells = new core.HTMLCollection(this, core.mapper(this, function(n) { |
| return n.nodeName === 'TD' || n.nodeName === 'TH'; |
| }, false)); |
| } |
| return this._cells; |
| }, |
| get rowIndex() { |
| var table = closest(this, 'TABLE'); |
| return table ? table.rows._toArray().indexOf(this) : -1; |
| }, |
| |
| get sectionRowIndex() { |
| return this._parentNode.rows._toArray().indexOf(this); |
| }, |
| insertCell: function(index) { |
| var td = this._ownerDocument.createElement('TD'); |
| var cells = this.cells._toArray(); |
| if (index < -1 || index > cells.length) { |
| throw new core.DOMException(core.INDEX_SIZE_ERR); |
| } |
| if (index === -1 || index === cells.length) { |
| this.appendChild(td); |
| } |
| else { |
| var ref = cells[index]; |
| this.insertBefore(td, ref); |
| } |
| return td; |
| }, |
| deleteCell: function(index) { |
| var cells = this.cells._toArray(); |
| if (index === -1) { |
| index = cells.length-1; |
| } |
| if (index < 0 || index >= cells.length) { |
| throw new core.DOMException(core.INDEX_SIZE_ERR); |
| } |
| var td = this.cells[index]; |
| this.removeChild(td); |
| } |
| }, |
| attributes: [ |
| 'align', |
| 'bgColor', |
| {prop: 'ch', attr: 'char'}, |
| {prop: 'chOff', attr: 'charoff'}, |
| 'vAlign' |
| ] |
| }); |
| |
| define('HTMLTableCellElement', { |
| tagNames: ['TH','TD'], |
| proto: { |
| _headers: null, |
| set headers(h) { |
| if (h === '') { |
| //Handle resetting headers so the dynamic getter returns a query |
| this._headers = null; |
| return; |
| } |
| if (!(h instanceof Array)) { |
| h = [h]; |
| } |
| this._headers = h; |
| }, |
| get headers() { |
| if (this._headers) { |
| return this._headers.join(' '); |
| } |
| var cellIndex = this.cellIndex, |
| headings = [], |
| siblings = this._parentNode.getElementsByTagName(this.tagName); |
| |
| for (var i=0; i<siblings.length; i++) { |
| if (siblings.item(i).cellIndex >= cellIndex) { |
| break; |
| } |
| headings.push(siblings.item(i).id); |
| } |
| this._headers = headings; |
| return headings.join(' '); |
| }, |
| get cellIndex() { |
| return closest(this, 'TR').cells._toArray().indexOf(this); |
| } |
| }, |
| attributes: [ |
| 'abbr', |
| 'align', |
| 'axis', |
| 'bgColor', |
| {prop: 'ch', attr: 'char'}, |
| {prop: 'chOff', attr: 'charoff'}, |
| {prop: 'colSpan', type: 'long'}, |
| 'height', |
| {prop: 'noWrap', type: 'boolean'}, |
| {prop: 'rowSpan', type: 'long'}, |
| 'scope', |
| 'vAlign', |
| 'width' |
| ] |
| }); |
| |
| define('HTMLFrameSetElement', { |
| tagName: 'FRAMESET', |
| attributes: [ |
| 'cols', |
| 'rows' |
| ] |
| }); |
| |
| function loadFrame (frame) { |
| if (frame._contentDocument) { |
| // We don't want to access document.parentWindow, since the getter will |
| // cause a new window to be allocated if it doesn't exist. Probe the |
| // private variable instead. |
| if (frame._contentDocument._parentWindow) { |
| // close calls delete on its document. |
| frame._contentDocument.parentWindow.close(); |
| } else { |
| delete frame._contentDocument; |
| } |
| } |
| |
| var src = frame.src.trim() === '' ? 'about:blank' : frame.src; |
| var parentDoc = frame._ownerDocument; |
| |
| // If the URL can't be resolved or the src attribute is missing / blank, |
| // then url should be set to the string "about:blank". |
| // (http://www.whatwg.org/specs/web-apps/current-work/#the-iframe-element) |
| var url = core.resourceLoader.resolve(parentDoc, src); |
| var contentDoc = frame._contentDocument = new core.HTMLDocument({ |
| parsingMode: 'html', |
| url: url, |
| documentRoot: Path.dirname(url) |
| }); |
| applyDocumentFeatures(contentDoc, parentDoc.implementation._features); |
| |
| var parent = parentDoc.parentWindow; |
| var contentWindow = contentDoc.parentWindow; |
| contentWindow.parent = parent; |
| contentWindow.top = parent.top; |
| |
| // Handle about:blank with a simulated load of an empty document. |
| if(url === 'about:blank') { |
| core.resourceLoader.enqueue(frame, function() { |
| contentDoc.write(); |
| contentDoc.close(); |
| })(); |
| } else { |
| core.resourceLoader.load(frame, url, function(html, filename) { |
| contentDoc.write(html); |
| contentDoc.close(); |
| }); |
| } |
| } |
| |
| define('HTMLFrameElement', { |
| tagName: 'FRAME', |
| init : function () { |
| // Set up the frames array. window.frames really just returns a reference |
| // to the window object, so the frames array is just implemented as indexes |
| // on the window. |
| var parent = this._ownerDocument.parentWindow; |
| var frameID = parent._length++; |
| var self = this; |
| defineGetter(parent, frameID, function () { |
| return self.contentWindow; |
| }); |
| |
| // The contentDocument/contentWindow shouldn't be created until the frame |
| // is inserted: |
| // "When an iframe element is first inserted into a document, the user |
| // agent must create a nested browsing context, and then process the |
| // iframe attributes for the first time." |
| // (http://www.whatwg.org/specs/web-apps/current-work/#the-iframe-element) |
| this._initInsertListener = function () { |
| loadFrame(self); |
| }; |
| this.addEventListener('DOMNodeInsertedIntoDocument', this._initInsertListener, false); |
| }, |
| proto: { |
| _attrModified: function(name, value, oldVal) { |
| core.HTMLElement.prototype._attrModified.call(this, name, value, oldVal); |
| var self = this; |
| if (name === 'name') { |
| // Remove named frame access. |
| if (oldVal) { |
| this._ownerDocument.parentWindow._frame(oldVal); |
| } |
| // Set up named frame access. |
| if (value) { |
| this._ownerDocument.parentWindow._frame(value, this); |
| } |
| } else if (name === 'src') { |
| // Page we don't fetch the page until the node is inserted. This at |
| // least seems to be the way Chrome does it. |
| if (!this._attachedToDocument) { |
| if (!this._waitingOnInsert) { |
| // First, remove the listener added in 'init'. |
| this.removeEventListener('DOMNodeInsertedIntoDocument', |
| this._initInsertListener, false) |
| |
| // If we aren't already waiting on an insert, add a listener. |
| // This guards against src being set multiple times before the frame |
| // is inserted into the document - we don't want to register multiple |
| // callbacks. |
| this.addEventListener('DOMNodeInsertedIntoDocument', function loader () { |
| self.removeEventListener('DOMNodeInsertedIntoDocument', loader, false); |
| this._waitingOnInsert = false; |
| loadFrame(self); |
| }, false); |
| this._waitingOnInsert = true; |
| } |
| } else { |
| loadFrame(self); |
| } |
| } |
| }, |
| _contentDocument : null, |
| get contentDocument() { |
| if (this._contentDocument == null) { |
| this._contentDocument = new core.HTMLDocument({ parsingMode: "html" }); |
| } |
| return this._contentDocument; |
| }, |
| get contentWindow() { |
| return this.contentDocument.parentWindow; |
| } |
| }, |
| attributes: [ |
| 'frameBorder', |
| 'longDesc', |
| 'marginHeight', |
| 'marginWidth', |
| 'name', |
| {prop: 'noResize', type: 'boolean'}, |
| 'scrolling', |
| {prop: 'src', type: 'string', write: false} |
| ] |
| }); |
| |
| define('HTMLIFrameElement', { |
| tagName: 'IFRAME', |
| parentClass: core.HTMLFrameElement, |
| attributes: [ |
| 'align', |
| 'frameBorder', |
| 'height', |
| 'longDesc', |
| 'marginHeight', |
| 'marginWidth', |
| 'name', |
| 'scrolling', |
| 'src', |
| 'width' |
| ] |
| }); |