Browse Source

increased readability of post conditions, reworked container position logic, implemented variable handles and label widths, fixed chrome opacity

Emanuele Marchi 10 years ago
parent
commit
02c8d9f18e

+ 72 - 101
dist/css/bootstrap3/bootstrap-switch.css

@@ -34,108 +34,11 @@
   -ms-user-select: none;
   user-select: none;
   vertical-align: middle;
-  min-width: 100px;
   -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
   transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
 }
-.bootstrap-switch.bootstrap-switch-mini {
-  min-width: 71px;
-}
-.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,
-.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,
-.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label {
-  padding-bottom: 4px;
-  padding-top: 4px;
-  font-size: 10px;
-  line-height: 9px;
-}
-.bootstrap-switch.bootstrap-switch-small {
-  min-width: 79px;
-}
-.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,
-.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,
-.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label {
-  padding-bottom: 3px;
-  padding-top: 3px;
-  font-size: 12px;
-  line-height: 18px;
-}
-.bootstrap-switch.bootstrap-switch-large {
-  min-width: 120px;
-}
-.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,
-.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,
-.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label {
-  padding-bottom: 9px;
-  padding-top: 9px;
-  font-size: 16px;
-  line-height: normal;
-}
-.bootstrap-switch.bootstrap-switch-disabled,
-.bootstrap-switch.bootstrap-switch-readonly,
-.bootstrap-switch.bootstrap-switch-indeterminate {
-  opacity: 0.5;
-  filter: alpha(opacity=50);
-  cursor: default !important;
-}
-.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,
-.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,
-.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,
-.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,
-.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,
-.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,
-.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,
-.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label,
-.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label {
-  cursor: default !important;
-}
-.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container {
-  -webkit-transition: margin-left 0.5s;
-  transition: margin-left 0.5s;
-}
-.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on {
-  border-bottom-left-radius: 0;
-  border-top-left-radius: 0;
-  border-bottom-right-radius: 3px;
-  border-top-right-radius: 3px;
-}
-.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off {
-  border-bottom-right-radius: 0;
-  border-top-right-radius: 0;
-  border-bottom-left-radius: 3px;
-  border-top-left-radius: 3px;
-}
-.bootstrap-switch.bootstrap-switch-focused {
-  border-color: #66afe9;
-  outline: 0;
-  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
-  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
-}
-.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-container,
-.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-container {
-  margin-left: 0%;
-}
-.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label,
-.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label {
-  border-bottom-right-radius: 3px;
-  border-top-right-radius: 3px;
-}
-.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-container,
-.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-container {
-  margin-left: -50%;
-}
-.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label,
-.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label {
-  border-bottom-left-radius: 3px;
-  border-top-left-radius: 3px;
-}
-.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-container,
-.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-indeterminate .bootstrap-switch-container {
-  margin-left: -25%;
-}
 .bootstrap-switch .bootstrap-switch-container {
   display: inline-block;
-  width: 150%;
   top: 0;
   border-radius: 4px;
   -webkit-transform: translate3d(0, 0, 0);
@@ -150,8 +53,7 @@
   cursor: pointer;
   display: inline-block !important;
   height: 100%;
-  padding-bottom: 4px;
-  padding-top: 4px;
+  padding: 6px 12px;
   font-size: 14px;
   line-height: 20px;
 }
@@ -159,7 +61,6 @@
 .bootstrap-switch .bootstrap-switch-handle-off {
   text-align: center;
   z-index: 1;
-  width: 33.333333333%;
 }
 .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary,
 .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary {
@@ -196,7 +97,6 @@
   margin-top: -1px;
   margin-bottom: -1px;
   z-index: 100;
-  width: 33.333333333%;
   color: #333333;
   background: #ffffff;
 }
@@ -221,3 +121,74 @@
 .bootstrap-switch input[type='checkbox'].form-control {
   height: auto;
 }
+.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,
+.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,
+.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label {
+  padding: 1px 5px;
+  font-size: 12px;
+  line-height: 1.5;
+}
+.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,
+.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,
+.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label {
+  padding: 5px 10px;
+  font-size: 12px;
+  line-height: 1.5;
+}
+.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,
+.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,
+.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label {
+  padding: 6px 16px;
+  font-size: 18px;
+  line-height: 1.33;
+}
+.bootstrap-switch.bootstrap-switch-disabled,
+.bootstrap-switch.bootstrap-switch-readonly,
+.bootstrap-switch.bootstrap-switch-indeterminate {
+  cursor: default !important;
+}
+.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,
+.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,
+.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,
+.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,
+.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,
+.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,
+.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,
+.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label,
+.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label {
+  opacity: 0.5;
+  filter: alpha(opacity=50);
+  cursor: default !important;
+}
+.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container {
+  -webkit-transition: margin-left 0.5s;
+  transition: margin-left 0.5s;
+}
+.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on {
+  border-bottom-left-radius: 0;
+  border-top-left-radius: 0;
+  border-bottom-right-radius: 3px;
+  border-top-right-radius: 3px;
+}
+.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off {
+  border-bottom-right-radius: 0;
+  border-top-right-radius: 0;
+  border-bottom-left-radius: 3px;
+  border-top-left-radius: 3px;
+}
+.bootstrap-switch.bootstrap-switch-focused {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
+  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label,
+.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label {
+  border-bottom-right-radius: 3px;
+  border-top-right-radius: 3px;
+}
+.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label,
+.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label {
+  border-bottom-left-radius: 3px;
+  border-top-left-radius: 3px;
+}

File diff suppressed because it is too large
+ 0 - 0
dist/css/bootstrap3/bootstrap-switch.min.css


+ 139 - 53
dist/js/bootstrap-switch.js

@@ -45,6 +45,8 @@
           onText: this.$element.data("on-text"),
           offText: this.$element.data("off-text"),
           labelText: this.$element.data("label-text"),
+          handleWidth: this.$element.data("handle-width"),
+          labelWidth: this.$element.data("label-width"),
           baseClass: this.$element.data("base-class"),
           wrapperClass: this.$element.data("wrapper-class")
         }, options);
@@ -57,9 +59,6 @@
               if (_this.options.size != null) {
                 classes.push("" + _this.options.baseClass + "-" + _this.options.size);
               }
-              if (_this.options.animate) {
-                classes.push("" + _this.options.baseClass + "-animate");
-              }
               if (_this.options.disabled) {
                 classes.push("" + _this.options.baseClass + "-disabled");
               }
@@ -94,9 +93,6 @@
           html: this.options.labelText,
           "class": "" + this.options.baseClass + "-label"
         });
-        if (this.options.indeterminate) {
-          this.$element.prop("indeterminate", true);
-        }
         this.$element.on("init.bootstrapSwitch", (function(_this) {
           return function() {
             return _this.options.onInit.apply(element, arguments);
@@ -109,11 +105,23 @@
         })(this));
         this.$container = this.$element.wrap(this.$container).parent();
         this.$wrapper = this.$container.wrap(this.$wrapper).parent();
-        this.$element.before(this.options.inverse ? this.$off : this.$on).before(this.$label).before(this.options.inverse ? this.$on : this.$off).trigger("init.bootstrapSwitch");
+        this.$element.before(this.options.inverse ? this.$off : this.$on).before(this.$label).before(this.options.inverse ? this.$on : this.$off);
+        if (this.options.indeterminate) {
+          this.$element.prop("indeterminate", true);
+        }
+        this._width();
+        this._containerPosition(this.options.state, (function(_this) {
+          return function() {
+            if (_this.options.animate) {
+              return _this.$wrapper.addClass("" + _this.options.baseClass + "-animate");
+            }
+          };
+        })(this));
         this._elementHandlers();
         this._handleHandlers();
         this._labelHandlers();
         this._formHandler();
+        this.$element.trigger("init.bootstrapSwitch");
       }
 
       BootstrapSwitch.prototype._constructor = BootstrapSwitch;
@@ -160,6 +168,7 @@
         if (value) {
           this.$wrapper.addClass("" + this.options.baseClass + "-" + value);
         }
+        this._width();
         this.options.size = value;
         return this.$element;
       };
@@ -169,14 +178,14 @@
           return this.options.animate;
         }
         value = !!value;
-        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-animate");
         this.options.animate = value;
