feat(popup): Added popup support

This commit is contained in:
Max Lynch
2014-03-13 14:57:48 -05:00
parent 53a426f802
commit a30b0b7d4f
12 changed files with 1049 additions and 98 deletions

View File

@@ -0,0 +1,63 @@
(function() {
'use strict';
angular.module('ionic.ui.popup', [])
/**
* @private
*/
.directive('ionPopupBackdrop', function() {
return {
restrict: 'E',
replace: true,
template: '<div class="popup-backdrop"></div>'
}
})
/**
* @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: '<div class="popup">' +
'<div class="popup-head">' +
'<h3 class="popup-title" ng-bind-html="title"></h3>' +
'<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
'</div>' +
'<div class="popup-body" ng-transclude>' +
'</div>' +
'<div class="popup-buttons row">' +
'<button ng-repeat="button in buttons" ng-click="_buttonTapped(button, $event)" class="button col" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
'</div>' +
'</div>'
};
}]);
})();

View File

@@ -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'
]);

View File

@@ -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: '<b>Save</b>',
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('<popup>' + opts.content + '</popup>')(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('<ion-popup-backdrop></ion-popup-backdrop>')($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 '<ion-popup title="' + opts.title + '" buttons="buttons" on-button-tap="onButtonTap(button, event)" on-close="onClose(button, result, event)">'
+ (content || '') +
'</ion-popup>';
};
// 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 || '<input ng-model="data.response" type="' + (opts.inputType || 'text') + '" placeholder="' + (opts.inputPlaceholder || '') + '">',
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);