,
* openPanels: !Array,
* maxOpen: number}>} panelGroup
*/
MdPanelService.prototype.newPanelGroup = function(groupName, config) {
if (!this._groups[groupName]) {
config = config || {};
var group = {
panels: [],
openPanels: [],
maxOpen: config.maxOpen > 0 ? config.maxOpen : Infinity
};
this._groups[groupName] = group;
}
return this._groups[groupName];
};
/**
* Sets the maximum number of panels in a group that can be opened at a given
* time.
* @param {string} groupName The name of the group to configure.
* @param {number} maxOpen The maximum number of panels that can be
* opened. Infinity can be passed in to remove the maxOpen limit.
*/
MdPanelService.prototype.setGroupMaxOpen = function(groupName, maxOpen) {
if (this._groups[groupName]) {
this._groups[groupName].maxOpen = maxOpen;
} else {
throw new Error('mdPanel: Group does not exist yet. Call newPanelGroup().');
}
};
/**
* Determines if the current number of open panels within a group exceeds the
* limit of allowed open panels.
* @param {string} groupName The name of the group to check.
* @returns {boolean} true if open count does exceed maxOpen and false if not.
* @private
*/
MdPanelService.prototype._openCountExceedsMaxOpen = function(groupName) {
if (this._groups[groupName]) {
var group = this._groups[groupName];
return group.maxOpen > 0 && group.openPanels.length > group.maxOpen;
}
return false;
};
/**
* Closes the first open panel within a specific group.
* @param {string} groupName The name of the group.
* @private
*/
MdPanelService.prototype._closeFirstOpenedPanel = function(groupName) {
this._groups[groupName].openPanels[0].close();
};
/**
* Wraps the users template in two elements, md-panel-outer-wrapper, which
* covers the entire attachTo element, and md-panel, which contains only the
* template. This allows the panel control over positioning, animations,
* and similar properties.
* @param {string} origTemplate The original template.
* @returns {string} The wrapped template.
* @private
*/
MdPanelService.prototype._wrapTemplate = function(origTemplate) {
var template = origTemplate || '';
// The panel should be initially rendered offscreen so we can calculate
// height and width for positioning.
return '' +
'' +
'
' + template + '
' +
'
';
};
/**
* Wraps a content element in a md-panel-outer wrapper and
* positions it off-screen. Allows for proper control over positoning
* and animations.
* @param {!angular.JQLite} contentElement Element to be wrapped.
* @return {!angular.JQLite} Wrapper element.
* @private
*/
MdPanelService.prototype._wrapContentElement = function(contentElement) {
var wrapper = angular.element('');
contentElement.addClass('md-panel').css('left', '-9999px');
wrapper.append(contentElement);
return wrapper;
};
/*****************************************************************************
* MdPanelRef *
*****************************************************************************/
/**
* A reference to a created panel. This reference contains a unique id for the
* panel, along with properties/functions used to control the panel.
* @param {!Object} config
* @param {!angular.$injector} $injector
* @final @constructor
*/
function MdPanelRef(config, $injector) {
// Injected variables.
/** @private @const {!angular.$q} */
this._$q = $injector.get('$q');
/** @private @const {!angular.$mdCompiler} */
this._$mdCompiler = $injector.get('$mdCompiler');
/** @private @const {!angular.$mdConstant} */
this._$mdConstant = $injector.get('$mdConstant');
/** @private @const {!angular.$mdUtil} */
this._$mdUtil = $injector.get('$mdUtil');
/** @private @const {!angular.$mdTheming} */
this._$mdTheming = $injector.get('$mdTheming');
/** @private @const {!angular.Scope} */
this._$rootScope = $injector.get('$rootScope');
/** @private @const {!angular.$animate} */
this._$animate = $injector.get('$animate');
/** @private @const {!MdPanelRef} */
this._$mdPanel = $injector.get('$mdPanel');
/** @private @const {!angular.$log} */
this._$log = $injector.get('$log');
/** @private @const {!angular.$window} */
this._$window = $injector.get('$window');
/** @private @const {!Function} */
this._$$rAF = $injector.get('$$rAF');
// Public variables.
/**
* Unique id for the panelRef.
* @type {string}
*/
this.id = config.id;
/** @type {!Object} */
this.config = config;
/** @type {!angular.JQLite|undefined} */
this.panelContainer;
/** @type {!angular.JQLite|undefined} */
this.panelEl;
/**
* Whether the panel is attached. This is synchronous. When attach is called,
* isAttached is set to true. When detach is called, isAttached is set to
* false.
* @type {boolean}
*/
this.isAttached = false;
// Private variables.
/** @private {Array} */
this._removeListeners = [];
/** @private {!angular.JQLite|undefined} */
this._topFocusTrap;
/** @private {!angular.JQLite|undefined} */
this._bottomFocusTrap;
/** @private {!$mdPanel|undefined} */
this._backdropRef;
/** @private {Function?} */
this._restoreScroll = null;
/**
* Keeps track of all the panel interceptors.
* @private {!Object}
*/
this._interceptors = Object.create(null);
/**
* Cleanup function, provided by `$mdCompiler` and assigned after the element
* has been compiled. When `contentElement` is used, the function is used to
* restore the element to it's proper place in the DOM.
* @private {!Function}
*/
this._compilerCleanup = null;
/**
* Cache for saving and restoring element inline styles, CSS classes etc.
* @type {{styles: string, classes: string}}
*/
this._restoreCache = {
styles: '',
classes: ''
};
}
MdPanelRef.interceptorTypes = {
CLOSE: 'onClose'
};
/**
* Opens an already created and configured panel. If the panel is already
* visible, does nothing.
* @returns {!angular.$q.Promise} A promise that is resolved when
* the panel is opened and animations finish.
*/
MdPanelRef.prototype.open = function() {
var self = this;
return this._$q(function(resolve, reject) {
var done = self._done(resolve, self);
var show = self._simpleBind(self.show, self);
var checkGroupMaxOpen = function() {
if (self.config.groupName) {
angular.forEach(self.config.groupName, function(group) {
if (self._$mdPanel._openCountExceedsMaxOpen(group)) {
self._$mdPanel._closeFirstOpenedPanel(group);
}
});
}
};
self.attach()
.then(show)
.then(checkGroupMaxOpen)
.then(done)
.catch(reject);
});
};
/**
* Closes the panel.
* @param {string} closeReason The event type that triggered the close.
* @returns {!angular.$q.Promise} A promise that is resolved when
* the panel is closed and animations finish.
*/
MdPanelRef.prototype.close = function(closeReason) {
var self = this;
return this._$q(function(resolve, reject) {
self._callInterceptors(MdPanelRef.interceptorTypes.CLOSE).then(function() {
var done = self._done(resolve, self);
var detach = self._simpleBind(self.detach, self);
var onCloseSuccess = self.config['onCloseSuccess'] || angular.noop;
onCloseSuccess = angular.bind(self, onCloseSuccess, self, closeReason);
self.hide()
.then(detach)
.then(done)
.then(onCloseSuccess)
.catch(reject);
}, reject);
});
};
/**
* Attaches the panel. The panel will be hidden afterwards.
* @returns {!angular.$q.Promise} A promise that is resolved when
* the panel is attached.
*/
MdPanelRef.prototype.attach = function() {
if (this.isAttached && this.panelEl) {
return this._$q.when(this);
}
var self = this;
return this._$q(function(resolve, reject) {
var done = self._done(resolve, self);
var onDomAdded = self.config['onDomAdded'] || angular.noop;
var addListeners = function(response) {
self.isAttached = true;
self._addEventListeners();
return response;
};
self._$q.all([
self._createBackdrop(),
self._createPanel()
.then(addListeners)
.catch(reject)
]).then(onDomAdded)
.then(done)
.catch(reject);
});
};
/**
* Only detaches the panel. Will NOT hide the panel first.
* @returns {!angular.$q.Promise} A promise that is resolved when
* the panel is detached.
*/
MdPanelRef.prototype.detach = function() {
if (!this.isAttached) {
return this._$q.when(this);
}
var self = this;
var onDomRemoved = self.config['onDomRemoved'] || angular.noop;
var detachFn = function() {
self._removeEventListeners();
// Remove the focus traps that we added earlier for keeping focus within
// the panel.
if (self._topFocusTrap && self._topFocusTrap.parentNode) {
self._topFocusTrap.parentNode.removeChild(self._topFocusTrap);
}
if (self._bottomFocusTrap && self._bottomFocusTrap.parentNode) {
self._bottomFocusTrap.parentNode.removeChild(self._bottomFocusTrap);
}
if (self._restoreCache.classes) {
self.panelEl[0].className = self._restoreCache.classes;
}
// Either restore the saved styles or clear the ones set by mdPanel.
self.panelEl[0].style.cssText = self._restoreCache.styles || '';
self._compilerCleanup();
self.panelContainer.remove();
self.isAttached = false;
return self._$q.when(self);
};
if (this._restoreScroll) {
this._restoreScroll();
this._restoreScroll = null;
}
return this._$q(function(resolve, reject) {
var done = self._done(resolve, self);
self._$q.all([
detachFn(),
self._backdropRef ? self._backdropRef.detach() : true
]).then(onDomRemoved)
.then(done)
.catch(reject);
});
};
/**
* Destroys the panel. The Panel cannot be opened again after this.
*/
MdPanelRef.prototype.destroy = function() {
var self = this;
if (this.config.groupName) {
angular.forEach(this.config.groupName, function(group) {
self.removeFromGroup(group);
});
}
this.config.scope.$destroy();
this.config.locals = null;
this._interceptors = null;
};
/**
* Shows the panel.
* @returns {!angular.$q.Promise} A promise that is resolved when
* the panel has shown and animations finish.
*/
MdPanelRef.prototype.show = function() {
if (!this.panelContainer) {
return this._$q(function(resolve, reject) {
reject('mdPanel: Panel does not exist yet. Call open() or attach().');
});
}
if (!this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
return this._$q.when(this);
}
var self = this;
var animatePromise = function() {
self.panelContainer.removeClass(MD_PANEL_HIDDEN);
return self._animateOpen();
};
return this._$q(function(resolve, reject) {
var done = self._done(resolve, self);
var onOpenComplete = self.config['onOpenComplete'] || angular.noop;
var addToGroupOpen = function() {
if (self.config.groupName) {
angular.forEach(self.config.groupName, function(group) {
self._$mdPanel._groups[group].openPanels.push(self);
});
}
};
self._$q.all([
self._backdropRef ? self._backdropRef.show() : self,
animatePromise().then(function() { self._focusOnOpen(); }, reject)
]).then(onOpenComplete)
.then(addToGroupOpen)
.then(done)
.catch(reject);
});
};
/**
* Hides the panel.
* @returns {!angular.$q.Promise} A promise that is resolved when
* the panel has hidden and animations finish.
*/
MdPanelRef.prototype.hide = function() {
if (!this.panelContainer) {
return this._$q(function(resolve, reject) {
reject('mdPanel: Panel does not exist yet. Call open() or attach().');
});
}
if (this.panelContainer.hasClass(MD_PANEL_HIDDEN)) {
return this._$q.when(this);
}
var self = this;
return this._$q(function(resolve, reject) {
var done = self._done(resolve, self);
var onRemoving = self.config['onRemoving'] || angular.noop;
var hidePanel = function() {
self.panelContainer.addClass(MD_PANEL_HIDDEN);
};
var removeFromGroupOpen = function() {
if (self.config.groupName) {
var group, index;
angular.forEach(self.config.groupName, function(group) {
group = self._$mdPanel._groups[group];
index = group.openPanels.indexOf(self);
if (index > -1) {
group.openPanels.splice(index, 1);
}
});
}
};
var focusOnOrigin = function() {
var origin = self.config['origin'];
if (origin) {
getElement(origin).focus();
}
};
self._$q.all([
self._backdropRef ? self._backdropRef.hide() : self,
self._animateClose()
.then(onRemoving)
.then(hidePanel)
.then(removeFromGroupOpen)
.then(focusOnOrigin)
.catch(reject)
]).then(done, reject);
});
};
/**
* Add a class to the panel. DO NOT use this to hide/show the panel.
* @deprecated
* This method is in the process of being deprecated in favor of using the panel
* and container JQLite elements that are referenced in the MdPanelRef object.
* Full deprecation is scheduled for material 1.2.
*
* @param {string} newClass Class to be added.
* @param {boolean} toElement Whether or not to add the class to the panel
* element instead of the container.
*/
MdPanelRef.prototype.addClass = function(newClass, toElement) {
this._$log.warn(
'mdPanel: The addClass method is in the process of being deprecated. ' +
'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
'To achieve the same results, use the panelContainer or panelEl ' +
'JQLite elements that are referenced in MdPanelRef.');
if (!this.panelContainer) {
throw new Error(
'mdPanel: Panel does not exist yet. Call open() or attach().');
}
if (!toElement && !this.panelContainer.hasClass(newClass)) {
this.panelContainer.addClass(newClass);
} else if (toElement && !this.panelEl.hasClass(newClass)) {
this.panelEl.addClass(newClass);
}
};
/**
* Remove a class from the panel. DO NOT use this to hide/show the panel.
* @deprecated
* This method is in the process of being deprecated in favor of using the panel
* and container JQLite elements that are referenced in the MdPanelRef object.
* Full deprecation is scheduled for material 1.2.
*
* @param {string} oldClass Class to be removed.
* @param {boolean} fromElement Whether or not to remove the class from the
* panel element instead of the container.
*/
MdPanelRef.prototype.removeClass = function(oldClass, fromElement) {
this._$log.warn(
'mdPanel: The removeClass method is in the process of being deprecated. ' +
'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
'To achieve the same results, use the panelContainer or panelEl ' +
'JQLite elements that are referenced in MdPanelRef.');
if (!this.panelContainer) {
throw new Error(
'mdPanel: Panel does not exist yet. Call open() or attach().');
}
if (!fromElement && this.panelContainer.hasClass(oldClass)) {
this.panelContainer.removeClass(oldClass);
} else if (fromElement && this.panelEl.hasClass(oldClass)) {
this.panelEl.removeClass(oldClass);
}
};
/**
* Toggle a class on the panel. DO NOT use this to hide/show the panel.
* @deprecated
* This method is in the process of being deprecated in favor of using the panel
* and container JQLite elements that are referenced in the MdPanelRef object.
* Full deprecation is scheduled for material 1.2.
*
* @param {string} toggleClass The class to toggle.
* @param {boolean} onElement Whether or not to toggle the class on the panel
* element instead of the container.
*/
MdPanelRef.prototype.toggleClass = function(toggleClass, onElement) {
this._$log.warn(
'mdPanel: The toggleClass method is in the process of being deprecated. ' +
'Full deprecation is scheduled for the Angular Material 1.2 release. ' +
'To achieve the same results, use the panelContainer or panelEl ' +
'JQLite elements that are referenced in MdPanelRef.');
if (!this.panelContainer) {
throw new Error(
'mdPanel: Panel does not exist yet. Call open() or attach().');
}
if (!onElement) {
this.panelContainer.toggleClass(toggleClass);
} else {
this.panelEl.toggleClass(toggleClass);
}
};
/**
* Compiles the panel, according to the passed in config and appends it to
* the DOM. Helps normalize differences in the compilation process between
* using a string template and a content element.
* @returns {!angular.$q.Promise} Promise that is resolved when
* the element has been compiled and added to the DOM.
* @private
*/
MdPanelRef.prototype._compile = function() {
var self = this;
// Compile the element via $mdCompiler. Note that when using a
// contentElement, the element isn't actually being compiled, rather the
// compiler saves it's place in the DOM and provides a way of restoring it.
return self._$mdCompiler.compile(self.config).then(function(compileData) {
var config = self.config;
if (config.contentElement) {
var panelEl = compileData.element;
// Since mdPanel modifies the inline styles and CSS classes, we need
// to save them in order to be able to restore on close.
self._restoreCache.styles = panelEl[0].style.cssText;
self._restoreCache.classes = panelEl[0].className;
self.panelContainer = self._$mdPanel._wrapContentElement(panelEl);
self.panelEl = panelEl;
} else {
self.panelContainer = compileData.link(config['scope']);
self.panelEl = angular.element(
self.panelContainer[0].querySelector('.md-panel')
);
}
// Save a reference to the cleanup function from the compiler.
self._compilerCleanup = compileData.cleanup;
// Attach the panel to the proper place in the DOM.
getElement(self.config['attachTo']).append(self.panelContainer);
return self;
});
};
/**
* Creates a panel and adds it to the dom.
* @returns {!angular.$q.Promise} A promise that is resolved when the panel is
* created.
* @private
*/
MdPanelRef.prototype._createPanel = function() {
var self = this;
return this._$q(function(resolve, reject) {
if (!self.config.locals) {
self.config.locals = {};
}
self.config.locals.mdPanelRef = self;
self._compile().then(function() {
if (self.config['disableParentScroll']) {
self._restoreScroll = self._$mdUtil.disableScrollAround(
null,
self.panelContainer,
{ disableScrollMask: true }
);
}
// Add a custom CSS class to the panel element.
if (self.config['panelClass']) {
self.panelEl.addClass(self.config['panelClass']);
}
// Handle click and touch events for the panel container.
if (self.config['propagateContainerEvents']) {
self.panelContainer.css('pointer-events', 'none');
}
// Panel may be outside the $rootElement, tell ngAnimate to animate
// regardless.
if (self._$animate.pin) {
self._$animate.pin(
self.panelContainer,
getElement(self.config['attachTo'])
);
}
self._configureTrapFocus();
self._addStyles().then(function() {
resolve(self);
}, reject);
}, reject);
});
};
/**
* Adds the styles for the panel, such as positioning and z-index. Also,
* themes the panel element and panel container using `$mdTheming`.
* @returns {!angular.$q.Promise}
* @private
*/
MdPanelRef.prototype._addStyles = function() {
var self = this;
return this._$q(function(resolve) {
self.panelContainer.css('z-index', self.config['zIndex']);
self.panelEl.css('z-index', self.config['zIndex'] + 1);
var hideAndResolve = function() {
// Theme the element and container.
self._setTheming();
// Remove left: -9999px and add hidden class.
self.panelEl.css('left', '');
self.panelContainer.addClass(MD_PANEL_HIDDEN);
resolve(self);
};
if (self.config['fullscreen']) {
self.panelEl.addClass('_md-panel-fullscreen');
hideAndResolve();
return; // Don't setup positioning.
}
var positionConfig = self.config['position'];
if (!positionConfig) {
hideAndResolve();
return; // Don't setup positioning.
}
// Wait for angular to finish processing the template
self._$rootScope['$$postDigest'](function() {
// Position it correctly. This is necessary so that the panel will have a
// defined height and width.
self._updatePosition(true);
// Theme the element and container.
self._setTheming();
resolve(self);
});
});
};
/**
* Sets the `$mdTheming` classes on the `panelContainer` and `panelEl`.
* @private
*/
MdPanelRef.prototype._setTheming = function() {
this._$mdTheming(this.panelEl);
this._$mdTheming(this.panelContainer);
};
/**
* Updates the position configuration of a panel
* @param {!MdPanelPosition} position
*/
MdPanelRef.prototype.updatePosition = function(position) {
if (!this.panelContainer) {
throw new Error(
'mdPanel: Panel does not exist yet. Call open() or attach().');
}
this.config['position'] = position;
this._updatePosition();
};
/**
* Calculates and updates the position of the panel.
* @param {boolean=} init
* @private
*/
MdPanelRef.prototype._updatePosition = function(init) {
var positionConfig = this.config['position'];
if (positionConfig) {
positionConfig._setPanelPosition(this.panelEl);
// Hide the panel now that position is known.
if (init) {
this.panelContainer.addClass(MD_PANEL_HIDDEN);
}
this.panelEl.css(
MdPanelPosition.absPosition.TOP,
positionConfig.getTop()
);
this.panelEl.css(
MdPanelPosition.absPosition.BOTTOM,
positionConfig.getBottom()
);
this.panelEl.css(
MdPanelPosition.absPosition.LEFT,
positionConfig.getLeft()
);
this.panelEl.css(
MdPanelPosition.absPosition.RIGHT,
positionConfig.getRight()
);
}
};
/**
* Focuses on the panel or the first focus target.
* @private
*/
MdPanelRef.prototype._focusOnOpen = function() {
if (this.config['focusOnOpen']) {
// Wait for the template to finish rendering to guarantee md-autofocus has
// finished adding the class md-autofocus, otherwise the focusable element
// isn't available to focus.
var self = this;
this._$rootScope['$$postDigest'](function() {
var target = self._$mdUtil.findFocusTarget(self.panelEl) ||
self.panelEl;
target.focus();
});
}
};
/**
* Shows the backdrop.
* @returns {!angular.$q.Promise} A promise that is resolved when the backdrop
* is created and attached.
* @private
*/
MdPanelRef.prototype._createBackdrop = function() {
if (this.config.hasBackdrop) {
if (!this._backdropRef) {
var backdropAnimation = this._$mdPanel.newPanelAnimation()
.openFrom(this.config.attachTo)
.withAnimation({
open: '_md-opaque-enter',
close: '_md-opaque-leave'
});
if (this.config.animation) {
backdropAnimation.duration(this.config.animation._rawDuration);
}
var backdropConfig = {
animation: backdropAnimation,
attachTo: this.config.attachTo,
focusOnOpen: false,
panelClass: '_md-panel-backdrop',
zIndex: this.config.zIndex - 1
};
this._backdropRef = this._$mdPanel.create(backdropConfig);
}
if (!this._backdropRef.isAttached) {
return this._backdropRef.attach();
}
}
};
/**
* Listen for escape keys and outside clicks to auto close.
* @private
*/
MdPanelRef.prototype._addEventListeners = function() {
this._configureEscapeToClose();
this._configureClickOutsideToClose();
this._configureScrollListener();
};
/**
* Remove event listeners added in _addEventListeners.
* @private
*/
MdPanelRef.prototype._removeEventListeners = function() {
this._removeListeners && this._removeListeners.forEach(function(removeFn) {
removeFn();
});
this._removeListeners = [];
};
/**
* Setup the escapeToClose event listeners.
* @private
*/
MdPanelRef.prototype._configureEscapeToClose = function() {
if (this.config['escapeToClose']) {
var parentTarget = getElement(this.config['attachTo']);
var self = this;
var keyHandlerFn = function(ev) {
if (ev.keyCode === self._$mdConstant.KEY_CODE.ESCAPE) {
ev.stopPropagation();
ev.preventDefault();
self.close(MdPanelRef.closeReasons.ESCAPE);
}
};
// Add keydown listeners
this.panelContainer.on('keydown', keyHandlerFn);
parentTarget.on('keydown', keyHandlerFn);
// Queue remove listeners function
this._removeListeners.push(function() {
self.panelContainer.off('keydown', keyHandlerFn);
parentTarget.off('keydown', keyHandlerFn);
});
}
};
/**
* Setup the clickOutsideToClose event listeners.
* @private
*/
MdPanelRef.prototype._configureClickOutsideToClose = function() {
if (this.config['clickOutsideToClose']) {
var target = this.config['propagateContainerEvents'] ?
angular.element(document.body) :
this.panelContainer;
var sourceEl;
// Keep track of the element on which the mouse originally went down
// so that we can only close the backdrop when the 'click' started on it.
// A simple 'click' handler does not work, it sets the target object as the
// element the mouse went down on.
var mousedownHandler = function(ev) {
sourceEl = ev.target;
};
// We check if our original element and the target is the backdrop
// because if the original was the backdrop and the target was inside the
// panel we don't want to panel to close.
var self = this;
var mouseupHandler = function(ev) {
if (self.config['propagateContainerEvents']) {
// We check if the sourceEl of the event is the panel element or one
// of it's children. If it is not, then close the panel.
if (sourceEl !== self.panelEl[0] && !self.panelEl[0].contains(sourceEl)) {
self.close();
}
} else if (sourceEl === target[0] && ev.target === target[0]) {
ev.stopPropagation();
ev.preventDefault();
self.close(MdPanelRef.closeReasons.CLICK_OUTSIDE);
}
};
// Add listeners
target.on('mousedown', mousedownHandler);
target.on('mouseup', mouseupHandler);
// Queue remove listeners function
this._removeListeners.push(function() {
target.off('mousedown', mousedownHandler);
target.off('mouseup', mouseupHandler);
});
}
};
/**
* Configures the listeners for updating the panel position on scroll.
* @private
*/
MdPanelRef.prototype._configureScrollListener = function() {
// No need to bind the event if scrolling is disabled.
if (!this.config['disableParentScroll']) {
var updatePosition = angular.bind(this, this._updatePosition);
var debouncedUpdatePosition = this._$$rAF.throttle(updatePosition);
var self = this;
var onScroll = function() {
debouncedUpdatePosition();
};
// Add listeners.
this._$window.addEventListener('scroll', onScroll, true);
// Queue remove listeners function.
this._removeListeners.push(function() {
self._$window.removeEventListener('scroll', onScroll, true);
});
}
};
/**
* Setup the focus traps. These traps will wrap focus when tabbing past the
* panel. When shift-tabbing, the focus will stick in place.
* @private
*/
MdPanelRef.prototype._configureTrapFocus = function() {
// Focus doesn't remain inside of the panel without this.
this.panelEl.attr('tabIndex', '-1');
if (this.config['trapFocus']) {
var element = this.panelEl;
// Set up elements before and after the panel to capture focus and
// redirect back into the panel.
this._topFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
this._bottomFocusTrap = FOCUS_TRAP_TEMPLATE.clone()[0];
// When focus is about to move out of the panel, we want to intercept it
// and redirect it back to the panel element.
var focusHandler = function() {
element.focus();
};
this._topFocusTrap.addEventListener('focus', focusHandler);
this._bottomFocusTrap.addEventListener('focus', focusHandler);
// Queue remove listeners function
this._removeListeners.push(this._simpleBind(function() {
this._topFocusTrap.removeEventListener('focus', focusHandler);
this._bottomFocusTrap.removeEventListener('focus', focusHandler);
}, this));
// The top focus trap inserted immediately before the md-panel element (as
// a sibling). The bottom focus trap inserted immediately after the
// md-panel element (as a sibling).
element[0].parentNode.insertBefore(this._topFocusTrap, element[0]);
element.after(this._bottomFocusTrap);
}
};
/**
* Updates the animation of a panel.
* @param {!MdPanelAnimation} animation
*/
MdPanelRef.prototype.updateAnimation = function(animation) {
this.config['animation'] = animation;
if (this._backdropRef) {
this._backdropRef.config.animation.duration(animation._rawDuration);
}
};
/**
* Animate the panel opening.
* @returns {!angular.$q.Promise} A promise that is resolved when the panel has
* animated open.
* @private
*/
MdPanelRef.prototype._animateOpen = function() {
this.panelContainer.addClass('md-panel-is-showing');
var animationConfig = this.config['animation'];
if (!animationConfig) {
// Promise is in progress, return it.
this.panelContainer.addClass('_md-panel-shown');
return this._$q.when(this);
}
var self = this;
return this._$q(function(resolve) {
var done = self._done(resolve, self);
var warnAndOpen = function() {
self._$log.warn(
'mdPanel: MdPanel Animations failed. ' +
'Showing panel without animating.');
done();
};
animationConfig.animateOpen(self.panelEl)
.then(done, warnAndOpen);
});
};
/**
* Animate the panel closing.
* @returns {!angular.$q.Promise} A promise that is resolved when the panel has
* animated closed.
* @private
*/
MdPanelRef.prototype._animateClose = function() {
var animationConfig = this.config['animation'];
if (!animationConfig) {
this.panelContainer.removeClass('md-panel-is-showing');
this.panelContainer.removeClass('_md-panel-shown');
return this._$q.when(this);
}
var self = this;
return this._$q(function(resolve) {
var done = function() {
self.panelContainer.removeClass('md-panel-is-showing');
resolve(self);
};
var warnAndClose = function() {
self._$log.warn(
'mdPanel: MdPanel Animations failed. ' +
'Hiding panel without animating.');
done();
};
animationConfig.animateClose(self.panelEl)
.then(done, warnAndClose);
});
};
/**
* Registers a interceptor with the panel. The callback should return a promise,
* which will allow the action to continue when it gets resolved, or will
* prevent an action if it is rejected.
* @param {string} type Type of interceptor.
* @param {!angular.$q.Promise} callback Callback to be registered.
* @returns {!MdPanelRef}
*/
MdPanelRef.prototype.registerInterceptor = function(type, callback) {
var error = null;
if (!angular.isString(type)) {
error = 'Interceptor type must be a string, instead got ' + typeof type;
} else if (!angular.isFunction(callback)) {
error = 'Interceptor callback must be a function, instead got ' + typeof callback;
}
if (error) {
throw new Error('MdPanel: ' + error);
}
var interceptors = this._interceptors[type] = this._interceptors[type] || [];
if (interceptors.indexOf(callback) === -1) {
interceptors.push(callback);
}
return this;
};
/**
* Removes a registered interceptor.
* @param {string} type Type of interceptor to be removed.
* @param {Function} callback Interceptor to be removed.
* @returns {!MdPanelRef}
*/
MdPanelRef.prototype.removeInterceptor = function(type, callback) {
var index = this._interceptors[type] ?
this._interceptors[type].indexOf(callback) : -1;
if (index > -1) {
this._interceptors[type].splice(index, 1);
}
return this;
};
/**
* Removes all interceptors.
* @param {string=} type Type of interceptors to be removed.
* If ommited, all interceptors types will be removed.
* @returns {!MdPanelRef}
*/
MdPanelRef.prototype.removeAllInterceptors = function(type) {
if (type) {
this._interceptors[type] = [];
} else {
this._interceptors = Object.create(null);
}
return this;
};
/**
* Invokes all the interceptors of a certain type sequantially in
* reverse order. Works in a similar way to `$q.all`, except it
* respects the order of the functions.
* @param {string} type Type of interceptors to be invoked.
* @returns {!angular.$q.Promise}
* @private
*/
MdPanelRef.prototype._callInterceptors = function(type) {
var self = this;
var $q = self._$q;
var interceptors = self._interceptors && self._interceptors[type] || [];
return interceptors.reduceRight(function(promise, interceptor) {
var isPromiseLike = interceptor && angular.isFunction(interceptor.then);
var response = isPromiseLike ? interceptor : null;
/**
* For interceptors to reject/cancel subsequent portions of the chain, simply
* return a `$q.reject()`
*/
return promise.then(function() {
if (!response) {
try {
response = interceptor(self);
} catch(e) {
response = $q.reject(e);
}
}
return response;
});
}, $q.resolve(self));
};
/**
* Faster, more basic than angular.bind
* http://jsperf.com/angular-bind-vs-custom-vs-native
* @param {function} callback
* @param {!Object} self
* @return {function} Callback function with a bound self.
*/
MdPanelRef.prototype._simpleBind = function(callback, self) {
return function(value) {
return callback.apply(self, value);
};
};
/**
* @param {function} callback
* @param {!Object} self
* @return {function} Callback function with a self param.
*/
MdPanelRef.prototype._done = function(callback, self) {
return function() {
callback(self);
};
};
/**
* Adds a panel to a group if the panel does not exist within the group already.
* A panel can only exist within a single group.
* @param {string} groupName The name of the group.
*/
MdPanelRef.prototype.addToGroup = function(groupName) {
if (!this._$mdPanel._groups[groupName]) {
this._$mdPanel.newPanelGroup(groupName);
}
var group = this._$mdPanel._groups[groupName];
var index = group.panels.indexOf(this);
if (index < 0) {
group.panels.push(this);
}
};
/**
* Removes a panel from a group if the panel exists within that group. The group
* must be created ahead of time.
* @param {string} groupName The name of the group.
*/
MdPanelRef.prototype.removeFromGroup = function(groupName) {
if (!this._$mdPanel._groups[groupName]) {
throw new Error('mdPanel: The group ' + groupName + ' does not exist.');
}
var group = this._$mdPanel._groups[groupName];
var index = group.panels.indexOf(this);
if (index > -1) {
group.panels.splice(index, 1);
}
};
/**
* Possible default closeReasons for the close function.
* @enum {string}
*/
MdPanelRef.closeReasons = {
CLICK_OUTSIDE: 'clickOutsideToClose',
ESCAPE: 'escapeToClose',
};
/*****************************************************************************
* MdPanelPosition *
*****************************************************************************/
/**
* Position configuration object. To use, create an MdPanelPosition with the
* desired properties, then pass the object as part of $mdPanel creation.
*
* Example:
*
* var panelPosition = new MdPanelPosition()
* .relativeTo(myButtonEl)
* .addPanelPosition(
* $mdPanel.xPosition.CENTER,
* $mdPanel.yPosition.ALIGN_TOPS
* );
*
* $mdPanel.create({
* position: panelPosition
* });
*
* @param {!angular.$injector} $injector
* @final @constructor
*/
function MdPanelPosition($injector) {
/** @private @const {!angular.$window} */
this._$window = $injector.get('$window');
/** @private {boolean} */
this._isRTL = $injector.get('$mdUtil').bidi() === 'rtl';
/** @private @const {!angular.$mdConstant} */
this._$mdConstant = $injector.get('$mdConstant');
/** @private {boolean} */
this._absolute = false;
/** @private {!angular.JQLite} */
this._relativeToEl;
/** @private {string} */
this._top = '';
/** @private {string} */
this._bottom = '';
/** @private {string} */
this._left = '';
/** @private {string} */
this._right = '';
/** @private {!Array} */
this._translateX = [];
/** @private {!Array} */
this._translateY = [];
/** @private {!Array<{x:string, y:string}>} */
this._positions = [];
/** @private {?{x:string, y:string}} */
this._actualPosition;
}
/**
* Possible values of xPosition.
* @enum {string}
*/
MdPanelPosition.xPosition = {
CENTER: 'center',
ALIGN_START: 'align-start',
ALIGN_END: 'align-end',
OFFSET_START: 'offset-start',
OFFSET_END: 'offset-end'
};
/**
* Possible values of yPosition.
* @enum {string}
*/
MdPanelPosition.yPosition = {
CENTER: 'center',
ALIGN_TOPS: 'align-tops',
ALIGN_BOTTOMS: 'align-bottoms',
ABOVE: 'above',
BELOW: 'below'
};
/**
* Possible values of absolute position.
* @enum {string}
*/
MdPanelPosition.absPosition = {
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom',
LEFT: 'left'
};
/**
* Margin between the edges of a panel and the viewport.
* @const {number}
*/
MdPanelPosition.viewportMargin = 8;
/**
* Sets absolute positioning for the panel.
* @return {!MdPanelPosition}
*/
MdPanelPosition.prototype.absolute = function() {
this._absolute = true;
return this;
};
/**
* Sets the value of a position for the panel. Clears any previously set
* position.
* @param {string} position Position to set
* @param {string=} value Value of the position. Defaults to '0'.
* @returns {!MdPanelPosition}
* @private
*/
MdPanelPosition.prototype._setPosition = function(position, value) {
if (position === MdPanelPosition.absPosition.RIGHT ||
position === MdPanelPosition.absPosition.LEFT) {
this._left = this._right = '';
} else if (
position === MdPanelPosition.absPosition.BOTTOM ||
position === MdPanelPosition.absPosition.TOP) {
this._top = this._bottom = '';
} else {
var positions = Object.keys(MdPanelPosition.absPosition).join()
.toLowerCase();
throw new Error('mdPanel: Position must be one of ' + positions + '.');
}
this['_' + position] = angular.isString(value) ? value : '0';
return this;
};
/**
* Sets the value of `top` for the panel. Clears any previously set vertical
* position.
* @param {string=} top Value of `top`. Defaults to '0'.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.top = function(top) {
return this._setPosition(MdPanelPosition.absPosition.TOP, top);
};
/**
* Sets the value of `bottom` for the panel. Clears any previously set vertical
* position.
* @param {string=} bottom Value of `bottom`. Defaults to '0'.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.bottom = function(bottom) {
return this._setPosition(MdPanelPosition.absPosition.BOTTOM, bottom);
};
/**
* Sets the panel to the start of the page - `left` if `ltr` or `right` for
* `rtl`. Clears any previously set horizontal position.
* @param {string=} start Value of position. Defaults to '0'.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.start = function(start) {
var position = this._isRTL ? MdPanelPosition.absPosition.RIGHT : MdPanelPosition.absPosition.LEFT;
return this._setPosition(position, start);
};
/**
* Sets the panel to the end of the page - `right` if `ltr` or `left` for `rtl`.
* Clears any previously set horizontal position.
* @param {string=} end Value of position. Defaults to '0'.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.end = function(end) {
var position = this._isRTL ? MdPanelPosition.absPosition.LEFT : MdPanelPosition.absPosition.RIGHT;
return this._setPosition(position, end);
};
/**
* Sets the value of `left` for the panel. Clears any previously set
* horizontal position.
* @param {string=} left Value of `left`. Defaults to '0'.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.left = function(left) {
return this._setPosition(MdPanelPosition.absPosition.LEFT, left);
};
/**
* Sets the value of `right` for the panel. Clears any previously set
* horizontal position.
* @param {string=} right Value of `right`. Defaults to '0'.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.right = function(right) {
return this._setPosition(MdPanelPosition.absPosition.RIGHT, right);
};
/**
* Centers the panel horizontally in the viewport. Clears any previously set
* horizontal position.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.centerHorizontally = function() {
this._left = '50%';
this._right = '';
this._translateX = ['-50%'];
return this;
};
/**
* Centers the panel vertically in the viewport. Clears any previously set
* vertical position.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.centerVertically = function() {
this._top = '50%';
this._bottom = '';
this._translateY = ['-50%'];
return this;
};
/**
* Centers the panel horizontally and vertically in the viewport. This is
* equivalent to calling both `centerHorizontally` and `centerVertically`.
* Clears any previously set horizontal and vertical positions.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.center = function() {
return this.centerHorizontally().centerVertically();
};
/**
* Sets element for relative positioning.
* @param {string|!Element|!angular.JQLite} element Query selector, DOM element,
* or angular element to set the panel relative to.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.relativeTo = function(element) {
this._absolute = false;
this._relativeToEl = getElement(element);
return this;
};
/**
* Sets the x and y positions for the panel relative to another element.
* @param {string} xPosition must be one of the MdPanelPosition.xPosition
* values.
* @param {string} yPosition must be one of the MdPanelPosition.yPosition
* values.
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.addPanelPosition = function(xPosition, yPosition) {
if (!this._relativeToEl) {
throw new Error('mdPanel: addPanelPosition can only be used with ' +
'relative positioning. Set relativeTo first.');
}
this._validateXPosition(xPosition);
this._validateYPosition(yPosition);
this._positions.push({
x: xPosition,
y: yPosition,
});
return this;
};
/**
* Ensures that yPosition is a valid position name. Throw an exception if not.
* @param {string} yPosition
*/
MdPanelPosition.prototype._validateYPosition = function(yPosition) {
// empty is ok
if (yPosition == null) {
return;
}
var positionKeys = Object.keys(MdPanelPosition.yPosition);
var positionValues = [];
for (var key, i = 0; key = positionKeys[i]; i++) {
var position = MdPanelPosition.yPosition[key];
positionValues.push(position);
if (position === yPosition) {
return;
}
}
throw new Error('mdPanel: Panel y position only accepts the following ' +
'values:\n' + positionValues.join(' | '));
};
/**
* Ensures that xPosition is a valid position name. Throw an exception if not.
* @param {string} xPosition
*/
MdPanelPosition.prototype._validateXPosition = function(xPosition) {
// empty is ok
if (xPosition == null) {
return;
}
var positionKeys = Object.keys(MdPanelPosition.xPosition);
var positionValues = [];
for (var key, i = 0; key = positionKeys[i]; i++) {
var position = MdPanelPosition.xPosition[key];
positionValues.push(position);
if (position === xPosition) {
return;
}
}
throw new Error('mdPanel: Panel x Position only accepts the following ' +
'values:\n' + positionValues.join(' | '));
};
/**
* Sets the value of the offset in the x-direction. This will add to any
* previously set offsets.
* @param {string|function(MdPanelPosition): string} offsetX
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.withOffsetX = function(offsetX) {
this._translateX.push(offsetX);
return this;
};
/**
* Sets the value of the offset in the y-direction. This will add to any
* previously set offsets.
* @param {string|function(MdPanelPosition): string} offsetY
* @returns {!MdPanelPosition}
*/
MdPanelPosition.prototype.withOffsetY = function(offsetY) {
this._translateY.push(offsetY);
return this;
};
/**
* Gets the value of `top` for the panel.
* @returns {string}
*/
MdPanelPosition.prototype.getTop = function() {
return this._top;
};
/**
* Gets the value of `bottom` for the panel.
* @returns {string}
*/
MdPanelPosition.prototype.getBottom = function() {
return this._bottom;
};
/**
* Gets the value of `left` for the panel.
* @returns {string}
*/
MdPanelPosition.prototype.getLeft = function() {
return this._left;
};
/**
* Gets the value of `right` for the panel.
* @returns {string}
*/
MdPanelPosition.prototype.getRight = function() {
return this._right;
};
/**
* Gets the value of `transform` for the panel.
* @returns {string}
*/
MdPanelPosition.prototype.getTransform = function() {
var translateX = this._reduceTranslateValues('translateX', this._translateX);
var translateY = this._reduceTranslateValues('translateY', this._translateY);
// It's important to trim the result, because the browser will ignore the set
// operation if the string contains only whitespace.
return (translateX + ' ' + translateY).trim();
};
/**
* Sets the `transform` value for a panel element.
* @param {!angular.JQLite} panelEl
* @returns {!angular.JQLite}
* @private
*/
MdPanelPosition.prototype._setTransform = function(panelEl) {
return panelEl.css(this._$mdConstant.CSS.TRANSFORM, this.getTransform());
};
/**
* True if the panel is completely on-screen with this positioning; false
* otherwise.
* @param {!angular.JQLite} panelEl
* @return {boolean}
* @private
*/
MdPanelPosition.prototype._isOnscreen = function(panelEl) {
// this works because we always use fixed positioning for the panel,
// which is relative to the viewport.
var left = parseInt(this.getLeft());
var top = parseInt(this.getTop());
if (this._translateX.length || this._translateY.length) {
var prefixedTransform = this._$mdConstant.CSS.TRANSFORM;
var offsets = getComputedTranslations(panelEl, prefixedTransform);
left += offsets.x;
top += offsets.y;
}
var right = left + panelEl[0].offsetWidth;
var bottom = top + panelEl[0].offsetHeight;
return (left >= 0) &&
(top >= 0) &&
(bottom <= this._$window.innerHeight) &&
(right <= this._$window.innerWidth);
};
/**
* Gets the first x/y position that can fit on-screen.
* @returns {{x: string, y: string}}
*/
MdPanelPosition.prototype.getActualPosition = function() {
return this._actualPosition;
};
/**
* Reduces a list of translate values to a string that can be used within
* transform.
* @param {string} translateFn
* @param {!Array} values
* @returns {string}
* @private
*/
MdPanelPosition.prototype._reduceTranslateValues =
function(translateFn, values) {
return values.map(function(translation) {
// TODO(crisbeto): this should add the units after #9609 is merged.
var translationValue = angular.isFunction(translation) ?
translation(this) : translation;
return translateFn + '(' + translationValue + ')';
}, this).join(' ');
};
/**
* Sets the panel position based on the created panel element and best x/y
* positioning.
* @param {!angular.JQLite} panelEl
* @private
*/
MdPanelPosition.prototype._setPanelPosition = function(panelEl) {
// Remove the "position adjusted" class in case it has been added before.
panelEl.removeClass('_md-panel-position-adjusted');
// Only calculate the position if necessary.
if (this._absolute) {
this._setTransform(panelEl);
return;
}
if (this._actualPosition) {
this._calculatePanelPosition(panelEl, this._actualPosition);
this._setTransform(panelEl);
this._constrainToViewport(panelEl);
return;
}
for (var i = 0; i < this._positions.length; i++) {
this._actualPosition = this._positions[i];
this._calculatePanelPosition(panelEl, this._actualPosition);
this._setTransform(panelEl);
if (this._isOnscreen(panelEl)) {
return;
}
}
this._constrainToViewport(panelEl);
};
/**
* Constrains a panel's position to the viewport.
* @param {!angular.JQLite} panelEl
* @private
*/
MdPanelPosition.prototype._constrainToViewport = function(panelEl) {
var margin = MdPanelPosition.viewportMargin;
var initialTop = this._top;
var initialLeft = this._left;
if (this.getTop()) {
var top = parseInt(this.getTop());
var bottom = panelEl[0].offsetHeight + top;
var viewportHeight = this._$window.innerHeight;
if (top < margin) {
this._top = margin + 'px';
} else if (bottom > viewportHeight) {
this._top = top - (bottom - viewportHeight + margin) + 'px';
}
}
if (this.getLeft()) {
var left = parseInt(this.getLeft());
var right = panelEl[0].offsetWidth + left;
var viewportWidth = this._$window.innerWidth;
if (left < margin) {
this._left = margin + 'px';
} else if (right > viewportWidth) {
this._left = left - (right - viewportWidth + margin) + 'px';
}
}
// Class that can be used to re-style the panel if it was repositioned.
panelEl.toggleClass(
'_md-panel-position-adjusted',
this._top !== initialTop || this._left !== initialLeft
);
};
/**
* Switches between 'start' and 'end'.
* @param {string} position Horizontal position of the panel
* @returns {string} Reversed position
* @private
*/
MdPanelPosition.prototype._reverseXPosition = function(position) {
if (position === MdPanelPosition.xPosition.CENTER) {
return;
}
var start = 'start';
var end = 'end';
return position.indexOf(start) > -1 ? position.replace(start, end) : position.replace(end, start);
};
/**
* Handles horizontal positioning in rtl or ltr environments.
* @param {string} position Horizontal position of the panel
* @returns {string} The correct position according the page direction
* @private
*/
MdPanelPosition.prototype._bidi = function(position) {
return this._isRTL ? this._reverseXPosition(position) : position;
};
/**
* Calculates the panel position based on the created panel element and the
* provided positioning.
* @param {!angular.JQLite} panelEl
* @param {!{x:string, y:string}} position
* @private
*/
MdPanelPosition.prototype._calculatePanelPosition = function(panelEl, position) {
var panelBounds = panelEl[0].getBoundingClientRect();
var panelWidth = panelBounds.width;
var panelHeight = panelBounds.height;
var targetBounds = this._relativeToEl[0].getBoundingClientRect();
var targetLeft = targetBounds.left;
var targetRight = targetBounds.right;
var targetWidth = targetBounds.width;
switch (this._bidi(position.x)) {
case MdPanelPosition.xPosition.OFFSET_START:
this._left = targetLeft - panelWidth + 'px';
break;
case MdPanelPosition.xPosition.ALIGN_END:
this._left = targetRight - panelWidth + 'px';
break;
case MdPanelPosition.xPosition.CENTER:
var left = targetLeft + (0.5 * targetWidth) - (0.5 * panelWidth);
this._left = left + 'px';
break;
case MdPanelPosition.xPosition.ALIGN_START:
this._left = targetLeft + 'px';
break;
case MdPanelPosition.xPosition.OFFSET_END:
this._left = targetRight + 'px';
break;
}
var targetTop = targetBounds.top;
var targetBottom = targetBounds.bottom;
var targetHeight = targetBounds.height;
switch (position.y) {
case MdPanelPosition.yPosition.ABOVE:
this._top = targetTop - panelHeight + 'px';
break;
case MdPanelPosition.yPosition.ALIGN_BOTTOMS:
this._top = targetBottom - panelHeight + 'px';
break;
case MdPanelPosition.yPosition.CENTER:
var top = targetTop + (0.5 * targetHeight) - (0.5 * panelHeight);
this._top = top + 'px';
break;
case MdPanelPosition.yPosition.ALIGN_TOPS:
this._top = targetTop + 'px';
break;
case MdPanelPosition.yPosition.BELOW:
this._top = targetBottom + 'px';
break;
}
};
/*****************************************************************************
* MdPanelAnimation *
*****************************************************************************/
/**
* Animation configuration object. To use, create an MdPanelAnimation with the
* desired properties, then pass the object as part of $mdPanel creation.
*
* Example:
*
* var panelAnimation = new MdPanelAnimation()
* .openFrom(myButtonEl)
* .closeTo('.my-button')
* .withAnimation($mdPanel.animation.SCALE);
*
* $mdPanel.create({
* animation: panelAnimation
* });
*
* @param {!angular.$injector} $injector
* @final @constructor
*/
function MdPanelAnimation($injector) {
/** @private @const {!angular.$mdUtil} */
this._$mdUtil = $injector.get('$mdUtil');
/**
* @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}|
* undefined}
*/
this._openFrom;
/**
* @private {{element: !angular.JQLite|undefined, bounds: !DOMRect}|
* undefined}
*/
this._closeTo;
/** @private {string|{open: string, close: string}} */
this._animationClass = '';
/** @private {number} */
this._openDuration;
/** @private {number} */
this._closeDuration;
/** @private {number|{open: number, close: number}} */
this._rawDuration;
}
/**
* Possible default animations.
* @enum {string}
*/
MdPanelAnimation.animation = {
SLIDE: 'md-panel-animate-slide',
SCALE: 'md-panel-animate-scale',
FADE: 'md-panel-animate-fade'
};
/**
* Specifies where to start the open animation. `openFrom` accepts a
* click event object, query selector, DOM element, or a Rect object that
* is used to determine the bounds. When passed a click event, the location
* of the click will be used as the position to start the animation.
* @param {string|!Element|!Event|{top: number, left: number}} openFrom
* @returns {!MdPanelAnimation}
*/
MdPanelAnimation.prototype.openFrom = function(openFrom) {
// Check if 'openFrom' is an Event.
openFrom = openFrom.target ? openFrom.target : openFrom;
this._openFrom = this._getPanelAnimationTarget(openFrom);
if (!this._closeTo) {
this._closeTo = this._openFrom;
}
return this;
};
/**
* Specifies where to animate the panel close. `closeTo` accepts a
* query selector, DOM element, or a Rect object that is used to determine
* the bounds.
* @param {string|!Element|{top: number, left: number}} closeTo
* @returns {!MdPanelAnimation}
*/
MdPanelAnimation.prototype.closeTo = function(closeTo) {
this._closeTo = this._getPanelAnimationTarget(closeTo);
return this;
};
/**
* Specifies the duration of the animation in milliseconds.
* @param {number|{open: number, close: number}} duration
* @returns {!MdPanelAnimation}
*/
MdPanelAnimation.prototype.duration = function(duration) {
if (duration) {
if (angular.isNumber(duration)) {
this._openDuration = this._closeDuration = toSeconds(duration);
} else if (angular.isObject(duration)) {
this._openDuration = toSeconds(duration.open);
this._closeDuration = toSeconds(duration.close);
}
}
// Save the original value so it can be passed to the backdrop.
this._rawDuration = duration;
return this;
function toSeconds(value) {
if (angular.isNumber(value)) return value / 1000;
}
};
/**
* Returns the element and bounds for the animation target.
* @param {string|!Element|{top: number, left: number}} location
* @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}}
* @private
*/
MdPanelAnimation.prototype._getPanelAnimationTarget = function(location) {
if (angular.isDefined(location.top) || angular.isDefined(location.left)) {
return {
element: undefined,
bounds: {
top: location.top || 0,
left: location.left || 0
}
};
} else {
return this._getBoundingClientRect(getElement(location));
}
};
/**
* Specifies the animation class.
*
* There are several default animations that can be used:
* (MdPanelAnimation.animation)
* SLIDE: The panel slides in and out from the specified
* elements.
* SCALE: The panel scales in and out.
* FADE: The panel fades in and out.
*
* @param {string|{open: string, close: string}} cssClass
* @returns {!MdPanelAnimation}
*/
MdPanelAnimation.prototype.withAnimation = function(cssClass) {
this._animationClass = cssClass;
return this;
};
/**
* Animate the panel open.
* @param {!angular.JQLite} panelEl
* @returns {!angular.$q.Promise} A promise that is resolved when the open
* animation is complete.
*/
MdPanelAnimation.prototype.animateOpen = function(panelEl) {
var animator = this._$mdUtil.dom.animator;
this._fixBounds(panelEl);
var animationOptions = {};
// Include the panel transformations when calculating the animations.
var panelTransform = panelEl[0].style.transform || '';
var openFrom = animator.toTransformCss(panelTransform);
var openTo = animator.toTransformCss(panelTransform);
switch (this._animationClass) {
case MdPanelAnimation.animation.SLIDE:
// Slide should start with opacity: 1.
panelEl.css('opacity', '1');
animationOptions = {
transitionInClass: '_md-panel-animate-enter'
};
var openSlide = animator.calculateSlideToOrigin(
panelEl, this._openFrom) || '';
openFrom = animator.toTransformCss(openSlide + ' ' + panelTransform);
break;
case MdPanelAnimation.animation.SCALE:
animationOptions = {
transitionInClass: '_md-panel-animate-enter'
};
var openScale = animator.calculateZoomToOrigin(
panelEl, this._openFrom) || '';
openFrom = animator.toTransformCss(openScale + ' ' + panelTransform);
break;
case MdPanelAnimation.animation.FADE:
animationOptions = {
transitionInClass: '_md-panel-animate-enter'
};
break;
default:
if (angular.isString(this._animationClass)) {
animationOptions = {
transitionInClass: this._animationClass
};
} else {
animationOptions = {
transitionInClass: this._animationClass['open'],
transitionOutClass: this._animationClass['close'],
};
}
}
animationOptions.duration = this._openDuration;
return animator
.translate3d(panelEl, openFrom, openTo, animationOptions);
};
/**
* Animate the panel close.
* @param {!angular.JQLite} panelEl
* @returns {!angular.$q.Promise} A promise that resolves when the close
* animation is complete.
*/
MdPanelAnimation.prototype.animateClose = function(panelEl) {
var animator = this._$mdUtil.dom.animator;
var reverseAnimationOptions = {};
// Include the panel transformations when calculating the animations.
var panelTransform = panelEl[0].style.transform || '';
var closeFrom = animator.toTransformCss(panelTransform);
var closeTo = animator.toTransformCss(panelTransform);
switch (this._animationClass) {
case MdPanelAnimation.animation.SLIDE:
// Slide should start with opacity: 1.
panelEl.css('opacity', '1');
reverseAnimationOptions = {
transitionInClass: '_md-panel-animate-leave'
};
var closeSlide = animator.calculateSlideToOrigin(
panelEl, this._closeTo) || '';
closeTo = animator.toTransformCss(closeSlide + ' ' + panelTransform);
break;
case MdPanelAnimation.animation.SCALE:
reverseAnimationOptions = {
transitionInClass: '_md-panel-animate-scale-out _md-panel-animate-leave'
};
var closeScale = animator.calculateZoomToOrigin(
panelEl, this._closeTo) || '';
closeTo = animator.toTransformCss(closeScale + ' ' + panelTransform);
break;
case MdPanelAnimation.animation.FADE:
reverseAnimationOptions = {
transitionInClass: '_md-panel-animate-fade-out _md-panel-animate-leave'
};
break;
default:
if (angular.isString(this._animationClass)) {
reverseAnimationOptions = {
transitionOutClass: this._animationClass
};
} else {
reverseAnimationOptions = {
transitionInClass: this._animationClass['close'],
transitionOutClass: this._animationClass['open']
};
}
}
reverseAnimationOptions.duration = this._closeDuration;
return animator
.translate3d(panelEl, closeFrom, closeTo, reverseAnimationOptions);
};
/**
* Set the height and width to match the panel if not provided.
* @param {!angular.JQLite} panelEl
* @private
*/
MdPanelAnimation.prototype._fixBounds = function(panelEl) {
var panelWidth = panelEl[0].offsetWidth;
var panelHeight = panelEl[0].offsetHeight;
if (this._openFrom && this._openFrom.bounds.height == null) {
this._openFrom.bounds.height = panelHeight;
}
if (this._openFrom && this._openFrom.bounds.width == null) {
this._openFrom.bounds.width = panelWidth;
}
if (this._closeTo && this._closeTo.bounds.height == null) {
this._closeTo.bounds.height = panelHeight;
}
if (this._closeTo && this._closeTo.bounds.width == null) {
this._closeTo.bounds.width = panelWidth;
}
};
/**
* Identify the bounding RECT for the target element.
* @param {!angular.JQLite} element
* @returns {{element: !angular.JQLite|undefined, bounds: !DOMRect}}
* @private
*/
MdPanelAnimation.prototype._getBoundingClientRect = function(element) {
if (element instanceof angular.element) {
return {
element: element,
bounds: element[0].getBoundingClientRect()
};
}
};
/*****************************************************************************
* Util Methods *
*****************************************************************************/
/**
* Returns the angular element associated with a css selector or element.
* @param el {string|!angular.JQLite|!Element}
* @returns {!angular.JQLite}
*/
function getElement(el) {
var queryResult = angular.isString(el) ?
document.querySelector(el) : el;
return angular.element(queryResult);
}
/**
* Gets the computed values for an element's translateX and translateY in px.
* @param {!angular.JQLite|!Element} el
* @param {string} property
* @return {{x: number, y: number}}
*/
function getComputedTranslations(el, property) {
// The transform being returned by `getComputedStyle` is in the format:
// `matrix(a, b, c, d, translateX, translateY)` if defined and `none`
// if the element doesn't have a transform.
var transform = getComputedStyle(el[0] || el)[property];
var openIndex = transform.indexOf('(');
var closeIndex = transform.lastIndexOf(')');
var output = { x: 0, y: 0 };
if (openIndex > -1 && closeIndex > -1) {
var parsedValues = transform
.substring(openIndex + 1, closeIndex)
.split(', ')
.slice(-2);
output.x = parseInt(parsedValues[0]);
output.y = parseInt(parsedValues[1]);
}
return output;
}
ngmaterial.components.panel = angular.module("material.components.panel");