initial commit
[namibia] / public / js / vendor / jquery.ibutton.js
1 /*!
2  * iButton jQuery Plug-in
3  *
4  * Copyright 2011 Giva, Inc. (http://www.givainc.com/labs/) 
5  * 
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  * 
10  *      http://www.apache.org/licenses/LICENSE-2.0
11  * 
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  *
18  * Date: 2011-07-26
19  * Rev:  1.0.03
20  */
21 ;(function($){
22         // set default options
23         $.iButton = {
24                 version: "1.0.03",
25                 setDefaults: function(options){
26                         $.extend(defaults, options);
27                 }
28         };
29         
30         $.fn.iButton = function(options) {
31                 var method = typeof arguments[0] == "string" && arguments[0];
32                 var args = method && Array.prototype.slice.call(arguments, 1) || arguments;
33                 // get a reference to the first iButton found
34                 var self = (this.length == 0) ? null : $.data(this[0], "iButton");
35                 
36                 // if a method is supplied, execute it for non-empty results
37                 if( self && method && this.length ){
38
39                         // if request a copy of the object, return it                   
40                         if( method.toLowerCase() == "object" ) return self;
41                         // if method is defined, run it and return either it's results or the chain
42                         else if( self[method] ){
43                                 // define a result variable to return to the jQuery chain
44                                 var result;
45                                 this.each(function (i){
46                                         // apply the method to the current element
47                                         var r = $.data(this, "iButton")[method].apply(self, args);
48                                         // if first iteration we need to check if we're done processing or need to add it to the jquery chain
49                                         if( i == 0 && r ){
50                                                 // if this is a jQuery item, we need to store them in a collection
51                                                 if( !!r.jquery ){
52                                                         result = $([]).add(r);
53                                                 // otherwise, just store the result and stop executing
54                                                 } else {
55                                                         result = r;
56                                                         // since we're a non-jQuery item, just cancel processing further items
57                                                         return false;
58                                                 }
59                                         // keep adding jQuery objects to the results
60                                         } else if( !!r && !!r.jquery ){
61                                                 result = result.add(r);
62                                         }
63                                 });
64
65                                 // return either the results (which could be a jQuery object) or the original chain
66                                 return result || this;
67                         // everything else, return the chain
68                         } else return this;
69                 // initializing request (only do if iButton not already initialized)
70                 } else {
71                         // create a new iButton for each object found
72                         return this.each(function (){
73                                 new iButton(this, options);
74                         });
75                 };
76         };
77
78         // count instances      
79         var counter = 0;
80         // detect iPhone
81         $.browser.iphone = (navigator.userAgent.toLowerCase().indexOf("iphone") > -1);
82         
83         var iButton = function (input, options){
84                 var self = this
85                         , $input = $(input)
86                         , id = ++counter
87                         , disabled = false
88                         , width = {}
89                         , mouse = {dragging: false, clicked: null}
90                         , dragStart = {position: null, offset: null, time: null }
91                         // make a copy of the options and use the metadata if provided
92                         , options = $.extend({}, defaults, options, (!!$.metadata ? $input.metadata() : {}))
93                         // check to see if we're using the default labels
94                         , bDefaultLabelsUsed = (options.labelOn == ON && options.labelOff == OFF)
95                         // set valid field types
96                         , allow = ":checkbox, :radio";
97
98                 // only do for checkboxes buttons, if matches inside that node
99     if( !$input.is(allow) ) return $input.find(allow).iButton(options);
100                 // if iButton already exists, stop processing
101                 else if($.data($input[0], "iButton") ) return;
102
103                 // store a reference to this marquee
104                 $.data($input[0], "iButton", self);
105                 
106                 // if using the "auto" setting, then don't resize handle or container if using the default label (since we'll trust the CSS)
107                 if( options.resizeHandle == "auto" ) options.resizeHandle = !bDefaultLabelsUsed;
108                 if( options.resizeContainer == "auto" ) options.resizeContainer = !bDefaultLabelsUsed;
109                 
110                 // toggles the state of a button (or can turn on/off)
111                 this.toggle = function (t){
112                         var toggle = (arguments.length > 0) ? t : !$input[0].checked;
113                         $input.attr("checked", toggle).trigger("change");
114                 };
115                 
116                 // disable/enable the control
117                 this.disable = function (t){
118                         var toggle = (arguments.length > 0) ? t : !disabled;
119                         // mark the control disabled
120                         disabled = toggle;
121                         // mark the input disabled
122                         $input.attr("disabled", toggle);
123                         // set the diabled styles
124                         $container[toggle ? "addClass" : "removeClass"](options.classDisabled);
125                         // run callback
126                         if( $.isFunction(options.disable) ) options.disable.apply(self, [disabled, $input, options]);
127                 };
128                 
129                 // repaint the button
130                 this.repaint = function (){
131                         positionHandle();
132                 };
133                 
134                 // this will destroy the iButton style
135                 this.destroy = function (){
136                         // remove behaviors
137                         $([$input[0], $container[0]]).unbind(".iButton");
138                         $(document).unbind(".iButton_" + id);
139                         // move the checkbox to it's original location
140                         $container.after($input).remove();
141                         // kill the reference
142                         $.data($input[0], "iButton", null);
143                         // run callback
144                         if( $.isFunction(options.destroy) ) options.destroy.apply(self, [$input, options]);
145                 };
146
147     $input
148                         // create the wrapper code
149                         .wrap('<div class="' + $.trim(options.classContainer + ' ' + options.className) + '" />')
150         .after(
151                                   '<div class="' + options.classHandle + '"><div class="' + options.classHandleRight + '"><div class="' + options.classHandleMiddle + '" /></div></div>'
152         + '<div class="' + options.classLabelOff + '"><span><label>'+ options.labelOff + '</label></span></div>'
153         + '<div class="' + options.classLabelOn + '"><span><label>' + options.labelOn   + '</label></span></div>'
154         + '<div class="' + options.classPaddingLeft + '"></div><div class="' + options.classPaddingRight + '"></div>'
155                         );
156
157     var $container = $input.parent()
158                         , $handle    = $input.siblings("." + options.classHandle)
159                         , $offlabel  = $input.siblings("." + options.classLabelOff)
160                         , $offspan   = $offlabel.children("span")
161                         , $onlabel   = $input.siblings("." + options.classLabelOn)
162                         , $onspan    = $onlabel.children("span");
163
164
165                 // if we need to do some resizing, get the widths only once
166                 if( options.resizeHandle || options.resizeContainer ){
167                         width.onspan = $onspan.outerWidth(); 
168                         width.offspan = $offspan.outerWidth();
169                 }
170                         
171                 // automatically resize the handle
172                 if( options.resizeHandle ){
173                         width.handle = Math.min(width.onspan, width.offspan);
174                         $handle.css("width", width.handle);
175                 } else {
176                         width.handle = $handle.width();
177                 }
178
179     // automatically resize the control
180                 if( options.resizeContainer ){
181                         width.container = (Math.max(width.onspan, width.offspan) + width.handle + 20);
182                         $container.css("width", width.container);
183                         // adjust the off label to match the new container size
184                         $offlabel.css("width", width.container - 5);
185                 } else {
186                         width.container = $container.width();
187                 }
188
189                 var handleRight = width.container - width.handle - 6;
190     
191                 var positionHandle = function (animate){
192                         var checked = $input[0].checked
193                                 , x = (checked) ? handleRight : 0
194                                 , animate = (arguments.length > 0) ? arguments[0] : true;
195
196                         if( animate && options.enableFx ){
197                                 $handle.stop().animate({left: x}, options.duration, options.easing);
198                                 $onlabel.stop().animate({width: x + 4}, options.duration, options.easing);
199                                 $onspan.stop().animate({marginLeft: x - handleRight}, options.duration, options.easing);
200                                 $offspan.stop().animate({marginRight: -x}, options.duration, options.easing);
201                         } else {
202                                 $handle.css("left", x);
203                                 $onlabel.css("width", x + 4);
204                                 $onspan.css("marginLeft", x - handleRight);
205                                 $offspan.css("marginRight", -x);
206                         }
207                 };
208
209                 // place the buttons in their default location  
210                 positionHandle(false);
211                 
212                 var getDragPos = function(e){
213                         return e.pageX || ((e.originalEvent.changedTouches) ? e.originalEvent.changedTouches[0].pageX : 0);
214                 };
215
216                 // monitor mouse clicks in the container
217                 $container.bind("mousedown.iButton touchstart.iButton", function(e) {
218                         // abort if disabled or allow clicking the input to toggle the status (if input is visible)
219                         if( $(e.target).is(allow) || disabled || (!options.allowRadioUncheck && $input.is(":radio:checked")) ) return;
220                         
221                         e.preventDefault();
222                         mouse.clicked = $handle;
223                         dragStart.position = getDragPos(e);
224                         dragStart.offset = dragStart.position - (parseInt($handle.css("left"), 10) || 0);
225                         dragStart.time = (new Date()).getTime();
226                         return false;
227                 });
228
229                 // make sure dragging support is enabled                
230                 if( options.enableDrag ){
231                         // monitor mouse movement on the page
232                         $(document).bind("mousemove.iButton_" + id + " touchmove.iButton_" + id, function(e) {
233                                 // if we haven't clicked on the container, cancel event
234                                 if( mouse.clicked != $handle ){ return }
235                                 e.preventDefault();
236                                 
237                                 var x = getDragPos(e);
238                                 if( x != dragStart.offset ){
239                                         mouse.dragging = true;
240                                         $container.addClass(options.classHandleActive);
241                                 }
242         
243                                 // make sure number is between 0 and 1                  
244                                 var pct = Math.min(1, Math.max(0, (x - dragStart.offset) / handleRight));
245                                 
246                                 $handle.css("left", pct * handleRight);
247                                 $onlabel.css("width", pct * handleRight + 4);
248                                 $offspan.css("marginRight", -pct * handleRight);
249                                 $onspan.css("marginLeft", -(1 - pct) * handleRight);
250                                 
251                                 return false;
252                         });
253                 }
254     
255                 // monitor when the mouse button is released
256                 $(document).bind("mouseup.iButton_" + id + " touchend.iButton_" + id, function(e) {
257                         if( mouse.clicked != $handle ){ return false }
258                         e.preventDefault();
259
260                         // track if the value has changed                       
261                         var changed = true;
262
263                         // if not dragging or click time under a certain millisecond, then just toggle                  
264                         if( !mouse.dragging || (((new Date()).getTime() - dragStart.time) < options.clickOffset ) ){
265                                 var checked = $input[0].checked;
266                                 $input.attr("checked", !checked);
267
268                                 // run callback
269                                 if( $.isFunction(options.click) ) options.click.apply(self, [!checked, $input, options]);
270                         } else {
271                                 var x = getDragPos(e);
272                                 
273                                 var pct = (x - dragStart.offset) / handleRight;
274                                 var checked = (pct >= 0.5);
275                                 
276                                 // if the value is the same, don't run change event
277                                 if( $input[0].checked == checked ) changed = false;
278
279                                 $input.attr("checked", checked);
280                         }
281                         
282                         // remove the active handler class                      
283                         $container.removeClass(options.classHandleActive);
284                         mouse.clicked =  null;
285                         mouse.dragging = null;
286                         // run any change event for the element
287                         if( changed ) $input.trigger("change");
288                         // if the value didn't change, just reset the handle
289                         else positionHandle();
290                         
291                         return false;
292                 });
293                 
294                 // animate when we get a change event
295                 $input
296                         .bind("change.iButton", function (){
297                                 // move handle
298                                 positionHandle();
299
300                                 // if a radio element, then we must repaint the other elements in it's group to show them as not selected
301                                 if( $input.is(":radio") ){
302                                         var el = $input[0];
303         
304                                         // try to use the DOM to get the grouped elements, but if not in a form get by name attr
305                                         var $radio = $(el.form ? el.form[el.name] : ":radio[name=" + el.name + "]");
306
307                                         // repaint the radio elements that are not checked      
308                                         $radio.filter(":not(:checked)").iButton("repaint");
309                                 }
310
311                                 // run callback
312                                 if( $.isFunction(options.change) ) options.change.apply(self, [$input, options]);
313                         })
314                         // if the element has focus, we need to highlight the container
315                         .bind("focus.iButton", function (){
316                                 $container.addClass(options.classFocus);
317                         })
318                         // if the element has focus, we need to highlight the container
319                         .bind("blur.iButton", function (){
320                                 $container.removeClass(options.classFocus);
321                         });
322
323                 // if a click event is registered, we must register on the checkbox so it's fired if triggered on the checkbox itself
324                 if( $.isFunction(options.click) ){
325                         $input.bind("click.iButton", function (){
326                                 options.click.apply(self, [$input[0].checked, $input, options]);
327                         });
328                 }
329
330                 // if the field is disabled, mark it as such
331                 if( $input.is(":disabled") ) this.disable(true);
332
333                 // special behaviors for IE    
334                 if( $.browser.msie ){
335                         // disable text selection in IE, other browsers are controlled via CSS
336                         $container.find("*").andSelf().attr("unselectable", "on");
337                         // IE needs to register to the "click" event to make changes immediately (the change event only occurs on blur)
338                         $input.bind("click.iButton", function (){ $input.triggerHandler("change.iButton"); });
339                 }
340                 
341                 // run the init callback
342                 if( $.isFunction(options.init) ) options.init.apply(self, [$input, options]);
343         };
344
345         var defaults = {
346                   duration: 200                           // the speed of the animation
347                 , easing: "swing"                         // the easing animation to use
348                 , labelOn: "ON"                           // the text to show when toggled on
349                 , labelOff: "OFF"                         // the text to show when toggled off
350                 , resizeHandle: "auto"                    // determines if handle should be resized
351                 , resizeContainer: "auto"                 // determines if container should be resized
352                 , enableDrag: true                        // determines if we allow dragging
353                 , enableFx: true                          // determines if we show animation
354                 , allowRadioUncheck: false                // determine if a radio button should be able to be unchecked
355                 , clickOffset: 120                        // if millseconds between a mousedown & mouseup event this value, then considered a mouse click
356
357                 // define the class statements
358                 , className:         ""
359                 , classContainer:    "ibutton-container"
360                 , classDisabled:     "ibutton-disabled"
361                 , classFocus:        "ibutton-focus"
362                 , classLabelOn:      "ibutton-label-on"
363                 , classLabelOff:     "ibutton-label-off"
364                 , classHandle:       "ibutton-handle"
365                 , classHandleMiddle: "ibutton-handle-middle"
366                 , classHandleRight:  "ibutton-handle-right"
367                 , classHandleActive: "ibutton-active-handle"
368                 , classPaddingLeft:  "ibutton-padding-left"
369                 , classPaddingRight: "ibutton-padding-right"
370
371                 // event handlers
372                 , init: null                              // callback that occurs when a iButton is initialized
373                 , change: null                            // callback that occurs when the button state is changed
374                 , click: null                             // callback that occurs when the button is clicked
375                 , disable: null                           // callback that occurs when the button is disabled/enabled
376                 , destroy: null                           // callback that occurs when the button is destroyed
377         }, ON = defaults.labelOn, OFF = defaults.labelOff;
378
379 })(jQuery);