/* Prototype-UI, version trunk * * Prototype-UI is freely distributable under the terms of an MIT-style license. * For details, see the PrototypeUI web site: http://www.prototype-ui.com/ * *--------------------------------------------------------------------------*/ if(typeof Prototype == 'undefined' || !Prototype.Version.match("1.6")) throw("Prototype-UI library require Prototype library >= 1.6.0"); if (Prototype.Browser.WebKit) { Prototype.Browser.WebKitVersion = parseFloat(navigator.userAgent.match(/AppleWebKit\/([\d\.\+]*)/)[1]); Prototype.Browser.Safari2 = (Prototype.Browser.WebKitVersion < 420); } if (Prototype.Browser.IE) { Prototype.Browser.IEVersion = parseFloat(navigator.appVersion.split(';')[1].strip().split(' ')[1]); Prototype.Browser.IE6 = Prototype.Browser.IEVersion == 6; Prototype.Browser.IE7 = Prototype.Browser.IEVersion == 7; } Prototype.falseFunction = function() { return false }; Prototype.trueFunction = function() { return true }; /* Namespace: UI Introduction: Prototype-UI is a library of user interface components based on the Prototype framework. Its aim is to easilly improve user experience in web applications. It also provides utilities to help developers. Guideline: - Prototype conventions are followed - Everything should be unobstrusive - All components are themable with CSS stylesheets, various themes are provided Warning: Prototype-UI is still under deep development, this release is targeted to developers only. All interfaces are subjects to changes, suggestions are welcome. DO NOT use it in production for now. Authors: - Sébastien Gruhier, - Samuel Lebeau, */ var UI = { Abstract: { }, Ajax: { } }; Object.extend(Class.Methods, { extend: Object.extend.methodize(), addMethods: Class.Methods.addMethods.wrap(function(proceed, source) { // ensure we are not trying to add null or undefined if (!source) return this; // no callback, vanilla way if (!source.hasOwnProperty('methodsAdded')) return proceed(source); var callback = source.methodsAdded; delete source.methodsAdded; proceed(source); callback.call(source, this); source.methodsAdded = callback; return this; }), addMethod: function(name, lambda) { var methods = {}; methods[name] = lambda; return this.addMethods(methods); }, method: function(name) { return this.prototype[name].valueOf(); }, classMethod: function() { $A(arguments).flatten().each(function(method) { this[method] = (function() { return this[method].apply(this, arguments); }).bind(this.prototype); }, this); return this; }, // prevent any call to this method undefMethod: function(name) { this.prototype[name] = undefined; return this; }, // remove the class' own implementation of this method removeMethod: function(name) { delete this.prototype[name]; return this; }, aliasMethod: function(newName, name) { this.prototype[newName] = this.prototype[name]; return this; }, aliasMethodChain: function(target, feature) { feature = feature.camelcase(); this.aliasMethod(target+"Without"+feature, target); this.aliasMethod(target, target+"With"+feature); return this; } }); Object.extend(Number.prototype, { // Snap a number to a grid snap: function(round) { return parseInt(round == 1 ? this : (this / round).floor() * round); } }); /* Interface: String */ Object.extend(String.prototype, { camelcase: function() { var string = this.dasherize().camelize(); return string.charAt(0).toUpperCase() + string.slice(1); }, /* Method: makeElement toElement is unfortunately already taken :/ Transforms html string into an extended element or null (when failed) > '
  • some text
  • '.makeElement(); // => LI href# > ''.makeElement(); // => IMG#foo (first one) Returns: Extended element */ makeElement: function() { var wrapper = new Element('div'); wrapper.innerHTML = this; return wrapper.down(); } }); Object.extend(Array.prototype, { empty: function() { return !this.length; }, extractOptions: function() { return this.last().constructor === Object ? this.pop() : { }; }, removeAt: function(index) { var object = this[index]; this.splice(index, 1); return object; }, remove: function(object) { var index; while ((index = this.indexOf(object)) != -1) this.removeAt(index); return object; }, insert: function(index) { var args = $A(arguments); args.shift(); this.splice.apply(this, [ index, 0 ].concat(args)); return this; } }); Element.addMethods({ getScrollDimensions: function(element) { return { width: element.scrollWidth, height: element.scrollHeight } }, getScrollOffset: function(element) { return Element._returnOffset(element.scrollLeft, element.scrollTop); }, setScrollOffset: function(element, offset) { element = $(element); if (arguments.length == 3) offset = { left: offset, top: arguments[2] }; element.scrollLeft = offset.left; element.scrollTop = offset.top; return element; }, // returns "clean" numerical style (without "px") or null if style can not be resolved // or is not numeric getNumStyle: function(element, style) { var value = parseFloat($(element).getStyle(style)); return isNaN(value) ? null : value; }, // by Tobie Langel (http://tobielangel.com/2007/5/22/prototype-quick-tip) appendText: function(element, text) { element = $(element); text = String.interpret(text); element.appendChild(document.createTextNode(text)); return element; } }); document.whenReady = function(callback) { if (document.loaded) callback.call(document); else document.observe('dom:loaded', callback); }; Object.extend(document.viewport, { // Alias this method for consistency getScrollOffset: document.viewport.getScrollOffsets, setScrollOffset: function(offset) { Element.setScrollOffset(Prototype.Browser.WebKit ? document.body : document.documentElement, offset); }, getScrollDimensions: function() { return Element.getScrollDimensions(Prototype.Browser.WebKit ? document.body : document.documentElement); } }); /* Interface: UI.Options Mixin to handle *options* argument in initializer pattern. TODO: find a better example than Circle that use an imaginary Point function, this example should be used in tests too. It assumes class defines a property called *options*, containing default options values. Instances hold their own *options* property after a first call to . Example: > var Circle = Class.create(UI.Options, { > > // default options > options: { > radius: 1, > origin: Point(0, 0) > }, > > // common usage is to call setOptions in initializer > initialize: function(options) { > this.setOptions(options); > } > }); > > var circle = new Circle({ origin: Point(1, 4) }); > > circle.options > // => { radius: 1, origin: Point(1,4) } Accessors: There are builtin methods to automatically write options accessors. All those methods can take either an array of option names nor option names as arguments. Notice that those methods won't override an accessor method if already present. * creates getters * creates setters * creates both getters and setters Common usage is to invoke them on a class to create accessors for all instances of this class. Invoking those methods on a class has the same effect as invoking them on the class prototype. See for more details. Example: > // Creates getter and setter for the "radius" options of circles > Circle.optionsAccessor('radius'); > > circle.setRadius(4); > // 4 > > circle.getRadius(); > // => 4 (circle.options.radius) Inheritance support: Subclasses can refine default *options* values, after a first instance call on setOptions, *options* attribute will hold all default options values coming from the inheritance hierarchy. */ (function() { UI.Options = { methodsAdded: function(klass) { klass.classMethod($w(' setOptions allOptions optionsGetter optionsSetter optionsAccessor ')); }, // Group: Methods /* Method: setOptions Extends object's *options* property with the given object */ setOptions: function(options) { if (!this.hasOwnProperty('options')) this.options = this.allOptions(); this.options = Object.extend(this.options, options || {}); }, /* Method: allOptions Computes the complete default options hash made by reverse extending all superclasses default options. > Widget.prototype.allOptions(); */ allOptions: function() { var superclass = this.constructor.superclass, ancestor = superclass && superclass.prototype; return (ancestor && ancestor.allOptions) ? Object.extend(ancestor.allOptions(), this.options) : Object.clone(this.options); }, /* Method: optionsGetter Creates default getters for option names given as arguments. With no argument, creates getters for all option names. */ optionsGetter: function() { addOptionsAccessors(this, arguments, false); }, /* Method: optionsSetter Creates default setters for option names given as arguments. With no argument, creates setters for all option names. */ optionsSetter: function() { addOptionsAccessors(this, arguments, true); }, /* Method: optionsAccessor Creates default getters/setters for option names given as arguments. With no argument, creates accessors for all option names. */ optionsAccessor: function() { this.optionsGetter.apply(this, arguments); this.optionsSetter.apply(this, arguments); } }; // Internal function addOptionsAccessors(receiver, names, areSetters) { names = $A(names).flatten(); if (names.empty()) names = Object.keys(receiver.allOptions()); names.each(function(name) { var accessorName = (areSetters ? 'set' : 'get') + name.camelcase(); receiver[accessorName] = receiver[accessorName] || (areSetters ? // Setter function(value) { return this.options[name] = value } : // Getter function() { return this.options[name] }); }); } })(); /* Namespace: CSS Utility functions for CSS/StyleSheet files access Authors: - Sébastien Gruhier, - Samuel Lebeau, */ var CSS = (function() { // Code based on: // - IE5.5+ PNG Alpha Fix v1.0RC4 (c) 2004-2005 Angus Turnbull http://www.twinhelix.com // - Whatever:hover - V2.02.060206 - hover, active & focus (c) 2005 - Peter Nederlof * Peterned - http://www.xs4all.nl/~peterned/ function fixPNG() { parseStylesheet.apply(this, $A(arguments).concat(fixRule)); }; function parseStylesheet() { var patterns = $A(arguments); var method = patterns.pop(); // To avoid flicking background //document.execCommand("BackgroundImageCache", false, true); // Parse all document stylesheets var styleSheets = $A(document.styleSheets); if (patterns.length > 1) { styleSheets = styleSheets.select(function(css) { return patterns.any(function(pattern) { return css.href && css.href.match(pattern) }); }); } styleSheets.each(function(styleSheet) {fixStylesheet.call(this, styleSheet, method)}); }; // Fixes a stylesheet function fixStylesheet(stylesheet, method) { // Parse import files if (stylesheet.imports) $A(stylesheet.imports).each(fixStylesheet); var href = stylesheet.href || document.location.href; var docPath = href.substr(0, href.lastIndexOf('/')); // Parse all CSS Rules $A(stylesheet.rules || stylesheet.cssRules).each(function(rule) { method.call(this, rule, docPath) }); }; var filterPattern = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="#{src}",sizingMethod="#{method}")'; // Fixes a rule if it has a PNG background function fixRule(rule, docPath) { var bgImg = rule.style.backgroundImage; // Rule with PNG background image if (bgImg && bgImg != 'none' && bgImg.match(/^url[("']+(.*\.png)[)"']+$/i)) { var src = RegExp.$1; var bgRepeat = rule.style.backgroundRepeat; // Relative path if (src[0] != '/') src = docPath + "/" + src; // Apply filter rule.style.filter = filterPattern.interpolate({ src: src, method: bgRepeat == "no-repeat" ? "crop" : "scale" }); rule.style.backgroundImage = "none"; } }; var preloadedImages = new Hash(); function preloadRule(rule, docPath) { var bgImg = rule.style.backgroundImage; if (bgImg && bgImg != 'none' && bgImg != 'initial' ) { if (!preloadedImages.get(bgImg)) { bgImg.match(/^url[("']+(.*)[)"']+$/i); var src = RegExp.$1; // Relative path if (!(src[0] == '/' || src.match(/^file:/) || src.match(/^https?:/))) src = docPath + "/" + src; preloadedImages.set(bgImg, true); var image = new Image(); image.src = src; } } } return { /* Method: fixPNG Fix transparency of PNG background of document stylesheets. (only on IE version<7, otherwise does nothing) Warning: All png background will not work as IE filter use for handling transparency in PNG is not compatible with all background. It does not support top/left position (so no CSS sprite) I recommend to create a special CSS file with png that needs to be fixed and call CSS.fixPNG on this CSS Examples: > CSS.fixPNG() // To fix all css > > CSS.fixPNG("mac_shadow.css") // to fix all css files with mac_shadow.css so mainly only on file > > CSS.fixPNG("shadow", "vista"); // To fix all css files with shadow or vista in their names Parameters patterns: (optional) list of pattern to filter css files */ fixPNG: (Prototype.Browser.IE && Prototype.Browser.IEVersion < 7) ? fixPNG : Prototype.emptyFunction, // By Tobie Langel (http://tobielangel.com) // inspired by http://yuiblog.com/blog/2007/06/07/style/ addRule: function(css, backwardCompatibility) { if (backwardCompatibility) css = css + '{' + backwardCompatibility + '}'; var style = new Element('style', { type: 'text/css', media: 'screen' }); $(document.getElementsByTagName('head')[0]).insert(style); if (style.styleSheet) style.styleSheet.cssText = css; else style.appendText(css); return style; }, preloadImages: function() { parseStylesheet.apply(this, $A(arguments).concat(preloadRule)); } }; })(); UI.Benchmark = { benchmark: function(lambda, iterations) { var date = new Date(); (iterations || 1).times(lambda); return (new Date() - date) / 1000; } }; /* Group: Drag UI provides Element#enableDrag method that allow elements to fire drag-related events. Events fired: - drag:started : fired when a drag is started (mousedown then mousemove) - drag:updated : fired when a drag is updated (mousemove) - drag:ended : fired when a drag is ended (mouseup) Notice it doesn't actually move anything, drag behavior has to be implemented by attaching handlers to drag events. Drag-related informations: event.memo contains useful information about the drag occuring: - dx : difference between pointer x position when drag started and actual x position - dy : difference between pointer y position when drag started and actual y position - mouseEvent : the original mouse event, useful to know pointer absolute position, or if key were pressed. Example, with event handling for a specific element: > // Now "resizable" will fire drag-related events > $('resizable').enableDrag(); > > // Let's observe them > $('resizable').observe('drag:started', function(event) { > this._dimensions = this.getDimensions(); > }).observe('drag:updated', function(event) { > var drag = event.memo; > > this.setStyle({ > width: this._dimensions.width + drag.dx + 'px', > height: this._dimensions.height + drag.dy + 'px' > }); > }); Example, with event delegating on the whole document: > // All elements in the having the "draggable" class name will fire drag events. > $$('.draggable').invoke('enableDrag'); > > document.observe('drag:started', function(event) { > UI.logger.info('trying to drag ' + event.element().id); > }): */ (function() { var initPointer, currentDraggable, dragging; document.observe('mousedown', onMousedown); function onMousedown(event) { var draggable = event.findElement('[ui:draggable="true"]'); if (draggable) { // prevent default browser action event.stop(); currentDraggable = draggable; initPointer = event.pointer(); document.observe("mousemove", onMousemove) .observe("mouseup", onMouseup); } }; function onMousemove(event) { event.stop(); if (dragging) fire('drag:updated', event); else { dragging = true; fire('drag:started', event); } }; function onMouseup(event) { document.stopObserving('mousemove', onMousemove) .stopObserving('mouseup', onMouseup); if (dragging) { dragging = false; fire('drag:ended', event); } }; function fire(eventName, mouseEvent) { var pointer = mouseEvent.pointer(); currentDraggable.fire(eventName, { dx: pointer.x - initPointer.x, dy: pointer.y - initPointer.y, mouseEvent: mouseEvent }) }; Element.addMethods({ enableDrag: function(element) { element = $(element); element.writeAttribute('ui:draggable', 'true'); return element; }, disableDrag: function(element){ element = $(element); element.writeAttribute('ui:draggable', null); return element; }, isDraggable: function(element) { return $(element).readAttribute('ui:draggable') == 'true'; } }); })(); /* Class: UI.IframeShim Handles IE6 bug when