From 368a8da7dbdb7bbf991d7919b2eddaeff4511945 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 12 Mar 2014 00:49:03 -0400 Subject: [PATCH 1/5] - Add data classes for notifications - Add basic API for notifications - Change the password required to be a notification --- data/database.py | 15 ++++++++++++++- data/model.py | 32 ++++++++++++++++++++++++++++++++ endpoints/api.py | 18 +++++++++++++++++- endpoints/common.py | 10 ++++++++-- endpoints/trigger.py | 4 ++-- initdb.py | 7 +++++++ test/data/test.db | Bin 171008 -> 483328 bytes 7 files changed, 80 insertions(+), 6 deletions(-) diff --git a/data/database.py b/data/database.py index 413d2261c..c98444ec6 100644 --- a/data/database.py +++ b/data/database.py @@ -271,9 +271,22 @@ class LogEntry(BaseModel): metadata_json = TextField(default='{}') +class NotificationKind(BaseModel): + name = CharField(index=True) + + +class Notification(BaseModel): + uuid = CharField(default=uuid_generator, index=True) + kind = ForeignKeyField(NotificationKind, index=True) + notification_user = ForeignKeyField(User, index=True) + metadata_json = TextField(default='{}') + created = DateTimeField(default=datetime.now, index=True) + + all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility, RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem, RepositoryBuild, Team, TeamMember, TeamRole, Webhook, LogEntryKind, LogEntry, PermissionPrototype, ImageStorage, - BuildTriggerService, RepositoryBuildTrigger] + BuildTriggerService, RepositoryBuildTrigger, NotificationKind, + Notification] diff --git a/data/model.py b/data/model.py index fd3b51855..9eec9f841 100644 --- a/data/model.py +++ b/data/model.py @@ -93,6 +93,12 @@ def create_user(username, password, email): new_user = User.create(username=username, password_hash=pw_hash, email=email) + + # If the password is None, then add a notification for the user to change + # their password ASAP. + if not pw_hash: + create_notification('password_required', new_user) + return new_user except Exception as ex: raise DataModelException(ex.message) @@ -662,6 +668,9 @@ def change_password(user, new_password): user.password_hash = pw_hash user.save() + # Remove any password required notifications for the user. + delete_notifications_by_kind(user, 'password_required') + def change_invoice_email(user, invoice_email): user.invoice_email = invoice_email @@ -1535,3 +1544,26 @@ def list_trigger_builds(namespace_name, repository_name, trigger_uuid, limit): return (list_repository_builds(namespace_name, repository_name, limit) .where(RepositoryBuildTrigger.uuid == trigger_uuid)) + + +def create_notification(kind, user, metadata={}): + kind_ref = NotificationKind.get(name=kind) + notification = Notification.create(kind=kind_ref, notification_user=user, + metadata_json=json.dumps(metadata)) + return notification + + +def list_notifications(user, kind=None): + query = (Notification.select() + .join(User) + .where(Notification.notification_user == user)) + + if kind: + query = query.join(NotificationKind).where(NotificationKind.name == kind) + + return query.order_by(Notification.created).desc() + + +def delete_notifications_by_kind(user, kind): + kind_ref = NotificationKind.get(name=kind) + Notification.delete().where(Notification.user == user, Notification.kind == kind_ref).execute() diff --git a/endpoints/api.py b/endpoints/api.py index 22a571be4..117ba4983 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -28,7 +28,7 @@ from auth.permissions import (ReadRepositoryPermission, ViewTeamPermission, UserPermission) from endpoints.common import (common_login, get_route_data, truthy_param, - start_build) + start_build, add_notification) from endpoints.trigger import (BuildTrigger, TriggerActivationException, TriggerDeactivationException, EmptyRepositoryException) @@ -2518,3 +2518,19 @@ def get_logs(namespace, start_time, end_time, performer_name=None, 'logs': [log_view(log) for log in logs] }) + +def notification_view(notification): + return { + 'kind': notification.kind.name, + 'created': notification.created, + 'metadata': json.loads(notification.metadata_json), + } + + +@api.route('/user/notifications', methods=['GET']) +@api_login_required +def list_user_notifications(): + notifications = model.list_notifications(current_user.db_user()) + return jsonify({ + 'notifications': [notification_view(notification) for notification in notifications] + }) diff --git a/endpoints/common.py b/endpoints/common.py index 0fc3dc3da..0f0e68df7 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -5,7 +5,7 @@ import urlparse import json from flask import session, make_response, render_template, request -from flask.ext.login import login_user, UserMixin +from flask.ext.login import login_user, UserMixin, current_user from flask.ext.principal import identity_changed from data import model @@ -120,13 +120,19 @@ app.jinja_env.globals['csrf_token'] = generate_csrf_token def render_page_template(name, **kwargs): - resp = make_response(render_template(name, route_data=get_route_data(), **kwargs)) resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp +def add_notification(kind, metadata=None, user=None): + if not user and current_user: + user = current_user.db_user() + + return model.create_notification(kind, user, metadata or {}) + + def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, trigger=None): host = urlparse.urlparse(request.url).netloc diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 41c32045a..82a3284ab 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -203,7 +203,7 @@ class GithubBuildTrigger(BuildTrigger): try: repo = gh_client.get_repo(source) - default_commit = repo.get_branch(repo.master_branch).commit + default_commit = repo.get_branch(repo.master_branch or 'master').commit commit_tree = repo.get_git_tree(default_commit.sha, recursive=True) return [os.path.dirname(elem.path) for elem in commit_tree.tree @@ -283,4 +283,4 @@ class GithubBuildTrigger(BuildTrigger): short_sha = GithubBuildTrigger.get_display_name(master_sha) ref = 'refs/heads/%s' % repo.master_branch - return self._prepare_build(config, repo, master_sha, short_sha, ref) \ No newline at end of file + return self._prepare_build(config, repo, master_sha, short_sha, ref) diff --git a/initdb.py b/initdb.py index a1e2fe646..561d98992 100644 --- a/initdb.py +++ b/initdb.py @@ -224,6 +224,11 @@ def initialize_database(): LogEntryKind.create(name='setup_repo_trigger') LogEntryKind.create(name='delete_repo_trigger') + NotificationKind.create(name='password_required') + NotificationKind.create(name='over_private_usage') + + NotificationKind.create(name='test_notification') + def wipe_database(): logger.debug('Wiping all data from the DB.') @@ -269,6 +274,8 @@ def populate_database(): outside_org.verified = True outside_org.save() + model.create_notification('test_notification', new_user_1, metadata={'some': 'value'}) + __generate_repository(new_user_4, 'randomrepo', 'Random repo repository.', False, [], (4, [], ['latest', 'prod'])) diff --git a/test/data/test.db b/test/data/test.db index 9779208a8859fc77e4860974aa2c534dc515b959..dd1b56708da5e40345ece6be2635fa96e48055fe 100644 GIT binary patch delta 20366 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 From f186fa288846a94ff5b161a7264c5873717a4780 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 12 Mar 2014 00:49:46 -0400 Subject: [PATCH 2/5] - Convert Quay over to Angular 1.2.9 and the new angular-strap - Add the beginnings of a notification service - Add the beginnings of a notification UI --- static/css/quay.css | 29 +- static/directives/delete-ui.html | 4 + static/directives/header-bar.html | 21 +- static/directives/notification-bar.html | 14 + static/directives/popup-input-button.html | 5 +- static/directives/popup-input-dialog.html | 4 +- static/directives/prototype-manager.html | 5 +- static/directives/robots-manager.html | 5 +- static/js/app.js | 202 +- static/lib/angular-motion.min.css | 8 + static/lib/angular-strap.js | 3543 +++++++++++++++++++++ static/lib/angular-strap.min.js | 12 +- static/lib/angular-strap.tpl.min.js | 9 + static/lib/bootstrap-additions.min.css | 1 + static/partials/repo-admin.html | 20 +- static/partials/team-view.html | 6 +- static/partials/view-repo.html | 13 +- templates/base.html | 11 +- 18 files changed, 3794 insertions(+), 118 deletions(-) create mode 100644 static/directives/delete-ui.html create mode 100644 static/directives/notification-bar.html create mode 100644 static/lib/angular-motion.min.css create mode 100644 static/lib/angular-strap.js create mode 100644 static/lib/angular-strap.tpl.min.js create mode 100644 static/lib/bootstrap-additions.min.css diff --git a/static/css/quay.css b/static/css/quay.css index bde24600a..dc80d0608 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -507,7 +507,22 @@ i.toggle-icon:hover { min-width: 200px; } -.user-notification { +.user-notification.notification-primary { + background: #428bca; + color: white; +} + +.user-notification.notification-info { + color: black; + background: #d9edf7; +} + +.user-notification.notification-warning { + color: #8a6d3b; + background: #fcf8e3; +} + +.user-notification.notification-error { background: red; } @@ -2122,16 +2137,16 @@ p.editable:hover i { padding-right: 6px; } -.delete-ui { +.delete-ui-element { outline: none; } -.delete-ui i { +.delete-ui-element i { cursor: pointer; vertical-align: middle; } -.delete-ui .delete-ui-button { +.delete-ui-element .delete-ui-button { display: inline-block; vertical-align: middle; color: white; @@ -2147,15 +2162,15 @@ p.editable:hover i { transition: width 500ms ease-in-out; } -.delete-ui .delete-ui-button button { +.delete-ui-element .delete-ui-button button { padding: 4px; } -.delete-ui:focus i { +.delete-ui-element:focus i { visibility: hidden; } -.delete-ui:focus .delete-ui-button { +.delete-ui-element:focus .delete-ui-button { width: 60px; } diff --git a/static/directives/delete-ui.html b/static/directives/delete-ui.html new file mode 100644 index 000000000..d04e840f0 --- /dev/null +++ b/static/directives/delete-ui.html @@ -0,0 +1,4 @@ + + + + diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index 21b42cbcb..bd534f13a 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -39,11 +39,14 @@ {{ user.username }} - - {{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }} + {{ notificationService.notifications.length }} @@ -51,8 +54,16 @@
  • Account Settings - - {{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }} + +
  • +
  • + + Notifications + + {{ notificationService.notifications.length }}
  • diff --git a/static/directives/notification-bar.html b/static/directives/notification-bar.html new file mode 100644 index 000000000..f0f5f3363 --- /dev/null +++ b/static/directives/notification-bar.html @@ -0,0 +1,14 @@ + diff --git a/static/directives/popup-input-button.html b/static/directives/popup-input-button.html index 005c037bc..4ff6dce77 100644 --- a/static/directives/popup-input-button.html +++ b/static/directives/popup-input-button.html @@ -1,5 +1,6 @@ - diff --git a/static/directives/popup-input-dialog.html b/static/directives/popup-input-dialog.html index 6632b1999..8bff8c185 100644 --- a/static/directives/popup-input-dialog.html +++ b/static/directives/popup-input-dialog.html @@ -1,4 +1,4 @@ -
    - +
    diff --git a/static/directives/prototype-manager.html b/static/directives/prototype-manager.html index 53567e1ac..26cec48c8 100644 --- a/static/directives/prototype-manager.html +++ b/static/directives/prototype-manager.html @@ -48,10 +48,7 @@ - - - - + diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html index 68a2ed08a..c696937d2 100644 --- a/static/directives/robots-manager.html +++ b/static/directives/robots-manager.html @@ -24,10 +24,7 @@ - - - - + diff --git a/static/js/app.js b/static/js/app.js index a5dbf2637..63fd27024 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -102,10 +102,9 @@ function getMarkedDown(string) { return Markdown.getSanitizingConverter().makeHtml(string || ''); } -quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) { +quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) { cfpLoadingBarProvider.includeSpinner = false; - $provide.factory('UtilService', ['$sanitize', function($sanitize) { var utilService = {}; @@ -143,6 +142,49 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return builderService; }]); + $provide.factory('StringBuilderService', ['$sce', function($sce) { + var stringBuilderService = {}; + + stringBuilderService.buildString = function(value_or_func, metadata) { + var fieldIcons = { + 'username': 'user', + 'activating_username': 'user', + 'delegate_user': 'user', + 'delegate_team': 'group', + 'team': 'group', + 'token': 'key', + 'repo': 'hdd-o', + 'robot': 'wrench', + 'tag': 'tag', + 'role': 'th-large', + 'original_role': 'th-large' + }; + + var description = value_or_func; + if (typeof description != 'string') { + description = description(metadata); + } + + for (var key in metadata) { + if (metadata.hasOwnProperty(key)) { + var value = metadata[key] != null ? metadata[key].toString() : '(Unknown)'; + var markedDown = getMarkedDown(value); + markedDown = markedDown.substr('

    '.length, markedDown.length - '

    '.length); + + var icon = fieldIcons[key]; + if (icon) { + markedDown = '' + markedDown; + } + + description = description.replace('{' + key + '}', '' + markedDown + ''); + } + } + return $sce.trustAsHtml(description.replace('\n', '
    ')); + }; + + return stringBuilderService; + }]); + $provide.factory('ImageMetadataService', ['UtilService', function(UtilService) { var metadataService = {}; @@ -340,7 +382,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu anonymous: true, username: null, email: null, - askForPassword: false, organizations: [], logins: [] } @@ -438,6 +479,70 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return userService; }]); + $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', + function($rootScope, $interval, UserService, ApiService, StringBuilderService) { + var notificationService = { + 'user': null, + 'notifications': [], + 'notificationClasses': [], + 'notificationSummaries': [] + }; + + var pollTimerHandle = null; + + var notificationKinds = { + 'test_notification': { + 'level': 'primary', + 'summary': 'This is a test notification', + 'message': 'This notification is a long message for testing' + } + }; + + notificationService.getSummary = function(notification) { + var kindInfo = notificationKinds[notification['kind']]; + return StringBuilderService.buildString(kindInfo['summary'], notification['metadata']); + }; + + notificationService.getSummaries = function(notifications) { + var summaries = []; + for (var i = 0; i < notifications.length; ++i) { + var notification = notifications[i]; + summaries.push(notificationService.getSummary(notification)); + } + return summaries.join('
    '); + }; + + notificationService.getClasses = function(notifications) { + var classes = []; + for (var i = 0; i < notifications.length; ++i) { + var notification = notifications[i]; + classes.push('notification-' + notificationKinds[notification['kind']]['level']); + } + return classes.join(' '); + }; + + notificationService.update = function() { + ApiService.listUserNotifications().then(function(resp) { + notificationService.notifications = resp['notifications']; + notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications); + notificationService.notificationSummaries = notificationService.getSummaries(notificationService.notifications); + }); + }; + + notificationService.reset = function() { + $interval.cancel(pollTimerHandle); + pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */); + }; + + // Watch for user changes and update. + $rootScope.$watch(function() { return UserService.currentUser(); }, function(currentUser) { + notificationService.reset(); + notificationService.update(); + }); + + return notificationService; + }]); + $provide.factory('KeyService', ['$location', function($location) { var keyService = {} @@ -1332,7 +1437,7 @@ quayApp.directive('logsView', function () { 'repository': '=repository', 'performer': '=performer' }, - controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder) { + controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder, StringBuilderService) { $scope.loading = true; $scope.logs = null; $scope.kindsAllowed = null; @@ -1547,43 +1652,9 @@ quayApp.directive('logsView', function () { return $scope.chart.getColor(kind); }; - $scope.getDescription = function(log) { - var fieldIcons = { - 'username': 'user', - 'activating_username': 'user', - 'delegate_user': 'user', - 'delegate_team': 'group', - 'team': 'group', - 'token': 'key', - 'repo': 'hdd-o', - 'robot': 'wrench', - 'tag': 'tag', - 'role': 'th-large', - 'original_role': 'th-large' - }; - + $scope.getDescription = function(log) { log.metadata['_ip'] = log.ip ? log.ip : null; - - var description = logDescriptions[log.kind] || log.kind; - if (typeof description != 'string') { - description = description(log.metadata); - } - - for (var key in log.metadata) { - if (log.metadata.hasOwnProperty(key)) { - var value = log.metadata[key] != null ? log.metadata[key].toString() : '(Unknown)'; - var markedDown = getMarkedDown(value); - markedDown = markedDown.substr('

    '.length, markedDown.length - '

    '.length); - - var icon = fieldIcons[key]; - if (icon) { - markedDown = '' + markedDown; - } - - description = description.replace('{' + key + '}', '' + markedDown + ''); - } - } - return $sce.trustAsHtml(description.replace('\n', '
    ')); + return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata); }; $scope.$watch('organization', update); @@ -1845,6 +1916,31 @@ quayApp.directive('prototypeManager', function () { }); +quayApp.directive('deleteUi', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/delete-ui.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'deleteTitle': '=deleteTitle', + 'buttonTitle': '=buttonTitle', + 'performDelete': '&performDelete' + }, + controller: function($scope, $element) { + $scope.buttonTitleInternal = $scope.buttonTitle || 'Delete'; + + $element.children().attr('tabindex', 0); + $scope.focus = function() { + $element[0].firstChild.focus(); + }; + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('popupInputButton', function () { var directiveDefinitionObject = { priority: 0, @@ -1863,7 +1959,7 @@ quayApp.directive('popupInputButton', function () { var box = $('#input-box'); box[0].value = ''; box.focus(); - }, 10); + }, 40); }; $scope.getRegexp = function(pattern) { @@ -2077,26 +2173,12 @@ quayApp.directive('headerBar', function () { restrict: 'C', scope: { }, - controller: function($scope, $element, $location, UserService, PlanService, ApiService) { - $scope.overPlan = false; - - var checkOverPlan = function() { - if ($scope.user.anonymous) { - $scope.overPlan = false; - return; - } - - ApiService.getUserPrivateAllowed().then(function(resp) { - $scope.overPlan = !resp['privateAllowed']; - }); - }; - - // Monitor any user changes and place the current user into the scope. - UserService.updateUserIn($scope, checkOverPlan); + controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) { + $scope.notificationService = NotificationService; + + // Monitor any user changes and place the current user into the scope. + UserService.updateUserIn($scope); - // Monitor any plan changes. - PlanService.registerListener(this, checkOverPlan); - $scope.signout = function() { ApiService.logout().then(function() { UserService.load(); diff --git a/static/lib/angular-motion.min.css b/static/lib/angular-motion.min.css new file mode 100644 index 000000000..ed4b8444d --- /dev/null +++ b/static/lib/angular-motion.min.css @@ -0,0 +1,8 @@ +/** + * angular-motion + * @version v0.3.2 - 2014-02-11 + * @link https://github.com/mgcrea/angular-motion + * @author Olivier Louvignes + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +.am-fade-and-scale{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-fade-and-scale.ng-enter,.am-fade-and-scale.am-fade-and-scale-add,.am-fade-and-scale.ng-hide-remove,.am-fade-and-scale.ng-move{-webkit-animation-name:fadeAndScaleIn;animation-name:fadeAndScaleIn}.am-fade-and-scale.ng-leave,.am-fade-and-scale.am-fade-and-scale-remove,.am-fade-and-scale.ng-hide{-webkit-animation-name:fadeAndScaleOut;animation-name:fadeAndScaleOut}.am-fade-and-scale.ng-enter{visibility:hidden;-webkit-animation-name:fadeAndScaleIn;animation-name:fadeAndScaleIn}.am-fade-and-scale.ng-enter.ng-enter-active{visibility:visible}.am-fade-and-scale.ng-leave{-webkit-animation-name:fadeAndScaleOut;animation-name:fadeAndScaleOut}@-webkit-keyframes fadeAndScaleIn{from{opacity:0;-webkit-transform:scale(0.7);transform:scale(0.7)}to{opacity:1}}@keyframes fadeAndScaleIn{from{opacity:0;-webkit-transform:scale(0.7);transform:scale(0.7)}to{opacity:1}}@-webkit-keyframes fadeAndScaleOut{from{opacity:1}to{opacity:0;-webkit-transform:scale(0.7);transform:scale(0.7)}}@keyframes fadeAndScaleOut{from{opacity:1}to{opacity:0;-webkit-transform:scale(0.7);transform:scale(0.7)}}.am-fade-and-slide-top{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-fade-and-slide-top.am-fade-and-slide-top-add,.am-fade-and-slide-top.ng-hide-remove,.am-fade-and-slide-top.ng-move{-webkit-animation-name:fadeAndSlideFromTop;animation-name:fadeAndSlideFromTop}.am-fade-and-slide-top.am-fade-and-slide-top-remove,.am-fade-and-slide-top.ng-hide{-webkit-animation-name:fadeAndSlideToTop;animation-name:fadeAndSlideToTop}.am-fade-and-slide-top.ng-enter{visibility:hidden;-webkit-animation-name:fadeAndSlideFromTop;animation-name:fadeAndSlideFromTop}.am-fade-and-slide-top.ng-enter.ng-enter-active{visibility:visible}.am-fade-and-slide-top.ng-leave{-webkit-animation-name:fadeAndSlideToTop;animation-name:fadeAndSlideToTop}.am-fade-and-slide-right{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-fade-and-slide-right.am-fade-and-slide-right-add,.am-fade-and-slide-right.ng-hide-remove,.am-fade-and-slide-right.ng-move{-webkit-animation-name:fadeAndSlideFromRight;animation-name:fadeAndSlideFromRight}.am-fade-and-slide-right.am-fade-and-slide-right-remove,.am-fade-and-slide-right.ng-hide{-webkit-animation-name:fadeAndSlideToRight;animation-name:fadeAndSlideToRight}.am-fade-and-slide-right.ng-enter{visibility:hidden;-webkit-animation-name:fadeAndSlideFromRight;animation-name:fadeAndSlideFromRight}.am-fade-and-slide-right.ng-enter.ng-enter-active{visibility:visible}.am-fade-and-slide-right.ng-leave{-webkit-animation-name:fadeAndSlideToRight;animation-name:fadeAndSlideToRight}.am-fade-and-slide-bottom{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-fade-and-slide-bottom.am-fade-and-slide-bottom-add,.am-fade-and-slide-bottom.ng-hide-remove,.am-fade-and-slide-bottom.ng-move{-webkit-animation-name:fadeAndSlideFromBottom;animation-name:fadeAndSlideFromBottom}.am-fade-and-slide-bottom.am-fade-and-slide-bottom-remove,.am-fade-and-slide-bottom.ng-hide{-webkit-animation-name:fadeAndSlideToBottom;animation-name:fadeAndSlideToBottom}.am-fade-and-slide-bottom.ng-enter{visibility:hidden;-webkit-animation-name:fadeAndSlideFromBottom;animation-name:fadeAndSlideFromBottom}.am-fade-and-slide-bottom.ng-enter.ng-enter-active{visibility:visible}.am-fade-and-slide-bottom.ng-leave{-webkit-animation-name:fadeAndSlideToBottom;animation-name:fadeAndSlideToBottom}.am-fade-and-slide-left{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-fade-and-slide-left.am-fade-and-slide-left-add,.am-fade-and-slide-left.ng-hide-remove,.am-fade-and-slide-left.ng-move{-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;-webkit-animation-name:fadeAndSlideFromLeft;animation-name:fadeAndSlideFromLeft}.am-fade-and-slide-left.am-fade-and-slide-left-remove,.am-fade-and-slide-left.ng-hide{-webkit-animation-name:fadeAndSlideToLeft;animation-name:fadeAndSlideToLeft}.am-fade-and-slide-left.ng-enter{visibility:hidden;-webkit-animation-name:fadeAndSlideFromLeft;animation-name:fadeAndSlideFromLeft}.am-fade-and-slide-left.ng-enter.ng-enter-active{visibility:visible}.am-fade-and-slide-left.ng-leave{-webkit-animation-name:fadeAndSlideToLeft;animation-name:fadeAndSlideToLeft}@-webkit-keyframes fadeAndSlideFromTop{from{opacity:0;-webkit-transform:translateY(-20%);transform:translateY(-20%)}to{opacity:1}}@keyframes fadeAndSlideFromTop{from{opacity:0;-webkit-transform:translateY(-20%);transform:translateY(-20%)}to{opacity:1}}@-webkit-keyframes fadeAndSlideToTop{from{opacity:1}to{opacity:0;-webkit-transform:translateY(-20%);transform:translateY(-20%)}}@keyframes fadeAndSlideToTop{from{opacity:1}to{opacity:0;-webkit-transform:translateY(-20%);transform:translateY(-20%)}}@-webkit-keyframes fadeAndSlideFromRight{from{opacity:0;-webkit-transform:translateX(20%);transform:translateX(20%)}to{opacity:1}}@keyframes fadeAndSlideFromRight{from{opacity:0;-webkit-transform:translateX(20%);transform:translateX(20%)}to{opacity:1}}@-webkit-keyframes fadeAndSlideToRight{from{opacity:1}to{opacity:0;-webkit-transform:translateX(20%);transform:translateX(20%)}}@keyframes fadeAndSlideToRight{from{opacity:1}to{opacity:0;-webkit-transform:translateX(20%);transform:translateX(20%)}}@-webkit-keyframes fadeAndSlideFromBottom{from{opacity:0;-webkit-transform:translateY(20%);transform:translateY(20%)}to{opacity:1}}@keyframes fadeAndSlideFromBottom{from{opacity:0;-webkit-transform:translateY(20%);transform:translateY(20%)}to{opacity:1}}@-webkit-keyframes fadeAndSlideToBottom{from{opacity:1}to{opacity:0;-webkit-transform:translateY(20%);transform:translateY(20%)}}@keyframes fadeAndSlideToBottom{from{opacity:1}to{opacity:0;-webkit-transform:translateY(20%);transform:translateY(20%)}}@-webkit-keyframes fadeAndSlideFromLeft{from{opacity:0;-webkit-transform:translateX(-20%);transform:translateX(-20%)}to{opacity:1}}@keyframes fadeAndSlideFromLeft{from{opacity:0;-webkit-transform:translateX(-20%);transform:translateX(-20%)}to{opacity:1}}@-webkit-keyframes fadeAndSlideToLeft{from{opacity:1}to{opacity:0;-webkit-transform:translateX(-20%);transform:translateX(-20%)}}@keyframes fadeAndSlideToLeft{from{opacity:1}to{opacity:0;-webkit-transform:translateX(-20%);transform:translateX(-20%)}}.am-fade{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;opacity:1}.am-fade.am-fade-add,.am-fade.ng-hide-remove,.am-fade.ng-move{-webkit-animation-name:fadeIn;animation-name:fadeIn}.am-fade.am-fade-remove,.am-fade.ng-hide{-webkit-animation-name:fadeOut;animation-name:fadeOut}.am-fade.ng-enter{visibility:hidden;-webkit-animation-name:fadeIn;animation-name:fadeIn}.am-fade.ng-enter.ng-enter-active{visibility:visible}.am-fade.ng-leave{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@-webkit-keyframes fadeOut{from{opacity:1}to{opacity:0}}@keyframes fadeOut{from{opacity:1}to{opacity:0}}.modal-backdrop.am-fade,.aside-backdrop.am-fade{background:rgba(0,0,0,.5);-webkit-animation-duration:.15s;animation-duration:.15s}.am-flip-x{-webkit-animation-duration:.4s;animation-duration:.4s;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-flip-x.am-flip-x-add,.am-flip-x.ng-hide-remove,.am-flip-x.ng-move{-webkit-animation-name:flipInXBounce;animation-name:flipInXBounce}.am-flip-x.am-flip-x-remove,.am-flip-x.ng-hide{-webkit-animation-name:flipOutX;animation-name:flipOutX}.am-flip-x.ng-enter{visibility:hidden;-webkit-animation-name:flipInXBounce;animation-name:flipInXBounce}.am-flip-x.ng-enter.ng-enter-active{visibility:visible}.am-flip-x.ng-leave{-webkit-animation-name:flipOutX;animation-name:flipOutX}.am-flip-x-linear{-webkit-animation-duration:.4s;animation-duration:.4s;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-flip-x-linear.am-flip-x-add,.am-flip-x-linear.ng-hide-remove,.am-flip-x-linear.ng-move{-webkit-animation-name:flipInX;animation-name:flipInX}.am-flip-x-linear.am-flip-x-remove,.am-flip-x-linear.ng-hide{-webkit-animation-name:flipOutX;animation-name:flipOutX}.am-flip-x-linear.ng-enter{visibility:hidden;-webkit-animation-name:flipInX;animation-name:flipInX}.am-flip-x-linear.ng-enter.ng-enter-active{visibility:visible}.am-flip-x-linear.ng-leave{-webkit-animation-name:flipOutX;animation-name:flipOutX}@-webkit-keyframes flipInX{from{opacity:0;-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg)}to{opacity:1;-webkit-transform:perspective(400px) rotateX(0deg);transform:perspective(400px) rotateX(0deg)}}@keyframes flipInX{from{opacity:0;-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg)}to{opacity:1;-webkit-transform:perspective(400px) rotateX(0deg);transform:perspective(400px) rotateX(0deg)}}@-webkit-keyframes flipInXBounce{from{opacity:0;-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg)}40%{-webkit-transform:perspective(400px) rotateX(-10deg);transform:perspective(400px) rotateX(-10deg)}70%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg)}to{opacity:1;-webkit-transform:perspective(400px) rotateX(0deg);transform:perspective(400px) rotateX(0deg)}}@keyframes flipInXBounce{from{opacity:0;-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg)}40%{-webkit-transform:perspective(400px) rotateX(-10deg);transform:perspective(400px) rotateX(-10deg)}70%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg)}to{opacity:1;-webkit-transform:perspective(400px) rotateX(0deg);transform:perspective(400px) rotateX(0deg)}}@-webkit-keyframes flipOutX{from{opacity:1;-webkit-transform:perspective(400px) rotateX(0deg);transform:perspective(400px) rotateX(0deg)}to{opacity:0;-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg)}}@keyframes flipOutX{from{opacity:1;-webkit-transform:perspective(400px) rotateX(0deg);transform:perspective(400px) rotateX(0deg)}to{opacity:0;-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg)}}.am-slide-top{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-slide-top.am-fade-and-slide-top-add,.am-slide-top.ng-hide-remove,.am-slide-top.ng-move{-webkit-animation-name:slideFromTop;animation-name:slideFromTop}.am-slide-top.am-fade-and-slide-top-remove,.am-slide-top.ng-hide{-webkit-animation-name:slideToTop;animation-name:slideToTop}.am-slide-top.ng-enter{visibility:hidden;-webkit-animation-name:slideFromTop;animation-name:slideFromTop}.am-slide-top.ng-enter.ng-enter-active{visibility:visible}.am-slide-top.ng-leave{-webkit-animation-name:slideToTop;animation-name:slideToTop}.am-slide-right{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-slide-right.am-fade-and-slide-right-add,.am-slide-right.ng-hide-remove,.am-slide-right.ng-move{-webkit-animation-name:slideFromRight;animation-name:slideFromRight}.am-slide-right.am-fade-and-slide-right-remove,.am-slide-right.ng-hide{-webkit-animation-name:slideToRight;animation-name:slideToRight}.am-slide-right.ng-enter{visibility:hidden;-webkit-animation-name:slideFromRight;animation-name:slideFromRight}.am-slide-right.ng-enter.ng-enter-active{visibility:visible}.am-slide-right.ng-leave{-webkit-animation-name:slideToRight;animation-name:slideToRight}.am-slide-bottom{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-slide-bottom.am-fade-and-slide-bottom-add,.am-slide-bottom.ng-hide-remove,.am-slide-bottom.ng-move{-webkit-animation-name:slideFromBottom;animation-name:slideFromBottom}.am-slide-bottom.am-fade-and-slide-bottom-remove,.am-slide-bottom.ng-hide{-webkit-animation-name:slideToBottom;animation-name:slideToBottom}.am-slide-bottom.ng-enter{visibility:hidden;-webkit-animation-name:slideFromBottom;animation-name:slideFromBottom}.am-slide-bottom.ng-enter.ng-enter-active{visibility:visible}.am-slide-bottom.ng-leave{-webkit-animation-name:slideToBottom;animation-name:slideToBottom}.am-slide-left{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards}.am-slide-left.am-fade-and-slide-left-add,.am-slide-left.ng-hide-remove,.am-slide-left.ng-move{-webkit-animation-name:slideFromLeft;animation-name:slideFromLeft}.am-slide-left.am-fade-and-slide-left-remove,.am-slide-left.ng-hide{-webkit-animation-name:slideToLeft;animation-name:slideToLeft}.am-slide-left.ng-enter{visibility:hidden;-webkit-animation-name:slideFromLeft;animation-name:slideFromLeft}.am-slide-left.ng-enter.ng-enter-active{visibility:visible}.am-slide-left.ng-leave{-webkit-animation-name:slideToLeft;animation-name:slideToLeft}@-webkit-keyframes slideFromTop{from{-webkit-transform:translateY(-100%);transform:translateY(-100%)}}@keyframes slideFromTop{from{-webkit-transform:translateY(-100%);transform:translateY(-100%)}}@-webkit-keyframes slideToTop{to{-webkit-transform:translateY(-100%);transform:translateY(-100%)}}@keyframes slideToTop{to{-webkit-transform:translateY(-100%);transform:translateY(-100%)}}@-webkit-keyframes slideFromRight{from{-webkit-transform:translateX(100%);transform:translateX(100%)}}@keyframes slideFromRight{from{-webkit-transform:translateX(100%);transform:translateX(100%)}}@-webkit-keyframes slideToRight{to{-webkit-transform:translateX(100%);transform:translateX(100%)}}@keyframes slideToRight{to{-webkit-transform:translateX(100%);transform:translateX(100%)}}@-webkit-keyframes slideFromBottom{from{-webkit-transform:translateY(100%);transform:translateY(100%)}}@keyframes slideFromBottom{from{-webkit-transform:translateY(100%);transform:translateY(100%)}}@-webkit-keyframes slideToBottom{to{-webkit-transform:translateY(100%);transform:translateY(100%)}}@keyframes slideToBottom{to{-webkit-transform:translateY(100%);transform:translateY(100%)}}@-webkit-keyframes slideFromLeft{from{-webkit-transform:translateX(-100%);transform:translateX(-100%)}}@keyframes slideFromLeft{from{-webkit-transform:translateX(-100%);transform:translateX(-100%)}}@-webkit-keyframes slideToLeft{to{-webkit-transform:translateX(-100%);transform:translateX(-100%)}}@keyframes slideToLeft{to{-webkit-transform:translateX(-100%);transform:translateX(-100%)}} \ No newline at end of file diff --git a/static/lib/angular-strap.js b/static/lib/angular-strap.js new file mode 100644 index 000000000..44d86bac9 --- /dev/null +++ b/static/lib/angular-strap.js @@ -0,0 +1,3543 @@ +/** + * angular-strap + * @version v2.0.0-rc.4 - 2014-03-07 + * @link http://mgcrea.github.io/angular-strap + * @author Olivier Louvignes (olivier@mg-crea.com) + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +(function (window, document, undefined) { + 'use strict'; + // Source: src/module.js + angular.module('mgcrea.ngStrap', [ + 'mgcrea.ngStrap.modal', + 'mgcrea.ngStrap.aside', + 'mgcrea.ngStrap.alert', + 'mgcrea.ngStrap.button', + 'mgcrea.ngStrap.select', + 'mgcrea.ngStrap.datepicker', + 'mgcrea.ngStrap.timepicker', + 'mgcrea.ngStrap.navbar', + 'mgcrea.ngStrap.tooltip', + 'mgcrea.ngStrap.popover', + 'mgcrea.ngStrap.dropdown', + 'mgcrea.ngStrap.typeahead', + 'mgcrea.ngStrap.scrollspy', + 'mgcrea.ngStrap.affix', + 'mgcrea.ngStrap.tab' + ]); + // Source: src/affix/affix.js + angular.module('mgcrea.ngStrap.affix', ['mgcrea.ngStrap.helpers.dimensions']).provider('$affix', function () { + var defaults = this.defaults = { offsetTop: 'auto' }; + this.$get = [ + '$window', + 'dimensions', + function ($window, dimensions) { + var bodyEl = angular.element($window.document.body); + function AffixFactory(element, config) { + var $affix = {}; + // Common vars + var options = angular.extend({}, defaults, config); + var targetEl = options.target; + // Initial private vars + var reset = 'affix affix-top affix-bottom', initialAffixTop = 0, initialOffsetTop = 0, affixed = null, unpin = null; + var parent = element.parent(); + // Options: custom parent + if (options.offsetParent) { + if (options.offsetParent.match(/^\d+$/)) { + for (var i = 0; i < options.offsetParent * 1 - 1; i++) { + parent = parent.parent(); + } + } else { + parent = angular.element(options.offsetParent); + } + } + // Options: offsets + var offsetTop = 0; + if (options.offsetTop) { + if (options.offsetTop === 'auto') { + options.offsetTop = '+0'; + } + if (options.offsetTop.match(/^[-+]\d+$/)) { + initialAffixTop -= options.offsetTop * 1; + if (options.offsetParent) { + offsetTop = dimensions.offset(parent[0]).top + options.offsetTop * 1; + } else { + offsetTop = dimensions.offset(element[0]).top - dimensions.css(element[0], 'marginTop', true) + options.offsetTop * 1; + } + } else { + offsetTop = options.offsetTop * 1; + } + } + var offsetBottom = 0; + if (options.offsetBottom) { + if (options.offsetParent && options.offsetBottom.match(/^[-+]\d+$/)) { + // add 1 pixel due to rounding problems... + offsetBottom = getScrollHeight() - (dimensions.offset(parent[0]).top + dimensions.height(parent[0])) + options.offsetBottom * 1 + 1; + } else { + offsetBottom = options.offsetBottom * 1; + } + } + $affix.init = function () { + initialOffsetTop = dimensions.offset(element[0]).top + initialAffixTop; + // Bind events + targetEl.on('scroll', this.checkPosition); + targetEl.on('click', this.checkPositionWithEventLoop); + // Both of these checkPosition() calls are necessary for the case where + // the user hits refresh after scrolling to the bottom of the page. + this.checkPosition(); + this.checkPositionWithEventLoop(); + }; + $affix.destroy = function () { + // Unbind events + targetEl.off('scroll', this.checkPosition); + targetEl.off('click', this.checkPositionWithEventLoop); + }; + $affix.checkPositionWithEventLoop = function () { + setTimeout(this.checkPosition, 1); + }; + $affix.checkPosition = function () { + // if (!this.$element.is(':visible')) return + var scrollTop = getScrollTop(); + var position = dimensions.offset(element[0]); + var elementHeight = dimensions.height(element[0]); + // Get required affix class according to position + var affix = getRequiredAffixClass(unpin, position, elementHeight); + // Did affix status changed this last check? + if (affixed === affix) + return; + affixed = affix; + // Add proper affix class + element.removeClass(reset).addClass('affix' + (affix !== 'middle' ? '-' + affix : '')); + if (affix === 'top') { + unpin = null; + element.css('position', options.offsetParent ? '' : 'relative'); + element.css('top', ''); + } else if (affix === 'bottom') { + if (options.offsetUnpin) { + unpin = -(options.offsetUnpin * 1); + } else { + // Calculate unpin threshold when affixed to bottom. + // Hopefully the browser scrolls pixel by pixel. + unpin = position.top - scrollTop; + } + element.css('position', options.offsetParent ? '' : 'relative'); + element.css('top', options.offsetParent ? '' : bodyEl[0].offsetHeight - offsetBottom - elementHeight - initialOffsetTop + 'px'); + } else { + // affix === 'middle' + unpin = null; + element.css('position', 'fixed'); + element.css('top', initialAffixTop + 'px'); + } + }; + // Private methods + function getRequiredAffixClass(unpin, position, elementHeight) { + var scrollTop = getScrollTop(); + var scrollHeight = getScrollHeight(); + if (scrollTop <= offsetTop) { + return 'top'; + } else if (unpin !== null && scrollTop + unpin <= position.top) { + return 'middle'; + } else if (offsetBottom !== null && position.top + elementHeight + initialAffixTop >= scrollHeight - offsetBottom) { + return 'bottom'; + } else { + return 'middle'; + } + } + function getScrollTop() { + return targetEl[0] === $window ? $window.pageYOffset : targetEl[0] === $window; + } + function getScrollHeight() { + return targetEl[0] === $window ? $window.document.body.scrollHeight : targetEl[0].scrollHeight; + } + $affix.init(); + return $affix; + } + return AffixFactory; + } + ]; + }).directive('bsAffix', [ + '$affix', + '$window', + function ($affix, $window) { + return { + restrict: 'EAC', + require: '^?bsAffixTarget', + link: function postLink(scope, element, attr, affixTarget) { + var options = { + scope: scope, + offsetTop: 'auto', + target: affixTarget ? affixTarget.$element : angular.element($window) + }; + angular.forEach([ + 'offsetTop', + 'offsetBottom', + 'offsetParent', + 'offsetUnpin' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + var affix = $affix(element, options); + scope.$on('$destroy', function () { + options = null; + affix = null; + }); + } + }; + } + ]).directive('bsAffixTarget', function () { + return { + controller: [ + '$element', + function ($element) { + this.$element = $element; + } + ] + }; + }); + // Source: src/alert/alert.js + // @BUG: following snippet won't compile correctly + // @TODO: submit issue to core + // ' ' + + angular.module('mgcrea.ngStrap.alert', []).provider('$alert', function () { + var defaults = this.defaults = { + animation: 'am-fade', + prefixClass: 'alert', + placement: null, + template: 'alert/alert.tpl.html', + container: false, + element: null, + backdrop: false, + keyboard: true, + show: true, + duration: false, + type: false + }; + this.$get = [ + '$modal', + '$timeout', + function ($modal, $timeout) { + function AlertFactory(config) { + var $alert = {}; + // Common vars + var options = angular.extend({}, defaults, config); + $alert = $modal(options); + // Support scope as string options [/*title, content, */type] + if (options.type) { + $alert.$scope.type = options.type; + } + // Support auto-close duration + var show = $alert.show; + if (options.duration) { + $alert.show = function () { + show(); + $timeout(function () { + $alert.hide(); + }, options.duration * 1000); + }; + } + return $alert; + } + return AlertFactory; + } + ]; + }).directive('bsAlert', [ + '$window', + '$location', + '$sce', + '$alert', + function ($window, $location, $sce, $alert) { + var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; + return { + restrict: 'EAC', + scope: true, + link: function postLink(scope, element, attr, transclusion) { + // Directive options + var options = { + scope: scope, + element: element, + show: false + }; + angular.forEach([ + 'template', + 'placement', + 'keyboard', + 'html', + 'container', + 'animation', + 'duration' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Support scope as data-attrs + angular.forEach([ + 'title', + 'content', + 'type' + ], function (key) { + attr[key] && attr.$observe(key, function (newValue, oldValue) { + scope[key] = $sce.trustAsHtml(newValue); + }); + }); + // Support scope as an object + attr.bsAlert && scope.$watch(attr.bsAlert, function (newValue, oldValue) { + if (angular.isObject(newValue)) { + angular.extend(scope, newValue); + } else { + scope.content = newValue; + } + }, true); + // Initialize alert + var alert = $alert(options); + // Trigger + element.on(attr.trigger || 'click', alert.toggle); + // Garbage collection + scope.$on('$destroy', function () { + alert.destroy(); + options = null; + alert = null; + }); + } + }; + } + ]); + // Source: src/aside/aside.js + angular.module('mgcrea.ngStrap.aside', ['mgcrea.ngStrap.modal']).provider('$aside', function () { + var defaults = this.defaults = { + animation: 'am-fade-and-slide-right', + prefixClass: 'aside', + placement: 'right', + template: 'aside/aside.tpl.html', + contentTemplate: false, + container: false, + element: null, + backdrop: true, + keyboard: true, + html: false, + show: true + }; + this.$get = [ + '$modal', + function ($modal) { + function AsideFactory(config) { + var $aside = {}; + // Common vars + var options = angular.extend({}, defaults, config); + $aside = $modal(options); + return $aside; + } + return AsideFactory; + } + ]; + }).directive('bsAside', [ + '$window', + '$location', + '$sce', + '$aside', + function ($window, $location, $sce, $aside) { + var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; + return { + restrict: 'EAC', + scope: true, + link: function postLink(scope, element, attr, transclusion) { + // Directive options + var options = { + scope: scope, + element: element, + show: false + }; + angular.forEach([ + 'template', + 'contentTemplate', + 'placement', + 'backdrop', + 'keyboard', + 'html', + 'container', + 'animation' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Support scope as data-attrs + angular.forEach([ + 'title', + 'content' + ], function (key) { + attr[key] && attr.$observe(key, function (newValue, oldValue) { + scope[key] = $sce.trustAsHtml(newValue); + }); + }); + // Support scope as an object + attr.bsAside && scope.$watch(attr.bsAside, function (newValue, oldValue) { + if (angular.isObject(newValue)) { + angular.extend(scope, newValue); + } else { + scope.content = newValue; + } + }, true); + // Initialize aside + var aside = $aside(options); + // Trigger + element.on(attr.trigger || 'click', aside.toggle); + // Garbage collection + scope.$on('$destroy', function () { + aside.destroy(); + options = null; + aside = null; + }); + } + }; + } + ]); + // Source: src/button/button.js + angular.module('mgcrea.ngStrap.button', ['ngAnimate']).provider('$button', function () { + var defaults = this.defaults = { + activeClass: 'active', + toggleEvent: 'click' + }; + this.$get = function () { + return { defaults: defaults }; + }; + }).directive('bsCheckboxGroup', function () { + return { + restrict: 'A', + require: 'ngModel', + compile: function postLink(element, attr) { + element.attr('data-toggle', 'buttons'); + element.removeAttr('ng-model'); + var children = element[0].querySelectorAll('input[type="checkbox"]'); + angular.forEach(children, function (child) { + var childEl = angular.element(child); + childEl.attr('bs-checkbox', ''); + childEl.attr('ng-model', attr.ngModel + '.' + childEl.attr('value')); + }); + } + }; + }).directive('bsCheckbox', [ + '$button', + '$$animateReflow', + function ($button, $$animateReflow) { + var defaults = $button.defaults; + var constantValueRegExp = /^(true|false|\d+)$/; + return { + restrict: 'A', + require: 'ngModel', + link: function postLink(scope, element, attr, controller) { + var options = defaults; + // Support label > input[type="checkbox"] + var isInput = element[0].nodeName === 'INPUT'; + var activeElement = isInput ? element.parent() : element; + var trueValue = angular.isDefined(attr.trueValue) ? attr.trueValue : true; + if (constantValueRegExp.test(attr.trueValue)) { + trueValue = scope.$eval(attr.trueValue); + } + var falseValue = angular.isDefined(attr.falseValue) ? attr.falseValue : false; + if (constantValueRegExp.test(attr.falseValue)) { + falseValue = scope.$eval(attr.falseValue); + } + // Parse exotic values + var hasExoticValues = typeof trueValue !== 'boolean' || typeof falseValue !== 'boolean'; + if (hasExoticValues) { + controller.$parsers.push(function (viewValue) { + // console.warn('$parser', element.attr('ng-model'), 'viewValue', viewValue); + return viewValue ? trueValue : falseValue; + }); + // Fix rendering for exotic values + scope.$watch(attr.ngModel, function (newValue, oldValue) { + controller.$render(); + }); + } + // model -> view + controller.$render = function () { + // console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue); + var isActive = angular.equals(controller.$modelValue, trueValue); + $$animateReflow(function () { + if (isInput) + element[0].checked = isActive; + activeElement.toggleClass(options.activeClass, isActive); + }); + }; + // view -> model + element.bind(options.toggleEvent, function () { + scope.$apply(function () { + // console.warn('!click', element.attr('ng-model'), 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue, 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue); + if (!isInput) { + controller.$setViewValue(!activeElement.hasClass('active')); + } + if (!hasExoticValues) { + controller.$render(); + } + }); + }); + } + }; + } + ]).directive('bsRadioGroup', function () { + return { + restrict: 'A', + require: 'ngModel', + compile: function postLink(element, attr) { + element.attr('data-toggle', 'buttons'); + element.removeAttr('ng-model'); + var children = element[0].querySelectorAll('input[type="radio"]'); + angular.forEach(children, function (child) { + angular.element(child).attr('bs-radio', ''); + angular.element(child).attr('ng-model', attr.ngModel); + }); + } + }; + }).directive('bsRadio', [ + '$button', + '$$animateReflow', + function ($button, $$animateReflow) { + var defaults = $button.defaults; + var constantValueRegExp = /^(true|false|\d+)$/; + return { + restrict: 'A', + require: 'ngModel', + link: function postLink(scope, element, attr, controller) { + var options = defaults; + // Support `label > input[type="radio"]` markup + var isInput = element[0].nodeName === 'INPUT'; + var activeElement = isInput ? element.parent() : element; + var value = constantValueRegExp.test(attr.value) ? scope.$eval(attr.value) : attr.value; + // model -> view + controller.$render = function () { + // console.warn('$render', element.attr('value'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue); + var isActive = angular.equals(controller.$modelValue, value); + $$animateReflow(function () { + if (isInput) + element[0].checked = isActive; + activeElement.toggleClass(options.activeClass, isActive); + }); + }; + // view -> model + element.bind(options.toggleEvent, function () { + scope.$apply(function () { + // console.warn('!click', element.attr('value'), 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue, 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue); + controller.$setViewValue(value); + controller.$render(); + }); + }); + } + }; + } + ]); + // Source: src/datepicker/datepicker.js + angular.module('mgcrea.ngStrap.datepicker', [ + 'mgcrea.ngStrap.helpers.dateParser', + 'mgcrea.ngStrap.tooltip' + ]).provider('$datepicker', function () { + var defaults = this.defaults = { + animation: 'am-fade', + prefixClass: 'datepicker', + placement: 'bottom-left', + template: 'datepicker/datepicker.tpl.html', + trigger: 'focus', + container: false, + keyboard: true, + html: false, + delay: 0, + useNative: false, + dateType: 'date', + dateFormat: 'shortDate', + strictFormat: false, + autoclose: false, + minDate: -Infinity, + maxDate: +Infinity, + startView: 0, + minView: 0, + startWeek: 0 + }; + this.$get = [ + '$window', + '$document', + '$rootScope', + '$sce', + '$locale', + 'dateFilter', + 'datepickerViews', + '$tooltip', + function ($window, $document, $rootScope, $sce, $locale, dateFilter, datepickerViews, $tooltip) { + var bodyEl = angular.element($window.document.body); + var isTouch = 'createTouch' in $window.document; + var isNative = /(ip(a|o)d|iphone|android)/gi.test($window.navigator.userAgent); + if (!defaults.lang) + defaults.lang = $locale.id; + function DatepickerFactory(element, controller, config) { + var $datepicker = $tooltip(element, angular.extend({}, defaults, config)); + var parentScope = config.scope; + var options = $datepicker.$options; + var scope = $datepicker.$scope; + if (options.startView) + options.startView -= options.minView; + // View vars + var pickerViews = datepickerViews($datepicker); + $datepicker.$views = pickerViews.views; + var viewDate = pickerViews.viewDate; + scope.$mode = options.startView; + var $picker = $datepicker.$views[scope.$mode]; + // Scope methods + scope.$select = function (date) { + $datepicker.select(date); + }; + scope.$selectPane = function (value) { + $datepicker.$selectPane(value); + }; + scope.$toggleMode = function () { + $datepicker.setMode((scope.$mode + 1) % $datepicker.$views.length); + }; + // Public methods + $datepicker.update = function (date) { + // console.warn('$datepicker.update() newValue=%o', date); + if (angular.isDate(date) && !isNaN(date.getTime())) { + $datepicker.$date = date; + $picker.update.call($picker, date); + } + // Build only if pristine + $datepicker.$build(true); + }; + $datepicker.select = function (date, keep) { + // console.warn('$datepicker.select', date, scope.$mode); + if (!angular.isDate(controller.$dateValue)) + controller.$dateValue = new Date(date); + controller.$dateValue.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); + if (!scope.$mode || keep) { + controller.$setViewValue(controller.$dateValue); + controller.$render(); + if (options.autoclose && !keep) { + $datepicker.hide(true); + } + } else { + angular.extend(viewDate, { + year: date.getFullYear(), + month: date.getMonth(), + date: date.getDate() + }); + $datepicker.setMode(scope.$mode - 1); + $datepicker.$build(); + } + }; + $datepicker.setMode = function (mode) { + // console.warn('$datepicker.setMode', mode); + scope.$mode = mode; + $picker = $datepicker.$views[scope.$mode]; + $datepicker.$build(); + }; + // Protected methods + $datepicker.$build = function (pristine) { + // console.warn('$datepicker.$build() viewDate=%o', viewDate); + if (pristine === true && $picker.built) + return; + if (pristine === false && !$picker.built) + return; + $picker.build.call($picker); + }; + $datepicker.$updateSelected = function () { + for (var i = 0, l = scope.rows.length; i < l; i++) { + angular.forEach(scope.rows[i], updateSelected); + } + }; + $datepicker.$isSelected = function (date) { + return $picker.isSelected(date); + }; + $datepicker.$selectPane = function (value) { + var steps = $picker.steps; + var targetDate = new Date(Date.UTC(viewDate.year + (steps.year || 0) * value, viewDate.month + (steps.month || 0) * value, viewDate.date + (steps.day || 0) * value)); + angular.extend(viewDate, { + year: targetDate.getUTCFullYear(), + month: targetDate.getUTCMonth(), + date: targetDate.getUTCDate() + }); + $datepicker.$build(); + }; + $datepicker.$onMouseDown = function (evt) { + // Prevent blur on mousedown on .dropdown-menu + evt.preventDefault(); + evt.stopPropagation(); + // Emulate click for mobile devices + if (isTouch) { + var targetEl = angular.element(evt.target); + if (targetEl[0].nodeName.toLowerCase() !== 'button') { + targetEl = targetEl.parent(); + } + targetEl.triggerHandler('click'); + } + }; + $datepicker.$onKeyDown = function (evt) { + if (!/(38|37|39|40|13)/.test(evt.keyCode) || evt.shiftKey || evt.altKey) + return; + evt.preventDefault(); + evt.stopPropagation(); + if (evt.keyCode === 13) { + if (!scope.$mode) { + return $datepicker.hide(true); + } else { + return scope.$apply(function () { + $datepicker.setMode(scope.$mode - 1); + }); + } + } + // Navigate with keyboard + $picker.onKeyDown(evt); + parentScope.$digest(); + }; + // Private + function updateSelected(el) { + el.selected = $datepicker.$isSelected(el.date); + } + function focusElement() { + element[0].focus(); + } + // Overrides + var _init = $datepicker.init; + $datepicker.init = function () { + if (isNative && options.useNative) { + element.prop('type', 'date'); + element.css('-webkit-appearance', 'textfield'); + return; + } else if (isTouch) { + element.prop('type', 'text'); + element.attr('readonly', 'true'); + element.on('click', focusElement); + } + _init(); + }; + var _destroy = $datepicker.destroy; + $datepicker.destroy = function () { + if (isNative && options.useNative) { + element.off('click', focusElement); + } + _destroy(); + }; + var _show = $datepicker.show; + $datepicker.show = function () { + _show(); + setTimeout(function () { + $datepicker.$element.on(isTouch ? 'touchstart' : 'mousedown', $datepicker.$onMouseDown); + if (options.keyboard) { + element.on('keydown', $datepicker.$onKeyDown); + } + }); + }; + var _hide = $datepicker.hide; + $datepicker.hide = function (blur) { + $datepicker.$element.off(isTouch ? 'touchstart' : 'mousedown', $datepicker.$onMouseDown); + if (options.keyboard) { + element.off('keydown', $datepicker.$onKeyDown); + } + _hide(blur); + }; + return $datepicker; + } + DatepickerFactory.defaults = defaults; + return DatepickerFactory; + } + ]; + }).directive('bsDatepicker', [ + '$window', + '$parse', + '$q', + '$locale', + 'dateFilter', + '$datepicker', + '$dateParser', + '$timeout', + function ($window, $parse, $q, $locale, dateFilter, $datepicker, $dateParser, $timeout) { + var defaults = $datepicker.defaults; + var isNative = /(ip(a|o)d|iphone|android)/gi.test($window.navigator.userAgent); + var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; + return { + restrict: 'EAC', + require: 'ngModel', + link: function postLink(scope, element, attr, controller) { + // Directive options + var options = { + scope: scope, + controller: controller + }; + angular.forEach([ + 'placement', + 'container', + 'delay', + 'trigger', + 'keyboard', + 'html', + 'animation', + 'template', + 'autoclose', + 'dateType', + 'dateFormat', + 'strictFormat', + 'startWeek', + 'useNative', + 'lang', + 'startView', + 'minView' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Initialize datepicker + if (isNative && options.useNative) + options.dateFormat = 'yyyy-MM-dd'; + var datepicker = $datepicker(element, controller, options); + options = datepicker.$options; + // Observe attributes for changes + angular.forEach([ + 'minDate', + 'maxDate' + ], function (key) { + // console.warn('attr.$observe(%s)', key, attr[key]); + angular.isDefined(attr[key]) && attr.$observe(key, function (newValue) { + // console.warn('attr.$observe(%s)=%o', key, newValue); + if (newValue === 'today') { + var today = new Date(); + datepicker.$options[key] = +new Date(today.getFullYear(), today.getMonth(), today.getDate() + (key === 'maxDate' ? 1 : 0), 0, 0, 0, key === 'minDate' ? 0 : -1); + } else if (angular.isString(newValue) && newValue.match(/^".+"$/)) { + datepicker.$options[key] = +new Date(newValue.substr(1, newValue.length - 2)); + } else { + datepicker.$options[key] = +new Date(newValue); + } + // console.warn(angular.isDate(newValue), newValue); + // Build only if dirty + !isNaN(datepicker.$options[key]) && datepicker.$build(false); + }); + }); + // Watch model for changes + scope.$watch(attr.ngModel, function (newValue, oldValue) { + datepicker.update(controller.$dateValue); + }, true); + var dateParser = $dateParser({ + format: options.dateFormat, + lang: options.lang, + strict: options.strictFormat + }); + // viewValue -> $parsers -> modelValue + controller.$parsers.unshift(function (viewValue) { + // console.warn('$parser("%s"): viewValue=%o', element.attr('ng-model'), viewValue); + // Null values should correctly reset the model value & validity + if (!viewValue) { + controller.$setValidity('date', true); + return; + } + var parsedDate = dateParser.parse(viewValue, controller.$dateValue); + if (!parsedDate || isNaN(parsedDate.getTime())) { + controller.$setValidity('date', false); + } else { + var isValid = parsedDate.getTime() >= options.minDate && parsedDate.getTime() <= options.maxDate; + controller.$setValidity('date', isValid); + // Only update the model when we have a valid date + if (isValid) + controller.$dateValue = parsedDate; + } + if (options.dateType === 'string') { + return dateFilter(viewValue, options.dateFormat); + } else if (options.dateType === 'number') { + return controller.$dateValue.getTime(); + } else if (options.dateType === 'iso') { + return controller.$dateValue.toISOString(); + } else { + return new Date(controller.$dateValue); + } + }); + // modelValue -> $formatters -> viewValue + controller.$formatters.push(function (modelValue) { + // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue); + if (angular.isUndefined(modelValue) || modelValue === null) + return; + var date = angular.isDate(modelValue) ? modelValue : new Date(modelValue); + // Setup default value? + // if(isNaN(date.getTime())) { + // var today = new Date(); + // date = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0); + // } + controller.$dateValue = date; + return controller.$dateValue; + }); + // viewValue -> element + controller.$render = function () { + // console.warn('$render("%s"): viewValue=%o', element.attr('ng-model'), controller.$viewValue); + element.val(!controller.$dateValue || isNaN(controller.$dateValue.getTime()) ? '' : dateFilter(controller.$dateValue, options.dateFormat)); + }; + // Garbage collection + scope.$on('$destroy', function () { + datepicker.destroy(); + options = null; + datepicker = null; + }); + } + }; + } + ]).provider('datepickerViews', function () { + var defaults = this.defaults = { + dayFormat: 'dd', + daySplit: 7 + }; + // Split array into smaller arrays + function split(arr, size) { + var arrays = []; + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); + } + return arrays; + } + this.$get = [ + '$locale', + '$sce', + 'dateFilter', + function ($locale, $sce, dateFilter) { + return function (picker) { + var scope = picker.$scope; + var options = picker.$options; + var weekDaysMin = $locale.DATETIME_FORMATS.SHORTDAY; + var weekDaysLabels = weekDaysMin.slice(options.startWeek).concat(weekDaysMin.slice(0, options.startWeek)); + var weekDaysLabelsHtml = $sce.trustAsHtml('' + weekDaysLabels.join('') + ''); + var startDate = picker.$date || new Date(); + var viewDate = { + year: startDate.getFullYear(), + month: startDate.getMonth(), + date: startDate.getDate() + }; + var timezoneOffset = startDate.getTimezoneOffset() * 60000; + var views = [ + { + format: 'dd', + split: 7, + steps: { month: 1 }, + update: function (date, force) { + if (!this.built || force || date.getFullYear() !== viewDate.year || date.getMonth() !== viewDate.month) { + angular.extend(viewDate, { + year: picker.$date.getFullYear(), + month: picker.$date.getMonth(), + date: picker.$date.getDate() + }); + picker.$build(); + } else if (date.getDate() !== viewDate.date) { + viewDate.date = picker.$date.getDate(); + picker.$updateSelected(); + } + }, + build: function () { + var firstDayOfMonth = new Date(viewDate.year, viewDate.month, 1); + var firstDate = new Date(+firstDayOfMonth - (firstDayOfMonth.getDay() - options.startWeek) * 86400000); + var days = [], day; + for (var i = 0; i < 42; i++) { + // < 7 * 6 + day = new Date(firstDate.getFullYear(), firstDate.getMonth(), firstDate.getDate() + i); + days.push({ + date: day, + label: dateFilter(day, this.format), + selected: picker.$date && this.isSelected(day), + muted: day.getMonth() !== viewDate.month, + disabled: this.isDisabled(day) + }); + } + scope.title = dateFilter(firstDayOfMonth, 'MMMM yyyy'); + scope.labels = weekDaysLabelsHtml; + scope.rows = split(days, this.split); + this.built = true; + }, + isSelected: function (date) { + return picker.$date && date.getFullYear() === picker.$date.getFullYear() && date.getMonth() === picker.$date.getMonth() && date.getDate() === picker.$date.getDate(); + }, + isDisabled: function (date) { + return date.getTime() < options.minDate || date.getTime() > options.maxDate; + }, + onKeyDown: function (evt) { + var actualTime = picker.$date.getTime(); + if (evt.keyCode === 37) + picker.select(new Date(actualTime - 1 * 86400000), true); + else if (evt.keyCode === 38) + picker.select(new Date(actualTime - 7 * 86400000), true); + else if (evt.keyCode === 39) + picker.select(new Date(actualTime + 1 * 86400000), true); + else if (evt.keyCode === 40) + picker.select(new Date(actualTime + 7 * 86400000), true); + } + }, + { + name: 'month', + format: 'MMM', + split: 4, + steps: { year: 1 }, + update: function (date, force) { + if (!this.built || date.getFullYear() !== viewDate.year) { + angular.extend(viewDate, { + year: picker.$date.getFullYear(), + month: picker.$date.getMonth(), + date: picker.$date.getDate() + }); + picker.$build(); + } else if (date.getMonth() !== viewDate.month) { + angular.extend(viewDate, { + month: picker.$date.getMonth(), + date: picker.$date.getDate() + }); + picker.$updateSelected(); + } + }, + build: function () { + var firstMonth = new Date(viewDate.year, 0, 1); + var months = [], month; + for (var i = 0; i < 12; i++) { + month = new Date(viewDate.year, i, 1); + months.push({ + date: month, + label: dateFilter(month, this.format), + selected: picker.$isSelected(month), + disabled: this.isDisabled(month) + }); + } + scope.title = dateFilter(month, 'yyyy'); + scope.labels = false; + scope.rows = split(months, this.split); + this.built = true; + }, + isSelected: function (date) { + return picker.$date && date.getFullYear() === picker.$date.getFullYear() && date.getMonth() === picker.$date.getMonth(); + }, + isDisabled: function (date) { + var lastDate = +new Date(date.getFullYear(), date.getMonth() + 1, 0); + return lastDate < options.minDate || date.getTime() > options.maxDate; + }, + onKeyDown: function (evt) { + var actualMonth = picker.$date.getMonth(); + if (evt.keyCode === 37) + picker.select(picker.$date.setMonth(actualMonth - 1), true); + else if (evt.keyCode === 38) + picker.select(picker.$date.setMonth(actualMonth - 4), true); + else if (evt.keyCode === 39) + picker.select(picker.$date.setMonth(actualMonth + 1), true); + else if (evt.keyCode === 40) + picker.select(picker.$date.setMonth(actualMonth + 4), true); + } + }, + { + name: 'year', + format: 'yyyy', + split: 4, + steps: { year: 12 }, + update: function (date, force) { + if (!this.built || force || parseInt(date.getFullYear() / 20, 10) !== parseInt(viewDate.year / 20, 10)) { + angular.extend(viewDate, { + year: picker.$date.getFullYear(), + month: picker.$date.getMonth(), + date: picker.$date.getDate() + }); + picker.$build(); + } else if (date.getFullYear() !== viewDate.year) { + angular.extend(viewDate, { + year: picker.$date.getFullYear(), + month: picker.$date.getMonth(), + date: picker.$date.getDate() + }); + picker.$updateSelected(); + } + }, + build: function () { + var firstYear = viewDate.year - viewDate.year % (this.split * 3); + var years = [], year; + for (var i = 0; i < 12; i++) { + year = new Date(firstYear + i, 0, 1); + years.push({ + date: year, + label: dateFilter(year, this.format), + selected: picker.$isSelected(year), + disabled: this.isDisabled(year) + }); + } + scope.title = years[0].label + '-' + years[years.length - 1].label; + scope.labels = false; + scope.rows = split(years, this.split); + this.built = true; + }, + isSelected: function (date) { + return picker.$date && date.getFullYear() === picker.$date.getFullYear(); + }, + isDisabled: function (date) { + var lastDate = +new Date(date.getFullYear() + 1, 0, 0); + return lastDate < options.minDate || date.getTime() > options.maxDate; + }, + onKeyDown: function (evt) { + var actualYear = picker.$date.getFullYear(); + if (evt.keyCode === 37) + picker.select(picker.$date.setYear(actualYear - 1), true); + else if (evt.keyCode === 38) + picker.select(picker.$date.setYear(actualYear - 4), true); + else if (evt.keyCode === 39) + picker.select(picker.$date.setYear(actualYear + 1), true); + else if (evt.keyCode === 40) + picker.select(picker.$date.setYear(actualYear + 4), true); + } + } + ]; + return { + views: options.minView ? Array.prototype.slice.call(views, options.minView) : views, + viewDate: viewDate + }; + }; + } + ]; + }); + // Source: src/dropdown/dropdown.js + angular.module('mgcrea.ngStrap.dropdown', ['mgcrea.ngStrap.tooltip']).provider('$dropdown', function () { + var defaults = this.defaults = { + animation: 'am-fade', + prefixClass: 'dropdown', + placement: 'bottom-left', + template: 'dropdown/dropdown.tpl.html', + trigger: 'click', + container: false, + keyboard: true, + html: false, + delay: 0 + }; + this.$get = [ + '$window', + '$rootScope', + '$tooltip', + function ($window, $rootScope, $tooltip) { + var bodyEl = angular.element($window.document.body); + var matchesSelector = Element.prototype.matchesSelector || Element.prototype.webkitMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector; + function DropdownFactory(element, config) { + var $dropdown = {}; + // Common vars + var options = angular.extend({}, defaults, config); + var scope = $dropdown.$scope = options.scope && options.scope.$new() || $rootScope.$new(); + $dropdown = $tooltip(element, options); + // Protected methods + $dropdown.$onKeyDown = function (evt) { + if (!/(38|40)/.test(evt.keyCode)) + return; + evt.preventDefault(); + evt.stopPropagation(); + // Retrieve focused index + var items = angular.element($dropdown.$element[0].querySelectorAll('li:not(.divider) a')); + if (!items.length) + return; + var index; + angular.forEach(items, function (el, i) { + if (matchesSelector && matchesSelector.call(el, ':focus')) + index = i; + }); + // Navigate with keyboard + if (evt.keyCode === 38 && index > 0) + index--; + else if (evt.keyCode === 40 && index < items.length - 1) + index++; + else if (angular.isUndefined(index)) + index = 0; + items.eq(index)[0].focus(); + }; + // Overrides + var show = $dropdown.show; + $dropdown.show = function () { + show(); + setTimeout(function () { + options.keyboard && $dropdown.$element.on('keydown', $dropdown.$onKeyDown); + bodyEl.on('click', onBodyClick); + }); + }; + var hide = $dropdown.hide; + $dropdown.hide = function () { + options.keyboard && $dropdown.$element.off('keydown', $dropdown.$onKeyDown); + bodyEl.off('click', onBodyClick); + hide(); + }; + // Private functions + function onBodyClick(evt) { + if (evt.target === element[0]) + return; + return evt.target !== element[0] && $dropdown.hide(); + } + return $dropdown; + } + return DropdownFactory; + } + ]; + }).directive('bsDropdown', [ + '$window', + '$location', + '$sce', + '$dropdown', + function ($window, $location, $sce, $dropdown) { + return { + restrict: 'EAC', + scope: true, + link: function postLink(scope, element, attr, transclusion) { + // Directive options + var options = { scope: scope }; + angular.forEach([ + 'placement', + 'container', + 'delay', + 'trigger', + 'keyboard', + 'html', + 'animation', + 'template' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Support scope as an object + attr.bsDropdown && scope.$watch(attr.bsDropdown, function (newValue, oldValue) { + scope.content = newValue; + }, true); + // Initialize dropdown + var dropdown = $dropdown(element, options); + // Garbage collection + scope.$on('$destroy', function () { + dropdown.destroy(); + options = null; + dropdown = null; + }); + } + }; + } + ]); + // Source: src/helpers/date-parser.js + angular.module('mgcrea.ngStrap.helpers.dateParser', []).provider('$dateParser', [ + '$localeProvider', + function ($localeProvider) { + var proto = Date.prototype; + function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + var defaults = this.defaults = { + format: 'shortDate', + strict: false + }; + this.$get = [ + '$locale', + function ($locale) { + var DateParserFactory = function (config) { + var options = angular.extend({}, defaults, config); + var $dateParser = {}; + var regExpMap = { + 'sss': '[0-9]{3}', + 'ss': '[0-5][0-9]', + 's': options.strict ? '[1-5]?[0-9]' : '[0-5][0-9]', + 'mm': '[0-5][0-9]', + 'm': options.strict ? '[1-5]?[0-9]' : '[0-5][0-9]', + 'HH': '[01][0-9]|2[0-3]', + 'H': options.strict ? '[0][1-9]|[1][012]' : '[01][0-9]|2[0-3]', + 'hh': '[0][1-9]|[1][012]', + 'h': options.strict ? '[1-9]|[1][012]' : '[0]?[1-9]|[1][012]', + 'a': 'AM|PM', + 'EEEE': $locale.DATETIME_FORMATS.DAY.join('|'), + 'EEE': $locale.DATETIME_FORMATS.SHORTDAY.join('|'), + 'dd': '[0-2][0-9]{1}|[3][01]{1}', + 'd': options.strict ? '[1-2]?[0-9]{1}|[3][01]{1}' : '[0-2][0-9]{1}|[3][01]{1}', + 'MMMM': $locale.DATETIME_FORMATS.MONTH.join('|'), + 'MMM': $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), + 'MM': '[0][1-9]|[1][012]', + 'M': options.strict ? '[1-9]|[1][012]' : '[0][1-9]|[1][012]', + 'yyyy': '(?:(?:[1]{1}[0-9]{1}[0-9]{1}[0-9]{1})|(?:[2]{1}[0-9]{3}))(?![[0-9]])', + 'yy': '(?:(?:[0-9]{1}[0-9]{1}))(?![[0-9]])' + }; + var setFnMap = { + 'sss': proto.setMilliseconds, + 'ss': proto.setSeconds, + 's': proto.setSeconds, + 'mm': proto.setMinutes, + 'm': proto.setMinutes, + 'HH': proto.setHours, + 'H': proto.setHours, + 'hh': proto.setHours, + 'h': proto.setHours, + 'dd': proto.setDate, + 'd': proto.setDate, + 'a': function (value) { + var hours = this.getHours(); + return this.setHours(value.match(/pm/i) ? hours + 12 : hours); + }, + 'MMMM': function (value) { + return this.setMonth($locale.DATETIME_FORMATS.MONTH.indexOf(value)); + }, + 'MMM': function (value) { + return this.setMonth($locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value)); + }, + 'MM': function (value) { + return this.setMonth(1 * value - 1); + }, + 'M': function (value) { + return this.setMonth(1 * value - 1); + }, + 'yyyy': proto.setFullYear, + 'yy': function (value) { + return this.setFullYear(2000 + 1 * value); + }, + 'y': proto.setFullYear + }; + var regex, setMap; + $dateParser.init = function () { + $dateParser.$format = $locale.DATETIME_FORMATS[options.format] || options.format; + regex = regExpForFormat($dateParser.$format); + setMap = setMapForFormat($dateParser.$format); + }; + $dateParser.isValid = function (date) { + if (angular.isDate(date)) + return !isNaN(date.getTime()); + return regex.test(date); + }; + $dateParser.parse = function (value, baseDate) { + if (angular.isDate(value)) + return value; + var matches = regex.exec(value); + if (!matches) + return false; + var date = baseDate || new Date(0); + for (var i = 0; i < matches.length - 1; i++) { + setMap[i] && setMap[i].call(date, matches[i + 1]); + } + return date; + }; + // Private functions + function setMapForFormat(format) { + var keys = Object.keys(setFnMap), i; + var map = [], sortedMap = []; + // Map to setFn + var clonedFormat = format; + for (i = 0; i < keys.length; i++) { + if (format.split(keys[i]).length > 1) { + var index = clonedFormat.search(keys[i]); + format = format.split(keys[i]).join(''); + if (setFnMap[keys[i]]) + map[index] = setFnMap[keys[i]]; + } + } + // Sort result map + angular.forEach(map, function (v) { + sortedMap.push(v); + }); + return sortedMap; + } + function escapeReservedSymbols(text) { + return text.replace(/\//g, '[\\/]').replace('/-/g', '[-]').replace(/\./g, '[.]').replace(/\\s/g, '[\\s]'); + } + function regExpForFormat(format) { + var keys = Object.keys(regExpMap), i; + var re = format; + // Abstract replaces to avoid collisions + for (i = 0; i < keys.length; i++) { + re = re.split(keys[i]).join('${' + i + '}'); + } + // Replace abstracted values + for (i = 0; i < keys.length; i++) { + re = re.split('${' + i + '}').join('(' + regExpMap[keys[i]] + ')'); + } + format = escapeReservedSymbols(format); + return new RegExp('^' + re + '$', ['i']); + } + $dateParser.init(); + return $dateParser; + }; + return DateParserFactory; + } + ]; + } + ]); + // Source: src/helpers/debounce.js + angular.module('mgcrea.ngStrap.helpers.debounce', []).constant('debounce', function (func, wait, immediate) { + var timeout, args, context, timestamp, result; + return function () { + context = this; + args = arguments; + timestamp = new Date(); + var later = function () { + var last = new Date() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) + result = func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) + result = func.apply(context, args); + return result; + }; + }).constant('throttle', function (func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function () { + previous = options.leading === false ? 0 : new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function () { + var now = new Date(); + if (!previous && options.leading === false) + previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }); + // Source: src/helpers/dimensions.js + angular.module('mgcrea.ngStrap.helpers.dimensions', []).factory('dimensions', [ + '$document', + '$window', + function ($document, $window) { + var jqLite = angular.element; + var fn = {}; + /** + * Test the element nodeName + * @param element + * @param name + */ + var nodeName = fn.nodeName = function (element, name) { + return element.nodeName && element.nodeName.toLowerCase() === name.toLowerCase(); + }; + /** + * Returns the element computed style + * @param element + * @param prop + * @param extra + */ + fn.css = function (element, prop, extra) { + var value; + if (element.currentStyle) { + //IE + value = element.currentStyle[prop]; + } else if (window.getComputedStyle) { + value = window.getComputedStyle(element)[prop]; + } else { + value = element.style[prop]; + } + return extra === true ? parseFloat(value) || 0 : value; + }; + /** + * Provides read-only equivalent of jQuery's offset function: + * @required-by bootstrap-tooltip, bootstrap-affix + * @url http://api.jquery.com/offset/ + * @param element + */ + fn.offset = function (element) { + var boxRect = element.getBoundingClientRect(); + var docElement = element.ownerDocument; + return { + width: element.offsetWidth, + height: element.offsetHeight, + top: boxRect.top + (window.pageYOffset || docElement.documentElement.scrollTop) - (docElement.documentElement.clientTop || 0), + left: boxRect.left + (window.pageXOffset || docElement.documentElement.scrollLeft) - (docElement.documentElement.clientLeft || 0) + }; + }; + /** + * Provides read-only equivalent of jQuery's position function + * @required-by bootstrap-tooltip, bootstrap-affix + * @url http://api.jquery.com/offset/ + * @param element + */ + fn.position = function (element) { + var offsetParentRect = { + top: 0, + left: 0 + }, offsetParentElement, offset; + // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent + if (fn.css(element, 'position') === 'fixed') { + // We assume that getBoundingClientRect is available when computed position is fixed + offset = element.getBoundingClientRect(); + } else { + // Get *real* offsetParentElement + offsetParentElement = offsetParent(element); + offset = fn.offset(element); + // Get correct offsets + offset = fn.offset(element); + if (!nodeName(offsetParentElement, 'html')) { + offsetParentRect = fn.offset(offsetParentElement); + } + // Add offsetParent borders + offsetParentRect.top += fn.css(offsetParentElement, 'borderTopWidth', true); + offsetParentRect.left += fn.css(offsetParentElement, 'borderLeftWidth', true); + } + // Subtract parent offsets and element margins + return { + width: element.offsetWidth, + height: element.offsetHeight, + top: offset.top - offsetParentRect.top - fn.css(element, 'marginTop', true), + left: offset.left - offsetParentRect.left - fn.css(element, 'marginLeft', true) + }; + }; + /** + * Returns the closest, non-statically positioned offsetParent of a given element + * @required-by fn.position + * @param element + */ + var offsetParent = function offsetParentElement(element) { + var docElement = element.ownerDocument; + var offsetParent = element.offsetParent || docElement; + if (nodeName(offsetParent, '#document')) + return docElement.documentElement; + while (offsetParent && !nodeName(offsetParent, 'html') && fn.css(offsetParent, 'position') === 'static') { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docElement.documentElement; + }; + /** + * Provides equivalent of jQuery's height function + * @required-by bootstrap-affix + * @url http://api.jquery.com/height/ + * @param element + * @param outer + */ + fn.height = function (element, outer) { + var value = element.offsetHeight; + if (outer) { + value += fn.css(element, 'marginTop', true) + fn.css(element, 'marginBottom', true); + } else { + value -= fn.css(element, 'paddingTop', true) + fn.css(element, 'paddingBottom', true) + fn.css(element, 'borderTopWidth', true) + fn.css(element, 'borderBottomWidth', true); + } + return value; + }; + /** + * Provides equivalent of jQuery's height function + * @required-by bootstrap-affix + * @url http://api.jquery.com/width/ + * @param element + * @param outer + */ + fn.width = function (element, outer) { + var value = element.offsetWidth; + if (outer) { + value += fn.css(element, 'marginLeft', true) + fn.css(element, 'marginRight', true); + } else { + value -= fn.css(element, 'paddingLeft', true) + fn.css(element, 'paddingRight', true) + fn.css(element, 'borderLeftWidth', true) + fn.css(element, 'borderRightWidth', true); + } + return value; + }; + return fn; + } + ]); + // Source: src/helpers/parse-options.js + angular.module('mgcrea.ngStrap.helpers.parseOptions', []).provider('$parseOptions', function () { + var defaults = this.defaults = { regexp: /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/ }; + this.$get = [ + '$parse', + '$q', + function ($parse, $q) { + function ParseOptionsFactory(attr, config) { + var $parseOptions = {}; + // Common vars + var options = angular.extend({}, defaults, config); + $parseOptions.$values = []; + // Private vars + var match, displayFn, valueName, keyName, groupByFn, valueFn, valuesFn; + $parseOptions.init = function () { + $parseOptions.$match = match = attr.match(options.regexp); + displayFn = $parse(match[2] || match[1]), valueName = match[4] || match[6], keyName = match[5], groupByFn = $parse(match[3] || ''), valueFn = $parse(match[2] ? match[1] : valueName), valuesFn = $parse(match[7]); + }; + $parseOptions.valuesFn = function (scope, controller) { + return $q.when(valuesFn(scope, controller)).then(function (values) { + $parseOptions.$values = values ? parseValues(values) : {}; + return $parseOptions.$values; + }); + }; + // Private functions + function parseValues(values) { + return values.map(function (match, index) { + var locals = {}, label, value; + locals[valueName] = match; + label = displayFn(locals); + value = valueFn(locals) || index; + return { + label: label, + value: value + }; + }); + } + $parseOptions.init(); + return $parseOptions; + } + return ParseOptionsFactory; + } + ]; + }); + // Source: src/modal/modal.js + angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions']).provider('$modal', function () { + var defaults = this.defaults = { + animation: 'am-fade', + backdropAnimation: 'am-fade', + prefixClass: 'modal', + placement: 'top', + template: 'modal/modal.tpl.html', + contentTemplate: false, + container: false, + element: null, + backdrop: true, + keyboard: true, + html: false, + show: true + }; + this.$get = [ + '$window', + '$rootScope', + '$compile', + '$q', + '$templateCache', + '$http', + '$animate', + '$timeout', + 'dimensions', + function ($window, $rootScope, $compile, $q, $templateCache, $http, $animate, $timeout, dimensions) { + var forEach = angular.forEach; + var trim = String.prototype.trim; + var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; + var bodyElement = angular.element($window.document.body); + var htmlReplaceRegExp = /ng-bind="/gi; + function ModalFactory(config) { + var $modal = {}; + // Common vars + var options = angular.extend({}, defaults, config); + $modal.$promise = fetchTemplate(options.template); + var scope = $modal.$scope = options.scope && options.scope.$new() || $rootScope.$new(); + if (!options.element && !options.container) { + options.container = 'body'; + } + // Support scope as string options + forEach([ + 'title', + 'content' + ], function (key) { + if (options[key]) + scope[key] = options[key]; + }); + // Provide scope helpers + scope.$hide = function () { + scope.$$postDigest(function () { + $modal.hide(); + }); + }; + scope.$show = function () { + scope.$$postDigest(function () { + $modal.show(); + }); + }; + scope.$toggle = function () { + scope.$$postDigest(function () { + $modal.toggle(); + }); + }; + // Support contentTemplate option + if (options.contentTemplate) { + $modal.$promise = $modal.$promise.then(function (template) { + var templateEl = angular.element(template); + return fetchTemplate(options.contentTemplate).then(function (contentTemplate) { + var contentEl = findElement('[ng-bind="content"]', templateEl[0]).removeAttr('ng-bind').html(contentTemplate); + // Drop the default footer as you probably don't want it if you use a custom contentTemplate + if (!config.template) + contentEl.next().remove(); + return templateEl[0].outerHTML; + }); + }); + } + // Fetch, compile then initialize modal + var modalLinker, modalElement; + var backdropElement = angular.element('
    '); + $modal.$promise.then(function (template) { + if (angular.isObject(template)) + template = template.data; + if (options.html) + template = template.replace(htmlReplaceRegExp, 'ng-bind-html="'); + template = trim.apply(template); + modalLinker = $compile(template); + $modal.init(); + }); + $modal.init = function () { + // Options: show + if (options.show) { + scope.$$postDigest(function () { + $modal.show(); + }); + } + }; + $modal.destroy = function () { + // Remove element + if (modalElement) { + modalElement.remove(); + modalElement = null; + } + if (backdropElement) { + backdropElement.remove(); + backdropElement = null; + } + // Destroy scope + scope.$destroy(); + }; + $modal.show = function () { + var parent = options.container ? findElement(options.container) : null; + var after = options.container ? null : options.element; + // Fetch a cloned element linked from template + modalElement = $modal.$element = modalLinker(scope, function (clonedElement, scope) { + }); + // Set the initial positioning. + modalElement.css({ display: 'block' }).addClass(options.placement); + // Options: animation + if (options.animation) { + if (options.backdrop) { + backdropElement.addClass(options.backdropAnimation); + } + modalElement.addClass(options.animation); + } + if (options.backdrop) { + $animate.enter(backdropElement, bodyElement, null, function () { + }); + } + $animate.enter(modalElement, parent, after, function () { + }); + scope.$isShown = true; + scope.$$phase || scope.$digest(); + // Focus once the enter-animation has started + // Weird PhantomJS bug hack + var el = modalElement[0]; + requestAnimationFrame(function () { + el.focus(); + }); + bodyElement.addClass(options.prefixClass + '-open'); + if (options.animation) { + bodyElement.addClass(options.prefixClass + '-with-' + options.animation); + } + // Bind events + if (options.backdrop) { + modalElement.on('click', hideOnBackdropClick); + backdropElement.on('click', hideOnBackdropClick); + } + if (options.keyboard) { + modalElement.on('keyup', $modal.$onKeyUp); + } + }; + $modal.hide = function () { + $animate.leave(modalElement, function () { + bodyElement.removeClass(options.prefixClass + '-open'); + if (options.animation) { + bodyElement.addClass(options.prefixClass + '-with-' + options.animation); + } + }); + if (options.backdrop) { + $animate.leave(backdropElement, function () { + }); + } + scope.$isShown = false; + scope.$$phase || scope.$digest(); + // Unbind events + if (options.backdrop) { + modalElement.off('click', hideOnBackdropClick); + backdropElement.off('click', hideOnBackdropClick); + } + if (options.keyboard) { + modalElement.off('keyup', $modal.$onKeyUp); + } + }; + $modal.toggle = function () { + scope.$isShown ? $modal.hide() : $modal.show(); + }; + $modal.focus = function () { + modalElement[0].focus(); + }; + // Protected methods + $modal.$onKeyUp = function (evt) { + evt.which === 27 && $modal.hide(); + }; + // Private methods + function hideOnBackdropClick(evt) { + if (evt.target !== evt.currentTarget) + return; + options.backdrop === 'static' ? $modal.focus() : $modal.hide(); + } + return $modal; + } + // Helper functions + function findElement(query, element) { + return angular.element((element || document).querySelectorAll(query)); + } + function fetchTemplate(template) { + return $q.when($templateCache.get(template) || $http.get(template)).then(function (res) { + if (angular.isObject(res)) { + $templateCache.put(template, res.data); + return res.data; + } + return res; + }); + } + return ModalFactory; + } + ]; + }).directive('bsModal', [ + '$window', + '$location', + '$sce', + '$modal', + function ($window, $location, $sce, $modal) { + return { + restrict: 'EAC', + scope: true, + link: function postLink(scope, element, attr, transclusion) { + // Directive options + var options = { + scope: scope, + element: element, + show: false + }; + angular.forEach([ + 'template', + 'contentTemplate', + 'placement', + 'backdrop', + 'keyboard', + 'html', + 'container', + 'animation' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Support scope as data-attrs + angular.forEach([ + 'title', + 'content' + ], function (key) { + attr[key] && attr.$observe(key, function (newValue, oldValue) { + scope[key] = $sce.trustAsHtml(newValue); + }); + }); + // Support scope as an object + attr.bsModal && scope.$watch(attr.bsModal, function (newValue, oldValue) { + if (angular.isObject(newValue)) { + angular.extend(scope, newValue); + } else { + scope.content = newValue; + } + }, true); + // Initialize modal + var modal = $modal(options); + // Trigger + element.on(attr.trigger || 'click', modal.toggle); + // Garbage collection + scope.$on('$destroy', function () { + modal.destroy(); + options = null; + modal = null; + }); + } + }; + } + ]); + // Source: src/navbar/navbar.js + angular.module('mgcrea.ngStrap.navbar', []).provider('$navbar', function () { + var defaults = this.defaults = { + activeClass: 'active', + routeAttr: 'data-match-route', + strict: false + }; + this.$get = function () { + return { defaults: defaults }; + }; + }).directive('bsNavbar', [ + '$window', + '$location', + '$navbar', + function ($window, $location, $navbar) { + var defaults = $navbar.defaults; + return { + restrict: 'A', + link: function postLink(scope, element, attr, controller) { + // Directive options + var options = angular.copy(defaults); + angular.forEach(Object.keys(defaults), function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Watch for the $location + scope.$watch(function () { + return $location.path(); + }, function (newValue, oldValue) { + var liElements = element[0].querySelectorAll('li[' + options.routeAttr + ']'); + angular.forEach(liElements, function (li) { + var liElement = angular.element(li); + var pattern = liElement.attr(options.routeAttr).replace('/', '\\/'); + if (options.strict) { + pattern = '^' + pattern + '$'; + } + var regexp = new RegExp(pattern, ['i']); + if (regexp.test(newValue)) { + liElement.addClass(options.activeClass); + } else { + liElement.removeClass(options.activeClass); + } + }); + }); + } + }; + } + ]); + // Source: src/popover/popover.js + angular.module('mgcrea.ngStrap.popover', ['mgcrea.ngStrap.tooltip']).provider('$popover', function () { + var defaults = this.defaults = { + animation: 'am-fade', + placement: 'right', + template: 'popover/popover.tpl.html', + contentTemplate: false, + trigger: 'click', + keyboard: true, + html: false, + title: '', + content: '', + delay: 0, + container: false + }; + this.$get = [ + '$tooltip', + function ($tooltip) { + function PopoverFactory(element, config) { + // Common vars + var options = angular.extend({}, defaults, config); + var $popover = $tooltip(element, options); + // Support scope as string options [/*title, */content] + if (options.content) { + $popover.$scope.content = options.content; + } + return $popover; + } + return PopoverFactory; + } + ]; + }).directive('bsPopover', [ + '$window', + '$location', + '$sce', + '$popover', + function ($window, $location, $sce, $popover) { + var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; + return { + restrict: 'EAC', + scope: true, + link: function postLink(scope, element, attr) { + // Directive options + var options = { scope: scope }; + angular.forEach([ + 'template', + 'contentTemplate', + 'placement', + 'container', + 'delay', + 'trigger', + 'keyboard', + 'html', + 'animation' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Support scope as data-attrs + angular.forEach([ + 'title', + 'content' + ], function (key) { + attr[key] && attr.$observe(key, function (newValue, oldValue) { + scope[key] = $sce.trustAsHtml(newValue); + angular.isDefined(oldValue) && requestAnimationFrame(function () { + popover && popover.$applyPlacement(); + }); + }); + }); + // Support scope as an object + attr.bsPopover && scope.$watch(attr.bsPopover, function (newValue, oldValue) { + if (angular.isObject(newValue)) { + angular.extend(scope, newValue); + } else { + scope.content = newValue; + } + angular.isDefined(oldValue) && requestAnimationFrame(function () { + popover && popover.$applyPlacement(); + }); + }, true); + // Initialize popover + var popover = $popover(element, options); + // Garbage collection + scope.$on('$destroy', function () { + popover.destroy(); + options = null; + popover = null; + }); + } + }; + } + ]); + // Source: src/scrollspy/scrollspy.js + angular.module('mgcrea.ngStrap.scrollspy', [ + 'mgcrea.ngStrap.helpers.debounce', + 'mgcrea.ngStrap.helpers.dimensions' + ]).provider('$scrollspy', function () { + // Pool of registered spies + var spies = this.$$spies = {}; + var defaults = this.defaults = { + debounce: 150, + throttle: 100, + offset: 100 + }; + this.$get = [ + '$window', + '$document', + '$rootScope', + 'dimensions', + 'debounce', + 'throttle', + function ($window, $document, $rootScope, dimensions, debounce, throttle) { + var windowEl = angular.element($window); + var docEl = angular.element($document.prop('documentElement')); + var bodyEl = angular.element($window.document.body); + // Helper functions + function nodeName(element, name) { + return element[0].nodeName && element[0].nodeName.toLowerCase() === name.toLowerCase(); + } + function ScrollSpyFactory(config) { + // Common vars + var options = angular.extend({}, defaults, config); + if (!options.element) + options.element = bodyEl; + var isWindowSpy = nodeName(options.element, 'body'); + var scrollEl = isWindowSpy ? windowEl : options.element; + var scrollId = isWindowSpy ? 'window' : options.id; + // Use existing spy + if (spies[scrollId]) { + spies[scrollId].$$count++; + return spies[scrollId]; + } + var $scrollspy = {}; + // Private vars + var unbindViewContentLoaded, unbindIncludeContentLoaded; + var trackedElements = $scrollspy.$trackedElements = []; + var sortedElements = []; + var activeTarget; + var debouncedCheckPosition; + var throttledCheckPosition; + var debouncedCheckOffsets; + var viewportHeight; + var scrollTop; + $scrollspy.init = function () { + // Setup internal ref counter + this.$$count = 1; + // Bind events + debouncedCheckPosition = debounce(this.checkPosition, options.debounce); + throttledCheckPosition = throttle(this.checkPosition, options.throttle); + scrollEl.on('click', this.checkPositionWithEventLoop); + windowEl.on('resize', debouncedCheckPosition); + scrollEl.on('scroll', throttledCheckPosition); + debouncedCheckOffsets = debounce(this.checkOffsets, options.debounce); + unbindViewContentLoaded = $rootScope.$on('$viewContentLoaded', debouncedCheckOffsets); + unbindIncludeContentLoaded = $rootScope.$on('$includeContentLoaded', debouncedCheckOffsets); + debouncedCheckOffsets(); + // Register spy for reuse + if (scrollId) { + spies[scrollId] = $scrollspy; + } + }; + $scrollspy.destroy = function () { + // Check internal ref counter + this.$$count--; + if (this.$$count > 0) { + return; + } + // Unbind events + scrollEl.off('click', this.checkPositionWithEventLoop); + windowEl.off('resize', debouncedCheckPosition); + scrollEl.off('scroll', debouncedCheckPosition); + unbindViewContentLoaded(); + unbindIncludeContentLoaded(); + }; + $scrollspy.checkPosition = function () { + // Not ready yet + if (!sortedElements.length) + return; + // Calculate the scroll position + scrollTop = (isWindowSpy ? $window.pageYOffset : scrollEl.prop('scrollTop')) || 0; + // Calculate the viewport height for use by the components + viewportHeight = Math.max($window.innerHeight, docEl.prop('clientHeight')); + // Activate first element if scroll is smaller + if (scrollTop < sortedElements[0].offsetTop && activeTarget !== sortedElements[0].target) { + return $scrollspy.$activateElement(sortedElements[0]); + } + // Activate proper element + for (var i = sortedElements.length; i--;) { + if (angular.isUndefined(sortedElements[i].offsetTop) || sortedElements[i].offsetTop === null) + continue; + if (activeTarget === sortedElements[i].target) + continue; + if (scrollTop < sortedElements[i].offsetTop) + continue; + if (sortedElements[i + 1] && scrollTop > sortedElements[i + 1].offsetTop) + continue; + return $scrollspy.$activateElement(sortedElements[i]); + } + }; + $scrollspy.checkPositionWithEventLoop = function () { + setTimeout(this.checkPosition, 1); + }; + // Protected methods + $scrollspy.$activateElement = function (element) { + if (activeTarget) { + var activeElement = $scrollspy.$getTrackedElement(activeTarget); + if (activeElement) { + activeElement.source.removeClass('active'); + if (nodeName(activeElement.source, 'li') && nodeName(activeElement.source.parent().parent(), 'li')) { + activeElement.source.parent().parent().removeClass('active'); + } + } + } + activeTarget = element.target; + element.source.addClass('active'); + if (nodeName(element.source, 'li') && nodeName(element.source.parent().parent(), 'li')) { + element.source.parent().parent().addClass('active'); + } + }; + $scrollspy.$getTrackedElement = function (target) { + return trackedElements.filter(function (obj) { + return obj.target === target; + })[0]; + }; + // Track offsets behavior + $scrollspy.checkOffsets = function () { + angular.forEach(trackedElements, function (trackedElement) { + var targetElement = document.querySelector(trackedElement.target); + trackedElement.offsetTop = targetElement ? dimensions.offset(targetElement).top : null; + if (options.offset && trackedElement.offsetTop !== null) + trackedElement.offsetTop -= options.offset * 1; + }); + sortedElements = trackedElements.filter(function (el) { + return el.offsetTop !== null; + }).sort(function (a, b) { + return a.offsetTop - b.offsetTop; + }); + debouncedCheckPosition(); + }; + $scrollspy.trackElement = function (target, source) { + trackedElements.push({ + target: target, + source: source + }); + }; + $scrollspy.untrackElement = function (target, source) { + var toDelete; + for (var i = trackedElements.length; i--;) { + if (trackedElements[i].target === target && trackedElements[i].source === source) { + toDelete = i; + break; + } + } + trackedElements = trackedElements.splice(toDelete, 1); + }; + $scrollspy.activate = function (i) { + trackedElements[i].addClass('active'); + }; + // Initialize plugin + $scrollspy.init(); + return $scrollspy; + } + return ScrollSpyFactory; + } + ]; + }).directive('bsScrollspy', [ + '$rootScope', + 'debounce', + 'dimensions', + '$scrollspy', + function ($rootScope, debounce, dimensions, $scrollspy) { + return { + restrict: 'EAC', + link: function postLink(scope, element, attr) { + var options = { scope: scope }; + angular.forEach([ + 'offset', + 'target' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + var scrollspy = $scrollspy(options); + scrollspy.trackElement(options.target, element); + scope.$on('$destroy', function () { + scrollspy.untrackElement(options.target, element); + scrollspy.destroy(); + options = null; + scrollspy = null; + }); + } + }; + } + ]).directive('bsScrollspyList', [ + '$rootScope', + 'debounce', + 'dimensions', + '$scrollspy', + function ($rootScope, debounce, dimensions, $scrollspy) { + return { + restrict: 'A', + compile: function postLink(element, attr) { + var children = element[0].querySelectorAll('li > a[href]'); + angular.forEach(children, function (child) { + var childEl = angular.element(child); + childEl.parent().attr('bs-scrollspy', '').attr('data-target', childEl.attr('href')); + }); + } + }; + } + ]); + // Source: src/select/select.js + angular.module('mgcrea.ngStrap.select', [ + 'mgcrea.ngStrap.tooltip', + 'mgcrea.ngStrap.helpers.parseOptions' + ]).provider('$select', function () { + var defaults = this.defaults = { + animation: 'am-fade', + prefixClass: 'select', + placement: 'bottom-left', + template: 'select/select.tpl.html', + trigger: 'focus', + container: false, + keyboard: true, + html: false, + delay: 0, + multiple: false, + sort: true, + caretHtml: ' ', + placeholder: 'Choose among the following...', + maxLength: 3, + maxLengthHtml: 'selected' + }; + this.$get = [ + '$window', + '$document', + '$rootScope', + '$tooltip', + function ($window, $document, $rootScope, $tooltip) { + var bodyEl = angular.element($window.document.body); + var isTouch = 'createTouch' in $window.document; + function SelectFactory(element, controller, config) { + var $select = {}; + // Common vars + var options = angular.extend({}, defaults, config); + $select = $tooltip(element, options); + var parentScope = config.scope; + var scope = $select.$scope; + scope.$matches = []; + scope.$activeIndex = 0; + scope.$isMultiple = options.multiple; + scope.$activate = function (index) { + scope.$$postDigest(function () { + $select.activate(index); + }); + }; + scope.$select = function (index, evt) { + scope.$$postDigest(function () { + $select.select(index); + }); + }; + scope.$isVisible = function () { + return $select.$isVisible(); + }; + scope.$isActive = function (index) { + return $select.$isActive(index); + }; + // Public methods + $select.update = function (matches) { + scope.$matches = matches; + $select.$updateActiveIndex(); + }; + $select.activate = function (index) { + if (options.multiple) { + scope.$activeIndex.sort(); + $select.$isActive(index) ? scope.$activeIndex.splice(scope.$activeIndex.indexOf(index), 1) : scope.$activeIndex.push(index); + if (options.sort) + scope.$activeIndex.sort(); + } else { + scope.$activeIndex = index; + } + return scope.$activeIndex; + }; + $select.select = function (index) { + var value = scope.$matches[index].value; + $select.activate(index); + if (options.multiple) { + controller.$setViewValue(scope.$activeIndex.map(function (index) { + return scope.$matches[index].value; + })); + } else { + controller.$setViewValue(value); + } + controller.$render(); + if (parentScope) + parentScope.$digest(); + // Hide if single select + if (!options.multiple) { + if (options.trigger === 'focus') + element[0].blur(); + else if ($select.$isShown) + $select.hide(); + } + // Emit event + scope.$emit('$select.select', value, index); + }; + // Protected methods + $select.$updateActiveIndex = function () { + if (controller.$modelValue && scope.$matches.length) { + if (options.multiple && angular.isArray(controller.$modelValue)) { + scope.$activeIndex = controller.$modelValue.map(function (value) { + return $select.$getIndex(value); + }); + } else { + scope.$activeIndex = $select.$getIndex(controller.$modelValue); + } + } else if (scope.$activeIndex >= scope.$matches.length) { + scope.$activeIndex = options.multiple ? [] : 0; + } + }; + $select.$isVisible = function () { + if (!options.minLength || !controller) { + return scope.$matches.length; + } + // minLength support + return scope.$matches.length && controller.$viewValue.length >= options.minLength; + }; + $select.$isActive = function (index) { + if (options.multiple) { + return scope.$activeIndex.indexOf(index) !== -1; + } else { + return scope.$activeIndex === index; + } + }; + $select.$getIndex = function (value) { + var l = scope.$matches.length, i = l; + if (!l) + return; + for (i = l; i--;) { + if (scope.$matches[i].value === value) + break; + } + if (i < 0) + return; + return i; + }; + $select.$onMouseDown = function (evt) { + // Prevent blur on mousedown on .dropdown-menu + evt.preventDefault(); + evt.stopPropagation(); + // Emulate click for mobile devices + if (isTouch) { + var targetEl = angular.element(evt.target); + targetEl.triggerHandler('click'); + } + }; + $select.$onKeyDown = function (evt) { + if (!/(38|40|13)/.test(evt.keyCode)) + return; + evt.preventDefault(); + evt.stopPropagation(); + // Select with enter + if (evt.keyCode === 13) { + return $select.select(scope.$activeIndex); + } + // Navigate with keyboard + if (evt.keyCode === 38 && scope.$activeIndex > 0) + scope.$activeIndex--; + else if (evt.keyCode === 40 && scope.$activeIndex < scope.$matches.length - 1) + scope.$activeIndex++; + else if (angular.isUndefined(scope.$activeIndex)) + scope.$activeIndex = 0; + scope.$digest(); + }; + // Overrides + var _show = $select.show; + $select.show = function () { + _show(); + if (options.multiple) { + $select.$element.addClass('select-multiple'); + } + setTimeout(function () { + $select.$element.on(isTouch ? 'touchstart' : 'mousedown', $select.$onMouseDown); + if (options.keyboard) { + element.on('keydown', $select.$onKeyDown); + } + }); + }; + var _hide = $select.hide; + $select.hide = function () { + $select.$element.off(isTouch ? 'touchstart' : 'mousedown', $select.$onMouseDown); + if (options.keyboard) { + element.off('keydown', $select.$onKeyDown); + } + _hide(); + }; + return $select; + } + SelectFactory.defaults = defaults; + return SelectFactory; + } + ]; + }).directive('bsSelect', [ + '$window', + '$parse', + '$q', + '$select', + '$parseOptions', + function ($window, $parse, $q, $select, $parseOptions) { + var defaults = $select.defaults; + return { + restrict: 'EAC', + require: 'ngModel', + link: function postLink(scope, element, attr, controller) { + // Directive options + var options = { scope: scope }; + angular.forEach([ + 'placement', + 'container', + 'delay', + 'trigger', + 'keyboard', + 'html', + 'animation', + 'template', + 'placeholder', + 'multiple', + 'maxLength', + 'maxLengthHtml' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Add support for select markup + if (element[0].nodeName.toLowerCase() === 'select') { + var inputEl = element; + inputEl.css('display', 'none'); + element = angular.element(''); + inputEl.after(element); + } + // Build proper ngOptions + var parsedOptions = $parseOptions(attr.ngOptions); + // Initialize select + var select = $select(element, controller, options); + // Watch ngOptions values before filtering for changes + var watchedOptions = parsedOptions.$match[7].replace(/\|.+/, '').trim(); + scope.$watch(watchedOptions, function (newValue, oldValue) { + // console.warn('scope.$watch(%s)', watchedOptions, newValue, oldValue); + parsedOptions.valuesFn(scope, controller).then(function (values) { + select.update(values); + controller.$render(); + }); + }, true); + // Watch model for changes + scope.$watch(attr.ngModel, function (newValue, oldValue) { + // console.warn('scope.$watch(%s)', attr.ngModel, newValue, oldValue); + select.$updateActiveIndex(); + }, true); + // Model rendering in view + controller.$render = function () { + // console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue); + var selected, index; + if (options.multiple && angular.isArray(controller.$modelValue)) { + selected = controller.$modelValue.map(function (value) { + index = select.$getIndex(value); + return angular.isDefined(index) ? select.$scope.$matches[index].label : false; + }).filter(angular.isDefined); + if (selected.length > (options.maxLength || defaults.maxLength)) { + selected = selected.length + ' ' + (options.maxLengthHtml || defaults.maxLengthHtml); + } else { + selected = selected.join(', '); + } + } else { + index = select.$getIndex(controller.$modelValue); + selected = angular.isDefined(index) ? select.$scope.$matches[index].label : false; + } + element.html((selected ? selected : attr.placeholder || defaults.placeholder) + defaults.caretHtml); + }; + // Garbage collection + scope.$on('$destroy', function () { + select.destroy(); + options = null; + select = null; + }); + } + }; + } + ]); + // Source: src/tab/tab.js + angular.module('mgcrea.ngStrap.tab', []).run([ + '$templateCache', + function ($templateCache) { + $templateCache.put('$pane', '{{pane.content}}'); + } + ]).provider('$tab', function () { + var defaults = this.defaults = { + animation: 'am-fade', + template: 'tab/tab.tpl.html' + }; + this.$get = function () { + return { defaults: defaults }; + }; + }).directive('bsTabs', [ + '$window', + '$animate', + '$tab', + function ($window, $animate, $tab) { + var defaults = $tab.defaults; + return { + restrict: 'EAC', + scope: true, + require: '?ngModel', + templateUrl: function (element, attr) { + return attr.template || defaults.template; + }, + link: function postLink(scope, element, attr, controller) { + // Directive options + var options = defaults; + angular.forEach(['animation'], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Require scope as an object + attr.bsTabs && scope.$watch(attr.bsTabs, function (newValue, oldValue) { + scope.panes = newValue; + }, true); + // Add base class + element.addClass('tabs'); + // Support animations + if (options.animation) { + element.addClass(options.animation); + } + scope.active = scope.activePane = 0; + // view -> model + scope.setActive = function (index, ev) { + scope.active = index; + if (controller) { + controller.$setViewValue(index); + } + }; + // model -> view + if (controller) { + controller.$render = function () { + scope.active = controller.$modelValue * 1; + }; + } + } + }; + } + ]); + // Source: src/timepicker/timepicker.js + angular.module('mgcrea.ngStrap.timepicker', [ + 'mgcrea.ngStrap.helpers.dateParser', + 'mgcrea.ngStrap.tooltip' + ]).provider('$timepicker', function () { + var defaults = this.defaults = { + animation: 'am-fade', + prefixClass: 'timepicker', + placement: 'bottom-left', + template: 'timepicker/timepicker.tpl.html', + trigger: 'focus', + container: false, + keyboard: true, + html: false, + delay: 0, + useNative: true, + timeType: 'date', + timeFormat: 'shortTime', + autoclose: false, + minTime: -Infinity, + maxTime: +Infinity, + length: 5, + hourStep: 1, + minuteStep: 5 + }; + this.$get = [ + '$window', + '$document', + '$rootScope', + '$sce', + '$locale', + 'dateFilter', + '$tooltip', + function ($window, $document, $rootScope, $sce, $locale, dateFilter, $tooltip) { + var bodyEl = angular.element($window.document.body); + var isTouch = 'createTouch' in $window.document; + var isNative = /(ip(a|o)d|iphone|android)/gi.test($window.navigator.userAgent); + if (!defaults.lang) + defaults.lang = $locale.id; + function timepickerFactory(element, controller, config) { + var $timepicker = $tooltip(element, angular.extend({}, defaults, config)); + var parentScope = config.scope; + var options = $timepicker.$options; + var scope = $timepicker.$scope; + // View vars + var selectedIndex = 0; + var startDate = controller.$dateValue || new Date(); + var viewDate = { + hour: startDate.getHours(), + meridian: startDate.getHours() < 12, + minute: startDate.getMinutes(), + second: startDate.getSeconds(), + millisecond: startDate.getMilliseconds() + }; + var format = $locale.DATETIME_FORMATS[options.timeFormat] || options.timeFormat; + var formats = /(h+)[:]?(m+)[ ]?(a?)/i.exec(format).slice(1); + // Scope methods + scope.$select = function (date, index) { + $timepicker.select(date, index); + }; + scope.$moveIndex = function (value, index) { + $timepicker.$moveIndex(value, index); + }; + scope.$switchMeridian = function (date) { + $timepicker.switchMeridian(date); + }; + // Public methods + $timepicker.update = function (date) { + // console.warn('$timepicker.update() newValue=%o', date); + if (angular.isDate(date) && !isNaN(date.getTime())) { + $timepicker.$date = date; + angular.extend(viewDate, { + hour: date.getHours(), + minute: date.getMinutes(), + second: date.getSeconds(), + millisecond: date.getMilliseconds() + }); + $timepicker.$build(); + } else if (!$timepicker.$isBuilt) { + $timepicker.$build(); + } + }; + $timepicker.select = function (date, index, keep) { + // console.warn('$timepicker.select', date, scope.$mode); + if (isNaN(controller.$dateValue.getTime())) + controller.$dateValue = new Date(1970, 0, 1); + if (!angular.isDate(date)) + date = new Date(date); + if (index === 0) + controller.$dateValue.setHours(date.getHours()); + else if (index === 1) + controller.$dateValue.setMinutes(date.getMinutes()); + controller.$setViewValue(controller.$dateValue); + controller.$render(); + if (options.autoclose && !keep) { + $timepicker.hide(true); + } + }; + $timepicker.switchMeridian = function (date) { + var hours = (date || controller.$dateValue).getHours(); + controller.$dateValue.setHours(hours < 12 ? hours + 12 : hours - 12); + controller.$render(); + }; + // Protected methods + $timepicker.$build = function () { + // console.warn('$timepicker.$build() viewDate=%o', viewDate); + var i, midIndex = scope.midIndex = parseInt(options.length / 2, 10); + var hours = [], hour; + for (i = 0; i < options.length; i++) { + hour = new Date(1970, 0, 1, viewDate.hour - (midIndex - i) * options.hourStep); + hours.push({ + date: hour, + label: dateFilter(hour, formats[0]), + selected: $timepicker.$date && $timepicker.$isSelected(hour, 0), + disabled: $timepicker.$isDisabled(hour, 0) + }); + } + var minutes = [], minute; + for (i = 0; i < options.length; i++) { + minute = new Date(1970, 0, 1, 0, viewDate.minute - (midIndex - i) * options.minuteStep); + minutes.push({ + date: minute, + label: dateFilter(minute, formats[1]), + selected: $timepicker.$date && $timepicker.$isSelected(minute, 1), + disabled: $timepicker.$isDisabled(minute, 1) + }); + } + var rows = []; + for (i = 0; i < options.length; i++) { + rows.push([ + hours[i], + minutes[i] + ]); + } + scope.rows = rows; + scope.showAM = !!formats[2]; + scope.isAM = ($timepicker.$date || hours[midIndex].date).getHours() < 12; + $timepicker.$isBuilt = true; + }; + $timepicker.$isSelected = function (date, index) { + if (!$timepicker.$date) + return false; + else if (index === 0) { + return date.getHours() === $timepicker.$date.getHours(); + } else if (index === 1) { + return date.getMinutes() === $timepicker.$date.getMinutes(); + } + }; + $timepicker.$isDisabled = function (date, index) { + var selectedTime; + if (index === 0) { + selectedTime = date.getTime() + viewDate.minute * 60000; + } else if (index === 1) { + selectedTime = date.getTime() + viewDate.hour * 3600000; + } + return selectedTime < options.minTime || selectedTime > options.maxTime; + }; + $timepicker.$moveIndex = function (value, index) { + var targetDate; + if (index === 0) { + targetDate = new Date(1970, 0, 1, viewDate.hour + value * options.length, viewDate.minute); + angular.extend(viewDate, { hour: targetDate.getHours() }); + } else if (index === 1) { + targetDate = new Date(1970, 0, 1, viewDate.hour, viewDate.minute + value * options.length * 5); + angular.extend(viewDate, { minute: targetDate.getMinutes() }); + } + $timepicker.$build(); + }; + $timepicker.$onMouseDown = function (evt) { + // Prevent blur on mousedown on .dropdown-menu + if (evt.target.nodeName.toLowerCase() !== 'input') + evt.preventDefault(); + evt.stopPropagation(); + // Emulate click for mobile devices + if (isTouch) { + var targetEl = angular.element(evt.target); + if (targetEl[0].nodeName.toLowerCase() !== 'button') { + targetEl = targetEl.parent(); + } + targetEl.triggerHandler('click'); + } + }; + $timepicker.$onKeyDown = function (evt) { + if (!/(38|37|39|40|13)/.test(evt.keyCode) || evt.shiftKey || evt.altKey) + return; + evt.preventDefault(); + evt.stopPropagation(); + // Close on enter + if (evt.keyCode === 13) + return $timepicker.hide(true); + // Navigate with keyboard + var newDate = new Date($timepicker.$date); + var hours = newDate.getHours(), hoursLength = dateFilter(newDate, 'h').length; + var minutes = newDate.getMinutes(), minutesLength = dateFilter(newDate, 'mm').length; + var lateralMove = /(37|39)/.test(evt.keyCode); + var count = 2 + !!formats[2] * 1; + // Navigate indexes (left, right) + if (lateralMove) { + if (evt.keyCode === 37) + selectedIndex = selectedIndex < 1 ? count - 1 : selectedIndex - 1; + else if (evt.keyCode === 39) + selectedIndex = selectedIndex < count - 1 ? selectedIndex + 1 : 0; + } + // Update values (up, down) + if (selectedIndex === 0) { + if (lateralMove) + return createSelection(0, hoursLength); + if (evt.keyCode === 38) + newDate.setHours(hours - options.hourStep); + else if (evt.keyCode === 40) + newDate.setHours(hours + options.hourStep); + } else if (selectedIndex === 1) { + if (lateralMove) + return createSelection(hoursLength + 1, hoursLength + 1 + minutesLength); + if (evt.keyCode === 38) + newDate.setMinutes(minutes - options.minuteStep); + else if (evt.keyCode === 40) + newDate.setMinutes(minutes + options.minuteStep); + } else if (selectedIndex === 2) { + if (lateralMove) + return createSelection(hoursLength + 1 + minutesLength + 1, hoursLength + 1 + minutesLength + 3); + $timepicker.switchMeridian(); + } + $timepicker.select(newDate, selectedIndex, true); + parentScope.$digest(); + }; + // Private + function createSelection(start, end) { + if (element[0].createTextRange) { + var selRange = element[0].createTextRange(); + selRange.collapse(true); + selRange.moveStart('character', start); + selRange.moveEnd('character', end); + selRange.select(); + } else if (element[0].setSelectionRange) { + element[0].setSelectionRange(start, end); + } else if (angular.isUndefined(element[0].selectionStart)) { + element[0].selectionStart = start; + element[0].selectionEnd = end; + } + } + function focusElement() { + element[0].focus(); + } + // Overrides + var _init = $timepicker.init; + $timepicker.init = function () { + if (isNative && options.useNative) { + element.prop('type', 'time'); + element.css('-webkit-appearance', 'textfield'); + return; + } else if (isTouch) { + element.prop('type', 'text'); + element.attr('readonly', 'true'); + element.on('click', focusElement); + } + _init(); + }; + var _destroy = $timepicker.destroy; + $timepicker.destroy = function () { + if (isNative && options.useNative) { + element.off('click', focusElement); + } + _destroy(); + }; + var _show = $timepicker.show; + $timepicker.show = function () { + _show(); + setTimeout(function () { + $timepicker.$element.on(isTouch ? 'touchstart' : 'mousedown', $timepicker.$onMouseDown); + if (options.keyboard) { + element.on('keydown', $timepicker.$onKeyDown); + } + }); + }; + var _hide = $timepicker.hide; + $timepicker.hide = function (blur) { + $timepicker.$element.off(isTouch ? 'touchstart' : 'mousedown', $timepicker.$onMouseDown); + if (options.keyboard) { + element.off('keydown', $timepicker.$onKeyDown); + } + _hide(blur); + }; + return $timepicker; + } + timepickerFactory.defaults = defaults; + return timepickerFactory; + } + ]; + }).directive('bsTimepicker', [ + '$window', + '$parse', + '$q', + '$locale', + 'dateFilter', + '$timepicker', + '$dateParser', + '$timeout', + function ($window, $parse, $q, $locale, dateFilter, $timepicker, $dateParser, $timeout) { + var defaults = $timepicker.defaults; + var isNative = /(ip(a|o)d|iphone|android)/gi.test($window.navigator.userAgent); + var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; + return { + restrict: 'EAC', + require: 'ngModel', + link: function postLink(scope, element, attr, controller) { + // Directive options + var options = { + scope: scope, + controller: controller + }; + angular.forEach([ + 'placement', + 'container', + 'delay', + 'trigger', + 'keyboard', + 'html', + 'animation', + 'template', + 'autoclose', + 'timeType', + 'timeFormat', + 'useNative', + 'lang' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Initialize timepicker + if (isNative && (options.useNative || defaults.useNative)) + options.timeFormat = 'HH:mm'; + var timepicker = $timepicker(element, controller, options); + options = timepicker.$options; + // Initialize parser + var dateParser = $dateParser({ + format: options.timeFormat, + lang: options.lang + }); + // Observe attributes for changes + angular.forEach([ + 'minTime', + 'maxTime' + ], function (key) { + // console.warn('attr.$observe(%s)', key, attr[key]); + angular.isDefined(attr[key]) && attr.$observe(key, function (newValue) { + if (newValue === 'now') { + timepicker.$options[key] = new Date().setFullYear(1970, 0, 1); + } else if (angular.isString(newValue) && newValue.match(/^".+"$/)) { + timepicker.$options[key] = +new Date(newValue.substr(1, newValue.length - 2)); + } else { + timepicker.$options[key] = dateParser.parse(newValue); + } + !isNaN(timepicker.$options[key]) && timepicker.$build(); + }); + }); + // Watch model for changes + scope.$watch(attr.ngModel, function (newValue, oldValue) { + // console.warn('scope.$watch(%s)', attr.ngModel, newValue, oldValue, controller.$dateValue); + timepicker.update(controller.$dateValue); + }, true); + // viewValue -> $parsers -> modelValue + controller.$parsers.unshift(function (viewValue) { + // console.warn('$parser("%s"): viewValue=%o', element.attr('ng-model'), viewValue); + // Null values should correctly reset the model value & validity + if (!viewValue) { + controller.$setValidity('date', true); + return; + } + var parsedTime = dateParser.parse(viewValue, controller.$dateValue); + if (!parsedTime || isNaN(parsedTime.getTime())) { + controller.$setValidity('date', false); + } else { + var isValid = parsedTime.getTime() >= options.minTime && parsedTime.getTime() <= options.maxTime; + controller.$setValidity('date', isValid); + // Only update the model when we have a valid date + if (isValid) + controller.$dateValue = parsedTime; + } + if (options.timeType === 'string') { + return dateFilter(viewValue, options.timeFormat); + } else if (options.timeType === 'number') { + return controller.$dateValue.getTime(); + } else if (options.timeType === 'iso') { + return controller.$dateValue.toISOString(); + } else { + return controller.$dateValue; + } + }); + // modelValue -> $formatters -> viewValue + controller.$formatters.push(function (modelValue) { + // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue); + var date = options.timeType === 'string' ? dateParser.parse(modelValue, controller.$dateValue) : new Date(modelValue); + // Setup default value: next hour? + // if(isNaN(date.getTime())) date = new Date(new Date().setMinutes(0) + 36e5); + controller.$dateValue = date; + return controller.$dateValue; + }); + // viewValue -> element + controller.$render = function () { + // console.warn('$render("%s"): viewValue=%o', element.attr('ng-model'), controller.$viewValue); + element.val(isNaN(controller.$dateValue.getTime()) ? '' : dateFilter(controller.$dateValue, options.timeFormat)); + }; + // Garbage collection + scope.$on('$destroy', function () { + timepicker.destroy(); + options = null; + timepicker = null; + }); + } + }; + } + ]); + // Source: src/tooltip/tooltip.js + angular.module('mgcrea.ngStrap.tooltip', [ + 'ngAnimate', + 'mgcrea.ngStrap.helpers.dimensions' + ]).provider('$tooltip', function () { + var defaults = this.defaults = { + animation: 'am-fade', + prefixClass: 'tooltip', + container: false, + placement: 'top', + template: 'tooltip/tooltip.tpl.html', + contentTemplate: false, + trigger: 'hover focus', + keyboard: false, + html: false, + show: false, + title: '', + type: '', + delay: 0 + }; + this.$get = [ + '$window', + '$rootScope', + '$compile', + '$q', + '$templateCache', + '$http', + '$animate', + '$timeout', + 'dimensions', + '$$animateReflow', + function ($window, $rootScope, $compile, $q, $templateCache, $http, $animate, $timeout, dimensions, $$animateReflow) { + var trim = String.prototype.trim; + var isTouch = 'createTouch' in $window.document; + var htmlReplaceRegExp = /ng-bind="/gi; + function TooltipFactory(element, config) { + var $tooltip = {}; + // Common vars + var options = $tooltip.$options = angular.extend({}, defaults, config); + $tooltip.$promise = fetchTemplate(options.template); + var scope = $tooltip.$scope = options.scope && options.scope.$new() || $rootScope.$new(); + if (options.delay && angular.isString(options.delay)) { + options.delay = parseFloat(options.delay); + } + // Support scope as string options + if (options.title) { + $tooltip.$scope.title = options.title; + } + // Provide scope helpers + scope.$hide = function () { + scope.$$postDigest(function () { + $tooltip.hide(); + }); + }; + scope.$show = function () { + scope.$$postDigest(function () { + $tooltip.show(); + }); + }; + scope.$toggle = function () { + scope.$$postDigest(function () { + $tooltip.toggle(); + }); + }; + $tooltip.$isShown = scope.$isShown = false; + // Private vars + var timeout, hoverState; + // Support contentTemplate option + if (options.contentTemplate) { + $tooltip.$promise = $tooltip.$promise.then(function (template) { + var templateEl = angular.element(template); + return fetchTemplate(options.contentTemplate).then(function (contentTemplate) { + findElement('[ng-bind="content"]', templateEl[0]).removeAttr('ng-bind').html(contentTemplate); + return templateEl[0].outerHTML; + }); + }); + } + // Fetch, compile then initialize tooltip + var tipLinker, tipElement, tipTemplate, tipContainer; + $tooltip.$promise.then(function (template) { + if (angular.isObject(template)) + template = template.data; + if (options.html) + template = template.replace(htmlReplaceRegExp, 'ng-bind-html="'); + template = trim.apply(template); + tipTemplate = template; + tipLinker = $compile(template); + $tooltip.init(); + }); + $tooltip.init = function () { + // Options: delay + if (options.delay && angular.isNumber(options.delay)) { + options.delay = { + show: options.delay, + hide: options.delay + }; + } + // Replace trigger on touch devices ? + // if(isTouch && options.trigger === defaults.trigger) { + // options.trigger.replace(/hover/g, 'click'); + // } + // Options : container + if (options.container === 'self') { + tipContainer = element; + } else if (options.container) { + tipContainer = findElement(options.container); + } + // Options: trigger + var triggers = options.trigger.split(' '); + angular.forEach(triggers, function (trigger) { + if (trigger === 'click') { + element.on('click', $tooltip.toggle); + } else if (trigger !== 'manual') { + element.on(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter); + element.on(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave); + trigger !== 'hover' && element.on(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown); + } + }); + // Options: show + if (options.show) { + scope.$$postDigest(function () { + options.trigger === 'focus' ? element[0].focus() : $tooltip.show(); + }); + } + }; + $tooltip.destroy = function () { + // Unbind events + var triggers = options.trigger.split(' '); + for (var i = triggers.length; i--;) { + var trigger = triggers[i]; + if (trigger === 'click') { + element.off('click', $tooltip.toggle); + } else if (trigger !== 'manual') { + element.off(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter); + element.off(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave); + trigger !== 'hover' && element.off(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown); + } + } + // Remove element + if (tipElement) { + tipElement.remove(); + tipElement = null; + } + // Destroy scope + scope.$destroy(); + }; + $tooltip.enter = function () { + clearTimeout(timeout); + hoverState = 'in'; + if (!options.delay || !options.delay.show) { + return $tooltip.show(); + } + timeout = setTimeout(function () { + if (hoverState === 'in') + $tooltip.show(); + }, options.delay.show); + }; + $tooltip.show = function () { + var parent = options.container ? tipContainer : null; + var after = options.container ? null : element; + // Remove any existing tipElement + if (tipElement) + tipElement.remove(); + // Fetch a cloned element linked from template + tipElement = $tooltip.$element = tipLinker(scope, function (clonedElement, scope) { + }); + // Set the initial positioning. + tipElement.css({ + top: '0px', + left: '0px', + display: 'block' + }).addClass(options.placement); + // Options: animation + if (options.animation) + tipElement.addClass(options.animation); + // Options: type + if (options.type) + tipElement.addClass(options.prefixClass + '-' + options.type); + $animate.enter(tipElement, parent, after, function () { + }); + $tooltip.$isShown = scope.$isShown = true; + scope.$$phase || scope.$digest(); + $$animateReflow($tooltip.$applyPlacement); + // Bind events + if (options.keyboard) { + if (options.trigger !== 'focus') { + $tooltip.focus(); + tipElement.on('keyup', $tooltip.$onKeyUp); + } else { + element.on('keyup', $tooltip.$onFocusKeyUp); + } + } + }; + $tooltip.leave = function () { + clearTimeout(timeout); + hoverState = 'out'; + if (!options.delay || !options.delay.hide) { + return $tooltip.hide(); + } + timeout = setTimeout(function () { + if (hoverState === 'out') { + $tooltip.hide(); + } + }, options.delay.hide); + }; + $tooltip.hide = function (blur) { + if (!$tooltip.$isShown) + return; + $animate.leave(tipElement, function () { + tipElement = null; + }); + $tooltip.$isShown = scope.$isShown = false; + scope.$$phase || scope.$digest(); + // Unbind events + if (options.keyboard) { + tipElement.off('keyup', $tooltip.$onKeyUp); + } + // Allow to blur the input when hidden, like when pressing enter key + if (blur && options.trigger === 'focus') { + return element[0].blur(); + } + }; + $tooltip.toggle = function () { + $tooltip.$isShown ? $tooltip.leave() : $tooltip.enter(); + }; + $tooltip.focus = function () { + tipElement[0].focus(); + }; + // Protected methods + $tooltip.$applyPlacement = function () { + if (!tipElement) + return; + // Get the position of the tooltip element. + var elementPosition = getPosition(); + // Get the height and width of the tooltip so we can center it. + var tipWidth = tipElement.prop('offsetWidth'), tipHeight = tipElement.prop('offsetHeight'); + // Get the tooltip's top and left coordinates to center it with this directive. + var tipPosition = getCalculatedOffset(options.placement, elementPosition, tipWidth, tipHeight); + // Now set the calculated positioning. + tipPosition.top += 'px'; + tipPosition.left += 'px'; + tipElement.css(tipPosition); + }; + $tooltip.$onKeyUp = function (evt) { + evt.which === 27 && $tooltip.hide(); + }; + $tooltip.$onFocusKeyUp = function (evt) { + evt.which === 27 && element[0].blur(); + }; + $tooltip.$onFocusElementMouseDown = function (evt) { + evt.preventDefault(); + evt.stopPropagation(); + // Some browsers do not auto-focus buttons (eg. Safari) + $tooltip.$isShown ? element[0].blur() : element[0].focus(); + }; + // Private methods + function getPosition() { + if (options.container === 'body') { + return dimensions.offset(element[0]); + } else { + return dimensions.position(element[0]); + } + } + function getCalculatedOffset(placement, position, actualWidth, actualHeight) { + var offset; + var split = placement.split('-'); + switch (split[0]) { + case 'right': + offset = { + top: position.top + position.height / 2 - actualHeight / 2, + left: position.left + position.width + }; + break; + case 'bottom': + offset = { + top: position.top + position.height, + left: position.left + position.width / 2 - actualWidth / 2 + }; + break; + case 'left': + offset = { + top: position.top + position.height / 2 - actualHeight / 2, + left: position.left - actualWidth + }; + break; + default: + offset = { + top: position.top - actualHeight, + left: position.left + position.width / 2 - actualWidth / 2 + }; + break; + } + if (!split[1]) { + return offset; + } + // Add support for corners @todo css + if (split[0] === 'top' || split[0] === 'bottom') { + switch (split[1]) { + case 'left': + offset.left = position.left; + break; + case 'right': + offset.left = position.left + position.width - actualWidth; + } + } else if (split[0] === 'left' || split[0] === 'right') { + switch (split[1]) { + case 'top': + offset.top = position.top - actualHeight; + break; + case 'bottom': + offset.top = position.top + position.height; + } + } + return offset; + } + return $tooltip; + } + // Helper functions + function findElement(query, element) { + return angular.element((element || document).querySelectorAll(query)); + } + function fetchTemplate(template) { + return $q.when($templateCache.get(template) || $http.get(template)).then(function (res) { + if (angular.isObject(res)) { + $templateCache.put(template, res.data); + return res.data; + } + return res; + }); + } + return TooltipFactory; + } + ]; + }).directive('bsTooltip', [ + '$window', + '$location', + '$sce', + '$tooltip', + '$$animateReflow', + function ($window, $location, $sce, $tooltip, $$animateReflow) { + return { + restrict: 'EAC', + scope: true, + link: function postLink(scope, element, attr, transclusion) { + // Directive options + var options = { scope: scope }; + angular.forEach([ + 'template', + 'contentTemplate', + 'placement', + 'container', + 'delay', + 'trigger', + 'keyboard', + 'html', + 'animation', + 'type' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Observe scope attributes for change + angular.forEach(['title'], function (key) { + attr[key] && attr.$observe(key, function (newValue, oldValue) { + scope[key] = $sce.trustAsHtml(newValue); + angular.isDefined(oldValue) && $$animateReflow(function () { + tooltip && tooltip.$applyPlacement(); + }); + }); + }); + // Support scope as an object + attr.bsTooltip && scope.$watch(attr.bsTooltip, function (newValue, oldValue) { + if (angular.isObject(newValue)) { + angular.extend(scope, newValue); + } else { + scope.content = newValue; + } + angular.isDefined(oldValue) && $$animateReflow(function () { + tooltip && tooltip.$applyPlacement(); + }); + }, true); + // Initialize popover + var tooltip = $tooltip(element, options); + // Garbage collection + scope.$on('$destroy', function () { + tooltip.destroy(); + options = null; + tooltip = null; + }); + } + }; + } + ]); + // Source: src/typeahead/typeahead.js + angular.module('mgcrea.ngStrap.typeahead', [ + 'mgcrea.ngStrap.tooltip', + 'mgcrea.ngStrap.helpers.parseOptions' + ]).provider('$typeahead', function () { + var defaults = this.defaults = { + animation: 'am-fade', + prefixClass: 'typeahead', + placement: 'bottom-left', + template: 'typeahead/typeahead.tpl.html', + trigger: 'focus', + container: false, + keyboard: true, + html: false, + delay: 0, + minLength: 1, + filter: 'filter', + limit: 6 + }; + this.$get = [ + '$window', + '$rootScope', + '$tooltip', + function ($window, $rootScope, $tooltip) { + var bodyEl = angular.element($window.document.body); + function TypeaheadFactory(element, config) { + var $typeahead = {}; + // Common vars + var options = angular.extend({}, defaults, config); + var controller = options.controller; + $typeahead = $tooltip(element, options); + var parentScope = config.scope; + var scope = $typeahead.$scope; + scope.$matches = []; + scope.$activeIndex = 0; + scope.$activate = function (index) { + scope.$$postDigest(function () { + $typeahead.activate(index); + }); + }; + scope.$select = function (index, evt) { + scope.$$postDigest(function () { + $typeahead.select(index); + }); + }; + scope.$isVisible = function () { + return $typeahead.$isVisible(); + }; + // Public methods + $typeahead.update = function (matches) { + scope.$matches = matches; + if (scope.$activeIndex >= matches.length) { + scope.$activeIndex = 0; + } + }; + $typeahead.activate = function (index) { + scope.$activeIndex = index; + }; + $typeahead.select = function (index) { + var value = scope.$matches[index].value; + if (controller) { + controller.$setViewValue(value); + controller.$render(); + if (parentScope) + parentScope.$digest(); + } + if (options.trigger === 'focus') + element[0].blur(); + else if ($typeahead.$isShown) + $typeahead.hide(); + scope.$activeIndex = 0; + // Emit event + scope.$emit('$typeahead.select', value, index); + }; + // Protected methods + $typeahead.$isVisible = function () { + if (!options.minLength || !controller) { + return !!scope.$matches.length; + } + // minLength support + return scope.$matches.length && angular.isString(controller.$viewValue) && controller.$viewValue.length >= options.minLength; + }; + $typeahead.$onMouseDown = function (evt) { + // Prevent blur on mousedown + evt.preventDefault(); + evt.stopPropagation(); + }; + $typeahead.$onKeyDown = function (evt) { + if (!/(38|40|13)/.test(evt.keyCode)) + return; + evt.preventDefault(); + evt.stopPropagation(); + // Select with enter + if (evt.keyCode === 13) { + return $typeahead.select(scope.$activeIndex); + } + // Navigate with keyboard + if (evt.keyCode === 38 && scope.$activeIndex > 0) + scope.$activeIndex--; + else if (evt.keyCode === 40 && scope.$activeIndex < scope.$matches.length - 1) + scope.$activeIndex++; + else if (angular.isUndefined(scope.$activeIndex)) + scope.$activeIndex = 0; + scope.$digest(); + }; + // Overrides + var show = $typeahead.show; + $typeahead.show = function () { + show(); + setTimeout(function () { + $typeahead.$element.on('mousedown', $typeahead.$onMouseDown); + if (options.keyboard) { + element.on('keydown', $typeahead.$onKeyDown); + } + }); + }; + var hide = $typeahead.hide; + $typeahead.hide = function () { + $typeahead.$element.off('mousedown', $typeahead.$onMouseDown); + if (options.keyboard) { + element.off('keydown', $typeahead.$onKeyDown); + } + hide(); + }; + return $typeahead; + } + TypeaheadFactory.defaults = defaults; + return TypeaheadFactory; + } + ]; + }).directive('bsTypeahead', [ + '$window', + '$parse', + '$q', + '$typeahead', + '$parseOptions', + function ($window, $parse, $q, $typeahead, $parseOptions) { + var defaults = $typeahead.defaults; + return { + restrict: 'EAC', + require: 'ngModel', + link: function postLink(scope, element, attr, controller) { + // Directive options + var options = { + scope: scope, + controller: controller + }; + angular.forEach([ + 'placement', + 'container', + 'delay', + 'trigger', + 'keyboard', + 'html', + 'animation', + 'template', + 'filter', + 'limit', + 'minLength' + ], function (key) { + if (angular.isDefined(attr[key])) + options[key] = attr[key]; + }); + // Build proper ngOptions + var filter = options.filter || defaults.filter; + var limit = options.limit || defaults.limit; + var ngOptions = attr.ngOptions; + if (filter) + ngOptions += ' | ' + filter + ':$viewValue'; + if (limit) + ngOptions += ' | limitTo:' + limit; + var parsedOptions = $parseOptions(ngOptions); + // Initialize typeahead + var typeahead = $typeahead(element, options); + // Watch model for changes + scope.$watch(attr.ngModel, function (newValue, oldValue) { + parsedOptions.valuesFn(scope, controller).then(function (values) { + if (values.length > limit) + values = values.slice(0, limit); + // if(matches.length === 1 && matches[0].value === newValue) return; + typeahead.update(values); + }); + }); + // Garbage collection + scope.$on('$destroy', function () { + typeahead.destroy(); + options = null; + typeahead = null; + }); + } + }; + } + ]); +}(window, document)); \ No newline at end of file diff --git a/static/lib/angular-strap.min.js b/static/lib/angular-strap.min.js index 08b9322ad..c358b9e1c 100644 --- a/static/lib/angular-strap.min.js +++ b/static/lib/angular-strap.min.js @@ -1,8 +1,10 @@ /** - * AngularStrap - Twitter Bootstrap directives for AngularJS - * @version v0.7.5 - 2013-07-21 - * @link http://mgcrea.github.com/angular-strap - * @author Olivier Louvignes + * angular-strap + * @version v2.0.0-rc.4 - 2014-03-07 + * @link http://mgcrea.github.io/angular-strap + * @author Olivier Louvignes (olivier@mg-crea.com) * @license MIT License, http://www.opensource.org/licenses/MIT */ -angular.module("$strap.config",[]).value("$strapConfig",{}),angular.module("$strap.filters",["$strap.config"]),angular.module("$strap.directives",["$strap.config"]),angular.module("$strap",["$strap.filters","$strap.directives","$strap.config"]),angular.module("$strap.directives").directive("bsAlert",["$parse","$timeout","$compile",function(t,e,n){return{restrict:"A",link:function(a,i,o){var r=t(o.bsAlert),s=(r.assign,r(a)),l=function(t){e(function(){i.alert("close")},1*t)};o.bsAlert?a.$watch(o.bsAlert,function(t,e){s=t,i.html((t.title?""+t.title+" ":"")+t.content||""),t.closed&&i.hide(),n(i.contents())(a),(t.type||e.type)&&(e.type&&i.removeClass("alert-"+e.type),t.type&&i.addClass("alert-"+t.type)),angular.isDefined(t.closeAfter)?l(t.closeAfter):o.closeAfter&&l(o.closeAfter),(angular.isUndefined(o.closeButton)||"0"!==o.closeButton&&"false"!==o.closeButton)&&i.prepend('')},!0):((angular.isUndefined(o.closeButton)||"0"!==o.closeButton&&"false"!==o.closeButton)&&i.prepend(''),o.closeAfter&&l(o.closeAfter)),i.addClass("alert").alert(),i.hasClass("fade")&&(i.removeClass("in"),setTimeout(function(){i.addClass("in")}));var u=o.ngRepeat&&o.ngRepeat.split(" in ").pop();i.on("close",function(t){var e;u?(t.preventDefault(),i.removeClass("in"),e=function(){i.trigger("closed"),a.$parent&&a.$parent.$apply(function(){for(var t=u.split("."),e=a.$parent,n=0;t.length>n;++n)e&&(e=e[t[n]]);e&&e.splice(a.$index,1)})},$.support.transition&&i.hasClass("fade")?i.on($.support.transition.end,e):e()):s&&(t.preventDefault(),i.removeClass("in"),e=function(){i.trigger("closed"),a.$apply(function(){s.closed=!0})},$.support.transition&&i.hasClass("fade")?i.on($.support.transition.end,e):e())})}}}]),angular.module("$strap.directives").directive("bsButton",["$parse","$timeout",function(t){return{restrict:"A",require:"?ngModel",link:function(e,n,a,i){if(i){n.parent('[data-toggle="buttons-checkbox"], [data-toggle="buttons-radio"]').length||n.attr("data-toggle","button");var o=!!e.$eval(a.ngModel);o&&n.addClass("active"),e.$watch(a.ngModel,function(t,e){var a=!!t,i=!!e;a!==i?$.fn.button.Constructor.prototype.toggle.call(r):a&&!o&&n.addClass("active")})}n.hasClass("btn")||n.on("click.button.data-api",function(){n.button("toggle")}),n.button();var r=n.data("button");r.toggle=function(){if(!i)return $.fn.button.Constructor.prototype.toggle.call(this);var a=n.parent('[data-toggle="buttons-radio"]');a.length?(n.siblings("[ng-model]").each(function(n,a){t($(a).attr("ng-model")).assign(e,!1)}),e.$digest(),i.$modelValue||(i.$setViewValue(!i.$modelValue),e.$digest())):e.$apply(function(){i.$setViewValue(!i.$modelValue)})}}}}]).directive("bsButtonsCheckbox",["$parse",function(){return{restrict:"A",require:"?ngModel",compile:function(t){t.attr("data-toggle","buttons-checkbox").find("a, button").each(function(t,e){$(e).attr("bs-button","")})}}}]).directive("bsButtonsRadio",["$timeout",function(t){return{restrict:"A",require:"?ngModel",compile:function(e,n){return e.attr("data-toggle","buttons-radio"),n.ngModel||e.find("a, button").each(function(t,e){$(e).attr("bs-button","")}),function(e,n,a,i){i&&(t(function(){n.find("[value]").button().filter('[value="'+i.$viewValue+'"]').addClass("active")}),n.on("click.button.data-api",function(t){e.$apply(function(){i.$setViewValue($(t.target).closest("button").attr("value"))})}),e.$watch(a.ngModel,function(t,i){if(t!==i){var o=n.find('[value="'+e.$eval(a.ngModel)+'"]');o.length&&o.button("toggle")}}))}}}}]),angular.module("$strap.directives").directive("bsButtonSelect",["$parse","$timeout",function(t){return{restrict:"A",require:"?ngModel",link:function(e,n,a,i){var o=t(a.bsButtonSelect);o.assign,i&&(n.text(e.$eval(a.ngModel)),e.$watch(a.ngModel,function(t){n.text(t)}));var r,s,l,u;n.bind("click",function(){r=o(e),s=i?e.$eval(a.ngModel):n.text(),l=r.indexOf(s),u=l>r.length-2?r[0]:r[l+1],e.$apply(function(){n.text(u),i&&i.$setViewValue(u)})})}}}]),angular.module("$strap.directives").directive("bsDatepicker",["$timeout","$strapConfig",function(t,e){var n=/(iP(a|o)d|iPhone)/g.test(navigator.userAgent),a=function a(t){return t=t||"en",{"/":"[\\/]","-":"[-]",".":"[.]"," ":"[\\s]",dd:"(?:(?:[0-2]?[0-9]{1})|(?:[3][01]{1}))",d:"(?:(?:[0-2]?[0-9]{1})|(?:[3][01]{1}))",mm:"(?:[0]?[1-9]|[1][012])",m:"(?:[0]?[1-9]|[1][012])",DD:"(?:"+$.fn.datepicker.dates[t].days.join("|")+")",D:"(?:"+$.fn.datepicker.dates[t].daysShort.join("|")+")",MM:"(?:"+$.fn.datepicker.dates[t].months.join("|")+")",M:"(?:"+$.fn.datepicker.dates[t].monthsShort.join("|")+")",yyyy:"(?:(?:[1]{1}[0-9]{1}[0-9]{1}[0-9]{1})|(?:[2]{1}[0-9]{3}))(?![[0-9]])",yy:"(?:(?:[0-9]{1}[0-9]{1}))(?![[0-9]])"}},i=function i(t,e){var n,i=t,o=a(e);return n=0,angular.forEach(o,function(t,e){i=i.split(e).join("${"+n+"}"),n++}),n=0,angular.forEach(o,function(t){i=i.split("${"+n+"}").join(t),n++}),RegExp("^"+i+"$",["i"])};return{restrict:"A",require:"?ngModel",link:function(t,a,o,r){var s=angular.extend({autoclose:!0},e.datepicker||{}),l=o.dateType||s.type||"date";angular.forEach(["format","weekStart","calendarWeeks","startDate","endDate","daysOfWeekDisabled","autoclose","startView","minViewMode","todayBtn","todayHighlight","keyboardNavigation","language","forceParse"],function(t){angular.isDefined(o[t])&&(s[t]=o[t])});var u=s.language||"en",c=o.dateFormat||s.format||$.fn.datepicker.dates[u]&&$.fn.datepicker.dates[u].format||"mm/dd/yyyy",d=n?"yyyy-mm-dd":c,p=i(d,u);r&&(r.$formatters.unshift(function(t){return"date"===l&&angular.isString(t)&&t?$.fn.datepicker.DPGlobal.parseDate(t,$.fn.datepicker.DPGlobal.parseFormat(c),u):t}),r.$parsers.unshift(function(t){return t?"date"===l&&angular.isDate(t)?(r.$setValidity("date",!0),t):angular.isString(t)&&p.test(t)?(r.$setValidity("date",!0),n?new Date(t):"string"===l?t:$.fn.datepicker.DPGlobal.parseDate(t,$.fn.datepicker.DPGlobal.parseFormat(d),u)):(r.$setValidity("date",!1),void 0):(r.$setValidity("date",!0),null)}),r.$render=function(){if(n){var t=r.$viewValue?$.fn.datepicker.DPGlobal.formatDate(r.$viewValue,$.fn.datepicker.DPGlobal.parseFormat(d),u):"";return a.val(t),t}return r.$viewValue||a.val(""),a.datepicker("update",r.$viewValue)}),n?a.prop("type","date").css("-webkit-appearance","textfield"):(r&&a.on("changeDate",function(e){t.$apply(function(){r.$setViewValue("string"===l?a.val():e.date)})}),a.datepicker(angular.extend(s,{format:d,language:u})),t.$on("$destroy",function(){var t=a.data("datepicker");t&&(t.picker.remove(),a.data("datepicker",null))}),o.$observe("startDate",function(t){a.datepicker("setStartDate",t)}),o.$observe("endDate",function(t){a.datepicker("setEndDate",t)}));var f=a.siblings('[data-toggle="datepicker"]');f.length&&f.on("click",function(){a.prop("disabled")||a.trigger("focus")})}}}]),angular.module("$strap.directives").directive("bsDropdown",["$parse","$compile","$timeout",function(t,e,n){var a=function(t,e){return e||(e=['"]),angular.forEach(t,function(t,n){if(t.divider)return e.splice(n+1,0,'
  • ');var i=""+'"+(t.text||"")+"";t.submenu&&t.submenu.length&&(i+=a(t.submenu).join("\n")),i+="",e.splice(n+1,0,i)}),e};return{restrict:"EA",scope:!0,link:function(i,o,r){var s=t(r.bsDropdown),l=s(i);n(function(){!angular.isArray(l);var t=angular.element(a(l).join(""));t.insertAfter(o),e(o.next("ul.dropdown-menu"))(i)}),o.addClass("dropdown-toggle").attr("data-toggle","dropdown")}}}]),angular.module("$strap.directives").factory("$modal",["$rootScope","$compile","$http","$timeout","$q","$templateCache","$strapConfig",function(t,e,n,a,i,o,r){var s=function s(s){function l(s){var l=angular.extend({show:!0},r.modal,s),u=l.scope?l.scope:t.$new(),c=l.template;return i.when(o.get(c)||n.get(c,{cache:!0}).then(function(t){return t.data})).then(function(t){var n=c.replace(".html","").replace(/[\/|\.|:]/g,"-")+"-"+u.$id,i=$('').attr("id",n).addClass("fade").html(t);return l.modalClass&&i.addClass(l.modalClass),$("body").append(i),a(function(){e(i)(u)}),u.$modal=function(t){i.modal(t)},angular.forEach(["show","hide"],function(t){u[t]=function(){i.modal(t)}}),u.dismiss=u.hide,angular.forEach(["show","shown","hide","hidden"],function(t){i.on(t,function(e){u.$emit("modal-"+t,e)})}),i.on("shown",function(){$("input[autofocus], textarea[autofocus]",i).first().trigger("focus")}),i.on("hidden",function(){l.persist||u.$destroy()}),u.$on("$destroy",function(){i.remove()}),i.modal(l),i})}return new l(s)};return s}]).directive("bsModal",["$q","$modal",function(t,e){return{restrict:"A",scope:!0,link:function(n,a,i){var o={template:n.$eval(i.bsModal),persist:!0,show:!1,scope:n};angular.forEach(["modalClass","backdrop","keyboard"],function(t){angular.isDefined(i[t])&&(o[t]=i[t])}),t.when(e(o)).then(function(t){a.attr("data-target","#"+t.attr("id")).attr("data-toggle","modal")})}}}]),angular.module("$strap.directives").directive("bsNavbar",["$location",function(t){return{restrict:"A",link:function(e,n){e.$watch(function(){return t.path()},function(t){$("li[data-match-route]",n).each(function(e,n){var a=angular.element(n),i=a.attr("data-match-route"),o=RegExp("^"+i+"$",["i"]);o.test(t)?a.addClass("active").find(".collapse.in").collapse("hide"):a.removeClass("active")})})}}}]),angular.module("$strap.directives").directive("bsPopover",["$parse","$compile","$http","$timeout","$q","$templateCache",function(t,e,n,a,i,o){return $("body").on("keyup",function(t){27===t.keyCode&&$(".popover.in").each(function(){$(this).popover("hide")})}),{restrict:"A",scope:!0,link:function(r,s,l){var u=t(l.bsPopover),c=(u.assign,u(r)),d={};angular.isObject(c)&&(d=c),i.when(d.content||o.get(c)||n.get(c,{cache:!0})).then(function(t){angular.isObject(t)&&(t=t.data),l.unique&&s.on("show",function(){$(".popover.in").each(function(){var t=$(this),e=t.data("bs.popover");e&&!e.$element.is(s)&&t.popover("hide")})}),l.hide&&r.$watch(l.hide,function(t,e){t?n.hide():t!==e&&n.show()}),l.show&&r.$watch(l.show,function(t,e){t?a(function(){n.show()}):t!==e&&n.hide()}),s.popover(angular.extend({},d,{content:t,html:!0}));var n=s.data("bs.popover");n.hasContent=function(){return this.getTitle()||t},n.getPosition=function(){var t=$.fn.popover.Constructor.prototype.getPosition.apply(this,arguments);return e(this.$tip)(r),r.$digest(),this.$tip.data("bs.popover",this),t},r.$popover=function(t){n(t)},angular.forEach(["show","hide"],function(t){r[t]=function(){n[t]()}}),r.dismiss=r.hide,angular.forEach(["show","shown","hide","hidden"],function(t){s.on(t,function(e){r.$emit("popover-"+t,e)})})})}}}]),angular.module("$strap.directives").directive("bsSelect",["$timeout",function(t){return{restrict:"A",require:"?ngModel",link:function(e,n,a,i){var o=e.$eval(a.bsSelect)||{};t(function(){n.selectpicker(o),n.next().removeClass("ng-scope")}),i&&e.$watch(a.ngModel,function(t,e){angular.equals(t,e)||n.selectpicker("refresh")})}}}]),angular.module("$strap.directives").directive("bsTabs",["$parse","$compile","$timeout",function(t,e,n){var a='
    ';return{restrict:"A",require:"?ngModel",priority:0,scope:!0,template:a,replace:!0,transclude:!0,compile:function(){return function(e,a,i,o){var r=t(i.bsTabs);r.assign,r(e),e.panes=[];var s,l,u,c=a.find("ul.nav-tabs"),d=a.find("div.tab-content"),p=0;n(function(){d.find("[data-title], [data-tab]").each(function(t){var n=angular.element(this);s="tab-"+e.$id+"-"+t,l=n.data("title")||n.data("tab"),u=!u&&n.hasClass("active"),n.attr("id",s).addClass("tab-pane"),i.fade&&n.addClass("fade"),e.panes.push({id:s,title:l,content:this.innerHTML,active:u})}),e.panes.length&&!u&&(d.find(".tab-pane:first-child").addClass("active"+(i.fade?" in":"")),e.panes[0].active=!0)}),o&&(a.on("show",function(t){var n=$(t.target);e.$apply(function(){o.$setViewValue(n.data("index"))})}),e.$watch(i.ngModel,function(t){angular.isUndefined(t)||(p=t,setTimeout(function(){var e=$(c[0].querySelectorAll("li")[1*t]);e.hasClass("active")||e.children("a").tab("show")}))}))}}}}]),angular.module("$strap.directives").directive("bsTimepicker",["$timeout","$strapConfig",function(t,e){var n="((?:(?:[0-1][0-9])|(?:[2][0-3])|(?:[0-9])):(?:[0-5][0-9])(?::[0-5][0-9])?(?:\\s?(?:am|AM|pm|PM))?)";return{restrict:"A",require:"?ngModel",link:function(a,i,o,r){if(r){i.on("changeTime.timepicker",function(){t(function(){r.$setViewValue(i.val())})});var s=RegExp("^"+n+"$",["i"]);r.$parsers.unshift(function(t){return!t||s.test(t)?(r.$setValidity("time",!0),t):(r.$setValidity("time",!1),void 0)})}i.attr("data-toggle","timepicker"),i.parent().addClass("bootstrap-timepicker"),i.timepicker(e.timepicker||{});var l=i.data("timepicker"),u=i.siblings('[data-toggle="timepicker"]');u.length&&u.on("click",$.proxy(l.showWidget,l))}}}]),angular.module("$strap.directives").directive("bsTooltip",["$parse","$compile",function(t){return{restrict:"A",scope:!0,link:function(e,n,a){var i=t(a.bsTooltip),o=(i.assign,i(e));e.$watch(a.bsTooltip,function(t,e){t!==e&&(o=t)}),a.unique&&n.on("show",function(){$(".tooltip.in").each(function(){var t=$(this),e=t.data("bs.tooltip");e&&!e.$element.is(n)&&t.tooltip("hide")})}),n.tooltip({title:function(){return angular.isFunction(o)?o.apply(null,arguments):o},html:!0});var r=n.data("bs.tooltip");r.show=function(){var t=$.fn.tooltip.Constructor.prototype.show.apply(this,arguments);return this.tip().data("bs.tooltip",this),t},e._tooltip=function(t){n.tooltip(t)},e.hide=function(){n.tooltip("hide")},e.show=function(){n.tooltip("show")},e.dismiss=e.hide}}}]),angular.module("$strap.directives").directive("bsTypeahead",["$parse",function(t){return{restrict:"A",require:"?ngModel",link:function(e,n,a,i){var o=t(a.bsTypeahead),r=(o.assign,o(e));e.$watch(a.bsTypeahead,function(t,e){t!==e&&(r=t)}),n.attr("data-provide","typeahead"),n.typeahead({source:function(){return angular.isFunction(r)?r.apply(null,arguments):r},minLength:a.minLength||1,items:a.items,updater:function(t){return i&&e.$apply(function(){i.$setViewValue(t)}),e.$emit("typeahead-updated",t),t}});var s=n.data("typeahead");s.lookup=function(){var t;return this.query=this.$element.val()||"",this.query.length=d?"top":null!==a&&d+a<=b.top?"middle":null!==u&&b.top+c+n>=e-u?"bottom":"middle"}function h(){return l[0]===b?b.pageYOffset:l[0]===b}function i(){return l[0]===b?b.document.body.scrollHeight:l[0].scrollHeight}var j={},k=angular.extend({},a,f),l=k.target,m="affix affix-top affix-bottom",n=0,o=0,p=null,q=null,r=d.parent();if(k.offsetParent)if(k.offsetParent.match(/^\d+$/))for(var s=0;s<1*k.offsetParent-1;s++)r=r.parent();else r=angular.element(k.offsetParent);var t=0;k.offsetTop&&("auto"===k.offsetTop&&(k.offsetTop="+0"),k.offsetTop.match(/^[-+]\d+$/)?(n-=1*k.offsetTop,t=k.offsetParent?c.offset(r[0]).top+1*k.offsetTop:c.offset(d[0]).top-c.css(d[0],"marginTop",!0)+1*k.offsetTop):t=1*k.offsetTop);var u=0;return k.offsetBottom&&(u=k.offsetParent&&k.offsetBottom.match(/^[-+]\d+$/)?i()-(c.offset(r[0]).top+c.height(r[0]))+1*k.offsetBottom+1:1*k.offsetBottom),j.init=function(){o=c.offset(d[0]).top+n,l.on("scroll",this.checkPosition),l.on("click",this.checkPositionWithEventLoop),this.checkPosition(),this.checkPositionWithEventLoop()},j.destroy=function(){l.off("scroll",this.checkPosition),l.off("click",this.checkPositionWithEventLoop)},j.checkPositionWithEventLoop=function(){setTimeout(this.checkPosition,1)},j.checkPosition=function(){var a=h(),b=c.offset(d[0]),f=c.height(d[0]),i=g(q,b,f);p!==i&&(p=i,d.removeClass(m).addClass("affix"+("middle"!==i?"-"+i:"")),"top"===i?(q=null,d.css("position",k.offsetParent?"":"relative"),d.css("top","")):"bottom"===i?(q=k.offsetUnpin?-(1*k.offsetUnpin):b.top-a,d.css("position",k.offsetParent?"":"relative"),d.css("top",k.offsetParent?"":e[0].offsetHeight-u-f-o+"px")):(q=null,d.css("position","fixed"),d.css("top",n+"px")))},j.init(),j}var e=angular.element(b.document.body);return d}]}).directive("bsAffix",["$affix","$window",function(a,b){return{restrict:"EAC",require:"^?bsAffixTarget",link:function(c,d,e,f){var g={scope:c,offsetTop:"auto",target:f?f.$element:angular.element(b)};angular.forEach(["offsetTop","offsetBottom","offsetParent","offsetUnpin"],function(a){angular.isDefined(e[a])&&(g[a]=e[a])});var h=a(d,g);c.$on("$destroy",function(){g=null,h=null})}}}]).directive("bsAffixTarget",function(){return{controller:["$element",function(a){this.$element=a}]}}),angular.module("mgcrea.ngStrap.alert",[]).provider("$alert",function(){var a=this.defaults={animation:"am-fade",prefixClass:"alert",placement:null,template:"alert/alert.tpl.html",container:!1,element:null,backdrop:!1,keyboard:!0,show:!0,duration:!1,type:!1};this.$get=["$modal","$timeout",function(b,c){function d(d){var e={},f=angular.extend({},a,d);e=b(f),f.type&&(e.$scope.type=f.type);var g=e.show;return f.duration&&(e.show=function(){g(),c(function(){e.hide()},1e3*f.duration)}),e}return d}]}).directive("bsAlert",["$window","$location","$sce","$alert",function(a,b,c,d){a.requestAnimationFrame||a.setTimeout;return{restrict:"EAC",scope:!0,link:function(a,b,e){var f={scope:a,element:b,show:!1};angular.forEach(["template","placement","keyboard","html","container","animation","duration"],function(a){angular.isDefined(e[a])&&(f[a]=e[a])}),angular.forEach(["title","content","type"],function(b){e[b]&&e.$observe(b,function(d){a[b]=c.trustAsHtml(d)})}),e.bsAlert&&a.$watch(e.bsAlert,function(b){angular.isObject(b)?angular.extend(a,b):a.content=b},!0);var g=d(f);b.on(e.trigger||"click",g.toggle),a.$on("$destroy",function(){g.destroy(),f=null,g=null})}}}]),angular.module("mgcrea.ngStrap.aside",["mgcrea.ngStrap.modal"]).provider("$aside",function(){var a=this.defaults={animation:"am-fade-and-slide-right",prefixClass:"aside",placement:"right",template:"aside/aside.tpl.html",contentTemplate:!1,container:!1,element:null,backdrop:!0,keyboard:!0,html:!1,show:!0};this.$get=["$modal",function(b){function c(c){var d={},e=angular.extend({},a,c);return d=b(e)}return c}]}).directive("bsAside",["$window","$location","$sce","$aside",function(a,b,c,d){a.requestAnimationFrame||a.setTimeout;return{restrict:"EAC",scope:!0,link:function(a,b,e){var f={scope:a,element:b,show:!1};angular.forEach(["template","contentTemplate","placement","backdrop","keyboard","html","container","animation"],function(a){angular.isDefined(e[a])&&(f[a]=e[a])}),angular.forEach(["title","content"],function(b){e[b]&&e.$observe(b,function(d){a[b]=c.trustAsHtml(d)})}),e.bsAside&&a.$watch(e.bsAside,function(b){angular.isObject(b)?angular.extend(a,b):a.content=b},!0);var g=d(f);b.on(e.trigger||"click",g.toggle),a.$on("$destroy",function(){g.destroy(),f=null,g=null})}}}]),angular.module("mgcrea.ngStrap.button",["ngAnimate"]).provider("$button",function(){var a=this.defaults={activeClass:"active",toggleEvent:"click"};this.$get=function(){return{defaults:a}}}).directive("bsCheckboxGroup",function(){return{restrict:"A",require:"ngModel",compile:function(a,b){a.attr("data-toggle","buttons"),a.removeAttr("ng-model");var c=a[0].querySelectorAll('input[type="checkbox"]');angular.forEach(c,function(a){var c=angular.element(a);c.attr("bs-checkbox",""),c.attr("ng-model",b.ngModel+"."+c.attr("value"))})}}}).directive("bsCheckbox",["$button","$$animateReflow",function(a,b){var c=a.defaults,d=/^(true|false|\d+)$/;return{restrict:"A",require:"ngModel",link:function(a,e,f,g){var h=c,i="INPUT"===e[0].nodeName,j=i?e.parent():e,k=angular.isDefined(f.trueValue)?f.trueValue:!0;d.test(f.trueValue)&&(k=a.$eval(f.trueValue));var l=angular.isDefined(f.falseValue)?f.falseValue:!1;d.test(f.falseValue)&&(l=a.$eval(f.falseValue));var m="boolean"!=typeof k||"boolean"!=typeof l;m&&(g.$parsers.push(function(a){return a?k:l}),a.$watch(f.ngModel,function(){g.$render()})),g.$render=function(){var a=angular.equals(g.$modelValue,k);b(function(){i&&(e[0].checked=a),j.toggleClass(h.activeClass,a)})},e.bind(h.toggleEvent,function(){a.$apply(function(){i||g.$setViewValue(!j.hasClass("active")),m||g.$render()})})}}}]).directive("bsRadioGroup",function(){return{restrict:"A",require:"ngModel",compile:function(a,b){a.attr("data-toggle","buttons"),a.removeAttr("ng-model");var c=a[0].querySelectorAll('input[type="radio"]');angular.forEach(c,function(a){angular.element(a).attr("bs-radio",""),angular.element(a).attr("ng-model",b.ngModel)})}}}).directive("bsRadio",["$button","$$animateReflow",function(a,b){var c=a.defaults,d=/^(true|false|\d+)$/;return{restrict:"A",require:"ngModel",link:function(a,e,f,g){var h=c,i="INPUT"===e[0].nodeName,j=i?e.parent():e,k=d.test(f.value)?a.$eval(f.value):f.value;g.$render=function(){var a=angular.equals(g.$modelValue,k);b(function(){i&&(e[0].checked=a),j.toggleClass(h.activeClass,a)})},e.bind(h.toggleEvent,function(){a.$apply(function(){g.$setViewValue(k),g.$render()})})}}}]),angular.module("mgcrea.ngStrap.datepicker",["mgcrea.ngStrap.helpers.dateParser","mgcrea.ngStrap.tooltip"]).provider("$datepicker",function(){var a=this.defaults={animation:"am-fade",prefixClass:"datepicker",placement:"bottom-left",template:"datepicker/datepicker.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,useNative:!1,dateType:"date",dateFormat:"shortDate",strictFormat:!1,autoclose:!1,minDate:-1/0,maxDate:+1/0,startView:0,minView:0,startWeek:0};this.$get=["$window","$document","$rootScope","$sce","$locale","dateFilter","datepickerViews","$tooltip",function(b,c,d,e,f,g,h,i){function j(b,c,d){function e(a){a.selected=g.$isSelected(a.date)}function f(){b[0].focus()}var g=i(b,angular.extend({},a,d)),j=d.scope,m=g.$options,n=g.$scope;m.startView&&(m.startView-=m.minView);var o=h(g);g.$views=o.views;var p=o.viewDate;n.$mode=m.startView;var q=g.$views[n.$mode];n.$select=function(a){g.select(a)},n.$selectPane=function(a){g.$selectPane(a)},n.$toggleMode=function(){g.setMode((n.$mode+1)%g.$views.length)},g.update=function(a){angular.isDate(a)&&!isNaN(a.getTime())&&(g.$date=a,q.update.call(q,a)),g.$build(!0)},g.select=function(a,b){angular.isDate(c.$dateValue)||(c.$dateValue=new Date(a)),c.$dateValue.setFullYear(a.getFullYear(),a.getMonth(),a.getDate()),!n.$mode||b?(c.$setViewValue(c.$dateValue),c.$render(),m.autoclose&&!b&&g.hide(!0)):(angular.extend(p,{year:a.getFullYear(),month:a.getMonth(),date:a.getDate()}),g.setMode(n.$mode-1),g.$build())},g.setMode=function(a){n.$mode=a,q=g.$views[n.$mode],g.$build()},g.$build=function(a){a===!0&&q.built||(a!==!1||q.built)&&q.build.call(q)},g.$updateSelected=function(){for(var a=0,b=n.rows.length;b>a;a++)angular.forEach(n.rows[a],e)},g.$isSelected=function(a){return q.isSelected(a)},g.$selectPane=function(a){var b=q.steps,c=new Date(Date.UTC(p.year+(b.year||0)*a,p.month+(b.month||0)*a,p.date+(b.day||0)*a));angular.extend(p,{year:c.getUTCFullYear(),month:c.getUTCMonth(),date:c.getUTCDate()}),g.$build()},g.$onMouseDown=function(a){if(a.preventDefault(),a.stopPropagation(),k){var b=angular.element(a.target);"button"!==b[0].nodeName.toLowerCase()&&(b=b.parent()),b.triggerHandler("click")}},g.$onKeyDown=function(a){if(/(38|37|39|40|13)/.test(a.keyCode)&&!a.shiftKey&&!a.altKey){if(a.preventDefault(),a.stopPropagation(),13===a.keyCode)return n.$mode?n.$apply(function(){g.setMode(n.$mode-1)}):g.hide(!0);q.onKeyDown(a),j.$digest()}};var r=g.init;g.init=function(){return l&&m.useNative?(b.prop("type","date"),void b.css("-webkit-appearance","textfield")):(k&&(b.prop("type","text"),b.attr("readonly","true"),b.on("click",f)),void r())};var s=g.destroy;g.destroy=function(){l&&m.useNative&&b.off("click",f),s()};var t=g.show;g.show=function(){t(),setTimeout(function(){g.$element.on(k?"touchstart":"mousedown",g.$onMouseDown),m.keyboard&&b.on("keydown",g.$onKeyDown)})};var u=g.hide;return g.hide=function(a){g.$element.off(k?"touchstart":"mousedown",g.$onMouseDown),m.keyboard&&b.off("keydown",g.$onKeyDown),u(a)},g}var k=(angular.element(b.document.body),"createTouch"in b.document),l=/(ip(a|o)d|iphone|android)/gi.test(b.navigator.userAgent);return a.lang||(a.lang=f.id),j.defaults=a,j}]}).directive("bsDatepicker",["$window","$parse","$q","$locale","dateFilter","$datepicker","$dateParser","$timeout",function(a,b,c,d,e,f,g){{var h=(f.defaults,/(ip(a|o)d|iphone|android)/gi.test(a.navigator.userAgent));a.requestAnimationFrame||a.setTimeout}return{restrict:"EAC",require:"ngModel",link:function(a,b,c,d){var i={scope:a,controller:d};angular.forEach(["placement","container","delay","trigger","keyboard","html","animation","template","autoclose","dateType","dateFormat","strictFormat","startWeek","useNative","lang","startView","minView"],function(a){angular.isDefined(c[a])&&(i[a]=c[a])}),h&&i.useNative&&(i.dateFormat="yyyy-MM-dd");var j=f(b,d,i);i=j.$options,angular.forEach(["minDate","maxDate"],function(a){angular.isDefined(c[a])&&c.$observe(a,function(b){if("today"===b){var c=new Date;j.$options[a]=+new Date(c.getFullYear(),c.getMonth(),c.getDate()+("maxDate"===a?1:0),0,0,0,"minDate"===a?0:-1)}else j.$options[a]=angular.isString(b)&&b.match(/^".+"$/)?+new Date(b.substr(1,b.length-2)):+new Date(b);!isNaN(j.$options[a])&&j.$build(!1)})}),a.$watch(c.ngModel,function(){j.update(d.$dateValue)},!0);var k=g({format:i.dateFormat,lang:i.lang,strict:i.strictFormat});d.$parsers.unshift(function(a){if(!a)return void d.$setValidity("date",!0);var b=k.parse(a,d.$dateValue);if(!b||isNaN(b.getTime()))d.$setValidity("date",!1);else{var c=b.getTime()>=i.minDate&&b.getTime()<=i.maxDate;d.$setValidity("date",c),c&&(d.$dateValue=b)}return"string"===i.dateType?e(a,i.dateFormat):"number"===i.dateType?d.$dateValue.getTime():"iso"===i.dateType?d.$dateValue.toISOString():new Date(d.$dateValue)}),d.$formatters.push(function(a){if(!angular.isUndefined(a)&&null!==a){var b=angular.isDate(a)?a:new Date(a);return d.$dateValue=b,d.$dateValue}}),d.$render=function(){b.val(!d.$dateValue||isNaN(d.$dateValue.getTime())?"":e(d.$dateValue,i.dateFormat))},a.$on("$destroy",function(){j.destroy(),i=null,j=null})}}}]).provider("datepickerViews",function(){function a(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c}this.defaults={dayFormat:"dd",daySplit:7};this.$get=["$locale","$sce","dateFilter",function(b,c,d){return function(e){var f=e.$scope,g=e.$options,h=b.DATETIME_FORMATS.SHORTDAY,i=h.slice(g.startWeek).concat(h.slice(0,g.startWeek)),j=c.trustAsHtml(''+i.join('')+""),k=e.$date||new Date,l={year:k.getFullYear(),month:k.getMonth(),date:k.getDate()},m=(6e4*k.getTimezoneOffset(),[{format:"dd",split:7,steps:{month:1},update:function(a,b){!this.built||b||a.getFullYear()!==l.year||a.getMonth()!==l.month?(angular.extend(l,{year:e.$date.getFullYear(),month:e.$date.getMonth(),date:e.$date.getDate()}),e.$build()):a.getDate()!==l.date&&(l.date=e.$date.getDate(),e.$updateSelected())},build:function(){for(var b,c=new Date(l.year,l.month,1),h=new Date(+c-864e5*(c.getDay()-g.startWeek)),i=[],k=0;42>k;k++)b=new Date(h.getFullYear(),h.getMonth(),h.getDate()+k),i.push({date:b,label:d(b,this.format),selected:e.$date&&this.isSelected(b),muted:b.getMonth()!==l.month,disabled:this.isDisabled(b)});f.title=d(c,"MMMM yyyy"),f.labels=j,f.rows=a(i,this.split),this.built=!0},isSelected:function(a){return e.$date&&a.getFullYear()===e.$date.getFullYear()&&a.getMonth()===e.$date.getMonth()&&a.getDate()===e.$date.getDate()},isDisabled:function(a){return a.getTime()g.maxDate},onKeyDown:function(a){var b=e.$date.getTime();37===a.keyCode?e.select(new Date(b-864e5),!0):38===a.keyCode?e.select(new Date(b-6048e5),!0):39===a.keyCode?e.select(new Date(b+864e5),!0):40===a.keyCode&&e.select(new Date(b+6048e5),!0)}},{name:"month",format:"MMM",split:4,steps:{year:1},update:function(a){this.built&&a.getFullYear()===l.year?a.getMonth()!==l.month&&(angular.extend(l,{month:e.$date.getMonth(),date:e.$date.getDate()}),e.$updateSelected()):(angular.extend(l,{year:e.$date.getFullYear(),month:e.$date.getMonth(),date:e.$date.getDate()}),e.$build())},build:function(){for(var b,c=(new Date(l.year,0,1),[]),g=0;12>g;g++)b=new Date(l.year,g,1),c.push({date:b,label:d(b,this.format),selected:e.$isSelected(b),disabled:this.isDisabled(b)});f.title=d(b,"yyyy"),f.labels=!1,f.rows=a(c,this.split),this.built=!0},isSelected:function(a){return e.$date&&a.getFullYear()===e.$date.getFullYear()&&a.getMonth()===e.$date.getMonth()},isDisabled:function(a){var b=+new Date(a.getFullYear(),a.getMonth()+1,0);return bg.maxDate},onKeyDown:function(a){var b=e.$date.getMonth();37===a.keyCode?e.select(e.$date.setMonth(b-1),!0):38===a.keyCode?e.select(e.$date.setMonth(b-4),!0):39===a.keyCode?e.select(e.$date.setMonth(b+1),!0):40===a.keyCode&&e.select(e.$date.setMonth(b+4),!0)}},{name:"year",format:"yyyy",split:4,steps:{year:12},update:function(a,b){!this.built||b||parseInt(a.getFullYear()/20,10)!==parseInt(l.year/20,10)?(angular.extend(l,{year:e.$date.getFullYear(),month:e.$date.getMonth(),date:e.$date.getDate()}),e.$build()):a.getFullYear()!==l.year&&(angular.extend(l,{year:e.$date.getFullYear(),month:e.$date.getMonth(),date:e.$date.getDate()}),e.$updateSelected())},build:function(){for(var b,c=l.year-l.year%(3*this.split),g=[],h=0;12>h;h++)b=new Date(c+h,0,1),g.push({date:b,label:d(b,this.format),selected:e.$isSelected(b),disabled:this.isDisabled(b)});f.title=g[0].label+"-"+g[g.length-1].label,f.labels=!1,f.rows=a(g,this.split),this.built=!0},isSelected:function(a){return e.$date&&a.getFullYear()===e.$date.getFullYear()},isDisabled:function(a){var b=+new Date(a.getFullYear()+1,0,0);return bg.maxDate},onKeyDown:function(a){var b=e.$date.getFullYear();37===a.keyCode?e.select(e.$date.setYear(b-1),!0):38===a.keyCode?e.select(e.$date.setYear(b-4),!0):39===a.keyCode?e.select(e.$date.setYear(b+1),!0):40===a.keyCode&&e.select(e.$date.setYear(b+4),!0)}}]);return{views:g.minView?Array.prototype.slice.call(m,g.minView):m,viewDate:l}}}]}),angular.module("mgcrea.ngStrap.dropdown",["mgcrea.ngStrap.tooltip"]).provider("$dropdown",function(){var a=this.defaults={animation:"am-fade",prefixClass:"dropdown",placement:"bottom-left",template:"dropdown/dropdown.tpl.html",trigger:"click",container:!1,keyboard:!0,html:!1,delay:0};this.$get=["$window","$rootScope","$tooltip",function(b,c,d){function e(b,e){function h(a){return a.target!==b[0]?a.target!==b[0]&&i.hide():void 0}{var i={},j=angular.extend({},a,e);i.$scope=j.scope&&j.scope.$new()||c.$new()}i=d(b,j),i.$onKeyDown=function(a){if(/(38|40)/.test(a.keyCode)){a.preventDefault(),a.stopPropagation();var b=angular.element(i.$element[0].querySelectorAll("li:not(.divider) a"));if(b.length){var c;angular.forEach(b,function(a,b){g&&g.call(a,":focus")&&(c=b)}),38===a.keyCode&&c>0?c--:40===a.keyCode&&c1){var g=f.search(c[b]);a=a.split(c[b]).join(""),m[c[b]]&&(d[g]=m[c[b]])}return angular.forEach(d,function(a){e.push(a)}),e}function f(a){return a.replace(/\//g,"[\\/]").replace("/-/g","[-]").replace(/\./g,"[.]").replace(/\\s/g,"[\\s]")}function g(a){var b,c=Object.keys(l),d=a;for(b=0;bj?d=setTimeout(i,b-j):(d=null,c||(h=a.apply(f,e)))},j=c&&!d;return d||(d=setTimeout(i,b)),j&&(h=a.apply(f,e)),h}}).constant("throttle",function(a,b,c){var d,e,f,g=null,h=0;c||(c={});var i=function(){h=c.leading===!1?0:new Date,g=null,f=a.apply(d,e)};return function(){var j=new Date;h||c.leading!==!1||(h=j);var k=b-(j-h);return d=this,e=arguments,0>=k?(clearTimeout(g),g=null,h=j,f=a.apply(d,e)):g||c.trailing===!1||(g=setTimeout(i,k)),f}}),angular.module("mgcrea.ngStrap.helpers.dimensions",[]).factory("dimensions",["$document","$window",function(){var b=(angular.element,{}),c=b.nodeName=function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()};b.css=function(b,c,d){var e;return e=b.currentStyle?b.currentStyle[c]:a.getComputedStyle?a.getComputedStyle(b)[c]:b.style[c],d===!0?parseFloat(e)||0:e},b.offset=function(b){var c=b.getBoundingClientRect(),d=b.ownerDocument;return{width:b.offsetWidth,height:b.offsetHeight,top:c.top+(a.pageYOffset||d.documentElement.scrollTop)-(d.documentElement.clientTop||0),left:c.left+(a.pageXOffset||d.documentElement.scrollLeft)-(d.documentElement.clientLeft||0)}},b.position=function(a){var e,f,g={top:0,left:0};return"fixed"===b.css(a,"position")?f=a.getBoundingClientRect():(e=d(a),f=b.offset(a),f=b.offset(a),c(e,"html")||(g=b.offset(e)),g.top+=b.css(e,"borderTopWidth",!0),g.left+=b.css(e,"borderLeftWidth",!0)),{width:a.offsetWidth,height:a.offsetHeight,top:f.top-g.top-b.css(a,"marginTop",!0),left:f.left-g.left-b.css(a,"marginLeft",!0)}};var d=function(a){var d=a.ownerDocument,e=a.offsetParent||d;if(c(e,"#document"))return d.documentElement;for(;e&&!c(e,"html")&&"static"===b.css(e,"position");)e=e.offsetParent;return e||d.documentElement};return b.height=function(a,c){var d=a.offsetHeight;return c?d+=b.css(a,"marginTop",!0)+b.css(a,"marginBottom",!0):d-=b.css(a,"paddingTop",!0)+b.css(a,"paddingBottom",!0)+b.css(a,"borderTopWidth",!0)+b.css(a,"borderBottomWidth",!0),d},b.width=function(a,c){var d=a.offsetWidth;return c?d+=b.css(a,"marginLeft",!0)+b.css(a,"marginRight",!0):d-=b.css(a,"paddingLeft",!0)+b.css(a,"paddingRight",!0)+b.css(a,"borderLeftWidth",!0)+b.css(a,"borderRightWidth",!0),d},b}]),angular.module("mgcrea.ngStrap.helpers.parseOptions",[]).provider("$parseOptions",function(){var a=this.defaults={regexp:/^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/};this.$get=["$parse","$q",function(b,c){function d(d,e){function f(a){return a.map(function(a,b){var c,d,e={};return e[k]=a,c=j(e),d=n(e)||b,{label:c,value:d}})}var g={},h=angular.extend({},a,e);g.$values=[];var i,j,k,l,m,n,o;return g.init=function(){g.$match=i=d.match(h.regexp),j=b(i[2]||i[1]),k=i[4]||i[6],l=i[5],m=b(i[3]||""),n=b(i[2]?i[1]:k),o=b(i[7])},g.valuesFn=function(a,b){return c.when(o(a,b)).then(function(a){return g.$values=a?f(a):{},g.$values})},g.init(),g}return d}]}),angular.module("mgcrea.ngStrap.modal",["mgcrea.ngStrap.helpers.dimensions"]).provider("$modal",function(){var a=this.defaults={animation:"am-fade",backdropAnimation:"am-fade",prefixClass:"modal",placement:"top",template:"modal/modal.tpl.html",contentTemplate:!1,container:!1,element:null,backdrop:!0,keyboard:!0,html:!1,show:!0};this.$get=["$window","$rootScope","$compile","$q","$templateCache","$http","$animate","$timeout","dimensions",function(c,d,e,f,g,h,i){function j(b){function c(a){a.target===a.currentTarget&&("static"===g.backdrop?f.focus():f.hide())}var f={},g=angular.extend({},a,b);f.$promise=l(g.template);var h=f.$scope=g.scope&&g.scope.$new()||d.$new();g.element||g.container||(g.container="body"),m(["title","content"],function(a){g[a]&&(h[a]=g[a])}),h.$hide=function(){h.$$postDigest(function(){f.hide()})},h.$show=function(){h.$$postDigest(function(){f.show()})},h.$toggle=function(){h.$$postDigest(function(){f.toggle()})},g.contentTemplate&&(f.$promise=f.$promise.then(function(a){var c=angular.element(a);return l(g.contentTemplate).then(function(a){var d=k('[ng-bind="content"]',c[0]).removeAttr("ng-bind").html(a);return b.template||d.next().remove(),c[0].outerHTML})}));var j,r,s=angular.element('
    ');return f.$promise.then(function(a){angular.isObject(a)&&(a=a.data),g.html&&(a=a.replace(q,'ng-bind-html="')),a=n.apply(a),j=e(a),f.init()}),f.init=function(){g.show&&h.$$postDigest(function(){f.show()})},f.destroy=function(){r&&(r.remove(),r=null),s&&(s.remove(),s=null),h.$destroy()},f.show=function(){var a=g.container?k(g.container):null,b=g.container?null:g.element;r=f.$element=j(h,function(){}),r.css({display:"block"}).addClass(g.placement),g.animation&&(g.backdrop&&s.addClass(g.backdropAnimation),r.addClass(g.animation)),g.backdrop&&i.enter(s,p,null,function(){}),i.enter(r,a,b,function(){}),h.$isShown=!0,h.$$phase||h.$digest();var d=r[0];o(function(){d.focus()}),p.addClass(g.prefixClass+"-open"),g.animation&&p.addClass(g.prefixClass+"-with-"+g.animation),g.backdrop&&(r.on("click",c),s.on("click",c)),g.keyboard&&r.on("keyup",f.$onKeyUp)},f.hide=function(){i.leave(r,function(){p.removeClass(g.prefixClass+"-open"),g.animation&&p.addClass(g.prefixClass+"-with-"+g.animation)}),g.backdrop&&i.leave(s,function(){}),h.$isShown=!1,h.$$phase||h.$digest(),g.backdrop&&(r.off("click",c),s.off("click",c)),g.keyboard&&r.off("keyup",f.$onKeyUp)},f.toggle=function(){h.$isShown?f.hide():f.show()},f.focus=function(){r[0].focus()},f.$onKeyUp=function(a){27===a.which&&f.hide()},f}function k(a,c){return angular.element((c||b).querySelectorAll(a))}function l(a){return f.when(g.get(a)||h.get(a)).then(function(b){return angular.isObject(b)?(g.put(a,b.data),b.data):b})}var m=angular.forEach,n=String.prototype.trim,o=c.requestAnimationFrame||c.setTimeout,p=angular.element(c.document.body),q=/ng-bind="/gi;return j}]}).directive("bsModal",["$window","$location","$sce","$modal",function(a,b,c,d){return{restrict:"EAC",scope:!0,link:function(a,b,e){var f={scope:a,element:b,show:!1};angular.forEach(["template","contentTemplate","placement","backdrop","keyboard","html","container","animation"],function(a){angular.isDefined(e[a])&&(f[a]=e[a])}),angular.forEach(["title","content"],function(b){e[b]&&e.$observe(b,function(d){a[b]=c.trustAsHtml(d)})}),e.bsModal&&a.$watch(e.bsModal,function(b){angular.isObject(b)?angular.extend(a,b):a.content=b},!0);var g=d(f);b.on(e.trigger||"click",g.toggle),a.$on("$destroy",function(){g.destroy(),f=null,g=null})}}}]),angular.module("mgcrea.ngStrap.navbar",[]).provider("$navbar",function(){var a=this.defaults={activeClass:"active",routeAttr:"data-match-route",strict:!1};this.$get=function(){return{defaults:a}}}).directive("bsNavbar",["$window","$location","$navbar",function(a,b,c){var d=c.defaults;return{restrict:"A",link:function(a,c,e){var f=angular.copy(d);angular.forEach(Object.keys(d),function(a){angular.isDefined(e[a])&&(f[a]=e[a])}),a.$watch(function(){return b.path()},function(a){var b=c[0].querySelectorAll("li["+f.routeAttr+"]");angular.forEach(b,function(b){var c=angular.element(b),d=c.attr(f.routeAttr).replace("/","\\/");f.strict&&(d="^"+d+"$");var e=new RegExp(d,["i"]);e.test(a)?c.addClass(f.activeClass):c.removeClass(f.activeClass)})})}}}]),angular.module("mgcrea.ngStrap.popover",["mgcrea.ngStrap.tooltip"]).provider("$popover",function(){var a=this.defaults={animation:"am-fade",placement:"right",template:"popover/popover.tpl.html",contentTemplate:!1,trigger:"click",keyboard:!0,html:!1,title:"",content:"",delay:0,container:!1};this.$get=["$tooltip",function(b){function c(c,d){var e=angular.extend({},a,d),f=b(c,e);return e.content&&(f.$scope.content=e.content),f}return c}]}).directive("bsPopover",["$window","$location","$sce","$popover",function(a,b,c,d){var e=a.requestAnimationFrame||a.setTimeout;return{restrict:"EAC",scope:!0,link:function(a,b,f){var g={scope:a};angular.forEach(["template","contentTemplate","placement","container","delay","trigger","keyboard","html","animation"],function(a){angular.isDefined(f[a])&&(g[a]=f[a])}),angular.forEach(["title","content"],function(b){f[b]&&f.$observe(b,function(d,f){a[b]=c.trustAsHtml(d),angular.isDefined(f)&&e(function(){h&&h.$applyPlacement()})})}),f.bsPopover&&a.$watch(f.bsPopover,function(b,c){angular.isObject(b)?angular.extend(a,b):a.content=b,angular.isDefined(c)&&e(function(){h&&h.$applyPlacement()})},!0);var h=d(b,g);a.$on("$destroy",function(){h.destroy(),g=null,h=null})}}}]),angular.module("mgcrea.ngStrap.scrollspy",["mgcrea.ngStrap.helpers.debounce","mgcrea.ngStrap.helpers.dimensions"]).provider("$scrollspy",function(){var a=this.$$spies={},c=this.defaults={debounce:150,throttle:100,offset:100};this.$get=["$window","$document","$rootScope","dimensions","debounce","throttle",function(d,e,f,g,h,i){function j(a,b){return a[0].nodeName&&a[0].nodeName.toLowerCase()===b.toLowerCase()}function k(e){var k=angular.extend({},c,e);k.element||(k.element=n);var o=j(k.element,"body"),p=o?l:k.element,q=o?"window":k.id;if(a[q])return a[q].$$count++,a[q];var r,s,t,u,v,w,x,y,z={},A=z.$trackedElements=[],B=[];return z.init=function(){this.$$count=1,u=h(this.checkPosition,k.debounce),v=i(this.checkPosition,k.throttle),p.on("click",this.checkPositionWithEventLoop),l.on("resize",u),p.on("scroll",v),w=h(this.checkOffsets,k.debounce),r=f.$on("$viewContentLoaded",w),s=f.$on("$includeContentLoaded",w),w(),q&&(a[q]=z)},z.destroy=function(){this.$$count--,this.$$count>0||(p.off("click",this.checkPositionWithEventLoop),l.off("resize",u),p.off("scroll",u),r(),s())},z.checkPosition=function(){if(B.length){if(y=(o?d.pageYOffset:p.prop("scrollTop"))||0,x=Math.max(d.innerHeight,m.prop("clientHeight")),yB[a+1].offsetTop))return z.$activateElement(B[a])}},z.checkPositionWithEventLoop=function(){setTimeout(this.checkPosition,1)},z.$activateElement=function(a){if(t){var b=z.$getTrackedElement(t);b&&(b.source.removeClass("active"),j(b.source,"li")&&j(b.source.parent().parent(),"li")&&b.source.parent().parent().removeClass("active"))}t=a.target,a.source.addClass("active"),j(a.source,"li")&&j(a.source.parent().parent(),"li")&&a.source.parent().parent().addClass("active")},z.$getTrackedElement=function(a){return A.filter(function(b){return b.target===a})[0]},z.checkOffsets=function(){angular.forEach(A,function(a){var c=b.querySelector(a.target);a.offsetTop=c?g.offset(c).top:null,k.offset&&null!==a.offsetTop&&(a.offsetTop-=1*k.offset)}),B=A.filter(function(a){return null!==a.offsetTop}).sort(function(a,b){return a.offsetTop-b.offsetTop}),u()},z.trackElement=function(a,b){A.push({target:a,source:b})},z.untrackElement=function(a,b){for(var c,d=A.length;d--;)if(A[d].target===a&&A[d].source===b){c=d;break}A=A.splice(c,1)},z.activate=function(a){A[a].addClass("active")},z.init(),z}var l=angular.element(d),m=angular.element(e.prop("documentElement")),n=angular.element(d.document.body);return k}]}).directive("bsScrollspy",["$rootScope","debounce","dimensions","$scrollspy",function(a,b,c,d){return{restrict:"EAC",link:function(a,b,c){var e={scope:a}; +angular.forEach(["offset","target"],function(a){angular.isDefined(c[a])&&(e[a]=c[a])});var f=d(e);f.trackElement(e.target,b),a.$on("$destroy",function(){f.untrackElement(e.target,b),f.destroy(),e=null,f=null})}}}]).directive("bsScrollspyList",["$rootScope","debounce","dimensions","$scrollspy",function(){return{restrict:"A",compile:function(a){var b=a[0].querySelectorAll("li > a[href]");angular.forEach(b,function(a){var b=angular.element(a);b.parent().attr("bs-scrollspy","").attr("data-target",b.attr("href"))})}}}]),angular.module("mgcrea.ngStrap.select",["mgcrea.ngStrap.tooltip","mgcrea.ngStrap.helpers.parseOptions"]).provider("$select",function(){var a=this.defaults={animation:"am-fade",prefixClass:"select",placement:"bottom-left",template:"select/select.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,multiple:!1,sort:!0,caretHtml:' ',placeholder:"Choose among the following...",maxLength:3,maxLengthHtml:"selected"};this.$get=["$window","$document","$rootScope","$tooltip",function(b,c,d,e){function f(b,c,d){var f={},h=angular.extend({},a,d);f=e(b,h);var i=d.scope,j=f.$scope;j.$matches=[],j.$activeIndex=0,j.$isMultiple=h.multiple,j.$activate=function(a){j.$$postDigest(function(){f.activate(a)})},j.$select=function(a){j.$$postDigest(function(){f.select(a)})},j.$isVisible=function(){return f.$isVisible()},j.$isActive=function(a){return f.$isActive(a)},f.update=function(a){j.$matches=a,f.$updateActiveIndex()},f.activate=function(a){return h.multiple?(j.$activeIndex.sort(),f.$isActive(a)?j.$activeIndex.splice(j.$activeIndex.indexOf(a),1):j.$activeIndex.push(a),h.sort&&j.$activeIndex.sort()):j.$activeIndex=a,j.$activeIndex},f.select=function(a){var d=j.$matches[a].value;f.activate(a),c.$setViewValue(h.multiple?j.$activeIndex.map(function(a){return j.$matches[a].value}):d),c.$render(),i&&i.$digest(),h.multiple||("focus"===h.trigger?b[0].blur():f.$isShown&&f.hide()),j.$emit("$select.select",d,a)},f.$updateActiveIndex=function(){c.$modelValue&&j.$matches.length?j.$activeIndex=h.multiple&&angular.isArray(c.$modelValue)?c.$modelValue.map(function(a){return f.$getIndex(a)}):f.$getIndex(c.$modelValue):j.$activeIndex>=j.$matches.length&&(j.$activeIndex=h.multiple?[]:0)},f.$isVisible=function(){return h.minLength&&c?j.$matches.length&&c.$viewValue.length>=h.minLength:j.$matches.length},f.$isActive=function(a){return h.multiple?-1!==j.$activeIndex.indexOf(a):j.$activeIndex===a},f.$getIndex=function(a){var b=j.$matches.length,c=b;if(b){for(c=b;c--&&j.$matches[c].value!==a;);if(!(0>c))return c}},f.$onMouseDown=function(a){if(a.preventDefault(),a.stopPropagation(),g){var b=angular.element(a.target);b.triggerHandler("click")}},f.$onKeyDown=function(a){if(/(38|40|13)/.test(a.keyCode)){if(a.preventDefault(),a.stopPropagation(),13===a.keyCode)return f.select(j.$activeIndex);38===a.keyCode&&j.$activeIndex>0?j.$activeIndex--:40===a.keyCode&&j.$activeIndex'),i.after(b)}var j=e(c.ngOptions),k=d(b,g,h),l=j.$match[7].replace(/\|.+/,"").trim();a.$watch(l,function(){j.valuesFn(a,g).then(function(a){k.update(a),g.$render()})},!0),a.$watch(c.ngModel,function(){k.$updateActiveIndex()},!0),g.$render=function(){var a,d;h.multiple&&angular.isArray(g.$modelValue)?(a=g.$modelValue.map(function(a){return d=k.$getIndex(a),angular.isDefined(d)?k.$scope.$matches[d].label:!1}).filter(angular.isDefined),a=a.length>(h.maxLength||f.maxLength)?a.length+" "+(h.maxLengthHtml||f.maxLengthHtml):a.join(", ")):(d=k.$getIndex(g.$modelValue),a=angular.isDefined(d)?k.$scope.$matches[d].label:!1),b.html((a?a:c.placeholder||f.placeholder)+f.caretHtml)},a.$on("$destroy",function(){k.destroy(),h=null,k=null})}}}]),angular.module("mgcrea.ngStrap.tab",[]).run(["$templateCache",function(a){a.put("$pane","{{pane.content}}")}]).provider("$tab",function(){var a=this.defaults={animation:"am-fade",template:"tab/tab.tpl.html"};this.$get=function(){return{defaults:a}}}).directive("bsTabs",["$window","$animate","$tab",function(a,b,c){var d=c.defaults;return{restrict:"EAC",scope:!0,require:"?ngModel",templateUrl:function(a,b){return b.template||d.template},link:function(a,b,c,e){var f=d;angular.forEach(["animation"],function(a){angular.isDefined(c[a])&&(f[a]=c[a])}),c.bsTabs&&a.$watch(c.bsTabs,function(b){a.panes=b},!0),b.addClass("tabs"),f.animation&&b.addClass(f.animation),a.active=a.activePane=0,a.setActive=function(b){a.active=b,e&&e.$setViewValue(b)},e&&(e.$render=function(){a.active=1*e.$modelValue})}}}]),angular.module("mgcrea.ngStrap.timepicker",["mgcrea.ngStrap.helpers.dateParser","mgcrea.ngStrap.tooltip"]).provider("$timepicker",function(){var a=this.defaults={animation:"am-fade",prefixClass:"timepicker",placement:"bottom-left",template:"timepicker/timepicker.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,useNative:!0,timeType:"date",timeFormat:"shortTime",autoclose:!1,minTime:-1/0,maxTime:+1/0,length:5,hourStep:1,minuteStep:5};this.$get=["$window","$document","$rootScope","$sce","$locale","dateFilter","$tooltip",function(b,c,d,e,f,g,h){function i(b,c,d){function e(a,c){if(b[0].createTextRange){var d=b[0].createTextRange();d.collapse(!0),d.moveStart("character",a),d.moveEnd("character",c),d.select()}else b[0].setSelectionRange?b[0].setSelectionRange(a,c):angular.isUndefined(b[0].selectionStart)&&(b[0].selectionStart=a,b[0].selectionEnd=c)}function i(){b[0].focus()}var l=h(b,angular.extend({},a,d)),m=d.scope,n=l.$options,o=l.$scope,p=0,q=c.$dateValue||new Date,r={hour:q.getHours(),meridian:q.getHours()<12,minute:q.getMinutes(),second:q.getSeconds(),millisecond:q.getMilliseconds()},s=f.DATETIME_FORMATS[n.timeFormat]||n.timeFormat,t=/(h+)[:]?(m+)[ ]?(a?)/i.exec(s).slice(1);o.$select=function(a,b){l.select(a,b)},o.$moveIndex=function(a,b){l.$moveIndex(a,b)},o.$switchMeridian=function(a){l.switchMeridian(a)},l.update=function(a){angular.isDate(a)&&!isNaN(a.getTime())?(l.$date=a,angular.extend(r,{hour:a.getHours(),minute:a.getMinutes(),second:a.getSeconds(),millisecond:a.getMilliseconds()}),l.$build()):l.$isBuilt||l.$build()},l.select=function(a,b,d){isNaN(c.$dateValue.getTime())&&(c.$dateValue=new Date(1970,0,1)),angular.isDate(a)||(a=new Date(a)),0===b?c.$dateValue.setHours(a.getHours()):1===b&&c.$dateValue.setMinutes(a.getMinutes()),c.$setViewValue(c.$dateValue),c.$render(),n.autoclose&&!d&&l.hide(!0)},l.switchMeridian=function(a){var b=(a||c.$dateValue).getHours();c.$dateValue.setHours(12>b?b+12:b-12),c.$render()},l.$build=function(){var a,b,c=o.midIndex=parseInt(n.length/2,10),d=[];for(a=0;an.maxTime},l.$moveIndex=function(a,b){var c;0===b?(c=new Date(1970,0,1,r.hour+a*n.length,r.minute),angular.extend(r,{hour:c.getHours()})):1===b&&(c=new Date(1970,0,1,r.hour,r.minute+a*n.length*5),angular.extend(r,{minute:c.getMinutes()})),l.$build()},l.$onMouseDown=function(a){if("input"!==a.target.nodeName.toLowerCase()&&a.preventDefault(),a.stopPropagation(),j){var b=angular.element(a.target);"button"!==b[0].nodeName.toLowerCase()&&(b=b.parent()),b.triggerHandler("click")}},l.$onKeyDown=function(a){if(/(38|37|39|40|13)/.test(a.keyCode)&&!a.shiftKey&&!a.altKey){if(a.preventDefault(),a.stopPropagation(),13===a.keyCode)return l.hide(!0);var b=new Date(l.$date),c=b.getHours(),d=g(b,"h").length,f=b.getMinutes(),h=g(b,"mm").length,i=/(37|39)/.test(a.keyCode),j=2+1*!!t[2];if(i&&(37===a.keyCode?p=1>p?j-1:p-1:39===a.keyCode&&(p=j-1>p?p+1:0)),0===p){if(i)return e(0,d);38===a.keyCode?b.setHours(c-n.hourStep):40===a.keyCode&&b.setHours(c+n.hourStep)}else if(1===p){if(i)return e(d+1,d+1+h);38===a.keyCode?b.setMinutes(f-n.minuteStep):40===a.keyCode&&b.setMinutes(f+n.minuteStep)}else if(2===p){if(i)return e(d+1+h+1,d+1+h+3);l.switchMeridian()}l.select(b,p,!0),m.$digest()}};var u=l.init;l.init=function(){return k&&n.useNative?(b.prop("type","time"),void b.css("-webkit-appearance","textfield")):(j&&(b.prop("type","text"),b.attr("readonly","true"),b.on("click",i)),void u())};var v=l.destroy;l.destroy=function(){k&&n.useNative&&b.off("click",i),v()};var w=l.show;l.show=function(){w(),setTimeout(function(){l.$element.on(j?"touchstart":"mousedown",l.$onMouseDown),n.keyboard&&b.on("keydown",l.$onKeyDown)})};var x=l.hide;return l.hide=function(a){l.$element.off(j?"touchstart":"mousedown",l.$onMouseDown),n.keyboard&&b.off("keydown",l.$onKeyDown),x(a)},l}var j=(angular.element(b.document.body),"createTouch"in b.document),k=/(ip(a|o)d|iphone|android)/gi.test(b.navigator.userAgent);return a.lang||(a.lang=f.id),i.defaults=a,i}]}).directive("bsTimepicker",["$window","$parse","$q","$locale","dateFilter","$timepicker","$dateParser","$timeout",function(a,b,c,d,e,f,g){{var h=f.defaults,i=/(ip(a|o)d|iphone|android)/gi.test(a.navigator.userAgent);a.requestAnimationFrame||a.setTimeout}return{restrict:"EAC",require:"ngModel",link:function(a,b,c,d){var j={scope:a,controller:d};angular.forEach(["placement","container","delay","trigger","keyboard","html","animation","template","autoclose","timeType","timeFormat","useNative","lang"],function(a){angular.isDefined(c[a])&&(j[a]=c[a])}),i&&(j.useNative||h.useNative)&&(j.timeFormat="HH:mm");var k=f(b,d,j);j=k.$options;var l=g({format:j.timeFormat,lang:j.lang});angular.forEach(["minTime","maxTime"],function(a){angular.isDefined(c[a])&&c.$observe(a,function(b){k.$options[a]="now"===b?(new Date).setFullYear(1970,0,1):angular.isString(b)&&b.match(/^".+"$/)?+new Date(b.substr(1,b.length-2)):l.parse(b),!isNaN(k.$options[a])&&k.$build()})}),a.$watch(c.ngModel,function(){k.update(d.$dateValue)},!0),d.$parsers.unshift(function(a){if(!a)return void d.$setValidity("date",!0);var b=l.parse(a,d.$dateValue);if(!b||isNaN(b.getTime()))d.$setValidity("date",!1);else{var c=b.getTime()>=j.minTime&&b.getTime()<=j.maxTime;d.$setValidity("date",c),c&&(d.$dateValue=b)}return"string"===j.timeType?e(a,j.timeFormat):"number"===j.timeType?d.$dateValue.getTime():"iso"===j.timeType?d.$dateValue.toISOString():d.$dateValue}),d.$formatters.push(function(a){var b="string"===j.timeType?l.parse(a,d.$dateValue):new Date(a);return d.$dateValue=b,d.$dateValue}),d.$render=function(){b.val(isNaN(d.$dateValue.getTime())?"":e(d.$dateValue,j.timeFormat))},a.$on("$destroy",function(){k.destroy(),j=null,k=null})}}}]),angular.module("mgcrea.ngStrap.tooltip",["ngAnimate","mgcrea.ngStrap.helpers.dimensions"]).provider("$tooltip",function(){var a=this.defaults={animation:"am-fade",prefixClass:"tooltip",container:!1,placement:"top",template:"tooltip/tooltip.tpl.html",contentTemplate:!1,trigger:"hover focus",keyboard:!1,html:!1,show:!1,title:"",type:"",delay:0};this.$get=["$window","$rootScope","$compile","$q","$templateCache","$http","$animate","$timeout","dimensions","$$animateReflow",function(c,d,e,f,g,h,i,j,k,l){function m(b,c){function f(){return"body"===j.container?k.offset(b[0]):k.position(b[0])}function g(a,b,c,d){var e,f=a.split("-");switch(f[0]){case"right":e={top:b.top+b.height/2-d/2,left:b.left+b.width};break;case"bottom":e={top:b.top+b.height,left:b.left+b.width/2-c/2};break;case"left":e={top:b.top+b.height/2-d/2,left:b.left-c};break;default:e={top:b.top-d,left:b.left+b.width/2-c/2}}if(!f[1])return e;if("top"===f[0]||"bottom"===f[0])switch(f[1]){case"left":e.left=b.left;break;case"right":e.left=b.left+b.width-c}else if("left"===f[0]||"right"===f[0])switch(f[1]){case"top":e.top=b.top-d;break;case"bottom":e.top=b.top+b.height}return e}var h={},j=h.$options=angular.extend({},a,c);h.$promise=o(j.template);var m=h.$scope=j.scope&&j.scope.$new()||d.$new();j.delay&&angular.isString(j.delay)&&(j.delay=parseFloat(j.delay)),j.title&&(h.$scope.title=j.title),m.$hide=function(){m.$$postDigest(function(){h.hide()})},m.$show=function(){m.$$postDigest(function(){h.show()})},m.$toggle=function(){m.$$postDigest(function(){h.toggle()})},h.$isShown=m.$isShown=!1;var s,t;j.contentTemplate&&(h.$promise=h.$promise.then(function(a){var b=angular.element(a);return o(j.contentTemplate).then(function(a){return n('[ng-bind="content"]',b[0]).removeAttr("ng-bind").html(a),b[0].outerHTML})}));var u,v,w,x;return h.$promise.then(function(a){angular.isObject(a)&&(a=a.data),j.html&&(a=a.replace(r,'ng-bind-html="')),a=p.apply(a),w=a,u=e(a),h.init()}),h.init=function(){j.delay&&angular.isNumber(j.delay)&&(j.delay={show:j.delay,hide:j.delay}),"self"===j.container?x=b:j.container&&(x=n(j.container));var a=j.trigger.split(" ");angular.forEach(a,function(a){"click"===a?b.on("click",h.toggle):"manual"!==a&&(b.on("hover"===a?"mouseenter":"focus",h.enter),b.on("hover"===a?"mouseleave":"blur",h.leave),"hover"!==a&&b.on(q?"touchstart":"mousedown",h.$onFocusElementMouseDown))}),j.show&&m.$$postDigest(function(){"focus"===j.trigger?b[0].focus():h.show()})},h.destroy=function(){for(var a=j.trigger.split(" "),c=a.length;c--;){var d=a[c];"click"===d?b.off("click",h.toggle):"manual"!==d&&(b.off("hover"===d?"mouseenter":"focus",h.enter),b.off("hover"===d?"mouseleave":"blur",h.leave),"hover"!==d&&b.off(q?"touchstart":"mousedown",h.$onFocusElementMouseDown))}v&&(v.remove(),v=null),m.$destroy()},h.enter=function(){return clearTimeout(s),t="in",j.delay&&j.delay.show?void(s=setTimeout(function(){"in"===t&&h.show()},j.delay.show)):h.show()},h.show=function(){var a=j.container?x:null,c=j.container?null:b;v&&v.remove(),v=h.$element=u(m,function(){}),v.css({top:"0px",left:"0px",display:"block"}).addClass(j.placement),j.animation&&v.addClass(j.animation),j.type&&v.addClass(j.prefixClass+"-"+j.type),i.enter(v,a,c,function(){}),h.$isShown=m.$isShown=!0,m.$$phase||m.$digest(),l(h.$applyPlacement),j.keyboard&&("focus"!==j.trigger?(h.focus(),v.on("keyup",h.$onKeyUp)):b.on("keyup",h.$onFocusKeyUp))},h.leave=function(){return clearTimeout(s),t="out",j.delay&&j.delay.hide?void(s=setTimeout(function(){"out"===t&&h.hide()},j.delay.hide)):h.hide()},h.hide=function(a){return h.$isShown?(i.leave(v,function(){v=null}),h.$isShown=m.$isShown=!1,m.$$phase||m.$digest(),j.keyboard&&v.off("keyup",h.$onKeyUp),a&&"focus"===j.trigger?b[0].blur():void 0):void 0},h.toggle=function(){h.$isShown?h.leave():h.enter()},h.focus=function(){v[0].focus()},h.$applyPlacement=function(){if(v){var a=f(),b=v.prop("offsetWidth"),c=v.prop("offsetHeight"),d=g(j.placement,a,b,c);d.top+="px",d.left+="px",v.css(d)}},h.$onKeyUp=function(a){27===a.which&&h.hide()},h.$onFocusKeyUp=function(a){27===a.which&&b[0].blur()},h.$onFocusElementMouseDown=function(a){a.preventDefault(),a.stopPropagation(),h.$isShown?b[0].blur():b[0].focus()},h}function n(a,c){return angular.element((c||b).querySelectorAll(a))}function o(a){return f.when(g.get(a)||h.get(a)).then(function(b){return angular.isObject(b)?(g.put(a,b.data),b.data):b})}var p=String.prototype.trim,q="createTouch"in c.document,r=/ng-bind="/gi;return m}]}).directive("bsTooltip",["$window","$location","$sce","$tooltip","$$animateReflow",function(a,b,c,d,e){return{restrict:"EAC",scope:!0,link:function(a,b,f){var g={scope:a};angular.forEach(["template","contentTemplate","placement","container","delay","trigger","keyboard","html","animation","type"],function(a){angular.isDefined(f[a])&&(g[a]=f[a])}),angular.forEach(["title"],function(b){f[b]&&f.$observe(b,function(d,f){a[b]=c.trustAsHtml(d),angular.isDefined(f)&&e(function(){h&&h.$applyPlacement()})})}),f.bsTooltip&&a.$watch(f.bsTooltip,function(b,c){angular.isObject(b)?angular.extend(a,b):a.content=b,angular.isDefined(c)&&e(function(){h&&h.$applyPlacement()})},!0);var h=d(b,g);a.$on("$destroy",function(){h.destroy(),g=null,h=null})}}}]),angular.module("mgcrea.ngStrap.typeahead",["mgcrea.ngStrap.tooltip","mgcrea.ngStrap.helpers.parseOptions"]).provider("$typeahead",function(){var a=this.defaults={animation:"am-fade",prefixClass:"typeahead",placement:"bottom-left",template:"typeahead/typeahead.tpl.html",trigger:"focus",container:!1,keyboard:!0,html:!1,delay:0,minLength:1,filter:"filter",limit:6};this.$get=["$window","$rootScope","$tooltip",function(b,c,d){function e(b,c){var e={},f=angular.extend({},a,c),g=f.controller;e=d(b,f);var h=c.scope,i=e.$scope;i.$matches=[],i.$activeIndex=0,i.$activate=function(a){i.$$postDigest(function(){e.activate(a)})},i.$select=function(a){i.$$postDigest(function(){e.select(a)})},i.$isVisible=function(){return e.$isVisible()},e.update=function(a){i.$matches=a,i.$activeIndex>=a.length&&(i.$activeIndex=0)},e.activate=function(a){i.$activeIndex=a},e.select=function(a){var c=i.$matches[a].value;g&&(g.$setViewValue(c),g.$render(),h&&h.$digest()),"focus"===f.trigger?b[0].blur():e.$isShown&&e.hide(),i.$activeIndex=0,i.$emit("$typeahead.select",c,a)},e.$isVisible=function(){return f.minLength&&g?i.$matches.length&&angular.isString(g.$viewValue)&&g.$viewValue.length>=f.minLength:!!i.$matches.length},e.$onMouseDown=function(a){a.preventDefault(),a.stopPropagation()},e.$onKeyDown=function(a){if(/(38|40|13)/.test(a.keyCode)){if(a.preventDefault(),a.stopPropagation(),13===a.keyCode)return e.select(i.$activeIndex);38===a.keyCode&&i.$activeIndex>0?i.$activeIndex--:40===a.keyCode&&i.$activeIndexj&&(a=a.slice(0,j)),m.update(a)})}),a.$on("$destroy",function(){m.destroy(),h=null,m=null})}}}])}(window,document),function(){"use strict";angular.module("mgcrea.ngStrap.alert").run(["$templateCache",function(a){a.put("alert/alert.tpl.html",'
     
    ')}]),angular.module("mgcrea.ngStrap.aside").run(["$templateCache",function(a){a.put("aside/aside.tpl.html",'')}]),angular.module("mgcrea.ngStrap.datepicker").run(["$templateCache",function(a){a.put("datepicker/datepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.dropdown").run(["$templateCache",function(a){a.put("dropdown/dropdown.tpl.html",'')}]),angular.module("mgcrea.ngStrap.modal").run(["$templateCache",function(a){a.put("modal/modal.tpl.html",'')}]),angular.module("mgcrea.ngStrap.popover").run(["$templateCache",function(a){a.put("popover/popover.tpl.html",'

    ')}]),angular.module("mgcrea.ngStrap.select").run(["$templateCache",function(a){a.put("select/select.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tab").run(["$templateCache",function(a){a.put("tab/tab.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.timepicker").run(["$templateCache",function(a){a.put("timepicker/timepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tooltip").run(["$templateCache",function(a){a.put("tooltip/tooltip.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.typeahead").run(["$templateCache",function(a){a.put("typeahead/typeahead.tpl.html",'')}])}(window,document); +//# sourceMappingURL=angular-strap.min.map \ No newline at end of file diff --git a/static/lib/angular-strap.tpl.min.js b/static/lib/angular-strap.tpl.min.js new file mode 100644 index 000000000..42fcc29ea --- /dev/null +++ b/static/lib/angular-strap.tpl.min.js @@ -0,0 +1,9 @@ +/** + * angular-strap + * @version v2.0.0-rc.4 - 2014-03-07 + * @link http://mgcrea.github.io/angular-strap + * @author Olivier Louvignes (olivier@mg-crea.com) + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +!function(){"use strict";angular.module("mgcrea.ngStrap.alert").run(["$templateCache",function(a){a.put("alert/alert.tpl.html",'
     
    ')}]),angular.module("mgcrea.ngStrap.aside").run(["$templateCache",function(a){a.put("aside/aside.tpl.html",'')}]),angular.module("mgcrea.ngStrap.datepicker").run(["$templateCache",function(a){a.put("datepicker/datepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.dropdown").run(["$templateCache",function(a){a.put("dropdown/dropdown.tpl.html",'')}]),angular.module("mgcrea.ngStrap.modal").run(["$templateCache",function(a){a.put("modal/modal.tpl.html",'')}]),angular.module("mgcrea.ngStrap.popover").run(["$templateCache",function(a){a.put("popover/popover.tpl.html",'

    ')}]),angular.module("mgcrea.ngStrap.select").run(["$templateCache",function(a){a.put("select/select.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tab").run(["$templateCache",function(a){a.put("tab/tab.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.timepicker").run(["$templateCache",function(a){a.put("timepicker/timepicker.tpl.html",'')}]),angular.module("mgcrea.ngStrap.tooltip").run(["$templateCache",function(a){a.put("tooltip/tooltip.tpl.html",'
    ')}]),angular.module("mgcrea.ngStrap.typeahead").run(["$templateCache",function(a){a.put("typeahead/typeahead.tpl.html",'')}])}(window,document); +//# sourceMappingURL=angular-strap.tpl.min.map \ No newline at end of file diff --git a/static/lib/bootstrap-additions.min.css b/static/lib/bootstrap-additions.min.css new file mode 100644 index 000000000..0328d189e --- /dev/null +++ b/static/lib/bootstrap-additions.min.css @@ -0,0 +1 @@ +.carousel-inner>.item>a>img,.carousel-inner>.item>img,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.aside .aside-dialog .aside-footer:after,.aside .aside-dialog .aside-footer:before,.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{content:" ";display:table}.aside .aside-dialog .aside-footer:after,.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.container-fluid:after,.container:after,.form-horizontal .form-group:after,.modal-footer:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.alert.top,.alert.top-left,.alert.top-right{position:fixed;top:50px;z-index:1050;margin:20px}.alert.top{right:0;left:0}.alert.top-right{right:0}.alert.top-right .close{padding-left:10px}.alert.top-left{left:0}.alert.top-left .close{padding-right:10px}.aside{z-index:1049;background:#fff;position:fixed;overflow:auto;min-width:320px;top:0;bottom:0}.aside:focus{outline:0}@media (max-width:991px){.aside{min-width:240px}}.aside.left{left:0;right:auto}.aside.right{left:auto;right:0}.aside .aside-dialog .aside-header{border-bottom:1px solid #e5e5e5;min-height:16.43px;padding:6px 15px;background:#428bca;color:#fff}.aside .aside-dialog .aside-header .close{color:#fff;font-size:25px;opacity:.8;padding:4px 8px;margin-right:-8px}.aside .aside-dialog .aside-body{position:relative;padding:20px}.aside .aside-dialog .aside-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #e5e5e5}.aside .aside-dialog .aside-footer .btn+.btn{margin-left:5px;margin-bottom:0}.aside .aside-dialog .aside-footer .btn-group .btn+.btn{margin-left:-1px}.aside .aside-dialog .aside-footer .btn-block+.btn-block{margin-left:0}.aside-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.aside-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.aside-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.aside-backdrop.animation-fade{background:rgba(0,0,0,.5);opacity:1}.callout{margin:20px 0;padding:20px;border-left:3px solid #eee}.callout h4{margin-top:0;margin-bottom:5px}.callout p:last-child{margin-bottom:0}.callout-danger{border-color:#eed3d7;background-color:#fdf7f7}.callout-danger h4{color:#b94a48}.callout-warning{border-color:#faebcc;background-color:#faf8f0}.callout-warning h4{color:#8a6d3b}.callout-info{border-color:#bce8f1;background-color:#f4f8fa}.callout-info h4{color:#34789a}.modal.center .modal-dialog{position:fixed;top:40%;left:50%;min-width:320px;max-width:630px;width:50%;-webkit-transform:translateX(-50%) translateY(-50%);transform:translateX(-50%) translateY(-50%)}[tabindex="-1"]{outline:0}a[ng-click]{cursor:pointer}.popover.top-left{margin-top:-10px}.popover.top-left .arrow{margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px;left:10%}.popover.top-left .arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.top-right{margin-top:-10px}.popover.top-right .arrow{margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px;left:90%}.popover.top-right .arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.bottom-left{margin-top:10px}.popover.bottom-left .arrow{margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px;left:10%}.popover.bottom-left .arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.bottom-right{margin-top:10px}.popover.bottom-right .arrow{margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px;left:90%}.popover.bottom-right .arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.tooltip.tooltip-info.top .tooltip-arrow{border-top-color:#d9edf7}.tooltip.tooltip-info.right .tooltip-arrow{border-right-color:#d9edf7}.tooltip.tooltip-info.bottom .tooltip-arrow{border-bottom-color:#d9edf7}.tooltip.tooltip-info.left .tooltip-arrow{border-left-color:#d9edf7}.tooltip.tooltip-info .tooltip-inner{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.tooltip.tooltip-info .tooltip-inner hr{border-top-color:#a6e1ec}.tooltip.tooltip-info .tooltip-inner .alert-link{color:#245269}.tooltip.tooltip-success.top .tooltip-arrow{border-top-color:#dff0d8}.tooltip.tooltip-success.right .tooltip-arrow{border-right-color:#dff0d8}.tooltip.tooltip-success.bottom .tooltip-arrow{border-bottom-color:#dff0d8}.tooltip.tooltip-success.left .tooltip-arrow{border-left-color:#dff0d8}.tooltip.tooltip-success .tooltip-inner{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.tooltip.tooltip-success .tooltip-inner hr{border-top-color:#c9e2b3}.tooltip.tooltip-success .tooltip-inner .alert-link{color:#2b542c}.tooltip.tooltip-danger.top .tooltip-arrow{border-top-color:#f2dede}.tooltip.tooltip-danger.right .tooltip-arrow{border-right-color:#f2dede}.tooltip.tooltip-danger.bottom .tooltip-arrow{border-bottom-color:#f2dede}.tooltip.tooltip-danger.left .tooltip-arrow{border-left-color:#f2dede}.tooltip.tooltip-danger .tooltip-inner{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.tooltip.tooltip-danger .tooltip-inner hr{border-top-color:#e4b9c0}.tooltip.tooltip-danger .tooltip-inner .alert-link{color:#843534} \ No newline at end of file diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 2e92301f4..6d2fe9665 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -112,10 +112,7 @@ - - - - + @@ -132,10 +129,7 @@
    - - - - + @@ -180,10 +174,7 @@
    - - - - + @@ -222,10 +213,7 @@ {{ webhook.parameters.url }} - - - - + diff --git a/static/partials/team-view.html b/static/partials/team-view.html index 94f9861e0..969cb00e9 100644 --- a/static/partials/team-view.html +++ b/static/partials/team-view.html @@ -17,10 +17,8 @@ - - - - + diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index 766eebf91..795420ccb 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -16,7 +16,8 @@
    diff --git a/templates/base.html b/templates/base.html index 5bbda45f3..c3ffec9ea 100644 --- a/templates/base.html +++ b/templates/base.html @@ -18,6 +18,8 @@ + + @@ -39,16 +41,17 @@ - - - - + + + + + From 578add3b9e6d1d0890bc1a20d038345269a9427e Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 12 Mar 2014 16:05:32 -0400 Subject: [PATCH 3/5] Finish basic notifications system and verify it works for the "password_required" notification. --- data/model.py | 2 +- initdb.py | 3 + static/css/quay.css | 45 ++++++++++++-- static/directives/notification-bar.html | 11 ++-- static/directives/notification-view.html | 7 +++ static/js/app.js | 72 ++++++++++++++++++++++- test/data/test.db | Bin 483328 -> 483328 bytes 7 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 static/directives/notification-view.html diff --git a/data/model.py b/data/model.py index 9eec9f841..99e3e52f7 100644 --- a/data/model.py +++ b/data/model.py @@ -1566,4 +1566,4 @@ def list_notifications(user, kind=None): def delete_notifications_by_kind(user, kind): kind_ref = NotificationKind.get(name=kind) - Notification.delete().where(Notification.user == user, Notification.kind == kind_ref).execute() + Notification.delete().where(Notification.notification_user == user, Notification.kind == kind_ref).execute() diff --git a/initdb.py b/initdb.py index 561d98992..78f694f67 100644 --- a/initdb.py +++ b/initdb.py @@ -266,6 +266,9 @@ def populate_database(): new_user_4.verified = True new_user_4.save() + new_user_5 = model.create_user('unverified', 'password', 'no5@thanks.com') + new_user_5.save() + reader = model.create_user('reader', 'password', 'no1@thanks.com') reader.verified = True reader.save() diff --git a/static/css/quay.css b/static/css/quay.css index dc80d0608..f706a8c5e 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -9,6 +9,43 @@ } } +.notification-view-element { + cursor: pointer; + margin-bottom: 10px; + border-bottom: 1px solid #eee; + padding-bottom: 10px; + position: relative; + max-width: 320px; +} + +.notification-view-element .circle { + position: absolute; + top: 14px; + left: 0px; + + width: 12px; + height: 12px; + display: inline-block; + border-radius: 50%; +} + +.notification-view-element .datetime { + margin-top: 10px; + font-size: 12px; + color: #aaa; + text-align: right; +} + +.notification-view-element .container { + padding: 10px; + border-radius: 6px; + margin-left: 16px; +} + +.notification-view-element .container:hover { + background: rgba(66, 139, 202, 0.1); +} + .dockerfile-path { margin-top: 10px; padding: 20px; @@ -507,22 +544,22 @@ i.toggle-icon:hover { min-width: 200px; } -.user-notification.notification-primary { +.notification-primary { background: #428bca; color: white; } -.user-notification.notification-info { +.notification-info { color: black; background: #d9edf7; } -.user-notification.notification-warning { +.notification-warning { color: #8a6d3b; background: #fcf8e3; } -.user-notification.notification-error { +.notification-error { background: red; } diff --git a/static/directives/notification-bar.html b/static/directives/notification-bar.html index f0f5f3363..5d25a40b4 100644 --- a/static/directives/notification-bar.html +++ b/static/directives/notification-bar.html @@ -3,12 +3,13 @@
    -

    Some title

    -
    -
    - +
    +
    +
    +
    +
    diff --git a/static/directives/notification-view.html b/static/directives/notification-view.html new file mode 100644 index 000000000..6d5751995 --- /dev/null +++ b/static/directives/notification-view.html @@ -0,0 +1,7 @@ +
    +
    +
    {{ getMessage(notification) }}
    +
    {{ parseDate(notification.created) | date:'medium'}}
    +
    +
    +
    diff --git a/static/js/app.js b/static/js/app.js index 63fd27024..acf97b97d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -494,10 +494,26 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu 'test_notification': { 'level': 'primary', 'summary': 'This is a test notification', - 'message': 'This notification is a long message for testing' + 'message': 'This notification is a long message for testing', + 'page': '/about/' + }, + 'password_required': { + 'level': 'error', + 'summary': 'A password is needed for your account', + 'message': 'In order to begin pushing and pulling repositories to Quay.io, a password must be set for your account', + 'page': '/user?tab=password' } }; + notificationService.getPage = function(notification) { + return notificationKinds[notification['kind']]['page']; + }; + + notificationService.getMessage = function(notification) { + var kindInfo = notificationKinds[notification['kind']]; + return StringBuilderService.buildString(kindInfo['message'], notification['metadata']); + }; + notificationService.getSummary = function(notification) { var kindInfo = notificationKinds[notification['kind']]; return StringBuilderService.buildString(kindInfo['summary'], notification['metadata']); @@ -512,16 +528,25 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return summaries.join('
    '); }; + notificationService.getClass = function(notification) { + return 'notification-' + notificationKinds[notification['kind']]['level']; + }; + notificationService.getClasses = function(notifications) { var classes = []; for (var i = 0; i < notifications.length; ++i) { var notification = notifications[i]; - classes.push('notification-' + notificationKinds[notification['kind']]['level']); + classes.push(notificationService.getClass(notification)); } return classes.join(' '); }; notificationService.update = function() { + var user = UserService.currentUser(); + if (!user || user.anonymous) { + return; + } + ApiService.listUserNotifications().then(function(resp) { notificationService.notifications = resp['notifications']; notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications); @@ -3320,6 +3345,49 @@ quayApp.directive('buildProgress', function () { }); +quayApp.directive('notificationView', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/notification-view.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'notification': '=notification', + 'parent': '=parent' + }, + controller: function($scope, $element, $location, NotificationService) { + $scope.getMessage = function(notification) { + return NotificationService.getMessage(notification); + }; + + $scope.parseDate = function(dateString) { + return Date.parse(dateString); + }; + + $scope.showNotification = function() { + var url = NotificationService.getPage($scope.notification); + if (url) { + var parts = url.split('?') + $location.path(parts[0]); + + if (parts.length > 1) { + $location.search(parts[1]); + } + + $scope.parent.$hide(); + } + }; + + $scope.getClass = function(notification) { + return NotificationService.getClass(notification); + }; + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('dockerfileBuildDialog', function () { var directiveDefinitionObject = { priority: 0, diff --git a/test/data/test.db b/test/data/test.db index dd1b56708da5e40345ece6be2635fa96e48055fe..168be98baff4437e0f89db90e86cf92e73f6cebe 100644 GIT binary patch delta 6464 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 From 525ef8d14ff1067b6ba1103d27e0aa28bb1a74b3 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 12 Mar 2014 19:00:24 -0400 Subject: [PATCH 4/5] Add support for targeting notifications to organizations and remove the password_required notification for new orbs --- data/database.py | 2 +- data/model.py | 42 +++++++++++++++++------ endpoints/api.py | 1 + static/css/quay.css | 16 ++++++++- static/directives/header-bar.html | 1 + static/directives/notification-view.html | 8 +++-- static/js/app.js | 7 +++- test/data/test.db | Bin 483328 -> 483328 bytes 8 files changed, 61 insertions(+), 16 deletions(-) diff --git a/data/database.py b/data/database.py index c98444ec6..a00078018 100644 --- a/data/database.py +++ b/data/database.py @@ -278,7 +278,7 @@ class NotificationKind(BaseModel): class Notification(BaseModel): uuid = CharField(default=uuid_generator, index=True) kind = ForeignKeyField(NotificationKind, index=True) - notification_user = ForeignKeyField(User, index=True) + target = ForeignKeyField(User, index=True) metadata_json = TextField(default='{}') created = DateTimeField(default=datetime.now, index=True) diff --git a/data/model.py b/data/model.py index 99e3e52f7..b47706703 100644 --- a/data/model.py +++ b/data/model.py @@ -59,7 +59,7 @@ class InvalidBuildTriggerException(DataModelException): pass -def create_user(username, password, email): +def create_user(username, password, email, is_organization=False): if not validate_email(email): raise InvalidEmailAddressException('Invalid email address: %s' % email) if not validate_username(username): @@ -96,7 +96,7 @@ def create_user(username, password, email): # If the password is None, then add a notification for the user to change # their password ASAP. - if not pw_hash: + if not pw_hash and not is_organization: create_notification('password_required', new_user) return new_user @@ -107,7 +107,7 @@ def create_user(username, password, email): def create_organization(name, email, creating_user): try: # Create the org - new_org = create_user(name, None, email) + new_org = create_user(name, None, email, is_organization=True) new_org.organization = True new_org.save() @@ -1546,24 +1546,44 @@ def list_trigger_builds(namespace_name, repository_name, trigger_uuid, .where(RepositoryBuildTrigger.uuid == trigger_uuid)) -def create_notification(kind, user, metadata={}): +def create_notification(kind, target, metadata={}): kind_ref = NotificationKind.get(name=kind) - notification = Notification.create(kind=kind_ref, notification_user=user, + notification = Notification.create(kind=kind_ref, target=target, metadata_json=json.dumps(metadata)) return notification def list_notifications(user, kind=None): + Org = User.alias() + AdminTeam = Team.alias() + AdminTeamMember = TeamMember.alias() + AdminUser = User.alias() + query = (Notification.select() - .join(User) - .where(Notification.notification_user == user)) + .join(User) + .switch(Notification) + .join(Org, JOIN_LEFT_OUTER, on=(Org.id == Notification.target)) + .join(AdminTeam, JOIN_LEFT_OUTER, on=(Org.id == + AdminTeam.organization)) + .join(TeamRole, JOIN_LEFT_OUTER, on=(AdminTeam.role == TeamRole.id)) + .switch(AdminTeam) + .join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id == + AdminTeamMember.team)) + .join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user == + AdminUser.id))) + + where_clause = ((Notification.target == user) | + ((AdminUser.id == user) & + (TeamRole.name == 'admin'))) + if kind: - query = query.join(NotificationKind).where(NotificationKind.name == kind) + where_clause = where_clause & (NotificationKind.name == kind) - return query.order_by(Notification.created).desc() + return query.where(where_clause).order_by(Notification.created).desc() -def delete_notifications_by_kind(user, kind): +def delete_notifications_by_kind(target, kind): kind_ref = NotificationKind.get(name=kind) - Notification.delete().where(Notification.notification_user == user, Notification.kind == kind_ref).execute() + Notification.delete().where(Notification.target == target, + Notification.kind == kind_ref).execute() diff --git a/endpoints/api.py b/endpoints/api.py index 117ba4983..edd1a9b87 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -2521,6 +2521,7 @@ def get_logs(namespace, start_time, end_time, performer_name=None, def notification_view(notification): return { + 'organization': notification.target.username if notification.target.organization else None, 'kind': notification.kind.name, 'created': notification.created, 'metadata': json.loads(notification.metadata_json), diff --git a/static/css/quay.css b/static/css/quay.css index f706a8c5e..98d95fa90 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -18,6 +18,16 @@ max-width: 320px; } +.notification-view-element .orginfo { + margin-top: 8px; + float: left; +} + +.notification-view-element .orginfo .orgname { + font-size: 12px; + color: #aaa; +} + .notification-view-element .circle { position: absolute; top: 14px; @@ -30,12 +40,16 @@ } .notification-view-element .datetime { - margin-top: 10px; + margin-top: 16px; font-size: 12px; color: #aaa; text-align: right; } +.notification-view-element .message { + margin-bottom: 4px; +} + .notification-view-element .container { padding: 10px; border-radius: 6px; diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index bd534f13a..688eb8411 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -44,6 +44,7 @@ ng-class="notificationService.notificationClasses" bs-tooltip="" title="{{ notificationService.notificationSummaries }}" + data-html="true" data-placement="left" data-container="body"> {{ notificationService.notifications.length }} diff --git a/static/directives/notification-view.html b/static/directives/notification-view.html index 6d5751995..5adb81261 100644 --- a/static/directives/notification-view.html +++ b/static/directives/notification-view.html @@ -1,7 +1,11 @@
    -
    {{ getMessage(notification) }}
    -
    {{ parseDate(notification.created) | date:'medium'}}
    +
    {{ getMessage(notification) }}
    +
    + + {{ notification.organization }} +
    +
    {{ parseDate(notification.created) | date:'medium'}}
    diff --git a/static/js/app.js b/static/js/app.js index acf97b97d..62734d089 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3356,11 +3356,16 @@ quayApp.directive('notificationView', function () { 'notification': '=notification', 'parent': '=parent' }, - controller: function($scope, $element, $location, NotificationService) { + controller: function($scope, $element, $location, UserService, NotificationService) { $scope.getMessage = function(notification) { return NotificationService.getMessage(notification); }; + $scope.getGravatar = function(orgname) { + var organization = UserService.getOrganization(orgname); + return organization['gravatar'] || ''; + }; + $scope.parseDate = function(dateString) { return Date.parse(dateString); }; diff --git a/test/data/test.db b/test/data/test.db index 168be98baff4437e0f89db90e86cf92e73f6cebe..e4f9f044f9453ed6c0cd3467f4a90b56f5babb2d 100644 GIT binary patch delta 5021 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 Date: Wed, 12 Mar 2014 19:19:39 -0400 Subject: [PATCH 5/5] Add a check_repository_usage method which adds (or removes) a notification on the user/org when they go over their plan usage --- endpoints/api.py | 5 ++- endpoints/common.py | 11 ++++-- static/directives/header-bar.html | 3 +- static/directives/notification-view.html | 2 +- static/js/app.js | 46 +++++++++++++---------- test/data/test.db | Bin 483328 -> 483328 bytes 6 files changed, 39 insertions(+), 28 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index edd1a9b87..5c86e8710 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -28,7 +28,7 @@ from auth.permissions import (ReadRepositoryPermission, ViewTeamPermission, UserPermission) from endpoints.common import (common_login, get_route_data, truthy_param, - start_build, add_notification) + start_build, check_repository_usage) from endpoints.trigger import (BuildTrigger, TriggerActivationException, TriggerDeactivationException, EmptyRepositoryException) @@ -2197,6 +2197,7 @@ def subscribe(user, plan, token, require_business_plan): cus = stripe.Customer.create(email=user.email, plan=plan, card=card) user.stripe_id = cus.id user.save() + check_repository_usage(user, plan_found) log_action('account_change_plan', user.username, {'plan': plan}) except stripe.CardError as e: return carderror_response(e) @@ -2213,6 +2214,7 @@ def subscribe(user, plan, token, require_business_plan): # We only have to cancel the subscription if they actually have one cus.cancel_subscription() cus.save() + check_repository_usage(user, plan_found) log_action('account_change_plan', user.username, {'plan': plan}) else: @@ -2228,6 +2230,7 @@ def subscribe(user, plan, token, require_business_plan): return carderror_response(e) response_json = subscription_view(cus.subscription, private_repos) + check_repository_usage(user, plan_found) log_action('account_change_plan', user.username, {'plan': plan}) resp = jsonify(response_json) diff --git a/endpoints/common.py b/endpoints/common.py index 0f0e68df7..064d29461 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -126,11 +126,14 @@ def render_page_template(name, **kwargs): return resp -def add_notification(kind, metadata=None, user=None): - if not user and current_user: - user = current_user.db_user() +def check_repository_usage(user_or_org, plan_found): + private_repos = model.get_private_repo_count(user_or_org.username) + repos_allowed = plan_found['privateRepos'] - return model.create_notification(kind, user, metadata or {}) + if private_repos > repos_allowed: + model.create_notification('over_private_usage', user_or_org, {'namespace': user_or_org.username}) + else: + model.delete_notifications_by_kind(user_or_org, 'over_private_usage') def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index 688eb8411..6d31cf951 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -43,8 +43,7 @@ ng-show="notificationService.notifications.length" ng-class="notificationService.notificationClasses" bs-tooltip="" - title="{{ notificationService.notificationSummaries }}" - data-html="true" + title="User Notifications" data-placement="left" data-container="body"> {{ notificationService.notifications.length }} diff --git a/static/directives/notification-view.html b/static/directives/notification-view.html index 5adb81261..6327a5df8 100644 --- a/static/directives/notification-view.html +++ b/static/directives/notification-view.html @@ -1,7 +1,7 @@
    -
    {{ getMessage(notification) }}
    +
    {{ notification.organization }} diff --git a/static/js/app.js b/static/js/app.js index 62734d089..07b0a53ff 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -479,8 +479,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return userService; }]); - $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', - function($rootScope, $interval, UserService, ApiService, StringBuilderService) { + $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', + function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService) { var notificationService = { 'user': null, 'notifications': [], @@ -493,20 +493,35 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu var notificationKinds = { 'test_notification': { 'level': 'primary', - 'summary': 'This is a test notification', 'message': 'This notification is a long message for testing', 'page': '/about/' }, 'password_required': { 'level': 'error', - 'summary': 'A password is needed for your account', 'message': 'In order to begin pushing and pulling repositories to Quay.io, a password must be set for your account', 'page': '/user?tab=password' + }, + 'over_private_usage': { + 'level': 'error', + 'message': 'Namespace {namespace} is over its allowed private repository count. ' + + '

    Please upgrade your plan to avoid disruptions in service.', + 'page': function(metadata) { + var organization = UserService.getOrganization(metadata['namespace']); + if (organization) { + return '/organization/' + metadata['namespace'] + '/admin'; + } else { + return '/user'; + } + } } }; notificationService.getPage = function(notification) { - return notificationKinds[notification['kind']]['page']; + var page = notificationKinds[notification['kind']]['page']; + if (typeof page != 'string') { + page = page(notification['metadata']); + } + return page; }; notificationService.getMessage = function(notification) { @@ -514,20 +529,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return StringBuilderService.buildString(kindInfo['message'], notification['metadata']); }; - notificationService.getSummary = function(notification) { - var kindInfo = notificationKinds[notification['kind']]; - return StringBuilderService.buildString(kindInfo['summary'], notification['metadata']); - }; - - notificationService.getSummaries = function(notifications) { - var summaries = []; - for (var i = 0; i < notifications.length; ++i) { - var notification = notifications[i]; - summaries.push(notificationService.getSummary(notification)); - } - return summaries.join('
    '); - }; - notificationService.getClass = function(notification) { return 'notification-' + notificationKinds[notification['kind']]['level']; }; @@ -550,7 +551,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu ApiService.listUserNotifications().then(function(resp) { notificationService.notifications = resp['notifications']; notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications); - notificationService.notificationSummaries = notificationService.getSummaries(notificationService.notifications); }); }; @@ -559,6 +559,12 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */); }; + // Watch for plan changes and update. + PlanService.registerListener(this, function(plan) { + notificationService.reset(); + notificationService.update(); + }); + // Watch for user changes and update. $rootScope.$watch(function() { return UserService.currentUser(); }, function(currentUser) { notificationService.reset(); diff --git a/test/data/test.db b/test/data/test.db index e4f9f044f9453ed6c0cd3467f4a90b56f5babb2d..ff059481c2a7476689903306e0764ffa8b0aa51c 100644 GIT binary patch delta 4762 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 delta 4748 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 RsYVQG!yTEek2X=|{|g^%hQt5>