initial commit
[JIRC.git] / node_modules / jsdom / lib / jsdom / level2 / html.js
1 var core = require("./core").dom.level2.core,
2 events = require("./core").dom.level2.events,
3 applyDocumentFeatures = require('../browser/documentfeatures').applyDocumentFeatures,
4 URL = require("url"),
5 Path = require('path'),
6 fs = require("fs"),
7 http = require('http'),
8 https = require('https');
9
10 // modify cloned instance for more info check: https://github.com/tmpvar/jsdom/issues/325
11 core = Object.create(core);
12
13 // Setup the javascript language processor
14 core.languageProcessors = {
15 javascript : require("./languages/javascript").javascript
16 };
17
18 core.resourceLoader = {
19 load: function(element, href, callback) {
20 var ownerImplementation = element._ownerDocument.implementation;
21
22 if (ownerImplementation.hasFeature('FetchExternalResources', element.tagName.toLowerCase())) {
23 var full = this.resolve(element._ownerDocument, href);
24 var url = URL.parse(full);
25 if (url.hostname) {
26 this.download(url, this.baseUrl(element._ownerDocument), this.enqueue(element, callback, full));
27 }
28 else {
29 this.readFile(url.pathname, this.enqueue(element, callback, full));
30 }
31 }
32 },
33 enqueue: function(element, callback, filename) {
34 var loader = this,
35 doc = element.nodeType === core.Node.DOCUMENT_NODE ?
36 element :
37 element._ownerDocument;
38
39 if (!doc._queue) {
40 return function() {};
41 }
42
43 return doc._queue.push(function(err, data) {
44 var ev = doc.createEvent('HTMLEvents');
45
46 if (!err) {
47 try {
48 callback.call(element, data, filename || doc.URL);
49 ev.initEvent('load', false, false);
50 }
51 catch(e) {
52 err = e;
53 }
54 }
55
56 if (err) {
57 ev.initEvent('error', false, false);
58 ev.error = err;
59 }
60
61 element.dispatchEvent(ev);
62 });
63 },
64
65 baseUrl: function(document) {
66 var baseElements = document.getElementsByTagName('base'),
67 baseUrl = document.URL;
68
69 if (baseElements.length > 0) {
70 baseUrl = baseElements.item(0).href;
71 }
72
73 return baseUrl;
74 },
75 resolve: function(document, href) {
76 if (href.match(/^\w+:\/\//)) {
77 return href;
78 }
79
80 var baseUrl = this.baseUrl(document);
81
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(/^\//, "");
89 }
90
91 return URL.resolve(baseUrl, href);
92 },
93 download: function(url, referrer, callback) {
94 var path = url.pathname + (url.search || ''),
95 options = {'method': 'GET', 'host': url.hostname, 'path': path},
96 request;
97 if (url.protocol === 'https:') {
98 options.port = url.port || 443;
99 request = https.request(options);
100 } else {
101 options.port = url.port || 80;
102 request = http.request(options);
103 }
104
105 // set header.
106 if (referrer) {
107 request.setHeader('Referer', referrer);
108 }
109
110 request.on('response', function (response) {
111 var data = '';
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);
116 } else {
117 callback(null, data);
118 }
119 }
120 response.setEncoding('utf8');
121 response.on('data', function (chunk) {
122 data += chunk.toString();
123 });
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');
128 success();
129 });
130 response.on('close', function (err) {
131 if (err) {
132 callback(err);
133 } else {
134 success();
135 }
136 });
137 });
138
139 request.on('error', callback);
140 request.end();
141 },
142 readFile: function(url, callback) {
143 fs.readFile(url.replace(/^file:\/\//, ""), 'utf8', callback);
144 }
145 };
146
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 || {};
153
154 var elem = core[elementClass] = function(document, name) {
155 parentClass.call(this, document, name || tagName.toUpperCase());
156 if (elem._init) {
157 elem._init.call(this);
158 }
159 };
160 elem._init = def.init;
161
162 elem.prototype = proto;
163 elem.prototype.__proto__ = parentClass.prototype;
164
165 attrs.forEach(function(n) {
166 var prop = n.prop || n,
167 attr = n.attr || prop.toLowerCase();
168
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') {
173 return !!s;
174 }
175 if (n.type && n.type === 'long') {
176 return +s;
177 }
178 if (n.normalize) {
179 return n.normalize(s);
180 }
181 return s;
182 });
183 }
184
185 if (!n.prop || n.write !== false) {
186 elem.prototype.__defineSetter__(prop, function(val) {
187 if (!val) {
188 this.removeAttribute(attr);
189 }
190 else {
191 var s = val.toString();
192 if (n.normalize) {
193 s = n.normalize(s);
194 }
195 this.setAttribute(attr, s);
196 }
197 });
198 }
199 });
200
201 tagNames.forEach(function(tag) {
202 core.Document.prototype._elementBuilders[tag.toLowerCase()] = function(doc, s) {
203 var el = new elem(doc, s);
204 return el;
205 };
206 });
207 }
208
209
210
211 core.HTMLCollection = function HTMLCollection(element, query) {
212 core.NodeList.call(this, element, query);
213 };
214 core.HTMLCollection.prototype = {
215 namedItem : function(name) {
216 var results = this.toArray(),
217 l = results.length,
218 node,
219 matchingName = null;
220
221 for (var i=0; i<l; i++) {
222 node = results[i];
223 if (node.getAttribute('id') === name) {
224 return node;
225 } else if (node.getAttribute('name') === name) {
226 matchingName = node;
227 }
228 }
229 return matchingName;
230 },
231 toString: function() {
232 return '[ jsdom HTMLCollection ]: contains ' + this.length + ' items';
233 }
234 };
235 core.HTMLCollection.prototype.__proto__ = core.NodeList.prototype;
236
237 core.HTMLOptionsCollection = core.HTMLCollection;
238
239 function closest(e, tagName) {
240 tagName = tagName.toUpperCase();
241 while (e) {
242 if (e.nodeName.toUpperCase() === tagName ||
243 (e.tagName && e.tagName.toUpperCase() === tagName))
244 {
245 return e;
246 }
247 e = e._parentNode;
248 }
249 return null;
250 }
251
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';
256 }, recursive));
257 }
258
259 function firstChild(e, tagName) {
260 if (!e) {
261 return null;
262 }
263 var c = descendants(e, tagName, false);
264 return c.length > 0 ? c[0] : null;
265 }
266
267 function ResourceQueue(paused) {
268 this.paused = !!paused;
269 }
270 ResourceQueue.prototype = {
271 push: function(callback) {
272 var q = this;
273 var item = {
274 prev: q.tail,
275 check: function() {
276 if (!q.paused && !this.prev && this.fired){
277 callback(this.err, this.data);
278 if (this.next) {
279 this.next.prev = null;
280 this.next.check();
281 }else{//q.tail===this
282 q.tail = null;
283 }
284 }
285 }
286 };
287 if (q.tail) {
288 q.tail.next = item;
289 }
290 q.tail = item;
291 return function(err, data) {
292 item.fired = 1;
293 item.err = err;
294 item.data = data;
295 item.check();
296 };
297 },
298 resume: function() {
299 if(!this.paused){
300 return;
301 }
302 this.paused = false;
303 var head = this.tail;
304 while(head && head.prev){
305 head = head.prev;
306 }
307 if(head){
308 head.check();
309 }
310 }
311 };
312
313 core.HTMLDocument = function HTMLDocument(options) {
314 options = options || {};
315 if (!options.contentType) {
316 options.contentType = 'text/html';
317 }
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';
325
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');
331 };
332
333 core.HTMLDocument.prototype = {
334 _referrer : "",
335 get referrer() {
336 return this._referrer || '';
337 },
338 get domain() {
339 return "";
340 },
341 _URL : "",
342 get URL() {
343 return this._URL;
344 },
345 get images() {
346 return this.getElementsByTagName('IMG');
347 },
348 get applets() {
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") {
353 return true;
354 } else if (upper === "OBJECT" &&
355 el.getElementsByTagName('APPLET').length > 0)
356 {
357 return true;
358 }
359 }
360 }));
361 },
362 get links() {
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)) {
367 return true;
368 }
369 }
370 }));
371 },
372 get forms() {
373 return this.getElementsByTagName('FORM');
374 },
375 get anchors() {
376 return this.getElementsByTagName('A');
377 },
378 open : function() {
379 this._childNodes = [];
380 this._documentElement = null;
381 this._modified();
382 },
383 close : function() {
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);
392 })(null, true);
393 },
394
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.
399 var node = this;
400 while (node.lastChild && node.lastChild.nodeType === this.ELEMENT_NODE) {
401 node = node.lastChild;
402 }
403 node.innerHTML = text;
404 } else {
405 this.innerHTML = text;
406 }
407 },
408
409 writeln : function(text) {
410 this.write(text + '\n');
411 },
412
413 getElementsByName : function(elementName) {
414 return new core.HTMLCollection(this, core.mapper(this, function(el) {
415 return (el.getAttribute && el.getAttribute("name") === elementName);
416 }));
417 },
418
419 get title() {
420 var head = this.head,
421 title = head ? firstChild(head, 'TITLE') : null;
422 return title ? title.textContent : '';
423 },
424
425 set title(val) {
426 var title = firstChild(this.head, 'TITLE');
427 if (!title) {
428 title = this.createElement('TITLE');
429 var head = this.head;
430 if (!head) {
431 head = this.createElement('HEAD');
432 this.documentElement.insertBefore(head, this.documentElement.firstChild);
433 }
434 head.appendChild(title);
435 }
436 title.textContent = val;
437 },
438
439 get head() {
440 return firstChild(this.documentElement, 'HEAD');
441 },
442
443 set head() { /* noop */ },
444
445 get body() {
446 var body = firstChild(this.documentElement, 'BODY');
447 if (!body) {
448 body = firstChild(this.documentElement, 'FRAMESET');
449 }
450 return body;
451 },
452
453 get documentElement() {
454 if (!this._documentElement) {
455 this._documentElement = firstChild(this, 'HTML');
456 }
457 return this._documentElement;
458 },
459
460 _cookie : "",
461 get cookie() { return this._cookie || ''; },
462 set cookie(val) { this._cookie = val; }
463 };
464 core.HTMLDocument.prototype.__proto__ = core.Document.prototype;
465
466 define('HTMLElement', {
467 parentClass: core.Element,
468 proto : {
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
472 // preventDefault).
473 dispatchEvent : function (event) {
474 var outcome = core.Node.prototype.dispatchEvent.call(this, event)
475
476 if (!event._preventDefault &&
477 event.target._eventDefaults[event.type] &&
478 typeof event.target._eventDefaults[event.type] === 'function')
479 {
480 event.target._eventDefaults[event.type](event)
481 }
482 return outcome;
483 },
484 _eventDefaults : {}
485 },
486 attributes: [
487 'id',
488 'title',
489 'lang',
490 'dir',
491 {prop: 'className', attr: 'class', normalize: function(s) { return s || ''; }}
492 ]
493 });
494
495 core.Document.prototype._defaultElementBuilder = function(document, tagName) {
496 return new core.HTMLElement(document, tagName);
497 };
498
499 //http://www.w3.org/TR/html5/forms.html#category-listed
500 var listedElements = /button|fieldset|input|keygen|object|select|textarea/i;
501
502 define('HTMLFormElement', {
503 tagName: 'FORM',
504 proto: {
505 get elements() {
506 return new core.HTMLCollection(this._ownerDocument, core.mapper(this, function(e) {
507 return listedElements.test(e.nodeName) ; // TODO exclude <input type="image">
508 }));
509 },
510 get length() {
511 return this.elements.length;
512 },
513 _dispatchSubmitEvent: function() {
514 var ev = this._ownerDocument.createEvent('HTMLEvents');
515 ev.initEvent('submit', true, true);
516 if (!this.dispatchEvent(ev)) {
517 this.submit();
518 };
519 },
520 submit: function() {
521 },
522 reset: function() {
523 this.elements.toArray().forEach(function(el) {
524 el.value = el.defaultValue;
525 });
526 }
527 },
528 attributes: [
529 'name',
530 {prop: 'acceptCharset', attr: 'accept-charset'},
531 'action',
532 'enctype',
533 'method',
534 'target'
535 ]
536 });
537
538 define('HTMLLinkElement', {
539 tagName: 'LINK',
540 proto: {
541 get href() {
542 return core.resourceLoader.resolve(this._ownerDocument, this.getAttribute('href'));
543 }
544 },
545 attributes: [
546 {prop: 'disabled', type: 'boolean'},
547 'charset',
548 'href',
549 'hreflang',
550 'media',
551 'rel',
552 'rev',
553 'target',
554 'type'
555 ]
556 });
557
558 define('HTMLMetaElement', {
559 tagName: 'META',
560 attributes: [
561 'content',
562 {prop: 'httpEquiv', attr: 'http-equiv'},
563 'name',
564 'scheme'
565 ]
566 });
567
568 define('HTMLHtmlElement', {
569 tagName: 'HTML',
570 attributes: [
571 'version'
572 ]
573 });
574
575 define('HTMLHeadElement', {
576 tagName: 'HEAD',
577 attributes: [
578 'profile'
579 ]
580 });
581
582 define('HTMLTitleElement', {
583 tagName: 'TITLE',
584 proto: {
585 get text() {
586 return this.innerHTML;
587 },
588 set text(s) {
589 this.innerHTML = s;
590 }
591 }
592 });
593
594 define('HTMLBaseElement', {
595 tagName: 'BASE',
596 attributes: [
597 'href',
598 'target'
599 ]
600 });
601
602
603 //**Deprecated**
604 define('HTMLIsIndexElement', {
605 tagName : 'ISINDEX',
606 parentClass : core.Element,
607 proto : {
608 get form() {
609 return closest(this, 'FORM');
610 }
611 },
612 attributes : [
613 'prompt'
614 ]
615 });
616
617
618 define('HTMLStyleElement', {
619 tagName: 'STYLE',
620 attributes: [
621 {prop: 'disabled', type: 'boolean'},
622 'media',
623 'type',
624 ]
625 });
626
627 define('HTMLBodyElement', {
628 proto: (function() {
629 var proto = {};
630 // The body element's "traditional" event handlers are proxied to the
631 // window object.
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;
639 });
640 proto.__defineGetter__(name, function () {
641 return this._ownerDocument.parentWindow[name];
642 });
643 });
644 return proto;
645 })(),
646 tagName: 'BODY',
647 attributes: [
648 'aLink',
649 'background',
650 'bgColor',
651 'link',
652 'text',
653 'vLink'
654 ]
655 });
656
657 define('HTMLSelectElement', {
658 tagName: 'SELECT',
659 proto: {
660 get options() {
661 return new core.HTMLOptionsCollection(this, core.mapper(this, function(n) {
662 return n.nodeName === 'OPTION';
663 }));
664 },
665
666 get length() {
667 return this.options.length;
668 },
669
670 get selectedIndex() {
671 return this.options.toArray().reduceRight(function(prev, option, i) {
672 return option.selected ? i : prev;
673 }, -1);
674 },
675
676 set selectedIndex(index) {
677 this.options.toArray().forEach(function(option, i) {
678 option.selected = i === index;
679 });
680 },
681
682 get value() {
683 var i = this.selectedIndex;
684 if (this.options.length && (i === -1)) {
685 i = 0;
686 }
687 if (i === -1) {
688 return '';
689 }
690 return this.options[i].value;
691 },
692
693 set value(val) {
694 var self = this;
695 this.options.toArray().forEach(function(option) {
696 if (option.value === val) {
697 option.selected = true;
698 } else {
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;
703 }
704 }
705 });
706 },
707
708 get form() {
709 return closest(this, 'FORM');
710 },
711
712 get type() {
713 return this.multiple ? 'select-multiple' : 'select-one';
714 },
715
716 add: function(opt, before) {
717 if (before) {
718 this.insertBefore(opt, before);
719 }
720 else {
721 this.appendChild(opt);
722 }
723 },
724
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);
730 }
731 },
732
733 blur: function() {
734 //TODO
735 },
736
737 focus: function() {
738 //TODO
739 }
740 },
741 attributes: [
742 {prop: 'disabled', type: 'boolean'},
743 {prop: 'multiple', type: 'boolean'},
744 'name',
745 {prop: 'size', type: 'long'},
746 {prop: 'tabIndex', type: 'long'},
747 ]
748 });
749
750 define('HTMLOptGroupElement', {
751 tagName: 'OPTGROUP',
752 attributes: [
753 {prop: 'disabled', type: 'boolean'},
754 'label'
755 ]
756 });
757
758 define('HTMLOptionElement', {
759 tagName: 'OPTION',
760 proto: {
761 _attrModified: function(name, value) {
762 if (name === 'selected') {
763 this.selected = this.defaultSelected;
764 }
765 core.HTMLElement.prototype._attrModified.call(this, arguments);
766 },
767 get form() {
768 return closest(this, 'FORM');
769 },
770 get defaultSelected() {
771 return !!this.getAttribute('selected');
772 },
773 set defaultSelected(s) {
774 if (s) this.setAttribute('selected', 'selected');
775 else this.removeAttribute('selected');
776 },
777 get text() {
778 return (this.hasAttribute('value')) ? this.getAttribute('value') : this.innerHTML;
779 },
780 get value() {
781 return (this.hasAttribute('value')) ? this.getAttribute('value') : this.innerHTML;
782 },
783 set value(val) {
784 this.setAttribute('value', val);
785 },
786 get index() {
787 return closest(this, 'SELECT').options.toArray().indexOf(this);
788 },
789 get selected() {
790 if (this._selected === undefined) {
791 this._selected = this.defaultSelected;
792 }
793 return this._selected;
794 },
795 set selected(s) {
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;
799 if (s) {
800 //Remove the selected bit from all other options in this select
801 var select = this._parentNode;
802 if (!select) return;
803 if (select.nodeName !== 'SELECT') {
804 select = select._parentNode;
805 if (!select) return;
806 if (select.nodeName !== 'SELECT') return;
807 }
808 if (!select.multiple) {
809 var o = select.options;
810 for (var i = 0; i < o.length; i++) {
811 if (o[i] !== this) {
812 o[i].selected = false;
813 }
814 }
815 }
816 }
817 }
818 },
819 attributes: [
820 {prop: 'disabled', type: 'boolean'},
821 'label'
822 ]
823 });
824
825 define('HTMLInputElement', {
826 tagName: 'INPUT',
827 proto: {
828 _initDefaultValue: function() {
829 if (this._defaultValue === undefined) {
830 var attr = this.getAttributeNode('value');
831 this._defaultValue = attr ? attr.value : null;
832 }
833 return this._defaultValue;
834 },
835 _initDefaultChecked: function() {
836 if (this._defaultChecked === undefined) {
837 this._defaultChecked = !!this.getAttribute('checked');
838 }
839 return this._defaultChecked;
840 },
841 get form() {
842 return closest(this, 'FORM');
843 },
844 get defaultValue() {
845 return this._initDefaultValue();
846 },
847 get defaultChecked() {
848 return this._initDefaultChecked();
849 },
850 get checked() {
851 return !!this.getAttribute('checked');
852 },
853 set checked(checked) {
854 this._initDefaultChecked();
855 this.setAttribute('checked', checked);
856 },
857 get value() {
858 return this.getAttribute('value');
859 },
860 set value(val) {
861 this._initDefaultValue();
862 if (val === null) {
863 this.removeAttribute('value');
864 }
865 else {
866 this.setAttribute('value', val);
867 }
868 },
869 blur: function() {
870 },
871 focus: function() {
872 },
873 select: function() {
874 },
875 click: function() {
876 if (this.type === 'checkbox' || this.type === 'radio') {
877 this.checked = !this.checked;
878 }
879 else if (this.type === 'submit') {
880 var form = this.form;
881 if (form) {
882 form._dispatchSubmitEvent();
883 }
884 }
885 }
886 },
887 attributes: [
888 'accept',
889 'accessKey',
890 'align',
891 'alt',
892 {prop: 'disabled', type: 'boolean'},
893 {prop: 'maxLength', type: 'long'},
894 'name',
895 {prop: 'readOnly', type: 'boolean'},
896 {prop: 'size', type: 'long'},
897 'src',
898 {prop: 'tabIndex', type: 'long'},
899 {prop: 'type', normalize: function(val) {
900 return val ? val.toLowerCase() : 'text';
901 }},
902 'useMap'
903 ]
904 });
905
906 define('HTMLTextAreaElement', {
907 tagName: 'TEXTAREA',
908 proto: {
909 _initDefaultValue: function() {
910 if (this._defaultValue === undefined) {
911 this._defaultValue = this.textContent;
912 }
913 return this._defaultValue;
914 },
915 get form() {
916 return closest(this, 'FORM');
917 },
918 get defaultValue() {
919 return this._initDefaultValue();
920 },
921 get value() {
922 return this.textContent;
923 },
924 set value(val) {
925 this._initDefaultValue();
926 this.textContent = val;
927 },
928 get type() {
929 return 'textarea';
930 },
931 blur: function() {
932 },
933 focus: function() {
934 },
935 select: function() {
936 }
937 },
938 attributes: [
939 'accessKey',
940 {prop: 'cols', type: 'long'},
941 {prop: 'disabled', type: 'boolean'},
942 {prop: 'maxLength', type: 'long'},
943 'name',
944 {prop: 'readOnly', type: 'boolean'},
945 {prop: 'rows', type: 'long'},
946 {prop: 'tabIndex', type: 'long'}
947 ]
948 });
949
950 define('HTMLButtonElement', {
951 tagName: 'BUTTON',
952 proto: {
953 get form() {
954 return closest(this, 'FORM');
955 }
956 },
957 attributes: [
958 'accessKey',
959 {prop: 'disabled', type: 'boolean'},
960 'name',
961 {prop: 'tabIndex', type: 'long'},
962 'type',
963 'value'
964 ]
965 });
966
967 define('HTMLLabelElement', {
968 tagName: 'LABEL',
969 proto: {
970 get form() {
971 return closest(this, 'FORM');
972 }
973 },
974 attributes: [
975 'accessKey',
976 {prop: 'htmlFor', attr: 'for'}
977 ]
978 });
979
980 define('HTMLFieldSetElement', {
981 tagName: 'FIELDSET',
982 proto: {
983 get form() {
984 return closest(this, 'FORM');
985 }
986 }
987 });
988
989 define('HTMLLegendElement', {
990 tagName: 'LEGEND',
991 proto: {
992 get form() {
993 return closest(this, 'FORM');
994 }
995 },
996 attributes: [
997 'accessKey',
998 'align'
999 ]
1000 });
1001
1002 define('HTMLUListElement', {
1003 tagName: 'UL',
1004 attributes: [
1005 {prop: 'compact', type: 'boolean'},
1006 'type'
1007 ]
1008 });
1009
1010 define('HTMLOListElement', {
1011 tagName: 'OL',
1012 attributes: [
1013 {prop: 'compact', type: 'boolean'},
1014 {prop: 'start', type: 'long'},
1015 'type'
1016 ]
1017 });
1018
1019 define('HTMLDListElement', {
1020 tagName: 'DL',
1021 attributes: [
1022 {prop: 'compact', type: 'boolean'}
1023 ]
1024 });
1025
1026 define('HTMLDirectoryElement', {
1027 tagName: 'DIR',
1028 attributes: [
1029 {prop: 'compact', type: 'boolean'}
1030 ]
1031 });
1032
1033 define('HTMLMenuElement', {
1034 tagName: 'MENU',
1035 attributes: [
1036 {prop: 'compact', type: 'boolean'}
1037 ]
1038 });
1039
1040 define('HTMLLIElement', {
1041 tagName: 'LI',
1042 attributes: [
1043 'type',
1044 {prop: 'value', type: 'long'}
1045 ]
1046 });
1047
1048 define('HTMLDivElement', {
1049 tagName: 'DIV',
1050 attributes: [
1051 'align'
1052 ]
1053 });
1054
1055 define('HTMLParagraphElement', {
1056 tagName: 'P',
1057 attributes: [
1058 'align'
1059 ]
1060 });
1061
1062 define('HTMLHeadingElement', {
1063 tagNames: ['H1','H2','H3','H4','H5','H6'],
1064 attributes: [
1065 'align'
1066 ]
1067 });
1068
1069 define('HTMLQuoteElement', {
1070 tagNames: ['Q','BLOCKQUOTE'],
1071 attributes: [
1072 'cite'
1073 ]
1074 });
1075
1076 define('HTMLPreElement', {
1077 tagName: 'PRE',
1078 attributes: [
1079 {prop: 'width', type: 'long'}
1080 ]
1081 });
1082
1083 define('HTMLBRElement', {
1084 tagName: 'BR',
1085 attributes: [
1086 'clear'
1087 ]
1088 });
1089
1090 define('HTMLBaseFontElement', {
1091 tagName: 'BASEFONT',
1092 attributes: [
1093 'color',
1094 'face',
1095 {prop: 'size', type: 'long'}
1096 ]
1097 });
1098
1099 define('HTMLFontElement', {
1100 tagName: 'FONT',
1101 attributes: [
1102 'color',
1103 'face',
1104 'size'
1105 ]
1106 });
1107
1108 define('HTMLHRElement', {
1109 tagName: 'HR',
1110 attributes: [
1111 'align',
1112 {prop: 'noShade', type: 'boolean'},
1113 'size',
1114 'width'
1115 ]
1116 });
1117
1118 define('HTMLModElement', {
1119 tagNames: ['INS', 'DEL'],
1120 attributes: [
1121 'cite',
1122 'dateTime'
1123 ]
1124 });
1125
1126 define('HTMLAnchorElement', {
1127 tagName: 'A',
1128
1129 proto: {
1130 blur: function() {
1131 },
1132 focus: function() {
1133 },
1134 get href() {
1135 return core.resourceLoader.resolve(this._ownerDocument, this.getAttribute('href'));
1136 },
1137 get hostname() {
1138 return URL.parse(this.href).hostname;
1139 },
1140 get pathname() {
1141 return URL.parse(this.href).pathname;
1142 }
1143 },
1144 attributes: [
1145 'accessKey',
1146 'charset',
1147 'coords',
1148 {prop: 'href', type: 'string', read: false},
1149 'hreflang',
1150 'name',
1151 'rel',
1152 'rev',
1153 'shape',
1154 {prop: 'tabIndex', type: 'long'},
1155 'target',
1156 'type'
1157 ]
1158 });
1159
1160 define('HTMLImageElement', {
1161 tagName: 'IMG',
1162 attributes: [
1163 'name',
1164 'align',
1165 'alt',
1166 'border',
1167 {prop: 'height', type: 'long'},
1168 {prop: 'hspace', type: 'long'},
1169 {prop: 'isMap', type: 'boolean'},
1170 'longDesc',
1171 'src',
1172 'useMap',
1173 {prop: 'vspace', type: 'long'},
1174 {prop: 'width', type: 'long'}
1175 ]
1176 });
1177
1178 define('HTMLObjectElement', {
1179 tagName: 'OBJECT',
1180 proto: {
1181 get form() {
1182 return closest(this, 'FORM');
1183 },
1184 get contentDocument() {
1185 return null;
1186 }
1187 },
1188 attributes: [
1189 'code',
1190 'align',
1191 'archive',
1192 'border',
1193 'codeBase',
1194 'codeType',
1195 'data',
1196 {prop: 'declare', type: 'boolean'},
1197 {prop: 'height', type: 'long'},
1198 {prop: 'hspace', type: 'long'},
1199 'name',
1200 'standby',
1201 {prop: 'tabIndex', type: 'long'},
1202 'type',
1203 'useMap',
1204 {prop: 'vspace', type: 'long'},
1205 {prop: 'width', type: 'long'}
1206 ]
1207 });
1208
1209 define('HTMLParamElement', {
1210 tagName: 'PARAM',
1211 attributes: [
1212 'name',
1213 'type',
1214 'value',
1215 'valueType'
1216 ]
1217 });
1218
1219 define('HTMLAppletElement', {
1220 tagName: 'APPLET',
1221 attributes: [
1222 'align',
1223 'alt',
1224 'archive',
1225 'code',
1226 'codeBase',
1227 'height',
1228 {prop: 'hspace', type: 'long'},
1229 'name',
1230 'object',
1231 {prop: 'vspace', type: 'long'},
1232 'width'
1233 ]
1234 });
1235
1236 define('HTMLMapElement', {
1237 tagName: 'MAP',
1238 proto: {
1239 get areas() {
1240 return this.getElementsByTagName("AREA");
1241 }
1242 },
1243 attributes: [
1244 'name'
1245 ]
1246 });
1247
1248 define('HTMLAreaElement', {
1249 tagName: 'AREA',
1250 attributes: [
1251 'accessKey',
1252 'alt',
1253 'coords',
1254 'href',
1255 {prop: 'noHref', type: 'boolean'},
1256 'shape',
1257 {prop: 'tabIndex', type: 'long'},
1258 'target'
1259 ]
1260 });
1261
1262 define('HTMLScriptElement', {
1263 tagName: 'SCRIPT',
1264 init: function() {
1265 this.addEventListener('DOMNodeInsertedIntoDocument', function() {
1266 if (this.src) {
1267 core.resourceLoader.load(this, this.src, this._eval);
1268 }
1269 else {
1270 var src = this.sourceLocation || {},
1271 filename = src.file || this._ownerDocument.URL;
1272
1273 if (src) {
1274 filename += ':' + src.line + ':' + src.col;
1275 }
1276 filename += '<script>';
1277
1278 core.resourceLoader.enqueue(this, this._eval, filename)(null, this.text);
1279 }
1280 });
1281 },
1282 proto: {
1283 _eval: function(text, filename) {
1284 if (this._ownerDocument.implementation.hasFeature("ProcessExternalResources", "script") &&
1285 this.language &&
1286 core.languageProcessors[this.language])
1287 {
1288 core.languageProcessors[this.language](this, text, filename);
1289 }
1290 },
1291 get language() {
1292 var type = this.type || "text/javascript";
1293 return type.split("/").pop().toLowerCase();
1294 },
1295 get text() {
1296 var i=0, children = this.childNodes, l = children.length, ret = [];
1297
1298 for (i; i<l; i++) {
1299 ret.push(children.item(i).value);
1300 }
1301
1302 return ret.join("");
1303 },
1304 set text(text) {
1305 while (this.childNodes.length) {
1306 this.removeChild(this.childNodes[0]);
1307 }
1308 this.appendChild(this._ownerDocument.createTextNode(text));
1309 }
1310 },
1311 attributes : [
1312 {prop: 'defer', 'type': 'boolean'},
1313 'htmlFor',
1314 'event',
1315 'charset',
1316 'type',
1317 'src'
1318 ]
1319 })
1320
1321 define('HTMLTableElement', {
1322 tagName: 'TABLE',
1323 proto: {
1324 get caption() {
1325 return firstChild(this, 'CAPTION');
1326 },
1327 get tHead() {
1328 return firstChild(this, 'THEAD');
1329 },
1330 get tFoot() {
1331 return firstChild(this, 'TFOOT');
1332 },
1333 get rows() {
1334 if (!this._rows) {
1335 var table = this;
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 });
1338
1339 if (sections.length === 0) {
1340 return core.mapDOMNodes(table, false, function(el) {
1341 return el.tagName === 'TR';
1342 });
1343 }
1344
1345 return sections.reduce(function(prev, s) {
1346 return prev.concat(s.rows.toArray());
1347 }, []);
1348
1349 });
1350 }
1351 return this._rows;
1352 },
1353 get tBodies() {
1354 if (!this._tBodies) {
1355 this._tBodies = descendants(this, 'TBODY', false);
1356 }
1357 return this._tBodies;
1358 },
1359 createTHead: function() {
1360 var el = this.tHead;
1361 if (!el) {
1362 el = this._ownerDocument.createElement('THEAD');
1363 this.appendChild(el);
1364 }
1365 return el;
1366 },
1367 deleteTHead: function() {
1368 var el = this.tHead;
1369 if (el) {
1370 el._parentNode.removeChild(el);
1371 }
1372 },
1373 createTFoot: function() {
1374 var el = this.tFoot;
1375 if (!el) {
1376 el = this._ownerDocument.createElement('TFOOT');
1377 this.appendChild(el);
1378 }
1379 return el;
1380 },
1381 deleteTFoot: function() {
1382 var el = this.tFoot;
1383 if (el) {
1384 el._parentNode.removeChild(el);
1385 }
1386 },
1387 createCaption: function() {
1388 var el = this.caption;
1389 if (!el) {
1390 el = this._ownerDocument.createElement('CAPTION');
1391 this.appendChild(el);
1392 }
1393 return el;
1394 },
1395 deleteCaption: function() {
1396 var c = this.caption;
1397 if (c) {
1398 c._parentNode.removeChild(c);
1399 }
1400 },
1401 insertRow: function(index) {
1402 var tr = this._ownerDocument.createElement('TR');
1403 if (this.childNodes.length === 0) {
1404 this.appendChild(this._ownerDocument.createElement('TBODY'));
1405 }
1406 var rows = this.rows.toArray();
1407 if (index < -1 || index > rows.length) {
1408 throw new core.DOMException(core.INDEX_SIZE_ERR);
1409 }
1410 if (index === -1 || (index === 0 && rows.length === 0)) {
1411 this.tBodies.item(0).appendChild(tr);
1412 }
1413 else if (index === rows.length) {
1414 var ref = rows[index-1];
1415 ref._parentNode.appendChild(tr);
1416 }
1417 else {
1418 var ref = rows[index];
1419 ref._parentNode.insertBefore(tr, ref);
1420 }
1421 return tr;
1422 },
1423 deleteRow: function(index) {
1424 var rows = this.rows.toArray(), l = rows.length;
1425 if (index === -1) {
1426 index = l-1;
1427 }
1428 if (index < 0 || index >= l) {
1429 throw new core.DOMException(core.INDEX_SIZE_ERR);
1430 }
1431 var tr = rows[index];
1432 tr._parentNode.removeChild(tr);
1433 }
1434 },
1435 attributes: [
1436 'align',
1437 'bgColor',
1438 'border',
1439 'cellPadding',
1440 'cellSpacing',
1441 'frame',
1442 'rules',
1443 'summary',
1444 'width'
1445 ]
1446 });
1447
1448 define('HTMLTableCaptionElement', {
1449 tagName: 'CAPTION',
1450 attributes: [
1451 'align'
1452 ]
1453 });
1454
1455 define('HTMLTableColElement', {
1456 tagNames: ['COL','COLGROUP'],
1457 attributes: [
1458 'align',
1459 {prop: 'ch', attr: 'char'},
1460 {prop: 'chOff', attr: 'charoff'},
1461 {prop: 'span', type: 'long'},
1462 'vAlign',
1463 'width',
1464 ]
1465 });
1466
1467 define('HTMLTableSectionElement', {
1468 tagNames: ['THEAD','TBODY','TFOOT'],
1469 proto: {
1470 get rows() {
1471 if (!this._rows) {
1472 this._rows = descendants(this, 'TR', false);
1473 }
1474 return this._rows;
1475 },
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);
1481 }
1482 if (index === -1 || index === rows.length) {
1483 this.appendChild(tr);
1484 }
1485 else {
1486 var ref = rows[index];
1487 this.insertBefore(tr, ref);
1488 }
1489 return tr;
1490 },
1491 deleteRow: function(index) {
1492 var rows = this.rows.toArray();
1493 if (index === -1) {
1494 index = rows.length-1;
1495 }
1496 if (index < 0 || index >= rows.length) {
1497 throw new core.DOMException(core.INDEX_SIZE_ERR);
1498 }
1499 var tr = this.rows[index];
1500 this.removeChild(tr);
1501 }
1502 },
1503 attributes: [
1504 'align',
1505 {prop: 'ch', attr: 'char'},
1506 {prop: 'chOff', attr: 'charoff'},
1507 {prop: 'span', type: 'long'},
1508 'vAlign',
1509 'width',
1510 ]
1511 });
1512
1513 define('HTMLTableRowElement', {
1514 tagName: 'TR',
1515 proto: {
1516 get cells() {
1517 if (!this._cells) {
1518 this._cells = new core.HTMLCollection(this, core.mapper(this, function(n) {
1519 return n.nodeName === 'TD' || n.nodeName === 'TH';
1520 }, false));
1521 }
1522 return this._cells;
1523 },
1524 get rowIndex() {
1525 return closest(this, 'TABLE').rows.toArray().indexOf(this);
1526 },
1527
1528 get sectionRowIndex() {
1529 return this._parentNode.rows.toArray().indexOf(this);
1530 },
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);
1536 }
1537 if (index === -1 || index === cells.length) {
1538 this.appendChild(td);
1539 }
1540 else {
1541 var ref = cells[index];
1542 this.insertBefore(td, ref);
1543 }
1544 return td;
1545 },
1546 deleteCell: function(index) {
1547 var cells = this.cells.toArray();
1548 if (index === -1) {
1549 index = cells.length-1;
1550 }
1551 if (index < 0 || index >= cells.length) {
1552 throw new core.DOMException(core.INDEX_SIZE_ERR);
1553 }
1554 var td = this.cells[index];
1555 this.removeChild(td);
1556 }
1557 },
1558 attributes: [
1559 'align',
1560 'bgColor',
1561 {prop: 'ch', attr: 'char'},
1562 {prop: 'chOff', attr: 'charoff'},
1563 'vAlign'
1564 ]
1565 });
1566
1567 define('HTMLTableCellElement', {
1568 tagNames: ['TH','TD'],
1569 proto: {
1570 _headers: null,
1571 set headers(h) {
1572 if (h === '') {
1573 //Handle resetting headers so the dynamic getter returns a query
1574 this._headers = null;
1575 return;
1576 }
1577 if (!(h instanceof Array)) {
1578 h = [h];
1579 }
1580 this._headers = h;
1581 },
1582 get headers() {
1583 if (this._headers) {
1584 return this._headers.join(' ');
1585 }
1586 var cellIndex = this.cellIndex,
1587 headings = [],
1588 siblings = this._parentNode.getElementsByTagName(this.tagName);
1589
1590 for (var i=0; i<siblings.length; i++) {
1591 if (siblings.item(i).cellIndex >= cellIndex) {
1592 break;
1593 }
1594 headings.push(siblings.item(i).id);
1595 }
1596 this._headers = headings;
1597 return headings.join(' ');
1598 },
1599 get cellIndex() {
1600 return closest(this, 'TR').cells.toArray().indexOf(this);
1601 }
1602 },
1603 attributes: [
1604 'abbr',
1605 'align',
1606 'axis',
1607 'bgColor',
1608 {prop: 'ch', attr: 'char'},
1609 {prop: 'chOff', attr: 'charoff'},
1610 {prop: 'colSpan', type: 'long'},
1611 'height',
1612 {prop: 'noWrap', type: 'boolean'},
1613 {prop: 'rowSpan', type: 'long'},
1614 'scope',
1615 'vAlign',
1616 'width'
1617 ]
1618 });
1619
1620 define('HTMLFrameSetElement', {
1621 tagName: 'FRAMESET',
1622 attributes: [
1623 'cols',
1624 'rows'
1625 ]
1626 });
1627
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();
1636 } else {
1637 delete frame._contentDocument;
1638 }
1639 }
1640
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({
1645 url: url,
1646 documentRoot: Path.dirname(url)
1647 });
1648 applyDocumentFeatures(contentDoc, parentDoc.implementation._features);
1649
1650 var parent = parentDoc.parentWindow;
1651 var contentWindow = contentDoc.parentWindow;
1652 contentWindow.parent = parent;
1653 contentWindow.top = parent.top;
1654
1655 core.resourceLoader.load(frame, url, function(html, filename) {
1656 contentDoc.write(html);
1657 contentDoc.close();
1658 });
1659 }
1660
1661 define('HTMLFrameElement', {
1662 tagName: 'FRAME',
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
1666 // on the window.
1667 var parent = this._ownerDocument.parentWindow;
1668 var frameID = parent._length++;
1669 var self = this;
1670 parent.__defineGetter__(frameID, function () {
1671 return self.contentWindow;
1672 });
1673
1674 // The contentDocument/contentWindow shouldn't be created until the frame
1675 // is inserted:
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;
1688 }, false);
1689 },
1690 proto: {
1691 setAttribute: function(name, value) {
1692 core.HTMLElement.prototype.setAttribute.call(this, name, value);
1693 var self = this;
1694 if (name === 'name') {
1695 // Set up named frame access.
1696 this._ownerDocument.parentWindow.__defineGetter__(value, function () {
1697 return self.contentWindow;
1698 });
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)
1707
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
1711 // callbacks.
1712 this.addEventListener('DOMNodeInsertedIntoDocument', function loader () {
1713 self.removeEventListener('DOMNodeInsertedIntoDocument', loader, false);
1714 this._waitingOnInsert = false;
1715 loadFrame(self);
1716 }, false);
1717 this._waitingOnInsert = true;
1718 }
1719 } else {
1720 loadFrame(self);
1721 }
1722 }
1723 },
1724 _contentDocument : null,
1725 get contentDocument() {
1726 if (this._contentDocument == null) {
1727 this._contentDocument = new core.HTMLDocument();
1728 }
1729 return this._contentDocument;
1730 },
1731 get contentWindow() {
1732 return this.contentDocument.parentWindow;
1733 }
1734 },
1735 attributes: [
1736 'frameBorder',
1737 'longDesc',
1738 'marginHeight',
1739 'marginWidth',
1740 'name',
1741 {prop: 'noResize', type: 'boolean'},
1742 'scrolling',
1743 {prop: 'src', type: 'string', write: false}
1744 ]
1745 });
1746
1747 define('HTMLIFrameElement', {
1748 tagName: 'IFRAME',
1749 parentClass: core.HTMLFrameElement,
1750 attributes: [
1751 'align',
1752 'frameBorder',
1753 'height',
1754 'longDesc',
1755 'marginHeight',
1756 'marginWidth',
1757 'name',
1758 'scrolling',
1759 'src',
1760 'width'
1761 ]
1762 });
1763
1764 exports.define = define;
1765 exports.dom = {
1766 level2 : {
1767 html : core
1768 }
1769 }
1770