+        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-animate");
         return this.$element;
       };
 
       BootstrapSwitch.prototype.toggleAnimate = function() {
-        this.$wrapper.toggleClass("" + this.options.baseClass + "-animate");
         this.options.animate = !this.options.animate;
+        this.$wrapper.toggleClass("" + this.options.baseClass + "-animate");
         return this.$element;
       };
 
@@ -185,16 +194,16 @@
           return this.options.disabled;
         }
         value = !!value;
-        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-disabled");
-        this.$element.prop("disabled", value);
         this.options.disabled = value;
+        this.$element.prop("disabled", value);
+        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-disabled");
         return this.$element;
       };
 
       BootstrapSwitch.prototype.toggleDisabled = function() {
-        this.$element.prop("disabled", !this.options.disabled);
-        this.$wrapper.toggleClass("" + this.options.baseClass + "-disabled");
         this.options.disabled = !this.options.disabled;
+        this.$element.prop("disabled", this.options.disabled);
+        this.$wrapper.toggleClass("" + this.options.baseClass + "-disabled");
         return this.$element;
       };
 
@@ -203,16 +212,16 @@
           return this.options.readonly;
         }
         value = !!value;
-        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-readonly");
-        this.$element.prop("readonly", value);
         this.options.readonly = value;
+        this.$element.prop("readonly", value);
+        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-readonly");
         return this.$element;
       };
 
       BootstrapSwitch.prototype.toggleReadonly = function() {
-        this.$element.prop("readonly", !this.options.readonly);
-        this.$wrapper.toggleClass("" + this.options.baseClass + "-readonly");
         this.options.readonly = !this.options.readonly;
+        this.$element.prop("readonly", this.options.readonly);
+        this.$wrapper.toggleClass("" + this.options.baseClass + "-readonly");
         return this.$element;
       };
 
@@ -221,16 +230,18 @@
           return this.options.indeterminate;
         }
         value = !!value;
-        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-indeterminate");
-        this.$element.prop("indeterminate", value);
         this.options.indeterminate = value;
+        this.$element.prop("indeterminate", value);
+        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-indeterminate");
+        this._containerPosition();
         return this.$element;
       };
 
       BootstrapSwitch.prototype.toggleIndeterminate = function() {
+        this.options.indeterminate = !this.options.indeterminate;
         this.$element.prop("indeterminate", !this.options.indeterminate);
         this.$wrapper.toggleClass("" + this.options.baseClass + "-indeterminate");
-        this.options.indeterminate = !this.options.indeterminate;
+        this._containerPosition();
         return this.$element;
       };
 
@@ -297,6 +308,8 @@
           return this.options.onText;
         }
         this.$on.html(value);
+        this._width();
+        this._containerPosition();
         this.options.onText = value;
         return this.$element;
       };
@@ -306,6 +319,8 @@
           return this.options.offText;
         }
         this.$off.html(value);
+        this._width();
+        this._containerPosition();
         this.options.offText = value;
         return this.$element;
       };
@@ -315,10 +330,29 @@
           return this.options.labelText;
         }
         this.$label.html(value);
+        this._width();
         this.options.labelText = value;
         return this.$element;
       };
 
+      BootstrapSwitch.prototype.handleWidth = function(value) {
+        if (typeof value === "undefined") {
+          return this.options.handleWidth;
+        }
+        this.options.handleWidth = value;
+        this._width();
+        return this.$element;
+      };
+
+      BootstrapSwitch.prototype.labelWidth = function(value) {
+        if (typeof value === "undefined") {
+          return this.options.labelWidth;
+        }
+        this.options.labelWidth = value;
+        this._width();
+        return this.$element;
+      };
+
       BootstrapSwitch.prototype.baseClass = function(value) {
         return this.options.baseClass;
       };
@@ -377,24 +411,81 @@
         return this.$element;
       };
 
