diff --git a/data/database.py b/data/database.py index 55e1e0471..5962f85ca 100644 --- a/data/database.py +++ b/data/database.py @@ -299,8 +299,21 @@ class OAuthAccessToken(BaseModel): data = CharField() # What the hell is this field for? +class NotificationKind(BaseModel): + name = CharField(index=True) + + +class Notification(BaseModel): + uuid = CharField(default=uuid_generator, index=True) + kind = ForeignKeyField(NotificationKind, index=True) + target = 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, - OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken] + OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind, + Notification] diff --git a/data/model/legacy.py b/data/model/legacy.py index 059e76c8e..6b811987b 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -58,7 +58,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): @@ -92,6 +92,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 and not is_organization: + create_notification('password_required', new_user) + return new_user except Exception as ex: raise DataModelException(ex.message) @@ -100,7 +106,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() @@ -661,6 +667,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 @@ -1537,3 +1546,46 @@ 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, target, metadata={}): + kind_ref = NotificationKind.get(name=kind) + 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) + + .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: + where_clause = where_clause & (NotificationKind.name == kind) + + return query.where(where_clause).order_by(Notification.created).desc() + + +def delete_notifications_by_kind(target, kind): + kind_ref = NotificationKind.get(name=kind) + Notification.delete().where(Notification.target == target, + Notification.kind == kind_ref).execute() diff --git a/endpoints/api/build.py b/endpoints/api/build.py index 14a4d3d77..bbfdfc289 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -18,9 +18,23 @@ user_files = app.config['USERFILES'] build_logs = app.config['BUILDLOGS'] +def get_trigger_config(trigger): + try: + return json.loads(trigger.config) + except: + return {} + + +def get_job_config(build_obj): + try: + return json.loads(build_obj.job_config) + except: + return None + + def trigger_view(trigger): if trigger and trigger.uuid: - config_dict = json.loads(trigger.config) + config_dict = get_trigger_config(trigger) build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name) return { 'service': trigger.service.name, @@ -42,7 +56,7 @@ def build_status_view(build_obj, can_write=False): 'started': format_date(build_obj.started), 'display_name': build_obj.display_name, 'status': status, - 'job_config': json.loads(build_obj.job_config) if can_write else None, + 'job_config': get_job_config(build_obj) if can_write else None, 'is_writer': can_write, 'trigger': trigger_view(build_obj.trigger), 'resource_key': build_obj.resource_key, @@ -54,7 +68,7 @@ def build_status_view(build_obj, can_write=False): return resp -@resource('/v1/repository//build/') +@resource('/v1/repository//build/') class RepositoryBuildList(RepositoryParamResource): """ Resource related to creating and listing repository builds. """ schemas = { @@ -127,7 +141,7 @@ class RepositoryBuildList(RepositoryParamResource): return resp, 201, headers -@resource('/v1/repository//build//status') +@resource('/v1/repository//build//status') class RepositoryBuildStatus(RepositoryParamResource): """ Resource for dealing with repository build status. """ @require_repo_read @@ -142,7 +156,7 @@ class RepositoryBuildStatus(RepositoryParamResource): return build_status_view(build, can_write) -@resource('/v1/repository//build//logs') +@resource('/v1/repository//build//logs') class RepositoryBuildLogs(RepositoryParamResource): """ Resource for loading repository build logs. """ @require_repo_write diff --git a/endpoints/api/image.py b/endpoints/api/image.py index 01f32a8bd..edfae14b8 100644 --- a/endpoints/api/image.py +++ b/endpoints/api/image.py @@ -29,7 +29,7 @@ def image_view(image): } -@resource('/v1/repository//image/') +@resource('/v1/repository//image/') class RepositoryImageList(RepositoryParamResource): """ Resource for listing repository images. """ @require_repo_read @@ -54,7 +54,7 @@ class RepositoryImageList(RepositoryParamResource): } -@resource('/v1/repository//image/') +@resource('/v1/repository//image/') class RepositoryImage(RepositoryParamResource): """ Resource for handling repository images. """ @require_repo_read @@ -68,7 +68,7 @@ class RepositoryImage(RepositoryParamResource): return image_view(image) -@resource('/v1/repository//image//changes') +@resource('/v1/repository//image//changes') class RepositoryImageChanges(RepositoryParamResource): """ Resource for handling repository image change lists. """ diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index 4c6f6813b..abd2c3e03 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -63,7 +63,7 @@ def get_logs(namespace, start_time, end_time, performer_name=None, } -@resource('/v1/repository//logs') +@resource('/v1/repository//logs') @internal_only class RepositoryLogs(RepositoryParamResource): """ Resource for fetching logs for the specific repository. """ diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py index 17b370f65..601a549e3 100644 --- a/endpoints/api/permission.py +++ b/endpoints/api/permission.py @@ -25,7 +25,7 @@ def wrap_role_view_org(role_json, user, org_members): return role_json -@resource('/v1/repository//permissions/team/') +@resource('/v1/repository//permissions/team/') class RepositoryTeamPermissionList(RepositoryParamResource): """ Resource for repository team permissions. """ @require_repo_admin @@ -40,7 +40,7 @@ class RepositoryTeamPermissionList(RepositoryParamResource): } -@resource('/v1/repository//permissions/user/') +@resource('/v1/repository//permissions/user/') class RepositoryUserPermissionList(RepositoryParamResource): """ Resource for repository user permissions. """ @require_repo_admin @@ -79,7 +79,7 @@ class RepositoryUserPermissionList(RepositoryParamResource): } -@resource('/v1/repository//permissions/user/') +@resource('/v1/repository//permissions/user/') class RepositoryUserPermission(RepositoryParamResource): """ Resource for managing individual user permissions. """ schemas = { @@ -174,7 +174,7 @@ class RepositoryUserPermission(RepositoryParamResource): return 'Deleted', 204 -@resource('/v1/repository//permissions/team/') +@resource('/v1/repository//permissions/team/') class RepositoryTeamPermission(RepositoryParamResource): """ Resource for managing individual team permissions. """ schemas = { diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index fe259e86d..ed09a886c 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -152,7 +152,7 @@ def image_view(image): 'size': extended_props.image_size, } -@resource('/v1/repository/') +@resource('/v1/repository/') class Repository(RepositoryParamResource): """Operations for managing a specific repository.""" schemas = { @@ -248,7 +248,7 @@ class Repository(RepositoryParamResource): return 'Deleted', 204 -@resource('/v1/repository//changevisibility') +@resource('/v1/repository//changevisibility') class RepositoryVisibility(RepositoryParamResource): """ Custom verb for changing the visibility of the repository. """ schemas = { diff --git a/endpoints/api/repotoken.py b/endpoints/api/repotoken.py index e8df11d7e..1d9f6bf3b 100644 --- a/endpoints/api/repotoken.py +++ b/endpoints/api/repotoken.py @@ -18,7 +18,7 @@ def token_view(token_obj): } -@resource('/v1/repository//tokens/') +@resource('/v1/repository//tokens/') class RepositoryTokenList(RepositoryParamResource): """ Resource for creating and listing repository tokens. """ schemas = { @@ -65,7 +65,7 @@ class RepositoryTokenList(RepositoryParamResource): return token_view(token), 201 -@resource('/v1/repository//tokens/') +@resource('/v1/repository//tokens/') class RepositoryToken(RepositoryParamResource): """ Resource for managing individual tokens. """ schemas = { diff --git a/endpoints/api/subscribe.py b/endpoints/api/subscribe.py index 13de3fdec..f9f9d7f14 100644 --- a/endpoints/api/subscribe.py +++ b/endpoints/api/subscribe.py @@ -2,6 +2,7 @@ import logging import stripe from endpoints.api import request_error, log_action, NotFound +from endpoints.common import check_repository_usage from data import model from data.plans import PLANS @@ -58,6 +59,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) @@ -74,6 +76,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: @@ -89,6 +92,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}) return response_json, status_code diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index 371fba2dd..a5783e59b 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -5,7 +5,7 @@ from data import model from auth.auth_context import get_authenticated_user -@resource('/v1/repository//tag/') +@resource('/v1/repository//tag/') class RepositoryTag(RepositoryParamResource): """ Resource for managing repository tags. """ @@ -24,7 +24,7 @@ class RepositoryTag(RepositoryParamResource): return 'Deleted', 204 -@resource('/v1/repository//tag//images') +@resource('/v1/repository//tag//images') class RepositoryTagImages(RepositoryParamResource): """ Resource for listing the images in a specific repository tag. """ @require_repo_read diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index fcd854e37..1eb7cd169 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -9,7 +9,8 @@ from app import app from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, log_action, request_error, query_param, parse_args, internal_only, validate_json_request, api, Unauthorized, NotFound, InvalidRequest) -from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus +from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus, + get_trigger_config) from endpoints.common import start_build from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException, TriggerActivationException, EmptyRepositoryException) @@ -25,7 +26,7 @@ def _prepare_webhook_url(scheme, username, password, hostname, path): return urlunparse((scheme, auth_hostname, path, '', '', '')) -@resource('/v1/repository//trigger/') +@resource('/v1/repository//trigger/') class BuildTriggerList(RepositoryParamResource): """ Resource for listing repository build triggers. """ @@ -39,7 +40,7 @@ class BuildTriggerList(RepositoryParamResource): } -@resource('/v1/repository//trigger/') +@resource('/v1/repository//trigger/') class BuildTrigger(RepositoryParamResource): """ Resource for managing specific build triggers. """ @@ -64,7 +65,7 @@ class BuildTrigger(RepositoryParamResource): raise NotFound() handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) - config_dict = json.loads(trigger.config) + config_dict = get_trigger_config(trigger) if handler.is_active(config_dict): try: handler.deactivate(trigger.auth_token, config_dict) @@ -81,7 +82,7 @@ class BuildTrigger(RepositoryParamResource): return 'No Content', 204 -@resource('/v1/repository//trigger//subdir') +@resource('/v1/repository//trigger//subdir') @internal_only class BuildTriggerSubdirs(RepositoryParamResource): """ Custom verb for fetching the subdirs which are buildable for a trigger. """ @@ -123,7 +124,7 @@ class BuildTriggerSubdirs(RepositoryParamResource): raise Unauthorized() -@resource('/v1/repository//trigger//activate') +@resource('/v1/repository//trigger//activate') @internal_only class BuildTriggerActivate(RepositoryParamResource): """ Custom verb for activating a build trigger once all required information has been collected. @@ -147,7 +148,7 @@ class BuildTriggerActivate(RepositoryParamResource): raise NotFound() handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) - existing_config_dict = json.loads(trigger.config) + existing_config_dict = get_trigger_config(trigger) if handler.is_active(existing_config_dict): raise InvalidRequest('Trigger config is not sufficient for activation.') @@ -191,7 +192,7 @@ class BuildTriggerActivate(RepositoryParamResource): raise Unauthorized() -@resource('/v1/repository//trigger//start') +@resource('/v1/repository//trigger//start') class ActivateBuildTrigger(RepositoryParamResource): """ Custom verb to manually activate a build trigger. """ @@ -205,11 +206,11 @@ class ActivateBuildTrigger(RepositoryParamResource): raise NotFound() handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) - existing_config_dict = json.loads(trigger.config) - if not handler.is_active(existing_config_dict): + config_dict = get_trigger_config(trigger) + if not handler.is_active(config_dict): raise InvalidRequest('Trigger is not active.') - specs = handler.manual_start(trigger.auth_token, json.loads(trigger.config)) + specs = handler.manual_start(trigger.auth_token, config_dict) dockerfile_id, tags, name, subdir = specs repo = model.get_repository(namespace, repository) @@ -225,7 +226,7 @@ class ActivateBuildTrigger(RepositoryParamResource): return resp, 201, headers -@resource('/v1/repository//trigger//builds') +@resource('/v1/repository//trigger//builds') class TriggerBuildList(RepositoryParamResource): """ Resource to represent builds that were activated from the specified trigger. """ @require_repo_admin @@ -242,7 +243,7 @@ class TriggerBuildList(RepositoryParamResource): } -@resource('/v1/repository//trigger//sources') +@resource('/v1/repository//trigger//sources') @internal_only class BuildTriggerSources(RepositoryParamResource): """ Custom verb to fetch the list of build sources for the trigger config. """ diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 97341213e..7b935ef26 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -1,5 +1,6 @@ import logging import stripe +import json from flask import request from flask.ext.login import logout_user @@ -8,7 +9,7 @@ from flask.ext.principal import identity_changed, AnonymousIdentity from app import app from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, NotFound, Unauthorized, require_user_admin, - require_user_read, InvalidToken, require_scope) + require_user_read, InvalidToken, require_scope, format_date) from endpoints.api.subscribe import subscribe from endpoints.common import common_login from data import model @@ -60,6 +61,15 @@ def user_view(user): } +def notification_view(notification): + return { + 'organization': notification.target.username if notification.target.organization else None, + 'kind': notification.kind.name, + 'created': format_date(notification.created), + 'metadata': json.loads(notification.metadata_json), + } + + @resource('/v1/user/') class User(ApiResource): """ Operations related to users. """ @@ -364,3 +374,15 @@ class Recovery(ApiResource): code = model.create_reset_password_email_code(email) send_recovery_email(email, code.code) return 'Created', 201 + + +@resource('/v1/user/notifications') +@internal_only +class UserNotificationList(ApiResource): + @require_user_admin + @nickname('listUserNotifications') + def get(self): + notifications = model.list_notifications(get_authenticated_user()) + return { + 'notifications': [notification_view(notification) for notification in notifications] + } \ No newline at end of file diff --git a/endpoints/api/webhook.py b/endpoints/api/webhook.py index 52a2dc939..b38d7ec43 100644 --- a/endpoints/api/webhook.py +++ b/endpoints/api/webhook.py @@ -14,7 +14,7 @@ def webhook_view(webhook): } -@resource('/v1/repository//webhook/') +@resource('/v1/repository//webhook/') class WebhookList(RepositoryParamResource): """ Resource for dealing with listing and creating webhooks. """ schemas = { @@ -52,7 +52,7 @@ class WebhookList(RepositoryParamResource): } -@resource('/v1/repository//webhook/') +@resource('/v1/repository//webhook/') class Webhook(RepositoryParamResource): """ Resource for dealing with specific webhooks. """ @require_repo_admin diff --git a/endpoints/common.py b/endpoints/common.py index c9b0686cc..ca9d5a4b9 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 @@ -13,13 +13,20 @@ from data.queue import dockerfile_build_queue from app import app, login_manager from auth.permissions import QuayDeferredPermissionUser from endpoints.api.discovery import swagger_route_data +from werkzeug.routing import BaseConverter logger = logging.getLogger(__name__) - route_data = None +class RepoPathConverter(BaseConverter): + regex = '[\.a-zA-Z0-9_\-]+/[\.a-zA-Z0-9_\-]+' + weight = 200 + +app.url_map.converters['repopath'] = RepoPathConverter + + def get_route_data(): global route_data if route_data: @@ -87,13 +94,21 @@ app.jinja_env.globals['csrf_token'] = generate_csrf_token def render_page_template(name, **kwargs): - - resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), - **kwargs)) + resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), **kwargs)) resp.headers['X-FRAME-OPTIONS'] = 'DENY' return resp +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'] + + 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, 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/endpoints/web.py b/endpoints/web.py index f7c3ec009..d5e9a3ce7 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -25,6 +25,7 @@ web = Blueprint('web', __name__) STATUS_TAGS = app.config['STATUS_TAGS'] + @web.route('/', methods=['GET'], defaults={'path': ''}) @web.route('/organization/', methods=['GET']) @no_cache @@ -32,6 +33,11 @@ def index(path): return render_page_template('index.html') +@web.route('/500', methods=['GET']) +def internal_error_display(): + return render_page_template('500.html') + + @web.route('/snapshot', methods=['GET']) @web.route('/snapshot/', methods=['GET']) @web.route('/snapshot/', methods=['GET']) diff --git a/initdb.py b/initdb.py index 77fa9f162..b37fc9225 100644 --- a/initdb.py +++ b/initdb.py @@ -225,6 +225,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.') @@ -262,6 +267,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() @@ -270,6 +278,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/screenshots/screenshots.js b/screenshots/screenshots.js index 9999eb8a8..fbef4ac01 100644 --- a/screenshots/screenshots.js +++ b/screenshots/screenshots.js @@ -15,8 +15,9 @@ var isDebug = !!options['d']; var rootUrl = isDebug ? 'http://localhost:5000/' : 'https://quay.io/'; var repo = isDebug ? 'complex' : 'r0'; -var org = isDebug ? 'buynlarge' : 'quay' -var orgrepo = 'orgrepo' +var org = isDebug ? 'buynlarge' : 'devtable' +var orgrepo = isDebug ? 'buynlarge/orgrepo' : 'quay/testconnect2'; +var buildrepo = isDebug ? 'devtable/building' : 'quay/testconnect2'; var outputDir = "screenshots/"; @@ -32,8 +33,16 @@ casper.on("page.error", function(msg, trace) { }); casper.start(rootUrl + 'signin', function () { + this.wait(1000); +}); + +casper.thenClick('.accordion-toggle[data-target="#collapseSignin"]', function() { + this.wait(1000); +}); + +casper.then(function () { this.fill('.form-signin', { - 'username': 'devtable', + 'username': isDebug ? 'devtable' : 'quaydemo', 'password': isDebug ? 'password': 'C>K98%y"_=54x"<', }, false); }); @@ -43,6 +52,7 @@ casper.thenClick('.form-signin button[type=submit]', function() { }); casper.then(function() { + this.waitForSelector('.fa-lock'); this.log('Generating user home screenshot.'); }); @@ -150,12 +160,25 @@ casper.then(function() { this.log('Generating oganization repository admin screenshot.'); }); -casper.thenOpen(rootUrl + 'repository/' + org + '/' + orgrepo + '/admin', function() { - this.waitForText('outsideorg') +casper.thenOpen(rootUrl + 'repository/' + orgrepo + '/admin', function() { + this.waitForText('Robot Account') }); casper.then(function() { this.capture(outputDir + 'org-repo-admin.png'); }); + +casper.then(function() { + this.log('Generating build history screenshot.'); +}); + +casper.thenOpen(rootUrl + 'repository/' + buildrepo + '/build', function() { + this.waitForText('Starting'); +}); + +casper.then(function() { + this.capture(outputDir + 'build-history.png'); +}); + casper.run(); diff --git a/static/css/quay.css b/static/css/quay.css index 7c0378167..1613d7a5f 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -9,6 +9,57 @@ } } +.notification-view-element { + cursor: pointer; + margin-bottom: 10px; + border-bottom: 1px solid #eee; + padding-bottom: 10px; + position: relative; + 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; + left: 0px; + + width: 12px; + height: 12px; + display: inline-block; + border-radius: 50%; +} + +.notification-view-element .datetime { + 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; + margin-left: 16px; +} + +.notification-view-element .container:hover { + background: rgba(66, 139, 202, 0.1); +} + .dockerfile-path { margin-top: 10px; padding: 20px; @@ -507,7 +558,22 @@ i.toggle-icon:hover { min-width: 200px; } -.user-notification { +.notification-primary { + background: #428bca; + color: white; +} + +.notification-info { + color: black; + background: #d9edf7; +} + +.notification-warning { + color: #8a6d3b; + background: #fcf8e3; +} + +.notification-error { background: red; } @@ -776,11 +842,20 @@ i.toggle-icon:hover { margin-bottom: 16px; } +.new-repo .section-title { + float: right; + color: #aaa; +} + .new-repo .repo-option { margin: 6px; margin-top: 16px; } +.new-repo .repo-option label { + font-weight: normal; +} + .new-repo .repo-option i { font-size: 18px; padding-left: 10px; @@ -2122,16 +2197,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 +2222,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..6d31cf951 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..5d25a40b4 --- /dev/null +++ b/static/directives/notification-bar.html @@ -0,0 +1,15 @@ + diff --git a/static/directives/notification-view.html b/static/directives/notification-view.html new file mode 100644 index 000000000..6327a5df8 --- /dev/null +++ b/static/directives/notification-view.html @@ -0,0 +1,11 @@ +
    +
    +
    +
    +
    + + {{ notification.organization }} +
    +
    {{ parseDate(notification.created) | date:'medium'}}
    +
    +
    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/directives/user-setup.html b/static/directives/user-setup.html index 34f2a0b78..d7cc12cbd 100644 --- a/static/directives/user-setup.html +++ b/static/directives/user-setup.html @@ -3,7 +3,7 @@

    - + Sign In

    diff --git a/static/img/500/background.svg b/static/img/500/background.svg new file mode 100644 index 000000000..711fc05c0 --- /dev/null +++ b/static/img/500/background.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/img/500/ship.svg b/static/img/500/ship.svg new file mode 100644 index 000000000..3330d4720 --- /dev/null +++ b/static/img/500/ship.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/img/500/water.svg b/static/img/500/water.svg new file mode 100644 index 000000000..2c85ebc62 --- /dev/null +++ b/static/img/500/water.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + diff --git a/static/img/build-history.png b/static/img/build-history.png new file mode 100644 index 000000000..794d58cb0 Binary files /dev/null and b/static/img/build-history.png differ diff --git a/static/img/org-admin.png b/static/img/org-admin.png index c069d0f5f..831d21a22 100644 Binary files a/static/img/org-admin.png and b/static/img/org-admin.png differ diff --git a/static/img/org-logs.png b/static/img/org-logs.png index a87bc87ec..d9dd7ace4 100644 Binary files a/static/img/org-logs.png and b/static/img/org-logs.png differ diff --git a/static/img/org-repo-admin.png b/static/img/org-repo-admin.png index 1b700a0f3..e41c798ad 100644 Binary files a/static/img/org-repo-admin.png and b/static/img/org-repo-admin.png differ diff --git a/static/img/org-repo-list.png b/static/img/org-repo-list.png index efedac965..0198cad9e 100644 Binary files a/static/img/org-repo-list.png and b/static/img/org-repo-list.png differ diff --git a/static/img/org-teams.png b/static/img/org-teams.png index b8778639b..975b0f1af 100644 Binary files a/static/img/org-teams.png and b/static/img/org-teams.png differ diff --git a/static/img/repo-admin.png b/static/img/repo-admin.png index 0e7c73422..891cdfc37 100644 Binary files a/static/img/repo-admin.png and b/static/img/repo-admin.png differ diff --git a/static/img/repo-changes.png b/static/img/repo-changes.png index 9fce92e42..bb1ad0e43 100644 Binary files a/static/img/repo-changes.png and b/static/img/repo-changes.png differ diff --git a/static/img/repo-view.png b/static/img/repo-view.png index 57ce983c7..aacbc65c3 100644 Binary files a/static/img/repo-view.png and b/static/img/repo-view.png differ diff --git a/static/img/user-home.png b/static/img/user-home.png index 886a59d52..d42afc9c1 100644 Binary files a/static/img/user-home.png and b/static/img/user-home.png differ diff --git a/static/js/app.js b/static/js/app.js index 33c0dca94..7cd7c2f72 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -102,9 +102,88 @@ 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; + /** + * Specialized wrapper around array which provides a toggle() method for viewing the contents of the + * array in a manner that is asynchronously filled in over a short time period. This prevents long + * pauses in the UI for ngRepeat's when the array is significant in size. + */ + $provide.factory('AngularViewArray', ['$interval', function($interval) { + var ADDTIONAL_COUNT = 50; + + function _ViewArray() { + this.isVisible = false; + this.visibleEntries = null; + this.hasEntries = false; + this.entries = []; + + this.timerRef_ = null; + this.currentIndex_ = 0; + } + + _ViewArray.prototype.push = function(elem) { + this.entries.push(elem); + this.hasEntries = true; + + if (this.isVisible) { + this.setVisible(true); + } + }; + + _ViewArray.prototype.toggle = function() { + this.setVisible(!this.isVisible); + }; + + _ViewArray.prototype.setVisible = function(newState) { + this.isVisible = newState; + + this.visibleEntries = []; + this.currentIndex_ = 0; + + if (newState) { + this.showAdditionalEntries_(); + this.startTimer_(); + } else { + this.stopTimer_(); + } + }; + + _ViewArray.prototype.showAdditionalEntries_ = function() { + var i = 0; + for (i = this.currentIndex_; i < (this.currentIndex_ + ADDTIONAL_COUNT) && i < this.entries.length; ++i) { + this.visibleEntries.push(this.entries[i]); + } + + this.currentIndex_ = i; + if (this.currentIndex_ >= this.entries.length) { + this.stopTimer_(); + } + }; + + _ViewArray.prototype.startTimer_ = function() { + var that = this; + this.timerRef_ = $interval(function() { + that.showAdditionalEntries_(); + }, 10); + }; + + _ViewArray.prototype.stopTimer_ = function() { + if (this.timerRef_) { + $interval.cancel(this.timerRef_); + this.timerRef_ = null; + } + }; + + var service = { + 'create': function() { + return new _ViewArray(); + } + }; + + return service; + }]); $provide.factory('UtilService', ['$sanitize', function($sanitize) { var utilService = {}; @@ -143,6 +222,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 = {}; @@ -360,7 +482,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu anonymous: true, username: null, email: null, - askForPassword: false, organizations: [], logins: [] } @@ -467,6 +588,101 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return userService; }]); + $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', + function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService) { + var notificationService = { + 'user': null, + 'notifications': [], + 'notificationClasses': [], + 'notificationSummaries': [] + }; + + var pollTimerHandle = null; + + var notificationKinds = { + 'test_notification': { + 'level': 'primary', + 'message': 'This notification is a long message for testing', + 'page': '/about/' + }, + 'password_required': { + 'level': 'error', + '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) { + var page = notificationKinds[notification['kind']]['page']; + if (typeof page != 'string') { + page = page(notification['metadata']); + } + return page; + }; + + notificationService.getMessage = function(notification) { + var kindInfo = notificationKinds[notification['kind']]; + return StringBuilderService.buildString(kindInfo['message'], notification['metadata']); + }; + + 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(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); + }); + }; + + notificationService.reset = function() { + $interval.cancel(pollTimerHandle); + 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(); + notificationService.update(); + }); + + return notificationService; + }]); + $provide.factory('KeyService', ['$location', function($location) { var keyService = {} @@ -1405,7 +1621,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; @@ -1620,43 +1836,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); @@ -1921,6 +2103,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, @@ -1939,7 +2146,7 @@ quayApp.directive('popupInputButton', function () { var box = $('#input-box'); box[0].value = ''; box.focus(); - }, 10); + }, 40); }; $scope.getRegexp = function(pattern) { @@ -2153,26 +2360,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(); @@ -3314,6 +3507,54 @@ 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, 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); + }; + + $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, @@ -3594,6 +3835,11 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi } } + if (response.status == 500) { + document.location = '/500'; + return false; + } + return true; }); diff --git a/static/js/controllers.js b/static/js/controllers.js index c539e3bd5..1575dae90 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -919,7 +919,8 @@ function BuildPackageCtrl($scope, Restangular, ApiService, $routeParams, $rootSc getBuildInfo(); } -function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html) { +function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, + ansi2html, AngularViewArray) { var namespace = $routeParams.namespace; var name = $routeParams.name; var pollTimerHandle = null; @@ -985,19 +986,9 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope }; $scope.hasLogs = function(container) { - return ((container.logs && container.logs.length) || (container._logs && container._logs.length)); + return container.logs.hasEntries; }; - $scope.toggleLogs = function(container) { - if (container._logs) { - container.logs = container._logs; - container._logs = null; - } else { - container._logs = container.logs; - container.logs = null; - } - }; - $scope.setCurrentBuild = function(buildId, opt_updateURL) { if (!$scope.builds) { return; } @@ -1085,17 +1076,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope var entry = logs[i]; var type = entry['type'] || 'entry'; if (type == 'command' || type == 'phase' || type == 'error') { - entry['_logs'] = []; + entry['logs'] = AngularViewArray.create(); entry['index'] = startIndex + i; $scope.logEntries.push(entry); - $scope.currentParentEntry = entry; + $scope.currentParentEntry = entry; } else if ($scope.currentParentEntry) { - if ($scope.currentParentEntry['logs']) { - $scope.currentParentEntry['logs'].push(entry); - } else { - $scope.currentParentEntry['_logs'].push(entry); - } + $scope.currentParentEntry['logs'].push(entry); } } }; @@ -1885,13 +1872,16 @@ function V1Ctrl($scope, $location, UserService) { UserService.updateUserIn($scope); } -function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService) { +function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService) { UserService.updateUserIn($scope); + $scope.githubRedirectUri = KeyService.githubRedirectUri; + $scope.githubClientId = KeyService.githubClientId; + $scope.repo = { 'is_public': 1, 'description': '', - 'initialize': false + 'initialize': '' }; // Watch the namespace on the repo. If it changes, we update the plan and the public/private @@ -1960,12 +1950,20 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService $scope.creating = false; $scope.created = created; - // Repository created. Start the upload process if applicable. - if ($scope.repo.initialize) { + // Start the upload process if applicable. + if ($scope.repo.initialize == 'dockerfile' || $scope.repo.initialize == 'zipfile') { $scope.createdForBuild = created; return; } + // Conduct the Github redirect if applicable. + if ($scope.repo.initialize == 'github') { + window.location = 'https://github.com/login/oauth/authorize?client_id=' + $scope.githubClientId + + '&scope=repo,user:email&redirect_uri=' + $scope.githubRedirectUri + '/trigger/' + + repo.namespace + '/' + repo.name; + return; + } + // Otherwise, redirect to the repo page. $location.path('/repository/' + created.namespace + '/' + created.name); }, function(result) { 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/landing.html b/static/partials/landing.html index 10aa345b4..083aebaa0 100644 --- a/static/partials/landing.html +++ b/static/partials/landing.html @@ -106,15 +106,20 @@
    -
    +
    -
    Docker diff in the cloud
    +
    Dockerfile Build in the cloud
    - We wanted to know what was changing in each image of our repositories just as much as you do. So we added diffs. Now you can see exactly which files were added, changed, or removed for each image. We've also provided two awesome ways to view your changes, either in a filterable list, or in a drill down tree view. + Like to use Dockerfiles to build your images? Simply upload your Dockerfile (and any additional files it needs) and we'll build your Dockerfile into an image and push it to your repository. +
    +
    + If you store your Dockerfile in GitHub, add a Build Trigger to your repository and we'll start a Dockerfile build for every change you make.
    - +
    @@ -128,4 +133,14 @@
    Want to share with the world? Make your repository fully public.
    + +
    +
    +
    +
    Docker diff whenever you need it
    +
    + We wanted to know what was changing in each image of our repositories just as much as you do. So we added diffs. Now you can see exactly which files were added, changed, or removed for each image. We've also provided two awesome ways to view your changes, either in a filterable list, or in a drill down tree view. +
    +
    +
    diff --git a/static/partials/new-repo.html b/static/partials/new-repo.html index 25f828620..686073a6b 100644 --- a/static/partials/new-repo.html +++ b/static/partials/new-repo.html @@ -25,13 +25,18 @@ / - + + + + Repository names must match [a-z0-9_-]+
    - Description:
    +
    Repository Description
    +
    @@ -42,13 +47,14 @@
    +
    Repository Visibility
    - + Anyone can see and pull from this repository. You choose who can push.
    @@ -57,7 +63,7 @@
    - + You choose who can see, pull and push from/to this repository.
    @@ -68,7 +74,8 @@ In order to make this repository private under your personal namespace under the organization {{ repo.namespace }}, you will need to upgrade your plan to - + {{ planRequired.title }} . This will cost ${{ planRequired.price / 100 }}/month. @@ -90,31 +97,75 @@
    -
    - -
    - - Automatically populate your repository with a new image constructed from a Dockerfile -
    +
    Initialize repository
    -
    -
    +
    + +
    + + + +
    + + +
    + + + +
    + + +
    + + + +
    + + +
    + + + +
    +
    +
    +
    +
    +
    Upload DockerfileZIP file
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + You will be redirected to authorize via GitHub once the repository has been created +
    +
    +
    +
    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/repo-build.html b/static/partials/repo-build.html index f7aec4838..7feed76a0 100644 --- a/static/partials/repo-build.html +++ b/static/partials/repo-build.html @@ -70,9 +70,9 @@
    + ng-switch on="container.type" ng-click="container.logs.toggle()"> + ng-class="container.logs.isVisible ? 'fa-chevron-down' : 'fa-chevron-right'" ng-show="hasLogs(container)">
    @@ -85,8 +85,8 @@
    -
    -
    +
    +
    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/500.html b/templates/500.html new file mode 100644 index 000000000..65d5dabaf --- /dev/null +++ b/templates/500.html @@ -0,0 +1,91 @@ + + Quay.io - Something went wrong! + + + + + + + + +
    + +

    Something went wrong on our end!

    +

    + We're currently working to fix the problem, but if its persists please feel free to contact us. In the meantime, try a refreshing drink (or just refreshing). +

    +
    + + diff --git a/templates/base.html b/templates/base.html index 0256d8632..1e29026bd 100644 --- a/templates/base.html +++ b/templates/base.html @@ -18,6 +18,8 @@ + + @@ -39,16 +41,17 @@ - - - - + + + + + diff --git a/test/data/test.db b/test/data/test.db index cf5c2a0b9..0063d6055 100644 Binary files a/test/data/test.db and b/test/data/test.db differ diff --git a/test/specs.py b/test/specs.py index c8f53a376..3e6294940 100644 --- a/test/specs.py +++ b/test/specs.py @@ -71,7 +71,6 @@ UPDATE_REPO_DETAILS = { 'description': 'A new description', } - class IndexTestSpec(object): def __init__(self, url, sess_repo=None, anon_code=403, no_access_code=403, read_code=200, admin_code=200): diff --git a/test/test_api_usage.py b/test/test_api_usage.py index aaf812aad..8f4e18bfb 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -788,6 +788,27 @@ class TestDeleteRepository(ApiTestCase): class TestGetRepository(ApiTestCase): PUBLIC_REPO = PUBLIC_USER + '/publicrepo' + + def test_getrepo_badnames(self): + self.login(ADMIN_ACCESS_USER) + + bad_names = ['logs', 'build', 'tokens', 'foo.bar', 'foo-bar', 'foo_bar'] + + # For each bad name, create the repo. + for bad_name in bad_names: + json = self.postJsonResponse(RepositoryList, expected_code=201, + data=dict(repository=bad_name, visibility='public', + description='')) + + # Make sure we can retrieve its information. + json = self.getJsonResponse(Repository, + params=dict(repository=ADMIN_ACCESS_USER + '/' + bad_name)) + + self.assertEquals(ADMIN_ACCESS_USER, json['namespace']) + self.assertEquals(bad_name, json['name']) + self.assertEquals(True, json['is_public']) + + def test_getrepo_public_asguest(self): json = self.getJsonResponse(Repository, params=dict(repository=self.PUBLIC_REPO))