- a more humane fix for the multiline className issue (LH #930)
[mootools/dkf.git] / Source / Element / Element.js
blobce62375356f0d44412a4168d1daee16133188564
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);
22         if (!props) props = {};
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;
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                 }
36                 if (parsed.classList && props['class'] == null) props['class'] = parsed.classList.join(' ');
37         }
39         return document.newElement(tag, props);
42 if (Browser.Element) Element.prototype = Browser.Element.prototype;
44 new Type('Element', Element).mirror(function(name){
45         if (Array.prototype[name]) return;
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         };
57         Elements.implement(obj);
58 });
60 if (!Browser.Element){
61         Element.parent = Object;
63         Element.ProtoType = {'$family': Function.from('element').hide()};
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, {
80                 properties: Type.isObject,
81                 iframe: function(obj){
82                         return (obj != null);
83                 }
84         });
85         var props = params.properties || {};
86         var iframe = document.id(params.iframe);
87         var onload = props.onload || function(){};
88         delete props.onload;
89         props.id = props.name = [props.id, props.name, iframe ? (iframe.id || iframe.name) : 'IFrame_' + Date.now()].pick();
90         iframe = new Element(iframe || 'iframe', props);
91         var onFrameLoad = function(){
92                 var host = Function.attempt(function(){
93                         return iframe.contentWindow.location.host;
94                 });
95                 if (!host || host == window.location.host){
96                         var win = new Window(iframe.contentWindow);
97                         new Document(iframe.contentWindow.document);
98                         Object.append(win.Element.prototype, Element.ProtoType);
99                 }
100                 onload.call(iframe.contentWindow, iframe.contentWindow.document);
101         };
102         var contentWindow = Function.attempt(function(){
103                 return iframe.contentWindow;
104         });
105         ((contentWindow && contentWindow.document.body) || window.frames[props.id]) ? onFrameLoad() : iframe.addListener('load', onFrameLoad);
106         return iframe;
109 var Elements = this.Elements = function(nodes){
110         if (nodes && nodes.length){
111                 var uniques = {}, node;
112                 for (var i = 0; node = nodes[i++];){
113                         var uid = Slick.uidOf(node);
114                         if (!uniques[uid]){
115                                 uniques[uid] = true;
116                                 this.push(node);
117                         }
118                 }
119         }
122 Elements.prototype = {length: 0};
123 Elements.parent = Array;
125 new Type('Elements', Elements).implement({
127         filter: function(filter, bind){
128                 if (!filter) return this;
129                 return new Elements(Array.filter(this, (typeOf(filter) == 'string') ? function(item){
130                         return item.match(filter);
131                 } : filter, bind));
132         }.protect(),
134         push: function(){
135                 var length = this.length;
136                 for (var i = 0, l = arguments.length; i < l; i++){
137                         var item = document.id(arguments[i]);
138                         if (item) this[length++] = item;
139                 }
140                 return (this.length = length);
141         }.protect()
143 }).implement(Array.prototype);
145 Array.mirror(Elements);
147 Document.implement({
149         newElement: function(tag, props){
150                 if (props && props.checked != null) props.defaultChecked = props.checked;
151                 return this.id(this.createElement(tag)).set(props);
152         },
154         newTextNode: function(text){
155                 return this.createTextNode(text);
156         },
158         getDocument: function(){
159                 return this;
160         },
162         getWindow: function(){
163                 return this.window;
164         },
166         id: (function(){
168                 var types = {
170                         string: function(id, nocash, doc){
171                                 id = Slick.find(doc, '#' + id);
172                                 return (id) ? types.element(id, nocash) : null;
173                         },
175                         element: function(el, nocash){
176                                 $uid(el);
177                                 if (!nocash && !el.$family && !(/^object|embed$/i).test(el.tagName)){
178                                         Object.append(el, Element.ProtoType);
179                                 }
180                                 return el;
181                         },
183                         object: function(obj, nocash, doc){
184                                 if (obj.toElement) return types.element(obj.toElement(doc), nocash);
185                                 return null;
186                         }
188                 };
190                 types.textnode = types.whitespace = types.window = types.document = function(zero){
191                         return zero;
192                 };
194                 return function(el, nocash, doc){
195                         if (el && el.$family && el.uid) return el;
196                         var type = typeOf(el);
197                         return (types[type]) ? types[type](el, nocash, doc || document) : null;
198                 };
200         })()
204 if (window.$ == null) Window.implement('$', function(el, nc){
205         return document.id(el, nc, this.document);
208 Window.implement({
210         getDocument: function(){
211                 return this.document;
212         },
214         getWindow: function(){
215                 return this;
216         }
220 [Document, Element].invoke('implement', {
222         getElements: function(expression){
223                 return Slick.search(this, expression, new Elements);
224         },
226         getElement: function(expression){
227                 return document.id(Slick.find(this, expression));
228         }
232 //<1.2compat>
234 (function(search, find, match){
236         this.Selectors = {};
237         var pseudos = this.Selectors.Pseudo = new Hash();
239         var addSlickPseudos = function(){
240                 for (var name in pseudos) if (pseudos.hasOwnProperty(name)){
241                         Slick.definePseudo(name, pseudos[name]);
242                         delete pseudos[name];
243                 }
244         };
246         Slick.search = function(context, expression, append){
247                 addSlickPseudos();
248                 return search.call(this, context, expression, append);
249         };
251         Slick.find = function(context, expression){
252                 addSlickPseudos();
253                 return find.call(this, context, expression);
254         };
256         Slick.match = function(node, selector){
257                 addSlickPseudos();
258                 return match.call(this, node, selector);
259         };
261 })(Slick.search, Slick.find, Slick.match);
263 if (window.$$ == null) Window.implement('$$', function(selector){
264         var elements = new Elements;
265         if (arguments.length == 1 && typeof selector == 'string') return Slick.search(this.document, selector, elements);
266         var args = Array.flatten(arguments);
267         for (var i = 0, l = args.length; i < l; i++){
268                 var item = args[i];
269                 switch (typeOf(item)){
270                         case 'element': elements.push(item); break;
271                         case 'string': Slick.search(this.document, item, elements);
272                 }
273         }
274         return elements;
277 //</1.2compat>
279 if (window.$$ == null) Window.implement('$$', function(selector){
280         if (arguments.length == 1){
281                 if (typeof selector == 'string') return Slick.search(this.document, selector, new Elements);
282                 else if (Type.isEnumerable(selector)) return new Elements(selector);
283         }
284         return new Elements(arguments);
287 (function(){
289 var collected = {}, storage = {};
290 var props = {input: 'checked', option: 'selected', textarea: 'value'};
292 var get = function(uid){
293         return (storage[uid] || (storage[uid] = {}));
296 var clean = function(item){
297         if (item.removeEvents) item.removeEvents();
298         if (item.clearAttributes) item.clearAttributes();
299         var uid = item.uid;
300         if (uid != null){
301                 delete collected[uid];
302                 delete storage[uid];
303         }
304         return item;
307 var camels = ['defaultValue', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan', 'frameBorder', 'maxLength', 'readOnly',
308         'rowSpan', 'tabIndex', 'useMap'
310 var bools = ['compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked', 'disabled', 'readOnly', 'multiple', 'selected',
311         'noresize', 'defer'
313  var attributes = {
314         'html': 'innerHTML',
315         'class': 'className',
316         'for': 'htmlFor',
317         'text': (function(){
318                 var temp = document.createElement('div');
319                 return (temp.innerText == null) ? 'textContent' : 'innerText';
320         })()
322 var readOnly = ['type'];
323 var expandos = ['value', 'defaultValue'];
324 var uriAttrs = /^href|src|usemap$/i;
326 bools = bools.associate(bools);
327 camels = camels.associate(camels.map(String.toLowerCase));
328 readOnly = readOnly.associate(readOnly);
330 Object.append(attributes, expandos.associate(expandos));
332 var inserters = {
334         before: function(context, element){
335                 var parent = element.parentNode;
336                 if (parent) parent.insertBefore(context, element);
337         },
339         after: function(context, element){
340                 var parent = element.parentNode;
341                 if (parent) parent.insertBefore(context, element.nextSibling);
342         },
344         bottom: function(context, element){
345                 element.appendChild(context);
346         },
348         top: function(context, element){
349                 element.insertBefore(context, element.firstChild);
350         }
354 inserters.inside = inserters.bottom;
356 //<1.2compat>
358 Object.each(inserters, function(inserter, where){
360         where = where.capitalize();
362         var methods = {};
364         methods['inject' + where] = function(el){
365                 inserter(this, document.id(el, true));
366                 return this;
367         };
369         methods['grab' + where] = function(el){
370                 inserter(document.id(el, true), this);
371                 return this;
372         };
374         Element.implement(methods);
378 //</1.2compat>
380 var injectCombinator = function(expression, combinator){
381         if (!expression) return combinator;
383         expression = Slick.parse(expression);
385         var expressions = expression.expressions;
386         for (var i = expressions.length; i--;)
387                 expressions[i][0].combinator = combinator;
389         return expression;
392 Element.implement({
394         set: function(prop, value){
395                 var property = Element.Properties[prop];
396                 (property && property.set) ? property.set.call(this, value) : this.setProperty(prop, value);
397         }.overloadSetter(),
399         get: function(prop){
400                 var property = Element.Properties[prop];
401                 return (property && property.get) ? property.get.apply(this) : this.getProperty(prop);
402         }.overloadGetter(),
404         erase: function(prop){
405                 var property = Element.Properties[prop];
406                 (property && property.erase) ? property.erase.apply(this) : this.removeProperty(prop);
407                 return this;
408         },
410         setProperty: function(attribute, value){
411                 attribute = camels[attribute] || attribute;
412                 if (value == null) return this.removeProperty(attribute);
413                 var key = attributes[attribute];
414                 (key) ? this[key] = value :
415                         (bools[attribute]) ? this[attribute] = !!value : this.setAttribute(attribute, '' + value);
416                 return this;
417         },
419         setProperties: function(attributes){
420                 for (var attribute in attributes) this.setProperty(attribute, attributes[attribute]);
421                 return this;
422         },
424         getProperty: function(attribute){
425                 attribute = camels[attribute] || attribute;
426                 var key = attributes[attribute] || readOnly[attribute];
427                 return (key) ? this[key] :
428                         (bools[attribute]) ? !!this[attribute] :
429                         (uriAttrs.test(attribute) ? this.getAttribute(attribute, 2) :
430                         (key = this.getAttributeNode(attribute)) ? key.nodeValue : null) || null;
431         },
433         getProperties: function(){
434                 var args = Array.from(arguments);
435                 return args.map(this.getProperty, this).associate(args);
436         },
438         removeProperty: function(attribute){
439                 attribute = camels[attribute] || attribute;
440                 var key = attributes[attribute];
441                 (key) ? this[key] = '' :
442                         (bools[attribute]) ? this[attribute] = false : this.removeAttribute(attribute);
443                 return this;
444         },
446         removeProperties: function(){
447                 Array.each(arguments, this.removeProperty, this);
448                 return this;
449         },
451         hasClass: function(className){
452                 return this.className.clean().contains(className, ' ');
453         },
455         addClass: function(className){
456                 if (!this.hasClass(className)) this.className = (this.className + ' ' + className).clean();
457                 return this;
458         },
460         removeClass: function(className){
461                 this.className = this.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)'), '$1');
462                 return this;
463         },
465         toggleClass: function(className, force){
466                 if (force == null) force = !this.hasClass(className);
467                 return (force) ? this.addClass(className) : this.removeClass(className);
468         },
470         adopt: function(){
471                 var parent = this, fragment, elements = Array.flatten(arguments), length = elements.length;
472                 if (length > 1) parent = fragment = document.createDocumentFragment();
474                 for (var i = 0; i < length; i++){
475                         var element = document.id(elements[i], true);
476                         if (element) parent.appendChild(element);
477                 }
479                 if (fragment) this.appendChild(fragment);
481                 return this;
482         },
484         appendText: function(text, where){
485                 return this.grab(this.getDocument().newTextNode(text), where);
486         },
488         grab: function(el, where){
489                 inserters[where || 'bottom'](document.id(el, true), this);
490                 return this;
491         },
493         inject: function(el, where){
494                 inserters[where || 'bottom'](this, document.id(el, true));
495                 return this;
496         },
498         replaces: function(el){
499                 el = document.id(el, true);
500                 el.parentNode.replaceChild(this, el);
501                 return this;
502         },
504         wraps: function(el, where){
505                 el = document.id(el, true);
506                 return this.replaces(el).grab(el, where);
507         },
509         getPrevious: function(expression){
510                 return document.id(Slick.find(this, injectCombinator(expression, '!~')));
511         },
513         getAllPrevious: function(expression){
514                 return Slick.search(this, injectCombinator(expression, '!~'), new Elements);
515         },
517         getNext: function(expression){
518                 return document.id(Slick.find(this, injectCombinator(expression, '~')));
519         },
521         getAllNext: function(expression){
522                 return Slick.search(this, injectCombinator(expression, '~'), new Elements);
523         },
525         getFirst: function(expression){
526                 return document.id(Slick.find(this, injectCombinator(expression, '>')));
527         },
529         getLast: function(expression){
530                 return document.id(Slick.find(this, injectCombinator(expression, '!^')));
531         },
533         getParent: function(expression){
534                 return document.id(Slick.find(this, injectCombinator(expression, '!')));
535         },
537         getParents: function(expression){
538                 return Slick.search(this, injectCombinator(expression, '!'), new Elements);
539         },
541         getSiblings: function(expression){
542                 return Slick.search(this, injectCombinator(expression, '~~'), new Elements);
543         },
545         getChildren: function(expression){
546                 return Slick.search(this, injectCombinator(expression, '>'), new Elements);
547         },
549         getWindow: function(){
550                 return this.ownerDocument.window;
551         },
553         getDocument: function(){
554                 return this.ownerDocument;
555         },
557         getElementById: function(id){
558                 return document.id(Slick.find(this, '#' + id));
559         },
561         getSelected: function(){
562                 this.selectedIndex; // Safari 3.2.1
563                 return new Elements(Array.from(this.options).filter(function(option){
564                         return option.selected;
565                 }));
566         },
568         toQueryString: function(){
569                 var queryString = [];
570                 this.getElements('input, select, textarea').each(function(el){
571                         var type = el.type;
572                         if (!el.name || el.disabled || type == 'submit' || type == 'reset' || type == 'file' || type == 'image') return;
574                         var value = (el.get('tag') == 'select') ? el.getSelected().map(function(opt){
575                                 // IE
576                                 return document.id(opt).get('value');
577                         }) : ((type == 'radio' || type == 'checkbox') && !el.checked) ? null : el.get('value');
579                         Array.from(value).each(function(val){
580                                 if (typeof val != 'undefined') queryString.push(encodeURIComponent(el.name) + '=' + encodeURIComponent(val));
581                         });
582                 });
583                 return queryString.join('&');
584         },
586         clone: function(contents, keepid){
587                 contents = contents !== false;
588                 var clone = this.cloneNode(contents);
589                 var clean = function(node, element){
590                         if (!keepid) node.removeAttribute('id');
591                         if (Browser.ie){
592                                 node.clearAttributes();
593                                 node.mergeAttributes(element);
594                                 node.removeAttribute('uid');
595                                 if (node.options){
596                                         var no = node.options, eo = element.options;
597                                         for (var j = no.length; j--;) no[j].selected = eo[j].selected;
598                                 }
599                         }
600                         var prop = props[element.tagName.toLowerCase()];
601                         if (prop && element[prop]) node[prop] = element[prop];
602                 };
604                 var i;
605                 if (contents){
606                         var ce = clone.getElementsByTagName('*'), te = this.getElementsByTagName('*');
607                         for (i = ce.length; i--;) clean(ce[i], te[i]);
608                 }
610                 clean(clone, this);
611                 if (Browser.ie){
612                         var ts = this.getElementsByTagName('object'),
613                                 cs = clone.getElementsByTagName('object'),
614                                 tl = ts.length, cl = cs.length;
615                         for (i = 0; i < tl && i < cl; i++)
616                                 cs[i].outerHTML = ts[i].outerHTML;
617                 }
618                 return document.id(clone);
619         },
621         destroy: function(){
622                 var children = clean(this).getElementsByTagName('*');
623                 Array.each(children, clean);
624                 Element.dispose(this);
625                 return null;
626         },
628         empty: function(){
629                 Array.from(this.childNodes).each(Element.dispose);
630                 return this;
631         },
633         dispose: function(){
634                 return (this.parentNode) ? this.parentNode.removeChild(this) : this;
635         },
637         match: function(expression){
638                 return !expression || Slick.match(this, expression);
639         }
643 var contains = {contains: function(element){
644         return Slick.contains(this, element);
647 if (!document.contains) Document.implement(contains);
648 if (!document.createElement('div').contains) Element.implement(contains);
650 //<1.2compat>
652 Element.implement('hasChild', function(element){
653         return this !== element && this.contains(element);
656 //</1.2compat>
658 [Element, Window, Document].invoke('implement', {
660         addListener: function(type, fn){
661                 if (type == 'unload'){
662                         var old = fn, self = this;
663                         fn = function(){
664                                 self.removeListener('unload', fn);
665                                 old();
666                         };
667                 } else {
668                         collected[this.uid] = this;
669                 }
670                 if (this.addEventListener) this.addEventListener(type, fn, false);
671                 else this.attachEvent('on' + type, fn);
672                 return this;
673         },
675         removeListener: function(type, fn){
676                 if (this.removeEventListener) this.removeEventListener(type, fn, false);
677                 else this.detachEvent('on' + type, fn);
678                 return this;
679         },
681         retrieve: function(property, dflt){
682                 var storage = get(this.uid), prop = storage[property];
683                 if (dflt != null && prop == null) prop = storage[property] = dflt;
684                 return prop != null ? prop : null;
685         },
687         store: function(property, value){
688                 var storage = get(this.uid);
689                 storage[property] = value;
690                 return this;
691         },
693         eliminate: function(property){
694                 var storage = get(this.uid);
695                 delete storage[property];
696                 return this;
697         }
701 // IE purge
702 if (window.attachEvent && !window.addEventListener) window.addListener('unload', function(){
703         Object.each(collected, clean);
704         if (window.CollectGarbage) CollectGarbage();
707 })();
709 Element.Properties = {};
711 //<1.2compat>
713 Element.Properties = new Hash;
715 //</1.2compat>
717 Element.Properties.style = {
719         set: function(style){
720                 this.style.cssText = style;
721         },
723         get: function(){
724                 return this.style.cssText;
725         },
727         erase: function(){
728                 this.style.cssText = '';
729         }
733 Element.Properties.tag = {
735         get: function(){
736                 return this.tagName.toLowerCase();
737         }
741 (function(maxLength){
742         if (maxLength != null) Element.Properties.maxlength = Element.Properties.maxLength = {
743                 get: function(){
744                         var maxlength = this.getAttribute('maxLength');
745                         return maxlength == maxLength ? null : maxlength;
746                 }
747         };
748 })(document.createElement('input').getAttribute('maxLength'));
750 Element.Properties.html = (function(){
752         var tableTest = Function.attempt(function(){
753                 var table = document.createElement('table');
754                 table.innerHTML = '<tr><td></td></tr>';
755         });
757         var wrapper = document.createElement('div');
759         var translations = {
760                 table: [1, '<table>', '</table>'],
761                 select: [1, '<select>', '</select>'],
762                 tbody: [2, '<table><tbody>', '</tbody></table>'],
763                 tr: [3, '<table><tbody><tr>', '</tr></tbody></table>']
764         };
765         translations.thead = translations.tfoot = translations.tbody;
767         var html = {
768                 set: function(){
769                         var html = Array.flatten(arguments).join('');
770                         var wrap = (!tableTest && translations[this.get('tag')]);
771                         if (wrap){
772                                 var first = wrapper;
773                                 first.innerHTML = wrap[1] + html + wrap[2];
774                                 for (var i = wrap[0]; i--;) first = first.firstChild;
775                                 this.empty().adopt(first.childNodes);
776                         } else {
777                                 this.innerHTML = html;
778                         }
779                 }
780         };
782         html.erase = html.set;
784         return html;
785 })();