UNPKG

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
/* * 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; };