Index: tree/tree.js =================================================================== --- tree/tree.js (revision 26) +++ tree/tree.js (revision 39) @@ -543,7 +543,7 @@ } TreeNode_bSpan_onclick = function() { - this.treeNode.toggle(); + this.treeNode.TreeNode.toggle(); }; TreeNode_SpanA = { Index: scriptaculous/builder.js =================================================================== --- scriptaculous/builder.js (revision 26) +++ scriptaculous/builder.js (revision 39) @@ -1,6 +1,9 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// script.aculo.us builder.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // -// See scriptaculous.js for full license. +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ var Builder = { NODEMAP: { @@ -33,7 +36,7 @@ var element = parentElement.firstChild || null; // see if browser added wrapping tags - if(element && (element.tagName != elementName)) + if(element && (element.tagName.toUpperCase() != elementName)) element = element.getElementsByTagName(elementName)[0]; // fallback to createElement approach @@ -45,7 +48,8 @@ // attributes (or text) if(arguments[1]) if(this._isStringOrNumber(arguments[1]) || - (arguments[1] instanceof Array)) { + (arguments[1] instanceof Array) || + arguments[1].tagName) { this._children(element, arguments[1]); } else { var attrs = this._attributes(arguments[1]); @@ -61,9 +65,9 @@ for(attr in arguments[1]) element[attr == 'class' ? 'className' : attr] = arguments[1][attr]; } - if(element.tagName != elementName) + if(element.tagName.toUpperCase() != elementName) element = parentElement.getElementsByTagName(elementName)[0]; - } + } } // text, or array of children @@ -75,14 +79,24 @@ _text: function(text) { return document.createTextNode(text); }, + + ATTR_MAP: { + 'className': 'class', + 'htmlFor': 'for' + }, + _attributes: function(attributes) { var attrs = []; for(attribute in attributes) - attrs.push((attribute=='className' ? 'class' : attribute) + - '="' + attributes[attribute].toString().escapeHTML() + '"'); + attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) + + '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'"') + '"'); return attrs.join(" "); }, _children: function(element, children) { + if(children.tagName) { + element.appendChild(children); + return; + } if(typeof children=='object') { // array can hold nodes and text children.flatten().each( function(e) { if(typeof e=='object') @@ -92,10 +106,31 @@ element.appendChild(Builder._text(e)); }); } else - if(Builder._isStringOrNumber(children)) - element.appendChild(Builder._text(children)); + if(Builder._isStringOrNumber(children)) + element.appendChild(Builder._text(children)); }, _isStringOrNumber: function(param) { return(typeof param=='string' || typeof param=='number'); + }, + build: function(html) { + var element = this.node('div'); + $(element).update(html.strip()); + return element.down(); + }, + dump: function(scope) { + if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope + + var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " + + "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " + + "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+ + "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+ + "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+ + "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/); + + tags.each( function(tag){ + scope[tag] = function() { + return Builder.node.apply(Builder, [tag].concat($A(arguments))); + } + }); } -} \ No newline at end of file +} Index: scriptaculous/effects.js =================================================================== --- scriptaculous/effects.js (revision 26) +++ scriptaculous/effects.js (revision 39) @@ -1,18 +1,19 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// script.aculo.us effects.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Contributors: // Justin Palmer (http://encytemedia.com/) // Mark Pilgrim (http://diveintomark.org/) // Martin Bialasinki // -// See scriptaculous.js for full license. +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ -/* ------------- element ext -------------- */ - // converts rgb() and #xxx to #xxxxxx format, // returns self (or first argument) if not convertable String.prototype.parseColor = function() { - color = "#"; - if(this.slice(0,4) == "rgb(") { + var color = '#'; + if(this.slice(0,4) == 'rgb(') { var cols = this.slice(4,this.length-1).split(','); var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); } else { @@ -22,88 +23,73 @@ } } return(color.length==7 ? color : (arguments[0] || this)); -} +} -Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { - var children = $(element).childNodes; - var text = ""; - var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); - - for (var i = 0; i < children.length; i++) { - if(children[i].nodeType==3) { - text+=children[i].nodeValue; - } else { - if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) - text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); - } - } - - return text; +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); } -Element.setContentZoom = function(element, percent) { +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +} + +Element.setContentZoom = function(element, percent) { element = $(element); - element.style.fontSize = (percent/100) + "em"; - if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + element.setStyle({fontSize: (percent/100) + 'em'}); + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + return element; } -Element.getOpacity = function(element){ - var opacity; - if (opacity = Element.getStyle(element, "opacity")) - return parseFloat(opacity); - if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/)) - if(opacity[1]) return parseFloat(opacity[1]) / 100; - return 1.0; +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; } -Element.setOpacity = function(element, value){ - element= $(element); - var els = element.style; - if (value == 1){ - els.opacity = '0.999999'; - if(/MSIE/.test(navigator.userAgent)) - els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,''); - } else { - if(value < 0.00001) value = 0; - els.opacity = value; - if(/MSIE/.test(navigator.userAgent)) - els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') + - "alpha(opacity="+value*100+")"; - } -} - -Element.getInlineOpacity = function(element){ - element= $(element); - var op; - op = element.style.opacity; - if (typeof op != "undefined" && op != "") return op; - return ""; -} - -Element.setInlineOpacity = function(element, value){ - element= $(element); - var els = element.style; - els.opacity = value; -} - -Element.childrenWithClassName = function(element, className) { - return $A($(element).getElementsByTagName('*')).select( - function(c) { return Element.hasClassName(c, className) }); -} - +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + /*--------------------------------------------------------------------------*/ +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, tagifyText: function(element) { - var tagifyStyle = "position:relative"; - if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1"; + if(typeof Builder == 'undefined') + throw("Effect.tagifyText requires including script.aculo.us' builder.js library"); + + var tagifyStyle = 'position:relative'; + if(Prototype.Browser.IE) tagifyStyle += ';zoom:1'; + element = $(element); $A(element.childNodes).each( function(child) { if(child.nodeType==3) { child.nodeValue.toArray().each( function(character) { element.insertBefore( Builder.node('span',{style: tagifyStyle}, - character == " " ? String.fromCharCode(160) : character), + character == ' ' ? String.fromCharCode(160) : character), child); }); Element.remove(child); @@ -123,12 +109,25 @@ speed: 0.1, delay: 0.0 }, arguments[2] || {}); - var speed = options.speed; - var delay = options.delay; + var masterDelay = options.delay; $A(elements).each( function(element, index) { - new effect(element, Object.extend(options, { delay: delay + index * speed })); + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || {}); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); } }; @@ -136,46 +135,55 @@ /* ------------- transitions ------------- */ -Effect.Transitions = {} +Effect.Transitions = { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + return (pos > 1 ? 1 : pos); + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + Math.round((pos % (1/pulses)) * pulses) == 0 ? + ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : + 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) + ); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } +}; -Effect.Transitions.linear = function(pos) { - return pos; -} -Effect.Transitions.sinoidal = function(pos) { - return (-Math.cos(pos*Math.PI)/2) + 0.5; -} -Effect.Transitions.reverse = function(pos) { - return 1-pos; -} -Effect.Transitions.flicker = function(pos) { - return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; -} -Effect.Transitions.wobble = function(pos) { - return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; -} -Effect.Transitions.pulse = function(pos) { - return (Math.floor(pos*10) % 2 == 0 ? - (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10))); -} -Effect.Transitions.none = function(pos) { - return 0; -} -Effect.Transitions.full = function(pos) { - return 1; -} - /* ------------- core effects ------------- */ -Effect.Queue = { - effects: [], +Effect.ScopedQueue = Class.create(); +Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { + initialize: function() { + this.effects = []; + this.interval = null; + }, _each: function(iterator) { this.effects._each(iterator); }, - interval: null, add: function(effect) { var timestamp = new Date().getTime(); - switch(effect.options.queue) { + var position = (typeof effect.options.queue == 'string') ? + effect.options.queue : effect.options.queue.position; + + switch(position) { case 'front': // move unstarted effects after this effect this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { @@ -183,6 +191,9 @@ e.finishOn += effect.finishOn; }); break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; case 'end': // start effect after last queued effect has finished timestamp = this.effects.pluck('finishOn').max() || timestamp; @@ -191,9 +202,12 @@ effect.startOn += timestamp; effect.finishOn += timestamp; - this.effects.push(effect); - if(!this.interval) - this.interval = setInterval(this.loop.bind(this), 40); + + if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 15); }, remove: function(effect) { this.effects = this.effects.reject(function(e) { return e==effect }); @@ -204,34 +218,72 @@ }, loop: function() { var timePos = new Date().getTime(); - this.effects.invoke('loop', timePos); + for(var i=0, len=this.effects.length;i= this.startOn) { @@ -243,36 +295,29 @@ this.event('afterFinish'); return; } - var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); - var frame = Math.round(pos * this.options.fps * this.options.duration); + var pos = (timePos - this.startOn) / this.totalTime, + frame = Math.round(pos * this.totalFrames); if(frame > this.currentFrame) { this.render(pos); this.currentFrame = frame; } } }, - render: function(pos) { - if(this.state == 'idle') { - this.state = 'running'; - this.event('beforeSetup'); - if(this.setup) this.setup(); - this.event('afterSetup'); - } - if(this.options.transition) pos = this.options.transition(pos); - pos *= (this.options.to-this.options.from); - pos += this.options.from; - this.position = pos; - this.event('beforeUpdate'); - if(this.update) this.update(pos); - this.event('afterUpdate'); - }, cancel: function() { - if(!this.options.sync) Effect.Queue.remove(this); + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).remove(this); this.state = 'finished'; }, event: function(eventName) { if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); if(this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + var data = $H(); + for(property in this) + if(typeof this[property] != 'function') data[property] = this[property]; + return '#'; } } @@ -296,57 +341,81 @@ } }); +Effect.Event = Class.create(); +Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), { + initialize: function() { + var options = Object.extend({ + duration: 0 + }, arguments[0] || {}); + this.start(options); + }, + update: Prototype.emptyFunction +}); + Effect.Opacity = Class.create(); Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { initialize: function(element) { this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); // make this work on IE on elements without 'layout' - if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) - this.element.style.zoom = 1; + if(Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); var options = Object.extend({ - from: Element.getOpacity(this.element) || 0.0, + from: this.element.getOpacity() || 0.0, to: 1.0 }, arguments[1] || {}); this.start(options); }, update: function(position) { - Element.setOpacity(this.element, position); + this.element.setOpacity(position); } }); -Effect.MoveBy = Class.create(); -Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), { - initialize: function(element, toTop, toLeft) { - this.element = $(element); - this.toTop = toTop; - this.toLeft = toLeft; - this.start(arguments[3]); +Effect.Move = Class.create(); +Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || {}); + this.start(options); }, setup: function() { // Bug in Opera: Opera returns the "real" position of a static element or // relative element that does not have top/left explicitly set. // ==> Always set top and left for position relative elements in your stylesheets - // (to 0 if you do not need them) - - Element.makePositioned(this.element); - this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0'); - this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0'); + // (to 0 if you do not need them) + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if(this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } }, update: function(position) { - var topd = this.toTop * position + this.originalTop; - var leftd = this.toLeft * position + this.originalLeft; - this.setPosition(topd, leftd); - }, - setPosition: function(topd, leftd) { - this.element.style.top = topd + "px"; - this.element.style.left = leftd + "px"; + this.element.setStyle({ + left: Math.round(this.options.x * position + this.originalLeft) + 'px', + top: Math.round(this.options.y * position + this.originalTop) + 'px' + }); } }); +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); +}; + Effect.Scale = Class.create(); Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { initialize: function(element, percent) { - this.element = $(element) + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); var options = Object.extend({ scaleX: true, scaleY: true, @@ -359,32 +428,30 @@ this.start(options); }, setup: function() { - var effect = this; - this.restoreAfterFinish = this.options.restoreAfterFinish || false; - this.elementPositioning = Element.getStyle(this.element,'position'); + this.elementPositioning = this.element.getStyle('position'); - effect.originalStyle = {}; + this.originalStyle = {}; ['top','left','width','height','fontSize'].each( function(k) { - effect.originalStyle[k] = effect.element.style[k]; - }); + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); this.originalTop = this.element.offsetTop; this.originalLeft = this.element.offsetLeft; - var fontSize = Element.getStyle(this.element,'font-size') || "100%"; - ['em','px','%'].each( function(fontSizeType) { + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { if(fontSize.indexOf(fontSizeType)>0) { - effect.fontSize = parseFloat(fontSize); - effect.fontSizeType = fontSizeType; + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; } - }); + }.bind(this)); this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; this.dims = null; if(this.options.scaleMode=='box') - this.dims = [this.element.clientHeight, this.element.clientWidth]; + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; if(/^content/.test(this.options.scaleMode)) this.dims = [this.element.scrollHeight, this.element.scrollWidth]; if(!this.dims) @@ -394,32 +461,28 @@ update: function(position) { var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); if(this.options.scaleContent && this.fontSize) - this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType; + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); }, finish: function(position) { - if (this.restoreAfterFinish) { - var effect = this; - ['top','left','width','height','fontSize'].each( function(k) { - effect.element.style[k] = effect.originalStyle[k]; - }); - } + if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle); }, setDimensions: function(height, width) { - var els = this.element.style; - if(this.options.scaleX) els.width = width + 'px'; - if(this.options.scaleY) els.height = height + 'px'; + var d = {}; + if(this.options.scaleX) d.width = Math.round(width) + 'px'; + if(this.options.scaleY) d.height = Math.round(height) + 'px'; if(this.options.scaleFromCenter) { var topd = (height - this.dims[0])/2; var leftd = (width - this.dims[1])/2; if(this.elementPositioning == 'absolute') { - if(this.options.scaleY) els.top = this.originalTop-topd + "px"; - if(this.options.scaleX) els.left = this.originalLeft-leftd + "px"; + if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; } else { - if(this.options.scaleY) els.top = -topd + "px"; - if(this.options.scaleX) els.left = -leftd + "px"; + if(this.options.scaleY) d.top = -topd + 'px'; + if(this.options.scaleX) d.left = -leftd + 'px'; } } + this.element.setStyle(d); } }); @@ -427,41 +490,35 @@ Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { initialize: function(element) { this.element = $(element); - var options = Object.extend({ - startcolor: "#ffff99" - }, arguments[1] || {}); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); this.start(options); }, setup: function() { // Prevent executing on elements not in the layout flow - if(this.element.style.display=='none') { this.cancel(); return; } + if(this.element.getStyle('display')=='none') { this.cancel(); return; } // Disable background image during the effect - this.oldBgImage = this.element.style.backgroundImage; - this.element.style.backgroundImage = "none"; + this.oldStyle = {}; + if (!this.options.keepBackgroundImage) { + this.oldStyle.backgroundImage = this.element.getStyle('background-image'); + this.element.setStyle({backgroundImage: 'none'}); + } if(!this.options.endcolor) - this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff'); - if (typeof this.options.restorecolor == "undefined") - this.options.restorecolor = this.element.style.backgroundColor; + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if(!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); // init color calculations - this.colors_base = [ - parseInt(this.options.startcolor.slice(1,3),16), - parseInt(this.options.startcolor.slice(3,5),16), - parseInt(this.options.startcolor.slice(5),16) ]; - this.colors_delta = [ - parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0], - parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1], - parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]]; + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); }, update: function(position) { - var effect = this; var colors = $R(0,2).map( function(i){ - return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position)) - }); - this.element.style.backgroundColor = "#" + - colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart(); + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); }, finish: function() { - this.element.style.backgroundColor = this.options.restorecolor; - this.element.style.backgroundImage = this.oldBgImage; + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); } }); @@ -474,6 +531,7 @@ setup: function() { Position.prepare(); var offsets = Position.cumulativeOffset(this.element); + if(this.options.offset) offsets[1] += this.options.offset; var max = window.innerHeight ? window.height - window.innerHeight : document.body.scrollHeight - @@ -492,93 +550,93 @@ /* ------------- combination effects ------------- */ Effect.Fade = function(element) { - var oldOpacity = Element.getInlineOpacity(element); + element = $(element); + var oldOpacity = element.getInlineOpacity(); var options = Object.extend({ - from: Element.getOpacity(element) || 1.0, + from: element.getOpacity() || 1.0, to: 0.0, - afterFinishInternal: function(effect) - { if (effect.options.to == 0) { - Element.hide(effect.element); - Element.setInlineOpacity(effect.element, oldOpacity); - } - } - }, arguments[1] || {}); + afterFinishInternal: function(effect) { + if(effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + }}, arguments[1] || {}); return new Effect.Opacity(element,options); } Effect.Appear = function(element) { + element = $(element); var options = Object.extend({ - from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0), + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), to: 1.0, - beforeSetup: function(effect) - { Element.setOpacity(effect.element, effect.options.from); - Element.show(effect.element); } - }, arguments[1] || {}); + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || {}); return new Effect.Opacity(element,options); } Effect.Puff = function(element) { element = $(element); - var oldOpacity = Element.getInlineOpacity(element); - var oldPosition = element.style.position; + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; return new Effect.Parallel( [ new Effect.Scale(element, 200, { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], Object.extend({ duration: 1.0, - beforeSetupInternal: function(effect) - { effect.effects[0].element.style.position = 'absolute'; }, - afterFinishInternal: function(effect) - { Element.hide(effect.effects[0].element); - effect.effects[0].element.style.position = oldPosition; - Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element) + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } }, arguments[1] || {}) ); } Effect.BlindUp = function(element) { element = $(element); - Element.makeClipping(element); - return new Effect.Scale(element, 0, + element.makeClipping(); + return new Effect.Scale(element, 0, Object.extend({ scaleContent: false, scaleX: false, restoreAfterFinish: true, - afterFinishInternal: function(effect) - { - Element.hide(effect.element); - Element.undoClipping(effect.element); - } + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } }, arguments[1] || {}) ); } Effect.BlindDown = function(element) { element = $(element); - var oldHeight = element.style.height; - var elementDimensions = Element.getDimensions(element); - return new Effect.Scale(element, 100, - Object.extend({ scaleContent: false, - scaleX: false, - scaleFrom: 0, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, - restoreAfterFinish: true, - afterSetup: function(effect) { - Element.makeClipping(effect.element); - effect.element.style.height = "0px"; - Element.show(effect.element); - }, - afterFinishInternal: function(effect) { - Element.undoClipping(effect.element); - effect.element.style.height = oldHeight; - } - }, arguments[1] || {}) - ); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || {})); } Effect.SwitchOff = function(element) { element = $(element); - var oldOpacity = Element.getInlineOpacity(element); - return new Effect.Appear(element, { + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ duration: 0.4, from: 0, transition: Effect.Transitions.flicker, @@ -587,211 +645,192 @@ duration: 0.3, scaleFromCenter: true, scaleX: false, scaleContent: false, restoreAfterFinish: true, beforeSetup: function(effect) { - Element.makePositioned(effect.element); - Element.makeClipping(effect.element); + effect.element.makePositioned().makeClipping(); }, - afterFinishInternal: function(effect) { - Element.hide(effect.element); - Element.undoClipping(effect.element); - Element.undoPositioned(effect.element); - Element.setInlineOpacity(effect.element, oldOpacity); + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); } }) } - }); + }, arguments[1] || {})); } Effect.DropOut = function(element) { element = $(element); - var oldTop = element.style.top; - var oldLeft = element.style.left; - var oldOpacity = Element.getInlineOpacity(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; return new Effect.Parallel( - [ new Effect.MoveBy(element, 100, 0, { sync: true }), + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), new Effect.Opacity(element, { sync: true, to: 0.0 }) ], Object.extend( { duration: 0.5, - beforeSetup: function(effect) { - Element.makePositioned(effect.effects[0].element); }, - afterFinishInternal: function(effect) { - Element.hide(effect.effects[0].element); - Element.undoPositioned(effect.effects[0].element); - effect.effects[0].element.style.left = oldLeft; - effect.effects[0].element.style.top = oldTop; - Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } }, arguments[1] || {})); } Effect.Shake = function(element) { element = $(element); - var oldTop = element.style.top; - var oldLeft = element.style.left; - return new Effect.MoveBy(element, 0, 20, - { duration: 0.05, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, -40, - { duration: 0.1, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, 40, - { duration: 0.1, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, -40, - { duration: 0.1, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, 40, - { duration: 0.1, afterFinishInternal: function(effect) { - new Effect.MoveBy(effect.element, 0, -20, - { duration: 0.05, afterFinishInternal: function(effect) { - Element.undoPositioned(effect.element); - effect.element.style.left = oldLeft; - effect.element.style.top = oldTop; + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); }}) }}) }}) }}) }}) }}); } Effect.SlideDown = function(element) { - element = $(element); - Element.cleanWhitespace(element); + element = $(element).cleanWhitespace(); // SlideDown need to have the content of the element wrapped in a container element with fixed height! - var oldInnerBottom = element.firstChild.style.bottom; - var elementDimensions = Element.getDimensions(element); - return new Effect.Scale(element, 100, - Object.extend({ scaleContent: false, + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, scaleX: false, - scaleFrom: 0, - scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, restoreAfterFinish: true, afterSetup: function(effect) { - Element.makePositioned(effect.element.firstChild); - if (window.opera) effect.element.firstChild.style.top = ""; - Element.makeClipping(effect.element); - element.style.height = '0'; - Element.show(element); - }, - afterUpdateInternal: function(effect) { - effect.element.firstChild.style.bottom = - (effect.dims[0] - effect.element.clientHeight) + 'px'; }, - afterFinishInternal: function(effect) { - Element.undoClipping(effect.element); - Element.undoPositioned(effect.element.firstChild); - effect.element.firstChild.style.bottom = oldInnerBottom; } + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } }, arguments[1] || {}) ); } - + Effect.SlideUp = function(element) { - element = $(element); - Element.cleanWhitespace(element); - var oldInnerBottom = element.firstChild.style.bottom; - return new Effect.Scale(element, 0, + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + return new Effect.Scale(element, window.opera ? 0 : 1, Object.extend({ scaleContent: false, scaleX: false, scaleMode: 'box', scaleFrom: 100, restoreAfterFinish: true, - beforeStartInternal: function(effect) { - Element.makePositioned(effect.element.firstChild); - if (window.opera) effect.element.firstChild.style.top = ""; - Element.makeClipping(effect.element); - Element.show(element); + beforeStartInternal: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); }, - afterUpdateInternal: function(effect) { - effect.element.firstChild.style.bottom = - (effect.dims[0] - effect.element.clientHeight) + 'px'; }, - afterFinishInternal: function(effect) { - Element.hide(effect.element); - Element.undoClipping(effect.element); - Element.undoPositioned(effect.element.firstChild); - effect.element.firstChild.style.bottom = oldInnerBottom; } + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom}); + effect.element.down().undoPositioned(); + } }, arguments[1] || {}) ); } +// Bug in opera makes the TD containing this element expand for a instance after finish Effect.Squish = function(element) { - // Bug in opera makes the TD containing this element expand for a instance after finish - return new Effect.Scale(element, window.opera ? 1 : 0, - { restoreAfterFinish: true, - beforeSetup: function(effect) { - Element.makeClipping(effect.element); }, - afterFinishInternal: function(effect) { - Element.hide(effect.element); - Element.undoClipping(effect.element); } + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } }); } Effect.Grow = function(element) { element = $(element); - var options = arguments[1] || {}; - - var elementDimensions = Element.getDimensions(element); - var originalWidth = elementDimensions.width; - var originalHeight = elementDimensions.height; - var oldTop = element.style.top; - var oldLeft = element.style.left; - var oldHeight = element.style.height; - var oldWidth = element.style.width; - var oldOpacity = Element.getInlineOpacity(element); - - var direction = options.direction || 'center'; - var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; - var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal; - var opacityTransition = options.opacityTransition || Effect.Transitions.full; - + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); var initialMoveX, initialMoveY; var moveX, moveY; - switch (direction) { + switch (options.direction) { case 'top-left': initialMoveX = initialMoveY = moveX = moveY = 0; break; case 'top-right': - initialMoveX = originalWidth; + initialMoveX = dims.width; initialMoveY = moveY = 0; - moveX = -originalWidth; + moveX = -dims.width; break; case 'bottom-left': initialMoveX = moveX = 0; - initialMoveY = originalHeight; - moveY = -originalHeight; + initialMoveY = dims.height; + moveY = -dims.height; break; case 'bottom-right': - initialMoveX = originalWidth; - initialMoveY = originalHeight; - moveX = -originalWidth; - moveY = -originalHeight; + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; break; case 'center': - initialMoveX = originalWidth / 2; - initialMoveY = originalHeight / 2; - moveX = -originalWidth / 2; - moveY = -originalHeight / 2; + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; break; } - return new Effect.MoveBy(element, initialMoveY, initialMoveX, { + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, duration: 0.01, - beforeSetup: function(effect) { - Element.hide(effect.element); - Element.makeClipping(effect.element); - Element.makePositioned(effect.element); + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); }, afterFinishInternal: function(effect) { new Effect.Parallel( - [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }), - new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }), + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), new Effect.Scale(effect.element, 100, { - scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, - sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true}) + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) ], Object.extend({ beforeSetup: function(effect) { - effect.effects[0].element.style.height = 0; - Element.show(effect.effects[0].element); - }, + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, afterFinishInternal: function(effect) { - var el = effect.effects[0].element; - var els = el.style; - Element.undoClipping(el); - Element.undoPositioned(el); - els.top = oldTop; - els.left = oldLeft; - els.height = oldHeight; - els.width = originalWidth + 'px'; - Element.setInlineOpacity(el, oldOpacity); + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ) @@ -801,66 +840,54 @@ Effect.Shrink = function(element) { element = $(element); - var options = arguments[1] || {}; - - var originalWidth = element.clientWidth; - var originalHeight = element.clientHeight; - var oldTop = element.style.top; - var oldLeft = element.style.left; - var oldHeight = element.style.height; - var oldWidth = element.style.width; - var oldOpacity = Element.getInlineOpacity(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; - var direction = options.direction || 'center'; - var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; - var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal; - var opacityTransition = options.opacityTransition || Effect.Transitions.none; - + var dims = element.getDimensions(); var moveX, moveY; - switch (direction) { + switch (options.direction) { case 'top-left': moveX = moveY = 0; break; case 'top-right': - moveX = originalWidth; + moveX = dims.width; moveY = 0; break; case 'bottom-left': moveX = 0; - moveY = originalHeight; + moveY = dims.height; break; case 'bottom-right': - moveX = originalWidth; - moveY = originalHeight; + moveX = dims.width; + moveY = dims.height; break; case 'center': - moveX = originalWidth / 2; - moveY = originalHeight / 2; + moveX = dims.width / 2; + moveY = dims.height / 2; break; } return new Effect.Parallel( - [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }), - new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}), - new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }) + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) ], Object.extend({ - beforeStartInternal: function(effect) { - Element.makePositioned(effect.effects[0].element); - Element.makeClipping(effect.effects[0].element); + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); }, afterFinishInternal: function(effect) { - var el = effect.effects[0].element; - var els = el.style; - Element.hide(el); - Element.undoClipping(el); - Element.undoPositioned(el); - els.top = oldTop; - els.left = oldLeft; - els.height = oldHeight; - els.width = oldWidth; - Element.setInlineOpacity(el, oldOpacity); - } + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } }, options) ); } @@ -868,23 +895,24 @@ Effect.Pulsate = function(element) { element = $(element); var options = arguments[1] || {}; - var oldOpacity = Element.getInlineOpacity(element); + var oldOpacity = element.getInlineOpacity(); var transition = options.transition || Effect.Transitions.sinoidal; - var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; reverser.bind(transition); return new Effect.Opacity(element, - Object.extend(Object.extend({ duration: 3.0, from: 0, - afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); } + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } }, options), {transition: reverser})); } Effect.Fold = function(element) { element = $(element); - var originalTop = element.style.top; - var originalLeft = element.style.left; - var originalWidth = element.style.width; - var originalHeight = element.style.height; - Element.makeClipping(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); return new Effect.Scale(element, 5, Object.extend({ scaleContent: false, scaleX: false, @@ -892,13 +920,175 @@ new Effect.Scale(element, 1, { scaleContent: false, scaleY: false, - afterFinishInternal: function(effect) { - Element.hide(effect.element); - Element.undoClipping(effect.element); - effect.element.style.top = originalTop; - effect.element.style.left = originalLeft; - effect.element.style.width = originalWidth; - effect.element.style.height = originalHeight; + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); } }); }}, arguments[1] || {})); -} +}; + +Effect.Morph = Class.create(); +Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: {} + }, arguments[1] || {}); + if (typeof options.style == 'string') { + if(options.style.indexOf(':') == -1) { + var cssText = '', selector = '.' + options.style; + $A(document.styleSheets).reverse().each(function(styleSheet) { + if (styleSheet.cssRules) cssRules = styleSheet.cssRules; + else if (styleSheet.rules) cssRules = styleSheet.rules; + $A(cssRules).reverse().each(function(rule) { + if (selector == rule.selectorText) { + cssText = rule.style.cssText; + throw $break; + } + }); + if (cssText) throw $break; + }); + this.style = cssText.parseStyle(); + options.afterFinishInternal = function(effect){ + effect.element.addClassName(effect.options.style); + effect.transforms.each(function(transform) { + if(transform.style != 'opacity') + effect.element.style[transform.style] = ''; + }); + } + } else this.style = options.style.parseStyle(); + } else this.style = $H(options.style) + this.start(options); + }, + setup: function(){ + function parseColor(color){ + if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ) + }); + } + this.transforms = this.style.map(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if(value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if(property == 'opacity') { + value = parseFloat(value); + if(Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + } else if(Element.CSS_LENGTH.test(value)) { + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); + value = parseFloat(components[1]); + unit = (components.length == 3) ? components[2] : null; + } + + var originalValue = this.element.getStyle(property); + return { + style: property.camelize(), + originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: unit=='color' ? parseColor(value) : value, + unit: unit + }; + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ) + }); + }, + update: function(position) { + var style = {}, transform, i = this.transforms.length; + while(i--) + style[(transform = this.transforms[i]).style] = + transform.unit=='color' ? '#'+ + (Math.round(transform.originalValue[0]+ + (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + + (Math.round(transform.originalValue[1]+ + (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + + (Math.round(transform.originalValue[2]+ + (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : + transform.originalValue + Math.round( + ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit; + this.element.setStyle(style, true); + } +}); + +Effect.Transform = Class.create(); +Object.extend(Effect.Transform.prototype, { + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || {}; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + var data = $H(track).values().first(); + this.tracks.push($H({ + ids: $H(track).keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var elements = [$(track.ids) || $$(track.ids)].flatten(); + return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = $w( + 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + + 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + + 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + + 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + + 'fontSize fontWeight height left letterSpacing lineHeight ' + + 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ + 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + + 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + + 'right textIndent top width wordSpacing zIndex'); + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.prototype.parseStyle = function(){ + var element = document.createElement('div'); + element.innerHTML = '
'; + var style = element.childNodes[0].style, styleRules = $H(); + + Element.CSS_PROPERTIES.each(function(property){ + if(style[property]) styleRules[property] = style[property]; + }); + if(Prototype.Browser.IE && this.indexOf('opacity') > -1) { + styleRules.opacity = this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]; + } + return styleRules; +}; + +Element.morph = function(element, style) { + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {})); + return element; +}; + +['getInlineOpacity','forceRerendering','setContentZoom', + 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( + function(f) { Element.Methods[f] = Element[f]; } +); + +Element.Methods.visualEffect = function(element, effect, options) { + s = effect.dasherize().camelize(); + effect_class = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[effect_class](element, options); + return $(element); +}; + +Element.addMethods(); \ No newline at end of file Index: scriptaculous/unittest.js =================================================================== --- scriptaculous/unittest.js (revision 26) +++ scriptaculous/unittest.js (revision 39) @@ -1,20 +1,27 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// (c) 2005 Jon Tirsen (http://www.tirsen.com) -// (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/) +// script.aculo.us unittest.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) +// (c) 2005-2007 Michael Schuerig (http://www.schuerig.de/michael/) // -// See scriptaculous.js for full license. +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ // experimental, Firefox-only Event.simulateMouse = function(element, eventName) { var options = Object.extend({ pointerX: 0, pointerY: 0, - buttons: 0 + buttons: 0, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false }, arguments[2] || {}); var oEvent = document.createEvent("MouseEvents"); oEvent.initMouseEvent(eventName, true, true, document.defaultView, options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, - false, false, false, false, 0, $(element)); + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, $(element)); if(this.mark) Element.remove(this.mark); this.mark = document.createElement('div'); @@ -64,25 +71,8 @@ Test.Unit = {}; // security exception workaround -Test.Unit.inspect = function(obj) { - var info = []; +Test.Unit.inspect = Object.inspect; - if(typeof obj=="string" || - typeof obj=="number") { - return obj; - } else { - for(property in obj) - if(typeof obj[property]!="function") - info.push(property + ' => ' + - (typeof obj[property] == "string" ? - '"' + obj[property] + '"' : - obj[property])); - } - - return ("'" + obj + "' #" + typeof obj + - ": {" + info.join(", ") + "}"); -} - Test.Unit.Logger = Class.create(); Test.Unit.Logger.prototype = { initialize: function(log) { @@ -97,6 +87,7 @@ this.lastLogLine = document.createElement('tr'); this.statusCell = document.createElement('td'); this.nameCell = document.createElement('td'); + this.nameCell.className = "nameCell"; this.nameCell.appendChild(document.createTextNode(testName)); this.messageCell = document.createElement('td'); this.lastLogLine.appendChild(this.statusCell); @@ -109,6 +100,7 @@ this.lastLogLine.className = status; this.statusCell.innerHTML = status; this.messageCell.innerHTML = this._toHTML(summary); + this.addLinksToResults(); }, message: function(message) { if (!this.log) return; @@ -130,6 +122,16 @@ }, _toHTML: function(txt) { return txt.escapeHTML().replace(/\n/g,"
"); + }, + addLinksToResults: function(){ + $$("tr.failed .nameCell").each( function(td){ // todo: limit to children of this.log + td.title = "Run only this test" + Event.observe(td, 'click', function(){ window.location.search = "?tests=" + td.innerHTML;}); + }); + $$("tr.passed .nameCell").each( function(td){ // todo: limit to children of this.log + td.title = "Run all tests" + Event.observe(td, 'click', function(){ window.location.search = "";}); + }); } } @@ -140,6 +142,7 @@ testLog: 'testlog' }, arguments[1] || {}); this.options.resultsURL = this.parseResultsURLQueryParameter(); + this.options.tests = this.parseTestsQueryParameter(); if (this.options.testLog) { this.options.testLog = $(this.options.testLog) || null; } @@ -157,7 +160,11 @@ this.tests = []; for(var testcase in testcases) { if(/^test/.test(testcase)) { - this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"])); + this.tests.push( + new Test.Unit.Testcase( + this.options.context ? ' -> ' + this.options.titles[testcase] : testcase, + testcases[testcase], testcases["setup"], testcases["teardown"] + )); } } } @@ -169,6 +176,11 @@ parseResultsURLQueryParameter: function() { return window.location.search.parseQuery()["resultsURL"]; }, + parseTestsQueryParameter: function(){ + if (window.location.search.parseQuery()["tests"]){ + return window.location.search.parseQuery()["tests"].split(','); + }; + }, // Returns: // "ERROR" if there was an error, // "FAILURE" if there was a failure, or @@ -228,6 +240,7 @@ errors += this.tests[i].errors; } return ( + (this.options.context ? this.options.context + ': ': '') + this.tests.length + " tests, " + assertions + " assertions, " + failures + " failures, " + @@ -257,6 +270,9 @@ this.failures++; this.messages.push("Failure: " + message); }, + info: function(message) { + this.messages.push("Info: " + message); + }, error: function(error) { this.errors++; this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")"); @@ -279,18 +295,54 @@ '", actual "' + Test.Unit.inspect(actual) + '"'); } catch(e) { this.error(e); } }, + assertInspect: function(expected, actual) { + var message = arguments[2] || "assertInspect"; + try { (expected == actual.inspect()) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertEnumEqual: function(expected, actual) { + var message = arguments[2] || "assertEnumEqual"; + try { $A(expected).length == $A(actual).length && + expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ? + this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + + ', actual ' + Test.Unit.inspect(actual)); } + catch(e) { this.error(e); } + }, assertNotEqual: function(expected, actual) { var message = arguments[2] || "assertNotEqual"; try { (expected != actual) ? this.pass() : this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); } catch(e) { this.error(e); } }, + assertIdentical: function(expected, actual) { + var message = arguments[2] || "assertIdentical"; + try { (expected === actual) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, + assertNotIdentical: function(expected, actual) { + var message = arguments[2] || "assertNotIdentical"; + try { !(expected === actual) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, assertNull: function(obj) { var message = arguments[1] || 'assertNull' try { (obj==null) ? this.pass() : this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); } catch(e) { this.error(e); } }, + assertMatch: function(expected, actual) { + var message = arguments[2] || 'assertMatch'; + var regex = new RegExp(expected); + try { (regex.exec(actual)) ? this.pass() : + this.fail(message + ' : regex: "' + Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); } + catch(e) { this.error(e); } + }, assertHidden: function(element) { var message = arguments[1] || 'assertHidden'; this.assertEqual("none", element.style.display, message); @@ -299,6 +351,22 @@ var message = arguments[1] || 'assertNotNull'; this.assert(object != null, message); }, + assertType: function(expected, actual) { + var message = arguments[2] || 'assertType'; + try { + (actual.constructor == expected) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + (actual.constructor) + '"'); } + catch(e) { this.error(e); } + }, + assertNotOfType: function(expected, actual) { + var message = arguments[2] || 'assertNotOfType'; + try { + (actual.constructor != expected) ? this.pass() : + this.fail(message + ': expected "' + Test.Unit.inspect(expected) + + '", actual "' + (actual.constructor) + '"'); } + catch(e) { this.error(e); } + }, assertInstanceOf: function(expected, actual) { var message = arguments[2] || 'assertInstanceOf'; try { @@ -313,6 +381,63 @@ this.fail(message + ": object was an instance of the not expected type"); } catch(e) { this.error(e); } }, + assertRespondsTo: function(method, obj) { + var message = arguments[2] || 'assertRespondsTo'; + try { + (obj[method] && typeof obj[method] == 'function') ? this.pass() : + this.fail(message + ": object doesn't respond to [" + method + "]"); } + catch(e) { this.error(e); } + }, + assertReturnsTrue: function(method, obj) { + var message = arguments[2] || 'assertReturnsTrue'; + try { + var m = obj[method]; + if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)]; + m() ? this.pass() : + this.fail(message + ": method returned false"); } + catch(e) { this.error(e); } + }, + assertReturnsFalse: function(method, obj) { + var message = arguments[2] || 'assertReturnsFalse'; + try { + var m = obj[method]; + if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)]; + !m() ? this.pass() : + this.fail(message + ": method returned true"); } + catch(e) { this.error(e); } + }, + assertRaise: function(exceptionName, method) { + var message = arguments[2] || 'assertRaise'; + try { + method(); + this.fail(message + ": exception expected but none was raised"); } + catch(e) { + ((exceptionName == null) || (e.name==exceptionName)) ? this.pass() : this.error(e); + } + }, + assertElementsMatch: function() { + var expressions = $A(arguments), elements = $A(expressions.shift()); + if (elements.length != expressions.length) { + this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions'); + return false; + } + elements.zip(expressions).all(function(pair, index) { + var element = $(pair.first()), expression = pair.last(); + if (element.match(expression)) return true; + this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect()); + }.bind(this)) && this.pass(); + }, + assertElementMatches: function(element, expression) { + this.assertElementsMatch([element], expression); + }, + benchmark: function(operation, iterations) { + var startAt = new Date(); + (iterations || 1).times(operation); + var timeTaken = ((new Date())-startAt); + this.info((arguments[2] || 'Operation') + ' finished ' + + iterations + ' iterations in ' + (timeTaken/1000)+'s' ); + return timeTaken; + }, _isVisible: function(element) { element = $(element); if(!element.parentNode) return true; @@ -327,6 +452,14 @@ }, assertVisible: function(element) { this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1])); + }, + benchmark: function(operation, iterations) { + var startAt = new Date(); + (iterations || 1).times(operation); + var timeTaken = ((new Date())-startAt); + this.info((arguments[2] || 'Operation') + ' finished ' + + iterations + ' iterations in ' + (timeTaken/1000)+'s' ); + return timeTaken; } } @@ -335,7 +468,17 @@ initialize: function(name, test, setup, teardown) { Test.Unit.Assertions.prototype.initialize.bind(this)(); this.name = name; - this.test = test || function() {}; + + if(typeof test == 'string') { + test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,'); + test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)'); + this.test = function() { + eval('with(this){'+test+'}'); + } + } else { + this.test = test || function() {}; + } + this.setup = setup || function() {}; this.teardown = teardown || function() {}; this.isWaiting = false; @@ -360,4 +503,62 @@ } catch(e) { this.error(e); } } -}); \ No newline at end of file +}); + +// *EXPERIMENTAL* BDD-style testing to please non-technical folk +// This draws many ideas from RSpec http://rspec.rubyforge.org/ + +Test.setupBDDExtensionMethods = function(){ + var METHODMAP = { + shouldEqual: 'assertEqual', + shouldNotEqual: 'assertNotEqual', + shouldEqualEnum: 'assertEnumEqual', + shouldBeA: 'assertType', + shouldNotBeA: 'assertNotOfType', + shouldBeAn: 'assertType', + shouldNotBeAn: 'assertNotOfType', + shouldBeNull: 'assertNull', + shouldNotBeNull: 'assertNotNull', + + shouldBe: 'assertReturnsTrue', + shouldNotBe: 'assertReturnsFalse', + shouldRespondTo: 'assertRespondsTo' + }; + Test.BDDMethods = {}; + for(m in METHODMAP) { + Test.BDDMethods[m] = eval( + 'function(){'+ + 'var args = $A(arguments);'+ + 'var scope = args.shift();'+ + 'scope.'+METHODMAP[m]+'.apply(scope,(args || []).concat([this])); }'); + } + [Array.prototype, String.prototype, Number.prototype].each( + function(p){ Object.extend(p, Test.BDDMethods) } + ); +} + +Test.context = function(name, spec, log){ + Test.setupBDDExtensionMethods(); + + var compiledSpec = {}; + var titles = {}; + for(specName in spec) { + switch(specName){ + case "setup": + case "teardown": + compiledSpec[specName] = spec[specName]; + break; + default: + var testName = 'test'+specName.gsub(/\s+/,'-').camelize(); + var body = spec[specName].toString().split('\n').slice(1); + if(/^\{/.test(body[0])) body = body.slice(1); + body.pop(); + body = body.map(function(statement){ + return statement.strip() + }); + compiledSpec[testName] = body.join('\n'); + titles[testName] = specName; + } + } + new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name }); +}; \ No newline at end of file Index: scriptaculous/scriptaculous.js =================================================================== --- scriptaculous/scriptaculous.js (revision 26) +++ scriptaculous/scriptaculous.js (revision 39) @@ -1,4 +1,6 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// script.aculo.us scriptaculous.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the @@ -18,30 +20,38 @@ // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// For details, see the script.aculo.us web site: http://script.aculo.us/ var Scriptaculous = { - Version: '1.5_rc5', + Version: '1.7.1_beta3', require: function(libraryName) { // inserting via DOM fails in Safari 2.0, so brute force approach document.write(''); }, + REQUIRED_PROTOTYPE: '1.5.1', load: function() { - if((typeof Prototype=='undefined') || - parseFloat(Prototype.Version.split(".")[0] + "." + - Prototype.Version.split(".")[1]) < 1.4) - throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0"); - var scriptTags = document.getElementsByTagName("script"); - for(var i=0;i= " + + Scriptaculous.REQUIRED_PROTOTYPE); + + $A(document.getElementsByTagName("script")).findAll( function(s) { + return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/)) + }).each( function(s) { + var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,''); + var includes = s.src.match(/\?.*load=([a-z,]*)/); + (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each( + function(include) { Scriptaculous.require(path+include+'.js') }); + }); } } Index: scriptaculous/dragdrop.js =================================================================== --- scriptaculous/dragdrop.js (revision 26) +++ scriptaculous/dragdrop.js (revision 39) @@ -1,8 +1,13 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// script.aculo.us dragdrop.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) // -// See scriptaculous.js for full license. +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ -/*--------------------------------------------------------------------------*/ +if(typeof Effect == 'undefined') + throw("dragdrop.js requires including script.aculo.us' effects.js library"); var Droppables = { drops: [], @@ -15,7 +20,8 @@ element = $(element); var options = Object.extend({ greedy: true, - hoverclass: null + hoverclass: null, + tree: false }, arguments[1] || {}); // cache containers @@ -32,27 +38,41 @@ if(options.accept) options.accept = [options.accept].flatten(); - // Element.makePositioned(element); // fix IE + Element.makePositioned(element); // fix IE options.element = element; this.drops.push(options); }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, isContained: function(element, drop) { - var parentNode = element.parentNode; - return drop._containers.detect(function(c) { return parentNode == c }); + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); }, - - isAffected: function(pX, pY, element, drop) { + + isAffected: function(point, element, drop) { return ( - (drop.element!=element) && (drop.element.parentNode) && + (drop.element!=element) && ((!drop._containers) || this.isContained(element, drop)) && ((!drop.accept) || (Element.classNames(element).detect( function(v) { return drop.accept.include(v) } ) )) && - ((!drop.checkDroppableIsntContained) || !Element.contains(element, drop.element)) && - Position.within(drop.element, pX, pY) ); + Position.within(drop.element, point[0], point[1]) ); }, deactivate: function(drop) { @@ -62,40 +82,40 @@ }, activate: function(drop) { - if(this.last_active) this.deactivate(this.last_active); if(drop.hoverclass) Element.addClassName(drop.element, drop.hoverclass); this.last_active = drop; }, - show: function(event, element) { + show: function(point, element) { if(!this.drops.length) return; - var pX = Event.pointerX(event); - var pY = Event.pointerY(event); - Position.prepare(); - - var i = this.drops.length-1; do { - var drop = this.drops[i]; - if(this.isAffected(pX, pY, element, drop)) { - if(drop.onHover) - drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); - if(drop.greedy) { - this.activate(drop); - return; - } - } - } while (i--); + var affected = []; if(this.last_active) this.deactivate(this.last_active); + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) { + drop = Droppables.findDeepestChild(affected); + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + Droppables.activate(drop); + } }, fire: function(event, element) { if(!this.last_active) return; Position.prepare(); - if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active)) - if (this.last_active.onDrop) - this.last_active.onDrop(element, this.last_active.element, event); + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) { + this.last_active.onDrop(element, this.last_active.element, event); + return true; + } }, reset: function() { @@ -105,21 +125,93 @@ } var Draggables = { + drags: [], observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + addObserver: function(observer) { this.observers.push(observer); this._cacheObserverCallbacks(); }, + removeObserver: function(element) { // element instead of observer fixes mem leaks this.observers = this.observers.reject( function(o) { return o.element==element }); this._cacheObserverCallbacks(); }, + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' if(this[eventName+'Count'] > 0) this.observers.each( function(o) { if(o[eventName]) o[eventName](eventName, draggable, event); }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); }, + _cacheObserverCallbacks: function() { ['onStart','onEnd','onDrag'].each( function(eventName) { Draggables[eventName+'Count'] = Draggables.observers.select( @@ -132,109 +224,184 @@ /*--------------------------------------------------------------------------*/ var Draggable = Class.create(); +Draggable._dragging = {}; + Draggable.prototype = { initialize: function(element) { - var options = Object.extend({ + var defaults = { handle: false, - starteffect: function(element) { - new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); - }, reverteffect: function(element, top_offset, left_offset) { var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; - new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur}); + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); }, - endeffect: function(element) { - new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); + endeffect: function(element) { + var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); }, zindex: 1000, revert: false, - snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] } - }, arguments[1] || {}); + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || typeof arguments[1].endeffect == 'undefined') + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || {}); - this.element = $(element); + this.element = $(element); + if(options.handle && (typeof options.handle == 'string')) - this.handle = Element.childrenWithClassName(this.element, options.handle)[0]; - + this.handle = this.element.down('.'+options.handle, 0); + if(!this.handle) this.handle = $(options.handle); if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } - this.offsetX = 0; - this.offsetY = 0; - this.originalLeft = this.currentLeft(); - this.originalTop = this.currentTop(); - this.originalX = this.element.offsetLeft; - this.originalY = this.element.offsetTop; + Element.makePositioned(this.element); // fix IE - this.options = options; + this.delta = this.currentDelta(); + this.options = options; + this.dragging = false; - this.active = false; - this.dragging = false; - - this.eventMouseDown = this.startDrag.bindAsEventListener(this); - this.eventMouseUp = this.endDrag.bindAsEventListener(this); - this.eventMouseMove = this.update.bindAsEventListener(this); - this.eventKeypress = this.keyPress.bindAsEventListener(this); + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); - this.registerEvents(); + Draggables.register(this); }, + destroy: function() { Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); - this.unregisterEvents(); + Draggables.unregister(this); }, - registerEvents: function() { - //if(!Draggable.eventsRegistered) { - //Draggable.eventsRegistered = true; - Event.observe(document, "mouseup", this.eventMouseUp); - Event.observe(document, "mousemove", this.eventMouseMove); - Event.observe(document, "keypress", this.eventKeypress); - Event.observe(this.handle, "mousedown", this.eventMouseDown); - //} + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); }, - unregisterEvents: function() { - //if(!Draggable.eventsRegistered) { - //Draggable.eventsRegistered = false; - //if(!this.active) return; - Event.stopObserving(document, "mouseup", this.eventMouseUp); - Event.stopObserving(document, "mousemove", this.eventMouseMove); - Event.stopObserving(document, "keypress", this.eventKeypress); - //} - }, - currentLeft: function() { - return parseInt(this.element.style.left || '0'); - }, - currentTop: function() { - return parseInt(this.element.style.top || '0') - }, - startDrag: function(event) { - if(Event.isLeftClick(event)) { - + + initDrag: function(event) { + if(typeof Draggable._dragging[this.element] != 'undefined' && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { // abort on form elements, fixes a Firefox issue var src = Event.element(event); - if(src.tagName && ( - src.tagName=='INPUT' || - src.tagName=='SELECT' || - src.tagName=='BUTTON' || - src.tagName=='TEXTAREA')) return; + if((tag_name = src.tagName.toUpperCase()) && ( + tag_name=='INPUT' || + tag_name=='SELECT' || + tag_name=='OPTION' || + tag_name=='BUTTON' || + tag_name=='TEXTAREA')) return; + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); - // this.registerEvents(); - Element.makePositioned(this.element); // fix IE - - this.element.wasDragged = false; - - this.active = true; - var pointer = [Event.pointerX(event), Event.pointerY(event)]; - var offsets = Position.cumulativeOffset(this.element); - this.offsetX = (pointer[0] - offsets[0]); - this.offsetY = (pointer[1] - offsets[1]); + Draggables.activate(this); Event.stop(event); } }, + + startDrag: function(event) { + this.dragging = true; + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + + if(!this.options.quiet){ + Position.prepare(); + Droppables.show(pointer, this.element); + } + + Draggables.notify('onDrag', this, event); + + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(Prototype.Browser.WebKit) window.scrollBy(0,0); + + Event.stop(event); + }, + finishDrag: function(event, success) { - // this.unregisterEvents(); - - this.active = false; this.dragging = false; - this.element.wasDragged = true; + + if(this.options.quiet){ + Position.prepare(); + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + Droppables.show(pointer, this.element); + } if(this.options.ghosting) { Position.relativize(this.element); @@ -242,19 +409,24 @@ this._clone = null; } - if(success) Droppables.fire(event, this.element); + var dropped = false; + if(success) { + dropped = Droppables.fire(event, this.element); + if (!dropped) dropped = false; + } + if(dropped && this.options.onDropped) this.options.onDropped(this.element); Draggables.notify('onEnd', this, event); var revert = this.options.revert; if(revert && typeof revert == 'function') revert = revert(this.element); - + + var d = this.currentDelta(); if(revert && this.options.reverteffect) { - this.options.reverteffect(this.element, - this.currentTop()-this.originalTop, - this.currentLeft()-this.originalLeft); + if (dropped == 0 || revert != 'failure') + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); } else { - this.originalLeft = this.currentLeft(); - this.originalTop = this.currentTop(); + this.delta = d; } if(this.options.zindex) @@ -262,104 +434,138 @@ if(this.options.endeffect) this.options.endeffect(this.element); - - + + Draggables.deactivate(this); Droppables.reset(); }, + keyPress: function(event) { - if(this.active) { - if(event.keyCode==Event.KEY_ESC) { - Element.undoPositioned(this.element); - this.finishDrag(event, false); - Event.stop(event); - } - } + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); }, + endDrag: function(event) { - Element.undoPositioned(this.element); - if(this.active && this.dragging) { - this.finishDrag(event, true); - Event.stop(event); + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; } - this.active = false; - this.dragging = false; - }, - draw: function(event) { - var pointer = [Event.pointerX(event), Event.pointerY(event)]; - var offsets = Position.cumulativeOffset(this.element); - offsets[0] -= this.currentLeft(); - offsets[1] -= this.currentTop(); - var style = this.element.style; - var pos = [ - (pointer[0] - offsets[0] - this.offsetX), - (pointer[1] - offsets[1] - this.offsetY)]; + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + if(this.options.snap) { if(typeof this.options.snap == 'function') { - pos = this.options.snap(pos[0],pos[1]); + p = this.options.snap(p[0],p[1],this); } else { - var draggable = this; if(this.options.snap instanceof Array) { - pos = pos.collect( function(v, i) { - return Math.round(v/draggable.options.snap[i])*draggable.options.snap[i] }) + p = p.map( function(v, i) { + return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) } else { - pos = pos.collect( function(v) { - return Math.round(v/draggable.options.snap)*draggable.options.snap }) + p = p.map( function(v) { + return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) } }} + var style = this.element.style; if((!this.options.constraint) || (this.options.constraint=='horizontal')) - style.left = pos[0] + "px"; + style.left = p[0] + "px"; if((!this.options.constraint) || (this.options.constraint=='vertical')) - style.top = pos[1] + "px"; + style.top = p[1] + "px"; + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering }, - update: function(event) { - if(this.active) { - if(!this.dragging) { - if(this.options.onStartDrag) this.options.onStartDrag(this.element); - - var style = this.element.style; - this.dragging = true; - - if(Element.getStyle(this.element,'position')=='') - style.position = "relative"; - - if(this.options.zindex) { - this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); - style.zIndex = this.options.zindex; + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); } - - if(this.options.ghosting) { - this._clone = this.element.cloneNode(true); - Position.absolutize(this.element); - this.element.parentNode.insertBefore(this._clone, this.element); - } - - Draggables.notify('onStart', this, event); - if(this.options.starteffect) this.options.starteffect(this.element); } - - Droppables.show(event, this.element); - Draggables.notify('onDrag', this, event); - this.draw(event); - if(this.options.change) this.options.change(this); - - // fix AppleWebKit rendering - if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); - - Event.stop(event); - } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; } } -function rollingStatus(chr, maxLength) { - var txt = window.status + chr; - if(txt.length > maxLength) txt = txt.substr(txt.length - maxLength); - window.status = txt; -} - /*--------------------------------------------------------------------------*/ var SortableObserver = Class.create(); @@ -369,9 +575,11 @@ this.observer = observer; this.lastValue = Sortable.serialize(this.element); }, + onStart: function() { this.lastValue = Sortable.serialize(this.element); }, + onEnd: function() { Sortable.unmark(); if(this.lastValue != Sortable.serialize(this.element)) @@ -380,36 +588,62 @@ } var Sortable = { - sortables: new Array(), - options: function(element){ - element = $(element); - return this.sortables.detect(function(s) { return s.element == element }); + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: {}, + + _findRootElement: function(element) { + while (element.tagName.toUpperCase() != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + destroy: function(element){ - element = $(element); - this.sortables.findAll(function(s) { return s.element == element }).each(function(s){ + var s = Sortable.options(element); + + if(s) { Draggables.removeObserver(s.element); s.droppables.each(function(d){ Droppables.remove(d) }); s.draggables.invoke('destroy'); - }); - this.sortables = this.sortables.reject(function(s) { return s.element == element }); + + delete Sortable.sortables[s.element.id]; + } }, + create: function(element) { - element = $(element); var options = Object.extend({ element: element, tag: 'li', // assumes li children, override with tag: 'tagname' dropOnEmpty: false, - tree: false, // fixme: unimplemented + tree: false, + treeTag: 'ul', overlap: 'vertical', // one of 'vertical', 'horizontal' constraint: 'vertical', // one of 'vertical', 'horizontal', false containment: element, // also takes array of elements (or id's); or false handle: false, // or a CSS class only: false, + delay: 0, hoverclass: null, ghosting: false, - format: null, + quiet: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + + // these take arrays of elements or ids and can be + // used for better initialization performance + elements: false, + handles: false, + onChange: Prototype.emptyFunction, onUpdate: Prototype.emptyFunction }, arguments[1] || {}); @@ -420,6 +654,11 @@ // build options for the draggables var options_for_draggable = { revert: true, + quiet: options.quiet, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, ghosting: options.ghosting, constraint: options.constraint, handle: options.handle }; @@ -445,10 +684,17 @@ var options_for_droppable = { overlap: options.overlap, containment: options.containment, + tree: options.tree, hoverclass: options.hoverclass, - onHover: Sortable.onHover, - greedy: !options.dropOnEmpty + onHover: Sortable.onHover } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } // fix for gecko engine Element.cleanWhitespace(element); @@ -456,27 +702,32 @@ options.draggables = []; options.droppables = []; - // make it so - // drop on empty handling - if(options.dropOnEmpty) { - Droppables.add(element, - {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false}); + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); options.droppables.push(element); } - (this.findElements(element, options) || []).each( function(e) { - // handles are per-draggable - var handle = options.handle ? - Element.childrenWithClassName(e, options.handle)[0] : e; + (options.elements || this.findElements(element, options) || []).each( function(e,i) { + var handle = options.handles ? $(options.handles[i]) : + (options.handle ? $(e).getElementsByClassName(options.handle)[0] : e); options.draggables.push( new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; options.droppables.push(e); }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } // keep reference - this.sortables.push(options); + this.sortables[element.id] = options; // for onupdate Draggables.addObserver(new SortableObserver(element, options.onUpdate)); @@ -485,23 +736,21 @@ // return all suitable-for-sortable elements in a guaranteed order findElements: function(element, options) { - if(!element.hasChildNodes()) return null; - var elements = []; - $A(element.childNodes).each( function(e) { - if(e.tagName && e.tagName==options.tag.toUpperCase() && - (!options.only || (Element.hasClassName(e, options.only)))) - elements.push(e); - if(options.tree) { - var grandchildren = this.findElements(e, options); - if(grandchildren) elements.push(grandchildren); - } - }); - - return (elements.length>0 ? elements.flatten() : null); + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, onHover: function(element, dropon, overlap) { - if(overlap>0.5) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { Sortable.mark(dropon, 'before'); if(dropon.previousSibling != element) { var oldParentNode = element.parentNode; @@ -524,18 +773,42 @@ } } }, - - onEmptyHover: function(element, dropon) { - if(element.parentNode!=dropon) { - var oldParentNode = element.parentNode; - dropon.appendChild(element); + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + Sortable.options(oldParentNode).onChange(element); - Sortable.options(dropon).onChange(element); + droponOptions.onChange(element); } }, unmark: function() { - if(Sortable._marker) Element.hide(Sortable._marker); + if(Sortable._marker) Sortable._marker.hide(); }, mark: function(dropon, position) { @@ -544,38 +817,154 @@ if(sortable && !sortable.ghosting) return; if(!Sortable._marker) { - Sortable._marker = $('dropmarker') || document.createElement('DIV'); - Element.hide(Sortable._marker); - Element.addClassName(Sortable._marker, 'dropmarker'); - Sortable._marker.style.position = 'absolute'; + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); } var offsets = Position.cumulativeOffset(dropon); - Sortable._marker.style.left = offsets[0] + 'px'; - Sortable._marker.style.top = offsets[1] + 'px'; + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); if(position=='after') if(sortable.overlap == 'horizontal') - Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px'; + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); else - Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); - Element.show(Sortable._marker); + Sortable._marker.show(); }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); - serialize: function(element) { + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { element = $(element); var sortableOptions = this.options(element); var options = Object.extend({ - tag: sortableOptions.tag, + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, only: sortableOptions.only, name: element.id, - format: sortableOptions.format || /^[^_]*_(.*)$/ + format: sortableOptions.format }, arguments[1] || {}); - var ret = $(this.findElements(element, options) || []).collect( function(item) { - return (encodeURIComponent(options.name) + "[]=" + - encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : '')); - }).join("&"); - return ret; + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + } + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || {}); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || {}); + + var nodeMap = {}; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || {}); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } } -} \ No newline at end of file +} + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +} Index: scriptaculous/slider.js =================================================================== --- scriptaculous/slider.js (revision 26) +++ scriptaculous/slider.js (revision 39) @@ -1,25 +1,9 @@ -// Copyright (c) 2005 Marty Haught, Thomas Fuchs +// script.aculo.us slider.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Marty Haught, Thomas Fuchs // -// See http://script.aculo.us for more info -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ if(!Control) var Control = {}; Control.Slider = Class.create(); @@ -65,6 +49,12 @@ this.trackLength = this.maximumOffset() - this.minimumOffset(); + this.handleLength = this.isVertical() ? + (this.handles[0].offsetHeight != 0 ? + this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) : + (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth : + this.handles[0].style.width.replace(/px$/,"")); + this.active = false; this.dragging = false; this.disabled = false; @@ -136,8 +126,8 @@ }, setValue: function(sliderValue, handleIdx){ if(!this.active) { - this.activeHandle = this.handles[handleIdx]; - this.activeHandleIdx = handleIdx; + this.activeHandleIdx = handleIdx || 0; + this.activeHandle = this.handles[this.activeHandleIdx]; this.updateStyles(); } handleIdx = handleIdx || this.activeHandleIdx || 0; @@ -162,10 +152,13 @@ handleIdx || this.activeHandleIdx || 0); }, translateToPx: function(value) { - return Math.round((this.trackLength / (this.range.end - this.range.start)) * (value - this.range.start)) + "px"; + return Math.round( + ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * + (value - this.range.start)) + "px"; }, translateToValue: function(offset) { - return ((offset/this.trackLength) * (this.range.end - this.range.start)) + this.range.start; + return ((offset/(this.trackLength-this.handleLength) * + (this.range.end-this.range.start)) + this.range.start); }, getRange: function(range) { var v = this.values.sortBy(Prototype.K); @@ -176,8 +169,11 @@ return(this.isVertical() ? this.alignY : this.alignX); }, maximumOffset: function(){ - return(this.isVertical() ? - this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX); + return(this.isVertical() ? + (this.track.offsetHeight != 0 ? this.track.offsetHeight : + this.track.style.height.replace(/px$/,"")) - this.alignY : + (this.track.offsetWidth != 0 ? this.track.offsetWidth : + this.track.style.width.replace(/px$/,"")) - this.alignY); }, isVertical: function(){ return (this.axis == 'vertical'); @@ -196,10 +192,10 @@ setSpan: function(span, range) { if(this.isVertical()) { span.style.top = this.translateToPx(range.start); - span.style.height = this.translateToPx(range.end - range.start); + span.style.height = this.translateToPx(range.end - range.start + this.range.start); } else { span.style.left = this.translateToPx(range.start); - span.style.width = this.translateToPx(range.end - range.start); + span.style.width = this.translateToPx(range.end - range.start + this.range.start); } }, updateStyles: function() { @@ -213,24 +209,30 @@ var handle = Event.element(event); var pointer = [Event.pointerX(event), Event.pointerY(event)]; - if(handle==this.track) { + var track = handle; + if(track==this.track) { var offsets = Position.cumulativeOffset(this.track); - this.event = event; + this.event = event; this.setValue(this.translateToValue( - this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0] + (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2) )); + var offsets = Position.cumulativeOffset(this.activeHandle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); } else { // find the handle (prevents issues with Safari) while((this.handles.indexOf(handle) == -1) && handle.parentNode) handle = handle.parentNode; - - this.activeHandle = handle; - this.activeHandleIdx = this.handles.indexOf(this.activeHandle); - this.updateStyles(); - - var offsets = Position.cumulativeOffset(this.activeHandle); - this.offsetX = (pointer[0] - offsets[0]); - this.offsetY = (pointer[1] - offsets[1]); + + if(this.handles.indexOf(handle)!=-1) { + this.activeHandle = handle; + this.activeHandleIdx = this.handles.indexOf(this.activeHandle); + this.updateStyles(); + + var offsets = Position.cumulativeOffset(this.activeHandle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + } } } Event.stop(event); @@ -240,8 +242,7 @@ if(this.active) { if(!this.dragging) this.dragging = true; this.draw(event); - // fix AppleWebKit rendering - if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + if(Prototype.Browser.WebKit) window.scrollBy(0,0); Event.stop(event); } }, @@ -252,7 +253,8 @@ pointer[1] -= this.offsetY + offsets[1]; this.event = event; this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] )); - if(this.initialized && this.options.onSlide) this.options.onSlide(this.values.length>1 ? this.values : this.value, this); + if(this.initialized && this.options.onSlide) + this.options.onSlide(this.values.length>1 ? this.values : this.value, this); }, endDrag: function(event) { if(this.active && this.dragging) { @@ -272,4 +274,4 @@ this.options.onChange(this.values.length>1 ? this.values : this.value, this); this.event = null; } -} +} \ No newline at end of file Index: scriptaculous/controls.js =================================================================== --- scriptaculous/controls.js (revision 26) +++ scriptaculous/controls.js (revision 39) @@ -1,12 +1,15 @@ -// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) -// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// script.aculo.us controls.js v1.7.1_beta3, Fri May 25 17:19:41 +0200 2007 + +// Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) // Contributors: // Richard Livsey // Rahul Bhargava // Rob Wills // -// See scriptaculous.js for full license. +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ // Autocompleter.Base handles all the autocompletion functionality // that's independent of the data source for autocompletion. This @@ -33,11 +36,15 @@ // useful when one of the tokens is \n (a newline), as it // allows smart autocompletion after linebreaks. +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + var Autocompleter = {} Autocompleter.Base = function() {}; Autocompleter.Base.prototype = { baseInitialize: function(element, update, options) { - this.element = $(element); + element = $(element) + this.element = element; this.update = $(update); this.hasFocus = false; this.changed = false; @@ -45,27 +52,30 @@ this.index = 0; this.entryCount = 0; - if (this.setOptions) + if(this.setOptions) this.setOptions(options); else this.options = options || {}; - + this.options.paramName = this.options.paramName || this.element.name; this.options.tokens = this.options.tokens || []; this.options.frequency = this.options.frequency || 0.4; this.options.minChars = this.options.minChars || 1; this.options.onShow = this.options.onShow || - function(element, update){ - if(!update.style.position || update.style.position=='absolute') { - update.style.position = 'absolute'; - Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); - } - Effect.Appear(update,{duration:0.15}); - }; + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; this.options.onHide = this.options.onHide || - function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; - if (typeof(this.options.tokens) == 'string') + if(typeof(this.options.tokens) == 'string') this.options.tokens = new Array(this.options.tokens); this.observer = null; @@ -74,15 +84,20 @@ Element.hide(this.update); - Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); - Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, 'keypress', this.onKeyPress.bindAsEventListener(this)); + + // Turn autocomplete back on when the user leaves the page, so that the + // field's value will be remembered on Mozilla-based browsers. + Event.observe(window, 'beforeunload', function(){ + element.setAttribute('autocomplete', 'on'); + }); }, show: function() { if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); if(!this.iefix && - (navigator.appVersion.indexOf('MSIE')>0) && - (navigator.userAgent.indexOf('Opera')<0) && + (Prototype.Browser.IE) && (Element.getStyle(this.update, 'position')=='absolute')) { new Insertion.After(this.update, '