Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage
diff --git a/static/directives/signup-form.html b/static/directives/signup-form.html
index b72c7636e..562223d96 100644
--- a/static/directives/signup-form.html
+++ b/static/directives/signup-form.html
@@ -19,7 +19,7 @@
diff --git a/static/directives/spinner.html b/static/directives/spinner.html
new file mode 100644
index 000000000..c0e0eb0e9
--- /dev/null
+++ b/static/directives/spinner.html
@@ -0,0 +1 @@
+
diff --git a/static/js/app.js b/static/js/app.js
index 60ca1e082..11c8c3d0e 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -586,9 +586,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
// WARNING WARNING WARNING
$routeProvider.
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
- fixFooter: true, reloadOnSearch: false}).
+ fixFooter: false, reloadOnSearch: false}).
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
- fixFooter: true}).
+ fixFooter: false}).
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
@@ -1316,6 +1316,21 @@ quayApp.directive('resourceView', function () {
});
+quayApp.directive('quaySpinner', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/spinner.html',
+ replace: false,
+ transclude: true,
+ restrict: 'C',
+ scope: {},
+ controller: function($scope, $element) {
+ }
+ };
+ return directiveDefinitionObject;
+});
+
+
quayApp.directive('organizationHeader', function () {
var directiveDefinitionObject = {
priority: 0,
@@ -1579,18 +1594,18 @@ quayApp.directive('entitySearch', function () {
number++;
var input = $element[0].firstChild.firstChild;
- var namespace = $scope.namespace || '';
$(input).typeahead({
name: 'entities' + number,
remote: {
url: '/api/entities/%QUERY',
replace: function (url, uriEncodedQuery) {
- url = url.replace('%QUERY', uriEncodedQuery);
- url += '?namespace=' + encodeURIComponent(namespace);
- if ($scope.includeTeams) {
- url += '&includeTeams=true'
- }
- return url;
+ var namespace = $scope.namespace || '';
+ url = url.replace('%QUERY', uriEncodedQuery);
+ url += '?namespace=' + encodeURIComponent(namespace);
+ if ($scope.includeTeams) {
+ url += '&includeTeams=true'
+ }
+ return url;
},
filter: function(data) {
var datums = [];
@@ -1616,8 +1631,8 @@ quayApp.directive('entitySearch', function () {
}
template += '
' + datum.value + ' ';
- if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member && datum.kind == 'user') {
- template += '
This user is outside your organization
';
+ if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
+ template += '
';
}
template += '
';
diff --git a/static/js/controllers.js b/static/js/controllers.js
index bbe67a53e..adfc79962 100644
--- a/static/js/controllers.js
+++ b/static/js/controllers.js
@@ -539,7 +539,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
newWebhook.customPOST($scope.newWebhook).then(function(resp) {
$scope.webhooks.push(resp);
$scope.newWebhook.url = '';
- $scope.newWebhookForm.$setPristine();
+ $scope.createWebhookForm.$setPristine();
});
};
@@ -590,23 +590,19 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
}
function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, UserService, KeyService, $routeParams) {
- $scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
+ if ($routeParams['migrate']) {
+ $('#migrateTab').tab('show')
+ }
+
+ UserService.updateUserIn($scope, function(user) {
$scope.askForPassword = currentUser.askForPassword;
- if (!currentUser.anonymous) {
- $scope.user = currentUser;
- }
- $scope.loading = false;
- }, true);
+ });
$scope.readyForPlan = function() {
// Show the subscribe dialog if a plan was requested.
return $routeParams['plan'];
};
- if ($routeParams['migrate']) {
- $('#migrateTab').tab('show')
- }
-
$scope.loading = true;
$scope.updatingUser = false;
$scope.changePasswordSuccess = false;
@@ -685,13 +681,11 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
};
}
-function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
+function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, Restangular) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var imageid = $routeParams.image;
- $('#copyClipboard').clipboardCopy();
-
$scope.parseDate = function(dateString) {
return Date.parse(dateString);
};
@@ -737,61 +731,63 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, Restangular) {
if ($scope.tree) { return; }
$scope.tree = new ImageFileChangeTree($scope.image, $scope.combinedChanges);
- setTimeout(function() {
+ $timeout(function() {
$scope.tree.draw('changes-tree-container');
}, 10);
};
- // Fetch the image.
- $scope.loading = true;
- var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid);
- imageFetch.get().then(function(image) {
- $scope.loading = false;
- $scope.repo = {
- 'name': name,
- 'namespace': namespace
- };
- $scope.image = image;
- $rootScope.title = 'View Image - ' + image.id;
- $rootScope.description = 'Viewing docker image ' + image.id + ' under repository ' + namespace + '/' + name +
+ var fetchImage = function() {
+ $scope.image = ApiService.at('repository', namespace, name, 'image', imageid).get(function(image) {
+ $scope.repo = {
+ 'name': name,
+ 'namespace': namespace
+ };
+
+ $rootScope.title = 'View Image - ' + image.id;
+ $rootScope.description = 'Viewing docker image ' + image.id + ' under repository ' + namespace + '/' + name +
': Image changes tree and list view';
- }, function() {
- $rootScope.title = 'Unknown Image';
- $scope.loading = false;
- });
- // Fetch the image changes.
- var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid + '/changes');
- changesFetch.get().then(function(changes) {
- var combinedChanges = [];
- var addCombinedChanges = function(c, kind) {
- for (var i = 0; i < c.length; ++i) {
- combinedChanges.push({
- 'kind': kind,
- 'file': c[i]
- });
- }
- };
+ // Fetch the image's changes.
+ fetchChanges();
- addCombinedChanges(changes.added, 'added');
- addCombinedChanges(changes.removed, 'removed');
- addCombinedChanges(changes.changed, 'changed');
+ $('#copyClipboard').clipboardCopy();
- $scope.combinedChanges = combinedChanges;
- $scope.imageChanges = changes;
- });
+ return image;
+ });
+ };
+
+ var fetchChanges = function() {
+ var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid + '/changes');
+ changesFetch.get().then(function(changes) {
+ var combinedChanges = [];
+ var addCombinedChanges = function(c, kind) {
+ for (var i = 0; i < c.length; ++i) {
+ combinedChanges.push({
+ 'kind': kind,
+ 'file': c[i]
+ });
+ }
+ };
+
+ addCombinedChanges(changes.added, 'added');
+ addCombinedChanges(changes.removed, 'removed');
+ addCombinedChanges(changes.changed, 'changed');
+
+ $scope.combinedChanges = combinedChanges;
+ $scope.imageChanges = changes;
+ });
+ };
+
+ // Fetch the image.
+ fetchImage();
}
function V1Ctrl($scope, $location, UserService) {
- $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- }, true);
+ UserService.updateUserIn($scope);
}
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangular, PlanService) {
- $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- }, true);
+ UserService.updateUserIn($scope);
$scope.repo = {
'is_public': 1,
@@ -805,6 +801,94 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
});
});
+ // Watch the namespace on the repo. If it changes, we update the plan and the public/private
+ // accordingly.
+ $scope.isUserNamespace = true;
+ $scope.$watch('repo.namespace', function(namespace) {
+ // Note: Can initially be undefined.
+ if (!namespace) { return; }
+
+ var isUserNamespace = (namespace == $scope.user.username);
+
+ $scope.planRequired = null;
+ $scope.isUserNamespace = isUserNamespace;
+
+ if (isUserNamespace) {
+ // Load the user's subscription information in case they want to create a private
+ // repository.
+ PlanService.getSubscription(null, subscribedToPlan, function() {
+ PlanService.getMinimumPlan(1, false, function(minimum) { $scope.planRequired = minimum; });
+ });
+ } else {
+ $scope.planRequired = null;
+
+ var checkPrivateAllowed = Restangular.one('organization/' + namespace + '/private');
+ checkPrivateAllowed.get().then(function(resp) {
+ $scope.planRequired = resp.privateAllowed ? null : {};
+ }, function() {
+ $scope.planRequired = {};
+ });
+
+ // Auto-set to private repo.
+ $scope.repo.is_public = '0';
+ }
+ });
+
+ $scope.createNewRepo = function() {
+ $('#repoName').popover('hide');
+
+ var uploader = $('#file-drop')[0];
+ if ($scope.repo.initialize && uploader.files.length < 1) {
+ $('#missingfileModal').modal();
+ return;
+ }
+
+ $scope.creating = true;
+ var repo = $scope.repo;
+ var data = {
+ 'namespace': repo.namespace,
+ 'repository': repo.name,
+ 'visibility': repo.is_public == '1' ? 'public' : 'private',
+ 'description': repo.description
+ };
+
+ var createPost = Restangular.one('repository');
+ createPost.customPOST(data).then(function(created) {
+ $scope.creating = false;
+ $scope.created = created;
+
+ // Repository created. Start the upload process if applicable.
+ if ($scope.repo.initialize) {
+ startFileUpload(created);
+ return;
+ }
+
+ // Otherwise, redirect to the repo page.
+ $location.path('/repository/' + created.namespace + '/' + created.name);
+ }, function(result) {
+ $scope.creating = false;
+ $scope.createError = result.data;
+ $timeout(function() {
+ $('#repoName').popover('show');
+ });
+ });
+ };
+
+ $scope.upgradePlan = function() {
+ var callbacks = {
+ 'started': function() { $scope.planChanging = true; },
+ 'opened': function() { $scope.planChanging = true; },
+ 'closed': function() { $scope.planChanging = false; },
+ 'success': subscribedToPlan,
+ 'failure': function(resp) {
+ $('#couldnotsubscribeModal').modal();
+ $scope.planChanging = false;
+ }
+ };
+
+ PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
+ };
+
var startBuild = function(repo, fileId) {
$scope.building = true;
@@ -890,120 +974,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
}
});
};
-
- $scope.createNewRepo = function() {
- $('#repoName').popover('hide');
-
- var uploader = $('#file-drop')[0];
- if ($scope.repo.initialize && uploader.files.length < 1) {
- $('#missingfileModal').modal();
- return;
- }
-
- $scope.creating = true;
- var repo = $scope.repo;
- var data = {
- 'namespace': repo.namespace,
- 'repository': repo.name,
- 'visibility': repo.is_public == '1' ? 'public' : 'private',
- 'description': repo.description
- };
-
- var createPost = Restangular.one('repository');
- createPost.customPOST(data).then(function(created) {
- $scope.creating = false;
- $scope.created = created;
-
- // Repository created. Start the upload process if applicable.
- if ($scope.repo.initialize) {
- startFileUpload(created);
- return;
- }
-
- // Otherwise, redirect to the repo page.
- $location.path('/repository/' + created.namespace + '/' + created.name);
- }, function(result) {
- $scope.creating = false;
- $scope.createError = result.data;
- $timeout(function() {
- $('#repoName').popover('show');
- });
- });
- };
-
- $scope.upgradePlan = function() {
- var callbacks = {
- 'started': function() { $scope.planChanging = true; },
- 'opened': function() { $scope.planChanging = true; },
- 'closed': function() { $scope.planChanging = false; },
- 'success': subscribedToPlan,
- 'failure': function(resp) {
- $('#couldnotsubscribeModal').modal();
- $scope.planChanging = false;
- }
- };
-
- PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
- };
-
- // Watch the namespace on the repo. If it changes, we update the plan and the public/private
- // accordingly.
- $scope.isUserNamespace = true;
- $scope.$watch('repo.namespace', function(namespace) {
- // Note: Can initially be undefined.
- if (!namespace) { return; }
-
- var isUserNamespace = (namespace == $scope.user.username);
-
- $scope.planRequired = null;
- $scope.isUserNamespace = isUserNamespace;
-
- if (isUserNamespace) {
- // Load the user's subscription information in case they want to create a private
- // repository.
- PlanService.getSubscription(null, subscribedToPlan, function() {
- PlanService.getMinimumPlan(1, false, function(minimum) { $scope.planRequired = minimum; });
- });
- } else {
- $scope.planRequired = null;
-
- var checkPrivateAllowed = Restangular.one('organization/' + namespace + '/private');
- checkPrivateAllowed.get().then(function(resp) {
- $scope.planRequired = resp.privateAllowed ? null : {};
- }, function() {
- $scope.planRequired = {};
- });
-
- // Auto-set to private repo.
- $scope.repo.is_public = '0';
- }
- });
}
-function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) {
+function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
+ var orgname = $routeParams.orgname;
+
$('.info-icon').popover({
- 'trigger': 'hover',
- 'html': true
+ 'trigger': 'hover',
+ 'html': true
});
$scope.TEAM_PATTERN = TEAM_PATTERN;
$rootScope.title = 'Loading...';
- var orgname = $routeParams.orgname;
-
- var loadOrganization = function() {
- var getOrganization = Restangular.one(getRestUrl('organization', orgname));
- getOrganization.get().then(function(resp) {
- $scope.organization = resp;
- $scope.loading = false;
-
- $rootScope.title = orgname;
- $rootScope.description = 'Viewing organization ' + orgname;
- }, function() {
- $scope.loading = false;
- });
- };
-
$scope.teamRoles = [
{ 'id': 'member', 'title': 'Member', 'kind': 'default' },
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
@@ -1063,10 +1046,21 @@ function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) {
});
};
+ var loadOrganization = function() {
+ $scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
+ $scope.organization = org;
+ $rootScope.title = orgname;
+ $rootScope.description = 'Viewing organization ' + orgname;
+ });
+ };
+
+ // Load the organization.
loadOrganization();
}
-function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService, PlanService) {
+function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService, PlanService, ApiService) {
+ var orgname = $routeParams.orgname;
+
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans.business;
@@ -1082,8 +1076,6 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
addPlans(plans.business);
});
- var orgname = $routeParams.orgname;
-
$scope.orgname = orgname;
$scope.membersLoading = true;
$scope.membersFound = null;
@@ -1133,35 +1125,32 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
};
var loadOrganization = function() {
- var getOrganization = Restangular.one(getRestUrl('organization', orgname));
- getOrganization.get().then(function(resp) {
- if (resp && resp.is_admin) {
- $scope.organization = resp;
+ $scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
+ if (org && org.is_admin) {
+ $scope.organization = org;
$rootScope.title = orgname + ' (Admin)';
$rootScope.description = 'Administration page for organization ' + orgname;
}
-
- $scope.loading = false;
- }, function() {
- $scope.loading = false;
});
};
+ // Load the organization.
loadOrganization();
}
-function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
+function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
$('.info-icon').popover({
- 'trigger': 'hover',
- 'html': true
+ 'trigger': 'hover',
+ 'html': true
});
- $scope.orgname = $routeParams.orgname;
var teamname = $routeParams.teamname;
+ var orgname = $routeParams.orgname;
+
+ $scope.orgname = orgname;
+ $scope.teamname = teamname;
$rootScope.title = 'Loading...';
- $scope.loading = true;
- $scope.teamname = teamname;
$scope.addNewMember = function(member) {
if ($scope.members[member.name]) { return; }
@@ -1195,57 +1184,38 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) {
};
var loadOrganization = function() {
- var getOrganization = Restangular.one(getRestUrl('organization', $scope.orgname))
- getOrganization.get().then(function(resp) {
- $scope.organization = resp;
+ $scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
+ $scope.organization = org;
$scope.team = $scope.organization.teams[teamname];
- $scope.loading = !$scope.organization || !$scope.members;
- }, function() {
- $scope.organization = null;
- $scope.members = null;
- $scope.loading = false;
+ $rootScope.title = teamname + ' (' + $scope.orgname + ')';
+ $rootScope.description = 'Team management page for team ' + teamname + ' under organization ' + $scope.orgname;
+ loadMembers();
+ return org;
});
};
var loadMembers = function() {
- var getMembers = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members'));
- getMembers.get().then(function(resp) {
+ $scope.membersResource = ApiService.at('organization', $scope.orgname, 'team', teamname, 'members').get(function(resp) {
$scope.members = resp.members;
$scope.canEditMembers = resp.can_edit;
- $scope.loading = !$scope.organization || !$scope.members;
- $rootScope.title = teamname + ' (' + $scope.orgname + ')';
- $rootScope.description = 'Team management page for team ' + teamname + ' under organization ' + $scope.orgname;
- }, function() {
- $scope.organization = null;
- $scope.members = null;
- $scope.loading = false;
- });
+ return resp.members;
+ });
};
+ // Load the organization.
loadOrganization();
- loadMembers();
}
function OrgsCtrl($scope, UserService) {
- $scope.loading = true;
-
- $scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- $scope.loading = false;
- }, true);
+ UserService.updateUserIn($scope);
browserchrome.update();
}
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, Restangular) {
- $scope.loading = true;
-
- $scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
- $scope.user = currentUser;
- $scope.loading = false;
- }, true);
+ UserService.updateUserIn($scope);
- requested = $routeParams['plan'];
+ var requested = $routeParams['plan'];
// Load the list of plans.
PlanService.getPlans(function(plans) {
@@ -1284,13 +1254,13 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
var createPost = Restangular.one('organization/');
createPost.customPOST(data).then(function(created) {
- $scope.creating = false;
$scope.created = created;
// Reset the organizations list.
UserService.load();
var showOrg = function() {
+ $scope.creating = false;
$location.path('/organization/' + org.name + '/');
};
@@ -1301,6 +1271,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
}
// Otherwise, show the subscribe for the plan.
+ $scope.creating = true;
var callbacks = {
'opened': function() { $scope.creating = true; },
'closed': showOrg,
@@ -1308,7 +1279,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
'failure': showOrg
};
- PlanService.changePlan($scope, org.name, $scope.currentPlan.stripeId, false, callbacks);
+ PlanService.changePlan($scope, org.name, $scope.currentPlan.stripeId, callbacks);
}, function(result) {
$scope.creating = false;
$scope.createError = result.data.message || result.data;
@@ -1320,49 +1291,38 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
}
-function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangular) {
+function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangular, ApiService) {
var orgname = $routeParams.orgname;
var membername = $routeParams.membername;
$scope.orgname = orgname;
- $scope.loading = true;
$scope.memberInfo = null;
$scope.ready = false;
- var checkReady = function() {
- $scope.loading = !$scope.organization || !$scope.memberInfo;
- if (!$scope.loading) {
- $rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')';
- $rootScope.description = 'Shows all the actions of ' + $scope.memberInfo.username +
- ' under organization ' + $scope.orgname;
- $timeout(function() {
- $scope.ready = true;
- });
- }
- };
-
var loadOrganization = function() {
- var getOrganization = Restangular.one(getRestUrl('organization', orgname))
- getOrganization.get().then(function(resp) {
- $scope.organization = resp;
- checkReady();
- }, function() {
- $scope.organization = null;
- $scope.loading = false;
+ $scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
+ $scope.organization = org;
+ return org;
});
};
var loadMemberInfo = function() {
- var getMemberInfo = Restangular.one(getRestUrl('organization', orgname, 'members', membername))
- getMemberInfo.get().then(function(resp) {
+ $scope.memberResource = ApiService.at('organization', $scope.orgname, 'members', membername).get(function(resp) {
$scope.memberInfo = resp.member;
- checkReady();
- }, function() {
- $scope.memberInfo = null;
- $scope.loading = false;
- });
+
+ $rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')';
+ $rootScope.description = 'Shows all the actions of ' + $scope.memberInfo.username +
+ ' under organization ' + $scope.orgname;
+
+ $timeout(function() {
+ $scope.ready = true;
+ });
+
+ return resp.member;
+ });
};
+ // Load the org info and the member info.
loadOrganization();
loadMemberInfo();
}
\ No newline at end of file
diff --git a/static/js/graphing.js b/static/js/graphing.js
index d92a6bfa3..fbf5f2162 100644
--- a/static/js/graphing.js
+++ b/static/js/graphing.js
@@ -54,7 +54,7 @@ ImageHistoryTree.prototype.calculateDimensions_ = function(container) {
var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10);
- var margin = { top: 40, right: 20, bottom: 20, left: 40 };
+ var margin = { top: 40, right: 20, bottom: 20, left: 80 };
var m = [margin.top, margin.right, margin.bottom, margin.left];
var w = cw - m[1] - m[3];
var h = ch - m[0] - m[2];
@@ -87,6 +87,7 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
var viewportHeight = $(window).height();
var boundingBox = document.getElementById(container).getBoundingClientRect();
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 110) + 'px';
+
$('#' + container).overscroll();
// Update the tree.
@@ -94,9 +95,13 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
var tree = this.tree_;
var vis = this.vis_;
+
+ var ow = w + m[1] + m[3];
+ var oh = h + m[0] + m[2];
rootSvg
- .attr("width", w + m[1] + m[3])
- .attr("height", h + m[0] + m[2]);
+ .attr("width", ow)
+ .attr("height", oh)
+ .attr("style", "width: " + ow + "px; height: " + oh + "px");
tree.size([w, h]);
vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
diff --git a/static/lib/jquery.overscroll.js b/static/lib/jquery.overscroll.js
new file mode 100644
index 000000000..7126a8b1e
--- /dev/null
+++ b/static/lib/jquery.overscroll.js
@@ -0,0 +1,793 @@
+/**
+ * Overscroll v1.7.3
+ * A jQuery Plugin that emulates the iPhone scrolling experience in a browser.
+ * http://azoffdesign.com/overscroll
+ *
+ * Intended for use with the latest jQuery
+ * http://code.jquery.com/jquery-latest.js
+ *
+ * Copyright 2013, Jonathan Azoff
+ * Licensed under the MIT license.
+ * https://github.com/azoff/overscroll/blob/master/mit.license
+ *
+ * For API documentation, see the README file
+ * http://azof.fr/pYCzuM
+ *
+ * Date: Tuesday, March 18th 2013
+ */
+(function(global, dom, browser, math, wait, cancel, namespace, $, none){
+
+ // We want to run this plug-in in strict-mode
+ // so that we may benefit from its optimizations
+ 'use strict';
+
+ // The key used to bind-instance specific data to an object
+ var datakey = 'overscroll';
+
+ // create node if there's not one present (e.g., for test runners)
+ if (dom.body === null) {
+ dom.documentElement.appendChild(
+ dom.createElement('body')
+ );
+ }
+
+ // quick fix for IE 8 and below since getComputedStyle() is not supported
+ // TODO: find a better solution
+ if (!global.getComputedStyle) {
+ global.getComputedStyle = function (el, pseudo) {
+ this.el = el;
+ this.getPropertyValue = function (prop) {
+ var re = /(\-([a-z]){1})/g;
+ if (prop == 'float') prop = 'styleFloat';
+ if (re.test(prop)) {
+ prop = prop.replace(re, function () {
+ return arguments[2].toUpperCase();
+ });
+ }
+ return el.currentStyle[prop] ? el.currentStyle[prop] : null;
+ };
+ return this;
+ };
+ }
+
+ // runs feature detection for overscroll
+ var compat = {
+ animate: (function(){
+ var fn = global.requestAnimationFrame ||
+ global.webkitRequestAnimationFrame ||
+ global.mozRequestAnimationFrame ||
+ global.oRequestAnimationFrame ||
+ global.msRequestAnimationFrame ||
+ function(callback) { wait(callback, 1000/60); };
+ return function(callback) {
+ fn.call(global, callback);
+ };
+ })(),
+ overflowScrolling: (function(){
+ var style = '';
+ var div = dom.createElement('div');
+ var prefixes = ['webkit', 'moz', 'o', 'ms'];
+ dom.body.appendChild(div);
+ $.each(prefixes, function(i, prefix){
+ div.style[prefix + 'OverflowScrolling'] = 'touch';
+ });
+ div.style.overflowScrolling = 'touch';
+ var computedStyle = global.getComputedStyle(div);
+ if (!!computedStyle.overflowScrolling) {
+ style = 'overflow-scrolling';
+ } else {
+ $.each(prefixes, function(i, prefix){
+ if (!!computedStyle[prefix + 'OverflowScrolling']) {
+ style = '-' + prefix + '-overflow-scrolling';
+ }
+ return !style;
+ });
+ }
+ div.parentNode.removeChild(div);
+ return style;
+ })(),
+ cursor: (function() {
+ var div = dom.createElement('div');
+ var prefixes = ['webkit', 'moz'];
+ var gmail = 'https://mail.google.com/mail/images/2/';
+ var style = {
+ grab: 'url('+gmail+'openhand.cur), move',
+ grabbing: 'url('+gmail+'closedhand.cur), move'
+ };
+ dom.body.appendChild(div);
+ $.each(prefixes, function(i, prefix){
+ var found, cursor = '-' + prefix + '-grab';
+ div.style.cursor = cursor;
+ var computedStyle = global.getComputedStyle(div);
+ found = computedStyle.cursor === cursor;
+ if (found) {
+ style = {
+ grab: '-' + prefix + '-grab',
+ grabbing: '-' + prefix + '-grabbing'
+ };
+ }
+ return !found;
+ });
+ div.parentNode.removeChild(div);
+ return style;
+ })()
+ };
+
+ // These are all the events that could possibly
+ // be used by the plug-in
+ var events = {
+ drag: 'mousemove touchmove',
+ end: 'mouseup mouseleave click touchend touchcancel',
+ hover: 'mouseenter mouseleave',
+ ignored: 'select dragstart drag',
+ scroll: 'scroll',
+ start: 'mousedown touchstart',
+ wheel: 'mousewheel DOMMouseScroll'
+ };
+
+ // These settings are used to tweak drift settings
+ // for the plug-in
+ var settings = {
+ captureThreshold: 3,
+ driftDecay: 1.1,
+ driftSequences: 22,
+ driftTimeout: 100,
+ scrollDelta: 15,
+ thumbOpacity: 0.7,
+ thumbThickness: 6,
+ thumbTimeout: 400,
+ wheelDelta: 20,
+ wheelTicks: 120
+ };
+
+ // These defaults are used to complement any options
+ // passed into the plug-in entry point
+ var defaults = {
+ cancelOn: 'select,input,textarea',
+ direction: 'multi',
+ dragHold: false,
+ hoverThumbs: false,
+ scrollDelta: settings.scrollDelta,
+ showThumbs: true,
+ persistThumbs: false,
+ captureWheel: true,
+ wheelDelta: settings.wheelDelta,
+ wheelDirection: 'multi',
+ zIndex: 999,
+ ignoreSizing: false
+ };
+
+ // Triggers a DOM event on the overscrolled element.
+ // All events are namespaced under the overscroll name
+ function triggerEvent(event, target) {
+ target.trigger('overscroll:' + event);
+ }
+
+ // Utility function to return a timestamp
+ function time() {
+ return (new Date()).getTime();
+ }
+
+ // Captures the position from an event, modifies the properties
+ // of the second argument to persist the position, and then
+ // returns the modified object
+ function capturePosition(event, position, index) {
+ position.x = event.pageX;
+ position.y = event.pageY;
+ position.time = time();
+ position.index = index;
+ return position;
+ }
+
+ // Used to move the thumbs around an overscrolled element
+ function moveThumbs(thumbs, sizing, left, top) {
+
+ var ml, mt;
+
+ if (thumbs && thumbs.added) {
+ if (thumbs.horizontal) {
+ ml = left * (1 + sizing.container.width / sizing.container.scrollWidth);
+ mt = top + sizing.thumbs.horizontal.top;
+ thumbs.horizontal.css('margin', mt + 'px 0 0 ' + ml + 'px');
+ }
+ if (thumbs.vertical) {
+ ml = left + sizing.thumbs.vertical.left;
+ mt = top * (1 + sizing.container.height / sizing.container.scrollHeight);
+ thumbs.vertical.css('margin', mt + 'px 0 0 ' + ml + 'px');
+ }
+ }
+
+ }
+
+ // Used to toggle the thumbs on and off
+ // of an overscrolled element
+ function toggleThumbs(thumbs, options, dragging) {
+ if (thumbs && thumbs.added && !options.persistThumbs) {
+ if (dragging) {
+ if (thumbs.vertical) {
+ thumbs.vertical.stop(true, true).fadeTo('fast', settings.thumbOpacity);
+ }
+ if (thumbs.horizontal) {
+ thumbs.horizontal.stop(true, true).fadeTo('fast', settings.thumbOpacity);
+ }
+ } else {
+ if (thumbs.vertical) {
+ thumbs.vertical.fadeTo('fast', 0);
+ }
+ if (thumbs.horizontal) {
+ thumbs.horizontal.fadeTo('fast', 0);
+ }
+ }
+ }
+ }
+
+ // Defers click event listeners to after a mouseup event.
+ // Used to avoid unintentional clicks
+ function deferClick(target) {
+ var clicks, key = 'events';
+ var events = $._data ? $._data(target[0], key) : target.data(key);
+ if (events && events.click) {
+ clicks = events.click.slice();
+ target.off('click').one('click', function(){
+ $.each(clicks, function(i, click){
+ target.click(click);
+ }); return false;
+ });
+ }
+ }
+
+ // Toggles thumbs on hover. This event is only triggered
+ // if the hoverThumbs option is set
+ function hover(event) {
+ var data = event.data,
+ thumbs = data.thumbs,
+ options = data.options,
+ dragging = event.type === 'mouseenter';
+ toggleThumbs(thumbs, options, dragging);
+ }
+
+ // This function is only ever used when the overscrolled element
+ // scrolled outside of the scope of this plugin.
+ function scroll(event) {
+ var data = event.data;
+ if (!data.flags.dragged) {
+ /*jshint validthis:true */
+ moveThumbs(data.thumbs, data.sizing, this.scrollLeft, this.scrollTop);
+ }
+ }
+
+ // handles mouse wheel scroll events
+ function wheel(event) {
+
+ // prevent any default wheel behavior
+ event.preventDefault();
+
+ var data = event.data,
+ options = data.options,
+ sizing = data.sizing,
+ thumbs = data.thumbs,
+ dwheel = data.wheel,
+ flags = data.flags,
+ original = event.originalEvent,
+ delta = 0, deltaX = 0, deltaY = 0;
+
+ // stop any drifts
+ flags.drifting = false;
+
+ // normalize the wheel ticks
+ if (original.detail) {
+ delta = -original.detail;
+ if (original.detailX) {
+ deltaX = -original.detailX;
+ }
+ if (original.detailY) {
+ deltaY = -original.detailY;
+ }
+ } else if (original.wheelDelta) {
+ delta = original.wheelDelta / settings.wheelTicks;
+ if (original.wheelDeltaX) {
+ deltaX = original.wheelDeltaX / settings.wheelTicks;
+ }
+ if (original.wheelDeltaY) {
+ deltaY = original.wheelDeltaY / settings.wheelTicks;
+ }
+ }
+
+ // apply a pixel delta to each tick
+ delta *= options.wheelDelta;
+ deltaX *= options.wheelDelta;
+ deltaY *= options.wheelDelta;
+
+ // initialize flags if this is the first tick
+ if (!dwheel) {
+ data.target.data(datakey).dragging = flags.dragging = true;
+ data.wheel = dwheel = { timeout: null };
+ toggleThumbs(thumbs, options, true);
+ }
+
+ // actually modify scroll offsets
+ if (options.wheelDirection === 'vertical'){
+ /*jshint validthis:true */
+ this.scrollTop -= delta;
+ } else if ( options.wheelDirection === 'horizontal') {
+ this.scrollLeft -= delta;
+ } else {
+ this.scrollLeft -= deltaX;
+ this.scrollTop -= deltaY || delta;
+ }
+
+ if (dwheel.timeout) { cancel(dwheel.timeout); }
+
+ moveThumbs(thumbs, sizing, this.scrollLeft, this.scrollTop);
+
+ dwheel.timeout = wait(function() {
+ data.target.data(datakey).dragging = flags.dragging = false;
+ toggleThumbs(thumbs, options, data.wheel = null);
+ }, settings.thumbTimeout);
+
+ }
+
+ // updates the current scroll offset during a mouse move
+ function drag(event) {
+
+ event.preventDefault();
+
+ var data = event.data,
+ touches = event.originalEvent.touches,
+ options = data.options,
+ sizing = data.sizing,
+ thumbs = data.thumbs,
+ position = data.position,
+ flags = data.flags,
+ target = data.target.get(0);
+
+
+ // correct page coordinates for touch devices
+ if (touches && touches.length) {
+ event = touches[0];
+ }
+
+ if (!flags.dragged) {
+ toggleThumbs(thumbs, options, true);
+ }
+
+ flags.dragged = true;
+
+ if (options.direction !== 'vertical') {
+ target.scrollLeft -= (event.pageX - position.x);
+ }
+
+ if (data.options.direction !== 'horizontal') {
+ target.scrollTop -= (event.pageY - position.y);
+ }
+
+ capturePosition(event, data.position);
+
+ if (--data.capture.index <= 0) {
+ data.target.data(datakey).dragging = flags.dragging = true;
+ capturePosition(event, data.capture, settings.captureThreshold);
+ }
+
+ moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop);
+
+ }
+
+ // sends the overscrolled element into a drift
+ function drift(target, event, callback) {
+
+ var data = event.data, dx, dy, xMod, yMod,
+ capture = data.capture,
+ options = data.options,
+ sizing = data.sizing,
+ thumbs = data.thumbs,
+ elapsed = time() - capture.time,
+ scrollLeft = target.scrollLeft,
+ scrollTop = target.scrollTop,
+ decay = settings.driftDecay;
+
+ // only drift if enough time has passed since
+ // the last capture event
+ if (elapsed > settings.driftTimeout) {
+ callback(data); return;
+ }
+
+ // determine offset between last capture and current time
+ dx = options.scrollDelta * (event.pageX - capture.x);
+ dy = options.scrollDelta * (event.pageY - capture.y);
+
+ // update target scroll offsets
+ if (options.direction !== 'vertical') {
+ scrollLeft -= dx;
+ } if (options.direction !== 'horizontal') {
+ scrollTop -= dy;
+ }
+
+ // split the distance to travel into a set of sequences
+ xMod = dx / settings.driftSequences;
+ yMod = dy / settings.driftSequences;
+
+ triggerEvent('driftstart', data.target);
+
+ data.drifting = true;
+
+ // animate the drift sequence
+ compat.animate(function render() {
+ if (data.drifting) {
+ var min = 1, max = -1;
+ data.drifting = false;
+ if (yMod > min && target.scrollTop > scrollTop || yMod < max && target.scrollTop < scrollTop) {
+ data.drifting = true;
+ target.scrollTop -= yMod;
+ yMod /= decay;
+ }
+ if (xMod > min && target.scrollLeft > scrollLeft || xMod < max && target.scrollLeft < scrollLeft) {
+ data.drifting = true;
+ target.scrollLeft -= xMod;
+ xMod /= decay;
+ }
+ moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop);
+ compat.animate(render);
+ } else {
+ triggerEvent('driftend', data.target);
+ callback(data);
+ }
+ });
+
+ }
+
+ // starts the drag operation and binds the mouse move handler
+ function start(event) {
+
+ var data = event.data,
+ touches = event.originalEvent.touches,
+ target = data.target,
+ dstart = data.start = $(event.target),
+ flags = data.flags;
+
+ // stop any drifts
+ flags.drifting = false;
+
+ // only start drag if the user has not explictly banned it.
+ if (dstart.size() && !dstart.is(data.options.cancelOn)) {
+
+ // without this the simple "click" event won't be recognized on touch clients
+ if (!touches) { event.preventDefault(); }
+
+ if (!compat.overflowScrolling) {
+ target.css('cursor', compat.cursor.grabbing);
+ target.data(datakey).dragging = flags.dragging = flags.dragged = false;
+
+ // apply the drag listeners to the doc or target
+ if(data.options.dragHold) {
+ $(document).on(events.drag, data, drag);
+ } else {
+ target.on(events.drag, data, drag);
+ }
+ }
+
+ data.position = capturePosition(event, {});
+ data.capture = capturePosition(event, {}, settings.captureThreshold);
+ triggerEvent('dragstart', target);
+
+ }
+
+ }
+
+ // ends the drag operation and unbinds the mouse move handler
+ function stop(event) {
+
+ var data = event.data,
+ target = data.target,
+ options = data.options,
+ flags = data.flags,
+ thumbs = data.thumbs,
+
+ // hides the thumbs after the animation is done
+ done = function () {
+ if (thumbs && !options.hoverThumbs) {
+ toggleThumbs(thumbs, options, false);
+ }
+ };
+
+ // remove drag listeners from doc or target
+ if(options.dragHold) {
+ $(document).unbind(events.drag, drag);
+ } else {
+ target.unbind(events.drag, drag);
+ }
+
+ // only fire events and drift if we started with a
+ // valid position
+ if (data.position) {
+
+ triggerEvent('dragend', target);
+
+ // only drift if a drag passed our threshold
+ if (flags.dragging && !compat.overflowScrolling) {
+ drift(target.get(0), event, done);
+ } else {
+ done();
+ }
+
+ }
+
+ // only if we moved, and the mouse down is the same as
+ // the mouse up target do we defer the event
+ if (flags.dragging && !compat.overflowScrolling && data.start && data.start.is(event.target)) {
+ deferClick(data.start);
+ }
+
+ // clear all internal flags and settings
+ target.data(datakey).dragging =
+ data.start =
+ data.capture =
+ data.position =
+ flags.dragged =
+ flags.dragging = false;
+
+ // set the cursor back to normal
+ target.css('cursor', compat.cursor.grab);
+
+ }
+
+ // Ensures that a full set of options are provided
+ // for the plug-in. Also does some validation
+ function getOptions(options) {
+
+ // fill in missing values with defaults
+ options = $.extend({}, defaults, options);
+
+ // check for inconsistent directional restrictions
+ if (options.direction !== 'multi' && options.direction !== options.wheelDirection) {
+ options.wheelDirection = options.direction;
+ }
+
+ // ensure positive values for deltas
+ options.scrollDelta = math.abs(parseFloat(options.scrollDelta));
+ options.wheelDelta = math.abs(parseFloat(options.wheelDelta));
+
+ // fix values for scroll offset
+ options.scrollLeft = options.scrollLeft === none ? null : math.abs(parseFloat(options.scrollLeft));
+ options.scrollTop = options.scrollTop === none ? null : math.abs(parseFloat(options.scrollTop));
+
+ return options;
+
+ }
+
+ // Returns the sizing information (bounding box) for the
+ // target DOM element
+ function getSizing(target) {
+
+ var $target = $(target),
+ width = $target.width(),
+ height = $target.height(),
+ scrollWidth = width >= target.scrollWidth ? width : target.scrollWidth,
+ scrollHeight = height >= target.scrollHeight ? height : target.scrollHeight,
+ hasScroll = scrollWidth > width || scrollHeight > height;
+
+ return {
+ valid: hasScroll,
+ container: {
+ width: width,
+ height: height,
+ scrollWidth: scrollWidth,
+ scrollHeight: scrollHeight
+ },
+ thumbs: {
+ horizontal: {
+ width: width * width / scrollWidth,
+ height: settings.thumbThickness,
+ corner: settings.thumbThickness / 2,
+ left: 0,
+ top: height - settings.thumbThickness
+ },
+ vertical: {
+ width: settings.thumbThickness,
+ height: height * height / scrollHeight,
+ corner: settings.thumbThickness / 2,
+ left: width - settings.thumbThickness,
+ top: 0
+ }
+ }
+ };
+
+ }
+
+ // Attempts to get (or implicitly creates) the
+ // remover function for the target passed
+ // in as an argument
+ function getRemover(target, orCreate) {
+
+ var $target = $(target), thumbs,
+ data = $target.data(datakey) || {},
+ style = $target.attr('style'),
+ fallback = orCreate ? function () {
+
+ data = $target.data(datakey);
+ thumbs = data.thumbs;
+
+ // restore original styles (if any)
+ if (style) {
+ $target.attr('style', style);
+ } else {
+ $target.removeAttr('style');
+ }
+
+ // remove any created thumbs
+ if (thumbs) {
+ if (thumbs.horizontal) { thumbs.horizontal.remove(); }
+ if (thumbs.vertical) { thumbs.vertical.remove(); }
+ }
+
+ // remove any bound overscroll events and data
+ $target
+ .removeData(datakey)
+ .off(events.wheel, wheel)
+ .off(events.start, start)
+ .off(events.end, stop)
+ .off(events.ignored, ignore);
+
+ } : $.noop;
+
+ return $.isFunction(data.remover) ? data.remover : fallback;
+
+ }
+
+ // Genterates CSS specific to a particular thumb.
+ // It requires sizing data and options
+ function getThumbCss(size, options) {
+ return {
+ position: 'absolute',
+ opacity: options.persistThumbs ? settings.thumbOpacity : 0,
+ 'background-color': 'black',
+ width: size.width + 'px',
+ height: size.height + 'px',
+ 'border-radius': size.corner + 'px',
+ 'margin': size.top + 'px 0 0 ' + size.left + 'px',
+ 'z-index': options.zIndex
+ };
+ }
+
+ // Creates the DOM elements used as "thumbs" within
+ // the target container.
+ function createThumbs(target, sizing, options) {
+
+ var div = '
',
+ thumbs = {},
+ css = false;
+
+ if (sizing.container.scrollWidth > 0 && options.direction !== 'vertical') {
+ css = getThumbCss(sizing.thumbs.horizontal, options);
+ thumbs.horizontal = $(div).css(css).prependTo(target);
+ }
+
+ if (sizing.container.scrollHeight > 0 && options.direction !== 'horizontal') {
+ css = getThumbCss(sizing.thumbs.vertical, options);
+ thumbs.vertical = $(div).css(css).prependTo(target);
+ }
+
+ thumbs.added = !!css;
+
+ return thumbs;
+
+ }
+
+ // ignores events on the overscroll element
+ function ignore(event) {
+ event.preventDefault();
+ }
+
+ // This function takes a jQuery element, some
+ // (optional) options, and sets up event metadata
+ // for each instance the plug-in affects
+ function setup(target, options) {
+
+ // create initial data properties for this instance
+ options = getOptions(options);
+ var sizing = getSizing(target),
+ thumbs, data = {
+ options: options, sizing: sizing,
+ flags: { dragging: false },
+ remover: getRemover(target, true)
+ };
+
+ // only apply handlers if the overscrolled element
+ // actually has an area to scroll
+ if (sizing.valid || options.ignoreSizing) {
+ // provide a circular-reference, enable events, and
+ // apply any required CSS
+ data.target = target = $(target).css({
+ position: 'relative',
+ cursor: compat.cursor.grab
+ }).on(events.start, data, start)
+ .on(events.end, data, stop)
+ .on(events.ignored, data, ignore);
+
+ // apply the stop listeners for drag end
+ if(options.dragHold) {
+ $(document).on(events.end, data, stop);
+ } else {
+ data.target.on(events.end, data, stop);
+ }
+
+ // apply any user-provided scroll offsets
+ if (options.scrollLeft !== null) {
+ target.scrollLeft(options.scrollLeft);
+ } if (options.scrollTop !== null) {
+ target.scrollTop(options.scrollTop);
+ }
+
+ // use native oversroll, if it exists
+ if (compat.overflowScrolling) {
+ target.css(compat.overflowScrolling, 'touch');
+ } else {
+ target.on(events.scroll, data, scroll);
+ }
+
+ // check to see if the user would like mousewheel support
+ if (options.captureWheel) {
+ target.on(events.wheel, data, wheel);
+ }
+
+ // add thumbs and listeners (if we're showing them)
+ if (options.showThumbs) {
+ if (compat.overflowScrolling) {
+ target.css('overflow', 'scroll');
+ } else {
+ target.css('overflow', 'hidden');
+ data.thumbs = thumbs = createThumbs(target, sizing, options);
+ if (thumbs.added) {
+ moveThumbs(thumbs, sizing, target.scrollLeft(), target.scrollTop());
+ if (options.hoverThumbs) {
+ target.on(events.hover, data, hover);
+ }
+ }
+ }
+ } else {
+ target.css('overflow', 'hidden');
+ }
+
+ target.data(datakey, data);
+ }
+
+ }
+
+ // Removes any event listeners and other instance-specific
+ // data from the target. It attempts to leave the target
+ // at the state it found it.
+ function teardown(target) {
+ getRemover(target)();
+ }
+
+ // This is the entry-point for enabling the plug-in;
+ // You can find it's exposure point at the end
+ // of this closure
+ function overscroll(options) {
+ /*jshint validthis:true */
+ return this.removeOverscroll().each(function() {
+ setup(this, options);
+ });
+ }
+
+ // This is the entry-point for disabling the plug-in;
+ // You can find it's exposure point at the end
+ // of this closure
+ function removeOverscroll() {
+ /*jshint validthis:true */
+ return this.each(function () {
+ teardown(this);
+ });
+ }
+
+ // Extend overscroll to expose settings to the user
+ overscroll.settings = settings;
+
+ // Extend jQuery's prototype to expose the plug-in.
+ // If the supports native overflowScrolling, overscroll will not
+ // attempt to override the browser's built in support
+ $.extend(namespace, {
+ overscroll: overscroll,
+ removeOverscroll: removeOverscroll
+ });
+
+})(window, document, navigator, Math, setTimeout, clearTimeout, jQuery.fn, jQuery);
diff --git a/static/partials/image-view.html b/static/partials/image-view.html
index 04dd9aabb..b2ea2d040 100644
--- a/static/partials/image-view.html
+++ b/static/partials/image-view.html
@@ -1,93 +1,89 @@
-
- No image found
-
+
+
+
-
-
+
+
+
+
-
-
-
-
-
-
-
- Full Image ID
-
-
-
-
-
-
Created
-
-
+
+
Created
+
+
-
-
+
+
-
-
-
-
-
-
-
- Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
-
-
-
-
-
-
-
-
- No matching changes
-
-
-
-
-
- {{folder}} / {{getFilename(change.file)}}
-
+
+
+
+
+
+
+
+ Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
+
+
+
+
+
+
+
+
+ No matching changes
+
+
+
+
+
+ {{folder}} / {{getFilename(change.file)}}
+
+
-
-
-
+
+
+
-
-
diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html
index de9927639..3033a20b7 100644
--- a/static/partials/new-organization.html
+++ b/static/partials/new-organization.html
@@ -1,8 +1,8 @@
-