| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| import angular from 'angular'; |
| import angularAnimate from 'angular-animate'; |
| import uibModal from 'angular-ui-bootstrap/src/modal/index-nocss'; |
| import template from './bottom-sheet.template.html'; |
| |
| const MODULE_NAME = 'brooklyn.components.bottom-sheet'; |
| |
| const CLASS_ANIMATION_FADE = 'fade'; |
| const CLASS_ANIMATION_SLIDE_UP = 'slide-up'; |
| const CLASS_BACKDROP = 'bottom-sheet-backdrop'; |
| const CLASS_CONTAINER = 'bottom-sheet-container'; |
| const CLASS_MODE_MODAL = 'bottom-sheet-modal'; |
| const CLASS_MODE_INSET = 'bottom-sheet-inset'; |
| const CLASS_OPENED = 'bottom-sheet-open'; |
| |
| const TEMPLATE_CONTAINER_URL = 'br/template/bottom-sheet/window.html'; |
| |
| export const MODES = ['modal', 'inset']; |
| |
| /** |
| * @ngdoc module |
| * @name br.bottom-sheet |
| * @requires ngAnimate |
| * @requires ui.bootstrap.module.modal |
| * |
| * @description |
| * [Bottom sheet UI pattern](https://material.io/guidelines/components/bottom-sheets.html#) implementation for Brooklyn |
| */ |
| angular.module(MODULE_NAME, [angularAnimate, uibModal]) |
| .directive('brBottomSheetBackdrop', ['$animate', brBottomSheetDackdropDirective]) |
| .directive('brBottomSheetContainer', ['$animate', brBottomSheetContainerDirective]) |
| .provider('brBottomSheet', brBottomSheetProvider) |
| .run(['$templateCache', brBottomSheetRun]); |
| |
| export default MODULE_NAME; |
| |
| /** |
| * @ngdoc directive |
| * @name brBottomSheetBackdrop |
| * @module br.bottom-sheet |
| * @restrict A |
| * |
| * @description |
| * A helper directive for the `brBottomSheet` service. It creates a backdrop element. |
| * |
| * @param {string=} animationClass The animation class to use |
| * @param {boolean=} animation Whether or not animating the directive |
| */ |
| export function brBottomSheetDackdropDirective($animate) { |
| return { |
| restrict: 'A', |
| link: (scope, element, attrs)=> { |
| element.addClass(CLASS_BACKDROP); |
| if (attrs.animationClass) { |
| if (attrs.animation) { |
| $animate.addClass(element, attrs.animationClass) |
| } else { |
| element.addClass(attrs.animationClass); |
| } |
| } |
| } |
| }; |
| } |
| |
| /** |
| * @ngdoc directive |
| * @name brBottomSheetContainer |
| * @module br.bottom-sheet |
| * @restrict A |
| * |
| * @description |
| * A helper directive for the `brBottomSheet` service. It creates a container element. |
| * |
| * @param {string=} templateUrl The template URL to use. Default to `br/template/bottom-sheet/window.html` |
| * @param {string=} animationClass The animation class to use |
| * @param {boolean=} animation Whether or not animating the directive |
| */ |
| export function brBottomSheetContainerDirective($animate) { |
| return { |
| restrict: 'A', |
| transclude: true, |
| templateUrl: function(tElement, tAttrs) { |
| return tAttrs.templateUrl || TEMPLATE_CONTAINER_URL; |
| }, |
| link: (scope, element, attrs)=> { |
| element.addClass(CLASS_CONTAINER); |
| if (attrs.animationClass) { |
| if (attrs.animation) { |
| $animate.addClass(element, attrs.animationClass) |
| } else { |
| element.addClass(attrs.animationClass); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @ngdoc provider |
| * @name brBottomSheetProvider |
| * @module br.bottom-sheet |
| * |
| * @description |
| * This provider allows you set up global options for the `brBottomSheet` service. It exposes only one method |
| * `brBottomSheetProvider.setOption(key, value)`. |
| */ |
| export function brBottomSheetProvider() { |
| let options = { |
| animation: true, |
| keyboard: true, |
| mode: MODES[0] // Can be either 'modal' or 'inset' |
| }; |
| |
| return { |
| /** |
| * @ngdoc method |
| * @name setOption |
| * @methodOf brBottomSheetProvider |
| * |
| * @description |
| * Globally set an option for the `brBottomSheet` service. If an option `key` already exists, it will be overridden. |
| * |
| * @param {string} key The option key to set |
| * @param {string} value The option value to set |
| */ |
| setOption: (key, value)=> { |
| if (key) { |
| options[key] = value; |
| } |
| }, |
| $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$animate', '$compile', ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $animate, $compile, $log)=> { |
| return new BrBottomSheet($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $animate, $compile, $log, options); |
| }] |
| } |
| } |
| |
| /** |
| * @ngdoc service |
| * @name brBottomSheet |
| * @module br.bottom-sheet |
| * |
| * @description |
| * This service provides an easy way of displaying a bottom sheet within a page. As per as the |
| * [spec](https://material.io/guidelines/components/bottom-sheets.html#), you can have only one bottom sheet at the time |
| * therefore if you try to open a bottom sheet but one is already displayed, the previous one will close automatically |
| * before the new one appears. |
| * |
| * The service exposes only one method `brBottomSheet.open(options)` that take an object parameters and return the bottom sheet instance. |
| * |
| * The scope associated with modal's content is augmented with: |
| * - `$close(reason)` (Type: `function`) - A method that can be used to close a bottom sheet, passing a reason. |
| * - `$dismiss(reason)` (Type: `function`) - A method that can be used to dismiss a bottom sheet, passing a reason. |
| * - `$updateMode(mode)` (Type: `function`) - A method that can be used to update the mode of the current bottom sheet. |
| * |
| * Those methods make it easy to close a bottom sheet instance without a need to create a dedicated controller. |
| * Also, when using `bindToController`, you can define an `$onInit` method in the controller that will fire upon initialization. |
| */ |
| function BrBottomSheet($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $animate, $compile, $log, defaultOptions) { |
| // Our bottom sheet instance |
| let bottomSheet; |
| |
| // We bind to the keydown event listen for the esc key press |
| $document.on('keydown', keydownListener); |
| // And we unbind ourselve when the rootScope is destroyed |
| $rootScope.$on('$destroy', function() { |
| $document.off('keydown', keydownListener); |
| }); |
| |
| return { |
| /** |
| * @ngdoc method |
| * @name open |
| * @methodOf $brBottomSheet |
| * |
| * @description |
| * Open a new bottom sheet based on the given options. If a bottom sheet is already open, it will be closed automatically |
| * first, then open the new one. |
| * |
| * @param {object} options Options to configure the bottom sheet instance. Supported options are as follow: |
| * - `animation` (Type `boolean`, Default: `true`) - Whether or not enable the animation when opening/dismissing the bottom sheet. |
| * - `keyboard` (Type `boolean`, Default: `true`) - Whether or not binding the escape key to close the bottom sheet. |
| * - `mode` (Type `string`, Default: `modal`) - Set the mode of the bottom sheet. Can be `modal` or `inset`. |
| * If `modal`, the bottom sheet will then take the full focus of the window with a backdrop behind. `inset` will |
| * display the bottom sheet on top of the current content but will allow a user to interact with the application behind. |
| * - `backdropClass` (Type `string`) - Custom CSS class to add to the backdrop DOM element. |
| * - `backdropAnimationClass` (Type `string`, Default: `fade`) - Custom CSS animation class to add to the backdrop |
| * DOM element. Setting this class will override the default animation. |
| * - `containerClass` (Type `string`) - Custom CSS class to add to the bottom sheet container DOM element. |
| * - `containerAnimationClass` (Type `string`, Default: `fade slide-up`) - Custom CSS animation class to add to the |
| * bottom sheet containet DOM element. Setting this class will override the default animation. |
| * - `containerTemplateUrl` (Type `string`, Default: `br/template/bottom-sheet/window.html`) - Custom template to use |
| * for the bottom sheet container. This expect a URL so the template can either be added as a standalone HTML or |
| * added via `$templateCache` service |
| * - `openedClass` (Type `string`, Default: `bottom-sheet-open`) - Custom CSS class to add to the `appendTo` DOM element |
| * when the bottom sheet is opened. |
| * - `appendTo` (Type: `angular.element,` Default: `body`) - DOM element to append the bottom sheet to. |
| * - `bindToController` (Type: `boolean`, Default: `false`) - When used with `controllerAs` & set to `true`, it will |
| * bind the `$scope` properties onto the controller. |
| * - `template` (Type: `string`) - Inline template representing the bottom sheet's content. |
| * - `templateUrl` (Type: `string`) - A path to a template representing bottom sheet's content. You need either a `template` or `templateUrl`. |
| * - `resolve` (Type: `Object`) - Members that will be resolved and passed to the controller as locals; |
| * it is equivalent of the `resolve` property in the router. |
| * - `scope` (Type: `$scope`) - The parent scope instance to be used for the bottom sheet's content. Defaults to `$rootScope`. |
| * |
| * @returns {object} The bottom sheet instance containing the following properties: |
| * - `close(result)` (Type: `function`) - Can be used to close a modal, passing a result. |
| * - `dismiss(reason)` (Type: `function`) - Can be used to dismiss a modal, passing a reason. |
| * - `updateMode(mode)` (Type: `function`) - Can be used to change the current bottom sheet `mode`. |
| * - `result` (Type: `promise`) - Is resolved when a modal is closed and rejected when a modal is dismissed. |
| * - `opened` (Type: `promise`) - Is resolved when a modal gets opened after downloading content's template and resolving all variables. |
| * - `closed` (Type: `promise`) - Is resolved when a modal is closed and the animation completes. |
| * - `rendered` (Type: `promise`) - Is resolved when a modal is rendered. |
| */ |
| open: (options)=> { |
| if (bottomSheet) { |
| // If there is a bottom sheet already, we trigger a dismiss (unless it already has been marked as destroyed) |
| // then we wait until it has been fully removed to launch the new instance. |
| if (!bottomSheet.scope.$$brDestructionScheduled) { |
| close('New bottom sheet on the queue', true); |
| } |
| bottomSheet.closedDeferred.promise.then(()=> { |
| open(options); |
| }); |
| } else { |
| open(options); |
| } |
| } |
| }; |
| |
| function open(options) { |
| options = angular.extend({}, defaultOptions, options); |
| options.resolve = options.resolve || {}; |
| options.appendTo = options.appendTo || $document.find('body').eq(0); |
| |
| // Perform some validations on options |
| if (MODES.indexOf(options.mode) === -1) { |
| throw new Error('"mode" not supported. Make sure that the mode is one of those: ' + MODES); |
| } |
| if (!options.appendTo.length) { |
| throw new Error('"appendTo" element not found. Make sure that the element passed is in DOM.'); |
| } |
| if (!options.template && !options.templateUrl) { |
| throw new Error('One of "template" or "templateUrl" options is required.'); |
| } |
| |
| // Create promises |
| let bottomSheetResultDeferred = $q.defer(); |
| let bottomSheetOpenedDeferred = $q.defer(); |
| let bottomSheetClosedDeferred = $q.defer(); |
| let bottomSheetRenderDeferred = $q.defer(); |
| let promises = $q.all([ |
| getTemplatePromise(options), |
| $uibResolve.resolve(options.resolve, {}, null, null) |
| ]); |
| |
| // Create bottom sheet instance |
| let bottomSheetInstance = { |
| result: bottomSheetResultDeferred.promise, |
| opened: bottomSheetOpenedDeferred.promise, |
| closed: bottomSheetClosedDeferred.promise, |
| rendered: bottomSheetRenderDeferred.promise, |
| close: (reason)=> { |
| close(reason, true); |
| }, |
| dismiss: (reason)=> { |
| close(reason, false); |
| }, |
| updateMode: (mode)=> { |
| updateMode(mode); |
| } |
| }; |
| |
| // Let's create our bottom sheet instance |
| promises.then((tplAndVars)=> { |
| let providedScope = options.scope || $rootScope; |
| |
| let bottomSheetScope = providedScope.$new(); |
| bottomSheetScope.$close = bottomSheetInstance.close; |
| bottomSheetScope.$dismiss = bottomSheetInstance.dismiss; |
| bottomSheetScope.$updateMode = bottomSheetInstance.updateMode; |
| |
| bottomSheetScope.$on('$destroy', function() { |
| if (!bottomSheetScope.$$brDestructionScheduled) { |
| bottomSheetScope.$dismiss('$brUnscheduledDestruction'); |
| } |
| }); |
| |
| bottomSheet = { |
| scope: bottomSheetScope, |
| deferred: bottomSheetResultDeferred, |
| renderDeferred: bottomSheetRenderDeferred, |
| closedDeferred: bottomSheetClosedDeferred, |
| animation: options.animation, |
| keyboard: options.keyboard, |
| mode: options.mode, |
| backdropClass: options.backdropClass, |
| backdropAnimationClass: options.backdropAnimationClass, |
| containerClass: options.containerClass, |
| containerAnimationClass: options.containerAnimationClass, |
| containerTemplateUrl: options.containerTemplateUrl, |
| openedClass: options.openedClass, |
| appendTo: options.appendTo, |
| content: tplAndVars[0], |
| ariaLabelledBy: options.ariaLabelledBy, |
| ariaDescribedBy: options.ariaDescribedBy, |
| }; |
| |
| // We create our own instance of controller, based on the given options |
| let ctrlInstance, ctrlInstantiate, ctrlLocals = {}; |
| |
| ctrlLocals.$scope = bottomSheetScope; |
| ctrlLocals.$scope.$resolve = {}; |
| ctrlLocals.brBottomSheetInstance = bottomSheetInstance; |
| |
| // If we passed a resolve block, all vars are injected into the local controller scope |
| let resolves = tplAndVars[1]; |
| angular.forEach(resolves, function(value, key) { |
| ctrlLocals[key] = value; |
| ctrlLocals.$scope.$resolve[key] = value; |
| }); |
| |
| // the third param will make the controller instantiate later,private api |
| // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126 |
| ctrlInstantiate = $controller(options.controller, ctrlLocals, true, options.controllerAs); |
| if (options.controllerAs && options.bindToController) { |
| ctrlInstance = ctrlInstantiate.instance; |
| ctrlInstance.$close = bottomSheetScope.$close; |
| ctrlInstance.$dismiss = bottomSheetScope.$dismiss; |
| angular.extend(ctrlInstance, { |
| $resolve: ctrlLocals.$scope.$resolve |
| }, providedScope); |
| } |
| |
| ctrlInstance = ctrlInstantiate(); |
| |
| if (angular.isFunction(ctrlInstance.$onInit)) { |
| ctrlInstance.$onInit(); |
| } |
| |
| // Create the backdrop if the mode is set to 'modal' |
| if (options.mode === MODES[0]) { |
| createBackdrop(); |
| } |
| |
| bottomSheet.containerElm = angular.element('<div br-bottom-sheet-container></div>'); |
| bottomSheet.containerElm.attr({ |
| 'class': bottomSheet.containerClass, |
| 'animation-class': bottomSheet.containerAnimationClass || CLASS_ANIMATION_SLIDE_UP + ' ' + CLASS_ANIMATION_FADE, |
| 'role': 'dialog', |
| 'tabindex': -1, |
| }).append(bottomSheet.content); |
| if (bottomSheet.containerTemplateUrl) { |
| bottomSheet.containerElm.attr('template-url', bottomSheet.containerTemplateUrl); |
| } |
| if (bottomSheet.animation) { |
| bottomSheet.containerElm.attr('animation', 'true'); |
| } |
| |
| let bodyClass = bottomSheet.openedClass || CLASS_OPENED; |
| bottomSheet.appendTo.addClass(bodyClass); |
| if (bottomSheet.mode === MODES[0]) { |
| bottomSheet.appendTo.addClass(CLASS_MODE_MODAL); |
| } |
| if (bottomSheet.mode === MODES[1]) { |
| bottomSheet.appendTo.addClass(CLASS_MODE_INSET); |
| } |
| $animate.enter($compile(bottomSheet.containerElm)(bottomSheet.scope), bottomSheet.appendTo); |
| |
| // Focus on the newly created bottom sheet |
| bottomSheet.containerElm[0].focus(); |
| |
| bottomSheetOpenedDeferred.resolve(true); |
| }).catch((reason)=> { |
| bottomSheetOpenedDeferred.reject(reason); |
| bottomSheetResultDeferred.reject(reason); |
| }); |
| |
| return bottomSheetInstance; |
| } |
| |
| function close(reason, dismiss = false) { |
| bottomSheet.scope.$$brDestructionScheduled = true; |
| |
| // Removing the backdrop, if exists |
| if (bottomSheet.mode === 'modal' && bottomSheet.backdropElm && bottomSheet.backdropScope) { |
| removeAfterAnimate(bottomSheet.backdropElm, bottomSheet.backdropScope, ()=> { |
| bottomSheet.backdropElm = undefined; |
| bottomSheet.backdropScope = undefined; |
| }); |
| } |
| // Removing the bottom sheet |
| removeAfterAnimate(bottomSheet.containerElm, bottomSheet.scope, ()=> { |
| let bodyClass = bottomSheet.openedClass || CLASS_OPENED; |
| bottomSheet.appendTo.removeClass(bodyClass, CLASS_MODE_MODAL, CLASS_MODE_INSET); |
| bottomSheet = undefined; |
| }, bottomSheet.closedDeferred); |
| |
| if (dismiss) { |
| bottomSheet.deferred.reject(reason); |
| } else { |
| bottomSheet.deferred.resolve(reason); |
| } |
| |
| // Move focus on the appendTo element |
| bottomSheet.appendTo[0].focus(); |
| } |
| |
| function updateMode(mode) { |
| if (MODES.indexOf(mode) === -1) { |
| $log.error('Mode ' + mode + ' is not supported. You can choose from the following list: ' + MODES); |
| return; |
| } |
| |
| if (!bottomSheet || bottomSheet.mode === mode) { |
| return; |
| } |
| |
| bottomSheet.appendTo.removeClass(CLASS_MODE_MODAL, CLASS_MODE_INSET); |
| |
| switch (mode) { |
| case MODES[0]: |
| createBackdrop().then(()=> { |
| bottomSheet.mode = mode; |
| bottomSheet.appendTo.addClass(CLASS_MODE_MODAL); |
| }); |
| break; |
| case MODES[1]: |
| removeAfterAnimate(bottomSheet.backdropElm, bottomSheet.backdropScope, ()=> { |
| bottomSheet.mode = mode; |
| bottomSheet.appendTo.addClass(CLASS_MODE_INSET); |
| }); |
| break; |
| } |
| } |
| |
| function keydownListener(event) { |
| if (event.isDefaultPrevented()) { |
| return event; |
| } |
| |
| if (bottomSheet && bottomSheet.keyboard) { |
| switch (event.which) { |
| case 27: { |
| if (bottomSheet.keyboard) { |
| event.preventDefault(); |
| $rootScope.$apply(()=> { |
| close('Escape key pressed', true); |
| }); |
| } |
| break; |
| } |
| } |
| } |
| } |
| |
| function createBackdrop(done) { |
| if (!bottomSheet || bottomSheet.mode === 'inset') { |
| return; |
| } |
| |
| bottomSheet.backdropScope = $rootScope.$new(true); |
| bottomSheet.backdropElm = angular.element('<div br-bottom-sheet-backdrop></div>'); |
| bottomSheet.backdropElm.attr({ |
| 'class': bottomSheet.backdropClass, |
| 'animation-class' : bottomSheet.backdropAnimationClass || CLASS_ANIMATION_FADE |
| }); |
| if (bottomSheet.animation) { |
| bottomSheet.backdropElm.attr('animation', 'true'); |
| } |
| $compile(bottomSheet.backdropElm)(bottomSheet.backdropScope); |
| $animate.enter(bottomSheet.backdropElm, bottomSheet.appendTo).then(()=> { |
| if (done) { |
| done(); |
| } |
| }) |
| } |
| |
| function removeAfterAnimate(domEl, scope, done, closedDeferred) { |
| $animate.leave(domEl).then(function() { |
| if (done) { |
| done(); |
| } |
| |
| domEl.remove(); |
| if (closedDeferred) { |
| closedDeferred.resolve(); |
| } |
| }); |
| |
| scope.$destroy(); |
| } |
| |
| function getTemplatePromise(options) { |
| return options.template ? $q.when(options.template) : |
| $templateRequest(angular.isFunction(options.templateUrl) ? |
| options.templateUrl() : options.templateUrl); |
| } |
| } |
| |
| export function brBottomSheetRun($templateCache) { |
| $templateCache.put(TEMPLATE_CONTAINER_URL, template); |
| } |