+      BootstrapSwitch.prototype._width = function() {
+        var $handles, handleWidth;
+        $handles = this.$on.add(this.$off);
+        $handles.add(this.$label).css("width", "");
+        handleWidth = this.options.handleWidth === "auto" ? Math.max(this.$on.width(), this.$off.width()) : this.options.handleWidth;
+        $handles.width(handleWidth);
+        this.$label.width((function(_this) {
+          return function(index, width) {
+            if (_this.options.labelWidth !== "auto") {
+              return _this.options.labelWidth;
+            }
+            if (width < handleWidth) {
+              return handleWidth;
+            } else {
+              return width;
+            }
+          };
+        })(this));
+        this._handleWidth = this.$on.outerWidth();
+        this._labelWidth = this.$label.outerWidth();
+        this.$container.width((this._handleWidth * 2) + this._labelWidth);
+        return this.$wrapper.width(this._handleWidth + this._labelWidth);
+      };
+
+      BootstrapSwitch.prototype._containerPosition = function(state, callback) {
+        if (state == null) {
+          state = this.options.state;
+        }
+        this.$container.css("margin-left", (function(_this) {
+          return function() {
+            var values;
+            values = [0, "-" + _this._handleWidth + "px"];
+            if (_this.options.indeterminate) {
+              return "-" + (_this._handleWidth / 2) + "px";
+            }
+            if (state) {
+              if (_this.options.inverse) {
+                return values[1];
+              } else {
+                return values[0];
+              }
+            } else {
+              if (_this.options.inverse) {
+                return values[0];
+              } else {
+                return values[1];
+              }
+            }
+          };
+        })(this));
+        if (!callback) {
+          return;
+        }
+        return this.$container.one($.support.transition.end, callback).emulateTransitionEnd(500);
+      };
+
       BootstrapSwitch.prototype._elementHandlers = function() {
         return this.$element.on({
           "change.bootstrapSwitch": (function(_this) {
             return function(e, skip) {
-              var checked;
+              var state;
               e.preventDefault();
               e.stopImmediatePropagation();
-              checked = _this.$element.is(":checked");
-              if (checked === _this.options.state) {
+              state = _this.$element.is(":checked");
+              _this._containerPosition(state);
+              if (state === _this.options.state) {
                 return;
               }
-              _this.options.state = checked;
-              _this.$wrapper.removeClass(checked ? "" + _this.options.baseClass + "-off" : "" + _this.options.baseClass + "-on").addClass(checked ? "" + _this.options.baseClass + "-on" : "" + _this.options.baseClass + "-off");
+              _this.options.state = state;
+              _this.$wrapper.toggleClass("" + _this.options.baseClass + "-off").toggleClass("" + _this.options.baseClass + "-on");
               if (!skip) {
                 if (_this.$element.is(":radio")) {
                   $("[name='" + (_this.$element.attr('name')) + "']").not(_this.$element).prop("checked", false).trigger("change.bootstrapSwitch", true);
                 }
-                return _this.$element.trigger("switchChange.bootstrapSwitch", [checked]);
+                return _this.$element.trigger("switchChange.bootstrapSwitch", [state]);
               }
             };
           })(this),
@@ -447,59 +538,52 @@
 
       BootstrapSwitch.prototype._labelHandlers = function() {
         return this.$label.on({
-          "mousemove.bootstrapSwitch touchmove.bootstrapSwitch": (function(_this) {
+          "mousedown.bootstrapSwitch touchstart.bootstrapSwitch": (function(_this) {
             return function(e) {
-              var left, pageX, percent, right;
-              if (!_this.isLabelDragging) {
+              if (_this._dragStart || _this.options.disabled || _this.options.readonly) {
                 return;
               }
               e.preventDefault();
-              _this.isLabelDragged = true;
-              pageX = e.pageX || e.originalEvent.touches[0].pageX;
-              percent = ((pageX - _this.$wrapper.offset().left) / _this.$wrapper.width()) * 100;
-              left = 25;
-              right = 75;
+              _this._dragStart = (e.pageX || e.originalEvent.touches[0].pageX) - parseInt(_this.$container.css("margin-left"), 10);
               if (_this.options.animate) {
                 _this.$wrapper.removeClass("" + _this.options.baseClass + "-animate");
               }
-              if (percent < left) {
-                percent = left;
-              } else if (percent > right) {
-                percent = right;
-              }
-              _this.$container.css("margin-left", "" + (percent - right) + "%");
               return _this.$element.trigger("focus.bootstrapSwitch");
             };
           })(this),
-          "mousedown.bootstrapSwitch touchstart.bootstrapSwitch": (function(_this) {
+          "mousemove.bootstrapSwitch touchmove.bootstrapSwitch": (function(_this) {
             return function(e) {
-              if (_this.isLabelDragging || _this.options.disabled || _this.options.readonly) {
+              var difference;
+              if (_this._dragStart == null) {
                 return;
               }
               e.preventDefault();
-              _this.isLabelDragging = true;
-              return _this.$element.trigger("focus.bootstrapSwitch");
+              difference = (e.pageX || e.originalEvent.touches[0].pageX) - _this._dragStart;
+              if (difference < -_this._handleWidth || difference > 0) {
+                return;
+              }
+              _this._dragEnd = difference;
+              return _this.$container.css("margin-left", "" + _this._dragEnd + "px");
             };
           })(this),
           "mouseup.bootstrapSwitch touchend.bootstrapSwitch": (function(_this) {
             return function(e) {
               var state;
-              if (!_this.isLabelDragging) {
+              if (!_this._dragStart) {
                 return;
               }
               e.preventDefault();
-              if (_this.isLabelDragged) {
-                state = parseInt(_this.$container.css("margin-left"), 10) > -(_this.$container.width() / 6);
-                _this.isLabelDragged = false;
+              if (_this.options.animate) {
+                _this.$wrapper.addClass("" + _this.options.baseClass + "-animate");
+              }
+              if (_this._dragEnd) {
+                state = _this._dragEnd > -(_this._handleWidth / 2);
+                _this._dragEnd = false;
                 _this.state(_this.options.inverse ? !state : state);
-                if (_this.options.animate) {
-                  _this.$wrapper.addClass("" + _this.options.baseClass + "-animate");
-                }
-                _this.$container.css("margin-left", "");
               } else {
                 _this.state(!_this.options.state);
               }
-              return _this.isLabelDragging = false;
+              return _this._dragStart = false;
             };
           })(this),
           "mouseleave.bootstrapSwitch": (function(_this) {
@@ -575,6 +659,8 @@
       onText: "ON",
       offText: "OFF",
       labelText: "&nbsp;",
+      handleWidth: "auto",
+      labelWidth: "auto",
       baseClass: "bootstrap-switch",
       wrapperClass: "wrapper",
       onInit: function() {},

File diff suppressed because it is too large
+ 0 - 0
dist/js/bootstrap-switch.min.js


+ 19 - 4
docs/js/main.js

@@ -27,7 +27,14 @@ $(function() {
   });
 
   // initialize all the inputs
-  $('input[type="checkbox"],[type="radio"]').not('#create-switch').not('#events-switch').bootstrapSwitch();
+  $('input[type="checkbox"],[type="radio"]')
+  .not('#create-switch')
+  .not('#events-switch')
+  .not('#switch-modal')
+  .one('switchChange.bootstrapSwitch', function(event, state) {
+    console.log(arguments);
+  })
+  .bootstrapSwitch();
 
   $('[data-get]').on("click", function() {
     var type = $(this).data('get');
@@ -47,15 +54,23 @@ $(function() {
     $('#switch-' + type).bootstrapSwitch('toggle' + type.charAt(0).toUpperCase() + type.slice(1));
   });
 
-  $('[data-set-text]').on('change', function(event) {
+  $('[data-set-value]').on('input', function(event) {
     event.preventDefault();
-    var type = $(this).data('set-text');
+    var type = $(this).data('set-value');
     var value = $.trim($(this).val());
 
-    if ( ! value) {
+    if ($(this).data('value') == value) {
       return;
     }
 
     $('#switch-' + type).bootstrapSwitch(type, value);
   });
+
+  $('#modal-switch')
+  .on("shown.bs.modal", function() {
+    $('#switch-modal').bootstrapSwitch();
+  })
+  .on("hidden.bs.modal", function() {
+    $('#switch-modal').bootstrapSwitch('destroy');
+  });
 });

+ 42 - 4
examples.html

@@ -64,7 +64,7 @@
         <div class="col-sm-6 col-lg-4">
           <h2 class="h4">State</h2>
           <p>
-            <input id="switch-state" type="checkbox" checked>
+            <input id="switch-state" type="checkbox">
           </p>
           <div class="btn-group">
             <button type="button" data-toggle="state" class="btn btn-default">Toggle</button>
@@ -181,7 +181,7 @@
           </p>
           <div class="row">
             <div class="col-sm-6">
-              <input type="text" data-set-text="onText" class="form-control">
+              <input type="text" data-set-value="onText" value="Yes" class="form-control">
             </div>
           </div>
         </div>
@@ -192,7 +192,7 @@
           </p>
           <div class="row">
             <div class="col-sm-6">
-              <input type="text" data-set-text="offText" class="form-control">
+              <input type="text" data-set-value="offText" value="No" class="form-control">
             </div>
           </div>
         </div>
@@ -203,7 +203,29 @@
           </p>
           <div class="row">
             <div class="col-sm-6">
-              <input type="text" data-set-text="labelText" class="form-control">
+              <input type="text" data-set-value="labelText" class="form-control">
+            </div>
+          </div>
+        </div>
+        <div class="col-sm-6 col-lg-4">
+          <h2 class="h4">Handle Width</h2>
+          <p>
+            <input id="switch-handleWidth" type="checkbox" data-handle-width="100">
+          </p>
+          <div class="row">
+            <div class="col-sm-6">
+              <input type="number" data-set-value="handleWidth" value="100" class="form-control">
+            </div>
+          </div>
+        </div>
+        <div class="col-sm-6 col-lg-4">
+          <h2 class="h4">Label Width</h2>
+          <p>
+            <input id="switch-labelWidth" type="checkbox" data-label-width="100">
+          </p>
+          <div class="row">
+            <div class="col-sm-6">
+              <input type="number" data-set-value="labelWidth" value="100" class="form-control">
             </div>
           </div>
         </div>
@@ -223,6 +245,22 @@
             <input type="radio" name="radio2" data-radio-all-off="true" class="switch-radio2">
             <input type="radio" name="radio2" data-radio-all-off="true" class="switch-radio2">
           </div>
+        </div><br>
+        <hr>
+        <h2 class="h4">Inside a Modal</h2>
+        <button data-toggle="modal" data-target="#modal-switch" class="btn btn-default">Open the modal</button>
+        <div id="modal-switch" tabindex="-1" role="dialog" aria-labelledby="modal-switch-label" class="modal fade">
+          <div class="modal-dialog">
+            <div class="modal-content">
+              <div class="modal-header">
+                <button type="button" data-dismiss="modal" class="close"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+                <div id="modal-switch-label" class="modal-title">Modal title</div>
+                <div class="modal-body">
+                  <input id="switch-modal" type="checkbox" checked>
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
     </div>

+ 1 - 1
gulpfile.coffee

@@ -45,7 +45,7 @@ gulp.task 'coffee', ->
   .pipe $.changed "#{paths.dist}/js"
   .pipe $.coffeelint 'coffeelint.json'
   .pipe $.coffeelint.reporter()
-    .on 'error', $.util.log
+  .pipe $.coffeelint.reporter("fail")
   .pipe $.coffee()
     .on 'error', $.util.log
   .pipe $.header banner, pkg: pkg

+ 16 - 0
options-3.html

@@ -176,6 +176,22 @@
             <td>String</td>
             <td>'&amp;nbsp;'</td>
           </tr>
+          <tr>
+            <td>handleWidth</td>
+            <td>data-handle-width</td>
+            <td>String | Number</td>
+            <td>Width of the left and right sides in pixels</td>
+            <td>'auto' or Number</td>
+            <td>'auto'</td>
+          </tr>
+          <tr>
+            <td>labelWidth</td>
+            <td>data-label-width</td>
+            <td>String | Number</td>
+            <td>Width of the center handle in pixels</td>
+            <td>'auto' or Number</td>
+            <td>'auto'</td>
+          </tr>
           <tr>
             <td>baseClass</td>
             <td>data-base-class</td>

+ 148 - 76
src/coffee/bootstrap-switch.coffee

@@ -18,6 +18,8 @@ do ($ = window.jQuery, window) ->
         onText: @$element.data "on-text"
         offText: @$element.data "off-text"
         labelText: @$element.data "label-text"
+        handleWidth: @$element.data "handle-width"
+        labelWidth: @$element.data "label-width"
         baseClass: @$element.data "base-class"
         wrapperClass: @$element.data "wrapper-class"
       , options
@@ -27,7 +29,6 @@ do ($ = window.jQuery, window) ->
 
           classes.push if @options.state then "#{@options.baseClass}-on" else "#{@options.baseClass}-off"
           classes.push "#{@options.baseClass}-#{@options.size}" if @options.size?
-          classes.push "#{@options.baseClass}-animate" if @options.animate
           classes.push "#{@options.baseClass}-disabled" if @options.disabled
           classes.push "#{@options.baseClass}-readonly" if @options.readonly
           classes.push "#{@options.baseClass}-indeterminate" if @options.indeterminate
@@ -46,9 +47,6 @@ do ($ = window.jQuery, window) ->
         html: @options.labelText
         class: "#{@options.baseClass}-label"
 
-      # indeterminate state
-      @$element.prop "indeterminate", true if @options.indeterminate
-
       # set up events
       @$element.on "init.bootstrapSwitch", => @options.onInit.apply element, arguments
       @$element.on "switchChange.bootstrapSwitch", => @options.onSwitchChange.apply element, arguments
@@ -62,21 +60,31 @@ do ($ = window.jQuery, window) ->
       .before(if @options.inverse then @$off else @$on)
       .before(@$label)
       .before(if @options.inverse then @$on else @$off)
-      .trigger "init.bootstrapSwitch"
 
+      # indeterminate state
+      @$element.prop "indeterminate", true  if @options.indeterminate
+
+      # normalize handles width
+      @_width()
+
+      # set container position
+      @_containerPosition @options.state, =>
+        @$wrapper.addClass "#{@options.baseClass}-animate"  if @options.animate
+
+      # initialise handlers
       @_elementHandlers()
       @_handleHandlers()
       @_labelHandlers()
       @_formHandler()
 
-      # TODO: @$label.hasClass "label-change-switch" in toggleState
+      @$element.trigger "init.bootstrapSwitch"
 
     _constructor: BootstrapSwitch
 
     state: (value, skip) ->
-      return @options.state if typeof value is "undefined"
-      return @$element if @options.disabled or @options.readonly
-      return @$element if @options.state and not @options.radioAllOff and @$element.is ':radio'
+      return @options.state  if typeof value is "undefined"
+      return @$element  if @options.disabled or @options.readonly
+      return @$element  if @options.state and not @options.radioAllOff and @$element.is ':radio'
 
       if @options.indeterminate
         @indeterminate false
@@ -88,7 +96,7 @@ do ($ = window.jQuery, window) ->
       @$element
 
     toggleState: (skip) ->
-      return @$element if @options.disabled or @options.readonly
+      return @$element  if @options.disabled or @options.readonly
 
       if @options.indeterminate
         @indeterminate false
@@ -97,77 +105,84 @@ do ($ = window.jQuery, window) ->
         @$element.prop("checked", not @options.state).trigger "change.bootstrapSwitch", skip
 
     size: (value) ->
-      return @options.size if typeof value is "undefined"
+      return @options.size  if typeof value is "undefined"
 
       @$wrapper.removeClass "#{@options.baseClass}-#{@options.size}" if @options.size?
       @$wrapper.addClass "#{@options.baseClass}-#{value}" if value
+      @_width()
       @options.size = value
       @$element
 
     animate: (value) ->
-      return @options.animate if typeof value is "undefined"
+      return @options.animate  if typeof value is "undefined"
 
       value = not not value
+      @options.animate = value
 
       @$wrapper[if value then "addClass" else "removeClass"]("#{@options.baseClass}-animate")
-      @options.animate = value
       @$element
 
     toggleAnimate: ->
-      @$wrapper.toggleClass "#{@options.baseClass}-animate"
       @options.animate = not @options.animate
+
+      @$wrapper.toggleClass "#{@options.baseClass}-animate"
       @$element
 
     disabled: (value) ->
-      return @options.disabled if typeof value is "undefined"
+      return @options.disabled  if typeof value is "undefined"
 
       value = not not value
+      @options.disabled = value
 
-      @$wrapper[if value then "addClass" else "removeClass"]("#{@options.baseClass}-disabled")
       @$element.prop "disabled", value
-      @options.disabled = value
+      @$wrapper[if value then "addClass" else "removeClass"]("#{@options.baseClass}-disabled")
       @$element
 
     toggleDisabled: ->
-      @$element.prop "disabled", not @options.disabled
-      @$wrapper.toggleClass "#{@options.baseClass}-disabled"
       @options.disabled = not @options.disabled
+
+      @$element.prop "disabled", @options.disabled
+      @$wrapper.toggleClass "#{@options.baseClass}-disabled"
       @$element
 
     readonly: (value) ->
-      return @options.readonly if typeof value is "undefined"
+      return @options.readonly  if typeof value is "undefined"
 
       value = not not value
+      @options.readonly = value
 
-      @$wrapper[if value then "addClass" else "removeClass"]("#{@options.baseClass}-readonly")
       @$element.prop "readonly", value
-      @options.readonly = value
+      @$wrapper[if value then "addClass" else "removeClass"]("#{@options.baseClass}-readonly")
       @$element
 
     toggleReadonly: ->
-      @$element.prop "readonly", not @options.readonly
-      @$wrapper.toggleClass "#{@options.baseClass}-readonly"
       @options.readonly = not @options.readonly
+
+      @$element.prop "readonly", @options.readonly
+      @$wrapper.toggleClass "#{@options.baseClass}-readonly"
       @$element
 
     indeterminate: (value) ->
-      return @options.indeterminate if typeof value is "undefined"
+      return @options.indeterminate  if typeof value is "undefined"
 
       value = not not value
+      @options.indeterminate = value
 
-      @$wrapper[if value then "addClass" else "removeClass"]("#{@options.baseClass}-indeterminate")
       @$element.prop "indeterminate", value
-      @options.indeterminate = value
+      @$wrapper[if value then "addClass" else "removeClass"]("#{@options.baseClass}-indeterminate")
+      @_containerPosition()
       @$element
 
     toggleIndeterminate: ->
+      @options.indeterminate = not @options.indeterminate
+
       @$element.prop "indeterminate", not @options.indeterminate
       @$wrapper.toggleClass "#{@options.baseClass}-indeterminate"
-      @options.indeterminate = not @options.indeterminate
+      @_containerPosition()
       @$element
 
     inverse: (value) ->
-      return @options.inverse if typeof value is "undefined"
+      return @options.inverse  if typeof value is "undefined"
 
       value = not not value
 
@@ -195,7 +210,7 @@ do ($ = window.jQuery, window) ->
     onColor: (value) ->
       color = @options.onColor
 
-      return color if typeof value is "undefined"
+      return color  if typeof value is "undefined"
 
       @$on.removeClass "#{@options.baseClass}-#{color}" if color?
       @$on.addClass "#{@options.baseClass}-#{value}"
@@ -205,7 +220,7 @@ do ($ = window.jQuery, window) ->
     offColor: (value) ->
       color = @options.offColor
 
-      return color if typeof value is "undefined"
+      return color  if typeof value is "undefined"
 
       @$off.removeClass "#{@options.baseClass}-#{color}" if color?
       @$off.addClass "#{@options.baseClass}-#{value}"
@@ -213,31 +228,50 @@ do ($ = window.jQuery, window) ->
       @$element
 
     onText: (value) ->
-      return @options.onText if typeof value is "undefined"
+      return @options.onText  if typeof value is "undefined"
 
       @$on.html value
+      @_width()
+      @_containerPosition()
       @options.onText = value
       @$element
 
     offText: (value) ->
-      return @options.offText if typeof value is "undefined"
+      return @options.offText  if typeof value is "undefined"
 
       @$off.html value
+      @_width()
+      @_containerPosition()
       @options.offText = value
       @$element
 
     labelText: (value) ->
-      return @options.labelText if typeof value is "undefined"
+      return @options.labelText  if typeof value is "undefined"
 
       @$label.html value
+      @_width()
       @options.labelText = value
       @$element
 
+    handleWidth: (value) ->
+      return @options.handleWidth  if typeof value is "undefined"
+
+      @options.handleWidth = value
+      @_width()
+      @$element
+
+    labelWidth: (value) ->
+      return @options.labelWidth  if typeof value is "undefined"
+
+      @options.labelWidth = value
+      @_width()
+      @$element
+
     baseClass: (value) ->
       @options.baseClass
 
     wrapperClass: (value) ->
-      return @options.wrapperClass if typeof value is "undefined"
+      return @options.wrapperClass  if typeof value is "undefined"
 
       value = $.fn.bootstrapSwitch.defaults.wrapperClass unless value
 
@@ -247,13 +281,13 @@ do ($ = window.jQuery, window) ->
       @$element
 
     radioAllOff: (value) ->
-      return @options.radioAllOff if typeof value is "undefined"
+      return @options.radioAllOff  if typeof value is "undefined"
 
       @options.radioAllOff = value
       @$element
 
     onInit: (value) ->
-      return @options.onInit if typeof value is "undefined"
+      return @options.onInit  if typeof value is "undefined"
 
       value = $.fn.bootstrapSwitch.defaults.onInit unless value
 
@@ -261,7 +295,7 @@ do ($ = window.jQuery, window) ->
       @$element
 
     onSwitchChange: (value) ->
-      return @options.onSwitchChange if typeof value is "undefined"
+      return @options.onSwitchChange  if typeof value is "undefined"
 
       value = $.fn.bootstrapSwitch.defaults.onSwitchChange unless value
 
@@ -276,20 +310,64 @@ do ($ = window.jQuery, window) ->
       @$element.unwrap().unwrap().off(".bootstrapSwitch").removeData "bootstrap-switch"
       @$element
 
+    _width: ->
+      $handles = @$on.add(@$off)
+
+      # remove width from inline style
+      $handles.add(@$label).css("width", "")
+
+      # save handleWidth for further label width calculation check
+      handleWidth = if @options.handleWidth is "auto"
+      then Math.max @$on.width(), @$off.width()
+      else @options.handleWidth
+
+      # set handles width
+      $handles.width handleWidth
+
+      # set label width
+      @$label.width (index, width) =>
+        return @options.labelWidth  if @options.labelWidth isnt "auto"
+
+        if width < handleWidth then handleWidth else width
+
+      # get handle and label widths
+      @_handleWidth = @$on.outerWidth()
+      @_labelWidth = @$label.outerWidth()
+
+      # set container and wrapper widths
+      @$container.width (@_handleWidth * 2) + @_labelWidth
+      @$wrapper.width @_handleWidth + @_labelWidth
+
+    _containerPosition: (state = @options.state, callback) ->
+      @$container
+      .css "margin-left", =>
+        values = [0, "-#{@_handleWidth}px"]
+
+        return "-#{@_handleWidth / 2}px"  if @options.indeterminate
+
+        if state
+          return  if @options.inverse then values[1] else values[0]
+        else
+          return  if @options.inverse then values[0] else values[1]
+
+      return  unless callback
+
+      @$container
+      .one($.support.transition.end, callback)
+      .emulateTransitionEnd 500
+
     _elementHandlers: ->
       @$element.on
         "change.bootstrapSwitch": (e, skip) =>
           e.preventDefault()
           e.stopImmediatePropagation()
 
-          checked = @$element.is ":checked"
-
-          return if checked is @options.state
+          state = @$element.is ":checked"
+          @_containerPosition state
+          return  if state is @options.state
 
-          @options.state = checked
-          @$wrapper
-          .removeClass(if checked then "#{@options.baseClass}-off" else "#{@options.baseClass}-on")
-          .addClass if checked then "#{@options.baseClass}-on" else "#{@options.baseClass}-off"
+          @options.state = state
+          @$wrapper.toggleClass("#{@options.baseClass}-off").toggleClass "#{@options.baseClass}-on"
 
           unless skip
             if @$element.is ":radio"
@@ -297,7 +375,7 @@ do ($ = window.jQuery, window) ->
               .not(@$element)
               .prop("checked", false)
               .trigger "change.bootstrapSwitch", true
-            @$element.trigger "switchChange.bootstrapSwitch", [checked]
+            @$element.trigger "switchChange.bootstrapSwitch", [state]
 
         "focus.bootstrapSwitch": (e) =>
           e.preventDefault()
@@ -308,7 +386,7 @@ do ($ = window.jQuery, window) ->
           @$wrapper.removeClass "#{@options.baseClass}-focused"
 
         "keydown.bootstrapSwitch": (e) =>
-          return if not e.which or @options.disabled or @options.readonly
+          return  if not e.which or @options.disabled or @options.readonly
 
           switch e.which
             when 37
@@ -333,49 +411,41 @@ do ($ = window.jQuery, window) ->
 
     _labelHandlers: ->
       @$label.on
-        "mousemove.bootstrapSwitch touchmove.bootstrapSwitch": (e) =>
-          return unless @isLabelDragging
+        "mousedown.bootstrapSwitch touchstart.bootstrapSwitch": (e) =>
+          return  if @_dragStart or @options.disabled or @options.readonly
 
           e.preventDefault()
 
-          @isLabelDragged = true
-          pageX = e.pageX or e.originalEvent.touches[0].pageX
-          percent = ((pageX - @$wrapper.offset().left) / @$wrapper.width()) * 100
-          left = 25
-          right = 75
-
-          @$wrapper.removeClass "#{@options.baseClass}-animate" if @options.animate
-          if percent < left
-            percent = left
-          else if percent > right
-            percent = right
-
-          @$container.css "margin-left", "#{percent - right}%"
+          @_dragStart = (e.pageX or e.originalEvent.touches[0].pageX) - parseInt @$container.css("margin-left"), 10
+          @$wrapper.removeClass "#{@options.baseClass}-animate"  if @options.animate
           @$element.trigger "focus.bootstrapSwitch"
 
-        "mousedown.bootstrapSwitch touchstart.bootstrapSwitch": (e) =>
-          return if @isLabelDragging or @options.disabled or @options.readonly
+        "mousemove.bootstrapSwitch touchmove.bootstrapSwitch": (e) =>
+          return  unless @_dragStart?
 
           e.preventDefault()
 
-          @isLabelDragging = true
-          @$element.trigger "focus.bootstrapSwitch"
+          difference = (e.pageX or e.originalEvent.touches[0].pageX) - @_dragStart
+          return  if difference < -@_handleWidth or difference > 0
+
+          @_dragEnd = difference
+          @$container.css "margin-left", "#{@_dragEnd}px"
 
         "mouseup.bootstrapSwitch touchend.bootstrapSwitch": (e) =>
-          return unless @isLabelDragging
+          return  unless @_dragStart
 
           e.preventDefault()
 
-          if @isLabelDragged
-            state = parseInt(@$container.css("margin-left"), 10) > -(@$container.width() / 6)
+          @$wrapper.addClass "#{@options.baseClass}-animate"  if @options.animate
+          if @_dragEnd
+            state = @_dragEnd > -(@_handleWidth / 2)
 
-            @isLabelDragged = false
+            @_dragEnd = false
             @state if @options.inverse then not state else state
-            @$wrapper.addClass "#{@options.baseClass}-animate" if @options.animate
-            @$container.css "margin-left", ""
           else
             @state not @options.state
-          @isLabelDragging = false
+
+          @_dragStart = false
 
         "mouseleave.bootstrapSwitch": (e) =>
           @$label.trigger "mouseup.bootstrapSwitch"
@@ -383,7 +453,7 @@ do ($ = window.jQuery, window) ->
     _formHandler: ->
       $form = @$element.closest "form"
 
-      return if $form.data "bootstrap-switch"
+      return  if $form.data "bootstrap-switch"
 
       $form
       .on "reset.bootstrapSwitch", ->
@@ -396,7 +466,7 @@ do ($ = window.jQuery, window) ->
       .data "bootstrap-switch", true
 
     _getClasses: (classes) ->
-      return ["#{@options.baseClass}-#{classes}"] unless $.isArray classes
+      return ["#{@options.baseClass}-#{classes}"]  unless $.isArray classes
 
       cls = []
       for c in classes
@@ -409,7 +479,7 @@ do ($ = window.jQuery, window) ->
       $this = $ @
       data = $this.data "bootstrap-switch"
 
-      $this.data "bootstrap-switch", data = new BootstrapSwitch @, option unless data
+      $this.data "bootstrap-switch", data = new BootstrapSwitch @, option  unless data
       ret = data[option].apply data, args if typeof option is "string"
     ret
 
@@ -428,6 +498,8 @@ do ($ = window.jQuery, window) ->
     onText: "ON"
     offText: "OFF"
     labelText: "&nbsp;"
+    handleWidth: "auto"
+    labelWidth: "auto"
     baseClass: "bootstrap-switch"
     wrapperClass: "wrapper"
     onInit: ->

+ 37 - 4
src/docs/examples.jade

@@ -7,7 +7,7 @@ block content
     .col-sm-6.col-lg-4
       h2.h4 State
       p
-        input#switch-state(type='checkbox', checked)
+        input#switch-state(type='checkbox',)
       .btn-group
         button.btn.btn-default(type='button' data-toggle='state') Toggle
         button.btn.btn-default(type='button', data-set='state', data-value='true') Set true
@@ -105,7 +105,7 @@ block content
         input#switch-onText(type='checkbox', checked, data-on-text='Yes')
       .row
         .col-sm-6
-          input.form-control(type='text', data-set-text='onText')
+          input.form-control(type='text', data-set-value='onText', value='Yes')
 
     .col-sm-6.col-lg-4
       h2.h4 Off Text
@@ -113,7 +113,7 @@ block content
         input#switch-offText(type='checkbox', data-off-text='No')
       .row
         .col-sm-6
-          input.form-control(type='text', data-set-text='offText')
+          input.form-control(type='text', data-set-value='offText', value='No')
 
     .col-sm-6.col-lg-4
       h2.h4 Label Text
@@ -121,7 +121,23 @@ block content
         input#switch-labelText(type='checkbox', data-label-text='Label')
       .row
         .col-sm-6
-          input.form-control(type='text', data-set-text='labelText')
+          input.form-control(type='text', data-set-value='labelText')
+
+    .col-sm-6.col-lg-4
+      h2.h4 Handle Width
+      p
+        input#switch-handleWidth(type='checkbox', data-handle-width='100')
+      .row
+        .col-sm-6
+          input.form-control(type='number', data-set-value='handleWidth', value='100')
+
+    .col-sm-6.col-lg-4
+      h2.h4 Label Width
+      p
+        input#switch-labelWidth(type='checkbox', data-label-width='100')
+      .row
+        .col-sm-6
+          input.form-control(type='number', data-set-value='labelWidth', value='100')
 
   br
   br
@@ -139,3 +155,20 @@ block content
         input.switch-radio2(type='radio', name='radio2', checked, data-radio-all-off='true')
         input.switch-radio2(type='radio', name='radio2', data-radio-all-off='true')
         input.switch-radio2(type='radio', name='radio2', data-radio-all-off='true')
+
+    br
+    hr
+
+    h2.h4 Inside a Modal
+    button.btn.btn-default(data-toggle='modal', data-target='#modal-switch') Open the modal
+    .modal.fade#modal-switch(tabindex='-1', role='dialog', aria-labelledby='modal-switch-label')
+      .modal-dialog
+        .modal-content
+          .modal-header
+            button.close(type='button', data-dismiss='modal')
+              span(aria-hidden='true') &times;
+              span.sr-only Close
+            .modal-title#modal-switch-label Modal title
+            .modal-body
+              input#switch-modal(type='checkbox', checked)
+

+ 14 - 0
src/docs/options-3.jade

@@ -106,6 +106,20 @@ block content
         td Text of the center handle of the switch
         td String
         td '&amp;nbsp;'
+      tr
+        td handleWidth
+        td data-handle-width
+        td String | Number
+        td Width of the left and right sides in pixels
+        td 'auto' or Number
+        td 'auto'
+      tr
+        td labelWidth
+        td data-label-width
+        td String | Number
+        td Width of the center handle in pixels
+        td 'auto' or Number
+        td 'auto'
       tr
         td baseClass
         td data-base-class

+ 93 - 119
src/less/bootstrap3/bootstrap-switch.less

@@ -12,123 +12,10 @@
   line-height: 8px;
   .user-select(none);
   vertical-align: middle;
-  min-width: 100px;
   .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s");
 
-  &.@{bootstrap-switch-base}-mini {
-    min-width: 71px;
-
-    .@{bootstrap-switch-base}-handle-on,
-    .@{bootstrap-switch-base}-handle-off,
-    .@{bootstrap-switch-base}-label {
-      padding-bottom: 4px;
-      padding-top: 4px;
-      font-size: 10px;
-      line-height: 9px;
-    }
-  }
-
-  &.@{bootstrap-switch-base}-small {
-    min-width: 79px;
-
-    .@{bootstrap-switch-base}-handle-on,
-    .@{bootstrap-switch-base}-handle-off,
-    .@{bootstrap-switch-base}-label {
-      padding-bottom: 3px;
-      padding-top: 3px;
-      font-size: 12px;
-      line-height: 18px;
-    }
-  }
-
-  &.@{bootstrap-switch-base}-large {
-    min-width: 120px;
-
-    .@{bootstrap-switch-base}-handle-on,
-    .@{bootstrap-switch-base}-handle-off,
-    .@{bootstrap-switch-base}-label {
-      padding-bottom: 9px;
-      padding-top: 9px;
-      font-size: 16px;
-      line-height: normal;
-    }
-  }
-
-  &.@{bootstrap-switch-base}-disabled,
-  &.@{bootstrap-switch-base}-readonly,
-  &.@{bootstrap-switch-base}-indeterminate {
-    .opacity(.5);
-    cursor: default !important;
-
-    .@{bootstrap-switch-base}-handle-on,
-    .@{bootstrap-switch-base}-handle-off,
-    .@{bootstrap-switch-base}-label {
-      cursor: default !important;
-    }
-  }
-
-  &.@{bootstrap-switch-base}-animate {
-
-    .@{bootstrap-switch-base}-container {
-      .transition(margin-left .5s);
-    }
-  }
-
-  &.@{bootstrap-switch-base}-inverse {
-
-    .@{bootstrap-switch-base}-handle-on {
-      .border-left-radius(0);
-      .border-right-radius(@border-radius-base - 1);
-    }
-
-    .@{bootstrap-switch-base}-handle-off {
-      .border-right-radius(0);
-      .border-left-radius(@border-radius-base - 1);
-    }
-  }
-
-  &.@{bootstrap-switch-base}-focused {
-    @color-rgba: rgba(red(@input-border-focus), green(@input-border-focus), blue(@input-border-focus), .6);
-    border-color: @input-border-focus;
-    outline: 0;
-    .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}");
-  }
-
-  &.@{bootstrap-switch-base}-on,
-  &.@{bootstrap-switch-base}-inverse.@{bootstrap-switch-base}-off {
-
-    .@{bootstrap-switch-base}-container {
-      margin-left: 0%;
-    }
-
-    .@{bootstrap-switch-base}-label {
-      .border-right-radius(@border-radius-base - 1);
-    }
-  }
-
-  &.@{bootstrap-switch-base}-off,
-  &.@{bootstrap-switch-base}-inverse.@{bootstrap-switch-base}-on {
-
-    .@{bootstrap-switch-base}-container {
-      margin-left: -50%;
-    }
-
-    .@{bootstrap-switch-base}-label {
-      .border-left-radius(@border-radius-base - 1);
-    }
-  }
-
-  &.@{bootstrap-switch-base}-indeterminate,
-  &.@{bootstrap-switch-base}-inverse.@{bootstrap-switch-base}-indeterminate {
-
-    .@{bootstrap-switch-base}-container {
-      margin-left: -25%;
-    }
-  }
-
   .@{bootstrap-switch-base}-container {
     display: inline-block;
-    width: 150%;
     top: 0;
     border-radius: @border-radius-base;
     .translate3d(0, 0, 0);
@@ -141,17 +28,15 @@
     cursor: pointer;
     display: inline-block !important;
     height: 100%;
-    padding-bottom: 4px;
-    padding-top: 4px;
-    font-size: 14px;
-    line-height: 20px;
+    padding: @padding-base-vertical @padding-base-horizontal;
+    font-size: @font-size-base;
+    line-height: @line-height-computed;
   }
 
   .@{bootstrap-switch-base}-handle-on,
   .@{bootstrap-switch-base}-handle-off {
     text-align: center;
     z-index: 1;
-    width: 33.333333333%;
 
     &.@{bootstrap-switch-base}-primary {
       color: #fff;
@@ -189,7 +74,6 @@
     margin-top: -1px;
     margin-bottom: -1px;
     z-index: 100;
-    width: 33.333333333%;
     color: @btn-default-color;
     background: @btn-default-bg;
   }
@@ -214,4 +98,94 @@
       height: auto;
     }
   }
+
+  &.@{bootstrap-switch-base}-mini {
+
+    .@{bootstrap-switch-base}-handle-on,
+    .@{bootstrap-switch-base}-handle-off,
+    .@{bootstrap-switch-base}-label {
+      padding: @padding-xs-vertical @padding-xs-horizontal;
+      font-size: @font-size-small;
+      line-height: @line-height-small;
+    }
+  }
+
+  &.@{bootstrap-switch-base}-small {
+
+    .@{bootstrap-switch-base}-handle-on,
+    .@{bootstrap-switch-base}-handle-off,
+    .@{bootstrap-switch-base}-label {
+      padding: @padding-small-vertical @padding-small-horizontal;
+      font-size: @font-size-small;
+      line-height: @line-height-small;
+    }
+  }
+
+  &.@{bootstrap-switch-base}-large {
+
+    .@{bootstrap-switch-base}-handle-on,
+    .@{bootstrap-switch-base}-handle-off,
+    .@{bootstrap-switch-base}-label {
+      padding: @padding-base-vertical @padding-large-horizontal;
+      font-size: @font-size-large;
+      line-height: @line-height-large;
+    }
+  }
+
+  &.@{bootstrap-switch-base}-disabled,
+  &.@{bootstrap-switch-base}-readonly,
+  &.@{bootstrap-switch-base}-indeterminate {
+    cursor: default !important;
+
+    .@{bootstrap-switch-base}-handle-on,
+    .@{bootstrap-switch-base}-handle-off,
+    .@{bootstrap-switch-base}-label {
+      .opacity(.5);
+      cursor: default !important;
+    }
+  }
+
+  &.@{bootstrap-switch-base}-animate {
+
+    .@{bootstrap-switch-base}-container {
+      .transition(margin-left .5s);
+    }
+  }
+
+  &.@{bootstrap-switch-base}-inverse {
+
+    .@{bootstrap-switch-base}-handle-on {
+      .border-left-radius(0);
+      .border-right-radius(@border-radius-base - 1);
+    }
+
+    .@{bootstrap-switch-base}-handle-off {
+      .border-right-radius(0);
+      .border-left-radius(@border-radius-base - 1);
+    }
+  }
+
+  &.@{bootstrap-switch-base}-focused {
+    @color-rgba: rgba(red(@input-border-focus), green(@input-border-focus), blue(@input-border-focus), .6);
+    border-color: @input-border-focus;
+    outline: 0;
+    .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}");
+  }
+
+  &.@{bootstrap-switch-base}-on,
+  &.@{bootstrap-switch-base}-inverse.@{bootstrap-switch-base}-off {
+
+    .@{bootstrap-switch-base}-label {
+      .border-right-radius(@border-radius-base - 1);
+    }
+  }
+
+  &.@{bootstrap-switch-base}-off,
+  &.@{bootstrap-switch-base}-inverse.@{bootstrap-switch-base}-on {
+
+
+    .@{bootstrap-switch-base}-label {
+      .border-left-radius(@border-radius-base - 1);
+    }
+  }
 }

+ 139 - 53
test/bootstrap-switch.js

@@ -45,6 +45,8 @@
           onText: this.$element.data("on-text"),
           offText: this.$element.data("off-text"),
           labelText: this.$element.data("label-text"),
+          handleWidth: this.$element.data("handle-width"),
+          labelWidth: this.$element.data("label-width"),
           baseClass: this.$element.data("base-class"),
           wrapperClass: this.$element.data("wrapper-class")
         }, options);
@@ -57,9 +59,6 @@
               if (_this.options.size != null) {
                 classes.push("" + _this.options.baseClass + "-" + _this.options.size);
               }
-              if (_this.options.animate) {
-                classes.push("" + _this.options.baseClass + "-animate");
-              }
               if (_this.options.disabled) {
                 classes.push("" + _this.options.baseClass + "-disabled");
               }
@@ -94,9 +93,6 @@
           html: this.options.labelText,
           "class": "" + this.options.baseClass + "-label"
         });
-        if (this.options.indeterminate) {
-          this.$element.prop("indeterminate", true);
-        }
         this.$element.on("init.bootstrapSwitch", (function(_this) {
           return function() {
             return _this.options.onInit.apply(element, arguments);
@@ -109,11 +105,23 @@
         })(this));
         this.$container = this.$element.wrap(this.$container).parent();
         this.$wrapper = this.$container.wrap(this.$wrapper).parent();
-        this.$element.before(this.options.inverse ? this.$off : this.$on).before(this.$label).before(this.options.inverse ? this.$on : this.$off).trigger("init.bootstrapSwitch");
+        this.$element.before(this.options.inverse ? this.$off : this.$on).before(this.$label).before(this.options.inverse ? this.$on : this.$off);
+        if (this.options.indeterminate) {
+          this.$element.prop("indeterminate", true);
+        }
+        this._width();
+        this._containerPosition(this.options.state, (function(_this) {
+          return function() {
+            if (_this.options.animate) {
+              return _this.$wrapper.addClass("" + _this.options.baseClass + "-animate");
+            }
+          };
+        })(this));
         this._elementHandlers();
         this._handleHandlers();
         this._labelHandlers();
         this._formHandler();
+        this.$element.trigger("init.bootstrapSwitch");
       }
 
       BootstrapSwitch.prototype._constructor = BootstrapSwitch;
@@ -160,6 +168,7 @@
         if (value) {
           this.$wrapper.addClass("" + this.options.baseClass + "-" + value);
         }
+        this._width();
         this.options.size = value;
         return this.$element;
       };
@@ -169,14 +178,14 @@
           return this.options.animate;
         }
         value = !!value;
-        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-animate");
         this.options.animate = value;
+        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-animate");
         return this.$element;
       };
 
       BootstrapSwitch.prototype.toggleAnimate = function() {
-        this.$wrapper.toggleClass("" + this.options.baseClass + "-animate");
         this.options.animate = !this.options.animate;
+        this.$wrapper.toggleClass("" + this.options.baseClass + "-animate");
         return this.$element;
       };
 
@@ -185,16 +194,16 @@
           return this.options.disabled;
         }
         value = !!value;
-        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-disabled");
-        this.$element.prop("disabled", value);
         this.options.disabled = value;
+        this.$element.prop("disabled", value);
+        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-disabled");
         return this.$element;
       };
 
       BootstrapSwitch.prototype.toggleDisabled = function() {
-        this.$element.prop("disabled", !this.options.disabled);
-        this.$wrapper.toggleClass("" + this.options.baseClass + "-disabled");
         this.options.disabled = !this.options.disabled;
+        this.$element.prop("disabled", this.options.disabled);
+        this.$wrapper.toggleClass("" + this.options.baseClass + "-disabled");
         return this.$element;
       };
 
@@ -203,16 +212,16 @@
           return this.options.readonly;
         }
         value = !!value;
-        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-readonly");
-        this.$element.prop("readonly", value);
         this.options.readonly = value;
+        this.$element.prop("readonly", value);
+        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-readonly");
         return this.$element;
       };
 
       BootstrapSwitch.prototype.toggleReadonly = function() {
-        this.$element.prop("readonly", !this.options.readonly);
-        this.$wrapper.toggleClass("" + this.options.baseClass + "-readonly");
         this.options.readonly = !this.options.readonly;
+        this.$element.prop("readonly", this.options.readonly);
+        this.$wrapper.toggleClass("" + this.options.baseClass + "-readonly");
         return this.$element;
       };
 
@@ -221,16 +230,18 @@
           return this.options.indeterminate;
         }
         value = !!value;
-        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-indeterminate");
-        this.$element.prop("indeterminate", value);
         this.options.indeterminate = value;
+        this.$element.prop("indeterminate", value);
+        this.$wrapper[value ? "addClass" : "removeClass"]("" + this.options.baseClass + "-indeterminate");
+        this._containerPosition();
         return this.$element;
       };
 
       BootstrapSwitch.prototype.toggleIndeterminate = function() {
+        this.options.indeterminate = !this.options.indeterminate;
         this.$element.prop("indeterminate", !this.options.indeterminate);
         this.$wrapper.toggleClass("" + this.options.baseClass + "-indeterminate");
-        this.options.indeterminate = !this.options.indeterminate;
+        this._containerPosition();
         return this.$element;
       };
 
@@ -297,6 +308,8 @@
           return this.options.onText;
         }
         this.$on.html(value);
+        this._width();
+        this._containerPosition();
         this.options.onText = value;
         return this.$element;
       };
@@ -306,6 +319,8 @@
           return this.options.offText;
         }
         this.$off.html(value);
