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
JavaScript
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);
}
});
}
}
};
}
}