428 lines
12 KiB
JavaScript
428 lines
12 KiB
JavaScript
// Required for Meteor package, the use of window prevents export by Meteor
|
|
(function(window) {
|
|
if (window.Package) {
|
|
M = {};
|
|
} else {
|
|
window.M = {};
|
|
}
|
|
|
|
// Check for jQuery
|
|
M.jQueryLoaded = !!window.jQuery;
|
|
})(window);
|
|
|
|
// AMD
|
|
if (typeof define === 'function' && define.amd) {
|
|
define('M', [], function() {
|
|
return M;
|
|
});
|
|
|
|
// Common JS
|
|
} else if (typeof exports !== 'undefined' && !exports.nodeType) {
|
|
if (typeof module !== 'undefined' && !module.nodeType && module.exports) {
|
|
exports = module.exports = M;
|
|
}
|
|
exports.default = M;
|
|
}
|
|
|
|
M.version = '1.0.0';
|
|
|
|
M.keys = {
|
|
TAB: 9,
|
|
ENTER: 13,
|
|
ESC: 27,
|
|
ARROW_UP: 38,
|
|
ARROW_DOWN: 40
|
|
};
|
|
|
|
/**
|
|
* TabPress Keydown handler
|
|
*/
|
|
M.tabPressed = false;
|
|
M.keyDown = false;
|
|
let docHandleKeydown = function(e) {
|
|
M.keyDown = true;
|
|
if (e.which === M.keys.TAB || e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) {
|
|
M.tabPressed = true;
|
|
}
|
|
};
|
|
let docHandleKeyup = function(e) {
|
|
M.keyDown = false;
|
|
if (e.which === M.keys.TAB || e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) {
|
|
M.tabPressed = false;
|
|
}
|
|
};
|
|
let docHandleFocus = function(e) {
|
|
if (M.keyDown) {
|
|
document.body.classList.add('keyboard-focused');
|
|
}
|
|
};
|
|
let docHandleBlur = function(e) {
|
|
document.body.classList.remove('keyboard-focused');
|
|
};
|
|
document.addEventListener('keydown', docHandleKeydown, true);
|
|
document.addEventListener('keyup', docHandleKeyup, true);
|
|
document.addEventListener('focus', docHandleFocus, true);
|
|
document.addEventListener('blur', docHandleBlur, true);
|
|
|
|
/**
|
|
* Initialize jQuery wrapper for plugin
|
|
* @param {Class} plugin javascript class
|
|
* @param {string} pluginName jQuery plugin name
|
|
* @param {string} classRef Class reference name
|
|
*/
|
|
M.initializeJqueryWrapper = function(plugin, pluginName, classRef) {
|
|
jQuery.fn[pluginName] = function(methodOrOptions) {
|
|
// Call plugin method if valid method name is passed in
|
|
if (plugin.prototype[methodOrOptions]) {
|
|
let params = Array.prototype.slice.call(arguments, 1);
|
|
|
|
// Getter methods
|
|
if (methodOrOptions.slice(0, 3) === 'get') {
|
|
let instance = this.first()[0][classRef];
|
|
return instance[methodOrOptions].apply(instance, params);
|
|
}
|
|
|
|
// Void methods
|
|
return this.each(function() {
|
|
let instance = this[classRef];
|
|
instance[methodOrOptions].apply(instance, params);
|
|
});
|
|
|
|
// Initialize plugin if options or no argument is passed in
|
|
} else if (typeof methodOrOptions === 'object' || !methodOrOptions) {
|
|
plugin.init(this, arguments[0]);
|
|
return this;
|
|
}
|
|
|
|
// Return error if an unrecognized method name is passed in
|
|
jQuery.error(`Method ${methodOrOptions} does not exist on jQuery.${pluginName}`);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Automatically initialize components
|
|
* @param {Element} context DOM Element to search within for components
|
|
*/
|
|
M.AutoInit = function(context) {
|
|
// Use document.body if no context is given
|
|
let root = !!context ? context : document.body;
|
|
|
|
let registry = {
|
|
Autocomplete: root.querySelectorAll('.autocomplete:not(.no-autoinit)'),
|
|
Carousel: root.querySelectorAll('.carousel:not(.no-autoinit)'),
|
|
Chips: root.querySelectorAll('.chips:not(.no-autoinit)'),
|
|
Collapsible: root.querySelectorAll('.collapsible:not(.no-autoinit)'),
|
|
Datepicker: root.querySelectorAll('.datepicker:not(.no-autoinit)'),
|
|
Dropdown: root.querySelectorAll('.dropdown-trigger:not(.no-autoinit)'),
|
|
Materialbox: root.querySelectorAll('.materialboxed:not(.no-autoinit)'),
|
|
Modal: root.querySelectorAll('.modal:not(.no-autoinit)'),
|
|
Parallax: root.querySelectorAll('.parallax:not(.no-autoinit)'),
|
|
Pushpin: root.querySelectorAll('.pushpin:not(.no-autoinit)'),
|
|
ScrollSpy: root.querySelectorAll('.scrollspy:not(.no-autoinit)'),
|
|
FormSelect: root.querySelectorAll('select:not(.no-autoinit)'),
|
|
Sidenav: root.querySelectorAll('.sidenav:not(.no-autoinit)'),
|
|
Tabs: root.querySelectorAll('.tabs:not(.no-autoinit)'),
|
|
TapTarget: root.querySelectorAll('.tap-target:not(.no-autoinit)'),
|
|
Timepicker: root.querySelectorAll('.timepicker:not(.no-autoinit)'),
|
|
Tooltip: root.querySelectorAll('.tooltipped:not(.no-autoinit)'),
|
|
FloatingActionButton: root.querySelectorAll('.fixed-action-btn:not(.no-autoinit)')
|
|
};
|
|
|
|
for (let pluginName in registry) {
|
|
let plugin = M[pluginName];
|
|
plugin.init(registry[pluginName]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generate approximated selector string for a jQuery object
|
|
* @param {jQuery} obj jQuery object to be parsed
|
|
* @returns {string}
|
|
*/
|
|
M.objectSelectorString = function(obj) {
|
|
let tagStr = obj.prop('tagName') || '';
|
|
let idStr = obj.attr('id') || '';
|
|
let classStr = obj.attr('class') || '';
|
|
return (tagStr + idStr + classStr).replace(/\s/g, '');
|
|
};
|
|
|
|
// Unique Random ID
|
|
M.guid = (function() {
|
|
function s4() {
|
|
return Math.floor((1 + Math.random()) * 0x10000)
|
|
.toString(16)
|
|
.substring(1);
|
|
}
|
|
return function() {
|
|
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
|
};
|
|
})();
|
|
|
|
/**
|
|
* Escapes hash from special characters
|
|
* @param {string} hash String returned from this.hash
|
|
* @returns {string}
|
|
*/
|
|
M.escapeHash = function(hash) {
|
|
return hash.replace(/(:|\.|\[|\]|,|=|\/)/g, '\\$1');
|
|
};
|
|
|
|
M.elementOrParentIsFixed = function(element) {
|
|
let $element = $(element);
|
|
let $checkElements = $element.add($element.parents());
|
|
let isFixed = false;
|
|
$checkElements.each(function() {
|
|
if ($(this).css('position') === 'fixed') {
|
|
isFixed = true;
|
|
return false;
|
|
}
|
|
});
|
|
return isFixed;
|
|
};
|
|
|
|
/**
|
|
* @typedef {Object} Edges
|
|
* @property {Boolean} top If the top edge was exceeded
|
|
* @property {Boolean} right If the right edge was exceeded
|
|
* @property {Boolean} bottom If the bottom edge was exceeded
|
|
* @property {Boolean} left If the left edge was exceeded
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} Bounding
|
|
* @property {Number} left left offset coordinate
|
|
* @property {Number} top top offset coordinate
|
|
* @property {Number} width
|
|
* @property {Number} height
|
|
*/
|
|
|
|
/**
|
|
* Escapes hash from special characters
|
|
* @param {Element} container Container element that acts as the boundary
|
|
* @param {Bounding} bounding element bounding that is being checked
|
|
* @param {Number} offset offset from edge that counts as exceeding
|
|
* @returns {Edges}
|
|
*/
|
|
M.checkWithinContainer = function(container, bounding, offset) {
|
|
let edges = {
|
|
top: false,
|
|
right: false,
|
|
bottom: false,
|
|
left: false
|
|
};
|
|
|
|
let containerRect = container.getBoundingClientRect();
|
|
// If body element is smaller than viewport, use viewport height instead.
|
|
let containerBottom =
|
|
container === document.body
|
|
? Math.max(containerRect.bottom, window.innerHeight)
|
|
: containerRect.bottom;
|
|
|
|
let scrollLeft = container.scrollLeft;
|
|
let scrollTop = container.scrollTop;
|
|
|
|
let scrolledX = bounding.left - scrollLeft;
|
|
let scrolledY = bounding.top - scrollTop;
|
|
|
|
// Check for container and viewport for each edge
|
|
if (scrolledX < containerRect.left + offset || scrolledX < offset) {
|
|
edges.left = true;
|
|
}
|
|
|
|
if (
|
|
scrolledX + bounding.width > containerRect.right - offset ||
|
|
scrolledX + bounding.width > window.innerWidth - offset
|
|
) {
|
|
edges.right = true;
|
|
}
|
|
|
|
if (scrolledY < containerRect.top + offset || scrolledY < offset) {
|
|
edges.top = true;
|
|
}
|
|
|
|
if (
|
|
scrolledY + bounding.height > containerBottom - offset ||
|
|
scrolledY + bounding.height > window.innerHeight - offset
|
|
) {
|
|
edges.bottom = true;
|
|
}
|
|
|
|
return edges;
|
|
};
|
|
|
|
M.checkPossibleAlignments = function(el, container, bounding, offset) {
|
|
let canAlign = {
|
|
top: true,
|
|
right: true,
|
|
bottom: true,
|
|
left: true,
|
|
spaceOnTop: null,
|
|
spaceOnRight: null,
|
|
spaceOnBottom: null,
|
|
spaceOnLeft: null
|
|
};
|
|
|
|
let containerAllowsOverflow = getComputedStyle(container).overflow === 'visible';
|
|
let containerRect = container.getBoundingClientRect();
|
|
let containerHeight = Math.min(containerRect.height, window.innerHeight);
|
|
let containerWidth = Math.min(containerRect.width, window.innerWidth);
|
|
let elOffsetRect = el.getBoundingClientRect();
|
|
|
|
let scrollLeft = container.scrollLeft;
|
|
let scrollTop = container.scrollTop;
|
|
|
|
let scrolledX = bounding.left - scrollLeft;
|
|
let scrolledYTopEdge = bounding.top - scrollTop;
|
|
let scrolledYBottomEdge = bounding.top + elOffsetRect.height - scrollTop;
|
|
|
|
// Check for container and viewport for left
|
|
canAlign.spaceOnRight = !containerAllowsOverflow
|
|
? containerWidth - (scrolledX + bounding.width)
|
|
: window.innerWidth - (elOffsetRect.left + bounding.width);
|
|
if (canAlign.spaceOnRight < 0) {
|
|
canAlign.left = false;
|
|
}
|
|
|
|
// Check for container and viewport for Right
|
|
canAlign.spaceOnLeft = !containerAllowsOverflow
|
|
? scrolledX - bounding.width + elOffsetRect.width
|
|
: elOffsetRect.right - bounding.width;
|
|
if (canAlign.spaceOnLeft < 0) {
|
|
canAlign.right = false;
|
|
}
|
|
|
|
// Check for container and viewport for Top
|
|
canAlign.spaceOnBottom = !containerAllowsOverflow
|
|
? containerHeight - (scrolledYTopEdge + bounding.height + offset)
|
|
: window.innerHeight - (elOffsetRect.top + bounding.height + offset);
|
|
if (canAlign.spaceOnBottom < 0) {
|
|
canAlign.top = false;
|
|
}
|
|
|
|
// Check for container and viewport for Bottom
|
|
canAlign.spaceOnTop = !containerAllowsOverflow
|
|
? scrolledYBottomEdge - (bounding.height - offset)
|
|
: elOffsetRect.bottom - (bounding.height + offset);
|
|
if (canAlign.spaceOnTop < 0) {
|
|
canAlign.bottom = false;
|
|
}
|
|
|
|
return canAlign;
|
|
};
|
|
|
|
M.getOverflowParent = function(element) {
|
|
if (element == null) {
|
|
return null;
|
|
}
|
|
|
|
if (element === document.body || getComputedStyle(element).overflow !== 'visible') {
|
|
return element;
|
|
}
|
|
|
|
return M.getOverflowParent(element.parentElement);
|
|
};
|
|
|
|
/**
|
|
* Gets id of component from a trigger
|
|
* @param {Element} trigger trigger
|
|
* @returns {string}
|
|
*/
|
|
M.getIdFromTrigger = function(trigger) {
|
|
let id = trigger.getAttribute('data-target');
|
|
if (!id) {
|
|
id = trigger.getAttribute('href');
|
|
if (id) {
|
|
id = id.slice(1);
|
|
} else {
|
|
id = '';
|
|
}
|
|
}
|
|
return id;
|
|
};
|
|
|
|
/**
|
|
* Multi browser support for document scroll top
|
|
* @returns {Number}
|
|
*/
|
|
M.getDocumentScrollTop = function() {
|
|
return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
|
|
};
|
|
|
|
/**
|
|
* Multi browser support for document scroll left
|
|
* @returns {Number}
|
|
*/
|
|
M.getDocumentScrollLeft = function() {
|
|
return window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
|
|
};
|
|
|
|
/**
|
|
* @typedef {Object} Edges
|
|
* @property {Boolean} top If the top edge was exceeded
|
|
* @property {Boolean} right If the right edge was exceeded
|
|
* @property {Boolean} bottom If the bottom edge was exceeded
|
|
* @property {Boolean} left If the left edge was exceeded
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} Bounding
|
|
* @property {Number} left left offset coordinate
|
|
* @property {Number} top top offset coordinate
|
|
* @property {Number} width
|
|
* @property {Number} height
|
|
*/
|
|
|
|
/**
|
|
* Get time in ms
|
|
* @license https://raw.github.com/jashkenas/underscore/master/LICENSE
|
|
* @type {function}
|
|
* @return {number}
|
|
*/
|
|
let getTime =
|
|
Date.now ||
|
|
function() {
|
|
return new Date().getTime();
|
|
};
|
|
|
|
/**
|
|
* Returns a function, that, when invoked, will only be triggered at most once
|
|
* during a given window of time. Normally, the throttled function will run
|
|
* as much as it can, without ever going more than once per `wait` duration;
|
|
* but if you'd like to disable the execution on the leading edge, pass
|
|
* `{leading: false}`. To disable execution on the trailing edge, ditto.
|
|
* @license https://raw.github.com/jashkenas/underscore/master/LICENSE
|
|
* @param {function} func
|
|
* @param {number} wait
|
|
* @param {Object=} options
|
|
* @returns {Function}
|
|
*/
|
|
M.throttle = function(func, wait, options) {
|
|
let context, args, result;
|
|
let timeout = null;
|
|
let previous = 0;
|
|
options || (options = {});
|
|
let later = function() {
|
|
previous = options.leading === false ? 0 : getTime();
|
|
timeout = null;
|
|
result = func.apply(context, args);
|
|
context = args = null;
|
|
};
|
|
return function() {
|
|
let now = getTime();
|
|
if (!previous && options.leading === false) previous = now;
|
|
let remaining = wait - (now - previous);
|
|
context = this;
|
|
args = arguments;
|
|
if (remaining <= 0) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
previous = now;
|
|
result = func.apply(context, args);
|
|
context = args = null;
|
|
} else if (!timeout && options.trailing !== false) {
|
|
timeout = setTimeout(later, remaining);
|
|
}
|
|
return result;
|
|
};
|
|
};
|