Using call(this, arg, arg) instead of apply(this, arguments) as per cpojer's request.
[mootools.git] / Source / Element / Element.js
blob20ac5734b4fd3d60ebd25554df13d0bc85832514
1 /*
2 ---
4 name: Element
6 description: One of the most important items in MooTools. Contains the dollar function, the dollars function, and an handful of cross-browser, time-saver methods to let you easily work with HTML Elements.
8 license: MIT-style license.
10 requires: [Window, Document, Array, String, Function, Number, Slick.Parser, Slick.Finder]
12 provides: [Element, Elements, $, $$, Iframe]
14 ...
17 // it needs to be this.Element cause IE8 erases the Element Object while pre-processing this script
18 this.Element = function(tag, props){
19         var konstructor = Element.Constructors[tag];
20         if (konstructor) return konstructor(props);
21         if (typeof tag != 'string') return document.id(tag).set(props);
22         
23         if (!props) props = {};
24         
25         if (!tag.test(/^[\w-]+$/)){
26                 var parsed = Slick.parse(tag).expressions[0][0];
27                 tag = (parsed.tag == '*') ? 'div' : parsed.tag;
28                 if (parsed.id && props.id == null) props.id = parsed.id;
29                 
30                 var attributes = parsed.attributes;
31                 if (attributes) for (var i = 0, l = attributes.length; i < l; i++){
32                         var attr = attributes[i];
33                         if (attr.value != null && attr.operator == '=' && props[attr.key] == null)
34                                 props[attr.key] = attr.value;
35                 }
36                 
37                 if (parsed.classList && props['class'] == null) props['class'] = parsed.classList.join(' ');
38         }
39         
40         return document.newElement(tag, props);
42         
43 if (Browser.Element){
44         Element.prototype = Browser.Element.prototype;
47 new Type('Element', Element).mirror(function(name, method){
48         var obj = {};
49         obj[name] = function(){
50                 var results = [], args = arguments, elements = true;
51                 for (var i = 0, l = this.length; i < l; i++){
52                         var element = this[i], result = results[i] = element[name].apply(element, args);
53                         elements = (elements && typeOf(result) == 'element');
54                 }
55                 return (elements) ? new Elements(results) : results;
56         };
57         
58         Elements.implement(obj);
59 });
61 if (!Browser.Element){
62         Element.parent = Object;
64         Element.ProtoType = {'$family': Function.from('element').hide()};
66         Element.mirror(function(name, method){
67                 Element.ProtoType[name] = method;
68         });
71 Element.Constructors = {};
73 //<1.2compat>
75 Element.Constructors = new Hash;
77 //</1.2compat>
79 var IFrame = new Type('IFrame', function(){
80         var params = Array.link(arguments, {
81                 properties: Type.isObject,
82                 iframe: function(obj){
83                         return (obj != null);
84                 }
85         });
86         var props = params.properties || {};
87         var iframe = document.id(params.iframe);
88         var onload = props.onload || function(){};
89         delete props.onload;
90         props.id = props.name = [props.id, props.name, iframe ? (iframe.id || iframe.name) : 'IFrame_' + Date.now()].pick();
91         iframe = new Element(iframe || 'iframe', props);
92         var onFrameLoad = function(){
93                 var host = Function.attempt(function(){
94                         return iframe.contentWindow.location.host;
95                 });
96                 if (!host || host == window.location.host){
97                         var win = new Window(iframe.contentWindow);
98                         new Document(iframe.contentWindow.document);
99                         Object.append(win.Element.prototype, Element.ProtoType);
100                 }
101                 onload.call(iframe.contentWindow, iframe.contentWindow.document);
102         };
103         var contentWindow = Function.attempt(function(){
104                 return iframe.contentWindow;
105         });
106         ((contentWindow && contentWindow.document.body) || window.frames[props.id]) ? onFrameLoad() : iframe.addListener('load', onFrameLoad);
107         return iframe;
110 var Elements = this.Elements = function(nodes){
111         if (nodes && nodes.length){
112                 var uniques = {}, node;
113                 for (var i = 0; node = nodes[i++];){
114                         var uid = Slick.uidOf(node);
115                         if (!uniques[uid]){
116                                 uniques[uid] = true;
117                                 this.push(node);
118                         }
119                 }
120         }
123 Elements.prototype = {length: 0};
124 Elements.parent = Array;
126 new Type('Elements', Elements).implement({
128         filter: function(filter, bind){
129                 if (!filter) return this;
130                 return new Elements(Array.filter(this, (typeOf(filter) == 'string') ? function(item){
131                         return item.match(filter);
132                 } : filter, bind));
133         }.protect(),
135         push: function(){
136                 var length = this.length;
137                 for (var i = 0, l = arguments.length; i < l; i++){
138                         var item = document.id(arguments[i]);
139                         if (item) this[length++] = item;
140                 }
141                 return (this.length = length);
142         }.protect()
144 }).implement(Array.prototype);
146 Array.mirror(Elements);
148 Document.implement({
150         newElement: function(tag, props){
151                 if (props && props.checked != null) props.defaultChecked = props.checked;
152                 return this.id(this.createElement(tag)).set(props);
153         },
155         newTextNode: function(text){
156                 return this.createTextNode(text);
157         },
159         getDocument: function(){
160                 return this;
161         },
163         getWindow: function(){
164                 return this.window;
165         },
166         
167         id: (function(){
168                 
169                 var types = {
171                         string: function(id, nocash, doc){
172                                 id = Slick.find(doc, '#' + id);
173                                 return (id) ? types.element(id, nocash) : null;
174                         },
175                         
176                         element: function(el, nocash){
177                                 $uid(el);
178                                 if (!nocash && !el.$family && !(/^object|embed$/i).test(el.tagName)){
179                                         Object.append(el, Element.ProtoType);
180                                 }
181                                 return el;
182                         },
183                         
184                         object: function(obj, nocash, doc){
185                                 if (obj.toElement) return types.element(obj.toElement(doc), nocash);
186                                 return null;
187                         }
188                         
189                 };
191                 types.textnode = types.whitespace = types.window = types.document = function(zero){
192                         return zero;
193                 };
194                 
195                 return function(el, nocash, doc){
196                         if (el && el.$family && el.uid) return el;
197                         var type = typeOf(el);
198                         return (types[type]) ? types[type](el, nocash, doc || document) : null;
199                 };
201         })()
205 if (window.$ == null) Window.implement('$', function(el, nc){
206         return document.id(el, nc, this.document);
209 Window.implement({
211         getDocument: function(){
212                 return this.document;
213         },
215         getWindow: function(){
216                 return this;
217         }
221 [Document, Element].invoke('implement', {
223         getElements: function(expression){
224                 return Slick.search(this, expression, new Elements);
225         },
227         getElement: function(expression){
228                 return document.id(Slick.find(this, expression));
229         }
233 //<1.2compat>
235 (function(search, find, match){
237         this.Selectors = {};
238         var pseudos = this.Selectors.Pseudo = new Hash();
240         var addSlickPseudos = function(){
241                 for (var name in pseudos) if (pseudos.hasOwnProperty(name)){
242                         Slick.definePseudo(name, pseudos[name])
243                         delete pseudos[name];
244                 }
245         }
247         Slick.search = function(context, expression, append){
248                 addSlickPseudos();
249                 return search.call(this, context, expression, append);
250         }
252         Slick.find = function(context, expression){
253                 addSlickPseudos();
254                 return find.call(this, context, expression);
255         }
257         Slick.match = function(node, selector){
258                 addSlickPseudos();
259                 return match.call(this, node, selector);
260         }
262 })(Slick.search, Slick.find, Slick.match);
264 if (window.$$ == null) Window.implement('$$', function(selector){
265         var elements = new Elements;
266         if (arguments.length == 1 && typeof selector == 'string') return Slick.search(this.document, selector, elements);
267         var args = Array.flatten(arguments);
268         for (var i = 0, l = args.length; i < l; i++){
269                 var item = args[i];
270                 switch (typeOf(item)){
271                         case 'element': elements.push(item); break;
272                         case 'string': Slick.search(this.document, item, elements);
273                 }
274         }
275         return elements;
278 //</1.2compat>
280 if (window.$$ == null) Window.implement('$$', function(selector){
281         if (arguments.length == 1){
282                 if (typeof selector == 'string') return Slick.search(this.document, selector, new Elements);
283                 else if (Type.isEnumerable(selector)) return new Elements(selector);
284         }
285         return new Elements(arguments);
288 (function(){
290 var collected = {}, storage = {};
291 var props = {input: 'checked', option: 'selected', textarea: 'value'};
293 var get = function(uid){
294         return (storage[uid] || (storage[uid] = {}));
297 var clean = function(item){
298         if (item.removeEvents) item.removeEvents();
299         if (item.clearAttributes) item.clearAttributes();
300         var uid = item.uid;
301         if (uid != null){
302                 delete collected[uid];
303                 delete storage[uid];
304         }
305         return item;
308 var camels = ['defaultValue', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan', 'frameBorder', 'maxLength', 'readOnly',
309         'rowSpan', 'tabIndex', 'useMap'
311 var bools = ['compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked', 'disabled', 'readOnly', 'multiple', 'selected',
312         'noresize', 'defer'
314  var attributes = {
315         'html': 'innerHTML',
316         'class': 'className',
317         'for': 'htmlFor',
318         'text': (function(){
319                 var temp = document.createElement('div');
320                 return (temp.innerText == null) ? 'textContent' : 'innerText';
321         })()
323 var readOnly = ['type'];
324 var expandos = ['value', 'defaultValue'];
325 var uriAttrs = /^href|src|usemap$/i;
327 bools = bools.associate(bools);
328 camels = camels.associate(camels.map(String.toLowerCase));
329 readOnly = readOnly.associate(readOnly);
331 Object.append(attributes, expandos.associate(expandos));
333 var inserters = {
335         before: function(context, element){
336                 var parent = element.parentNode;
337                 if (parent) parent.insertBefore(context, element);
338         },
340         after: function(context, element){
341                 var parent = element.parentNode;
342                 if (parent) parent.insertBefore(context, element.nextSibling);
343         },
345         bottom: function(context, element){
346                 element.appendChild(context);
347         },
349         top: function(context, element){
350                 element.insertBefore(context, element.firstChild);
351         }
355 inserters.inside = inserters.bottom;
357 //<1.2compat>
359 Object.each(inserters, function(inserter, where){
361         where = where.capitalize();
362         
363         var methods = {};
364         
365         methods['inject' + where] = function(el){
366                 inserter(this, document.id(el, true));
367                 return this;
368         };
369         
370         methods['grab' + where] = function(el){
371                 inserter(document.id(el, true), this);
372                 return this;
373         };
375         Element.implement(methods);
379 //</1.2compat>
381 Element.implement({
383         set: function(prop, value){
384                 var property = Element.Properties[prop];
385                 (property && property.set) ? property.set.apply(this, Array.slice(arguments, 1)) : this.setProperty(prop, value);
386         }.overloadSetter(),
388         get: function(prop){
389                 var property = Element.Properties[prop];
390                 return (property && property.get) ? property.get.apply(this, Array.slice(arguments, 1)) : this.getProperty(prop);
391         }.overloadGetter(),
393         erase: function(prop){
394                 var property = Element.Properties[prop];
395                 (property && property.erase) ? property.erase.apply(this) : this.removeProperty(prop);
396                 return this;
397         },
399         setProperty: function(attribute, value){
400                 attribute = camels[attribute] || attribute;
401                 if (value == null) return this.removeProperty(attribute);
402                 var key = attributes[attribute];
403                 (key) ? this[key] = value :
404                         (bools[attribute]) ? this[attribute] = !!value : this.setAttribute(attribute, '' + value);
405                 return this;
406         },
408         setProperties: function(attributes){
409                 for (var attribute in attributes) this.setProperty(attribute, attributes[attribute]);
410                 return this;
411         },
413         getProperty: function(attribute){
414                 attribute = camels[attribute] || attribute;
415                 var key = attributes[attribute] || readOnly[attribute];
416                 return (key) ? this[key] :
417                         (bools[attribute]) ? !!this[attribute] :
418                         (uriAttrs.test(attribute) ? this.getAttribute(attribute, 2) :
419                         (key = this.getAttributeNode(attribute)) ? key.nodeValue : null) || null;
420         },
422         getProperties: function(){
423                 var args = Array.from(arguments);
424                 return args.map(this.getProperty, this).associate(args);
425         },
427         removeProperty: function(attribute){
428                 attribute = camels[attribute] || attribute;
429                 var key = attributes[attribute];
430                 (key) ? this[key] = '' :
431                         (bools[attribute]) ? this[attribute] = false : this.removeAttribute(attribute);
432                 return this;
433         },
435         removeProperties: function(){
436                 Array.each(arguments, this.removeProperty, this);
437                 return this;
438         },
440         hasClass: function(className){
441                 return this.className.contains(className, ' ');
442         },
444         addClass: function(className){
445                 if (!this.hasClass(className)) this.className = (this.className + ' ' + className).clean();
446                 return this;
447         },
449         removeClass: function(className){
450                 this.className = this.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)'), '$1');
451                 return this;
452         },
454         toggleClass: function(className, force){
455                 if (force == null) force = !this.hasClass(className);
456                 return (force) ? this.addClass(className) : this.removeClass(className);
457         },
459         adopt: function(){
460                 var parent = this, fragment, elements = Array.flatten(arguments), length = elements.length;
461                 if (length > 1) parent = fragment = document.createDocumentFragment();
462                 
463                 for (var i = 0; i < length; i++){
464                         var element = document.id(elements[i], true);
465                         if (element) parent.appendChild(element);
466                 }
467                 
468                 if (fragment) this.appendChild(fragment);
469                 
470                 return this;
471         },
473         appendText: function(text, where){
474                 return this.grab(this.getDocument().newTextNode(text), where);
475         },
477         grab: function(el, where){
478                 inserters[where || 'bottom'](document.id(el, true), this);
479                 return this;
480         },
482         inject: function(el, where){
483                 inserters[where || 'bottom'](this, document.id(el, true));
484                 return this;
485         },
487         replaces: function(el){
488                 el = document.id(el, true);
489                 el.parentNode.replaceChild(this, el);
490                 return this;
491         },
493         wraps: function(el, where){
494                 el = document.id(el, true);
495                 return this.replaces(el).grab(el, where);
496         },
498         getPrevious: function(match){
499                 return document.id(Slick.find(this, '!~ ' + (match || '')));
500         },
502         getAllPrevious: function(match){
503                 return Slick.search(this, '!~ ' + (match || ''), new Elements);
504         },
506         getNext: function(match){
507                 return document.id(Slick.find(this, '~ ' + (match || '')));
508         },
510         getAllNext: function(match){
511                 return Slick.search(this, '~ ' + (match || ''), new Elements);
512         },
514         getFirst: function(match){
515                 return document.id(Slick.find(this, '> ' + (match || '')));
516         },
518         getLast: function(match){
519                 return document.id(Slick.find(this, '!^ ' + (match || '')));
520         },
522         getParent: function(match){
523                 return document.id(Slick.find(this, '! ' + (match || '')));
524         },
526         getParents: function(match){
527                 return Slick.search(this, '! ' + (match || ''), new Elements);
528         },
529         
530         getSiblings: function(match){
531                 return Slick.search(this, '~~ ' + (match || ''), new Elements);
532         },
534         getChildren: function(match){
535                 return Slick.search(this, '> ' + (match || ''), new Elements);
536         },
538         getWindow: function(){
539                 return this.ownerDocument.window;
540         },
542         getDocument: function(){
543                 return this.ownerDocument;
544         },
546         getElementById: function(id){
547                 return document.id(Slick.find(this, '#' + id));
548         },
550         getSelected: function(){
551                 this.selectedIndex; // Safari 3.2.1
552                 return new Elements(Array.from(this.options).filter(function(option){
553                         return option.selected;
554                 }));
555         },
557         toQueryString: function(){
558                 var queryString = [];
559                 this.getElements('input, select, textarea').each(function(el){
560                         var type = el.type;
561                         if (!el.name || el.disabled || type == 'submit' || type == 'reset' || type == 'file' || type == 'image') return;
562                         
563                         var value = (el.get('tag') == 'select') ? el.getSelected().map(function(opt){
564                                 // IE
565                                 return document.id(opt).get('value');
566                         }) : ((type == 'radio' || type == 'checkbox') && !el.checked) ? null : el.get('value');
567                         
568                         Array.from(value).each(function(val){
569                                 if (typeof val != 'undefined') queryString.push(encodeURIComponent(el.name) + '=' + encodeURIComponent(val));
570                         });
571                 });
572                 return queryString.join('&');
573         },
575         clone: function(contents, keepid){
576                 contents = contents !== false;
577                 var clone = this.cloneNode(contents);
578                 var clean = function(node, element){
579                         if (!keepid) node.removeAttribute('id');
580                         if (Browser.ie){
581                                 node.clearAttributes();
582                                 node.mergeAttributes(element);
583                                 node.removeAttribute('uid');
584                                 if (node.options){
585                                         var no = node.options, eo = element.options;
586                                         for (var j = no.length; j--;) no[j].selected = eo[j].selected;
587                                 }
588                         }
589                         var prop = props[element.tagName.toLowerCase()];
590                         if (prop && element[prop]) node[prop] = element[prop];
591                 };
593                 if (contents){
594                         var ce = clone.getElementsByTagName('*'), te = this.getElementsByTagName('*');
595                         for (var i = ce.length; i--;) clean(ce[i], te[i]);
596                 }
598                 clean(clone, this);
599                 return document.id(clone);
600         },
601         
602         destroy: function(){
603                 var children = clean(this).getElementsByTagName('*');
604                 Array.each(children, clean);
605                 Element.dispose(this);
606                 return null;
607         },
608         
609         empty: function(){
610                 Array.from(this.childNodes).each(Element.dispose);
611                 return this;
612         },
614         dispose: function(){
615                 return (this.parentNode) ? this.parentNode.removeChild(this) : this;
616         },
618         match: function(expression){
619                 return !expression || Slick.match(this, expression);
620         }
624 var contains = {contains: function(element){
625         return Slick.contains(this, element);
628 if (!document.contains) Document.implement(contains);
629 if (!document.createElement('div').contains) Element.implement(contains);
631 //<1.2compat>
633 Element.implement('hasChild', function(element){
634         return this !== element && this.contains(element);
637 //</1.2compat>
639 [Element, Window, Document].invoke('implement', {
641         addListener: function(type, fn){
642                 if (type == 'unload'){
643                         var old = fn, self = this;
644                         fn = function(){
645                                 self.removeListener('unload', fn);
646                                 old();
647                         };
648                 } else {
649                         collected[this.uid] = this;
650                 }
651                 if (this.addEventListener) this.addEventListener(type, fn, false);
652                 else this.attachEvent('on' + type, fn);
653                 return this;
654         },
656         removeListener: function(type, fn){
657                 if (this.removeEventListener) this.removeEventListener(type, fn, false);
658                 else this.detachEvent('on' + type, fn);
659                 return this;
660         },
662         retrieve: function(property, dflt){
663                 var storage = get(this.uid), prop = storage[property];
664                 if (dflt != null && prop == null) prop = storage[property] = dflt;
665                 return prop != null ? prop : null;
666         },
668         store: function(property, value){
669                 var storage = get(this.uid);
670                 storage[property] = value;
671                 return this;
672         },
674         eliminate: function(property){
675                 var storage = get(this.uid);
676                 delete storage[property];
677                 return this;
678         }
682 // purge
684 window.addListener('unload', function(){
685         Object.each(collected, clean);
686         if (window.CollectGarbage) CollectGarbage();
689 })();
691 Element.Properties = {};
693 //<1.2compat>
695 Element.Properties = new Hash;
697 //</1.2compat>
699 Element.Properties.style = {
701         set: function(style){
702                 this.style.cssText = style;
703         },
705         get: function(){
706                 return this.style.cssText;
707         },
709         erase: function(){
710                 this.style.cssText = '';
711         }
715 Element.Properties.tag = {
717         get: function(){
718                 return this.tagName.toLowerCase();
719         }
723 (function(maxLength){
724         if (maxLength != null) Element.Properties.maxlength = Element.Properties.maxLength = {
725                 get: function(){
726                         var maxlength = this.getAttribute('maxLength');
727                         return maxlength == maxLength ? null : maxlength;
728                 }
729         };
730 })(document.createElement('input').getAttribute('maxLength'));
732 Element.Properties.html = (function(){
733         
734         var tableTest = Function.attempt(function(){
735                 var table = document.createElement('table');
736                 table.innerHTML = '<tr><td></td></tr>';
737         });
738         
739         var wrapper = document.createElement('div');
741         var translations = {
742                 table: [1, '<table>', '</table>'],
743                 select: [1, '<select>', '</select>'],
744                 tbody: [2, '<table><tbody>', '</tbody></table>'],
745                 tr: [3, '<table><tbody><tr>', '</tr></tbody></table>']
746         };
747         translations.thead = translations.tfoot = translations.tbody;
749         var html = {
750                 set: function(){
751                         var html = Array.flatten(arguments).join('');
752                         var wrap = (!tableTest && translations[this.get('tag')]);
753                         if (wrap){
754                                 var first = wrapper;
755                                 first.innerHTML = wrap[1] + html + wrap[2];
756                                 for (var i = wrap[0]; i--;) first = first.firstChild;
757                                 this.empty().adopt(first.childNodes);
758                         } else {
759                                 this.innerHTML = html;
760                         }
761                 }
762         };
764         html.erase = html.set;
766         return html;
767 })();