+        this._width();
+        this._containerPosition();
         this.options.offText = value;
         return this.$element;
       };
@@ -315,10 +330,29 @@
           return this.options.labelText;
         }
         this.$label.html(value);
+        this._width();
         this.options.labelText = value;
         return this.$element;
       };
 
+      BootstrapSwitch.prototype.handleWidth = function(value) {
+        if (typeof value === "undefined") {
+          return this.options.handleWidth;
+        }
+        this.options.handleWidth = value;
+        this._width();
+        return this.$element;
+      };
+
+      BootstrapSwitch.prototype.labelWidth = function(value) {
+        if (typeof value === "undefined") {
+          return this.options.labelWidth;
+        }
+        this.options.labelWidth = value;
+        this._width();
+        return this.$element;
+      };
+
       BootstrapSwitch.prototype.baseClass = function(value) {
         return this.options.baseClass;
       };
@@ -377,24 +411,81 @@
         return this.$element;
       };
 
+      BootstrapSwitch.prototype._width = function() {
+        var $handles, handleWidth;
+        $handles = this.$on.add(this.$off);
+        $handles.add(this.$label).css("width", "");
+        handleWidth = this.options.handleWidth === "auto" ? Math.max(this.$on.width(), this.$off.width()) : this.options.handleWidth;
+        $handles.width(handleWidth);
+        this.$label.width((function(_this) {
+          return function(index, width) {
+            if (_this.options.labelWidth !== "auto") {
+              return _this.options.labelWidth;
+            }
+            if (width < handleWidth) {
+              return handleWidth;
+            } else {
+              return width;
+            }
+          };
+        })(this));
+        this._handleWidth = this.$on.outerWidth();
+        this._labelWidth = this.$label.outerWidth();
+        this.$container.width((this._handleWidth * 2) + this._labelWidth);
+        return this.$wrapper.width(this._handleWidth + this._labelWidth);
+      };
+
+      BootstrapSwitch.prototype._containerPosition = function(state, callback) {
+        if (state == null) {
+          state = this.options.state;
+        }
+        this.$container.css("margin-left", (function(_this) {
+          return function() {
+            var values;
+            values = [0, "-" + _this._handleWidth + "px"];
+            if (_this.options.indeterminate) {
+              return "-" + (_this._handleWidth / 2) + "px";
+            }
+            if (state) {
+              if (_this.options.inverse) {
+                return values[1];
+              } else {
+                return values[0];
+              }
+            } else {
+              if (_this.options.inverse) {
+                return values[0];
+              } else {
+                return values[1];
+              }
+            }
+          };
+        })(this));
+        if (!callback) {
+          return;
+        }
+        return this.$container.one($.support.transition.end, callback).emulateTransitionEnd(500);
+      };
+
       BootstrapSwitch.prototype._elementHandlers = function() {
         return this.$element.on({
           "change.bootstrapSwitch": (function(_this) {
             return function(e, skip) {
-              var checked;
+              var state;
               e.preventDefault();
               e.stopImmediatePropagation();
-              checked = _this.$element.is(":checked");
-              if (checked === _this.options.state) {
+              state = _this.$element.is(":checked");
+              _this._containerPosition(state);
+              if (state === _this.options.state) {
                 return;
               }
-              _this.options.state = checked;
-              _this.$wrapper.removeClass(checked ? "" + _this.options.baseClass + "-off" : "" + _this.options.baseClass + "-on").addClass(checked ? "" + _this.options.baseClass + "-on" : "" + _this.options.baseClass + "-off");
+              _this.options.state = state;
+              _this.$wrapper.toggleClass("" + _this.options.baseClass + "-off").toggleClass("" + _this.options.baseClass + "-on");
               if (!skip) {
                 if (_this.$element.is(":radio")) {
                   $("[name='" + (_this.$element.attr('name')) + "']").not(_this.$element).prop("checked", false).trigger("change.bootstrapSwitch", true);
                 }
-                return _this.$element.trigger("switchChange.bootstrapSwitch", [checked]);
+                return _this.$element.trigger("switchChange.bootstrapSwitch", [state]);
               }
             };
           })(this),
@@ -447,59 +538,52 @@
 
       BootstrapSwitch.prototype._labelHandlers = function() {
         return this.$label.on({
-          "mousemove.bootstrapSwitch touchmove.bootstrapSwitch": (function(_this) {
+          "mousedown.bootstrapSwitch touchstart.bootstrapSwitch": (function(_this) {
             return function(e) {
-              var left, pageX, percent, right;
-              if (!_this.isLabelDragging) {
+              if (_this._dragStart || _this.options.disabled || _this.options.readonly) {
                 return;
               }
               e.preventDefault();
-              _this.isLabelDragged = true;
-              pageX = e.pageX || e.originalEvent.touches[0].pageX;
-              percent = ((pageX - _this.$wrapper.offset().left) / _this.$wrapper.width()) * 100;
-              left = 25;
-              right = 75;
+              _this._dragStart = (e.pageX || e.originalEvent.touches[0].pageX) - parseInt(_this.$container.css("margin-left"), 10);
               if (_this.options.animate) {
                 _this.$wrapper.removeClass("" + _this.options.baseClass + "-animate");
               }
-              if (percent < left) {
-                percent = left;
-              } else if (percent > right) {
-                percent = right;
-              }
-              _this.$container.css("margin-left", "" + (percent - right) + "%");
               return _this.$element.trigger("focus.bootstrapSwitch");
             };
           })(this),
-          "mousedown.bootstrapSwitch touchstart.bootstrapSwitch": (function(_this) {
+          "mousemove.bootstrapSwitch touchmove.bootstrapSwitch": (function(_this) {
             return function(e) {
-              if (_this.isLabelDragging || _this.options.disabled || _this.options.readonly) {
+              var difference;
+              if (_this._dragStart == null) {
                 return;
               }
               e.preventDefault();
-              _this.isLabelDragging = true;
-              return _this.$element.trigger("focus.bootstrapSwitch");
+              difference = (e.pageX || e.originalEvent.touches[0].pageX) - _this._dragStart;
+              if (difference < -_this._handleWidth || difference > 0) {
+                return;
+              }
+              _this._dragEnd = difference;
+              return _this.$container.css("margin-left", "" + _this._dragEnd + "px");
             };
           })(this),
           "mouseup.bootstrapSwitch touchend.bootstrapSwitch": (function(_this) {
             return function(e) {
               var state;
-              if (!_this.isLabelDragging) {
+              if (!_this._dragStart) {
                 return;
               }
               e.preventDefault();
-              if (_this.isLabelDragged) {
-                state = parseInt(_this.$container.css("margin-left"), 10) > -(_this.$container.width() / 6);
-                _this.isLabelDragged = false;
+              if (_this.options.animate) {
+                _this.$wrapper.addClass("" + _this.options.baseClass + "-animate");
+              }
+              if (_this._dragEnd) {
+                state = _this._dragEnd > -(_this._handleWidth / 2);
+                _this._dragEnd = false;
                 _this.state(_this.options.inverse ? !state : state);
-                if (_this.options.animate) {
-                  _this.$wrapper.addClass("" + _this.options.baseClass + "-animate");
-                }
-                _this.$container.css("margin-left", "");
               } else {
                 _this.state(!_this.options.state);
               }
-              return _this.isLabelDragging = false;
+              return _this._dragStart = false;
             };
           })(this),
           "mouseleave.bootstrapSwitch": (function(_this) {
@@ -575,6 +659,8 @@
       onText: "ON",
       offText: "OFF",
       labelText: "&nbsp;",
+      handleWidth: "auto",
+      labelWidth: "auto",
       baseClass: "bootstrap-switch",
       wrapperClass: "wrapper",
       onInit: function() {},

Some files were not shown because too many files changed in this diff