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

754 lines (630 loc) 25.8 kB
angular.module('material.core') .provider('$$interimElement', InterimElementProvider); /* * @ngdoc service * @name $$interimElement * @module material.core * * @description * * Factory that contructs `$$interimElement.$service` services. * Used internally in material design for elements that appear on screen temporarily. * The service provides a promise-like API for interacting with the temporary * elements. * * ```js * app.service('$mdToast', function($$interimElement) { * var $mdToast = $$interimElement(toastDefaultOptions); * return $mdToast; * }); * ``` * @param {object=} defaultOptions Options used by default for the `show` method on the service. * * @returns {$$interimElement.$service} * */ function InterimElementProvider() { createInterimElementProvider.$get = InterimElementFactory; return createInterimElementProvider; /** * Returns a new provider which allows configuration of a new interimElement * service. Allows configuration of default options & methods for options, * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method) */ function createInterimElementProvider(interimFactoryName) { var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove']; var customMethods = {}; var providerConfig = { presets: {} }; var provider = { setDefaults: setDefaults, addPreset: addPreset, addMethod: addMethod, $get: factory }; /** * all interim elements will come with the 'build' preset */ provider.addPreset('build', { methods: ['controller', 'controllerAs', 'resolve', 'multiple', 'template', 'templateUrl', 'themable', 'transformTemplate', 'parent', 'contentElement'] }); return provider; /** * Save the configured defaults to be used when the factory is instantiated */ function setDefaults(definition) { providerConfig.optionsFactory = definition.options; providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS); return provider; } /** * Add a method to the factory that isn't specific to any interim element operations */ function addMethod(name, fn) { customMethods[name] = fn; return provider; } /** * Save the configured preset to be used when the factory is instantiated */ function addPreset(name, definition) { definition = definition || {}; definition.methods = definition.methods || []; definition.options = definition.options || function() { return {}; }; if (/^cancel|hide|show$/.test(name)) { throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!"); } if (definition.methods.indexOf('_options') > -1) { throw new Error("Method '_options' in " + interimFactoryName + " is reserved!"); } providerConfig.presets[name] = { methods: definition.methods.concat(EXPOSED_METHODS), optionsFactory: definition.options, argOption: definition.argOption }; return provider; } function addPresetMethod(presetName, methodName, method) { providerConfig.presets[presetName][methodName] = method; } /** * Create a factory that has the given methods & defaults implementing interimElement */ /* @ngInject */ function factory($$interimElement, $injector) { var defaultMethods; var defaultOptions; var interimElementService = $$interimElement(); /* * publicService is what the developer will be using. * It has methods hide(), cancel(), show(), build(), and any other * presets which were set during the config phase. */ var publicService = { hide: interimElementService.hide, cancel: interimElementService.cancel, show: showInterimElement, // Special internal method to destroy an interim element without animations // used when navigation changes causes a $scope.$destroy() action destroy : destroyInterimElement }; defaultMethods = providerConfig.methods || []; // This must be invoked after the publicService is initialized defaultOptions = invokeFactory(providerConfig.optionsFactory, {}); // Copy over the simple custom methods angular.forEach(customMethods, function(fn, name) { publicService[name] = fn; }); angular.forEach(providerConfig.presets, function(definition, name) { var presetDefaults = invokeFactory(definition.optionsFactory, {}); var presetMethods = (definition.methods || []).concat(defaultMethods); // Every interimElement built with a preset has a field called `$type`, // which matches the name of the preset. // Eg in preset 'confirm', options.$type === 'confirm' angular.extend(presetDefaults, { $type: name }); // This creates a preset class which has setter methods for every // method given in the `.addPreset()` function, as well as every // method given in the `.setDefaults()` function. // // @example // .setDefaults({ // methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'], // options: dialogDefaultOptions // }) // .addPreset('alert', { // methods: ['title', 'ok'], // options: alertDialogOptions // }) // // Set values will be passed to the options when interimElement.show() is called. function Preset(opts) { this._options = angular.extend({}, presetDefaults, opts); } angular.forEach(presetMethods, function(name) { Preset.prototype[name] = function(value) { this._options[name] = value; return this; }; }); // Create shortcut method for one-linear methods if (definition.argOption) { var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1); publicService[methodName] = function(arg) { var config = publicService[name](arg); return publicService.show(config); }; } // eg $mdDialog.alert() will return a new alert preset publicService[name] = function(arg) { // If argOption is supplied, eg `argOption: 'content'`, then we assume // if the argument is not an options object then it is the `argOption` option. // // @example `$mdToast.simple('hello')` // sets options.content to hello // // because argOption === 'content' if (arguments.length && definition.argOption && !angular.isObject(arg) && !angular.isArray(arg)) { return (new Preset())[definition.argOption](arg); } else { return new Preset(arg); } }; }); return publicService; /** * */ function showInterimElement(opts) { // opts is either a preset which stores its options on an _options field, // or just an object made up of options opts = opts || { }; if (opts._options) opts = opts._options; return interimElementService.show( angular.extend({}, defaultOptions, opts) ); } /** * Special method to hide and destroy an interimElement WITHOUT * any 'leave` or hide animations ( an immediate force hide/remove ) * * NOTE: This calls the onRemove() subclass method for each component... * which must have code to respond to `options.$destroy == true` */ function destroyInterimElement(opts) { return interimElementService.destroy(opts); } /** * Helper to call $injector.invoke with a local of the factory name for * this provider. * If an $mdDialog is providing options for a dialog and tries to inject * $mdDialog, a circular dependency error will happen. * We get around that by manually injecting $mdDialog as a local. */ function invokeFactory(factory, defaultVal) { var locals = {}; locals[interimFactoryName] = publicService; return $injector.invoke(factory || function() { return defaultVal; }, {}, locals); } } } /* @ngInject */ function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate, $mdUtil, $mdCompiler, $mdTheming, $injector, $exceptionHandler) { return function createInterimElementService() { var SHOW_CANCELLED = false; /* * @ngdoc service * @name $$interimElement.$service * * @description * A service used to control inserting and removing an element into the DOM. * */ var service; var showPromises = []; // Promises for the interim's which are currently opening. var hidePromises = []; // Promises for the interim's which are currently hiding. var showingInterims = []; // Interim elements which are currently showing up. // Publish instance $$interimElement service; // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect return service = { show: show, hide: waitForInterim(hide), cancel: waitForInterim(cancel), destroy : destroy, $injector_: $injector }; /* * @ngdoc method * @name $$interimElement.$service#show * @kind function * * @description * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected * with hide or cancel, respectively. To external cancel/hide, developers should use the * * @param {*} options is hashMap of settings * @returns a Promise * */ function show(options) { options = options || {}; var interimElement = new InterimElement(options || {}); // When an interim element is currently showing, we have to cancel it. // Just hiding it, will resolve the InterimElement's promise, the promise should be // rejected instead. var hideAction = options.multiple ? $q.resolve() : $q.all(showPromises); if (!options.multiple) { // Wait for all opening interim's to finish their transition. hideAction = hideAction.then(function() { // Wait for all closing and showing interim's to be completely closed. var promiseArray = hidePromises.concat(showingInterims.map(service.cancel)); return $q.all(promiseArray); }); } var showAction = hideAction.then(function() { return interimElement .show() .catch(function(reason) { return reason; }) .finally(function() { showPromises.splice(showPromises.indexOf(showAction), 1); showingInterims.push(interimElement); }); }); showPromises.push(showAction); // In Angular 1.6+, exceptions inside promises will cause a rejection. We need to handle // the rejection and only log it if it's an error. interimElement.deferred.promise.catch(function(fault) { if (fault instanceof Error) { $exceptionHandler(fault); } return fault; }); // Return a promise that will be resolved when the interim // element is hidden or cancelled... return interimElement.deferred.promise; } /* * @ngdoc method * @name $$interimElement.$service#hide * @kind function * * @description * Removes the `$interimElement` from the DOM and resolves the promise returned from `show` * * @param {*} resolveParam Data to resolve the promise with * @returns a Promise that will be resolved after the element has been removed. * */ function hide(reason, options) { options = options || {}; if (options.closeAll) { // We have to make a shallow copy of the array, because otherwise the map will break. return $q.all(showingInterims.slice().reverse().map(closeElement)); } else if (options.closeTo !== undefined) { return $q.all(showingInterims.slice(options.closeTo).map(closeElement)); } // Hide the latest showing interim element. return closeElement(showingInterims[showingInterims.length - 1]); function closeElement(interim) { var hideAction = interim .remove(reason, false, options || { }) .catch(function(reason) { return reason; }) .finally(function() { hidePromises.splice(hidePromises.indexOf(hideAction), 1); }); showingInterims.splice(showingInterims.indexOf(interim), 1); hidePromises.push(hideAction); return interim.deferred.promise; } } /* * @ngdoc method * @name $$interimElement.$service#cancel * @kind function * * @description * Removes the `$interimElement` from the DOM and rejects the promise returned from `show` * * @param {*} reason Data to reject the promise with * @returns Promise that will be resolved after the element has been removed. * */ function cancel(reason, options) { var interim = showingInterims.pop(); if (!interim) { return $q.when(reason); } var cancelAction = interim .remove(reason, true, options || {}) .catch(function(reason) { return reason; }) .finally(function() { hidePromises.splice(hidePromises.indexOf(cancelAction), 1); }); hidePromises.push(cancelAction); // Since Angular 1.6.7, promises will be logged to $exceptionHandler when the promise // is not handling the rejection. We create a pseudo catch handler, which will prevent the // promise from being logged to the $exceptionHandler. return interim.deferred.promise.catch(angular.noop); } /** * Creates a function to wait for at least one interim element to be available. * @param callbackFn Function to be used as callback * @returns {Function} */ function waitForInterim(callbackFn) { return function() { var fnArguments = arguments; if (!showingInterims.length) { // When there are still interim's opening, then wait for the first interim element to // finish its open animation. if (showPromises.length) { return showPromises[0].finally(function () { return callbackFn.apply(service, fnArguments); }); } return $q.when("No interim elements currently showing up."); } return callbackFn.apply(service, fnArguments); }; } /* * Special method to quick-remove the interim element without animations * Note: interim elements are in "interim containers" */ function destroy(targetEl) { var interim = !targetEl ? showingInterims.shift() : null; var parentEl = angular.element(targetEl).length && angular.element(targetEl)[0].parentNode; if (parentEl) { // Try to find the interim in the stack which corresponds to the supplied DOM element. var filtered = showingInterims.filter(function(entry) { return entry.options.element[0] === parentEl; }); // Note: This function might be called when the element already has been removed, // in which case we won't find any matches. if (filtered.length) { interim = filtered[0]; showingInterims.splice(showingInterims.indexOf(interim), 1); } } return interim ? interim.remove(SHOW_CANCELLED, false, { '$destroy': true }) : $q.when(SHOW_CANCELLED); } /* * Internal Interim Element Object * Used internally to manage the DOM element and related data */ function InterimElement(options) { var self, element, showAction = $q.when(true); options = configureScopeAndTransitions(options); return self = { options : options, deferred: $q.defer(), show : createAndTransitionIn, remove : transitionOutAndRemove }; /** * Compile, link, and show this interim element * Use optional autoHided and transition-in effects */ function createAndTransitionIn() { return $q(function(resolve, reject) { // Trigger onCompiling callback before the compilation starts. // This is useful, when modifying options, which can be influenced by developers. options.onCompiling && options.onCompiling(options); compileElement(options) .then(function( compiledData ) { element = linkElement( compiledData, options ); // Expose the cleanup function from the compiler. options.cleanupElement = compiledData.cleanup; showAction = showElement(element, options, compiledData.controller) .then(resolve, rejectAll); }).catch(rejectAll); function rejectAll(fault) { // Force the '$md<xxx>.show()' promise to reject self.deferred.reject(fault); // Continue rejection propagation reject(fault); } }); } /** * After the show process has finished/rejected: * - announce 'removing', * - perform the transition-out, and * - perform optional clean up scope. */ function transitionOutAndRemove(response, isCancelled, opts) { // abort if the show() and compile failed if ( !element ) return $q.when(false); options = angular.extend(options || {}, opts || {}); options.cancelAutoHide && options.cancelAutoHide(); options.element.triggerHandler('$mdInterimElementRemove'); if ( options.$destroy === true ) { return hideElement(options.element, options).then(function(){ (isCancelled && rejectAll(response)) || resolveAll(response); }); } else { $q.when(showAction).finally(function() { hideElement(options.element, options).then(function() { isCancelled ? rejectAll(response) : resolveAll(response); }, rejectAll); }); return self.deferred.promise; } /** * The `show()` returns a promise that will be resolved when the interim * element is hidden or cancelled... */ function resolveAll(response) { self.deferred.resolve(response); } /** * Force the '$md<xxx>.show()' promise to reject */ function rejectAll(fault) { self.deferred.reject(fault); } } /** * Prepare optional isolated scope and prepare $animate with default enter and leave * transitions for the new element instance. */ function configureScopeAndTransitions(options) { options = options || { }; if ( options.template ) { options.template = $mdUtil.processTemplate(options.template); } return angular.extend({ preserveScope: false, cancelAutoHide : angular.noop, scope: options.scope || $rootScope.$new(options.isolateScope), /** * Default usage to enable $animate to transition-in; can be easily overridden via 'options' */ onShow: function transitionIn(scope, element, options) { return $animate.enter(element, options.parent); }, /** * Default usage to enable $animate to transition-out; can be easily overridden via 'options' */ onRemove: function transitionOut(scope, element) { // Element could be undefined if a new element is shown before // the old one finishes compiling. return element && $animate.leave(element) || $q.when(); } }, options ); } /** * Compile an element with a templateUrl, controller, and locals */ function compileElement(options) { var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null; return compiled || $q(function (resolve) { resolve({ locals: {}, link: function () { return options.element; } }); }); } /** * Link an element with compiled configuration */ function linkElement(compileData, options){ angular.extend(compileData.locals, options); var element = compileData.link(options.scope); // Search for parent at insertion time, if not specified options.element = element; options.parent = findParent(element, options); if (options.themable) $mdTheming(element); return element; } /** * Search for parent at insertion time, if not specified */ function findParent(element, options) { var parent = options.parent; // Search for parent at insertion time, if not specified if (angular.isFunction(parent)) { parent = parent(options.scope, element, options); } else if (angular.isString(parent)) { parent = angular.element($document[0].querySelector(parent)); } else { parent = angular.element(parent); } // If parent querySelector/getter function fails, or it's just null, // find a default. if (!(parent || {}).length) { var el; if ($rootElement[0] && $rootElement[0].querySelector) { el = $rootElement[0].querySelector(':not(svg) > body'); } if (!el) el = $rootElement[0]; if (el.nodeName == '#comment') { el = $document[0].body; } return angular.element(el); } return parent; } /** * If auto-hide is enabled, start timer and prepare cancel function */ function startAutoHide() { var autoHideTimer, cancelAutoHide = angular.noop; if (options.hideDelay) { autoHideTimer = $timeout(service.hide, options.hideDelay) ; cancelAutoHide = function() { $timeout.cancel(autoHideTimer); }; } // Cache for subsequent use options.cancelAutoHide = function() { cancelAutoHide(); options.cancelAutoHide = undefined; }; } /** * Show the element ( with transitions), notify complete and start * optional auto-Hide */ function showElement(element, options, controller) { // Trigger onShowing callback before the `show()` starts var notifyShowing = options.onShowing || angular.noop; // Trigger onComplete callback when the `show()` finishes var notifyComplete = options.onComplete || angular.noop; // Necessary for consistency between Angular 1.5 and 1.6. try { notifyShowing(options.scope, element, options, controller); } catch (e) { return $q.reject(e); } return $q(function (resolve, reject) { try { // Start transitionIn $q.when(options.onShow(options.scope, element, options, controller)) .then(function () { notifyComplete(options.scope, element, options); startAutoHide(); resolve(element); }, reject); } catch (e) { reject(e.message); } }); } function hideElement(element, options) { var announceRemoving = options.onRemoving || angular.noop; return $q(function (resolve, reject) { try { // Start transitionIn var action = $q.when( options.onRemove(options.scope, element, options) || true ); // Trigger callback *before* the remove operation starts announceRemoving(element, action); if (options.$destroy) { // For $destroy, onRemove should be synchronous resolve(element); if (!options.preserveScope && options.scope ) { // scope destroy should still be be done after the current digest is done action.then( function() { options.scope.$destroy(); }); } } else { // Wait until transition-out is done action.then(function () { if (!options.preserveScope && options.scope ) { options.scope.$destroy(); } resolve(element); }, reject); } } catch (e) { reject(e.message); } }); } } }; } }