mobius1-selectr
Version:
A lightweight, dependency-free, mobile-friendly javascript select box replacement.
583 lines (463 loc) • 14.8 kB
JavaScript
// Avoid `console` errors in browsers that lack a console.
(function() {
var method;
var noop = function () {};
var methods = [
'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',
'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',
'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn'
];
var length = methods.length;
var console = (window.console = window.console || {});
while (length--) {
method = methods[length];
// Only stub undefined methods.
if (!console[method]) {
console[method] = noop;
}
}
}());
// Plugins
(function(global) {
'use strict';
var defaultConfig = {
size: 10,
scrollY: true,
scrollX: false,
responsive: false
};
/**
* Attach removable event listener
* @param {Object} el HTMLElement
* @param {String} type Event type
* @param {Function} callback Event callback
* @param {Object} scope Function scope
* @return {Void}
*/
function on(el, type, callback) {
el.addEventListener(type, callback, false);
}
/**
* Remove event listener
* @param {Object} el HTMLElement
* @param {String} type Event type
* @param {Function} callback Event callback
* @return {Void}
*/
function off(el, type, callback) {
el.removeEventListener(type, callback);
}
/**
* Iterator helper
* @param {(Array|Object)} collection Any object, array or array-like collection.
* @param {Function} callback The callback function
* @param {Object} scope Change the value of this
* @return {Void}
*/
function each(collection, callback, scope) {
if ("[object Object]" === Object.prototype.toString.call(collection)) {
for (var d in collection) {
if (Object.prototype.hasOwnProperty.call(collection, d)) {
callback.call(scope, d, collection[d]);
}
}
} else {
for (var e = 0, f = collection.length; e < f; e++) {
callback.call(scope, e, collection[e]);
}
}
}
/**
* Merge objects together into the first.
* @param {Object} src Source object
* @param {Object} obj Object to merge into source object
* @return {Object}
*/
function extend(src, props) {
props = props || {};
var p;
for (p in src) {
if (src.hasOwnProperty(p)) {
if (!props.hasOwnProperty(p)) {
props[p] = src[p];
}
}
}
return props;
}
/**
* Create new element and apply propertiess and attributes
* @param {String} name The new element's nodeName
* @param {Object} prop CSS properties and values
* @return {Object} The newly create HTMLElement
*/
function createElement(name, props) {
var c = document,
d = c.createElement(name);
if (props && "[object Object]" === Object.prototype.toString.call(props)) {
var e;
for (e in props)
if ("html" === e) d.innerHTML = props[e];
else if ("text" === e) {
var f = c.createTextNode(props[e]);
d.appendChild(f);
} else d.setAttribute(e, props[e]);
}
return d;
}
/**
* Emulate jQuery's css method
* @param {Object} el HTMLElement
* @param {Object} prop CSS properties and values
* @return {Object|Void}
*/
function style(el, obj) {
if ( !obj ) {
return window.getComputedStyle(el);
}
if ("[object Object]" === Object.prototype.toString.call(obj)) {
var s = "";
each(obj, function(prop, val) {
if ( typeof val !== "string" && prop !== "opacity" ) {
val += "px";
}
s += prop + ": " + val + ";";
});
el.style.cssText += s;
}
}
/**
* Find the closest matching ancestor to a given element
* @param {Object} el HTMLElement
* @param {Function} fn Callback
* @return {Boolean|Object} The matching HTMLElement or false
*/
function closest(el, fn) {
return el && el !== document.body && (fn(el) ? el : closest(el.parentNode, fn));
}
/**
* Get an element's DOMRect relative to the document instead of the viewport.
* @param {Object} t HTMLElement
* @param {Boolean} e Include margins
* @return {Object} Formatted DOMRect copy
*/
function rect(t, e) {
var o = window,
r = t.getBoundingClientRect(),
x = o.pageXOffset,
y = o.pageYOffset,
m = {},
f = "none";
if (e) {
var s = style(t);
m = {
top: parseInt(s["margin-top"], 10),
left: parseInt(s["margin-left"], 10),
right: parseInt(s["margin-right"], 10),
bottom: parseInt(s["margin-bottom"], 10)
};
f = s.float;
}
return {
w: r.width,
h: r.height,
x1: r.left + x,
x2: r.right + x,
y1: r.top + y,
y2: r.bottom + y,
margin: m,
float: f
};
}
function debounce(a, b, c) {
var d;
return function() {
var e = this,
f = arguments,
g = function() {
d = null;
if (!c) a.apply(e, f);
},
h = c && !d;
clearTimeout(d);
d = setTimeout(g, b);
if (h) {
a.apply(e, f);
}
};
}
/**
* requestAnimationFrame Polyfill
*/
var raf = window.requestAnimationFrame || (function() {
var timeLast = 0;
return window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) {
var timeCurrent = (new Date()).getTime(),
timeDelta;
/* Dynamically set the delay on a per-tick basis to more closely match 60fps. */
/* Technique by Erik Moller. MIT license: https://gist.github.com/paulirish/1579671. */
timeDelta = Math.max(0, 16 - (timeCurrent - timeLast));
timeLast = timeCurrent + timeDelta;
return setTimeout(function() { callback(timeCurrent + timeDelta); }, timeDelta);
};
})();
function round(value, precision) {
var m = Math.pow(10, precision || 0);
return Math.round(value * m) / m;
}
/**
* Get native scrollbar width
* @return {Number} Scrollbar width
*/
function getScrollBarWidth() {
var width = 0, div = createElement("div", { class: "scrollbar-measure" });
style(div, {
width: 100,
height: 100,
overflow: "scroll",
position: "absolute",
top: -9999
});
document.body.appendChild(div);
width = div.offsetWidth - div.clientWidth;
document.body.removeChild(div);
return width;
}
function Scrollr(el, options) {
this.el = el;
if ( typeof el === "string" ) {
this.el = document.querySelector(el);
}
this.config = extend(defaultConfig, options);
this.render();
}
Scrollr.prototype.render = function() {
var that = this;
if ( this.rendered ) return false;
this.size = getScrollBarWidth();
this.wrapper = createElement("div", {
class: "scrollr-wrapper"
});
this.el.classList.add("scrollr-content");
this.railContainer = createElement("div", {
class: "scrollr-rails"
});
this.rails = {
x: { node: createElement("div", { class: "scrollr-rail scrollr-rail-x" }) },
y: { node: createElement("div", { class: "scrollr-rail scrollr-rail-y" }) }
}
this.bars = {
x: { node: createElement("div", { class: "scrollr-bar" }) },
y: { node: createElement("div", { class: "scrollr-bar" }) }
}
this.rails.x.node.appendChild(this.bars.x.node);
this.rails.y.node.appendChild(this.bars.y.node);
this.railContainer.appendChild(this.rails.x.node);
this.railContainer.appendChild(this.rails.y.node);
this.el.parentNode.replaceChild(this.wrapper, this.el);
this.wrapper.appendChild(this.el);
this.wrapper.appendChild(this.railContainer);
// Bind events
this.events = {};
this.events.move = this.move.bind(this);
this.events.drag = this.drag.bind(this);
this.events.stop = this.stop.bind(this);
this.events.down = this.down.bind(this);
this.events.update = this.update.bind(this);
this.events.debounce = debounce(this.events.update, 50);
on(this.el, "scroll", this.events.move);
on(this.el, "mouseenter", this.events.move);
on(this.wrapper, "mousedown", this.events.down);
on(document, 'mousemove', this.events.drag);
on(document, 'mouseup', this.events.stop);
on(this.wrapper, "selectstart", function(e) {
if ( that.dragging ) {
e.preventDefault();
}
});
on(window, 'resize', this.events.debounce);
on(document, 'DOMContentLoaded', this.events.update);
this.update();
this.rendered = true;
};
Scrollr.prototype.destroy = function() {
if ( !this.rendered ) return false;
this.el.classList.remove("scrollr-content");
this.wrapper.parentNode.replaceChild(this.el, this.wrapper);
off(this.el, "scroll", this.events.move);
off(this.el, "mouseenter", this.events.move);
off(document, 'mousemove', this.events.drag);
off(document, 'mouseup', this.events.stop);
off(window, 'resize', this.events.debounce);
off(document, 'DOMContentLoaded', this.events.update);
this.rendered = false;
};
Scrollr.prototype.move = function() {
var that = this;
if ( !that.dragging ) {
var scrollTop = that.el.scrollTop;
var scrollLeft = that.el.scrollLeft;
that.data.direction.x = false;
that.data.direction.y = false;
// Scrolling x
if ( scrollLeft !== that.data.scrollLeft ) {
that.data.direction.x = true;
}
// Scrolling y
if ( scrollTop !== that.data.scrollTop ) {
that.data.direction.y = true;
}
that.data.scrollTop = scrollTop;
that.data.scrollLeft = scrollLeft;
// Scrolled amounts
that.data.scrolled.x = scrollLeft / (that.data.scrollWidth - that.data.clientWidth);
that.data.scrolled.y = scrollTop / (that.data.scrollHeight - that.data.clientHeight);
// Handle positions
that.bars.x.position = that.data.scrolled.x * (that.rails.x.rect.w - that.bars.x.size);
that.bars.y.position = that.data.scrolled.y * (that.rails.y.rect.h - that.bars.y.size);
raf(function() {
if(that.data.ratio.x >= 1) {
} else {
style(that.bars.x.node, {
transform: "translate3d("+that.bars.x.position+"px, 0px, 0px)"
})
}
if(that.data.ratio.y >= 1) {
} else {
style(that.bars.y.node, {
transform: "translate3d(0px, "+that.bars.y.position+"px, 0px)"
})
}
});
}
};
Scrollr.prototype.down = function(e) {
var that = this;
// e.preventDefault();
var bar = closest(e.target, function(el) {
return el && el.classList.contains("scrollr-bar");
});
if ( bar ) {
var rectX = rect(that.bars.x.node);
var rectY = rect(that.bars.y.node);
var x = e.pageX - rectX.x1;
var y = e.pageY - rectY.y1;
this.data.origin = {
x: x,
y: y,
bar: bar
};
this.dragging = true;
return false;
}
};
Scrollr.prototype.drag = function(e) {
if ( this.dragging ) {
e.preventDefault();
var that = this;
that.bars.x.position = e.pageX - that.data.origin.x;
that.bars.y.position = e.pageY - that.data.origin.y;
var x = that.bars.x.position / (that.rect.w - that.bars.x.size);
var y = that.bars.y.position / (that.rect.h - that.bars.y.size);
var scrollX = that.data.origin.bar === that.bars.x.node && x >= 0 && x <= 1;
var scrollY = that.data.origin.bar === that.bars.y.node && y >= 0 && y <= 1;
raf(function() {
if ( scrollX ) {
that.el.scrollLeft = that.data.scrollLeft = x * (that.data.scrollWidth - that.data.clientWidth);
style(that.bars.x.node, {
transform: "translate3d("+that.bars.x.position+"px, 0px, 0px)"
});
}
if ( scrollY ) {
that.el.scrollTop = that.data.scrollTop = y * (that.data.scrollHeight - that.data.clientHeight);
style(that.bars.y.node, {
transform: "translate3d(0px, "+that.bars.y.position+"px, 0px)"
});
}
});
}
};
Scrollr.prototype.stop = function(e) {
var that = this;
if ( that.dragging ) {
that.dragging = false;
}
};
Scrollr.prototype.update = function() {
var s = {};
this.getData();
if ( this.data.scroll.x ) {
s["height"] = "auto";
s["max-width"] = this.rect.w + this.size;
// s["max-height"] = this.rect.h + this.size;
s["margin-bottom"] = -this.size;
this.bars.y.size -= 10;
}
if ( this.data.scroll.y ) {
s["width"] = "auto";
// s["max-width"] = this.rect.w + this.size;
s["max-height"] = this.rect.h + this.size;
s["margin-right"] = -this.size;
this.bars.x.size -= 10;
}
this.wrapper.classList.toggle("scrollr-x", this.data.scroll.x);
this.wrapper.classList.toggle("scrollr-y", this.data.scroll.y);
style(this.el, s);
this.rails.x.rect = rect(this.rails.x.node);
this.rails.y.rect = rect(this.rails.y.node);
style(this.bars.x.node, {
width: this.bars.x.size
});
style(this.bars.y.node, {
height: this.bars.y.size
});
style(this.rails.x.node, {
opacity: this.data.scroll.x ? 1 : 0
});
style(this.rails.y.node, {
opacity: this.data.scroll.y ? 1 : 0
});
};
Scrollr.prototype.getData = function() {
this.rect = rect(this.wrapper.parentNode);
if ( this.config.responsive ) {
this.rect = rect(this.wrapper.parentNode);
}
var scrollTop = this.el.scrollTop;
var scrollLeft = this.el.scrollLeft;
var scrollHeight = this.el.scrollHeight;
var clientHeight = round(this.rect.h);
var scrollWidth = this.el.scrollWidth;
var clientWidth = round(this.rect.w);
var scrolled = {
x: scrollLeft / (scrollWidth - clientWidth),
y: scrollTop / (scrollHeight - clientHeight)
};
var ratio = {
x: clientWidth / scrollWidth,
y: clientHeight / scrollHeight
};
var scroll = {
x: scrollWidth > clientWidth,
y: scrollHeight > clientHeight
};
this.data = {
scrolled: scrolled,
ratio: ratio,
scrollTop: scrollTop,
scrollLeft: scrollLeft,
scrollHeight: scrollHeight,
clientHeight: clientHeight,
scrollWidth: scrollWidth,
clientWidth: clientWidth,
scroll: scroll,
direction: {}
};
this.bars.x.size = round(this.data.clientWidth * (this.data.clientWidth / scrollWidth), 1);
this.bars.y.size = round(this.data.clientHeight * (this.data.clientHeight / scrollHeight), 1);
this.bars.x.position = scrolled.x * (clientWidth);
this.bars.y.position = scrolled.y * (clientHeight);
};
global.Scrollr = Scrollr;
}(this));