angular-material-npfixed
Version:
The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M
893 lines (752 loc) • 28.5 kB
JavaScript
/*
* This var has to be outside the angular factory, otherwise when
* there are multiple material apps on the same page, each app
* will create its own instance of this array and the app's IDs
* will not be unique.
*/
var nextUniqueId = 0;
/**
* @ngdoc module
* @name material.core.util
* @description
* Util
*/
angular
.module('material.core')
.factory('$mdUtil', UtilFactory);
/**
* @ngInject
*/
function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window, $$rAF) {
// Setup some core variables for the processTemplate method
var startSymbol = $interpolate.startSymbol(),
endSymbol = $interpolate.endSymbol(),
usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}'));
/**
* Checks if the target element has the requested style by key
* @param {DOMElement|JQLite} target Target element
* @param {string} key Style key
* @param {string=} expectedVal Optional expected value
* @returns {boolean} Whether the target element has the style or not
*/
var hasComputedStyle = function (target, key, expectedVal) {
var hasValue = false;
if ( target && target.length ) {
var computedStyles = $window.getComputedStyle(target[0]);
hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true);
}
return hasValue;
};
function validateCssValue(value) {
return !value ? '0' :
hasPx(value) || hasPercent(value) ? value : value + 'px';
}
function hasPx(value) {
return String(value).indexOf('px') > -1;
}
function hasPercent(value) {
return String(value).indexOf('%') > -1;
}
var $mdUtil = {
dom: {},
now: window.performance && window.performance.now ?
angular.bind(window.performance, window.performance.now) : Date.now || function() {
return new Date().getTime();
},
/**
* Cross-version compatibility method to retrieve an option of a ngModel controller,
* which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c).
* @param {!angular.ngModelCtrl} ngModelCtrl
* @param {!string} optionName
* @returns {Object|undefined}
*/
getModelOption: function (ngModelCtrl, optionName) {
if (!ngModelCtrl.$options) {
return;
}
var $options = ngModelCtrl.$options;
// The newer versions of Angular introduced a `getOption function and made the option values no longer
// visible on the $options object.
return $options.getOption ? $options.getOption(optionName) : $options[optionName]
},
/**
* Bi-directional accessor/mutator used to easily update an element's
* property based on the current 'dir'ectional value.
*/
bidi : function(element, property, lValue, rValue) {
var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
// If accessor
if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl';
// If mutator
var elem = angular.element(element);
if ( ltr && angular.isDefined(lValue)) {
elem.css(property, validateCssValue(lValue));
}
else if ( !ltr && angular.isDefined(rValue)) {
elem.css(property, validateCssValue(rValue) );
}
},
bidiProperty: function (element, lProperty, rProperty, value) {
var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
var elem = angular.element(element);
if ( ltr && angular.isDefined(lProperty)) {
elem.css(lProperty, validateCssValue(value));
elem.css(rProperty, '');
}
else if ( !ltr && angular.isDefined(rProperty)) {
elem.css(rProperty, validateCssValue(value) );
elem.css(lProperty, '');
}
},
clientRect: function(element, offsetParent, isOffsetRect) {
var node = getNode(element);
offsetParent = getNode(offsetParent || node.offsetParent || document.body);
var nodeRect = node.getBoundingClientRect();
// The user can ask for an offsetRect: a rect relative to the offsetParent,
// or a clientRect: a rect relative to the page
var offsetRect = isOffsetRect ?
offsetParent.getBoundingClientRect() :
{left: 0, top: 0, width: 0, height: 0};
return {
left: nodeRect.left - offsetRect.left,
top: nodeRect.top - offsetRect.top,
width: nodeRect.width,
height: nodeRect.height
};
},
offsetRect: function(element, offsetParent) {
return $mdUtil.clientRect(element, offsetParent, true);
},
// Annoying method to copy nodes to an array, thanks to IE
nodesToArray: function(nodes) {
nodes = nodes || [];
var results = [];
for (var i = 0; i < nodes.length; ++i) {
results.push(nodes.item(i));
}
return results;
},
/**
* Determines the absolute position of the viewport.
* Useful when making client rectangles absolute.
* @returns {number}
*/
getViewportTop: function() {
return window.scrollY || window.pageYOffset || 0;
},
/**
* Finds the proper focus target by searching the DOM.
*
* @param containerEl
* @param attributeVal
* @returns {*}
*/
findFocusTarget: function(containerEl, attributeVal) {
var AUTO_FOCUS = this.prefixer('md-autofocus', true);
var elToFocus;
elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
if ( !elToFocus && attributeVal != AUTO_FOCUS) {
// Scan for deprecated attribute
elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true));
if ( !elToFocus ) {
// Scan for fallback to 'universal' API
elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
}
}
return elToFocus;
/**
* Can target and nested children for specified Selector (attribute)
* whose value may be an expression that evaluates to True/False.
*/
function scanForFocusable(target, selector) {
var elFound, items = target[0].querySelectorAll(selector);
// Find the last child element with the focus attribute
if ( items && items.length ){
items.length && angular.forEach(items, function(it) {
it = angular.element(it);
// Check the element for the md-autofocus class to ensure any associated expression
// evaluated to true.
var isFocusable = it.hasClass('md-autofocus');
if (isFocusable) elFound = it;
});
}
return elFound;
}
},
/**
* Disables scroll around the passed parent element.
* @param element Unused
* @param {!Element|!angular.JQLite} parent Element to disable scrolling within.
* Defaults to body if none supplied.
* @param options Object of options to modify functionality
* - disableScrollMask Boolean of whether or not to create a scroll mask element or
* use the passed parent element.
*/
disableScrollAround: function(element, parent, options) {
options = options || {};
$mdUtil.disableScrollAround._count = Math.max(0, $mdUtil.disableScrollAround._count || 0);
$mdUtil.disableScrollAround._count++;
if ($mdUtil.disableScrollAround._restoreScroll) {
return $mdUtil.disableScrollAround._restoreScroll;
}
var body = $document[0].body;
var restoreBody = disableBodyScroll();
var restoreElement = disableElementScroll(parent);
return $mdUtil.disableScrollAround._restoreScroll = function() {
if (--$mdUtil.disableScrollAround._count <= 0) {
restoreBody();
restoreElement();
delete $mdUtil.disableScrollAround._restoreScroll;
}
};
/**
* Creates a virtual scrolling mask to prevent touchmove, keyboard, scrollbar clicking,
* and wheel events
*/
function disableElementScroll(element) {
element = angular.element(element || body);
var scrollMask;
if (options.disableScrollMask) {
scrollMask = element;
} else {
scrollMask = angular.element(
'<div class="md-scroll-mask">' +
' <div class="md-scroll-mask-bar"></div>' +
'</div>');
element.append(scrollMask);
}
scrollMask.on('wheel', preventDefault);
scrollMask.on('touchmove', preventDefault);
return function restoreElementScroll() {
scrollMask.off('wheel');
scrollMask.off('touchmove');
if (!options.disableScrollMask) {
scrollMask[0].parentNode.removeChild(scrollMask[0]);
}
};
function preventDefault(e) {
e.preventDefault();
}
}
// Converts the body to a position fixed block and translate it to the proper scroll position
function disableBodyScroll() {
var documentElement = $document[0].documentElement;
var prevDocumentStyle = documentElement.style.cssText || '';
var prevBodyStyle = body.style.cssText || '';
var viewportTop = $mdUtil.getViewportTop();
var clientWidth = body.clientWidth;
var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1;
if (hasVerticalScrollbar) {
angular.element(body).css({
position: 'fixed',
width: '100%',
top: -viewportTop + 'px'
});
}
if (body.clientWidth < clientWidth) {
body.style.overflow = 'hidden';
}
// This should be applied after the manipulation to the body, because
// adding a scrollbar can potentially resize it, causing the measurement
// to change.
if (hasVerticalScrollbar) {
documentElement.style.overflowY = 'scroll';
}
return function restoreScroll() {
// Reset the inline style CSS to the previous.
body.style.cssText = prevBodyStyle;
documentElement.style.cssText = prevDocumentStyle;
// The body loses its scroll position while being fixed.
body.scrollTop = viewportTop;
};
}
},
enableScrolling: function() {
var restoreFn = this.disableScrollAround._restoreScroll;
restoreFn && restoreFn();
},
floatingScrollbars: function() {
if (this.floatingScrollbars.cached === undefined) {
var tempNode = angular.element('<div><div></div></div>').css({
width: '100%',
'z-index': -1,
position: 'absolute',
height: '35px',
'overflow-y': 'scroll'
});
tempNode.children().css('height', '60px');
$document[0].body.appendChild(tempNode[0]);
this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
tempNode.remove();
}
return this.floatingScrollbars.cached;
},
// Mobile safari only allows you to set focus in click event listeners...
forceFocus: function(element) {
var node = element[0] || element;
document.addEventListener('click', function focusOnClick(ev) {
if (ev.target === node && ev.$focus) {
node.focus();
ev.stopImmediatePropagation();
ev.preventDefault();
node.removeEventListener('click', focusOnClick);
}
}, true);
var newEvent = document.createEvent('MouseEvents');
newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
false, false, false, false, 0, null);
newEvent.$material = true;
newEvent.$focus = true;
node.dispatchEvent(newEvent);
},
/**
* facade to build md-backdrop element with desired styles
* NOTE: Use $compile to trigger backdrop postLink function
*/
createBackdrop: function(scope, addClass) {
return $compile($mdUtil.supplant('<md-backdrop class="{0}">', [addClass]))(scope);
},
/**
* supplant() method from Crockford's `Remedial Javascript`
* Equivalent to use of $interpolate; without dependency on
* interpolation symbols and scope. Note: the '{<token>}' can
* be property names, property chains, or array indices.
*/
supplant: function(template, values, pattern) {
pattern = pattern || /\{([^\{\}]*)\}/g;
return template.replace(pattern, function(a, b) {
var p = b.split('.'),
r = values;
try {
for (var s in p) {
if (p.hasOwnProperty(s) ) {
r = r[p[s]];
}
}
} catch (e) {
r = a;
}
return (typeof r === 'string' || typeof r === 'number') ? r : a;
});
},
fakeNgModel: function() {
return {
$fake: true,
$setTouched: angular.noop,
$setViewValue: function(value) {
this.$viewValue = value;
this.$render(value);
this.$viewChangeListeners.forEach(function(cb) {
cb();
});
},
$isEmpty: function(value) {
return ('' + value).length === 0;
},
$parsers: [],
$formatters: [],
$viewChangeListeners: [],
$render: angular.noop
};
},
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
// @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
// @param invokeApply should the $timeout trigger $digest() dirty checking
debounce: function(func, wait, scope, invokeApply) {
var timer;
return function debounced() {
var context = scope,
args = Array.prototype.slice.call(arguments);
$timeout.cancel(timer);
timer = $timeout(function() {
timer = undefined;
func.apply(context, args);
}, wait || 10, invokeApply);
};
},
// Returns a function that can only be triggered every `delay` milliseconds.
// In other words, the function will not be called unless it has been more
// than `delay` milliseconds since the last call.
throttle: function throttle(func, delay) {
var recent;
return function throttled() {
var context = this;
var args = arguments;
var now = $mdUtil.now();
if (!recent || (now - recent > delay)) {
func.apply(context, args);
recent = now;
}
};
},
/**
* Measures the number of milliseconds taken to run the provided callback
* function. Uses a high-precision timer if available.
*/
time: function time(cb) {
var start = $mdUtil.now();
cb();
return $mdUtil.now() - start;
},
/**
* Create an implicit getter that caches its `getter()`
* lookup value
*/
valueOnUse : function (scope, key, getter) {
var value = null, args = Array.prototype.slice.call(arguments);
var params = (args.length > 3) ? args.slice(3) : [ ];
Object.defineProperty(scope, key, {
get: function () {
if (value === null) value = getter.apply(scope, params);
return value;
}
});
},
/**
* Get a unique ID.
*
* @returns {string} an unique numeric string
*/
nextUid: function() {
return '' + nextUniqueId++;
},
// Stop watchers and events from firing on a scope without destroying it,
// by disconnecting it from its parent and its siblings' linked lists.
disconnectScope: function disconnectScope(scope) {
if (!scope) return;
// we can't destroy the root scope or a scope that has been already destroyed
if (scope.$root === scope) return;
if (scope.$$destroyed) return;
var parent = scope.$parent;
scope.$$disconnected = true;
// See Scope.$destroy
if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
scope.$$nextSibling = scope.$$prevSibling = null;
},
// Undo the effects of disconnectScope above.
reconnectScope: function reconnectScope(scope) {
if (!scope) return;
// we can't disconnect the root node or scope already disconnected
if (scope.$root === scope) return;
if (!scope.$$disconnected) return;
var child = scope;
var parent = child.$parent;
child.$$disconnected = false;
// See Scope.$new for this logic...
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else {
parent.$$childHead = parent.$$childTail = child;
}
},
/*
* getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
*
* @param el Element to start walking the DOM from
* @param check Either a string or a function. If a string is passed, it will be evaluated against
* each of the parent nodes' tag name. If a function is passed, the loop will call it with each of
* the parents and will use the return value to determine whether the node is a match.
* @param onlyParent Only start checking from the parent element, not `el`.
*/
getClosest: function getClosest(el, validateWith, onlyParent) {
if ( angular.isString(validateWith) ) {
var tagName = validateWith.toUpperCase();
validateWith = function(el) {
return el.nodeName.toUpperCase() === tagName;
};
}
if (el instanceof angular.element) el = el[0];
if (onlyParent) el = el.parentNode;
if (!el) return null;
do {
if (validateWith(el)) {
return el;
}
} while (el = el.parentNode);
return null;
},
/**
* Build polyfill for the Node.contains feature (if needed)
*/
elementContains: function(node, child) {
var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains);
var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) {
// compares the positions of two nodes and returns a bitmask
return (node === child) || !!(this.compareDocumentPosition(arg) & 16)
});
return findFn(child);
},
/**
* Functional equivalent for $element.filter(‘md-bottom-sheet’)
* useful with interimElements where the element and its container are important...
*
* @param {[]} elements to scan
* @param {string} name of node to find (e.g. 'md-dialog')
* @param {boolean=} optional flag to allow deep scans; defaults to 'false'.
* @param {boolean=} optional flag to enable log warnings; defaults to false
*/
extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
var found = scanTree(element);
if (!found && !!warnNotFound) {
$log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) );
}
return angular.element(found || element);
/**
* Breadth-First tree scan for element with matching `nodeName`
*/
function scanTree(element) {
return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null);
}
/**
* Case-insensitive scan of current elements only (do not descend).
*/
function scanLevel(element) {
if ( element ) {
for (var i = 0, len = element.length; i < len; i++) {
if (element[i].nodeName.toLowerCase() === nodeName) {
return element[i];
}
}
}
return null;
}
/**
* Scan children of specified node
*/
function scanChildren(element) {
var found;
if ( element ) {
for (var i = 0, len = element.length; i < len; i++) {
var target = element[i];
if ( !found ) {
for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
found = found || scanTree([target.childNodes[j]]);
}
}
}
}
return found;
}
},
/**
* Give optional properties with no value a boolean true if attr provided or false otherwise
*/
initOptionalProperties: function(scope, attr, defaults) {
defaults = defaults || {};
angular.forEach(scope.$$isolateBindings, function(binding, key) {
if (binding.optional && angular.isUndefined(scope[key])) {
var attrIsDefined = angular.isDefined(attr[binding.attrName]);
scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
}
});
},
/**
* Alternative to $timeout calls with 0 delay.
* nextTick() coalesces all calls within a single frame
* to minimize $digest thrashing
*
* @param callback
* @param digest
* @returns {*}
*/
nextTick: function(callback, digest, scope) {
//-- grab function reference for storing state details
var nextTick = $mdUtil.nextTick;
var timeout = nextTick.timeout;
var queue = nextTick.queue || [];
//-- add callback to the queue
queue.push({scope: scope, callback: callback});
//-- set default value for digest
if (digest == null) digest = true;
//-- store updated digest/queue values
nextTick.digest = nextTick.digest || digest;
nextTick.queue = queue;
//-- either return existing timeout or create a new one
return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));
/**
* Grab a copy of the current queue
* Clear the queue for future use
* Process the existing queue
* Trigger digest if necessary
*/
function processQueue() {
var queue = nextTick.queue;
var digest = nextTick.digest;
nextTick.queue = [];
nextTick.timeout = null;
nextTick.digest = false;
queue.forEach(function(queueItem) {
var skip = queueItem.scope && queueItem.scope.$$destroyed;
if (!skip) {
queueItem.callback();
}
});
if (digest) $rootScope.$digest();
}
},
/**
* Processes a template and replaces the start/end symbols if the application has
* overriden them.
*
* @param template The template to process whose start/end tags may be replaced.
* @returns {*}
*/
processTemplate: function(template) {
if (usesStandardSymbols) {
return template;
} else {
if (!template || !angular.isString(template)) return template;
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
}
},
/**
* Scan up dom hierarchy for enabled parent;
*/
getParentWithPointerEvents: function (element) {
var parent = element.parent();
// jqLite might return a non-null, but still empty, parent; so check for parent and length
while (hasComputedStyle(parent, 'pointer-events', 'none')) {
parent = parent.parent();
}
return parent;
},
getNearestContentElement: function (element) {
var current = element.parent()[0];
// Look for the nearest parent md-content, stopping at the rootElement.
while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') {
current = current.parentNode;
}
return current;
},
/**
* Checks if the current browser is natively supporting the `sticky` position.
* @returns {string} supported sticky property name
*/
checkStickySupport: function() {
var stickyProp;
var testEl = angular.element('<div>');
$document[0].body.appendChild(testEl[0]);
var stickyProps = ['sticky', '-webkit-sticky'];
for (var i = 0; i < stickyProps.length; ++i) {
testEl.css({
position: stickyProps[i],
top: 0,
'z-index': 2
});
if (testEl.css('position') == stickyProps[i]) {
stickyProp = stickyProps[i];
break;
}
}
testEl.remove();
return stickyProp;
},
/**
* Parses an attribute value, mostly a string.
* By default checks for negated values and returns `false´ if present.
* Negated values are: (native falsy) and negative strings like:
* `false` or `0`.
* @param value Attribute value which should be parsed.
* @param negatedCheck When set to false, won't check for negated values.
* @returns {boolean}
*/
parseAttributeBoolean: function(value, negatedCheck) {
return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0');
},
hasComputedStyle: hasComputedStyle,
/**
* Returns true if the parent form of the element has been submitted.
*
* @param element An Angular or HTML5 element.
*
* @returns {boolean}
*/
isParentFormSubmitted: function(element) {
var parent = $mdUtil.getClosest(element, 'form');
var form = parent ? angular.element(parent).controller('form') : null;
return form ? form.$submitted : false;
},
/**
* Animate the requested element's scrollTop to the requested scrollPosition with basic easing.
*
* @param {!HTMLElement} element The element to scroll.
* @param {number} scrollEnd The new/final scroll position.
* @param {number=} duration Duration of the scroll. Default is 1000ms.
*/
animateScrollTo: function(element, scrollEnd, duration) {
var scrollStart = element.scrollTop;
var scrollChange = scrollEnd - scrollStart;
var scrollingDown = scrollStart < scrollEnd;
var startTime = $mdUtil.now();
$$rAF(scrollChunk);
function scrollChunk() {
var newPosition = calculateNewPosition();
element.scrollTop = newPosition;
if (scrollingDown ? newPosition < scrollEnd : newPosition > scrollEnd) {
$$rAF(scrollChunk);
}
}
function calculateNewPosition() {
var easeDuration = duration || 1000;
var currentTime = $mdUtil.now() - startTime;
return ease(currentTime, scrollStart, scrollChange, easeDuration);
}
function ease(currentTime, start, change, duration) {
// If the duration has passed (which can occur if our app loses focus due to $$rAF), jump
// straight to the proper position
if (currentTime > duration) {
return start + change;
}
var ts = (currentTime /= duration) * currentTime;
var tc = ts * currentTime;
return start + change * (-2 * tc + 3 * ts);
}
},
/**
* Provides an easy mechanism for removing duplicates from an array.
*
* var myArray = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4];
*
* $mdUtil.uniq(myArray) => [1, 2, 3, 4]
*
* @param {array} array The array whose unique values should be returned.
*
* @returns {array} A copy of the array containing only unique values.
*/
uniq: function(array) {
if (!array) { return; }
return array.filter(function(value, index, self) {
return self.indexOf(value) === index;
});
}
};
// Instantiate other namespace utility methods
$mdUtil.dom.animator = $$mdAnimate($mdUtil);
return $mdUtil;
function getNode(el) {
return el[0] || el;
}
}
/*
* Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
* We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
*/
angular.element.prototype.focus = angular.element.prototype.focus || function() {
if (this.length) {
this[0].focus();
}
return this;
};
angular.element.prototype.blur = angular.element.prototype.blur || function() {
if (this.length) {
this[0].blur();
}
return this;
};