From 5c656a9d7e5443a79e001010cc43b5319c907671 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Thu, 14 Jun 2007 07:48:08 +0100 Subject: [PATCH] update prototype to 1.5.0 --- prototype.js | 1534 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 1134 insertions(+), 400 deletions(-) diff --git a/prototype.js b/prototype.js index 0e85338ba..505822177 100644 --- a/prototype.js +++ b/prototype.js @@ -1,5 +1,5 @@ -/* Prototype JavaScript framework, version 1.4.0 - * (c) 2005 Sam Stephenson +/* Prototype JavaScript framework, version 1.5.0 + * (c) 2005-2007 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. * For details, see the Prototype web site: http://prototype.conio.net/ @@ -7,11 +7,14 @@ /*--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.4.0', - ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + Version: '1.5.0', + BrowserFeatures: { + XPath: !!document.evaluate + }, + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', emptyFunction: function() {}, - K: function(x) {return x} + K: function(x) { return x } } var Class = { @@ -25,22 +28,42 @@ var Class = { var Abstract = new Object(); Object.extend = function(destination, source) { - for (property in source) { + for (var property in source) { destination[property] = source[property]; } return destination; } -Object.inspect = function(object) { - try { - if (object == undefined) return 'undefined'; - if (object == null) return 'null'; - return object.inspect ? object.inspect() : object.toString(); - } catch (e) { - if (e instanceof RangeError) return '...'; - throw e; +Object.extend(Object, { + inspect: function(object) { + try { + if (object === undefined) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({}, object); } -} +}); Function.prototype.bind = function() { var __method = this, args = $A(arguments), object = args.shift(); @@ -50,9 +73,9 @@ Function.prototype.bind = function() { } Function.prototype.bindAsEventListener = function(object) { - var __method = this; + var __method = this, args = $A(arguments), object = args.shift(); return function(event) { - return __method.call(object, event || window.event); + return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments))); } } @@ -77,7 +100,7 @@ var Try = { these: function() { var returnValue; - for (var i = 0; i < arguments.length; i++) { + for (var i = 0, length = arguments.length; i < length; i++) { var lambda = arguments[i]; try { returnValue = lambda(); @@ -102,40 +125,73 @@ PeriodicalExecuter.prototype = { }, registerCallback: function() { - setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; }, onTimerEvent: function() { if (!this.currentlyExecuting) { try { this.currentlyExecuting = true; - this.callback(); + this.callback(this); } finally { this.currentlyExecuting = false; } } } } - -/*--------------------------------------------------------------------------*/ - -function $() { - var elements = new Array(); - - for (var i = 0; i < arguments.length; i++) { - var element = arguments[i]; - if (typeof element == 'string') - element = document.getElementById(element); - - if (arguments.length == 1) - return element; - - elements.push(element); - } - - return elements; +String.interpret = function(value){ + return value == null ? '' : String(value); } + Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : this; + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + stripTags: function() { return this.replace(/<\/?[^>]+>/gi, ''); }, @@ -153,7 +209,7 @@ Object.extend(String.prototype, { }, evalScripts: function() { - return this.extractScripts().map(eval); + return this.extractScripts().map(function(script) { return eval(script) }); }, escapeHTML: function() { @@ -166,15 +222,28 @@ Object.extend(String.prototype, { unescapeHTML: function() { var div = document.createElement('div'); div.innerHTML = this.stripTags(); - return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; }, - toQueryParams: function() { - var pairs = this.match(/^\??(.*)$/)[1].split('&'); - return pairs.inject({}, function(params, pairString) { - var pair = pairString.split('='); - params[pair[0]] = pair[1]; - return params; + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return {}; + + return match[1].split(separator || '&').inject({}, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var name = decodeURIComponent(pair[0]); + var value = pair[1] ? decodeURIComponent(pair[1]) : undefined; + + if (hash[name] !== undefined) { + if (hash[name].constructor != Array) + hash[name] = [hash[name]]; + if (value) hash[name].push(value); + } + else hash[name] = value; + } + return hash; }); }, @@ -182,29 +251,71 @@ Object.extend(String.prototype, { return this.split(''); }, - camelize: function() { - var oStringList = this.split('-'); - if (oStringList.length == 1) return oStringList[0]; - - var camelizedString = this.indexOf('-') == 0 - ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) - : oStringList[0]; - - for (var i = 1, len = oStringList.length; i < len; i++) { - var s = oStringList[i]; - camelizedString += s.charAt(0).toUpperCase() + s.substring(1); - } - - return camelizedString; + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); }, - inspect: function() { - return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function(){ + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.replace(/\\/g, '\\\\'); + if (useDoubleQuotes) + return '"' + escapedString.replace(/"/g, '\\"') + '"'; + else + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; } }); +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + String.prototype.parseQuery = String.prototype.toQueryParams; +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + String.interpret(object[match[3]]); + }); + } +} + var $break = new Object(); var $continue = new Object(); @@ -222,6 +333,14 @@ var Enumerable = { } catch (e) { if (e != $break) throw e; } + return this; + }, + + eachSlice: function(number, iterator) { + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.map(iterator); }, all: function(iterator) { @@ -234,7 +353,7 @@ var Enumerable = { }, any: function(iterator) { - var result = true; + var result = false; this.each(function(value, index) { if (result = !!(iterator || Prototype.K)(value, index)) throw $break; @@ -245,12 +364,12 @@ var Enumerable = { collect: function(iterator) { var results = []; this.each(function(value, index) { - results.push(iterator(value, index)); + results.push((iterator || Prototype.K)(value, index)); }); return results; }, - detect: function (iterator) { + detect: function(iterator) { var result; this.each(function(value, index) { if (iterator(value, index)) { @@ -291,6 +410,14 @@ var Enumerable = { return found; }, + inGroupsOf: function(number, fillWith) { + fillWith = fillWith === undefined ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + inject: function(memo, iterator) { this.each(function(value, index) { memo = iterator(memo, value, index); @@ -300,7 +427,7 @@ var Enumerable = { invoke: function(method) { var args = $A(arguments).slice(1); - return this.collect(function(value) { + return this.map(function(value) { return value[method].apply(value, args); }); }, @@ -309,7 +436,7 @@ var Enumerable = { var result; this.each(function(value, index) { value = (iterator || Prototype.K)(value, index); - if (value >= (result || value)) + if (result == undefined || value >= result) result = value; }); return result; @@ -319,7 +446,7 @@ var Enumerable = { var result; this.each(function(value, index) { value = (iterator || Prototype.K)(value, index); - if (value <= (result || value)) + if (result == undefined || value < result) result = value; }); return result; @@ -352,7 +479,7 @@ var Enumerable = { }, sortBy: function(iterator) { - return this.collect(function(value, index) { + return this.map(function(value, index) { return {value: value, criteria: iterator(value, index)}; }).sort(function(left, right) { var a = left.criteria, b = right.criteria; @@ -361,7 +488,7 @@ var Enumerable = { }, toArray: function() { - return this.collect(Prototype.K); + return this.map(); }, zip: function() { @@ -371,11 +498,14 @@ var Enumerable = { var collections = [this].concat(args).map($A); return this.map(function(value, index) { - iterator(value = collections.pluck(index)); - return value; + return iterator(collections.pluck(index)); }); }, + size: function() { + return this.toArray().length; + }, + inspect: function() { return '#'; } @@ -394,7 +524,7 @@ var $A = Array.from = function(iterable) { return iterable.toArray(); } else { var results = []; - for (var i = 0; i < iterable.length; i++) + for (var i = 0, length = iterable.length; i < length; i++) results.push(iterable[i]); return results; } @@ -402,11 +532,12 @@ var $A = Array.from = function(iterable) { Object.extend(Array.prototype, Enumerable); -Array.prototype._reverse = Array.prototype.reverse; +if (!Array.prototype._reverse) + Array.prototype._reverse = Array.prototype.reverse; Object.extend(Array.prototype, { _each: function(iterator) { - for (var i = 0; i < this.length; i++) + for (var i = 0, length = this.length; i < length; i++) iterator(this[i]); }, @@ -425,13 +556,13 @@ Object.extend(Array.prototype, { compact: function() { return this.select(function(value) { - return value != undefined || value != null; + return value != null; }); }, flatten: function() { return this.inject([], function(array, value) { - return array.concat(value.constructor == Array ? + return array.concat(value && value.constructor == Array ? value.flatten() : [value]); }); }, @@ -444,7 +575,7 @@ Object.extend(Array.prototype, { }, indexOf: function(object) { - for (var i = 0; i < this.length; i++) + for (var i = 0, length = this.length; i < length; i++) if (this[i] == object) return i; return -1; }, @@ -453,23 +584,88 @@ Object.extend(Array.prototype, { return (inline !== false ? this : this.toArray())._reverse(); }, - shift: function() { - var result = this[0]; - for (var i = 0; i < this.length - 1; i++) - this[i] = this[i + 1]; - this.length--; - return result; + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function() { + return this.inject([], function(array, value) { + return array.include(value) ? array : array.concat([value]); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; }, inspect: function() { return '[' + this.map(Object.inspect).join(', ') + ']'; } }); -var Hash = { + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string){ + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if(window.opera){ + Array.prototype.concat = function(){ + var array = []; + for(var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for(var i = 0, length = arguments.length; i < length; i++) { + if(arguments[i].constructor == Array) { + for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + } +} +var Hash = function(obj) { + Object.extend(this, obj || {}); +}; + +Object.extend(Hash, { + toQueryString: function(obj) { + var parts = []; + + this.prototype._each.call(obj, function(pair) { + if (!pair.key) return; + + if (pair.value && pair.value.constructor == Array) { + var values = pair.value.compact(); + if (values.length < 2) pair.value = values.reduce(); + else { + key = encodeURIComponent(pair.key); + values.each(function(value) { + value = value != undefined ? encodeURIComponent(value) : ''; + parts.push(key + '=' + encodeURIComponent(value)); + }); + return; + } + } + if (pair.value == undefined) pair[1] = ''; + parts.push(pair.map(encodeURIComponent).join('=')); + }); + + return parts.join('&'); + } +}); + +Object.extend(Hash.prototype, Enumerable); +Object.extend(Hash.prototype, { _each: function(iterator) { - for (key in this) { + for (var key in this) { var value = this[key]; - if (typeof value == 'function') continue; + if (value && value == Hash.prototype[key]) continue; var pair = [key, value]; pair.key = key; @@ -487,16 +683,30 @@ var Hash = { }, merge: function(hash) { - return $H(hash).inject($H(this), function(mergedHash, pair) { + return $H(hash).inject(this, function(mergedHash, pair) { mergedHash[pair.key] = pair.value; return mergedHash; }); }, + remove: function() { + var result; + for(var i = 0, length = arguments.length; i < length; i++) { + var value = this[arguments[i]]; + if (value !== undefined){ + if (result === undefined) result = value; + else { + if (result.constructor != Array) result = [result]; + result.push(value) + } + } + delete this[arguments[i]]; + } + return result; + }, + toQueryString: function() { - return this.map(function(pair) { - return pair.map(encodeURIComponent).join('='); - }).join('&'); + return Hash.toQueryString(this); }, inspect: function() { @@ -504,14 +714,12 @@ var Hash = { return pair.map(Object.inspect).join(': '); }).join(', ') + '}>'; } -} +}); function $H(object) { - var hash = Object.extend({}, object || {}); - Object.extend(hash, Enumerable); - Object.extend(hash, Hash); - return hash; -} + if (object && object.constructor == Hash) return object; + return new Hash(object); +}; ObjectRange = Class.create(); Object.extend(ObjectRange.prototype, Enumerable); Object.extend(ObjectRange.prototype, { @@ -523,10 +731,10 @@ Object.extend(ObjectRange.prototype, { _each: function(iterator) { var value = this.start; - do { + while (this.include(value)) { iterator(value); value = value.succ(); - } while (this.include(value)); + } }, include: function(value) { @@ -545,9 +753,9 @@ var $R = function(start, end, exclusive) { var Ajax = { getTransport: function() { return Try.these( + function() {return new XMLHttpRequest()}, function() {return new ActiveXObject('Msxml2.XMLHTTP')}, - function() {return new ActiveXObject('Microsoft.XMLHTTP')}, - function() {return new XMLHttpRequest()} + function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false; }, @@ -561,18 +769,18 @@ Ajax.Responders = { this.responders._each(iterator); }, - register: function(responderToAdd) { - if (!this.include(responderToAdd)) - this.responders.push(responderToAdd); + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); }, - unregister: function(responderToRemove) { - this.responders = this.responders.without(responderToRemove); + unregister: function(responder) { + this.responders = this.responders.without(responder); }, dispatch: function(callback, request, transport, json) { this.each(function(responder) { - if (responder[callback] && typeof responder[callback] == 'function') { + if (typeof responder[callback] == 'function') { try { responder[callback].apply(responder, [request, transport, json]); } catch (e) {} @@ -587,7 +795,6 @@ Ajax.Responders.register({ onCreate: function() { Ajax.activeRequestCount++; }, - onComplete: function() { Ajax.activeRequestCount--; } @@ -599,19 +806,15 @@ Ajax.Base.prototype = { this.options = { method: 'post', asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', parameters: '' } Object.extend(this.options, options || {}); - }, - responseIsSuccess: function() { - return this.transport.status == undefined - || this.transport.status == 0 - || (this.transport.status >= 200 && this.transport.status < 300); - }, - - responseIsFailure: function() { - return !this.responseIsSuccess(); + this.options.method = this.options.method.toLowerCase(); + if (typeof this.options.parameters == 'string') + this.options.parameters = this.options.parameters.toQueryParams(); } } @@ -620,6 +823,8 @@ Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + _complete: false, + initialize: function(url, options) { this.transport = Ajax.getTransport(); this.setOptions(options); @@ -627,74 +832,138 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { }, request: function(url) { - var parameters = this.options.parameters || ''; - if (parameters.length > 0) parameters += '&_='; + this.url = url; + this.method = this.options.method; + var params = this.options.parameters; + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + params = Hash.toQueryString(params); + if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_=' + + // when GET, append parameters to URL + if (this.method == 'get' && params) + this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params; try { - this.url = url; - if (this.options.method == 'get' && parameters.length > 0) - this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; - Ajax.Responders.dispatch('onCreate', this, this.transport); - this.transport.open(this.options.method, this.url, + this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous); - if (this.options.asynchronous) { - this.transport.onreadystatechange = this.onStateChange.bind(this); - setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); - } + if (this.options.asynchronous) + setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10); + this.transport.onreadystatechange = this.onStateChange.bind(this); this.setRequestHeaders(); - var body = this.options.postBody ? this.options.postBody : parameters; - this.transport.send(this.options.method == 'post' ? body : null); + var body = this.method == 'post' ? (this.options.postBody || params) : null; - } catch (e) { + this.transport.send(body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { this.dispatchException(e); } }, - setRequestHeaders: function() { - var requestHeaders = - ['X-Requested-With', 'XMLHttpRequest', - 'X-Prototype-Version', Prototype.Version]; - - if (this.options.method == 'post') { - requestHeaders.push('Content-type', - 'application/x-www-form-urlencoded'); - - /* Force "Connection: close" for Mozilla browsers to work around - * a bug where XMLHttpReqeuest sends an incorrect Content-length - * header. See Mozilla Bugzilla #246651. - */ - if (this.transport.overrideMimeType) - requestHeaders.push('Connection', 'close'); - } - - if (this.options.requestHeaders) - requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); - - for (var i = 0; i < requestHeaders.length; i += 2) - this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); - }, - onStateChange: function() { var readyState = this.transport.readyState; - if (readyState != 1) + if (readyState > 1 && !((readyState == 4) && this._complete)) this.respondToReadyState(this.transport.readyState); }, - header: function(name) { + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (typeof extras.push == 'function') + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + return !this.transport.status + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + this.transport.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.getHeader('Content-type') || 'text/javascript').strip(). + match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i)) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + state, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { try { return this.transport.getResponseHeader(name); - } catch (e) {} + } catch (e) { return null } }, evalJSON: function() { try { - return eval(this.header('X-JSON')); - } catch (e) {} + var json = this.getHeader('X-JSON'); + return json ? eval('(' + json + ')') : null; + } catch (e) { return null } }, evalResponse: function() { @@ -705,35 +974,6 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { } }, - respondToReadyState: function(readyState) { - var event = Ajax.Request.Events[readyState]; - var transport = this.transport, json = this.evalJSON(); - - if (event == 'Complete') { - try { - (this.options['on' + this.transport.status] - || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] - || Prototype.emptyFunction)(transport, json); - } catch (e) { - this.dispatchException(e); - } - - if ((this.header('Content-type') || '').match(/^text\/javascript/i)) - this.evalResponse(); - } - - try { - (this.options['on' + event] || Prototype.emptyFunction)(transport, json); - Ajax.Responders.dispatch('on' + event, this, transport, json); - } catch (e) { - this.dispatchException(e); - } - - /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ - if (event == 'Complete') - this.transport.onreadystatechange = Prototype.emptyFunction; - }, - dispatchException: function(exception) { (this.options.onException || Prototype.emptyFunction)(this, exception); Ajax.Responders.dispatch('onException', this, exception); @@ -744,41 +984,37 @@ Ajax.Updater = Class.create(); Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { initialize: function(container, url, options) { - this.containers = { - success: container.success ? $(container.success) : $(container), - failure: container.failure ? $(container.failure) : - (container.success ? null : $(container)) + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) } this.transport = Ajax.getTransport(); this.setOptions(options); var onComplete = this.options.onComplete || Prototype.emptyFunction; - this.options.onComplete = (function(transport, object) { + this.options.onComplete = (function(transport, param) { this.updateContent(); - onComplete(transport, object); + onComplete(transport, param); }).bind(this); this.request(url); }, updateContent: function() { - var receiver = this.responseIsSuccess() ? - this.containers.success : this.containers.failure; + var receiver = this.container[this.success() ? 'success' : 'failure']; var response = this.transport.responseText; - if (!this.options.evalScripts) - response = response.stripScripts(); + if (!this.options.evalScripts) response = response.stripScripts(); - if (receiver) { - if (this.options.insertion) { + if (receiver = $(receiver)) { + if (this.options.insertion) new this.options.insertion(receiver, response); - } else { - Element.update(receiver, response); - } + else + receiver.update(response); } - if (this.responseIsSuccess()) { + if (this.success()) { if (this.onComplete) setTimeout(this.onComplete.bind(this), 10); } @@ -807,7 +1043,7 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { }, stop: function() { - this.updater.onComplete = undefined; + this.updater.options.onComplete = undefined; clearTimeout(this.timer); (this.onComplete || Prototype.emptyFunction).apply(this, arguments); }, @@ -827,60 +1063,227 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { this.updater = new Ajax.Updater(this.container, this.url, this.options); } }); -document.getElementsByClassName = function(className, parentElement) { - var children = ($(parentElement) || document.body).getElementsByTagName('*'); - return $A(children).inject([], function(elements, child) { - if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) - elements.push(child); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); return elements; - }); + } + if (typeof element == 'string') + element = document.getElementById(element); + return Element.extend(element); } +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(query.snapshotItem(i)); + return results; + }; +} + +document.getElementsByClassName = function(className, parentElement) { + if (Prototype.BrowserFeatures.XPath) { + var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; + return document._getElementsByXPath(q, parentElement); + } else { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + var elements = [], child; + for (var i = 0, length = children.length; i < length; i++) { + child = children[i]; + if (Element.hasClassName(child, className)) + elements.push(Element.extend(child)); + } + return elements; + } +}; + /*--------------------------------------------------------------------------*/ -if (!window.Element) { +if (!window.Element) var Element = new Object(); -} -Object.extend(Element, { +Element.extend = function(element) { + if (!element || _nativeExtensions || element.nodeType == 3) return element; + + if (!element._extended && element.tagName && element != window) { + var methods = Object.clone(Element.Methods), cache = Element.extend.cache; + + if (element.tagName == 'FORM') + Object.extend(methods, Form.Methods); + if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName)) + Object.extend(methods, Form.Element.Methods); + + Object.extend(methods, Element.Methods.Simulated); + + for (var property in methods) { + var value = methods[property]; + if (typeof value == 'function' && !(property in element)) + element[property] = cache.findOrStore(value); + } + } + + element._extended = true; + return element; +}; + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +}; + +Element.Methods = { visible: function(element) { return $(element).style.display != 'none'; }, - toggle: function() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - Element[Element.visible(element) ? 'hide' : 'show'](element); - } + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; }, - hide: function() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - element.style.display = 'none'; - } + hide: function(element) { + $(element).style.display = 'none'; + return element; }, - show: function() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - element.style.display = ''; - } + show: function(element) { + $(element).style.display = ''; + return element; }, remove: function(element) { element = $(element); element.parentNode.removeChild(element); + return element; }, update: function(element, html) { + html = typeof html == 'undefined' ? '' : html.toString(); $(element).innerHTML = html.stripScripts(); setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + replace: function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $A($(element).getElementsByTagName('*')); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (typeof selector == 'string') + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + return Selector.findElement($(element).ancestors(), expression, index); + }, + + down: function(element, expression, index) { + return Selector.findElement($(element).descendants(), expression, index); + }, + + previous: function(element, expression, index) { + return Selector.findElement($(element).previousSiblings(), expression, index); + }, + + next: function(element, expression, index) { + return Selector.findElement($(element).nextSiblings(), expression, index); + }, + + getElementsBySelector: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + getElementsByClassName: function(element, className) { + return document.getElementsByClassName(className, element); + }, + + readAttribute: function(element, name) { + element = $(element); + if (document.all && !window.opera) { + var t = Element._attributeTranslations; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + var attribute = element.attributes[name]; + if(attribute) return attribute.nodeValue; + } + return element.getAttribute(name); }, getHeight: function(element) { - element = $(element); - return element.offsetHeight; + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; }, classNames: function(element) { @@ -889,67 +1292,131 @@ Object.extend(Element, { hasClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).include(className); + var elementClassName = element.className; + if (elementClassName.length == 0) return false; + if (elementClassName == className || + elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + return true; + return false; }, addClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).add(className); + Element.classNames(element).add(className); + return element; }, removeClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).remove(className); + Element.classNames(element).remove(className); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className); + return element; + }, + + observe: function() { + Event.observe.apply(Event, arguments); + return $A(arguments).first(); + }, + + stopObserving: function() { + Event.stopObserving.apply(Event, arguments); + return $A(arguments).first(); }, // removes whitespace-only text node children cleanWhitespace: function(element) { element = $(element); - for (var i = 0; i < element.childNodes.length; i++) { - var node = element.childNodes[i]; + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) - Element.remove(node); + element.removeChild(node); + node = nextNode; } + return element; }, empty: function(element) { return $(element).innerHTML.match(/^\s*$/); }, + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + scrollTo: function(element) { element = $(element); - var x = element.x ? element.x : element.offsetLeft, - y = element.y ? element.y : element.offsetTop; - window.scrollTo(x, y); + var pos = Position.cumulativeOffset(element); + window.scrollTo(pos[0], pos[1]); + return element; }, getStyle: function(element, style) { element = $(element); - var value = element.style[style.camelize()]; + if (['float','cssFloat'].include(style)) + style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat'); + style = style.camelize(); + var value = element.style[style]; if (!value) { if (document.defaultView && document.defaultView.getComputedStyle) { var css = document.defaultView.getComputedStyle(element, null); - value = css ? css.getPropertyValue(style) : null; + value = css ? css[style] : null; } else if (element.currentStyle) { - value = element.currentStyle[style.camelize()]; + value = element.currentStyle[style]; } } + if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none')) + value = element['offset'+style.capitalize()] + 'px'; + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) if (Element.getStyle(element, 'position') == 'static') value = 'auto'; - + if(style == 'opacity') { + if(value) return parseFloat(value); + if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } return value == 'auto' ? null : value; }, setStyle: function(element, style) { element = $(element); - for (name in style) - element.style[name.camelize()] = style[name]; + for (var name in style) { + var value = style[name]; + if(name == 'opacity') { + if (value == 1) { + value = (/Gecko/.test(navigator.userAgent) && + !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else if(value == '') { + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else { + if(value < 0.00001) value = 0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')'; + } + } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat'; + element.style[name.camelize()] = value; + } + return element; }, getDimensions: function(element) { element = $(element); - if (Element.getStyle(element, 'display') != 'none') + var display = $(element).getStyle('display'); + if (display != 'none' && display != null) // Safari bug return {width: element.offsetWidth, height: element.offsetHeight}; // All *Width and *Height properties give 0 on elements with display none, @@ -957,12 +1424,13 @@ Object.extend(Element, { var els = element.style; var originalVisibility = els.visibility; var originalPosition = els.position; + var originalDisplay = els.display; els.visibility = 'hidden'; els.position = 'absolute'; - els.display = ''; + els.display = 'block'; var originalWidth = element.clientWidth; var originalHeight = element.clientHeight; - els.display = 'none'; + els.display = originalDisplay; els.position = originalPosition; els.visibility = originalVisibility; return {width: originalWidth, height: originalHeight}; @@ -981,6 +1449,7 @@ Object.extend(Element, { element.style.left = 0; } } + return element; }, undoPositioned: function(element) { @@ -993,24 +1462,153 @@ Object.extend(Element, { element.style.bottom = element.style.right = ''; } + return element; }, makeClipping: function(element) { element = $(element); - if (element._overflow) return; - element._overflow = element.style.overflow; + if (element._overflow) return element; + element._overflow = element.style.overflow || 'auto'; if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') element.style.overflow = 'hidden'; + return element; }, undoClipping: function(element) { element = $(element); - if (element._overflow) return; - element.style.overflow = element._overflow; - element._overflow = undefined; + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; } +}; + +Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf}); + +Element._attributeTranslations = {}; + +Element._attributeTranslations.names = { + colspan: "colSpan", + rowspan: "rowSpan", + valign: "vAlign", + datetime: "dateTime", + accesskey: "accessKey", + tabindex: "tabIndex", + enctype: "encType", + maxlength: "maxLength", + readonly: "readOnly", + longdesc: "longDesc" +}; + +Element._attributeTranslations.values = { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + + title: function(element) { + var node = element.getAttributeNode('title'); + return node.specified ? node.nodeValue : null; + } +}; + +Object.extend(Element._attributeTranslations.values, { + href: Element._attributeTranslations.values._getAttr, + src: Element._attributeTranslations.values._getAttr, + disabled: Element._attributeTranslations.values._flag, + checked: Element._attributeTranslations.values._flag, + readonly: Element._attributeTranslations.values._flag, + multiple: Element._attributeTranslations.values._flag }); +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + var t = Element._attributeTranslations; + attribute = t.names[attribute] || attribute; + return $(element).getAttributeNode(attribute).specified; + } +}; + +// IE is missing .innerHTML support for TABLE-related elements +if (document.all && !window.opera){ + Element.Methods.update = function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + var tagName = element.tagName.toUpperCase(); + if (['THEAD','TBODY','TR','TD'].include(tagName)) { + var div = document.createElement('div'); + switch (tagName) { + case 'THEAD': + case 'TBODY': + div.innerHTML = '' + html.stripScripts() + '
'; + depth = 2; + break; + case 'TR': + div.innerHTML = '' + html.stripScripts() + '
'; + depth = 3; + break; + case 'TD': + div.innerHTML = '
' + html.stripScripts() + '
'; + depth = 4; + } + $A(element.childNodes).each(function(node){ + element.removeChild(node) + }); + depth.times(function(){ div = div.firstChild }); + + $A(div.childNodes).each( + function(node){ element.appendChild(node) }); + } else { + element.innerHTML = html.stripScripts(); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + } +}; + +Object.extend(Element, Element.Methods); + +var _nativeExtensions = false; + +if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) { + var className = 'HTML' + tag + 'Element'; + if(window[className]) return; + var klass = window[className] = {}; + klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__; + }); + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + var cache = Element.extend.cache; + for (var property in methods) { + var value = methods[property]; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = cache.findOrStore(value); + } + } + + if (typeof HTMLElement != 'undefined') { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + copy(Form.Methods, HTMLFormElement.prototype); + [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) { + copy(Form.Element.Methods, klass.prototype); + }); + _nativeExtensions = true; + } +} + var Toggle = new Object(); Toggle.display = Element.toggle; @@ -1029,7 +1627,8 @@ Abstract.Insertion.prototype = { try { this.element.insertAdjacentHTML(this.adjacency, this.content); } catch (e) { - if (this.element.tagName.toLowerCase() == 'tbody') { + var tagName = this.element.tagName.toUpperCase(); + if (['TBODY', 'TR'].include(tagName)) { this.insertContent(this.contentFromAnonymousTable()); } else { throw e; @@ -1128,220 +1727,358 @@ Element.ClassNames.prototype = { add: function(classNameToAdd) { if (this.include(classNameToAdd)) return; - this.set(this.toArray().concat(classNameToAdd).join(' ')); + this.set($A(this).concat(classNameToAdd).join(' ')); }, remove: function(classNameToRemove) { if (!this.include(classNameToRemove)) return; - this.set(this.select(function(className) { - return className != classNameToRemove; - }).join(' ')); + this.set($A(this).without(classNameToRemove).join(' ')); }, toString: function() { - return this.toArray().join(' '); + return $A(this).join(' '); } -} +}; Object.extend(Element.ClassNames.prototype, Enumerable); -var Field = { - clear: function() { - for (var i = 0; i < arguments.length; i++) - $(arguments[i]).value = ''; +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); }, - focus: function(element) { - $(element).focus(); + parseExpression: function() { + function abort(message) { throw 'Parse error in selector: ' + message; } + + if (this.expression == '') abort('empty expression'); + + var params = this.params, expr = this.expression, match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') return this.params.wildcard = true; + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { + modifier = match[1], clause = match[2], rest = match[3]; + switch (modifier) { + case '#': params.id = clause; break; + case '.': params.classNames.push(clause); break; + case '': + case undefined: params.tagName = clause.toUpperCase(); break; + default: abort(expr.inspect()); + } + expr = rest; + } + + if (expr.length > 0) abort(expr.inspect()); }, - present: function() { - for (var i = 0; i < arguments.length; i++) - if ($(arguments[i]).value == '') return false; - return true; + buildMatchExpression: function() { + var params = this.params, conditions = [], clause; + + if (params.wildcard) + conditions.push('true'); + if (clause = params.id) + conditions.push('element.readAttribute("id") == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0, length = clause.length; i < length; i++) + conditions.push('element.hasClassName(' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.readAttribute(' + attribute.name.inspect() + ')'; + var splitValueBy = function(delimiter) { + return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; + } + + switch (attribute.operator) { + case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; + case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; + case '|=': conditions.push( + splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() + ); break; + case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; + case '': + case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); }, - select: function(element) { - $(element).select(); + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + element = $(element); \ + return ' + this.buildMatchExpression()); }, - activate: function(element) { - element = $(element); - element.focus(); - if (element.select) - element.select(); + findElements: function(scope) { + var element; + + if (element = $(this.params.id)) + if (this.match(element)) + if (!scope || Element.childOf(element, scope)) + return [element]; + + scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + + var results = []; + for (var i = 0, length = scope.length; i < length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; + }, + + toString: function() { + return this.expression; } } -/*--------------------------------------------------------------------------*/ +Object.extend(Selector, { + matchElements: function(elements, expression) { + var selector = new Selector(expression); + return elements.select(selector.match.bind(selector)).map(Element.extend); + }, + findElement: function(elements, expression, index) { + if (typeof expression == 'number') index = expression, expression = false; + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + return expressions.map(function(expression) { + return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) { + var selector = new Selector(expr); + return results.inject([], function(elements, result) { + return elements.concat(selector.findElements(result || element)); + }); + }); + }).flatten(); + } +}); + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} var Form = { - serialize: function(form) { - var elements = Form.getElements($(form)); - var queryComponents = new Array(); + reset: function(form) { + $(form).reset(); + return form; + }, - for (var i = 0; i < elements.length; i++) { - var queryComponent = Form.Element.serialize(elements[i]); - if (queryComponent) - queryComponents.push(queryComponent); - } + serializeElements: function(elements, getHash) { + var data = elements.inject({}, function(result, element) { + if (!element.disabled && element.name) { + var key = element.name, value = $(element).getValue(); + if (value != undefined) { + if (result[key]) { + if (result[key].constructor != Array) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); - return queryComponents.join('&'); + return getHash ? data : Hash.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, getHash) { + return Form.serializeElements(Form.getElements(form), getHash); }, getElements: function(form) { - form = $(form); - var elements = new Array(); - - for (tagName in Form.Element.Serializers) { - var tagElements = form.getElementsByTagName(tagName); - for (var j = 0; j < tagElements.length; j++) - elements.push(tagElements[j]); - } - return elements; + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); }, getInputs: function(form, typeName, name) { form = $(form); var inputs = form.getElementsByTagName('input'); - if (!typeName && !name) - return inputs; + if (!typeName && !name) return $A(inputs).map(Element.extend); - var matchingInputs = new Array(); - for (var i = 0; i < inputs.length; i++) { + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { var input = inputs[i]; - if ((typeName && input.type != typeName) || - (name && input.name != name)) + if ((typeName && input.type != typeName) || (name && input.name != name)) continue; - matchingInputs.push(input); + matchingInputs.push(Element.extend(input)); } return matchingInputs; }, disable: function(form) { - var elements = Form.getElements(form); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; + form = $(form); + form.getElements().each(function(element) { element.blur(); element.disabled = 'true'; - } + }); + return form; }, enable: function(form) { - var elements = Form.getElements(form); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; + form = $(form); + form.getElements().each(function(element) { element.disabled = ''; - } + }); + return form; }, findFirstElement: function(form) { - return Form.getElements(form).find(function(element) { + return $(form).getElements().find(function(element) { return element.type != 'hidden' && !element.disabled && ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); }); }, focusFirstElement: function(form) { - Field.activate(Form.findFirstElement(form)); - }, - - reset: function(form) { - $(form).reset(); + form = $(form); + form.findFirstElement().activate(); + return form; } } +Object.extend(Form, Form.Methods); + +/*--------------------------------------------------------------------------*/ + Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +} + +Form.Element.Methods = { serialize: function(element) { element = $(element); - var method = element.tagName.toLowerCase(); - var parameter = Form.Element.Serializers[method](element); - - if (parameter) { - var key = encodeURIComponent(parameter[0]); - if (key.length == 0) return; - - if (parameter[1].constructor != Array) - parameter[1] = [parameter[1]]; - - return parameter[1].map(function(value) { - return key + '=' + encodeURIComponent(value); - }).join('&'); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = {}; + pair[element.name] = value; + return Hash.toQueryString(pair); + } } + return ''; }, getValue: function(element) { element = $(element); var method = element.tagName.toLowerCase(); - var parameter = Form.Element.Serializers[method](element); + return Form.Element.Serializers[method](element); + }, - if (parameter) - return parameter[1]; + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select && ( element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type) ) ) + element.select(); + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.blur(); + element.disabled = false; + return element; } } +Object.extend(Form.Element, Form.Element.Methods); +var Field = Form.Element; +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + Form.Element.Serializers = { input: function(element) { switch (element.type.toLowerCase()) { - case 'submit': - case 'hidden': - case 'password': - case 'text': - return Form.Element.Serializers.textarea(element); case 'checkbox': case 'radio': return Form.Element.Serializers.inputSelector(element); + default: + return Form.Element.Serializers.textarea(element); } - return false; }, inputSelector: function(element) { - if (element.checked) - return [element.name, element.value]; + return element.checked ? element.value : null; }, textarea: function(element) { - return [element.name, element.value]; + return element.value; }, select: function(element) { - return Form.Element.Serializers[element.type == 'select-one' ? + return this[element.type == 'select-one' ? 'selectOne' : 'selectMany'](element); }, selectOne: function(element) { - var value = '', opt, index = element.selectedIndex; - if (index >= 0) { - opt = element.options[index]; - value = opt.value; - if (!value && !('value' in opt)) - value = opt.text; - } - return [element.name, value]; + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; }, selectMany: function(element) { - var value = new Array(); - for (var i = 0; i < element.length; i++) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { var opt = element.options[i]; - if (opt.selected) { - var optValue = opt.value; - if (!optValue && !('value' in opt)) - optValue = opt.text; - value.push(optValue); - } + if (opt.selected) values.push(this.optionValue(opt)); } - return [element.name, value]; + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; } } /*--------------------------------------------------------------------------*/ -var $F = Form.Element.getValue; - -/*--------------------------------------------------------------------------*/ - Abstract.TimedObserver = function() {} Abstract.TimedObserver.prototype = { initialize: function(element, frequency, callback) { @@ -1359,7 +2096,9 @@ Abstract.TimedObserver.prototype = { onTimerEvent: function() { var value = this.getValue(); - if (this.lastValue != value) { + var changed = ('string' == typeof this.lastValue && 'string' == typeof value + ? this.lastValue != value : String(this.lastValue) != String(value)); + if (changed) { this.callback(this.element, value); this.lastValue = value; } @@ -1404,9 +2143,7 @@ Abstract.EventObserver.prototype = { }, registerFormCallbacks: function() { - var elements = Form.getElements(this.element); - for (var i = 0; i < elements.length; i++) - this.registerCallback(elements[i]); + Form.getElements(this.element).each(this.registerCallback.bind(this)); }, registerCallback: function(element) { @@ -1416,11 +2153,7 @@ Abstract.EventObserver.prototype = { case 'radio': Event.observe(element, 'click', this.onElementEvent.bind(this)); break; - case 'password': - case 'text': - case 'textarea': - case 'select-one': - case 'select-multiple': + default: Event.observe(element, 'change', this.onElementEvent.bind(this)); break; } @@ -1455,6 +2188,10 @@ Object.extend(Event, { KEY_RIGHT: 39, KEY_DOWN: 40, KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, element: function(event) { return event.target || event.srcElement; @@ -1510,7 +2247,7 @@ Object.extend(Event, { unloadCache: function() { if (!Event.observers) return; - for (var i = 0; i < Event.observers.length; i++) { + for (var i = 0, length = Event.observers.length; i < length; i++) { Event.stopObserving.apply(this, Event.observers[i]); Event.observers[i][0] = null; } @@ -1518,7 +2255,7 @@ Object.extend(Event, { }, observe: function(element, name, observer, useCapture) { - var element = $(element); + element = $(element); useCapture = useCapture || false; if (name == 'keypress' && @@ -1526,11 +2263,11 @@ Object.extend(Event, { || element.attachEvent)) name = 'keydown'; - this._observeAndCache(element, name, observer, useCapture); + Event._observeAndCache(element, name, observer, useCapture); }, stopObserving: function(element, name, observer, useCapture) { - var element = $(element); + element = $(element); useCapture = useCapture || false; if (name == 'keypress' && @@ -1541,13 +2278,16 @@ Object.extend(Event, { if (element.removeEventListener) { element.removeEventListener(name, observer, useCapture); } else if (element.detachEvent) { - element.detachEvent('on' + name, observer); + try { + element.detachEvent('on' + name, observer); + } catch (e) {} } } }); /* prevent memory leaks in IE */ -Event.observe(window, 'unload', Event.unloadCache, false); +if (navigator.appVersion.match(/\bMSIE\b/)) + Event.observe(window, 'unload', Event.unloadCache, false); var Position = { // set to true if needed, warning: firefox performance problems // NOT neeeded for page scrolling, only if draggable contained in @@ -1594,7 +2334,8 @@ var Position = { valueL += element.offsetLeft || 0; element = element.offsetParent; if (element) { - p = Element.getStyle(element, 'position'); + if(element.tagName=='BODY') break; + var p = Element.getStyle(element, 'position'); if (p == 'relative' || p == 'absolute') break; } } while (element); @@ -1650,17 +2391,6 @@ var Position = { element.offsetWidth; }, - clone: function(source, target) { - source = $(source); - target = $(target); - target.style.position = 'absolute'; - var offsets = this.cumulativeOffset(source); - target.style.top = offsets[1] + 'px'; - target.style.left = offsets[0] + 'px'; - target.style.width = source.offsetWidth + 'px'; - target.style.height = source.offsetHeight + 'px'; - }, - page: function(forElement) { var valueT = 0, valueL = 0; @@ -1677,8 +2407,10 @@ var Position = { element = forElement; do { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; + if (!window.opera || element.tagName=='BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } } while (element = element.parentNode); return [valueL, valueT]; @@ -1739,10 +2471,10 @@ var Position = { element._originalHeight = element.style.height; element.style.position = 'absolute'; - element.style.top = top + 'px';; - element.style.left = left + 'px';; - element.style.width = width + 'px';; - element.style.height = height + 'px';; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; }, relativize: function(element) { @@ -1778,4 +2510,6 @@ if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { return [valueL, valueT]; } -} \ No newline at end of file +} + +Element.addMethods(); \ No newline at end of file