diff --git a/config/build.config.js b/config/build.config.js index 800d8a48ec..6db81e153f 100644 --- a/config/build.config.js +++ b/config/build.config.js @@ -51,7 +51,6 @@ module.exports = { 'js/views/loadingView.js', 'js/views/modalView.js', 'js/views/navBarView.js', - 'js/views/popupView.js', 'js/views/sideMenuView.js', 'js/views/sliderView.js', 'js/views/tabBarView.js', diff --git a/js/ext/angular/src/directive/ionicPopup.js b/js/ext/angular/src/directive/ionicPopup.js new file mode 100644 index 0000000000..b1902e6ecb --- /dev/null +++ b/js/ext/angular/src/directive/ionicPopup.js @@ -0,0 +1,63 @@ +(function() { +'use strict'; + +angular.module('ionic.ui.popup', []) + +/** + * @private + */ +.directive('ionPopupBackdrop', function() { + return { + restrict: 'E', + replace: true, + template: '' + } +}) + +/** + * @private + */ +.directive('ionPopup', ['$ionicBind', function($ionicBind) { + return { + restrict: 'E', + replace: true, + transclude: true, + scope: true, + link: function($scope, $element, $attr) { + $ionicBind($scope, $attr, { + title: '@', + buttons: '=', + $onButtonTap: '&onButtonTap', + $onClose: '&onClose' + }); + + $scope._buttonTapped = function(button, event) { + var result = button.onTap && button.onTap(event); + + // A way to return false + if(event.defaultPrevented) { + return $scope.$onClose({button: button, result: false, event: event }); + } + + // Truthy test to see if we should close the window + if(result) { + return $scope.$onClose({button: button, result: result, event: event }); + } + $scope.$onButtonTap({button: button, event: event}); + } + }, + template: '' + }; +}]); + +})(); diff --git a/js/ext/angular/src/ionicAngular.js b/js/ext/angular/src/ionicAngular.js index 78ccc4a46a..556d48f9f1 100644 --- a/js/ext/angular/src/ionicAngular.js +++ b/js/ext/angular/src/ionicAngular.js @@ -42,7 +42,8 @@ angular.module('ionic.ui', [ 'ionic.ui.checkbox', 'ionic.ui.toggle', 'ionic.ui.radio', - 'ionic.ui.touch' + 'ionic.ui.touch', + 'ionic.ui.popup' ]); diff --git a/js/ext/angular/src/service/ionicPopup.js b/js/ext/angular/src/service/ionicPopup.js index 1139f77319..6827d4ffce 100644 --- a/js/ext/angular/src/service/ionicPopup.js +++ b/js/ext/angular/src/service/ionicPopup.js @@ -1,57 +1,529 @@ +(function(ionic) { +'use strict'; + angular.module('ionic.service.popup', ['ionic.service.templateLoad']) +/** + * @ngdoc service + * @name $ionicPopup + * @module ionic + * @restrict E + * @codepen jsHjf + * @description + * + * The Ionic Popup service makes it easy to programatically create and show popup + * windows that require the user to respond in order to continue: + * + * ![Popup](http://ionicframework.com.s3.amazonaws.com/docs/controllers/popup.png) + * + * The popup system has support for nicer versions of the built in `alert()` `prompt()` and `confirm()` functions + * you are used to in the browser, but with more powerful support for customizing input types in the case of + * prompt, or customizing the look of the window. + * + * But the true power of the Popup is when a built-in popup just won't cut it. Luckily, the popup window + * has full support for arbitrary popup content, and a simple promise-based system for returning data + * entered by the user. + * + * @usage + * To trigger a Popup in your code, use the $ionicPopup service in your angular controllers: + * + * ```js + * angular.module('mySuperApp', ['ionic']) + * .controller(function($scope, $ionicPopup) { + * + * // Triggered on a button click, or some other target + $scope.showPopup = function() { + $scope.data = {} -.factory('$ionicPopup', ['$rootScope', '$document', '$compile', 'TemplateLoader', function($rootScope, $document, $compile, TemplateLoader) { + // An elaborate, custom popup + $ionicPopup.show({ + templateUrl: 'popup-template.html', + title: 'Enter Wi-Fi Password', + subTitle: 'Please use normal things', + scope: $scope, + buttons: [ + { text: 'Cancel', onTap: function(e) { return true; } }, + { + text: 'Save', + type: 'button-positive', + onTap: function(e) { + return $scope.data.wifi; + } + }, + ] + }).then(function(res) { + console.log('Tapped!', res); + }, function(err) { + console.log('Err:', err); + }, function(msg) { + console.log('message:', msg); + }); - var getPopup = function() { - // Make sure there is only one loading element on the page at one point in time - var existing = angular.element($document[0].querySelector('.popup')); - if(existing.length) { - var scope = existing.scope(); - if(scope.popup) { - return scope; - } - } - }; - - return { - alert: function(message, $scope) { - - // If there is an existing popup, just show that one - var existing = getPopup(); - if(existing) { - return existing.popup.alert(message); - } - - var defaults = { - title: message, - animation: 'fade-in', + // A confirm dialog + $scope.showConfirm = function() { + $ionicPopup.confirm({ + title: 'Consume Ice Cream', + content: 'Are you sure you want to eat this ice cream?' + }).then(function(res) { + if(res) { + console.log('You are sure'); + } else { + console.log('You are not sure'); + } + }); }; - opts = angular.extend(defaults, opts); + // A prompt dialog + $scope.showPrompt = function() { + $ionicPopup.prompt({ + title: 'ID Check', + content: 'What is your name?' + }).then(function(res) { + console.log('Your name is', res); + }); + }; - var scope = $scope && $scope.$new() || $rootScope.$new(true); - angular.extend(scope, opts); + // A prompt with password input dialog + $scope.showPasswordPrompt = function() { + $ionicPopup.prompt({ + title: 'Password Check', + content: 'Enter your secret password', + inputType: 'password', + inputPlaceholder: 'Your password' + }).then(function(res) { + console.log('Your password is', res); + }); + }; - // Compile the template - var element = $compile('' + opts.content + '')(scope); - $document[0].body.appendChild(element[0]); + // An alert dialog + $scope.showAlert = function() { + $ionicPopup.alert({ + title: 'Don\'t eat that!', + content: 'It might taste good' + }).then(function(res) { + console.log('Thank you for not eating my delicious ice cream cone'); + }); + }; + }; + }); + ``` - var popup = new ionic.views.Popup({el: element[0] }); - popup.alert(message); + + */ +.factory('$ionicPopup', ['$rootScope', '$q', '$document', '$compile', '$timeout', '$ionicTemplateLoader', + function($rootScope, $q, $document, $compile, $timeout, $ionicTemplateLoader) { - scope.popup = popup; + // TODO: Make this configurable + var popupOptions = { + // How long to wait after a popup is already shown to show another one + stackPushDelay: 50 + } - return popup; - }, - confirm: function(cb) { - }, - prompt: function(cb) { - }, - show: function(data) { - // data.title - // data.template - // data.buttons + // Center the given popup + var positionPopup = function(popup) { + popup.el.style.marginLeft = (-popup.el.offsetWidth) / 2 + 'px'; + popup.el.style.marginTop = (-popup.el.offsetHeight) / 2 + 'px'; + }; + + // Hide the body of the given popup if it's empty + var hideBody = function(popup) { + var bodyEl = popup.el.querySelector('.popup-body'); + if(bodyEl && bodyEl.innerHTML.trim() == '') { + bodyEl.style.display = 'none'; } }; + + // Show a single popup + var showSinglePopup = function(popup, opts) { + var _this = this; + + ionic.requestAnimationFrame(function() { + hideBody(popup); + positionPopup(popup); + popup.el.classList.remove('popup-hidden'); + popup.el.classList.add('popup-showing'); + popup.el.classList.add('active'); + }); + }; + + // Show a popup that was already shown at one point in the past + var reshowSinglePopup = function(popup) { + ionic.requestAnimationFrame(function() { + popup.el.classList.remove('popup-hidden'); + popup.el.classList.add('popup-showing'); + popup.el.classList.add('active'); + }); + }; + + // Hide a single popup + var hideSinglePopup = function(popup) { + ionic.requestAnimationFrame(function() { + popup.el.classList.remove('active'); + popup.el.classList.add('popup-hidden'); + }); + }; + + // Remove a popup once and for all + var removeSinglePopup = function(popup) { + // Force a reflow so the animation will actually run + popup.el.offsetWidth; + + popup.el.classList.remove('active'); + popup.el.classList.add('popup-hidden'); + + $timeout(function() { + popup.el.remove(); + }, 400); + }; + + + /** + * Popup stack and directive + */ + + var popupStack = []; + var backdropEl = null; + + // Show the backdrop element + var showBackdrop = function() { + var el = $compile('')($rootScope.$new(true)); + $document[0].body.appendChild(el[0]); + backdropEl = el; + }; + + // Remove the backdrop element + var removeBackdrop = function() { + backdropEl.remove(); + }; + + // Push the new popup onto the stack with the given data and scope. + // If this is the first one in the stack, show the backdrop, otherwise don't. + var pushAndShow = function(popup, data) { + var lastPopup = popupStack[popupStack.length-1]; + + popupStack.push(popup); + + // If this is the first popup, show the backdrop + if(popupStack.length == 1) { + showBackdrop(); + } + + // If we have an existing popup, add a delay between hiding and showing it + if(lastPopup) { + hideSinglePopup(lastPopup); + $timeout(function() { + showSinglePopup(popup); + }, popupOptions.stackPushDelay); + } else { + // Otherwise, immediately show it + showSinglePopup(popup); + } + + }; + + // Pop the current popup off the stack. If there are other popups, show them + // otherwise hide the backdrop. + var popAndRemove = function(popup) { + var lastPopup = popupStack.pop(); + var nextPopup = popupStack[popupStack.length-1]; + removeSinglePopup(lastPopup); + + if(nextPopup) { + reshowSinglePopup(nextPopup); + } else { + removeBackdrop(); + } + }; + + // Append the element to the screen, create the popup view, + // and add the popup to the scope + var constructPopupOnScope = function(element, scope) { + var popup = { + el: element[0], + scope: scope + }; + + scope.popup = popup; + + return popup; + } + + var buildPopupTemplate = function(opts, content) { + return '' + + (content || '') + + ''; + }; + + + // Given an options object, build a new popup window and return a promise + // which will contain the constructed popup at a later date. Perhaps at a later + // year even. At this point, it's hard to say. + var createPopup = function(opts, responseDeferred) { + var q = $q.defer(); + + // Create some defaults + var defaults = { + title: '', + animation: 'fade-in', + }; + + opts = angular.extend(defaults, opts); + + // Create a new scope, and bind some of the options stuff to that scope + var scope = opts.scope && opts.scope.$new() || $rootScope.$new(true); + angular.extend(scope, opts); + + scope.onClose = function(button, result, event) { + popAndRemove(scope.popup); + responseDeferred.resolve(result); + }; + + // Check if we need to load a template for the content of the popup + if(opts.templateUrl) { + + // Load the template externally + $ionicTemplateLoader.load(opts.templateUrl).then(function(templateString) { + + var popupTemplate = buildPopupTemplate(opts, templateString); + var element = $compile(popupTemplate)(scope); + $document[0].body.appendChild(element[0]); + q.resolve(constructPopupOnScope(element, scope)); + + }, function(err) { + // Error building the popup + q.reject(err); + }); + + } else { + // Compile the template + var popupTemplate = buildPopupTemplate(opts, opts.content); + var element = $compile(popupTemplate)(scope); + $document[0].body.appendChild(element[0]); + q.resolve(constructPopupOnScope(element, scope)); + } + + return q.promise; + }; + + + // Public API + return { + showPopup: function(data) { + var q = $q.defer(); + + createPopup(data, q).then(function(popup, scope) { + + // We constructed the popup, push it on the stack and show it + pushAndShow(popup, data); + + }, function(err) { + console.error('Unable to load popup:', err); + }); + + return q.promise; + }, + + /** + * @ngdoc method + * @name $ionicPopup#show + * @description show a complex popup. This is the master show function for all popups + * @param {data} object The options for showing a popup, of the form: + * + * ``` + * { + * content: '', // String. The content of the popup + * title: '', // String. The title of the popup + * subTitle: '', // String (optional). The sub-title of the popup + * templateUrl: '', // URL String (optional). The URL of a template to load as the content (instead of the `content` field) + * scope: null, // Scope (optional). A scope to apply to the popup content (for using ng-model in a template, for example) + * buttons: + * [ + * { + * text: 'Cancel', + * type: 'button-default', + * onTap: function(e) { + * // e.preventDefault() is the only way to return a false value + * e.preventDefault(); + * } + * }, + * { + * text: 'OK', + * type: 'button-positive', + * onTap: function(e) { + * // When the user taps one of the buttons, you need to return the + * // Data you want back to the popup service which will then resolve + * // the promise waiting for a response. + * // + * // To return "false", call e.preventDefault(); + * return scope.data.response; + * } + * } + * ] + * + * } + * ``` + */ + show: function(data) { + return this.showPopup(data); + }, + + /** + * @ngdoc method + * @name $ionicPopup#alert + * @description show a simple popup with one button that the user has to tap + * + * Show a simple alert dialog + * + * ```javascript + * $ionicPopup.alert({ + * title: 'Hey!;, + * content: 'Don\'t do that!' + * }).then(function(res) { + * // Accepted + * }); + * ``` + * + * @returns {Promise} that resolves when the alert is accepted + * @param {data} object The options for showing an alert, of the form: + * + * ``` + * { + * content: '', // String. The content of the popup + * title: '', // String. The title of the popup + * okText: '', // String. The text of the OK button + * okType: '', // String (default: button-positive). The type of the OK button + * } + * ``` + */ + alert: function(opts) { + return this.showPopup({ + content: opts.content || '', + title: opts.title || '', + buttons: [ + { + text: opts.okText || 'OK', + type: opts.okType || 'button-positive', + onTap: function(e) { + return true; + } + } + ] + }); + }, + + /** + * @ngdoc method + * @name $ionicPopup#confirm + * @description + * Show a simple confirm popup with a cancel and accept button: + * + * ```javascript + * $ionicPopup.confirm({ + * title: 'Consume Ice Cream', + * content: 'Are you sure you want to eat this ice cream?' + * }).then(function(res) { + * if(res) { + * console.log('You are sure'); + * } else { + * console.log('You are not sure'); + * } + * }); + * ``` + * + * @returns {Promise} that resolves with the chosen option + * @param {data} object The options for showing a confirm dialog, of the form: + * + * ``` + * { + * content: '', // String. The content of the popup + * title: '', // String. The title of the popup + * cancelText: '', // String. The text of the Cancel button + * cancelType: '', // String (default: button-default). The type of the kCancel button + * okText: '', // String. The text of the OK button + * okType: '', // String (default: button-positive). The type of the OK button + * } + * ``` + */ + confirm: function(opts) { + return this.showPopup({ + content: opts.content || '', + title: opts.title || '', + buttons: [ + { + text: opts.cancelText || 'Cancel' , + type: opts.cancelType || 'button-default', + onTap: function(e) { e.preventDefault(); } + }, + { + text: opts.okText || 'OK', + type: opts.okType || 'button-positive', + onTap: function(e) { + return true; + } + } + ] + }); + }, + + /** + * @ngdoc method + * @name $ionicPopup#prompt + * @description show a simple prompt dialog. + * + * ```javascript + * $ionicPopup.prompt({ + * title: 'Password Check', + * content: 'Enter your secret password', + * inputType: 'password', + * inputPlaceholder: 'Your password' + * }).then(function(res) { + * console.log('Your password is', res); + * }); + * ``` + * + * @returns {Promise} that resolves with the entered data + * @param {data} object The options for showing a prompt dialog, of the form: + * + * ``` + * { + * content: // String. The content of the popup + * title: // String. The title of the popup + * subTitle: // String. The sub title of the popup + * inputType: // String (default: "text"). The type of input to use + * inputPlaceholder: // String (default: ""). A placeholder to use for the input. + * cancelText: // String. The text of the Cancel button + * cancelType: // String (default: button-default). The type of the kCancel button + * okText: // String. The text of the OK button + * okType: // String (default: button-positive). The type of the OK button + * } + * ``` + */ + prompt: function(opts) { + var scope = $rootScope.$new(true); + scope.data = {}; + return this.showPopup({ + content: opts.content || '', + title: opts.title || '', + subTitle: opts.subTitle || '', + scope: scope, + buttons: [ + { + text: opts.cancelText || 'Cancel', + type: opts.cancelType|| 'button-default', + onTap: function(e) { e.preventDefault(); } + }, + { + text: opts.okText || 'OK', + type: opts.okType || 'button-positive', + onTap: function(e) { + return scope.data.response; + } + } + ] + }); + } + + }; }]); + +})(ionic); diff --git a/js/ext/angular/test/directive/ionicPopup.unit.js b/js/ext/angular/test/directive/ionicPopup.unit.js new file mode 100644 index 0000000000..300c998fcc --- /dev/null +++ b/js/ext/angular/test/directive/ionicPopup.unit.js @@ -0,0 +1,97 @@ +'use strict'; + +describe('Ionic Popup Directive', function() { + var compile, scope, popupEl; + + beforeEach(module('ionic')); + + beforeEach(inject(function($compile, $rootScope, $controller) { + compile = $compile; + scope = $rootScope; + + })); + + it('Should build', function() { + popupEl = angular.element(''); + popupEl = compile(popupEl)(scope); + + expect(popupEl[0].classList.contains('popup')).toBe(true); + }); + + it('Set scope', function() { + popupEl = angular.element(''); + + var buttons = [ + { + type: 'button-whack', + text: 'Whacky' + } + ]; + + scope.buttons = buttons; + + popupEl = compile(popupEl)(scope); + + scope.$apply(); + + scope = popupEl.scope(); + + expect(scope.title).toEqual('Cats'); + + expect(scope.buttons).toBe(buttons); + expect(scope.buttons[0].text).toEqual('Whacky'); + expect(scope.buttons[0].type).toEqual('button-whack'); + + expect(popupEl[0].querySelector('button').classList.contains('button-whack')).toBe(true); + + expect(popupEl[0].classList.contains('popup')).toBe(true); + }); + + it('Does call callbacks', function() { + popupEl = angular.element(''); + + // For spy + /* Not sure how to trigger these + var cb = { + onTap: function(e) {}, + onClose: function(e) {} + }; + */ + + var buttons = [ + { + type: 'button-whack', + text: 'Whacky', + onTap: function(e) {} + } + ]; + + scope.buttons = buttons; + + /* + scope.onTap = cb.onTap; + scope.onClose = cb.onClose; + */ + + popupEl = compile(popupEl)(scope); + + scope.$apply(); + + spyOn(buttons[0], 'onTap'); + /* + spyOn(cb, 'onTap'); + spyOn(cb, 'onClose'); + */ + + var button = popupEl[0].querySelector('button'); + ionic.trigger('click', { + target: button + }); + + /* + expect(cb.onTap).toHaveBeenCalled(); + expect(cb.onClose).toHaveBeenCalled(); + */ + expect(buttons[0].onTap).toHaveBeenCalled(); + }); +}); diff --git a/js/ext/angular/test/popup.html b/js/ext/angular/test/popup.html new file mode 100644 index 0000000000..6e3350cde2 --- /dev/null +++ b/js/ext/angular/test/popup.html @@ -0,0 +1,107 @@ + + + + Popups + + + + + + + + + + + + + + + + + + + + + diff --git a/js/ext/angular/test/service/ionicPopup.unit.js b/js/ext/angular/test/service/ionicPopup.unit.js new file mode 100644 index 0000000000..99afff1b57 --- /dev/null +++ b/js/ext/angular/test/service/ionicPopup.unit.js @@ -0,0 +1,70 @@ +describe('Ionic Popup', function() { + var popup, timeout, scope; + + beforeEach(module('ionic')); + + beforeEach(inject(function($ionicPopup, $rootScope, $timeout) { + ionic.requestAnimationFrame = function(cb) { cb(); } + scope = $rootScope; + popup = $ionicPopup; + timeout = $timeout; + })); + + it('Should show popup', function() { + popup.show({ + title: 'Cats', + content: 'Dogs', + buttons: [ + { + text: 'Okay', + type: 'button-balanced', + onTap: function(e) {} + } + ] + }); + + timeout.flush(); + + var popupBackdropEl = document.body.querySelector('.popup-backdrop'); + expect(popupBackdropEl).not.toEqual(null); + + var popupEl = document.body.querySelector('.popup'); + expect(popupEl.classList.contains('active')).toBe(true); + expect(popupEl.classList.contains('popup-showing')).toBe(true); + + popup.show({ + title: 'Cats', + content: 'Dogs', + buttons: [ + { + text: 'Okay', + type: 'button-balanced', + onTap: function(e) {} + } + ] + }); + + timeout.flush(); + + // Make sure there are two popups + expect(document.body.querySelectorAll('.popup').length).toEqual(2); + }); + + it('Should set correct element data', function() { + popup.show({ + title: 'Cats', + content: 'Dogs', + buttons: [ + { + text: 'Okay', + type: 'button-balanced', + onTap: function(e) {} + } + ] + }); + + var popupEl = document.body.querySelector('.popup'); + expect(popupEl.querySelector('.popup-title').innerText).toEqual('Cats'); + expect(popupEl.querySelector('.popup-body').innerText).toEqual('Dogs'); + }); +}); diff --git a/js/views/popupView.js b/js/views/popupView.js deleted file mode 100644 index 713113d45e..0000000000 --- a/js/views/popupView.js +++ /dev/null @@ -1,38 +0,0 @@ -(function(ionic) { -'use strict'; - /** - * An ActionSheet is the slide up menu popularized on iOS. - * - * You see it all over iOS apps, where it offers a set of options - * triggered after an action. - */ - ionic.views.Popup = ionic.views.View.inherit({ - initialize: function(opts) { - var _this = this; - - this.el = opts.el; - }, - - setTitle: function(title) { - var titleEl = el.querySelector('.popup-title'); - if(titleEl) { - titleEl.innerHTML = title; - } - }, - alert: function(message) { - var _this = this; - - ionic.requestAnimationFrame(function() { - _this.setTitle(message); - _this.el.classList.add('active'); - }); - }, - hide: function() { - // Force a reflow so the animation will actually run - this.el.offsetWidth; - - this.el.classList.remove('active'); - } - }); - -})(ionic); diff --git a/release/css/ionic.css b/release/css/ionic.css index 3fd8bbb462..faf14e14bb 100644 --- a/release/css/ionic.css +++ b/release/css/ionic.css @@ -1,3 +1,4 @@ +@charset "UTF-8"; /*! * Copyright 2014 Drifty Co. * http://drifty.com/ @@ -3175,15 +3176,39 @@ a.subdued { * -------------------------------------------------- */ .popup { - position: fixed; } + position: fixed; + top: 50%; + left: 50%; + border-radius: 8px; + background-color: rgba(255, 255, 255, 0.9); } -.popup-content { +.popup-head { + padding: 15px 0px; + text-align: center; + border-bottom: 1px solid #eee; } + +.popup-title { + font-size: 14px; + margin: 0; + padding: 0; } + +.popup-body { padding: 10px; } -.loading-backdrop { - -webkit-transition: visibility 0s linear 0.3s; - -moz-transition: visibility 0s linear 0.3s; - transition: visibility 0s linear 0.3s; +.popup-buttons.row { + padding: 0; } +.popup-buttons .button { + line-height: 30px; + border: none; + border-right: 1px solid #eee; + border-radius: 0px 0px 8px 8px; } + .popup-buttons .button:last-child { + border-right: none; } + +.popup-backdrop.enabled { + background-color: rgba(0, 0, 0, 0.4); } + +.loading-backdrop, .popup-backdrop { position: fixed; top: 0; left: 0; @@ -3191,14 +3216,19 @@ a.subdued { visibility: hidden; width: 100%; height: 100%; } - .loading-backdrop.enabled { - background-color: rgba(0, 0, 0, 0.7); } - .loading-backdrop.active { + .loading-backdrop.active, .popup-backdrop.active { -webkit-transition-delay: 0s; -moz-transition-delay: 0s; transition-delay: 0s; visibility: visible; } +.loading-backdrop { + -webkit-transition: visibility 0s linear 0.3s; + -moz-transition: visibility 0s linear 0.3s; + transition: visibility 0s linear 0.3s; } + .loading-backdrop.enabled { + background-color: rgba(0, 0, 0, 0.7); } + .loading { position: fixed; top: 50%; diff --git a/scss/_animations.scss b/scss/_animations.scss index 4a86fcc624..bebdd5486b 100644 --- a/scss/_animations.scss +++ b/scss/_animations.scss @@ -184,6 +184,56 @@ $slide-in-up-function: cubic-bezier(.1, .7, .1, 1); to { background-color: rgba(0,0,0,0); } } +// Scale Out +// Scale from hero (1 in this case) to zero +// ------------------------------- + +@-webkit-keyframes scaleOut { + from { -webkit-transform: scale(1); opacity: 1; } + to { -webkit-transform: scale(0.8); opacity: 0; } +} +@-moz-keyframes scaleOut { + from { -moz-transform: scale(1); opacity: 1; } + to { -moz-transform: scale(0.8); opacity: 0; } +} +@keyframes scaleOut { + from { transform: scale(1); opacity: 1; } + to { transform: scale(0.8); opacity: 0; } +} + +// Scale In +// Scale from 0 to hero (1 in this case) +// ------------------------------- + +@-webkit-keyframes scaleIn { + from { -webkit-transform: scale(0); } + to { -webkit-transform: scale(1); } +} +@-moz-keyframes scaleIn { + from { -moz-transform: scale(0); } + to { -moz-transform: scale(1); } +} +@keyframes scaleIn { + from { transform: scale(0); } + to { transform: scale(1); } +} + +// Super Scale In +// Scale from super (1.x) to duper (1 in this case) +// ------------------------------- + +@-webkit-keyframes superScaleIn { + from { -webkit-transform: scale(1.2); opacity: 0; } + to { -webkit-transform: scale(1); opacity: 1 } +} +@-moz-keyframes superScaleIn { + from { -moz-transform: scale(1.2); opacity: 0; } + to { -moz-transform: scale(1); opacity: 1; } +} +@keyframes superScaleIn { + from { transform: scale(1.2); opacity: 0; } + to { transform: scale(1); opacity: 1; } +} // Spin // ------------------------------- diff --git a/scss/_popup.scss b/scss/_popup.scss index 1c7ea9ec45..cf2f45fa51 100644 --- a/scss/_popup.scss +++ b/scss/_popup.scss @@ -5,14 +5,95 @@ .popup { position: fixed; + top: 50%; + left: 50%; + z-index: 11; + + // Start hidden + visibility: hidden; + + width: $popup-width; + + border-radius: $popup-border-radius; + background-color: $popup-background-color; + + &.popup-hidden { + @include animation-name(scaleOut); + @include animation-duration($popup-leave-animation-duration); + @include animation-timing-function(ease-in-out); + @include animation-fill-mode(both); + } + + &.popup-showing { + visibility: visible; + } + + &.active { + @include animation-name(superScaleIn); + @include animation-duration($popup-enter-animation-duration); + @include animation-timing-function(ease-in-out); + @include animation-fill-mode(both); + } } -.popup-content { +.popup-head { + padding: 15px 0px; + text-align: center; + border-bottom: 1px solid #eee; +} +.popup-title { + font-size: 15px; + margin: 0; + padding: 0; +} +.popup-sub-title { + font-size: 11px; + margin: 5px 0 0 0; + padding: 0; + font-weight: normal; +} +.popup-body { padding: 10px; } +.popup-buttons { + &.row { + padding: 10px 10px; + } + + .button { + margin: 0px 5px; + min-height: $popup-button-min-height; + line-height: $popup-button-line-height; + border-radius: $popup-button-border-radius; + + &:first-child { + margin-left: 0px; + } + &:last-child { + margin-right: 0px; + } + } +} + +.popup-backdrop { + position: fixed; + top: 0; + left: 0; + z-index: 10; + + width: 100%; + height: 100%; + + background-color: rgba(0,0,0,0.4); + + @include animation-name(fadeIn); + @include animation-duration($popup-backdrop-fadein-duration); + @include animation-timing-function(linear); + @include animation-fill-mode(both); +} + .loading-backdrop { - @include transition(visibility 0s linear 0.3s); position: fixed; top: 0; left: 0; @@ -22,16 +103,19 @@ width: 100%; height: 100%; - &.enabled { - background-color: rgba(0,0,0,0.7); - } - &.active { @include transition-delay(0s); visibility: visible; } } +.loading-backdrop { + @include transition(visibility 0s linear 0.3s); + &.active { + background-color: rgba(0,0,0,0.7); + } +} + .loading { position: fixed; top: 50%; diff --git a/scss/_variables.scss b/scss/_variables.scss index caf63b6511..904ee95846 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -548,6 +548,22 @@ $sheet-border-radius: 3px 3px 3px 3px !default; $sheet-border-radius-top: 3px 3px 0px 0px !default; $sheet-border-radius-bottom: 0px 0px 3px 3px !default; +// Popups +// ------------------------------- + +$popup-backdrop-fadein-duration: 0.1s; +$popup-width: 250px; +$popup-enter-animation: superScaleIn; +$popup-enter-animation-duration: 0.2s; +$popup-leave-animation-duration: 0.1s; + +$popup-border-radius: 0px; +$popup-background-color: rgba(255,255,255,0.9); + +$popup-button-border-radius: 2px; +$popup-button-line-height: 20px; +$popup-button-min-height: 45px; + // Badges // -------------------------------