/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v1.1.3 */ goog.provide('ngmaterial.components.panel'); goog.require('ngmaterial.components.backdrop'); goog.require('ngmaterial.core'); /** * @ngdoc module * @name material.components.panel */ MdPanelService['$inject'] = ["presets", "$rootElement", "$rootScope", "$injector", "$window"]; angular .module('material.components.panel', [ 'material.core', 'material.components.backdrop' ]) .provider('$mdPanel', MdPanelProvider); /***************************************************************************** * PUBLIC DOCUMENTATION * *****************************************************************************/ /** * @ngdoc service * @name $mdPanelProvider * @module material.components.panel * * @description * `$mdPanelProvider` allows users to create configuration presets that will be * stored within a cached presets object. When the configuration is needed, the * user can request the preset by passing it as the first parameter in the * `$mdPanel.create` or `$mdPanel.open` methods. * * @usage * * (function(angular, undefined) { * 'use strict'; * * angular * .module('demoApp', ['ngMaterial']) * .config(DemoConfig) * .controller('DemoCtrl', DemoCtrl) * .controller('DemoMenuCtrl', DemoMenuCtrl); * * function DemoConfig($mdPanelProvider) { * $mdPanelProvider.definePreset('demoPreset', { * attachTo: angular.element(document.body), * controller: DemoMenuCtrl, * controllerAs: 'ctrl', * template: '' + * '', * panelClass: 'menu-panel-container', * focusOnOpen: false, * zIndex: 100, * propagateContainerEvents: true, * groupName: 'menus' * }); * } * * function PanelProviderCtrl($mdPanel) { * this.navigation = { * name: 'navigation', * items: [ * 'Home', * 'About', * 'Contact' * ] * }; * this.favorites = { * name: 'favorites', * items: [ * 'Add to Favorites' * ] * }; * this.more = { * name: 'more', * items: [ * 'Account', * 'Sign Out' * ] * }; * * $mdPanel.newPanelGroup('menus', { * maxOpen: 2 * }); * * this.showMenu = function($event, menu) { * $mdPanel.open('demoPreset', { * id: 'menu_' + menu.name, * position: $mdPanel.newPanelPosition() * .relativeTo($event.srcElement) * .addPanelPosition( * $mdPanel.xPosition.ALIGN_START, * $mdPanel.yPosition.BELOW * ), * locals: { * items: menu.items * }, * openFrom: $event * }); * }; * } * * function PanelMenuCtrl(mdPanelRef) { * this.closeMenu = function() { * mdPanelRef && mdPanelRef.close(); * }; * } * })(angular); * */ /** * @ngdoc method * @name $mdPanelProvider#definePreset * @description * Takes the passed in preset name and preset configuration object and adds it * to the `_presets` object of the provider. This `_presets` object is then * passed along to the `$mdPanel` service. * * @param {string} name Preset name. * @param {!Object} preset Specific configuration object that can contain any * and all of the parameters avaialble within the `$mdPanel.create` method. * However, parameters that pertain to id, position, animation, and user * interaction are not allowed and will be removed from the preset * configuration. */ /***************************************************************************** * MdPanel Service * *****************************************************************************/ /** * @ngdoc service * @name $mdPanel * @module material.components.panel * * @description * `$mdPanel` is a robust, low-level service for creating floating panels on * the screen. It can be used to implement tooltips, dialogs, pop-ups, etc. * * @usage * * (function(angular, undefined) { * 'use strict'; * * angular * .module('demoApp', ['ngMaterial']) * .controller('DemoDialogController', DialogController); * * var panelRef; * * function showPanel($event) { * var panelPosition = $mdPanel.newPanelPosition() * .absolute() * .top('50%') * .left('50%'); * * var panelAnimation = $mdPanel.newPanelAnimation() * .targetEvent($event) * .defaultAnimation('md-panel-animate-fly') * .closeTo('.show-button'); * * var config = { * attachTo: angular.element(document.body), * controller: DialogController, * controllerAs: 'ctrl', * position: panelPosition, * animation: panelAnimation, * targetEvent: $event, * templateUrl: 'dialog-template.html', * clickOutsideToClose: true, * escapeToClose: true, * focusOnOpen: true * } * * $mdPanel.open(config) * .then(function(result) { * panelRef = result; * }); * } * * function DialogController(MdPanelRef) { * function closeDialog() { * if (MdPanelRef) MdPanelRef.close(); * } * } * })(angular); * */ /** * @ngdoc method * @name $mdPanel#create * @description * Creates a panel with the specified options. * * @param config {!Object=} Specific configuration object that may contain the * following properties: * * - `id` - `{string=}`: An ID to track the panel by. When an ID is provided, * the created panel is added to a tracked panels object. Any subsequent * requests made to create a panel with that ID are ignored. This is useful * in having the panel service not open multiple panels from the same user * interaction when there is no backdrop and events are propagated. Defaults * to an arbitrary string that is not tracked. * - `template` - `{string=}`: HTML template to show in the panel. This * **must** be trusted HTML with respect to Angular’s * [$sce service](https://docs.angularjs.org/api/ng/service/$sce). * - `templateUrl` - `{string=}`: The URL that will be used as the content of * the panel. * - `contentElement` - `{(string|!angular.JQLite|!Element)=}`: Pre-compiled * element to be used as the panel's content. * - `controller` - `{(function|string)=}`: The controller to associate with * the panel. The controller can inject a reference to the returned * panelRef, which allows the panel to be closed, hidden, and shown. Any * fields passed in through locals or resolve will be bound to the * controller. * - `controllerAs` - `{string=}`: An alias to assign the controller to on * the scope. * - `bindToController` - `{boolean=}`: Binds locals to the controller * instead of passing them in. Defaults to true, as this is a best * practice. * - `locals` - `{Object=}`: An object containing key/value pairs. The keys * will be used as names of values to inject into the controller. For * example, `locals: {three: 3}` would inject `three` into the controller, * with the value 3. * - `resolve` - `{Object=}`: Similar to locals, except it takes promises as * values. The panel will not open until all of the promises resolve. * - `attachTo` - `{(string|!angular.JQLite|!Element)=}`: The element to * attach the panel to. Defaults to appending to the root element of the * application. * - `propagateContainerEvents` - `{boolean=}`: Whether pointer or touch * events should be allowed to propagate 'go through' the container, aka the * wrapper, of the panel. Defaults to false. * - `panelClass` - `{string=}`: A css class to apply to the panel element. * This class should define any borders, box-shadow, etc. for the panel. * - `zIndex` - `{number=}`: The z-index to place the panel at. * Defaults to 80. * - `position` - `{MdPanelPosition=}`: An MdPanelPosition object that * specifies the alignment of the panel. For more information, see * `MdPanelPosition`. * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click * outside the panel to close it. Defaults to false. * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to * close the panel. Defaults to false. * - `onCloseSuccess` - `{function(!panelRef, string)=}`: Function that is * called after the close successfully finishes. The first parameter passed * into this function is the current panelRef and the 2nd is an optional * string explaining the close reason. The currently supported closeReasons * can be found in the MdPanelRef.closeReasons enum. These are by default * passed along by the panel. * - `trapFocus` - `{boolean=}`: Whether focus should be trapped within the * panel. If `trapFocus` is true, the user will not be able to interact * with the rest of the page until the panel is dismissed. Defaults to * false. * - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on * open. Only disable if focusing some other way, as focus management is * required for panels to be accessible. Defaults to true. * - `fullscreen` - `{boolean=}`: Whether the panel should be full screen. * Applies the class `._md-panel-fullscreen` to the panel on open. Defaults * to false. * - `animation` - `{MdPanelAnimation=}`: An MdPanelAnimation object that * specifies the animation of the panel. For more information, see * `MdPanelAnimation`. * - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop * behind the panel. Defaults to false. * - `disableParentScroll` - `{boolean=}`: Whether the user can scroll the * page behind the panel. Defaults to false. * - `onDomAdded` - `{function=}`: Callback function used to announce when * the panel is added to the DOM. * - `onOpenComplete` - `{function=}`: Callback function used to announce * when the open() action is finished. * - `onRemoving` - `{function=}`: Callback function used to announce the * close/hide() action is starting. * - `onDomRemoved` - `{function=}`: Callback function used to announce when * the panel is removed from the DOM. * - `origin` - `{(string|!angular.JQLite|!Element)=}`: The element to focus * on when the panel closes. This is commonly the element which triggered * the opening of the panel. If you do not use `origin`, you need to control * the focus manually. * - `groupName` - `{(string|!Array)=}`: A group name or an array of * group names. The group name is used for creating a group of panels. The * group is used for configuring the number of open panels and identifying * specific behaviors for groups. For instance, all tooltips could be * identified using the same groupName. * * @returns {!MdPanelRef} panelRef */ /** * @ngdoc method * @name $mdPanel#open * @description * Calls the create method above, then opens the panel. This is a shortcut for * creating and then calling open manually. If custom methods need to be * called when the panel is added to the DOM or opened, do not use this method. * Instead create the panel, chain promises on the domAdded and openComplete * methods, and call open from the returned panelRef. * * @param {!Object=} config Specific configuration object that may contain * the properties defined in `$mdPanel.create`. * @returns {!angular.$q.Promise} panelRef A promise that resolves * to an instance of the panel. */ /** * @ngdoc method * @name $mdPanel#newPanelPosition * @description * Returns a new instance of the MdPanelPosition object. Use this to create * the position config object. * * @returns {!MdPanelPosition} panelPosition */ /** * @ngdoc method * @name $mdPanel#newPanelAnimation * @description * Returns a new instance of the MdPanelAnimation object. Use this to create * the animation config object. * * @returns {!MdPanelAnimation} panelAnimation */ /** * @ngdoc method * @name $mdPanel#newPanelGroup * @description * Creates a panel group and adds it to a tracked list of panel groups. * * @param {string} groupName Name of the group to create. * @param {!Object=} config Specific configuration object that may contain the * following properties: * * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed to * be open within a defined panel group. * * @returns {!Object, * openPanels: !Array, * maxOpen: number}>} panelGroup */ /** * @ngdoc method * @name $mdPanel#setGroupMaxOpen * @description * 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. */ /***************************************************************************** * MdPanelRef * *****************************************************************************/ /** * @ngdoc type * @name MdPanelRef * @module material.components.panel * @description * A reference to a created panel. This reference contains a unique id for the * panel, along with the following properties: * * - `id` - `{string}`: The unique id for the panel. This id is used to track * when a panel was interacted with. * - `config` - `{!Object=}`: The entire config object that was used in * create. * - `isAttached` - `{boolean}`: Whether the panel is attached to the DOM. * Visibility to the user does not factor into isAttached. * - `panelContainer` - `{angular.JQLite}`: The wrapper element containing the * panel. This property is added in order to have access to the `addClass`, * `removeClass`, `toggleClass`, etc methods. * - `panelEl` - `{angular.JQLite}`: The panel element. This property is added * in order to have access to the `addClass`, `removeClass`, `toggleClass`, * etc methods. */ /** * @ngdoc method * @name MdPanelRef#open * @description * Attaches and shows the panel. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * opened. */ /** * @ngdoc method * @name MdPanelRef#close * @description * Hides and detaches the panel. Note that this will **not** destroy the panel. * If you don't intend on using the panel again, call the {@link #destroy * destroy} method afterwards. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * closed. */ /** * @ngdoc method * @name MdPanelRef#attach * @description * Create the panel elements and attach them to the DOM. The panel will be * hidden by default. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * attached. */ /** * @ngdoc method * @name MdPanelRef#detach * @description * Removes the panel from the DOM. This will NOT hide the panel before removing * it. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel is * detached. */ /** * @ngdoc method * @name MdPanelRef#show * @description * Shows the panel. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel has * shown and animations are completed. */ /** * @ngdoc method * @name MdPanelRef#hide * @description * Hides the panel. * * @returns {!angular.$q.Promise} A promise that is resolved when the panel has * hidden and animations are completed. */ /** * @ngdoc method * @name MdPanelRef#destroy * @description * Destroys the panel. The panel cannot be opened again after this is called. */ /** * @ngdoc method * @name MdPanelRef#addClass * @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. * @description * Adds a class to the panel. DO NOT use this hide/show the panel. * * @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. */ /** * @ngdoc method * @name MdPanelRef#removeClass * @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. * @description * Removes a class from the panel. DO NOT use this to hide/show the panel. * * @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. */ /** * @ngdoc method * @name MdPanelRef#toggleClass * @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. * @description * Toggles a class on the panel. DO NOT use this to hide/show the panel. * * @param {string} toggleClass Class to be toggled. * @param {boolean} onElement Whether or not to remove the class from the panel * element instead of the container. */ /** * @ngdoc method * @name MdPanelRef#updatePosition * @description * Updates the position configuration of a panel. Use this to update the * position of a panel that is open, without having to close and re-open the * panel. * * @param {!MdPanelPosition} position */ /** * @ngdoc method * @name MdPanelRef#addToGroup * @description * 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 to add the panel to. */ /** * @ngdoc method * @name MdPanelRef#removeFromGroup * @description * 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. */ /** * @ngdoc method * @name MdPanelRef#registerInterceptor * @description * Registers an 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. The interceptors are called sequentially * and it reverse order. `type` must be one of the following * values available on `$mdPanel.interceptorTypes`: * * `CLOSE` - Gets called before the panel begins closing. * * @param {string} type Type of interceptor. * @param {!angular.$q.Promise} callback Callback to be registered. * @returns {!MdPanelRef} */ /** * @ngdoc method * @name MdPanelRef#removeInterceptor * @description * Removes a registered interceptor. * * @param {string} type Type of interceptor to be removed. * @param {function(): !angular.$q.Promise} callback Interceptor to be removed. * @returns {!MdPanelRef} */ /** * @ngdoc method * @name MdPanelRef#removeAllInterceptors * @description * Removes all interceptors. If a type is supplied, only the * interceptors of that type will be cleared. * * @param {string=} type Type of interceptors to be removed. * @returns {!MdPanelRef} */ /** * @ngdoc method * @name MdPanelRef#updateAnimation * @description * Updates the animation configuration for a panel. You can use this to change * the panel's animation without having to re-create it. * * @param {!MdPanelAnimation} animation */ /***************************************************************************** * MdPanelPosition * *****************************************************************************/ /** * @ngdoc type * @name MdPanelPosition * @module material.components.panel * @description * * Object for configuring the position of the panel. * * @usage * * #### Centering the panel * * * new MdPanelPosition().absolute().center(); * * * #### Overlapping the panel with an element * * * new MdPanelPosition() * .relativeTo(someElement) * .addPanelPosition( * $mdPanel.xPosition.ALIGN_START, * $mdPanel.yPosition.ALIGN_TOPS * ); * * * #### Aligning the panel with the bottom of an element * * * new MdPanelPosition() * .relativeTo(someElement) * .addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW); * */ /** * @ngdoc method * @name MdPanelPosition#absolute * @description * Positions the panel absolutely relative to the parent element. If the parent * is document.body, this is equivalent to positioning the panel absolutely * within the viewport. * * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#relativeTo * @description * Positions the panel relative to a specific element. * * @param {string|!Element|!angular.JQLite} element Query selector, DOM element, * or angular element to position the panel with respect to. * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#top * @description * 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} */ /** * @ngdoc method * @name MdPanelPosition#bottom * @description * 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} */ /** * @ngdoc method * @name MdPanelPosition#start * @description * 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} */ /** * @ngdoc method * @name MdPanelPosition#end * @description * 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} */ /** * @ngdoc method * @name MdPanelPosition#left * @description * 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} */ /** * @ngdoc method * @name MdPanelPosition#right * @description * 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} */ /** * @ngdoc method * @name MdPanelPosition#centerHorizontally * @description * Centers the panel horizontally in the viewport. Clears any previously set * horizontal position. * * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#centerVertically * @description * Centers the panel vertically in the viewport. Clears any previously set * vertical position. * * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#center * @description * 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} */ /** * @ngdoc method * @name MdPanelPosition#addPanelPosition * @description * Sets the x and y position for the panel relative to another element. Can be * called multiple times to specify an ordered list of panel positions. The * first position which allows the panel to be completely on-screen will be * chosen; the last position will be chose whether it is on-screen or not. * * xPosition must be one of the following values available on * $mdPanel.xPosition: * * * CENTER | ALIGN_START | ALIGN_END | OFFSET_START | OFFSET_END * *
 *    *************
 *    *           *
 *    *   PANEL   *
 *    *           *
 *    *************
 *   A B    C    D E
 *
 * A: OFFSET_START (for LTR displays)
 * B: ALIGN_START (for LTR displays)
 * C: CENTER
 * D: ALIGN_END (for LTR displays)
 * E: OFFSET_END (for LTR displays)
 * 
* * yPosition must be one of the following values available on * $mdPanel.yPosition: * * CENTER | ALIGN_TOPS | ALIGN_BOTTOMS | ABOVE | BELOW * *
 *   F
 *   G *************
 *     *           *
 *   H *   PANEL   *
 *     *           *
 *   I *************
 *   J
 *
 * F: BELOW
 * G: ALIGN_TOPS
 * H: CENTER
 * I: ALIGN_BOTTOMS
 * J: ABOVE
 * 
* * @param {string} xPosition * @param {string} yPosition * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#withOffsetX * @description * Sets the value of the offset in the x-direction. * * @param {string} offsetX * @returns {!MdPanelPosition} */ /** * @ngdoc method * @name MdPanelPosition#withOffsetY * @description * Sets the value of the offset in the y-direction. * * @param {string} offsetY * @returns {!MdPanelPosition} */ /***************************************************************************** * MdPanelAnimation * *****************************************************************************/ /** * @ngdoc type * @name MdPanelAnimation * @module material.components.panel * @description * Animation configuration object. To use, create an MdPanelAnimation with the * desired properties, then pass the object as part of $mdPanel creation. * * @usage * * * var panelAnimation = new MdPanelAnimation() * .openFrom(myButtonEl) * .duration(1337) * .closeTo('.my-button') * .withAnimation($mdPanel.animation.SCALE); * * $mdPanel.create({ * animation: panelAnimation * }); * */ /** * @ngdoc method * @name MdPanelAnimation#openFrom * @description * 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}} * @returns {!MdPanelAnimation} */ /** * @ngdoc method * @name MdPanelAnimation#closeTo * @description * 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}} * @returns {!MdPanelAnimation} */ /** * @ngdoc method * @name MdPanelAnimation#withAnimation * @description * Specifies the animation class. * * There are several default animations that can be used: * ($mdPanel.animation) * SLIDE: The panel slides in and out from the specified * elements. It will not fade in or out. * SCALE: The panel scales in and out. Slide and fade are * included in this animation. * FADE: The panel fades in and out. * * Custom classes will by default fade in and out unless * "transition: opacity 1ms" is added to the to custom class. * * @param {string|{open: string, close: string}} cssClass * @returns {!MdPanelAnimation} */ /** * @ngdoc method * @name MdPanelAnimation#duration * @description * Specifies the duration of the animation in milliseconds. The `duration` * method accepts either a number or an object with separate open and close * durations. * * @param {number|{open: number, close: number}} duration * @returns {!MdPanelAnimation} */ /***************************************************************************** * PUBLIC DOCUMENTATION * *****************************************************************************/ var MD_PANEL_Z_INDEX = 80; var MD_PANEL_HIDDEN = '_md-panel-hidden'; var FOCUS_TRAP_TEMPLATE = angular.element( '
'); var _presets = {}; /** * A provider that is used for creating presets for the panel API. * @final @constructor ngInject */ function MdPanelProvider() { return { 'definePreset': definePreset, 'getAllPresets': getAllPresets, 'clearPresets': clearPresets, '$get': $getProvider() }; } /** * Takes the passed in panel configuration object and adds it to the `_presets` * object at the specified name. * @param {string} name Name of the preset to set. * @param {!Object} preset Specific configuration object that can contain any * and all of the parameters avaialble within the `$mdPanel.create` method. * However, parameters that pertain to id, position, animation, and user * interaction are not allowed and will be removed from the preset * configuration. */ function definePreset(name, preset) { if (!name || !preset) { throw new Error('mdPanelProvider: The panel preset definition is ' + 'malformed. The name and preset object are required.'); } else if (_presets.hasOwnProperty(name)) { throw new Error('mdPanelProvider: The panel preset you have requested ' + 'has already been defined.'); } // Delete any property on the preset that is not allowed. delete preset.id; delete preset.position; delete preset.animation; _presets[name] = preset; } /** * Gets a clone of the `_presets`. * @return {!Object} */ function getAllPresets() { return angular.copy(_presets); } /** * Clears all of the stored presets. */ function clearPresets() { _presets = {}; } /** * Represents the `$get` method of the Angular provider. From here, a new * reference to the MdPanelService is returned where the needed arguments are * passed in including the MdPanelProvider `_presets`. * @param {!Object} _presets * @param {!angular.JQLite} $rootElement * @param {!angular.Scope} $rootScope * @param {!angular.$injector} $injector * @param {!angular.$window} $window */ function $getProvider() { return [ '$rootElement', '$rootScope', '$injector', '$window', function($rootElement, $rootScope, $injector, $window) { return new MdPanelService(_presets, $rootElement, $rootScope, $injector, $window); } ]; } /***************************************************************************** * MdPanel Service * *****************************************************************************/ /** * A service that is used for controlling/displaying panels on the screen. * @param {!Object} presets * @param {!angular.JQLite} $rootElement * @param {!angular.Scope} $rootScope * @param {!angular.$injector} $injector * @param {!angular.$window} $window * @final @constructor ngInject */ function MdPanelService(presets, $rootElement, $rootScope, $injector, $window) { /** * Default config options for the panel. * Anything angular related needs to be done later. Therefore * scope: $rootScope.$new(true), * attachTo: $rootElement, * are added later. * @private {!Object} */ this._defaultConfigOptions = { bindToController: true, clickOutsideToClose: false, disableParentScroll: false, escapeToClose: false, focusOnOpen: true, fullscreen: false, hasBackdrop: false, propagateContainerEvents: false, transformTemplate: angular.bind(this, this._wrapTemplate), trapFocus: false, zIndex: MD_PANEL_Z_INDEX }; /** @private {!Object} */ this._config = {}; /** @private {!Object} */ this._presets = presets; /** @private @const */ this._$rootElement = $rootElement; /** @private @const */ this._$rootScope = $rootScope; /** @private @const */ this._$injector = $injector; /** @private @const */ this._$window = $window; /** @private @const */ this._$mdUtil = this._$injector.get('$mdUtil'); /** @private {!Object} */ this._trackedPanels = {}; /** * @private {!Object, * openPanels: !Array, * maxOpen: number}>} */ this._groups = Object.create(null); /** * Default animations that can be used within the panel. * @type {enum} */ this.animation = MdPanelAnimation.animation; /** * Possible values of xPosition for positioning the panel relative to * another element. * @type {enum} */ this.xPosition = MdPanelPosition.xPosition; /** * Possible values of yPosition for positioning the panel relative to * another element. * @type {enum} */ this.yPosition = MdPanelPosition.yPosition; /** * Possible values for the interceptors that can be registered on a panel. * @type {enum} */ this.interceptorTypes = MdPanelRef.interceptorTypes; /** * Possible values for closing of a panel. * @type {enum} */ this.closeReasons = MdPanelRef.closeReasons; /** * Possible values of absolute position. * @type {enum} */ this.absPosition = MdPanelPosition.absPosition; } /** * Creates a panel with the specified options. * @param {string=} preset Name of a preset configuration that can be used to * extend the panel configuration. * @param {!Object=} config Configuration object for the panel. * @returns {!MdPanelRef} */ MdPanelService.prototype.create = function(preset, config) { if (typeof preset === 'string') { preset = this._getPresetByName(preset); } else if (typeof preset === 'object' && (angular.isUndefined(config) || !config)) { config = preset; preset = {}; } preset = preset || {}; config = config || {}; // If the passed-in config contains an ID and the ID is within _trackedPanels, // return the tracked panel after updating its config with the passed-in // config. if (angular.isDefined(config.id) && this._trackedPanels[config.id]) { var trackedPanel = this._trackedPanels[config.id]; angular.extend(trackedPanel.config, config); return trackedPanel; } // Combine the passed-in config, the _defaultConfigOptions, and the preset // configuration into the `_config`. this._config = angular.extend({ // If no ID is set within the passed-in config, then create an arbitrary ID. id: config.id || 'panel_' + this._$mdUtil.nextUid(), scope: this._$rootScope.$new(true), attachTo: this._$rootElement }, this._defaultConfigOptions, config, preset); // Create the panelRef and add it to the `_trackedPanels` object. var panelRef = new MdPanelRef(this._config, this._$injector); this._trackedPanels[config.id] = panelRef; // Add the panel to each of its requested groups. if (this._config.groupName) { if (angular.isString(this._config.groupName)) { this._config.groupName = [this._config.groupName]; } angular.forEach(this._config.groupName, function(group) { panelRef.addToGroup(group); }); } this._config.scope.$on('$destroy', angular.bind(panelRef, panelRef.detach)); return panelRef; }; /** * Creates and opens a panel with the specified options. * @param {string=} preset Name of a preset configuration that can be used to * extend the panel configuration. * @param {!Object=} config Configuration object for the panel. * @returns {!angular.$q.Promise} The panel created from create. */ MdPanelService.prototype.open = function(preset, config) { var panelRef = this.create(preset, config); return panelRef.open().then(function() { return panelRef; }); }; /** * Gets a specific preset configuration object saved within `_presets`. * @param {string} preset Name of the preset to search for. * @returns {!Object} The preset configuration object. */ MdPanelService.prototype._getPresetByName = function(preset) { if (!this._presets[preset]) { throw new Error('mdPanel: The panel preset configuration that you ' + 'requested does not exist. Use the $mdPanelProvider to create a ' + 'preset before requesting one.'); } return this._presets[preset]; }; /** * Returns a new instance of the MdPanelPosition. Use this to create the * positioning object. * @returns {!MdPanelPosition} */ MdPanelService.prototype.newPanelPosition = function() { return new MdPanelPosition(this._$injector); }; /** * Returns a new instance of the MdPanelAnimation. Use this to create the * animation object. * @returns {!MdPanelAnimation} */ MdPanelService.prototype.newPanelAnimation = function() { return new MdPanelAnimation(this._$injector); }; /** * Creates a panel group and adds it to a tracked list of panel groups. * @param groupName {string} Name of the group to create. * @param config {!Object=} Specific configuration object that may contain the * following properties: * * - `maxOpen` - `{number=}`: The maximum number of panels that are allowed * open within a defined panel group. * * @returns {!Object, * 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");