UNPKG

mobius1-selectr

Version:

A lightweight, dependency-free, mobile-friendly javascript select box replacement.

583 lines (463 loc) 14.8 kB
// 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));