Replace $defined
[mootools.git] / Source / Element / Element.js
blob859472db8840372fd89b9df241eafd93ddb84065
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 var Element = function(tag, props){
18         var konstructor = Element.Constructors[tag];
19         if (konstructor) return konstructor(props);
20         if (typeof tag != 'string') return document.id(tag).set(props);
21         
22         if (!props) props = {};
23         
24         if (!tag.test(/^[\w-]+$/)){
25                 var parsed = Slick.parse(tag).expressions[0][0];
26                 tag = (parsed.tag == '*') ? 'div' : parsed.tag;
27                 if (parsed.id && props.id == null) props.id = parsed.id;
28                 
29                 var attributes = parsed.attributes;
30                 if (attributes) for (var i = 0, l = attributes.length; i < l; i++){
31                         var attr = attributes[i];
32                         if (attr.value != null && attr.operator == '=' && props[attr.key] == null)
33                                 props[attr.key] = attr.value;
34                 }
35                 
36                 if (parsed.classList && props['class'] == null) props['class'] = parsed.classList.join(' ');
37         }
38         
39         return document.newElement(tag, props);
41         
42 if (Browser.Element){
43         Element.prototype = Browser.Element.prototype;
46 new Type('Element', Element).mirror(function(name, method){
47         var obj = {};
48         obj[name] = function(){
49                 var results = [], args = arguments, elements = true;
50                 for (var i = 0, l = this.length; i < l; i++){
51                         var element = this[i], result = results[i] = element[name].apply(element, args);
52                         elements = (elements && typeOf(result) == 'element');
53                 }
54                 return (elements) ? new Elements(results) : results;
55         };
56         
57         Elements.implement(obj);
58 });
60 if (!Browser.Element){
61         Element.parent = Object;
62         
63         Element.ProtoType = {};
64         
65         Element.mirror(function(name, method){
66                 Element.ProtoType[name] = method;
67         });
70 Element.Constructors = {};
72 //<1.2compat>
74 Element.Constructors = new Hash;
76 //</1.2compat>
78 var IFrame = new Type('IFrame', function(){
79         var params = Array.link(arguments, {properties: Type.isObject, iframe: function(obj){ return (obj != null);}});
80         var props = params.properties || {};
81         var iframe = document.id(params.iframe);
82         var onload = props.onload || function(){};
83         delete props.onload;
84         props.id = props.name = [props.id, props.name, iframe ? (iframe.id || iframe.name) : 'IFrame_' + Date.now()].pick();
85         iframe = new Element(iframe || 'iframe', props);
86         var onFrameLoad = function(){
87                 var host = Function.attempt(function(){
88                         return iframe.contentWindow.location.host;
89                 });
90                 if (!host || host == window.location.host){
91                         var win = new Window(iframe.contentWindow);
92                         new Document(iframe.contentWindow.document);
93                         Object.append(win.Element.prototype, Element.ProtoType);
94                 }
95                 onload.call(iframe.contentWindow, iframe.contentWindow.document);
96         };
97         var contentWindow = Function.attempt(function(){
98                 return iframe.contentWindow;
99         });
100         ((contentWindow && contentWindow.document.body) || window.frames[props.id]) ? onFrameLoad() : iframe.addListener('load', onFrameLoad);
101         return iframe;
104 var Elements = this.Elements = function(nodes){
105         if (nodes && nodes.length){
106                 var uniques = {}, node;
107                 for (var i = 0; node = nodes[i++];){
108                         var uid = Slick.uidOf(node);
109                         if (!uniques[uid]){
110                                 uniques[uid] = true;
111                                 this.push(node);
112                         }
113                 }
114         }
117 Elements.prototype = {length: 0};
118 Elements.parent = Array;
120 new Type('Elements', Elements).implement({
122         filter: function(filter, bind){
123                 if (!filter) return this;
124                 return new Elements(Array.filter(this, (typeOf(filter) == 'string') ? function(item){
125                         return item.match(filter);
126                 } : filter, bind));
127         }.protect(),
129         push: function(){
130                 var length = this.length;
131                 for (var i = 0, l = arguments.length; i < l; i++){
132                         var item = document.id(arguments[i]);
133                         if (item) this[length++] = item;
134                 }
135                 return (this.length = length);
136         }.protect()
138 }).implement(Array.prototype);
140 Array.mirror(Elements);
142 Document.implement({
144         newElement: function(tag, props){
145                 if (props && props.checked != null) props.defaultChecked = props.checked;
146                 return this.id(this.createElement(tag)).set(props);
147         },
149         newTextNode: function(text){
150                 return this.createTextNode(text);
151         },
153         getDocument: function(){
154                 return this;
155         },
157         getWindow: function(){
158                 return this.window;
159         },
160         
161         id: (function(){
162                 
163                 var types = {
165                         string: function(id, nocash, doc){
166                                 id = doc.getElementById(id);
167                                 return (id) ? types.element(id, nocash) : null;
168                         },
169                         
170                         element: function(el, nocash){
171                                 $uid(el);
172                                 if (!nocash && !el.$family && !(/^object|embed$/i).test(el.tagName)){
173                                         Object.append(el, Element.ProtoType);
174                                 };
175                                 return el;
176                         },
177                         
178                         object: function(obj, nocash, doc){
179                                 if (obj.toElement) return types.element(obj.toElement(doc), nocash);
180                                 return null;
181                         }
182                         
183                 };
185                 types.textnode = types.whitespace = types.window = types.document = function(zero){
186                         return zero;
187                 };
188                 
189                 return function(el, nocash, doc){
190                         if (el && el.$family && el.uid) return el;
191                         var type = typeOf(el);
192                         return (types[type]) ? types[type](el, nocash, doc || document) : null;
193                 };
195         })()
199 if (window.$ == null) Window.implement('$', function(el, nc){
200         return document.id(el, nc, this.document);
203 Window.implement({
205         getDocument: function(){
206                 return this.document;
207         },
209         getWindow: function(){
210                 return this;
211         }
215 [Document, Element].invoke('implement', {
217         getElements: function(expression){
218                 return Slick.search(this, expression, new Elements);
219         },
221         getElement: function(expression){
222                 return document.id(Slick.find(this, expression));
223         }
227 //<1.2compat>
229 if (window.$$ == null) Window.implement('$$', function(selector){
230         var elements = new Elements;
231         if (arguments.length == 1 && typeof selector == 'string') return Slick.search(this.document, selector, elements);
232         var args = Array.flatten(arguments);
233         for (var i = 0, l = args.length; i < l; i++){
234                 var item = args[i];
235                 switch (typeOf(item)){
236                         case 'element': elements.push(item); break;
237                         case 'string': Slick.search(this.document, item, elements);
238                 }
239         }
240         return elements;
243 //</1.2compat>
245 if (window.$$ == null) Window.implement('$$', function(selector){
246         return Slick.search(this.document, selector, new Elements);
249 (function(){
251 var collected = {}, storage = {};
252 var props = {input: 'checked', option: 'selected', textarea: 'value'};
254 var get = function(uid){
255         return (storage[uid] || (storage[uid] = {}));
258 var clean = function(item){
259         if (item.removeEvents) item.removeEvents();
260         if (item.clearAttributes) item.clearAttributes();
261         var uid = item.uid;
262         if (uid != null){
263                 delete collected[uid];
264                 delete storage[uid];
265         }
266         return item;
269 var purge = function(){
270         Object.each(collected, clean);
271         if (window.CollectGarbage) CollectGarbage();
274 var camels = ['defaultValue', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan', 'frameBorder', 'maxLength', 'readOnly',
275         'rowSpan', 'tabIndex', 'useMap'
277 var bools = ['compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked', 'disabled', 'readOnly', 'multiple', 'selected',
278         'noresize', 'defer'
280  var attributes = {
281         'html': 'innerHTML',
282         'class': 'className',
283         'for': 'htmlFor',
284         'text': (function(){
285                 var temp = document.createElement('div');
286                 return (temp.innerText == null) ? 'textContent' : 'innerText';
287         })()
289 var readOnly = ['type'];
290 var expandos = ['value', 'defaultValue'];
291 var uriAttrs = /^href|src|usemap$/i;
293 bools = bools.associate(bools);
294 camels = camels.associate(camels.map(String.toLowerCase));
295 readOnly = readOnly.associate(readOnly);
297 Object.append(attributes, expandos.associate(expandos));
299 var inserters = {
301         before: function(context, element){
302                 var parent = element.parentNode;
303                 if (parent) parent.insertBefore(context, element);
304         },
306         after: function(context, element){
307                 var parent = element.parentNode;
308                 if (parent) parent.insertBefore(context, element.nextSibling);
309         },
311         bottom: function(context, element){
312                 element.appendChild(context);
313         },
315         top: function(context, element){
316                 element.insertBefore(context, element.firstChild);
317         }
321 inserters.inside = inserters.bottom;
323 //<1.2compat>
325 Object.each(inserters, function(inserter, where){
327         where = where.capitalize();
328         
329         var methods = {};
330         
331         methods['inject' + where] = function(el){
332                 inserter(this, document.id(el, true));
333                 return this;
334         };
335         
336         methods['grab' + where] = function(el){
337                 inserter(document.id(el, true), this);
338                 return this;
339         };
341         Element.implement(methods);
345 //</1.2compat>
347 Element.implement({
349         set: function(prop, value){
350                 var property = Element.Properties[prop];
351                 (property && property.set) ? property.set.apply(this, Array.slice(arguments, 1)) : this.setProperty(prop, value);
352         }.overloadSetter(),
354         get: function(prop){
355                 var property = Element.Properties[prop];
356                 return (property && property.get) ? property.get.apply(this, Array.slice(arguments, 1)) : this.getProperty(prop);
357         }.overloadGetter(),
359         erase: function(prop){
360                 var property = Element.Properties[prop];
361                 (property && property.erase) ? property.erase.apply(this) : this.removeProperty(prop);
362                 return this;
363         },
365         setProperty: function(attribute, value){
366                 attribute = camels[attribute] || attribute;
367                 if (value == null) return this.removeProperty(attribute);
368                 var key = attributes[attribute];
369                 (key) ? this[key] = value :
370                         (bools[attribute]) ? this[attribute] = !!value : this.setAttribute(attribute, '' + value);
371                 return this;
372         },
374         setProperties: function(attributes){
375                 for (var attribute in attributes) this.setProperty(attribute, attributes[attribute]);
376                 return this;
377         },
379         getProperty: function(attribute){
380                 attribute = camels[attribute] || attribute;
381                 var key = attributes[attribute] || readOnly[attribute];
382                 return (key) ? this[key] :
383                         (bools[attribute]) ? !!this[attribute] :
384                         (uriAttrs.test(attribute) ? this.getAttribute(attribute, 2) :
385                         (key = this.getAttributeNode(attribute)) ? key.nodeValue : null) || null;
386         },
388         getProperties: function(){
389                 var args = Array.from(arguments);
390                 return args.map(this.getProperty, this).associate(args);
391         },
393         removeProperty: function(attribute){
394                 attribute = camels[attribute] || attribute;
395                 var key = attributes[attribute];
396                 (key) ? this[key] = '' :
397                         (bools[attribute]) ? this[attribute] = false : this.removeAttribute(attribute);
398                 return this;
399         },
401         removeProperties: function(){
402                 Array.each(arguments, this.removeProperty, this);
403                 return this;
404         },
406         hasClass: function(className){
407                 return this.className.contains(className, ' ');
408         },
410         addClass: function(className){
411                 if (!this.hasClass(className)) this.className = (this.className + ' ' + className).clean();
412                 return this;
413         },
415         removeClass: function(className){
416                 this.className = this.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)'), '$1');
417                 return this;
418         },
420         toggleClass: function(className, force){
421                 if (force == null) force = !this.hasClass(className);
422                 return (force) ? this.addClass(className) : this.removeClass(className);
423         },
425         adopt: function(){
426                 var parent = this, fragment, elements = Array.flatten(arguments), length = elements.length;
427                 if (length > 1) parent = fragment = document.createDocumentFragment();
428                 
429                 for (var i = 0; i < length; i++){
430                         var element = document.id(elements[i], true);
431                         if (element) parent.appendChild(element);
432                 }
433                 
434                 if (fragment) this.appendChild(fragment);
435                 
436                 return this;
437         },
439         appendText: function(text, where){
440                 return this.grab(this.getDocument().newTextNode(text), where);
441         },
443         grab: function(el, where){
444                 inserters[where || 'bottom'](document.id(el, true), this);
445                 return this;
446         },
448         inject: function(el, where){
449                 inserters[where || 'bottom'](this, document.id(el, true));
450                 return this;
451         },
453         replaces: function(el){
454                 el = document.id(el, true);
455                 el.parentNode.replaceChild(this, el);
456                 return this;
457         },
459         wraps: function(el, where){
460                 el = document.id(el, true);
461                 return this.replaces(el).grab(el, where);
462         },
464         getPrevious: function(match){
465                 return document.id(Slick.find(this, '!+ ' + (match || '')));
466         },
468         getAllPrevious: function(match){
469                 return Slick.search(this, '!~ ' + (match || ''), new Elements);
470         },
472         getNext: function(match){
473                 return document.id(Slick.find(this, '~ ' + (match || '')));
474         },
476         getAllNext: function(match){
477                 return Slick.search(this, '~ ' + (match || ''), new Elements);
478         },
480         getFirst: function(match){
481                 return document.id(Slick.find(this, '> ' + (match || '')));
482         },
484         getLast: function(match){
485                 return document.id(Slick.find(this, '!^ ' + (match || '')));
486         },
488         getParent: function(match){
489                 return document.id(Slick.find(this, '! ' + (match || '')));
490         },
492         getParents: function(match){
493                 return Slick.search(this, '! ' + (match || ''), new Elements);
494         },
495         
496         getSiblings: function(match){
497                 return Slick.search(this, '~~ ' + (match || ''), new Elements);
498         },
500         getChildren: function(match){
501                 return Slick.search(this, '> ' + (match || ''), new Elements);
502         },
504         getWindow: function(){
505                 return this.ownerDocument.window;
506         },
508         getDocument: function(){
509                 return this.ownerDocument;
510         },
512         getElementById: function(id){
513                 return document.id(Slick.find(this, '#' + id));
514         },
516         getSelected: function(){
517                 this.selectedIndex; // Safari 3.2.1
518                 return new Elements(Array.from(this.options).filter(function(option){
519                         return option.selected;
520                 }));
521         },
523         toQueryString: function(){
524                 var queryString = [];
525                 this.getElements('input, select, textarea').each(function(el){
526                         var type = el.type;
527                         if (!el.name || el.disabled || type == 'submit' || type == 'reset' || type == 'file' || type == 'image') return;
528                         
529                         var value = (el.get('tag') == 'select') ? el.getSelected().map(function(opt){
530                                 // IE
531                                 return document.id(opt).get('value');
532                         }) : ((type == 'radio' || type == 'checkbox') && !el.checked) ? null : el.get('value');
533                         
534                         Array.from(value).each(function(val){
535                                 if (typeof val != 'undefined') queryString.push(encodeURIComponent(el.name) + '=' + encodeURIComponent(val));
536                         });
537                 });
538                 return queryString.join('&');
539         },
541         clone: function(contents, keepid){
542                 contents = contents !== false;
543                 var clone = this.cloneNode(contents);
544                 var clean = function(node, element){
545                         if (!keepid) node.removeAttribute('id');
546                         if (Browser.ie){
547                                 node.clearAttributes();
548                                 node.mergeAttributes(element);
549                                 node.removeAttribute('uid');
550                                 if (node.options){
551                                         var no = node.options, eo = element.options;
552                                         for (var j = no.length; j--;) no[j].selected = eo[j].selected;
553                                 }
554                         }
555                         var prop = props[element.tagName.toLowerCase()];
556                         if (prop && element[prop]) node[prop] = element[prop];
557                 };
559                 if (contents){
560                         var ce = clone.getElementsByTagName('*'), te = this.getElementsByTagName('*');
561                         for (var i = ce.length; i--;) clean(ce[i], te[i]);
562                 }
564                 clean(clone, this);
565                 return document.id(clone);
566         },
567         
568         destroy: function(){
569                 var children = clean(this).getElementsByTagName('*');
570                 Array.each(children, clean);
571                 Element.dispose(this);
572                 return null;
573         },
574         
575         empty: function(){
576                 Array.from(this.childNodes).each(Element.dispose);
577                 return this;
578         },
580         dispose: function(){
581                 return (this.parentNode) ? this.parentNode.removeChild(this) : this;
582         },
584         match: function(expression){
585                 return !expression || Slick.match(this, expression);
586         }
590 var contains = {contains: function(element){
591         return Slick.contains(this, element);
594 if (!document.contains) Document.implement(contains);
595 if (!document.createElement('div').contains) Element.implement(contains);
597 //<1.2compat>
599 Element.implement('hasChild', function(element){
600         return this !== element && this.contains(element);
603 //</1.2compat>
605 [Element, Window, Document].invoke('implement', {
607         addListener: function(type, fn){
608                 if (type == 'unload'){
609                         var old = fn, self = this;
610                         fn = function(){
611                                 self.removeListener('unload', fn);
612                                 old();
613                         };
614                 } else {
615                         collected[this.uid] = this;
616                 }
617                 if (this.addEventListener) this.addEventListener(type, fn, false);
618                 else this.attachEvent('on' + type, fn);
619                 return this;
620         },
622         removeListener: function(type, fn){
623                 if (this.removeEventListener) this.removeEventListener(type, fn, false);
624                 else this.detachEvent('on' + type, fn);
625                 return this;
626         },
628         retrieve: function(property, dflt){
629                 var storage = get(this.uid), prop = storage[property];
630                 if (dflt != null && prop == null) prop = storage[property] = dflt;
631                 return prop != null ? prop : null;
632         },
634         store: function(property, value){
635                 var storage = get(this.uid);
636                 storage[property] = value;
637                 return this;
638         },
640         eliminate: function(property){
641                 var storage = get(this.uid);
642                 delete storage[property];
643                 return this;
644         }
648 window.addListener('unload', purge);
650 })();
652 Element.Properties = {};
654 //<1.2compat>
656 Element.Properties = new Hash;
658 //</1.2compat>
660 Element.Properties.style = {
662         set: function(style){
663                 this.style.cssText = style;
664         },
666         get: function(){
667                 return this.style.cssText;
668         },
670         erase: function(){
671                 this.style.cssText = '';
672         }
676 Element.Properties.tag = {
678         get: function(){
679                 return this.tagName.toLowerCase();
680         }
684 Element.Properties.html = (function(){
685         
686         var tableTest = Function.attempt(function(){
687                 var table = document.createElement('table');
688                 table.innerHTML = '<tr><td></td></tr>';
689         });
690         
691         var wrapper = document.createElement('div');
693         var translations = {
694                 table: [1, '<table>', '</table>'],
695                 select: [1, '<select>', '</select>'],
696                 tbody: [2, '<table><tbody>', '</tbody></table>'],
697                 tr: [3, '<table><tbody><tr>', '</tr></tbody></table>']
698         };
699         translations.thead = translations.tfoot = translations.tbody;
701         var html = {
702                 set: function(){
703                         var html = Array.flatten(arguments).join('');
704                         var wrap = (!tableTest && translations[this.get('tag')]);
705                         if (wrap){
706                                 var first = wrapper;
707                                 first.innerHTML = wrap[1] + html + wrap[2];
708                                 for (var i = wrap[0]; i--;) first = first.firstChild;
709                                 this.empty().adopt(first.childNodes);
710                         } else {
711                                 this.innerHTML = html;
712                         }
713                 }
714         };
716         html.erase = html.set;
718         return html;
719 })();