formstone
Version:
Library of modular front end components.
462 lines (370 loc) • 12.5 kB
JavaScript
/* global define */
(function(factory) {
if (typeof define === "function" && define.amd) {
define([
"jquery",
"./core",
"./touch"
], factory);
} else {
factory(jQuery, Formstone);
}
}(function($, Formstone) {
"use strict";
/**
* @method private
* @name resize
* @description Handles window resize
*/
function resize(windowWidth) {
Functions.iterate.call($Instances, resizeInstance);
}
/**
* @method private
* @name cacheInstances
* @description Caches active instances
*/
function cacheInstances() {
$Instances = $(Classes.element);
}
/**
* @method private
* @name construct
* @description Builds instance.
* @param data [object] "Instance data"
*/
function construct(data) {
if (!data.formatter) {
data.formatter = formatNumber;
}
data.min = parseFloat(this.attr("min")) || 0;
data.max = parseFloat(this.attr("max")) || 100;
data.step = parseFloat(this.attr("step")) || 1;
data.digits = data.step.toString().length - data.step.toString().indexOf(".");
data.value = parseFloat(this.val()) || (data.min + ((data.max - data.min) / 2));
var html = "";
// Not valid in the spec
data.vertical = this.attr("orient") === "vertical" || data.vertical;
data.disabled = this.is(":disabled") || this.is("[readonly]");
html += '<div class="' + RawClasses.track + '" aria-hidden="true">';
if (data.fill) {
html += '<span class="' + RawClasses.fill + '"></span>';
}
html += '<div class="' + RawClasses.handle + '" role="slider">';
html += '<span class="' + RawClasses.marker + '"></span>';
html += '</div>';
html += '</div>';
var baseClasses = [
RawClasses.base,
data.theme,
data.customClass,
(data.vertical) ? RawClasses.vertical : "",
(data.labels) ? RawClasses.labels : "",
(data.disabled) ? RawClasses.disabled : ""
];
this.addClass(RawClasses.element)
.wrap('<div class="' + baseClasses.join(" ") + '"></div>')
.after(html);
data.$container = this.parents(Classes.base);
data.$track = data.$container.find(Classes.track);
data.$fill = data.$container.find(Classes.fill);
data.$handle = data.$container.find(Classes.handle);
data.$output = data.$container.find(Classes.output);
if (data.labels) {
var labelMax = '<span class="' + [RawClasses.label, RawClasses.label_max].join(" ") + '">' + data.formatter.call(this, (data.labels.max) ? data.labels.max : data.max) + '</span>',
labelMin = '<span class="' + [RawClasses.label, RawClasses.label_min].join(" ") + '">' + data.formatter.call(this, (data.labels.max) ? data.labels.min : data.min) + '</span>';
data.$container.prepend((data.vertical) ? labelMax : labelMin)
.append((data.vertical) ? labelMin : labelMax);
}
data.$labels = data.$container.find(Classes.label);
// Bind click events
this.on(Events.focus, data, onFocus)
.on(Events.blur, data, onBlur)
.on(Events.change, data, onChange);
data.$container.fsTouch({
pan: true,
axis: data.vertical ? "y" : "x"
}).on(Events.panStart, data, onPanStart)
.on(Events.pan, data, onPan)
.on(Events.panEnd, data, onPanEnd);
cacheInstances();
resizeInstance.call(this, data);
}
/**
* @method private
* @name destruct
* @description Tears down instance.
* @param data [object] "Instance data"
*/
function destruct(data) {
data.$container.off(Events.namespace)
.fsTouch("destroy");
data.$track.remove();
data.$labels.remove();
this.unwrap()
.removeClass(RawClasses.element)
.off(Events.namespace);
cacheInstances();
}
/**
* @method
* @name enable
* @description Enables target instance
* @example $(".target").range("enable");
*/
function enable(data) {
if (data.disabled) {
this.prop("disabled", false);
data.$container.removeClass(RawClasses.disabled);
data.disabled = false;
}
}
/**
* @method
* @name disable
* @description Disables target instance
* @example $(".target").range("disable");
*/
function disable(data) {
if (!data.disabled) {
this.prop("disabled", true);
data.$container.addClass(RawClasses.disabled);
data.disabled = true;
}
}
/**
* @method
* @name update
* @description Updates instance.
* @example $(".target").range("update");
*/
function updateInstance(data) {
data.min = parseFloat(data.$el.attr("min")) || 0;
data.max = parseFloat(data.$el.attr("max")) || 100;
data.step = parseFloat(data.$el.attr("step")) || 1;
data.digits = data.step.toString().length - data.step.toString().indexOf(".");
data.value = parseFloat(this.val()) || (data.min + ((data.max - data.min) / 2));
if (data.labels) {
data.$labels.filter(Classes.label_max).html(data.formatter.call(this, (data.labels.max) ? data.labels.max : data.max));
data.$labels.filter(Classes.label_min).html(data.formatter.call(this, (data.labels.max) ? data.labels.min : data.min));
}
resizeInstance.call(this, data);
}
/**
* @method
* @name resize
* @description Resizes instance
* @example $(".target").range("resize");
*/
/**
* @method private
* @name resizeInstance
* @description Resizes each instance
* @param data [object] "Instance data"
*/
function resizeInstance(data) {
data.stepCount = (data.max - data.min) / data.step;
data.offset = data.$track.offset();
if (data.vertical) {
data.trackHeight = data.$track.outerHeight();
data.handleHeight = data.$handle.outerHeight();
data.increment = data.trackHeight / data.stepCount;
} else {
data.trackWidth = data.$track.outerWidth();
data.handleWidth = data.$handle.outerWidth();
data.increment = data.trackWidth / data.stepCount;
}
var percent = (data.$el.val() - data.min) / (data.max - data.min);
position(data, percent, true); // isResize
}
/**
* @method private
* @name onTrackDown
* @description Handles panstart event to track
* @param e [object] "Event data"
*/
function onPanStart(e) {
Functions.killEvent(e);
var data = e.data;
if (!data.disabled) {
resizeInstance(data);
onPan(e);
data.$container.addClass(RawClasses.focus);
}
}
/**
* @method private
* @name onPan
* @description Handles pan event
* @param e [object] "Event data"
*/
function onPan(e) {
Functions.killEvent();
var data = e.data,
percent = 0;
if (!data.disabled) {
if (data.vertical) {
percent = 1 - (e.pageY - data.offset.top) / data.trackHeight;
} else {
percent = (e.pageX - data.offset.left) / data.trackWidth;
}
position(data, percent);
}
}
/**
* @method private
* @name onPanEnd
* @description Handles panend event
* @param e [object] "Event data"
*/
function onPanEnd(e) {
Functions.killEvent(e);
var data = e.data;
if (!data.disabled) {
data.$container.removeClass(RawClasses.focus);
}
}
/**
* @method private
* @name onFocus
* @description Handles instance focus
* @param e [object] "Event data"
*/
function onFocus(e) {
e.data.$container.addClass(RawClasses.focus);
}
/**
* @method private
* @name onBlur
* @description Handles instance blur
* @param e [object] "Event data"
*/
function onBlur(e) {
e.data.$container.removeClass(RawClasses.focus);
}
/**
* @method private
* @name position
* @description Positions handle
* @param data [object] "Instance Data"
* @param perc [number] "Position precentage"
* @param isResize [boolean] "Called from resize"
*/
function position(data, perc, isResize) {
if (data.increment > 1) {
if (data.vertical) {
perc = (Math.round(perc * data.stepCount) * data.increment) / data.trackHeight;
} else {
perc = (Math.round(perc * data.stepCount) * data.increment) / data.trackWidth;
}
}
if (perc < 0) {
perc = 0;
}
if (perc > 1) {
perc = 1;
}
var value = ((data.min - data.max) * perc);
value = -parseFloat(value.toFixed(data.digits));
data.$fill.css((data.vertical) ? "height" : "width", (perc * 100) + "%");
data.$handle.css((data.vertical) ? "bottom" : "left", (perc * 100) + "%");
/* .attr("aria-valuenow", value) */
value += data.min;
if (value !== data.value && value !== false && isResize !== true) {
data.$el.val(value)
.trigger(Events.raw.change, [true]);
data.value = value;
}
}
/**
* @method private
* @name onChange
* @description Handles change events
* @param e [object] "Event data"
* @param internal [boolean] "Flag for internal change"
*/
function onChange(e, internal) {
var data = e.data;
if (!internal && !data.disabled) {
var percent = (data.$el.val() - data.min) / (data.max - data.min);
position(data, percent);
}
}
/**
* @method private
* @name formatNumber
* @description Formats provided number
* @param number [number] "Number to format"
*/
function formatNumber(number) {
var parts = number.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
/**
* @plugin
* @name Range
* @description A jQuery plugin for cross browser range inputs.
* @type widget
* @main range.js
* @main range.css
* @dependency jQuery
* @dependency core.js
* @dependency touch.js
*/
var Plugin = Formstone.Plugin("range", {
widget: true,
/**
* @options
* @param customClass [string] <''> "Class applied to instance"
* @param fill [boolean] <false> "Flag to draw fill"
* @param formatter [function] <false> "Value format function"
* @param labels [boolean] <true> "Flag to draw labels"
* @param labels.max [string] "Max value label; defaults to max value"
* @param labels.min [string] "Min value label; defaults to min value"
* @param theme [string] <"fs-light"> "Theme class name"
* @param vertical [boolean] <false> "Flag to render vertical range; Deprecated use 'orientation' attribute instead
*/
defaults: {
customClass: "",
fill: false,
formatter: false,
labels: {
max: false,
min: false
},
theme: "fs-light",
vertical: false
},
classes: [
"track",
"handle",
"fill",
"marker",
"labels",
"label",
"label_min",
"label_max",
"vertical",
"focus",
"disabled"
],
methods: {
_construct: construct,
_destruct: destruct,
_resize: resize,
// Public Methods
enable: enable,
disable: disable,
resize: resizeInstance,
update: updateInstance
}
}),
// Localize References
Classes = Plugin.classes,
RawClasses = Classes.raw,
Events = Plugin.events,
Functions = Plugin.functions,
$Instances = [];
})
);