1 var core
= require("./core").dom
.level2
.core
,
2 events
= require("./core").dom
.level2
.events
,
3 applyDocumentFeatures
= require('../browser/documentfeatures').applyDocumentFeatures
,
5 Path
= require('path'),
7 http
= require('http'),
8 https
= require('https');
10 // modify cloned instance for more info check: https://github.com/tmpvar/jsdom/issues/325
11 core
= Object
.create(core
);
13 // Setup the javascript language processor
14 core
.languageProcessors
= {
15 javascript
: require("./languages/javascript").javascript
18 core
.resourceLoader
= {
19 load: function(element
, href
, callback
) {
20 var ownerImplementation
= element
._ownerDocument
.implementation
;
22 if (ownerImplementation
.hasFeature('FetchExternalResources', element
.tagName
.toLowerCase())) {
23 var full
= this.resolve(element
._ownerDocument
, href
);
24 var url
= URL
.parse(full
);
26 this.download(url
, this.baseUrl(element
._ownerDocument
), this.enqueue(element
, callback
, full
));
29 this.readFile(url
.pathname
, this.enqueue(element
, callback
, full
));
33 enqueue: function(element
, callback
, filename
) {
35 doc
= element
.nodeType
=== core
.Node
.DOCUMENT_NODE
?
37 element
._ownerDocument
;
43 return doc
._queue
.push(function(err
, data
) {
44 var ev
= doc
.createEvent('HTMLEvents');
48 callback
.call(element
, data
, filename
|| doc
.URL
);
49 ev
.initEvent('load', false, false);
57 ev
.initEvent('error', false, false);
61 element
.dispatchEvent(ev
);
65 baseUrl: function(document
) {
66 var baseElements
= document
.getElementsByTagName('base'),
67 baseUrl
= document
.URL
;
69 if (baseElements
.length
> 0) {
70 baseUrl
= baseElements
.item(0).href
;
75 resolve: function(document
, href
) {
76 if (href
.match(/^\w+:\/\//)) {
80 var baseUrl
= this.baseUrl(document
);
82 // See RFC 2396 section 3 for this weirdness. URLs without protocol
83 // have their protocol default to the current one.
84 // http://www.ietf.org/rfc/rfc2396.txt
85 if (href
.match(/^\/\//)) {
86 return baseUrl
? baseUrl
.match(/^(\w+:)\/\//)[1] + href
: null;
87 } else if (!href
.match(/^\/[^\/]/)) {
88 href
= href
.replace(/^\//, "");
91 return URL
.resolve(baseUrl
, href
);
93 download: function(url
, referrer
, callback
) {
94 var path
= url
.pathname
+ (url
.search
|| ''),
95 options
= {'method': 'GET', 'host': url
.hostname
, 'path': path
},
97 if (url
.protocol
=== 'https:') {
98 options
.port
= url
.port
|| 443;
99 request
= https
.request(options
);
101 options
.port
= url
.port
|| 80;
102 request
= http
.request(options
);
107 request
.setHeader('Referer', referrer
);
110 request
.on('response', function (response
) {
112 function success () {
113 if ([301, 302, 303, 307].indexOf(response
.statusCode
) > -1) {
114 var redirect
= URL
.resolve(url
, response
.headers
["location"]);
115 core
.resourceLoader
.download(URL
.parse(redirect
), referrer
, callback
);
117 callback(null, data
);
120 response
.setEncoding('utf8');
121 response
.on('data', function (chunk
) {
122 data
+= chunk
.toString();
124 response
.on('end', function() {
125 // According to node docs, 'close' can fire after 'end', but not
126 // vice versa. Remove 'close' listener so we don't call success twice.
127 response
.removeAllListeners('close');
130 response
.on('close', function (err
) {
139 request
.on('error', callback
);
142 readFile: function(url
, callback
) {
143 fs
.readFile(url
.replace(/^file:\/\//, ""), 'utf8', callback
);
147 function define(elementClass
, def
) {
148 var tagName
= def
.tagName
,
149 tagNames
= def
.tagNames
|| (tagName
? [tagName
] : []),
150 parentClass
= def
.parentClass
|| core
.HTMLElement
,
151 attrs
= def
.attributes
|| [],
152 proto
= def
.proto
|| {};
154 var elem
= core
[elementClass
] = function(document
, name
) {
155 parentClass
.call(this, document
, name
|| tagName
.toUpperCase());
157 elem
._init
.call(this);
160 elem
._init
= def
.init
;
162 elem
.prototype = proto
;
163 elem
.prototype.__proto__
= parentClass
.prototype;
165 attrs
.forEach(function(n
) {
166 var prop
= n
.prop
|| n
,
167 attr
= n
.attr
|| prop
.toLowerCase();
169 if (!n
.prop
|| n
.read
!== false) {
170 elem
.prototype.__defineGetter__(prop
, function() {
171 var s
= this.getAttribute(attr
);
172 if (n
.type
&& n
.type
=== 'boolean') {
175 if (n
.type
&& n
.type
=== 'long') {
179 return n
.normalize(s
);
185 if (!n
.prop
|| n
.write
!== false) {
186 elem
.prototype.__defineSetter__(prop
, function(val
) {
188 this.removeAttribute(attr
);
191 var s
= val
.toString();
195 this.setAttribute(attr
, s
);
201 tagNames
.forEach(function(tag
) {
202 core
.Document
.prototype._elementBuilders
[tag
.toLowerCase()] = function(doc
, s
) {
203 var el
= new elem(doc
, s
);
211 core
.HTMLCollection
= function HTMLCollection(element
, query
) {
212 core
.NodeList
.call(this, element
, query
);
214 core
.HTMLCollection
.prototype = {
215 namedItem : function(name
) {
216 var results
= this.toArray(),
221 for (var i
=0; i
<l
; i
++) {
223 if (node
.getAttribute('id') === name
) {
225 } else if (node
.getAttribute('name') === name
) {
231 toString: function() {
232 return '[ jsdom HTMLCollection ]: contains ' + this.length
+ ' items';
235 core
.HTMLCollection
.prototype.__proto__
= core
.NodeList
.prototype;
237 core
.HTMLOptionsCollection
= core
.HTMLCollection
;
239 function closest(e
, tagName
) {
240 tagName
= tagName
.toUpperCase();
242 if (e
.nodeName
.toUpperCase() === tagName
||
243 (e
.tagName
&& e
.tagName
.toUpperCase() === tagName
))
252 function descendants(e
, tagName
, recursive
) {
253 var owner
= recursive
? e
._ownerDocument
|| e
: e
;
254 return new core
.HTMLCollection(owner
, core
.mapper(e
, function(n
) {
255 return n
.nodeName
=== tagName
&& typeof n
._publicId
== 'undefined';
259 function firstChild(e
, tagName
) {
263 var c
= descendants(e
, tagName
, false);
264 return c
.length
> 0 ? c
[0] : null;
267 function ResourceQueue(paused
) {
268 this.paused
= !!paused
;
270 ResourceQueue
.prototype = {
271 push: function(callback
) {
276 if (!q
.paused
&& !this.prev
&& this.fired
){
277 callback(this.err
, this.data
);
279 this.next
.prev
= null;
281 }else{//q.tail===this
291 return function(err
, data
) {
303 var head
= this.tail
;
304 while(head
&& head
.prev
){
313 core
.HTMLDocument
= function HTMLDocument(options
) {
314 options
= options
|| {};
315 if (!options
.contentType
) {
316 options
.contentType
= 'text/html';
318 core
.Document
.call(this, options
);
319 this._referrer
= options
.referrer
;
320 this._cookie
= options
.cookie
;
321 this._URL
= options
.url
|| '/';
322 this._documentRoot
= options
.documentRoot
|| Path
.dirname(this._URL
);
323 this._queue
= new ResourceQueue(options
.deferClose
);
324 this.readyState
= 'loading';
326 // Add level2 features
327 this.implementation
.addFeature('core' , '2.0');
328 this.implementation
.addFeature('html' , '2.0');
329 this.implementation
.addFeature('xhtml' , '2.0');
330 this.implementation
.addFeature('xml' , '2.0');
333 core
.HTMLDocument
.prototype = {
336 return this._referrer
|| '';
346 return this.getElementsByTagName('IMG');
349 return new core
.HTMLCollection(this, core
.mapper(this, function(el
) {
350 if (el
&& el
.tagName
) {
351 var upper
= el
.tagName
.toUpperCase();
352 if (upper
=== "APPLET") {
354 } else if (upper
=== "OBJECT" &&
355 el
.getElementsByTagName('APPLET').length
> 0)
363 return new core
.HTMLCollection(this, core
.mapper(this, function(el
) {
364 if (el
&& el
.tagName
) {
365 var upper
= el
.tagName
.toUpperCase();
366 if (upper
=== "AREA" || (upper
=== "A" && el
.href
)) {
373 return this.getElementsByTagName('FORM');
376 return this.getElementsByTagName('A');
379 this._childNodes
= [];
380 this._documentElement
= null;
384 this._queue
.resume();
385 // Set the readyState to 'complete' once all resources are loaded.
386 // As a side-effect the document's load-event will be dispatched.
387 core
.resourceLoader
.enqueue(this, function() {
388 this.readyState
= 'complete';
389 var ev
= this.createEvent('HTMLEvents');
390 ev
.initEvent('DOMContentLoaded', false, false);
391 this.dispatchEvent(ev
);
395 write : function(text
) {
396 if (this.readyState
=== "loading") {
397 // During page loading, document.write appends to the current element
398 // Find the last child that has been added to the document.
400 while (node
.lastChild
&& node
.lastChild
.nodeType
=== this.ELEMENT_NODE
) {
401 node
= node
.lastChild
;
403 node
.innerHTML
= text
;
405 this.innerHTML
= text
;
409 writeln : function(text
) {
410 this.write(text
+ '\n');
413 getElementsByName : function(elementName
) {
414 return new core
.HTMLCollection(this, core
.mapper(this, function(el
) {
415 return (el
.getAttribute
&& el
.getAttribute("name") === elementName
);
420 var head
= this.head
,
421 title
= head
? firstChild(head
, 'TITLE') : null;
422 return title
? title
.textContent
: '';
426 var title
= firstChild(this.head
, 'TITLE');
428 title
= this.createElement('TITLE');
429 var head
= this.head
;
431 head
= this.createElement('HEAD');
432 this.documentElement
.insertBefore(head
, this.documentElement
.firstChild
);
434 head
.appendChild(title
);
436 title
.textContent
= val
;
440 return firstChild(this.documentElement
, 'HEAD');
443 set head() { /* noop */ },
446 var body
= firstChild(this.documentElement
, 'BODY');
448 body
= firstChild(this.documentElement
, 'FRAMESET');
453 get documentElement() {
454 if (!this._documentElement
) {
455 this._documentElement
= firstChild(this, 'HTML');
457 return this._documentElement
;
461 get cookie() { return this._cookie
|| ''; },
462 set cookie(val
) { this._cookie
= val
; }
464 core
.HTMLDocument
.prototype.__proto__
= core
.Document
.prototype;
466 define('HTMLElement', {
467 parentClass
: core
.Element
,
469 // Add default event behavior (click link to navigate, click button to submit
470 // form, etc). We start by wrapping dispatchEvent so we can forward events to
471 // the element's _eventDefault function (only events that did not incur
473 dispatchEvent : function (event
) {
474 var outcome
= core
.Node
.prototype.dispatchEvent
.call(this, event
)
476 if (!event
._preventDefault
&&
477 event
.target
._eventDefaults
[event
.type
] &&
478 typeof event
.target
._eventDefaults
[event
.type
] === 'function')
480 event
.target
._eventDefaults
[event
.type
](event
)
491 {prop
: 'className', attr
: 'class', normalize: function(s
) { return s
|| ''; }}
495 core
.Document
.prototype._defaultElementBuilder = function(document
, tagName
) {
496 return new core
.HTMLElement(document
, tagName
);
499 //http://www.w3.org/TR/html5/forms.html#category-listed
500 var listedElements
= /button|fieldset|input|keygen|object|select|textarea/i;
502 define('HTMLFormElement', {
506 return new core
.HTMLCollection(this._ownerDocument
, core
.mapper(this, function(e
) {
507 return listedElements
.test(e
.nodeName
) ; // TODO exclude <input type="image">
511 return this.elements
.length
;
513 _dispatchSubmitEvent: function() {
514 var ev
= this._ownerDocument
.createEvent('HTMLEvents');
515 ev
.initEvent('submit', true, true);
516 if (!this.dispatchEvent(ev
)) {
523 this.elements
.toArray().forEach(function(el
) {
524 el
.value
= el
.defaultValue
;
530 {prop
: 'acceptCharset', attr
: 'accept-charset'},
538 define('HTMLLinkElement', {
542 return core
.resourceLoader
.resolve(this._ownerDocument
, this.getAttribute('href'));
546 {prop
: 'disabled', type
: 'boolean'},
558 define('HTMLMetaElement', {
562 {prop
: 'httpEquiv', attr
: 'http-equiv'},
568 define('HTMLHtmlElement', {
575 define('HTMLHeadElement', {
582 define('HTMLTitleElement', {
586 return this.innerHTML
;
594 define('HTMLBaseElement', {
604 define('HTMLIsIndexElement', {
606 parentClass
: core
.Element
,
609 return closest(this, 'FORM');
618 define('HTMLStyleElement', {
621 {prop
: 'disabled', type
: 'boolean'},
627 define('HTMLBodyElement', {
630 // The body element's "traditional" event handlers are proxied to the
632 // See: http://dev.w3.org/html5/spec/Overview.html#the-body-element
633 ['onafterprint', 'onbeforeprint', 'onbeforeunload', 'onblur', 'onerror',
634 'onfocus', 'onhashchange', 'onload', 'onmessage', 'onoffline', 'ononline',
635 'onpagehide', 'onpageshow', 'onpopstate', 'onresize', 'onscroll',
636 'onstorage', 'onunload'].forEach(function (name
) {
637 proto
.__defineSetter__(name
, function (handler
) {
638 this._ownerDocument
.parentWindow
[name
] = handler
;
640 proto
.__defineGetter__(name
, function () {
641 return this._ownerDocument
.parentWindow
[name
];
657 define('HTMLSelectElement', {
661 return new core
.HTMLOptionsCollection(this, core
.mapper(this, function(n
) {
662 return n
.nodeName
=== 'OPTION';
667 return this.options
.length
;
670 get selectedIndex() {
671 return this.options
.toArray().reduceRight(function(prev
, option
, i
) {
672 return option
.selected
? i
: prev
;
676 set selectedIndex(index
) {
677 this.options
.toArray().forEach(function(option
, i
) {
678 option
.selected
= i
=== index
;
683 var i
= this.selectedIndex
;
684 if (this.options
.length
&& (i
=== -1)) {
690 return this.options
[i
].value
;
695 this.options
.toArray().forEach(function(option
) {
696 if (option
.value
=== val
) {
697 option
.selected
= true;
699 if (!self
.hasAttribute('multiple')) {
700 // Remove the selected bit from all other options in this group
701 // if the multiple attr is not present on the select
702 option
.selected
= false;
709 return closest(this, 'FORM');
713 return this.multiple
? 'select-multiple' : 'select-one';
716 add: function(opt
, before
) {
718 this.insertBefore(opt
, before
);
721 this.appendChild(opt
);
725 remove: function(index
) {
726 var opts
= this.options
.toArray();
727 if (index
>= 0 && index
< opts
.length
) {
728 var el
= opts
[index
];
729 el
._parentNode
.removeChild(el
);
742 {prop
: 'disabled', type
: 'boolean'},
743 {prop
: 'multiple', type
: 'boolean'},
745 {prop
: 'size', type
: 'long'},
746 {prop
: 'tabIndex', type
: 'long'},
750 define('HTMLOptGroupElement', {
753 {prop
: 'disabled', type
: 'boolean'},
758 define('HTMLOptionElement', {
761 _attrModified: function(name
, value
) {
762 if (name
=== 'selected') {
763 this.selected
= this.defaultSelected
;
765 core
.HTMLElement
.prototype._attrModified
.call(this, arguments
);
768 return closest(this, 'FORM');
770 get defaultSelected() {
771 return !!this.getAttribute('selected');
773 set defaultSelected(s
) {
774 if (s
) this.setAttribute('selected', 'selected');
775 else this.removeAttribute('selected');
778 return (this.hasAttribute('value')) ? this.getAttribute('value') : this.innerHTML
;
781 return (this.hasAttribute('value')) ? this.getAttribute('value') : this.innerHTML
;
784 this.setAttribute('value', val
);
787 return closest(this, 'SELECT').options
.toArray().indexOf(this);
790 if (this._selected
=== undefined) {
791 this._selected
= this.defaultSelected
;
793 return this._selected
;
796 // TODO: The 'selected' content attribute is the initial value of the
797 // IDL attribute, but the IDL attribute should not relfect the content
798 this._selected
= !!s
;
800 //Remove the selected bit from all other options in this select
801 var select
= this._parentNode
;
803 if (select
.nodeName
!== 'SELECT') {
804 select
= select
._parentNode
;
806 if (select
.nodeName
!== 'SELECT') return;
808 if (!select
.multiple
) {
809 var o
= select
.options
;
810 for (var i
= 0; i
< o
.length
; i
++) {
812 o
[i
].selected
= false;
820 {prop
: 'disabled', type
: 'boolean'},
825 define('HTMLInputElement', {
828 _initDefaultValue: function() {
829 if (this._defaultValue
=== undefined) {
830 var attr
= this.getAttributeNode('value');
831 this._defaultValue
= attr
? attr
.value
: null;
833 return this._defaultValue
;
835 _initDefaultChecked: function() {
836 if (this._defaultChecked
=== undefined) {
837 this._defaultChecked
= !!this.getAttribute('checked');
839 return this._defaultChecked
;
842 return closest(this, 'FORM');
845 return this._initDefaultValue();
847 get defaultChecked() {
848 return this._initDefaultChecked();
851 return !!this.getAttribute('checked');
853 set checked(checked
) {
854 this._initDefaultChecked();
855 this.setAttribute('checked', checked
);
858 return this.getAttribute('value');
861 this._initDefaultValue();
863 this.removeAttribute('value');
866 this.setAttribute('value', val
);
876 if (this.type
=== 'checkbox' || this.type
=== 'radio') {
877 this.checked
= !this.checked
;
879 else if (this.type
=== 'submit') {
880 var form
= this.form
;
882 form
._dispatchSubmitEvent();
892 {prop
: 'disabled', type
: 'boolean'},
893 {prop
: 'maxLength', type
: 'long'},
895 {prop
: 'readOnly', type
: 'boolean'},
896 {prop
: 'size', type
: 'long'},
898 {prop
: 'tabIndex', type
: 'long'},
899 {prop
: 'type', normalize: function(val
) {
900 return val
? val
.toLowerCase() : 'text';
906 define('HTMLTextAreaElement', {
909 _initDefaultValue: function() {
910 if (this._defaultValue
=== undefined) {
911 this._defaultValue
= this.textContent
;
913 return this._defaultValue
;
916 return closest(this, 'FORM');
919 return this._initDefaultValue();
922 return this.textContent
;
925 this._initDefaultValue();
926 this.textContent
= val
;
940 {prop
: 'cols', type
: 'long'},
941 {prop
: 'disabled', type
: 'boolean'},
942 {prop
: 'maxLength', type
: 'long'},
944 {prop
: 'readOnly', type
: 'boolean'},
945 {prop
: 'rows', type
: 'long'},
946 {prop
: 'tabIndex', type
: 'long'}
950 define('HTMLButtonElement', {
954 return closest(this, 'FORM');
959 {prop
: 'disabled', type
: 'boolean'},
961 {prop
: 'tabIndex', type
: 'long'},
967 define('HTMLLabelElement', {
971 return closest(this, 'FORM');
976 {prop
: 'htmlFor', attr
: 'for'}
980 define('HTMLFieldSetElement', {
984 return closest(this, 'FORM');
989 define('HTMLLegendElement', {
993 return closest(this, 'FORM');
1002 define('HTMLUListElement', {
1005 {prop
: 'compact', type
: 'boolean'},
1010 define('HTMLOListElement', {
1013 {prop
: 'compact', type
: 'boolean'},
1014 {prop
: 'start', type
: 'long'},
1019 define('HTMLDListElement', {
1022 {prop
: 'compact', type
: 'boolean'}
1026 define('HTMLDirectoryElement', {
1029 {prop
: 'compact', type
: 'boolean'}
1033 define('HTMLMenuElement', {
1036 {prop
: 'compact', type
: 'boolean'}
1040 define('HTMLLIElement', {
1044 {prop
: 'value', type
: 'long'}
1048 define('HTMLDivElement', {
1055 define('HTMLParagraphElement', {
1062 define('HTMLHeadingElement', {
1063 tagNames
: ['H1','H2','H3','H4','H5','H6'],
1069 define('HTMLQuoteElement', {
1070 tagNames
: ['Q','BLOCKQUOTE'],
1076 define('HTMLPreElement', {
1079 {prop
: 'width', type
: 'long'}
1083 define('HTMLBRElement', {
1090 define('HTMLBaseFontElement', {
1091 tagName
: 'BASEFONT',
1095 {prop
: 'size', type
: 'long'}
1099 define('HTMLFontElement', {
1108 define('HTMLHRElement', {
1112 {prop
: 'noShade', type
: 'boolean'},
1118 define('HTMLModElement', {
1119 tagNames
: ['INS', 'DEL'],
1126 define('HTMLAnchorElement', {
1135 return core
.resourceLoader
.resolve(this._ownerDocument
, this.getAttribute('href'));
1138 return URL
.parse(this.href
).hostname
;
1141 return URL
.parse(this.href
).pathname
;
1148 {prop
: 'href', type
: 'string', read
: false},
1154 {prop
: 'tabIndex', type
: 'long'},
1160 define('HTMLImageElement', {
1167 {prop
: 'height', type
: 'long'},
1168 {prop
: 'hspace', type
: 'long'},
1169 {prop
: 'isMap', type
: 'boolean'},
1173 {prop
: 'vspace', type
: 'long'},
1174 {prop
: 'width', type
: 'long'}
1178 define('HTMLObjectElement', {
1182 return closest(this, 'FORM');
1184 get contentDocument() {
1196 {prop
: 'declare', type
: 'boolean'},
1197 {prop
: 'height', type
: 'long'},
1198 {prop
: 'hspace', type
: 'long'},
1201 {prop
: 'tabIndex', type
: 'long'},
1204 {prop
: 'vspace', type
: 'long'},
1205 {prop
: 'width', type
: 'long'}
1209 define('HTMLParamElement', {
1219 define('HTMLAppletElement', {
1228 {prop
: 'hspace', type
: 'long'},
1231 {prop
: 'vspace', type
: 'long'},
1236 define('HTMLMapElement', {
1240 return this.getElementsByTagName("AREA");
1248 define('HTMLAreaElement', {
1255 {prop
: 'noHref', type
: 'boolean'},
1257 {prop
: 'tabIndex', type
: 'long'},
1262 define('HTMLScriptElement', {
1265 this.addEventListener('DOMNodeInsertedIntoDocument', function() {
1267 core
.resourceLoader
.load(this, this.src
, this._eval
);
1270 var src
= this.sourceLocation
|| {},
1271 filename
= src
.file
|| this._ownerDocument
.URL
;
1274 filename
+= ':' + src
.line
+ ':' + src
.col
;
1276 filename
+= '<script>';
1278 core
.resourceLoader
.enqueue(this, this._eval
, filename
)(null, this.text
);
1283 _eval: function(text
, filename
) {
1284 if (this._ownerDocument
.implementation
.hasFeature("ProcessExternalResources", "script") &&
1286 core
.languageProcessors
[this.language
])
1288 core
.languageProcessors
[this.language
](this, text
, filename
);
1292 var type
= this.type
|| "text/javascript";
1293 return type
.split("/").pop().toLowerCase();
1296 var i
=0, children
= this.childNodes
, l
= children
.length
, ret
= [];
1299 ret
.push(children
.item(i
).value
);
1302 return ret
.join("");
1305 while (this.childNodes
.length
) {
1306 this.removeChild(this.childNodes
[0]);
1308 this.appendChild(this._ownerDocument
.createTextNode(text
));
1312 {prop
: 'defer', 'type': 'boolean'},
1321 define('HTMLTableElement', {
1325 return firstChild(this, 'CAPTION');
1328 return firstChild(this, 'THEAD');
1331 return firstChild(this, 'TFOOT');
1336 this._rows
= new core
.HTMLCollection(this._ownerDocument
, function() {
1337 var sections
= [table
.tHead
].concat(table
.tBodies
.toArray(), table
.tFoot
).filter(function(s
) { return !!s
});
1339 if (sections
.length
=== 0) {
1340 return core
.mapDOMNodes(table
, false, function(el
) {
1341 return el
.tagName
=== 'TR';
1345 return sections
.reduce(function(prev
, s
) {
1346 return prev
.concat(s
.rows
.toArray());
1354 if (!this._tBodies
) {
1355 this._tBodies
= descendants(this, 'TBODY', false);
1357 return this._tBodies
;
1359 createTHead: function() {
1360 var el
= this.tHead
;
1362 el
= this._ownerDocument
.createElement('THEAD');
1363 this.appendChild(el
);
1367 deleteTHead: function() {
1368 var el
= this.tHead
;
1370 el
._parentNode
.removeChild(el
);
1373 createTFoot: function() {
1374 var el
= this.tFoot
;
1376 el
= this._ownerDocument
.createElement('TFOOT');
1377 this.appendChild(el
);
1381 deleteTFoot: function() {
1382 var el
= this.tFoot
;
1384 el
._parentNode
.removeChild(el
);
1387 createCaption: function() {
1388 var el
= this.caption
;
1390 el
= this._ownerDocument
.createElement('CAPTION');
1391 this.appendChild(el
);
1395 deleteCaption: function() {
1396 var c
= this.caption
;
1398 c
._parentNode
.removeChild(c
);
1401 insertRow: function(index
) {
1402 var tr
= this._ownerDocument
.createElement('TR');
1403 if (this.childNodes
.length
=== 0) {
1404 this.appendChild(this._ownerDocument
.createElement('TBODY'));
1406 var rows
= this.rows
.toArray();
1407 if (index
< -1 || index
> rows
.length
) {
1408 throw new core
.DOMException(core
.INDEX_SIZE_ERR
);
1410 if (index
=== -1 || (index
=== 0 && rows
.length
=== 0)) {
1411 this.tBodies
.item(0).appendChild(tr
);
1413 else if (index
=== rows
.length
) {
1414 var ref
= rows
[index
-1];
1415 ref
._parentNode
.appendChild(tr
);
1418 var ref
= rows
[index
];
1419 ref
._parentNode
.insertBefore(tr
, ref
);
1423 deleteRow: function(index
) {
1424 var rows
= this.rows
.toArray(), l
= rows
.length
;
1428 if (index
< 0 || index
>= l
) {
1429 throw new core
.DOMException(core
.INDEX_SIZE_ERR
);
1431 var tr
= rows
[index
];
1432 tr
._parentNode
.removeChild(tr
);
1448 define('HTMLTableCaptionElement', {
1455 define('HTMLTableColElement', {
1456 tagNames
: ['COL','COLGROUP'],
1459 {prop
: 'ch', attr
: 'char'},
1460 {prop
: 'chOff', attr
: 'charoff'},
1461 {prop
: 'span', type
: 'long'},
1467 define('HTMLTableSectionElement', {
1468 tagNames
: ['THEAD','TBODY','TFOOT'],
1472 this._rows
= descendants(this, 'TR', false);
1476 insertRow: function(index
) {
1477 var tr
= this._ownerDocument
.createElement('TR');
1478 var rows
= this.rows
.toArray();
1479 if (index
< -1 || index
> rows
.length
) {
1480 throw new core
.DOMException(core
.INDEX_SIZE_ERR
);
1482 if (index
=== -1 || index
=== rows
.length
) {
1483 this.appendChild(tr
);
1486 var ref
= rows
[index
];
1487 this.insertBefore(tr
, ref
);
1491 deleteRow: function(index
) {
1492 var rows
= this.rows
.toArray();
1494 index
= rows
.length
-1;
1496 if (index
< 0 || index
>= rows
.length
) {
1497 throw new core
.DOMException(core
.INDEX_SIZE_ERR
);
1499 var tr
= this.rows
[index
];
1500 this.removeChild(tr
);
1505 {prop
: 'ch', attr
: 'char'},
1506 {prop
: 'chOff', attr
: 'charoff'},
1507 {prop
: 'span', type
: 'long'},
1513 define('HTMLTableRowElement', {
1518 this._cells
= new core
.HTMLCollection(this, core
.mapper(this, function(n
) {
1519 return n
.nodeName
=== 'TD' || n
.nodeName
=== 'TH';
1525 return closest(this, 'TABLE').rows
.toArray().indexOf(this);
1528 get sectionRowIndex() {
1529 return this._parentNode
.rows
.toArray().indexOf(this);
1531 insertCell: function(index
) {
1532 var td
= this._ownerDocument
.createElement('TD');
1533 var cells
= this.cells
.toArray();
1534 if (index
< -1 || index
> cells
.length
) {
1535 throw new core
.DOMException(core
.INDEX_SIZE_ERR
);
1537 if (index
=== -1 || index
=== cells
.length
) {
1538 this.appendChild(td
);
1541 var ref
= cells
[index
];
1542 this.insertBefore(td
, ref
);
1546 deleteCell: function(index
) {
1547 var cells
= this.cells
.toArray();
1549 index
= cells
.length
-1;
1551 if (index
< 0 || index
>= cells
.length
) {
1552 throw new core
.DOMException(core
.INDEX_SIZE_ERR
);
1554 var td
= this.cells
[index
];
1555 this.removeChild(td
);
1561 {prop
: 'ch', attr
: 'char'},
1562 {prop
: 'chOff', attr
: 'charoff'},
1567 define('HTMLTableCellElement', {
1568 tagNames
: ['TH','TD'],
1573 //Handle resetting headers so the dynamic getter returns a query
1574 this._headers
= null;
1577 if (!(h
instanceof Array
)) {
1583 if (this._headers
) {
1584 return this._headers
.join(' ');
1586 var cellIndex
= this.cellIndex
,
1588 siblings
= this._parentNode
.getElementsByTagName(this.tagName
);
1590 for (var i
=0; i
<siblings
.length
; i
++) {
1591 if (siblings
.item(i
).cellIndex
>= cellIndex
) {
1594 headings
.push(siblings
.item(i
).id
);
1596 this._headers
= headings
;
1597 return headings
.join(' ');
1600 return closest(this, 'TR').cells
.toArray().indexOf(this);
1608 {prop
: 'ch', attr
: 'char'},
1609 {prop
: 'chOff', attr
: 'charoff'},
1610 {prop
: 'colSpan', type
: 'long'},
1612 {prop
: 'noWrap', type
: 'boolean'},
1613 {prop
: 'rowSpan', type
: 'long'},
1620 define('HTMLFrameSetElement', {
1621 tagName
: 'FRAMESET',
1628 function loadFrame (frame
) {
1629 if (frame
._contentDocument
) {
1630 // We don't want to access document.parentWindow, since the getter will
1631 // cause a new window to be allocated if it doesn't exist. Probe the
1632 // private variable instead.
1633 if (frame
._contentDocument
._parentWindow
) {
1634 // close calls delete on its document.
1635 frame
._contentDocument
.parentWindow
.close();
1637 delete frame
._contentDocument
;
1641 var src
= frame
.src
;
1642 var parentDoc
= frame
._ownerDocument
;
1643 var url
= core
.resourceLoader
.resolve(parentDoc
, src
);
1644 var contentDoc
= frame
._contentDocument
= new core
.HTMLDocument({
1646 documentRoot
: Path
.dirname(url
)
1648 applyDocumentFeatures(contentDoc
, parentDoc
.implementation
._features
);
1650 var parent
= parentDoc
.parentWindow
;
1651 var contentWindow
= contentDoc
.parentWindow
;
1652 contentWindow
.parent
= parent
;
1653 contentWindow
.top
= parent
.top
;
1655 core
.resourceLoader
.load(frame
, url
, function(html
, filename
) {
1656 contentDoc
.write(html
);
1661 define('HTMLFrameElement', {
1663 init : function () {
1664 // Set up the frames array. window.frames really just returns a reference
1665 // to the window object, so the frames array is just implemented as indexes
1667 var parent
= this._ownerDocument
.parentWindow
;
1668 var frameID
= parent
._length
++;
1670 parent
.__defineGetter__(frameID
, function () {
1671 return self
.contentWindow
;
1674 // The contentDocument/contentWindow shouldn't be created until the frame
1676 // "When an iframe element is first inserted into a document, the user
1677 // agent must create a nested browsing context, and then process the
1678 // iframe attributes for the first time."
1679 // (http://dev.w3.org/html5/spec/Overview.html#the-iframe-element)
1680 this._initInsertListener
= this.addEventListener('DOMNodeInsertedIntoDocument', function () {
1681 var parentDoc
= self
._ownerDocument
;
1682 // Calling contentDocument creates the Document if it doesn't exist.
1683 var doc
= self
.contentDocument
;
1684 applyDocumentFeatures(doc
, parentDoc
.implementation
._features
);
1685 var window
= self
.contentWindow
;
1686 window
.parent
= parent
;
1687 window
.top
= parent
.top
;
1691 setAttribute: function(name
, value
) {
1692 core
.HTMLElement
.prototype.setAttribute
.call(this, name
, value
);
1694 if (name
=== 'name') {
1695 // Set up named frame access.
1696 this._ownerDocument
.parentWindow
.__defineGetter__(value
, function () {
1697 return self
.contentWindow
;
1699 } else if (name
=== 'src') {
1700 // Page we don't fetch the page until the node is inserted. This at
1701 // least seems to be the way Chrome does it.
1702 if (!this._attachedToDocument
) {
1703 if (!this._waitingOnInsert
) {
1704 // First, remove the listener added in 'init'.
1705 this.removeEventListener('DOMNodeInsertedIntoDocument',
1706 this._initInsertListener
, false)
1708 // If we aren't already waiting on an insert, add a listener.
1709 // This guards against src being set multiple times before the frame
1710 // is inserted into the document - we don't want to register multiple
1712 this.addEventListener('DOMNodeInsertedIntoDocument', function loader () {
1713 self
.removeEventListener('DOMNodeInsertedIntoDocument', loader
, false);
1714 this._waitingOnInsert
= false;
1717 this._waitingOnInsert
= true;
1724 _contentDocument
: null,
1725 get contentDocument() {
1726 if (this._contentDocument
== null) {
1727 this._contentDocument
= new core
.HTMLDocument();
1729 return this._contentDocument
;
1731 get contentWindow() {
1732 return this.contentDocument
.parentWindow
;
1741 {prop
: 'noResize', type
: 'boolean'},
1743 {prop
: 'src', type
: 'string', write
: false}
1747 define('HTMLIFrameElement', {
1749 parentClass
: core
.HTMLFrameElement
,
1764 exports
.define
= define
;