mirror of
https://github.com/sismics/docs.git
synced 2024-11-22 14:07:55 +01:00
Closes #151: upgrade JS libraries
This commit is contained in:
parent
c355cb8bd5
commit
84d4d3b165
@ -43,13 +43,11 @@
|
||||
"grunt-contrib-clean": "^1.0.0",
|
||||
"grunt-contrib-concat": "^1.0.1",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-contrib-less": "^1.3.0",
|
||||
"grunt-contrib-less": "^1.4.1",
|
||||
"grunt-contrib-uglify": "^1.0.1",
|
||||
"grunt-css": "^0.5.4",
|
||||
"grunt-htmlrefs": "^0.5.0",
|
||||
"grunt-ng-annotate": "^2.0.2",
|
||||
"grunt-text-replace": "^0.4.0",
|
||||
"protractor": "^3.3.0",
|
||||
"selenium": "^2.20.0"
|
||||
"grunt-text-replace": "^0.4.0"
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,16 @@
|
||||
*/
|
||||
angular.module('docs',
|
||||
// Dependencies
|
||||
['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog', 'ngProgress', 'monospaced.qrcode', 'yaru22.angular-timeago',
|
||||
'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload', 'pascalprecht.translate']
|
||||
['ui.router', 'ui.bootstrap', 'dialog', 'ngProgress', 'monospaced.qrcode', 'yaru22.angular-timeago', 'ui.validate',
|
||||
'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'ngFileUpload', 'pascalprecht.translate']
|
||||
)
|
||||
|
||||
/**
|
||||
* Configuring modules.
|
||||
*/
|
||||
.config(function($stateProvider, $httpProvider, RestangularProvider, $translateProvider, timeAgoSettings) {
|
||||
.config(function($locationProvider, $urlRouterProvider, $stateProvider, $httpProvider, RestangularProvider, $translateProvider, timeAgoSettings) {
|
||||
$locationProvider.hashPrefix('');
|
||||
|
||||
// Configuring UI Router
|
||||
$stateProvider
|
||||
.state('main', {
|
||||
@ -417,20 +419,6 @@ angular.module('docs',
|
||||
$rootScope.appName = data.name;
|
||||
});
|
||||
})
|
||||
/**
|
||||
* Redirection support for ui-router.
|
||||
* Thanks to https://github.com/acollard
|
||||
* See https://github.com/angular-ui/ui-router/issues/1584#issuecomment-76993045
|
||||
*/
|
||||
.run(function($rootScope, $state){
|
||||
$rootScope.$on('$stateChangeStart', function(event, toState, toParams) {
|
||||
var redirect = toState.redirectTo;
|
||||
if (redirect) {
|
||||
event.preventDefault();
|
||||
$state.go(redirect, toParams);
|
||||
}
|
||||
});
|
||||
})
|
||||
/**
|
||||
* Initialize ngProgress.
|
||||
*/
|
||||
|
@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Document controller.
|
||||
*/
|
||||
angular.module('docs').controller('Document', function($scope, $timeout, $state, Restangular) {
|
||||
angular.module('docs').controller('Document', function ($scope, $rootScope, $timeout, $state, Restangular) {
|
||||
/**
|
||||
* Documents table sort status.
|
||||
*/
|
||||
@ -13,7 +13,7 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
$scope.currentPage = 1;
|
||||
$scope.limit = _.isUndefined(localStorage.documentsPageSize) ? 10 : localStorage.documentsPageSize;
|
||||
$scope.search = $state.params.search ? $state.params.search : '';
|
||||
$scope.setSearch = function(search) { $scope.search = search };
|
||||
$scope.setSearch = function (search) { $scope.search = search };
|
||||
|
||||
// A timeout promise is used to slow down search requests to the server
|
||||
// We keep track of it for cancellation purpose
|
||||
@ -22,9 +22,9 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
/**
|
||||
* Load new documents page.
|
||||
*/
|
||||
$scope.pageDocuments = function() {
|
||||
Restangular.one('document')
|
||||
.getList('list', {
|
||||
$scope.pageDocuments = function () {
|
||||
Restangular.one('document/list')
|
||||
.get({
|
||||
offset: $scope.offset,
|
||||
limit: $scope.limit,
|
||||
sort_column: $scope.sortColumn,
|
||||
@ -40,7 +40,7 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
/**
|
||||
* Reload documents.
|
||||
*/
|
||||
$scope.loadDocuments = function() {
|
||||
$scope.loadDocuments = function () {
|
||||
$scope.offset = 0;
|
||||
$scope.currentPage = 1;
|
||||
$scope.pageDocuments();
|
||||
@ -49,8 +49,8 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
/**
|
||||
* Watch for current page change.
|
||||
*/
|
||||
$scope.$watch('currentPage', function(prev, next) {
|
||||
if (prev == next) {
|
||||
$scope.$watch('currentPage', function (prev, next) {
|
||||
if (prev === next) {
|
||||
return;
|
||||
}
|
||||
$scope.offset = ($scope.currentPage - 1) * $scope.limit;
|
||||
@ -60,15 +60,15 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
/**
|
||||
* Watch for search scope change.
|
||||
*/
|
||||
$scope.$watch('search', function() {
|
||||
$scope.$watch('search', function () {
|
||||
if (timeoutPromise) {
|
||||
// Cancel previous timeout
|
||||
$timeout.cancel(timeoutPromise);
|
||||
}
|
||||
|
||||
if ($state.current.name == 'document.default'
|
||||
|| $state.current.name == 'document.default.search') {
|
||||
$state.go($scope.search == '' ?
|
||||
if ($state.current.name === 'document.default'
|
||||
|| $state.current.name === 'document.default.search') {
|
||||
$state.go($scope.search === '' ?
|
||||
'document.default' : 'document.default.search', {
|
||||
search: $scope.search
|
||||
}, {
|
||||
@ -86,8 +86,8 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
/**
|
||||
* Sort documents.
|
||||
*/
|
||||
$scope.sortDocuments = function(sortColumn) {
|
||||
if (sortColumn == $scope.sortColumn) {
|
||||
$scope.sortDocuments = function (sortColumn) {
|
||||
if (sortColumn === $scope.sortColumn) {
|
||||
$scope.asc = !$scope.asc;
|
||||
} else {
|
||||
$scope.asc = true;
|
||||
@ -99,9 +99,9 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
/**
|
||||
* Watch for page size change.
|
||||
*/
|
||||
$scope.$watch('limit', function(next, prev) {
|
||||
$scope.$watch('limit', function (next, prev) {
|
||||
localStorage.documentsPageSize = next;
|
||||
if (next == prev) {
|
||||
if (next === prev) {
|
||||
return;
|
||||
}
|
||||
$scope.loadDocuments();
|
||||
@ -110,13 +110,13 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
/**
|
||||
* Display a document.
|
||||
*/
|
||||
$scope.viewDocument = function(id) {
|
||||
$scope.viewDocument = function (id) {
|
||||
$state.go('document.view', { id: id });
|
||||
};
|
||||
|
||||
// Load tags
|
||||
var tags = [];
|
||||
Restangular.one('tag/list').getList().then(function(data) {
|
||||
Restangular.one('tag/list').get().then(function (data) {
|
||||
tags = data.tags;
|
||||
});
|
||||
|
||||
@ -126,7 +126,16 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
|
||||
*/
|
||||
$scope.getChildrenTags = function(parent) {
|
||||
return _.filter(tags, function(tag) {
|
||||
return tag.parent == parent;
|
||||
return tag.parent === parent;
|
||||
});
|
||||
};
|
||||
|
||||
// Hack to reload the pagination directive after language change
|
||||
$scope.paginationShown = true;
|
||||
$rootScope.$on('$translateChangeSuccess', function () {
|
||||
$scope.paginationShown = false;
|
||||
$timeout(function () {
|
||||
$scope.paginationShown = true;
|
||||
});
|
||||
})
|
||||
});
|
@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Document default controller.
|
||||
*/
|
||||
angular.module('docs').controller('DocumentDefault', function($scope, $rootScope, $state, Restangular, $upload, $translate) {
|
||||
angular.module('docs').controller('DocumentDefault', function($scope, $rootScope, $state, Restangular, Upload, $translate) {
|
||||
// Load user audit log
|
||||
Restangular.one('auditlog').get().then(function(data) {
|
||||
$scope.logs = data.logs;
|
||||
@ -13,7 +13,7 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
|
||||
* Load unlinked files.
|
||||
*/
|
||||
$scope.loadFiles = function() {
|
||||
Restangular.one('file').getList('list').then(function (data) {
|
||||
Restangular.one('file/list').get().then(function (data) {
|
||||
$scope.files = data.files;
|
||||
// TODO Keep currently uploading files
|
||||
});
|
||||
@ -59,7 +59,7 @@ angular.module('docs').controller('DocumentDefault', function($scope, $rootScope
|
||||
$scope.uploadFile = function(file, newfile) {
|
||||
// Upload the file
|
||||
newfile.status = $translate.instant('document.default.upload_progress');
|
||||
return $upload.upload({
|
||||
return Upload.upload({
|
||||
method: 'PUT',
|
||||
url: '../api/file',
|
||||
file: file
|
||||
|
@ -11,7 +11,7 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
|
||||
$scope.vocabularies = [];
|
||||
|
||||
// Orphan files to add
|
||||
$scope.orphanFiles = $stateParams.files ? $stateParams.files.split(',') : [];
|
||||
$scope.orphanFiles = $stateParams.files ? $stateParams.files : [];
|
||||
|
||||
/**
|
||||
* Close an alert.
|
||||
@ -25,8 +25,8 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
|
||||
*/
|
||||
$scope.getTitleTypeahead = function($viewValue) {
|
||||
var deferred = $q.defer();
|
||||
Restangular.one('document')
|
||||
.getList('list', {
|
||||
Restangular.one('document/list')
|
||||
.get({
|
||||
limit: 5,
|
||||
sort_column: 1,
|
||||
asc: true,
|
||||
|
@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Document modal PDF controller.
|
||||
*/
|
||||
angular.module('docs').controller('DocumentModalPdf', function ($scope, $window, $stateParams, $modalInstance) {
|
||||
angular.module('docs').controller('DocumentModalPdf', function ($scope, $window, $stateParams, $uibModalInstance) {
|
||||
$scope.export = {
|
||||
metadata: false,
|
||||
comments: false,
|
||||
@ -19,11 +19,11 @@ angular.module('docs').controller('DocumentModalPdf', function ($scope, $window,
|
||||
+ '&fitimagetopage=' + $scope.export.fitimagetopage
|
||||
+ '&margin=' + $scope.export.margin);
|
||||
|
||||
$modalInstance.close();
|
||||
$uibModalInstance.close();
|
||||
};
|
||||
|
||||
// Close the modal
|
||||
$scope.close = function () {
|
||||
$modalInstance.close();
|
||||
$uibModalInstance.close();
|
||||
}
|
||||
});
|
@ -3,9 +3,9 @@
|
||||
/**
|
||||
* Document modal share controller.
|
||||
*/
|
||||
angular.module('docs').controller('DocumentModalShare', function ($scope, $modalInstance) {
|
||||
angular.module('docs').controller('DocumentModalShare', function ($scope, $uibModalInstance) {
|
||||
$scope.name = '';
|
||||
$scope.close = function(name) {
|
||||
$modalInstance.close(name);
|
||||
$uibModalInstance.close(name);
|
||||
}
|
||||
});
|
@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Document view controller.
|
||||
*/
|
||||
angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $modal, Restangular, $translate) {
|
||||
angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $uibModal, Restangular, $translate) {
|
||||
// Load document data from server
|
||||
Restangular.one('document', $stateParams.id).get().then(function(data) {
|
||||
$scope.document = data;
|
||||
@ -81,7 +81,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
|
||||
* Open the share dialog.
|
||||
*/
|
||||
$scope.share = function () {
|
||||
$modal.open({
|
||||
$uibModal.open({
|
||||
templateUrl: 'partial/docs/document.share.html',
|
||||
controller: 'DocumentModalShare'
|
||||
}).result.then(function (name) {
|
||||
@ -131,7 +131,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
|
||||
* Export the current document to PDF.
|
||||
*/
|
||||
$scope.exportPdf = function() {
|
||||
$modal.open({
|
||||
$uibModal.open({
|
||||
templateUrl: 'partial/docs/document.pdf.html',
|
||||
controller: 'DocumentModalPdf'
|
||||
});
|
||||
|
@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Document view content controller.
|
||||
*/
|
||||
angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, $upload, $translate) {
|
||||
angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, Upload, $translate) {
|
||||
/**
|
||||
* Configuration for file sorting.
|
||||
*/
|
||||
@ -27,7 +27,7 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
|
||||
* Load files from server.
|
||||
*/
|
||||
$scope.loadFiles = function () {
|
||||
Restangular.one('file').getList('list', { id: $stateParams.id }).then(function (data) {
|
||||
Restangular.one('file/list').get({ id: $stateParams.id }).then(function (data) {
|
||||
$scope.files = data.files;
|
||||
// TODO Keep currently uploading files
|
||||
});
|
||||
@ -53,7 +53,7 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
|
||||
];
|
||||
|
||||
$dialog.messageBox(title, msg, btns, function (result) {
|
||||
if (result == 'ok') {
|
||||
if (result === 'ok') {
|
||||
Restangular.one('file', file.id).remove().then(function () {
|
||||
// File deleted, decrease used quota
|
||||
$rootScope.userInfo.storage_current -= file.size;
|
||||
@ -105,7 +105,7 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
|
||||
$scope.uploadFile = function(file, newfile) {
|
||||
// Upload the file
|
||||
newfile.status = $translate.instant('document.view.content.upload_progress');
|
||||
return $upload.upload({
|
||||
return Upload.upload({
|
||||
method: 'PUT',
|
||||
url: '../api/file',
|
||||
file: file,
|
||||
@ -126,7 +126,7 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
|
||||
})
|
||||
.error(function (data) {
|
||||
newfile.status = $translate.instant('document.view.content.upload_error');
|
||||
if (data.type == 'QuotaReached') {
|
||||
if (data.type === 'QuotaReached') {
|
||||
newfile.status += ' - ' + $translate.instant('document.view.content.upload_error_quota');
|
||||
}
|
||||
});
|
||||
|
@ -3,14 +3,14 @@
|
||||
/**
|
||||
* File modal view controller.
|
||||
*/
|
||||
angular.module('docs').controller('FileModalView', function($rootScope, $modalInstance, $scope, $state, $stateParams, Restangular) {
|
||||
angular.module('docs').controller('FileModalView', function($uibModalInstance, $scope, $state, $stateParams, Restangular, $transitions) {
|
||||
// Load files
|
||||
Restangular.one('file').getList('list', { id: $stateParams.id }).then(function(data) {
|
||||
Restangular.one('file/list').get({ id: $stateParams.id }).then(function(data) {
|
||||
$scope.files = data.files;
|
||||
|
||||
// Search current file
|
||||
_.each($scope.files, function(value) {
|
||||
if (value.id == $stateParams.fileId) {
|
||||
if (value.id === $stateParams.fileId) {
|
||||
$scope.file = value;
|
||||
}
|
||||
});
|
||||
@ -21,7 +21,7 @@ angular.module('docs').controller('FileModalView', function($rootScope, $modalIn
|
||||
*/
|
||||
$scope.nextFile = function() {
|
||||
_.each($scope.files, function(value, key) {
|
||||
if (value.id == $stateParams.fileId) {
|
||||
if (value.id === $stateParams.fileId) {
|
||||
var next = $scope.files[key + 1];
|
||||
if (next) {
|
||||
$state.go('^.file', { id: $stateParams.id, fileId: next.id });
|
||||
@ -35,7 +35,7 @@ angular.module('docs').controller('FileModalView', function($rootScope, $modalIn
|
||||
*/
|
||||
$scope.previousFile = function() {
|
||||
_.each($scope.files, function(value, key) {
|
||||
if (value.id == $stateParams.fileId) {
|
||||
if (value.id === $stateParams.fileId) {
|
||||
var previous = $scope.files[key - 1];
|
||||
if (previous) {
|
||||
$state.go('^.file', { id: $stateParams.id, fileId: previous.id });
|
||||
@ -66,16 +66,16 @@ angular.module('docs').controller('FileModalView', function($rootScope, $modalIn
|
||||
* Close the file preview.
|
||||
*/
|
||||
$scope.closeFile = function () {
|
||||
$modalInstance.dismiss();
|
||||
$uibModalInstance.dismiss();
|
||||
};
|
||||
|
||||
// Close the modal when the user exits this state
|
||||
var off = $rootScope.$on('$stateChangeStart', function(event, toState) {
|
||||
if (!$modalInstance.closed) {
|
||||
if (toState.name == $state.current.name) {
|
||||
$modalInstance.close();
|
||||
var off = $transitions.onStart({}, function(transition) {
|
||||
if (!$uibModalInstance.closed) {
|
||||
if (transition.to().name === $state.current.name) {
|
||||
$uibModalInstance.close();
|
||||
} else {
|
||||
$modalInstance.dismiss();
|
||||
$uibModalInstance.dismiss();
|
||||
}
|
||||
}
|
||||
off();
|
||||
|
@ -3,8 +3,8 @@
|
||||
/**
|
||||
* File view controller.
|
||||
*/
|
||||
angular.module('docs').controller('FileView', function($modal, $state, $stateParams) {
|
||||
var modal = $modal.open({
|
||||
angular.module('docs').controller('FileView', function($uibModal, $state, $stateParams) {
|
||||
var modal = $uibModal.open({
|
||||
windowClass: 'modal modal-fileview',
|
||||
templateUrl: 'partial/docs/file.view.html',
|
||||
controller: 'FileModalView'
|
||||
|
@ -26,7 +26,7 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope,
|
||||
|
||||
// Update the theme
|
||||
$scope.update = function() {
|
||||
$scope.theme.name = $scope.theme.name.length == 0 ? 'Sismics Docs' : $scope.theme.name;
|
||||
$scope.theme.name = $scope.theme.name.length === 0 ? 'Sismics Docs' : $scope.theme.name;
|
||||
Restangular.one('theme').post('', $scope.theme).then(function() {
|
||||
var stylesheet = $('#theme-stylesheet')[0];
|
||||
stylesheet.href = stylesheet.href.replace(/\?.*|$/, '?' + new Date().getTime());
|
||||
@ -39,7 +39,7 @@ angular.module('docs').controller('SettingsConfig', function($scope, $rootScope,
|
||||
$scope.sendImage = function(type, image) {
|
||||
// Build the payload
|
||||
var formData = new FormData();
|
||||
formData.append('image', image[0]);
|
||||
formData.append('image', image);
|
||||
|
||||
// Send the file
|
||||
var done = function() {
|
||||
|
@ -60,7 +60,7 @@ angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog,
|
||||
];
|
||||
|
||||
$dialog.messageBox(title, msg, btns, function(result) {
|
||||
if (result == 'ok') {
|
||||
if (result === 'ok') {
|
||||
Restangular.one('group', $stateParams.name).remove().then(function() {
|
||||
$scope.loadGroups();
|
||||
$state.go('settings.group');
|
||||
@ -77,7 +77,7 @@ angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog,
|
||||
$scope.getGroupTypeahead = function($viewValue) {
|
||||
var deferred = $q.defer();
|
||||
Restangular.one('group')
|
||||
.getList('', {
|
||||
.get({
|
||||
sort_column: 1,
|
||||
asc: true
|
||||
}).then(function(data) {
|
||||
@ -93,8 +93,8 @@ angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog,
|
||||
*/
|
||||
$scope.getUserTypeahead = function($viewValue) {
|
||||
var deferred = $q.defer();
|
||||
Restangular.one('user')
|
||||
.getList('list', {
|
||||
Restangular.one('user/list')
|
||||
.get({
|
||||
search: $viewValue,
|
||||
sort_column: 1,
|
||||
asc: true
|
||||
|
@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Settings security controller.
|
||||
*/
|
||||
angular.module('docs').controller('SettingsSecurity', function($scope, User, $dialog, $modal, Restangular, $translate) {
|
||||
angular.module('docs').controller('SettingsSecurity', function($scope, User, $dialog, $uibModal, Restangular, $translate) {
|
||||
User.userInfo().then(function(data) {
|
||||
$scope.user = data;
|
||||
});
|
||||
@ -20,7 +20,7 @@ angular.module('docs').controller('SettingsSecurity', function($scope, User, $di
|
||||
];
|
||||
|
||||
$dialog.messageBox(title, msg, btns, function(result) {
|
||||
if (result == 'ok') {
|
||||
if (result === 'ok') {
|
||||
Restangular.one('user/enable_totp').post().then(function(data) {
|
||||
$scope.secret = data.secret;
|
||||
User.userInfo(true).then(function(data) {
|
||||
@ -35,11 +35,11 @@ angular.module('docs').controller('SettingsSecurity', function($scope, User, $di
|
||||
* Disable TOTP.
|
||||
*/
|
||||
$scope.disableTotp = function() {
|
||||
$modal.open({
|
||||
$uibModal.open({
|
||||
templateUrl: 'partial/docs/settings.security.disabletotp.html',
|
||||
controller: 'SettingsSecurityModalDisableTotp'
|
||||
}).result.then(function (password) {
|
||||
if (password == null) {
|
||||
if (password === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,9 @@
|
||||
/**
|
||||
* Settings modal disable TOTP controller.
|
||||
*/
|
||||
angular.module('docs').controller('SettingsSecurityModalDisableTotp', function ($scope, $modalInstance) {
|
||||
angular.module('docs').controller('SettingsSecurityModalDisableTotp', function ($scope, $uibModalInstance) {
|
||||
$scope.password = '';
|
||||
$scope.close = function(password) {
|
||||
$modalInstance.close(password);
|
||||
$uibModalInstance.close(password);
|
||||
}
|
||||
});
|
@ -8,7 +8,7 @@ angular.module('docs').controller('SettingsSession', function($scope, Restangula
|
||||
* Load sessions.
|
||||
*/
|
||||
$scope.loadSession = function() {
|
||||
Restangular.one('user').getList('session').then(function(data) {
|
||||
Restangular.one('user/session').get().then(function(data) {
|
||||
$scope.sessions = data.sessions;
|
||||
});
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ angular.module('docs').controller('TagEdit', function($scope, $stateParams, Rest
|
||||
|
||||
// Replace the tag from the list with this reference
|
||||
_.each($scope.tags, function(tag, i) {
|
||||
if (tag.id == $scope.tag.id) {
|
||||
if (tag.id === $scope.tag.id) {
|
||||
$scope.tags[i] = $scope.tag;
|
||||
}
|
||||
});
|
||||
@ -36,7 +36,7 @@ angular.module('docs').controller('TagEdit', function($scope, $stateParams, Rest
|
||||
];
|
||||
|
||||
$dialog.messageBox(title, msg, btns, function(result) {
|
||||
if (result == 'ok') {
|
||||
if (result === 'ok') {
|
||||
Restangular.one('tag', tag.id).remove().then(function() {
|
||||
$scope.loadTags();
|
||||
$state.go('tag.default');
|
||||
|
@ -6,7 +6,7 @@
|
||||
angular.module('docs').directive('acl', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<span ng-if="data.type"><em>{{ \'acl.\' + data.type | translate }}</em> {{ data.name }}</span>',
|
||||
template: '<span ng-show="data.type"><em>{{ \'acl.\' + data.type | translate }}</em> {{ data.name }}</span>',
|
||||
replace: true,
|
||||
scope: {
|
||||
data: '='
|
||||
|
@ -43,7 +43,7 @@ angular.module('docs').directive('aclEdit', function() {
|
||||
// Compute ACLs to add
|
||||
$scope.acl.source = $scope.source;
|
||||
var acls = [];
|
||||
if ($scope.acl.perm == 'READWRITE') {
|
||||
if ($scope.acl.perm === 'READWRITE') {
|
||||
acls = [{
|
||||
source: $scope.source,
|
||||
target: $scope.acl.target.name,
|
||||
|
@ -8,7 +8,7 @@ angular.module('docs').directive('imgError', function() {
|
||||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
element.bind('error', function() {
|
||||
//call the function that was passed
|
||||
// call the function that was passed
|
||||
scope.$apply(attrs.imgError);
|
||||
});
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ angular.module('docs').directive('selectRelation', function() {
|
||||
$scope.addRelation = function($item) {
|
||||
// Does the new relation is already in the model
|
||||
var duplicate = _.find($scope.relations, function(relation) {
|
||||
if ($item.id == relation.id) {
|
||||
if ($item.id === relation.id) {
|
||||
return relation;
|
||||
}
|
||||
});
|
||||
@ -41,7 +41,7 @@ angular.module('docs').directive('selectRelation', function() {
|
||||
*/
|
||||
$scope.deleteRelation = function(deleteRelation) {
|
||||
$scope.relations = _.reject($scope.relations, function(relation) {
|
||||
return relation.id == deleteRelation.id;
|
||||
return relation.id === deleteRelation.id;
|
||||
})
|
||||
};
|
||||
|
||||
@ -50,8 +50,8 @@ angular.module('docs').directive('selectRelation', function() {
|
||||
*/
|
||||
$scope.getDocumentTypeahead = function($viewValue) {
|
||||
var deferred = $q.defer();
|
||||
Restangular.one('document')
|
||||
.getList('list', {
|
||||
Restangular.one('document/list')
|
||||
.get({
|
||||
limit: 5,
|
||||
sort_column: 1,
|
||||
asc: true,
|
||||
|
@ -25,14 +25,14 @@ angular.module('docs').directive('selectTag', function() {
|
||||
$scope.addTag = function($event) {
|
||||
// Does the new tag exists
|
||||
var tag = _.find($scope.allTags, function(tag) {
|
||||
if (tag.name == $scope.input) {
|
||||
if (tag.name === $scope.input) {
|
||||
return tag;
|
||||
}
|
||||
});
|
||||
|
||||
// Does the new tag is already in the model
|
||||
var duplicate = _.find($scope.tags, function(tag2) {
|
||||
if (tag && tag2.id == tag.id) {
|
||||
if (tag && tag2.id === tag.id) {
|
||||
return tag2;
|
||||
}
|
||||
});
|
||||
@ -55,7 +55,7 @@ angular.module('docs').directive('selectTag', function() {
|
||||
*/
|
||||
$scope.deleteTag = function(deleteTag) {
|
||||
$scope.tags = _.reject($scope.tags, function(tag) {
|
||||
return tag.id == deleteTag.id;
|
||||
return tag.id === deleteTag.id;
|
||||
})
|
||||
};
|
||||
},
|
||||
|
@ -12,7 +12,7 @@ angular.module('docs').factory('User', function(Restangular) {
|
||||
* @param force If true, force reloading data
|
||||
*/
|
||||
userInfo: function(force) {
|
||||
if (userInfo == null || force) {
|
||||
if (userInfo === null || force) {
|
||||
userInfo = Restangular.one('user').get();
|
||||
}
|
||||
return userInfo;
|
||||
|
@ -11,7 +11,9 @@ angular.module('share',
|
||||
/**
|
||||
* Configuring modules.
|
||||
*/
|
||||
.config(function($stateProvider, $httpProvider, RestangularProvider) {
|
||||
.config(function($locationProvider, $stateProvider, $httpProvider, RestangularProvider) {
|
||||
$locationProvider.hashPrefix('');
|
||||
|
||||
// Configuring UI Router
|
||||
$stateProvider
|
||||
.state('main', {
|
||||
|
@ -3,14 +3,14 @@
|
||||
/**
|
||||
* File modal view controller.
|
||||
*/
|
||||
angular.module('share').controller('FileModalView', function($rootScope, $modalInstance, $scope, $state, $stateParams, Restangular) {
|
||||
angular.module('share').controller('FileModalView', function($uibModalInstance, $scope, $state, $stateParams, Restangular, $transitions) {
|
||||
// Load files
|
||||
Restangular.one('file').getList('list', { id: $stateParams.documentId, share: $stateParams.shareId }).then(function(data) {
|
||||
Restangular.one('file/list').get({ id: $stateParams.documentId, share: $stateParams.shareId }).then(function(data) {
|
||||
$scope.files = data.files;
|
||||
|
||||
// Search current file
|
||||
_.each($scope.files, function(value) {
|
||||
if (value.id == $stateParams.fileId) {
|
||||
if (value.id === $stateParams.fileId) {
|
||||
$scope.file = value;
|
||||
}
|
||||
});
|
||||
@ -21,7 +21,7 @@ angular.module('share').controller('FileModalView', function($rootScope, $modalI
|
||||
*/
|
||||
$scope.nextFile = function() {
|
||||
_.each($scope.files, function(value, key) {
|
||||
if (value.id == $stateParams.fileId) {
|
||||
if (value.id === $stateParams.fileId) {
|
||||
var next = $scope.files[key + 1];
|
||||
if (next) {
|
||||
$state.go('share.file', { documentId: $stateParams.documentId, shareId: $stateParams.shareId, fileId: next.id });
|
||||
@ -35,7 +35,7 @@ angular.module('share').controller('FileModalView', function($rootScope, $modalI
|
||||
*/
|
||||
$scope.previousFile = function() {
|
||||
_.each($scope.files, function(value, key) {
|
||||
if (value.id == $stateParams.fileId) {
|
||||
if (value.id === $stateParams.fileId) {
|
||||
var previous = $scope.files[key - 1];
|
||||
if (previous) {
|
||||
$state.go('share.file', { documentId: $stateParams.documentId, shareId: $stateParams.shareId, fileId: previous.id });
|
||||
@ -66,16 +66,16 @@ angular.module('share').controller('FileModalView', function($rootScope, $modalI
|
||||
* Close the file preview.
|
||||
*/
|
||||
$scope.closeFile = function () {
|
||||
$modalInstance.dismiss();
|
||||
$uibModalInstance.dismiss();
|
||||
};
|
||||
|
||||
// Close the modal when the user exits this state
|
||||
var off = $rootScope.$on('$stateChangeStart', function(event, toState){
|
||||
if (!$modalInstance.closed) {
|
||||
if (toState.name == 'share.file') {
|
||||
$modalInstance.close();
|
||||
var off = $transitions.onStart({}, function(transition) {
|
||||
if (!$uibModalInstance.closed) {
|
||||
if (transition.to().name === $state.current.name) {
|
||||
$uibModalInstance.close();
|
||||
} else {
|
||||
$modalInstance.dismiss();
|
||||
$uibModalInstance.dismiss();
|
||||
}
|
||||
}
|
||||
off();
|
||||
|
@ -3,8 +3,8 @@
|
||||
/**
|
||||
* File view controller.
|
||||
*/
|
||||
angular.module('share').controller('FileView', function($modal, $state, $stateParams) {
|
||||
var modal = $modal.open({
|
||||
angular.module('share').controller('FileView', function($uibModal, $state, $stateParams) {
|
||||
var modal = $uibModal.open({
|
||||
windowClass: 'modal modal-fileview',
|
||||
templateUrl: 'partial/share/file.view.html',
|
||||
controller: 'FileModalView'
|
||||
@ -14,7 +14,7 @@ angular.module('share').controller('FileView', function($modal, $state, $statePa
|
||||
modal.closed = false;
|
||||
modal.result.then(function() {
|
||||
modal.closed = true;
|
||||
},function(result) {
|
||||
},function() {
|
||||
modal.closed = true;
|
||||
$state.go('share', { documentId: $stateParams.documentId, shareId: $stateParams.shareId });
|
||||
});
|
||||
|
@ -3,19 +3,19 @@
|
||||
/**
|
||||
* Share controller.
|
||||
*/
|
||||
angular.module('share').controller('Share', function($scope, $state, $stateParams, Restangular, $modal) {
|
||||
angular.module('share').controller('Share', function($scope, $state, $stateParams, Restangular, $uibModal) {
|
||||
// Load document
|
||||
Restangular.one('document', $stateParams.documentId).get({ share: $stateParams.shareId })
|
||||
.then(function (data) {
|
||||
$scope.document = data;
|
||||
}, function (response) {
|
||||
if (response.status == 403) {
|
||||
if (response.status === 403) {
|
||||
$state.go('403');
|
||||
}
|
||||
});
|
||||
|
||||
// Load files
|
||||
Restangular.one('file').getList('list', { id: $stateParams.documentId, share: $stateParams.shareId })
|
||||
Restangular.one('file/list').get({ id: $stateParams.documentId, share: $stateParams.shareId })
|
||||
.then(function (data) {
|
||||
$scope.files = data.files;
|
||||
});
|
||||
@ -38,7 +38,7 @@ angular.module('share').controller('Share', function($scope, $state, $stateParam
|
||||
* Export the current document to PDF.
|
||||
*/
|
||||
$scope.exportPdf = function() {
|
||||
$modal.open({
|
||||
$uibModal.open({
|
||||
templateUrl: 'partial/share/share.pdf.html',
|
||||
controller: 'ShareModalPdf'
|
||||
});
|
||||
|
@ -3,7 +3,7 @@
|
||||
/**
|
||||
* Document modal PDF controller.
|
||||
*/
|
||||
angular.module('share').controller('ShareModalPdf', function ($scope, $window, $stateParams, $modalInstance) {
|
||||
angular.module('share').controller('ShareModalPdf', function ($scope, $window, $stateParams, $uibModalInstance) {
|
||||
$scope.export = {
|
||||
metadata: false,
|
||||
comments: false,
|
||||
@ -20,11 +20,11 @@ angular.module('share').controller('ShareModalPdf', function ($scope, $window, $
|
||||
+ '&margin=' + $scope.export.margin
|
||||
+ '&share=' + $stateParams.shareId);
|
||||
|
||||
$modalInstance.close();
|
||||
$uibModalInstance.close();
|
||||
};
|
||||
|
||||
// Close the modal
|
||||
$scope.close = function () {
|
||||
$modalInstance.close();
|
||||
$uibModalInstance.close();
|
||||
}
|
||||
});
|
@ -36,8 +36,8 @@
|
||||
<script src="lib/angular.touch.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-router.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-bootstrap.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-utils.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-sortable.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-validate.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.restangular.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.colorpicker.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.file-upload.js" type="text/javascript"></script>
|
||||
@ -89,7 +89,7 @@
|
||||
<script src="app/docs/directive/AclEdit.js" type="text/javascript"></script>
|
||||
<!-- endref -->
|
||||
</head>
|
||||
<body translate-cloak>
|
||||
<body translate-cloak ng-cloak>
|
||||
<nav class="navbar navbar-inverse" role="navigation" ng-controller="Navigation">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle"
|
||||
@ -110,13 +110,13 @@
|
||||
|
||||
<div class="collapse navbar-collapse" collapse="isCollapsed">
|
||||
<ul class="nav navbar-nav" ng-show="!userInfo.anonymous">
|
||||
<li ng-class="{active: $uiRoute}" ui-route="/document.*">
|
||||
<li ui-sref-active="{ active: 'document.**' }">
|
||||
<a href="#/document"><span class="glyphicon glyphicon-book"></span> {{ 'index.nav_documents' | translate }}</a>
|
||||
</li>
|
||||
<li ng-class="{active: $uiRoute}" ui-route="/tag.*">
|
||||
<li ui-sref-active="{ active: 'tag.**' }">
|
||||
<a href="#/tag"><span class="glyphicon glyphicon-tags"></span> {{ 'index.nav_tags' | translate }}</a>
|
||||
</li>
|
||||
<li ng-class="{active: $uiRoute}" ui-route="/user.*|/group.*">
|
||||
<li ui-sref-active="{ active: 'user.**', active2: 'group.**' }">
|
||||
<a href="#/user"><span class="glyphicon glyphicon-user"></span> {{ 'index.nav_users_groups' | translate }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
@ -124,17 +124,18 @@
|
||||
<ul class="nav navbar-nav navbar-right" ng-show="!userInfo.anonymous">
|
||||
<li ng-show="errorNumber > 0">
|
||||
<a href="#/settings/log" ng-click="openLogs()" class="nav-text-error">
|
||||
<span class="glyphicon glyphicon-warning-sign"></span> {{ 'index.error_info' | translate: '{ count: errorNumber }' }}
|
||||
<span class="glyphicon glyphicon-warning-sign"></span>
|
||||
<span translate="index.error_info" translate-values="{ count: errorNumber }"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ userInfo.username == 'guest' ? '#/user/guest' : '#/settings/account' }}"
|
||||
title="{{ 'index.logged_as' | translate: '{ username: userInfo.username }' }}">
|
||||
translate-attr="{ title: 'index.logged_as' }" translate-values="{ username: userInfo.username }">
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
{{ userInfo.username }}
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: $uiRoute}" ui-route="/settings.*" ng-show="userInfo.username != 'guest'">
|
||||
<li ui-sref-active="{ active: 'settings.**' }" ng-show="userInfo.username != 'guest'">
|
||||
<a href="#/settings/account">
|
||||
<span class="glyphicon glyphicon-cog"></span> {{ 'index.nav_settings' | translate }}
|
||||
</a>
|
||||
@ -154,8 +155,8 @@
|
||||
<div class="row" ng-controller="Footer">
|
||||
<div class="col-md-12 footer text-center text-muted">
|
||||
<ul class="list-inline">
|
||||
<li dropdown class="dropdown">
|
||||
<a href dropdown-toggle>
|
||||
<li uib-dropdown class="dropdown">
|
||||
<a href uib-dropdown-toggle>
|
||||
<span ng-switch="currentLang">
|
||||
<span ng-switch-when="en">English</span>
|
||||
<span ng-switch-when="fr">Français</span>
|
||||
|
File diff suppressed because it is too large
Load Diff
33005
docs-web/src/main/webapp/src/lib/angular.js
vendored
33005
docs-web/src/main/webapp/src/lib/angular.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -1,13 +1,806 @@
|
||||
/*
|
||||
AngularJS v1.2.8
|
||||
(c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
/**
|
||||
* @license AngularJS v1.6.6
|
||||
* (c) 2010-2017 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(p,h,q){'use strict';function E(a){var e=[];s(e,h.noop).chars(a);return e.join("")}function k(a){var e={};a=a.split(",");var d;for(d=0;d<a.length;d++)e[a[d]]=!0;return e}function F(a,e){function d(a,b,d,g){b=h.lowercase(b);if(t[b])for(;f.last()&&u[f.last()];)c("",f.last());v[b]&&f.last()==b&&c("",b);(g=w[b]||!!g)||f.push(b);var l={};d.replace(G,function(a,b,e,c,d){l[b]=r(e||c||d||"")});e.start&&e.start(b,l,g)}function c(a,b){var c=0,d;if(b=h.lowercase(b))for(c=f.length-1;0<=c&&f[c]!=b;c--);
|
||||
if(0<=c){for(d=f.length-1;d>=c;d--)e.end&&e.end(f[d]);f.length=c}}var b,g,f=[],l=a;for(f.last=function(){return f[f.length-1]};a;){g=!0;if(f.last()&&x[f.last()])a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(b,a){a=a.replace(H,"$1").replace(I,"$1");e.chars&&e.chars(r(a));return""}),c("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(e.comment&&e.comment(a.substring(4,b)),a=a.substring(b+3),g=!1);else if(y.test(a)){if(b=a.match(y))a=
|
||||
a.replace(b[0],""),g=!1}else if(J.test(a)){if(b=a.match(z))a=a.substring(b[0].length),b[0].replace(z,c),g=!1}else K.test(a)&&(b=a.match(A))&&(a=a.substring(b[0].length),b[0].replace(A,d),g=!1);g&&(b=a.indexOf("<"),g=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),e.chars&&e.chars(r(g)))}if(a==l)throw L("badparse",a);l=a}c()}function r(a){if(!a)return"";var e=M.exec(a);a=e[1];var d=e[3];if(e=e[2])n.innerHTML=e.replace(/</g,"<"),e="textContent"in n?n.textContent:n.innerText;return a+e+d}function B(a){return a.replace(/&/g,
|
||||
"&").replace(N,function(a){return"&#"+a.charCodeAt(0)+";"}).replace(/</g,"<").replace(/>/g,">")}function s(a,e){var d=!1,c=h.bind(a,a.push);return{start:function(a,g,f){a=h.lowercase(a);!d&&x[a]&&(d=a);d||!0!==C[a]||(c("<"),c(a),h.forEach(g,function(d,f){var g=h.lowercase(f),k="img"===a&&"src"===g||"background"===g;!0!==O[g]||!0===D[g]&&!e(d,k)||(c(" "),c(f),c('="'),c(B(d)),c('"'))}),c(f?"/>":">"))},end:function(a){a=h.lowercase(a);d||!0!==C[a]||(c("</"),c(a),c(">"));a==d&&(d=!1)},chars:function(a){d||
|
||||
c(B(a))}}}var L=h.$$minErr("$sanitize"),A=/^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,z=/^<\s*\/\s*([\w:-]+)[^>]*>/,G=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,K=/^</,J=/^<\s*\//,H=/\x3c!--(.*?)--\x3e/g,y=/<!DOCTYPE([^>]*?)>/i,I=/<!\[CDATA\[(.*?)]]\x3e/g,N=/([^\#-~| |!])/g,w=k("area,br,col,hr,img,wbr");p=k("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr");q=k("rp,rt");var v=h.extend({},q,p),t=h.extend({},p,k("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),
|
||||
u=h.extend({},q,k("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),x=k("script,style"),C=h.extend({},w,t,u,v),D=k("background,cite,href,longdesc,src,usemap"),O=h.extend({},D,k("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width")),
|
||||
n=document.createElement("pre"),M=/^(\s*)([\s\S]*?)(\s*)$/;h.module("ngSanitize",[]).provider("$sanitize",function(){this.$get=["$$sanitizeUri",function(a){return function(e){var d=[];F(e,s(d,function(c,b){return!/^unsafe/.test(a(c,b))}));return d.join("")}}]});h.module("ngSanitize").filter("linky",["$sanitize",function(a){var e=/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,d=/^mailto:/;return function(c,b){function g(a){a&&m.push(E(a))}function f(a,c){m.push("<a ");h.isDefined(b)&&
|
||||
(m.push('target="'),m.push(b),m.push('" '));m.push('href="');m.push(a);m.push('">');g(c);m.push("</a>")}if(!c)return c;for(var l,k=c,m=[],n,p;l=k.match(e);)n=l[0],l[2]==l[3]&&(n="mailto:"+n),p=l.index,g(k.substr(0,p)),f(n,l[0].replace(d,"")),k=k.substring(p+l[0].length);g(k);return a(m.join(""))}}])})(window,window.angular);
|
||||
(function(window, angular) {'use strict';
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
* Any commits to this file should be reviewed with security in mind. *
|
||||
* Changes to this file can potentially create security vulnerabilities. *
|
||||
* An approval from 2 Core members with history of modifying *
|
||||
* this file is required. *
|
||||
* *
|
||||
* Does the change somehow allow for arbitrary javascript to be executed? *
|
||||
* Or allows for someone to change the prototype of built-in objects? *
|
||||
* Or gives undesired access to variables likes document or window? *
|
||||
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
|
||||
|
||||
var $sanitizeMinErr = angular.$$minErr('$sanitize');
|
||||
var bind;
|
||||
var extend;
|
||||
var forEach;
|
||||
var isDefined;
|
||||
var lowercase;
|
||||
var noop;
|
||||
var nodeContains;
|
||||
var htmlParser;
|
||||
var htmlSanitizeWriter;
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
* @name ngSanitize
|
||||
* @description
|
||||
*
|
||||
* # ngSanitize
|
||||
*
|
||||
* The `ngSanitize` module provides functionality to sanitize HTML.
|
||||
*
|
||||
*
|
||||
* <div doc-module-components="ngSanitize"></div>
|
||||
*
|
||||
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $sanitize
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Sanitizes an html string by stripping all potentially dangerous tokens.
|
||||
*
|
||||
* The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
|
||||
* then serialized back to properly escaped html string. This means that no unsafe input can make
|
||||
* it into the returned string.
|
||||
*
|
||||
* The whitelist for URL sanitization of attribute values is configured using the functions
|
||||
* `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
|
||||
* `$compileProvider`}.
|
||||
*
|
||||
* The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
|
||||
*
|
||||
* @param {string} html HTML input.
|
||||
* @returns {string} Sanitized HTML.
|
||||
*
|
||||
* @example
|
||||
<example module="sanitizeExample" deps="angular-sanitize.js" name="sanitize-service">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('sanitizeExample', ['ngSanitize'])
|
||||
.controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
|
||||
$scope.snippet =
|
||||
'<p style="color:blue">an html\n' +
|
||||
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
|
||||
'snippet</p>';
|
||||
$scope.deliberatelyTrustDangerousSnippet = function() {
|
||||
return $sce.trustAsHtml($scope.snippet);
|
||||
};
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Directive</td>
|
||||
<td>How</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="bind-html-with-sanitize">
|
||||
<td>ng-bind-html</td>
|
||||
<td>Automatically uses $sanitize</td>
|
||||
<td><pre><div ng-bind-html="snippet"><br/></div></pre></td>
|
||||
<td><div ng-bind-html="snippet"></div></td>
|
||||
</tr>
|
||||
<tr id="bind-html-with-trust">
|
||||
<td>ng-bind-html</td>
|
||||
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()">
|
||||
</div></pre>
|
||||
</td>
|
||||
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
|
||||
</tr>
|
||||
<tr id="bind-default">
|
||||
<td>ng-bind</td>
|
||||
<td>Automatically escapes</td>
|
||||
<td><pre><div ng-bind="snippet"><br/></div></pre></td>
|
||||
<td><div ng-bind="snippet"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should sanitize the html snippet by default', function() {
|
||||
expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
|
||||
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
|
||||
});
|
||||
|
||||
it('should inline raw snippet if bound to a trusted value', function() {
|
||||
expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).
|
||||
toBe("<p style=\"color:blue\">an html\n" +
|
||||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||||
"snippet</p>");
|
||||
});
|
||||
|
||||
it('should escape snippet without any filter', function() {
|
||||
expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).
|
||||
toBe("<p style=\"color:blue\">an html\n" +
|
||||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||||
"snippet</p>");
|
||||
});
|
||||
|
||||
it('should update', function() {
|
||||
element(by.model('snippet')).clear();
|
||||
element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
|
||||
expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')).
|
||||
toBe('new <b>text</b>');
|
||||
expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).toBe(
|
||||
'new <b onclick="alert(1)">text</b>');
|
||||
expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).toBe(
|
||||
"new <b onclick=\"alert(1)\">text</b>");
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc provider
|
||||
* @name $sanitizeProvider
|
||||
* @this
|
||||
*
|
||||
* @description
|
||||
* Creates and configures {@link $sanitize} instance.
|
||||
*/
|
||||
function $SanitizeProvider() {
|
||||
var svgEnabled = false;
|
||||
|
||||
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
|
||||
if (svgEnabled) {
|
||||
extend(validElements, svgElements);
|
||||
}
|
||||
return function(html) {
|
||||
var buf = [];
|
||||
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
|
||||
return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
|
||||
}));
|
||||
return buf.join('');
|
||||
};
|
||||
}];
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $sanitizeProvider#enableSvg
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Enables a subset of svg to be supported by the sanitizer.
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* <p>By enabling this setting without taking other precautions, you might expose your
|
||||
* application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
|
||||
* outside of the containing element and be rendered over other elements on the page (e.g. a login
|
||||
* link). Such behavior can then result in phishing incidents.</p>
|
||||
*
|
||||
* <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
|
||||
* tags within the sanitized content:</p>
|
||||
*
|
||||
* <br>
|
||||
*
|
||||
* <pre><code>
|
||||
* .rootOfTheIncludedContent svg {
|
||||
* overflow: hidden !important;
|
||||
* }
|
||||
* </code></pre>
|
||||
* </div>
|
||||
*
|
||||
* @param {boolean=} flag Enable or disable SVG support in the sanitizer.
|
||||
* @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
|
||||
* without an argument or self for chaining otherwise.
|
||||
*/
|
||||
this.enableSvg = function(enableSvg) {
|
||||
if (isDefined(enableSvg)) {
|
||||
svgEnabled = enableSvg;
|
||||
return this;
|
||||
} else {
|
||||
return svgEnabled;
|
||||
}
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private stuff
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bind = angular.bind;
|
||||
extend = angular.extend;
|
||||
forEach = angular.forEach;
|
||||
isDefined = angular.isDefined;
|
||||
lowercase = angular.lowercase;
|
||||
noop = angular.noop;
|
||||
|
||||
htmlParser = htmlParserImpl;
|
||||
htmlSanitizeWriter = htmlSanitizeWriterImpl;
|
||||
|
||||
nodeContains = window.Node.prototype.contains || /** @this */ function(arg) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return !!(this.compareDocumentPosition(arg) & 16);
|
||||
};
|
||||
|
||||
// Regular Expressions for parsing tags and attributes
|
||||
var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
// Match everything outside of normal chars and " (quote character)
|
||||
NON_ALPHANUMERIC_REGEXP = /([^#-~ |!])/g;
|
||||
|
||||
|
||||
// Good source of info about elements and attributes
|
||||
// http://dev.w3.org/html5/spec/Overview.html#semantics
|
||||
// http://simon.html5.org/html-elements
|
||||
|
||||
// Safe Void Elements - HTML5
|
||||
// http://dev.w3.org/html5/spec/Overview.html#void-elements
|
||||
var voidElements = toMap('area,br,col,hr,img,wbr');
|
||||
|
||||
// Elements that you can, intentionally, leave open (and which close themselves)
|
||||
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
|
||||
var optionalEndTagBlockElements = toMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'),
|
||||
optionalEndTagInlineElements = toMap('rp,rt'),
|
||||
optionalEndTagElements = extend({},
|
||||
optionalEndTagInlineElements,
|
||||
optionalEndTagBlockElements);
|
||||
|
||||
// Safe Block Elements - HTML5
|
||||
var blockElements = extend({}, optionalEndTagBlockElements, toMap('address,article,' +
|
||||
'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
|
||||
'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul'));
|
||||
|
||||
// Inline Elements - HTML5
|
||||
var inlineElements = extend({}, optionalEndTagInlineElements, toMap('a,abbr,acronym,b,' +
|
||||
'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' +
|
||||
'samp,small,span,strike,strong,sub,sup,time,tt,u,var'));
|
||||
|
||||
// SVG Elements
|
||||
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
|
||||
// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
|
||||
// They can potentially allow for arbitrary javascript to be executed. See #11290
|
||||
var svgElements = toMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' +
|
||||
'hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,' +
|
||||
'radialGradient,rect,stop,svg,switch,text,title,tspan');
|
||||
|
||||
// Blocked Elements (will be stripped)
|
||||
var blockedElements = toMap('script,style');
|
||||
|
||||
var validElements = extend({},
|
||||
voidElements,
|
||||
blockElements,
|
||||
inlineElements,
|
||||
optionalEndTagElements);
|
||||
|
||||
//Attributes that have href and hence need to be sanitized
|
||||
var uriAttrs = toMap('background,cite,href,longdesc,src,xlink:href');
|
||||
|
||||
var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
|
||||
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
|
||||
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
|
||||
'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
|
||||
'valign,value,vspace,width');
|
||||
|
||||
// SVG attributes (without "id" and "name" attributes)
|
||||
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
|
||||
var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
|
||||
'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
|
||||
'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
|
||||
'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
|
||||
'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
|
||||
'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
|
||||
'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
|
||||
'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
|
||||
'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
|
||||
'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
|
||||
'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
|
||||
'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
|
||||
'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
|
||||
'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
|
||||
'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
|
||||
|
||||
var validAttrs = extend({},
|
||||
uriAttrs,
|
||||
svgAttrs,
|
||||
htmlAttrs);
|
||||
|
||||
function toMap(str, lowercaseKeys) {
|
||||
var obj = {}, items = str.split(','), i;
|
||||
for (i = 0; i < items.length; i++) {
|
||||
obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an inert document that contains the dirty HTML that needs sanitizing
|
||||
* Depending upon browser support we use one of three strategies for doing this.
|
||||
* Support: Safari 10.x -> XHR strategy
|
||||
* Support: Firefox -> DomParser strategy
|
||||
*/
|
||||
var getInertBodyElement /* function(html: string): HTMLBodyElement */ = (function(window, document) {
|
||||
var inertDocument;
|
||||
if (document && document.implementation) {
|
||||
inertDocument = document.implementation.createHTMLDocument('inert');
|
||||
} else {
|
||||
throw $sanitizeMinErr('noinert', 'Can\'t create an inert html document');
|
||||
}
|
||||
var inertBodyElement = (inertDocument.documentElement || inertDocument.getDocumentElement()).querySelector('body');
|
||||
|
||||
// Check for the Safari 10.1 bug - which allows JS to run inside the SVG G element
|
||||
inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
|
||||
if (!inertBodyElement.querySelector('svg')) {
|
||||
return getInertBodyElement_XHR;
|
||||
} else {
|
||||
// Check for the Firefox bug - which prevents the inner img JS from being sanitized
|
||||
inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
|
||||
if (inertBodyElement.querySelector('svg img')) {
|
||||
return getInertBodyElement_DOMParser;
|
||||
} else {
|
||||
return getInertBodyElement_InertDocument;
|
||||
}
|
||||
}
|
||||
|
||||
function getInertBodyElement_XHR(html) {
|
||||
// We add this dummy element to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
|
||||
html = '<remove></remove>' + html;
|
||||
try {
|
||||
html = encodeURI(html);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
var xhr = new window.XMLHttpRequest();
|
||||
xhr.responseType = 'document';
|
||||
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
|
||||
xhr.send(null);
|
||||
var body = xhr.response.body;
|
||||
body.firstChild.remove();
|
||||
return body;
|
||||
}
|
||||
|
||||
function getInertBodyElement_DOMParser(html) {
|
||||
// We add this dummy element to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
|
||||
html = '<remove></remove>' + html;
|
||||
try {
|
||||
var body = new window.DOMParser().parseFromString(html, 'text/html').body;
|
||||
body.firstChild.remove();
|
||||
return body;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getInertBodyElement_InertDocument(html) {
|
||||
inertBodyElement.innerHTML = html;
|
||||
|
||||
// Support: IE 9-11 only
|
||||
// strip custom-namespaced attributes on IE<=11
|
||||
if (document.documentMode) {
|
||||
stripCustomNsAttrs(inertBodyElement);
|
||||
}
|
||||
|
||||
return inertBodyElement;
|
||||
}
|
||||
})(window, window.document);
|
||||
|
||||
/**
|
||||
* @example
|
||||
* htmlParser(htmlString, {
|
||||
* start: function(tag, attrs) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* });
|
||||
*
|
||||
* @param {string} html string
|
||||
* @param {object} handler
|
||||
*/
|
||||
function htmlParserImpl(html, handler) {
|
||||
if (html === null || html === undefined) {
|
||||
html = '';
|
||||
} else if (typeof html !== 'string') {
|
||||
html = '' + html;
|
||||
}
|
||||
|
||||
var inertBodyElement = getInertBodyElement(html);
|
||||
if (!inertBodyElement) return '';
|
||||
|
||||
//mXSS protection
|
||||
var mXSSAttempts = 5;
|
||||
do {
|
||||
if (mXSSAttempts === 0) {
|
||||
throw $sanitizeMinErr('uinput', 'Failed to sanitize html because the input is unstable');
|
||||
}
|
||||
mXSSAttempts--;
|
||||
|
||||
// trigger mXSS if it is going to happen by reading and writing the innerHTML
|
||||
html = inertBodyElement.innerHTML;
|
||||
inertBodyElement = getInertBodyElement(html);
|
||||
} while (html !== inertBodyElement.innerHTML);
|
||||
|
||||
var node = inertBodyElement.firstChild;
|
||||
while (node) {
|
||||
switch (node.nodeType) {
|
||||
case 1: // ELEMENT_NODE
|
||||
handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
|
||||
break;
|
||||
case 3: // TEXT NODE
|
||||
handler.chars(node.textContent);
|
||||
break;
|
||||
}
|
||||
|
||||
var nextNode;
|
||||
if (!(nextNode = node.firstChild)) {
|
||||
if (node.nodeType === 1) {
|
||||
handler.end(node.nodeName.toLowerCase());
|
||||
}
|
||||
nextNode = getNonDescendant('nextSibling', node);
|
||||
if (!nextNode) {
|
||||
while (nextNode == null) {
|
||||
node = getNonDescendant('parentNode', node);
|
||||
if (node === inertBodyElement) break;
|
||||
nextNode = getNonDescendant('nextSibling', node);
|
||||
if (node.nodeType === 1) {
|
||||
handler.end(node.nodeName.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
node = nextNode;
|
||||
}
|
||||
|
||||
while ((node = inertBodyElement.firstChild)) {
|
||||
inertBodyElement.removeChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
function attrToMap(attrs) {
|
||||
var map = {};
|
||||
for (var i = 0, ii = attrs.length; i < ii; i++) {
|
||||
var attr = attrs[i];
|
||||
map[attr.name] = attr.value;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Escapes all potentially dangerous characters, so that the
|
||||
* resulting string can be safely inserted into attribute or
|
||||
* element text.
|
||||
* @param value
|
||||
* @returns {string} escaped text
|
||||
*/
|
||||
function encodeEntities(value) {
|
||||
return value.
|
||||
replace(/&/g, '&').
|
||||
replace(SURROGATE_PAIR_REGEXP, function(value) {
|
||||
var hi = value.charCodeAt(0);
|
||||
var low = value.charCodeAt(1);
|
||||
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
|
||||
}).
|
||||
replace(NON_ALPHANUMERIC_REGEXP, function(value) {
|
||||
return '&#' + value.charCodeAt(0) + ';';
|
||||
}).
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* create an HTML/XML writer which writes to buffer
|
||||
* @param {Array} buf use buf.join('') to get out sanitized html string
|
||||
* @returns {object} in the form of {
|
||||
* start: function(tag, attrs) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* }
|
||||
*/
|
||||
function htmlSanitizeWriterImpl(buf, uriValidator) {
|
||||
var ignoreCurrentElement = false;
|
||||
var out = bind(buf, buf.push);
|
||||
return {
|
||||
start: function(tag, attrs) {
|
||||
tag = lowercase(tag);
|
||||
if (!ignoreCurrentElement && blockedElements[tag]) {
|
||||
ignoreCurrentElement = tag;
|
||||
}
|
||||
if (!ignoreCurrentElement && validElements[tag] === true) {
|
||||
out('<');
|
||||
out(tag);
|
||||
forEach(attrs, function(value, key) {
|
||||
var lkey = lowercase(key);
|
||||
var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
|
||||
if (validAttrs[lkey] === true &&
|
||||
(uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
|
||||
out(' ');
|
||||
out(key);
|
||||
out('="');
|
||||
out(encodeEntities(value));
|
||||
out('"');
|
||||
}
|
||||
});
|
||||
out('>');
|
||||
}
|
||||
},
|
||||
end: function(tag) {
|
||||
tag = lowercase(tag);
|
||||
if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
|
||||
out('</');
|
||||
out(tag);
|
||||
out('>');
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (tag == ignoreCurrentElement) {
|
||||
ignoreCurrentElement = false;
|
||||
}
|
||||
},
|
||||
chars: function(chars) {
|
||||
if (!ignoreCurrentElement) {
|
||||
out(encodeEntities(chars));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
|
||||
* ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
|
||||
* to allow any of these custom attributes. This method strips them all.
|
||||
*
|
||||
* @param node Root element to process
|
||||
*/
|
||||
function stripCustomNsAttrs(node) {
|
||||
while (node) {
|
||||
if (node.nodeType === window.Node.ELEMENT_NODE) {
|
||||
var attrs = node.attributes;
|
||||
for (var i = 0, l = attrs.length; i < l; i++) {
|
||||
var attrNode = attrs[i];
|
||||
var attrName = attrNode.name.toLowerCase();
|
||||
if (attrName === 'xmlns:ns1' || attrName.lastIndexOf('ns1:', 0) === 0) {
|
||||
node.removeAttributeNode(attrNode);
|
||||
i--;
|
||||
l--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var nextNode = node.firstChild;
|
||||
if (nextNode) {
|
||||
stripCustomNsAttrs(nextNode);
|
||||
}
|
||||
|
||||
node = getNonDescendant('nextSibling', node);
|
||||
}
|
||||
}
|
||||
|
||||
function getNonDescendant(propName, node) {
|
||||
// An element is clobbered if its `propName` property points to one of its descendants
|
||||
var nextNode = node[propName];
|
||||
if (nextNode && nodeContains.call(node, nextNode)) {
|
||||
throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText);
|
||||
}
|
||||
return nextNode;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeText(chars) {
|
||||
var buf = [];
|
||||
var writer = htmlSanitizeWriter(buf, noop);
|
||||
writer.chars(chars);
|
||||
return buf.join('');
|
||||
}
|
||||
|
||||
|
||||
// define ngSanitize module and register $sanitize service
|
||||
angular.module('ngSanitize', [])
|
||||
.provider('$sanitize', $SanitizeProvider)
|
||||
.info({ angularVersion: '1.6.6' });
|
||||
|
||||
/**
|
||||
* @ngdoc filter
|
||||
* @name linky
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and
|
||||
* plain email address links.
|
||||
*
|
||||
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
|
||||
*
|
||||
* @param {string} text Input text.
|
||||
* @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in.
|
||||
* @param {object|function(url)} [attributes] Add custom attributes to the link element.
|
||||
*
|
||||
* Can be one of:
|
||||
*
|
||||
* - `object`: A map of attributes
|
||||
* - `function`: Takes the url as a parameter and returns a map of attributes
|
||||
*
|
||||
* If the map of attributes contains a value for `target`, it overrides the value of
|
||||
* the target parameter.
|
||||
*
|
||||
*
|
||||
* @returns {string} Html-linkified and {@link $sanitize sanitized} text.
|
||||
*
|
||||
* @usage
|
||||
<span ng-bind-html="linky_expression | linky"></span>
|
||||
*
|
||||
* @example
|
||||
<example module="linkyExample" deps="angular-sanitize.js" name="linky-filter">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Filter</th>
|
||||
<th>Source</th>
|
||||
<th>Rendered</th>
|
||||
</tr>
|
||||
<tr id="linky-filter">
|
||||
<td>linky filter</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippet | linky"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippet | linky"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="linky-target">
|
||||
<td>linky target</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippetWithSingleURL | linky:'_blank'"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="linky-custom-attributes">
|
||||
<td>linky custom attributes</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="escaped-html">
|
||||
<td>no filter</td>
|
||||
<td><pre><div ng-bind="snippet"><br></div></pre></td>
|
||||
<td><div ng-bind="snippet"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('linkyExample', ['ngSanitize'])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.snippet =
|
||||
'Pretty text with some links:\n' +
|
||||
'http://angularjs.org/,\n' +
|
||||
'mailto:us@somewhere.org,\n' +
|
||||
'another@somewhere.org,\n' +
|
||||
'and one more: ftp://127.0.0.1/.';
|
||||
$scope.snippetWithSingleURL = 'http://angularjs.org/';
|
||||
}]);
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should linkify the snippet with urls', function() {
|
||||
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
||||
toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
|
||||
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
|
||||
expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
|
||||
});
|
||||
|
||||
it('should not linkify snippet without the linky filter', function() {
|
||||
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
|
||||
toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
|
||||
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
|
||||
expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update', function() {
|
||||
element(by.model('snippet')).clear();
|
||||
element(by.model('snippet')).sendKeys('new http://link.');
|
||||
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
||||
toBe('new http://link.');
|
||||
expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
|
||||
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
|
||||
.toBe('new http://link.');
|
||||
});
|
||||
|
||||
it('should work with the target property', function() {
|
||||
expect(element(by.id('linky-target')).
|
||||
element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
|
||||
toBe('http://angularjs.org/');
|
||||
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
|
||||
});
|
||||
|
||||
it('should optionally add custom attributes', function() {
|
||||
expect(element(by.id('linky-custom-attributes')).
|
||||
element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
|
||||
toBe('http://angularjs.org/');
|
||||
expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
|
||||
var LINKY_URL_REGEXP =
|
||||
/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
|
||||
MAILTO_REGEXP = /^mailto:/i;
|
||||
|
||||
var linkyMinErr = angular.$$minErr('linky');
|
||||
var isDefined = angular.isDefined;
|
||||
var isFunction = angular.isFunction;
|
||||
var isObject = angular.isObject;
|
||||
var isString = angular.isString;
|
||||
|
||||
return function(text, target, attributes) {
|
||||
if (text == null || text === '') return text;
|
||||
if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
|
||||
|
||||
var attributesFn =
|
||||
isFunction(attributes) ? attributes :
|
||||
isObject(attributes) ? function getAttributesObject() {return attributes;} :
|
||||
function getEmptyAttributesObject() {return {};};
|
||||
|
||||
var match;
|
||||
var raw = text;
|
||||
var html = [];
|
||||
var url;
|
||||
var i;
|
||||
while ((match = raw.match(LINKY_URL_REGEXP))) {
|
||||
// We can not end in these as they are sometimes found at the end of the sentence
|
||||
url = match[0];
|
||||
// if we did not match ftp/http/www/mailto then assume mailto
|
||||
if (!match[2] && !match[4]) {
|
||||
url = (match[3] ? 'http://' : 'mailto:') + url;
|
||||
}
|
||||
i = match.index;
|
||||
addText(raw.substr(0, i));
|
||||
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
|
||||
raw = raw.substring(i + match[0].length);
|
||||
}
|
||||
addText(raw);
|
||||
return $sanitize(html.join(''));
|
||||
|
||||
function addText(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
html.push(sanitizeText(text));
|
||||
}
|
||||
|
||||
function addLink(url, text) {
|
||||
var key, linkAttributes = attributesFn(url);
|
||||
html.push('<a ');
|
||||
|
||||
for (key in linkAttributes) {
|
||||
html.push(key + '="' + linkAttributes[key] + '" ');
|
||||
}
|
||||
|
||||
if (isDefined(target) && !('target' in linkAttributes)) {
|
||||
html.push('target="',
|
||||
target,
|
||||
'" ');
|
||||
}
|
||||
html.push('href="',
|
||||
url.replace(/"/g, '"'),
|
||||
'">');
|
||||
addText(text);
|
||||
html.push('</a>');
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
|
||||
})(window, window.angular);
|
@ -1,12 +1,749 @@
|
||||
/*
|
||||
AngularJS v1.2.8
|
||||
(c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
/**
|
||||
* @license AngularJS v1.6.6
|
||||
* (c) 2010-2017 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(y,v,z){'use strict';function t(g,a,b){q.directive(g,["$parse","$swipe",function(l,n){var r=75,h=0.3,d=30;return function(p,m,k){function e(e){if(!u)return!1;var c=Math.abs(e.y-u.y);e=(e.x-u.x)*a;return f&&c<r&&0<e&&e>d&&c/e<h}var c=l(k[g]),u,f;n.bind(m,{start:function(e,c){u=e;f=!0},cancel:function(e){f=!1},end:function(a,f){e(a)&&p.$apply(function(){m.triggerHandler(b);c(p,{$event:f})})}})}}])}var q=v.module("ngTouch",[]);q.factory("$swipe",[function(){function g(a){var b=a.touches&&a.touches.length?
|
||||
a.touches:[a];a=a.changedTouches&&a.changedTouches[0]||a.originalEvent&&a.originalEvent.changedTouches&&a.originalEvent.changedTouches[0]||b[0].originalEvent||b[0];return{x:a.clientX,y:a.clientY}}return{bind:function(a,b){var l,n,r,h,d=!1;a.on("touchstart mousedown",function(a){r=g(a);d=!0;n=l=0;h=r;b.start&&b.start(r,a)});a.on("touchcancel",function(a){d=!1;b.cancel&&b.cancel(a)});a.on("touchmove mousemove",function(a){if(d&&r){var m=g(a);l+=Math.abs(m.x-h.x);n+=Math.abs(m.y-h.y);h=m;10>l&&10>n||
|
||||
(n>l?(d=!1,b.cancel&&b.cancel(a)):(a.preventDefault(),b.move&&b.move(m,a)))}});a.on("touchend mouseup",function(a){d&&(d=!1,b.end&&b.end(g(a),a))})}}}]);q.config(["$provide",function(g){g.decorator("ngClickDirective",["$delegate",function(a){a.shift();return a}])}]);q.directive("ngClick",["$parse","$timeout","$rootElement",function(g,a,b){function l(a,c,b){for(var f=0;f<a.length;f+=2)if(Math.abs(a[f]-c)<d&&Math.abs(a[f+1]-b)<d)return a.splice(f,f+2),!0;return!1}function n(a){if(!(Date.now()-m>h)){var c=
|
||||
a.touches&&a.touches.length?a.touches:[a],b=c[0].clientX,c=c[0].clientY;1>b&&1>c||l(k,b,c)||(a.stopPropagation(),a.preventDefault(),a.target&&a.target.blur())}}function r(b){b=b.touches&&b.touches.length?b.touches:[b];var c=b[0].clientX,d=b[0].clientY;k.push(c,d);a(function(){for(var a=0;a<k.length;a+=2)if(k[a]==c&&k[a+1]==d){k.splice(a,a+2);break}},h,!1)}var h=2500,d=25,p="ng-click-active",m,k;return function(a,c,d){function f(){q=!1;c.removeClass(p)}var h=g(d.ngClick),q=!1,s,t,w,x;c.on("touchstart",
|
||||
function(a){q=!0;s=a.target?a.target:a.srcElement;3==s.nodeType&&(s=s.parentNode);c.addClass(p);t=Date.now();a=a.touches&&a.touches.length?a.touches:[a];a=a[0].originalEvent||a[0];w=a.clientX;x=a.clientY});c.on("touchmove",function(a){f()});c.on("touchcancel",function(a){f()});c.on("touchend",function(a){var h=Date.now()-t,e=a.changedTouches&&a.changedTouches.length?a.changedTouches:a.touches&&a.touches.length?a.touches:[a],g=e[0].originalEvent||e[0],e=g.clientX,g=g.clientY,p=Math.sqrt(Math.pow(e-
|
||||
w,2)+Math.pow(g-x,2));q&&(750>h&&12>p)&&(k||(b[0].addEventListener("click",n,!0),b[0].addEventListener("touchstart",r,!0),k=[]),m=Date.now(),l(k,e,g),s&&s.blur(),v.isDefined(d.disabled)&&!1!==d.disabled||c.triggerHandler("click",[a]));f()});c.onclick=function(a){};c.on("click",function(b,c){a.$apply(function(){h(a,{$event:c||b})})});c.on("mousedown",function(a){c.addClass(p)});c.on("mousemove mouseup",function(a){c.removeClass(p)})}}]);t("ngSwipeLeft",-1,"swipeleft");t("ngSwipeRight",1,"swiperight")})(window,
|
||||
window.angular);
|
||||
(function(window, angular) {'use strict';
|
||||
|
||||
/* global ngTouchClickDirectiveFactory: false */
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
* @name ngTouch
|
||||
* @description
|
||||
*
|
||||
* # ngTouch
|
||||
*
|
||||
* The `ngTouch` module provides touch events and other helpers for touch-enabled devices.
|
||||
* The implementation is based on jQuery Mobile touch event handling
|
||||
* ([jquerymobile.com](http://jquerymobile.com/)).
|
||||
*
|
||||
*
|
||||
* See {@link ngTouch.$swipe `$swipe`} for usage.
|
||||
*
|
||||
* <div doc-module-components="ngTouch"></div>
|
||||
*
|
||||
*/
|
||||
|
||||
// define ngTouch module
|
||||
/* global -ngTouch */
|
||||
var ngTouch = angular.module('ngTouch', []);
|
||||
|
||||
ngTouch.info({ angularVersion: '1.6.6' });
|
||||
|
||||
ngTouch.provider('$touch', $TouchProvider);
|
||||
|
||||
function nodeName_(element) {
|
||||
return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc provider
|
||||
* @name $touchProvider
|
||||
*
|
||||
* @description
|
||||
* The `$touchProvider` allows enabling / disabling {@link ngTouch.ngClick ngTouch's ngClick directive}.
|
||||
*/
|
||||
$TouchProvider.$inject = ['$provide', '$compileProvider'];
|
||||
function $TouchProvider($provide, $compileProvider) {
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $touchProvider#ngClickOverrideEnabled
|
||||
*
|
||||
* @param {boolean=} enabled update the ngClickOverrideEnabled state if provided, otherwise just return the
|
||||
* current ngClickOverrideEnabled state
|
||||
* @returns {*} current value if used as getter or itself (chaining) if used as setter
|
||||
*
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Call this method to enable/disable {@link ngTouch.ngClick ngTouch's ngClick directive}. If enabled,
|
||||
* the default ngClick directive will be replaced by a version that eliminates the 300ms delay for
|
||||
* click events on browser for touch-devices.
|
||||
*
|
||||
* The default is `false`.
|
||||
*
|
||||
*/
|
||||
var ngClickOverrideEnabled = false;
|
||||
var ngClickDirectiveAdded = false;
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
this.ngClickOverrideEnabled = function(enabled) {
|
||||
if (angular.isDefined(enabled)) {
|
||||
|
||||
if (enabled && !ngClickDirectiveAdded) {
|
||||
ngClickDirectiveAdded = true;
|
||||
|
||||
// Use this to identify the correct directive in the delegate
|
||||
ngTouchClickDirectiveFactory.$$moduleName = 'ngTouch';
|
||||
$compileProvider.directive('ngClick', ngTouchClickDirectiveFactory);
|
||||
|
||||
$provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
|
||||
if (ngClickOverrideEnabled) {
|
||||
// drop the default ngClick directive
|
||||
$delegate.shift();
|
||||
} else {
|
||||
// drop the ngTouch ngClick directive if the override has been re-disabled (because
|
||||
// we cannot de-register added directives)
|
||||
var i = $delegate.length - 1;
|
||||
while (i >= 0) {
|
||||
if ($delegate[i].$$moduleName === 'ngTouch') {
|
||||
$delegate.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return $delegate;
|
||||
}]);
|
||||
}
|
||||
|
||||
ngClickOverrideEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
return ngClickOverrideEnabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $touch
|
||||
* @kind object
|
||||
*
|
||||
* @description
|
||||
* Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method.
|
||||
*
|
||||
*/
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
this.$get = function() {
|
||||
return {
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $touch#ngClickOverrideEnabled
|
||||
*
|
||||
* @returns {*} current value of `ngClickOverrideEnabled` set in the {@link ngTouch.$touchProvider $touchProvider},
|
||||
* i.e. if {@link ngTouch.ngClick ngTouch's ngClick} directive is enabled.
|
||||
*
|
||||
* @kind function
|
||||
*/
|
||||
ngClickOverrideEnabled: function() {
|
||||
return ngClickOverrideEnabled;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/* global ngTouch: false */
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $swipe
|
||||
*
|
||||
* @description
|
||||
* The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe
|
||||
* behavior, to make implementing swipe-related directives more convenient.
|
||||
*
|
||||
* Requires the {@link ngTouch `ngTouch`} module to be installed.
|
||||
*
|
||||
* `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`.
|
||||
*
|
||||
* # Usage
|
||||
* The `$swipe` service is an object with a single method: `bind`. `bind` takes an element
|
||||
* which is to be watched for swipes, and an object with four handler functions. See the
|
||||
* documentation for `bind` below.
|
||||
*/
|
||||
|
||||
ngTouch.factory('$swipe', [function() {
|
||||
// The total distance in any direction before we make the call on swipe vs. scroll.
|
||||
var MOVE_BUFFER_RADIUS = 10;
|
||||
|
||||
var POINTER_EVENTS = {
|
||||
'mouse': {
|
||||
start: 'mousedown',
|
||||
move: 'mousemove',
|
||||
end: 'mouseup'
|
||||
},
|
||||
'touch': {
|
||||
start: 'touchstart',
|
||||
move: 'touchmove',
|
||||
end: 'touchend',
|
||||
cancel: 'touchcancel'
|
||||
},
|
||||
'pointer': {
|
||||
start: 'pointerdown',
|
||||
move: 'pointermove',
|
||||
end: 'pointerup',
|
||||
cancel: 'pointercancel'
|
||||
}
|
||||
};
|
||||
|
||||
function getCoordinates(event) {
|
||||
var originalEvent = event.originalEvent || event;
|
||||
var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent];
|
||||
var e = (originalEvent.changedTouches && originalEvent.changedTouches[0]) || touches[0];
|
||||
|
||||
return {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
}
|
||||
|
||||
function getEvents(pointerTypes, eventType) {
|
||||
var res = [];
|
||||
angular.forEach(pointerTypes, function(pointerType) {
|
||||
var eventName = POINTER_EVENTS[pointerType][eventType];
|
||||
if (eventName) {
|
||||
res.push(eventName);
|
||||
}
|
||||
});
|
||||
return res.join(' ');
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $swipe#bind
|
||||
*
|
||||
* @description
|
||||
* The main method of `$swipe`. It takes an element to be watched for swipe motions, and an
|
||||
* object containing event handlers.
|
||||
* The pointer types that should be used can be specified via the optional
|
||||
* third argument, which is an array of strings `'mouse'`, `'touch'` and `'pointer'`. By default,
|
||||
* `$swipe` will listen for `mouse`, `touch` and `pointer` events.
|
||||
*
|
||||
* The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end`
|
||||
* receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }` and the raw
|
||||
* `event`. `cancel` receives the raw `event` as its single parameter.
|
||||
*
|
||||
* `start` is called on either `mousedown`, `touchstart` or `pointerdown`. After this event, `$swipe` is
|
||||
* watching for `touchmove`, `mousemove` or `pointermove` events. These events are ignored until the total
|
||||
* distance moved in either dimension exceeds a small threshold.
|
||||
*
|
||||
* Once this threshold is exceeded, either the horizontal or vertical delta is greater.
|
||||
* - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow.
|
||||
* - If the vertical distance is greater, this is a scroll, and we let the browser take over.
|
||||
* A `cancel` event is sent.
|
||||
*
|
||||
* `move` is called on `mousemove`, `touchmove` and `pointermove` after the above logic has determined that
|
||||
* a swipe is in progress.
|
||||
*
|
||||
* `end` is called when a swipe is successfully completed with a `touchend`, `mouseup` or `pointerup`.
|
||||
*
|
||||
* `cancel` is called either on a `touchcancel` or `pointercancel` from the browser, or when we begin scrolling
|
||||
* as described above.
|
||||
*
|
||||
*/
|
||||
bind: function(element, eventHandlers, pointerTypes) {
|
||||
// Absolute total movement, used to control swipe vs. scroll.
|
||||
var totalX, totalY;
|
||||
// Coordinates of the start position.
|
||||
var startCoords;
|
||||
// Last event's position.
|
||||
var lastPos;
|
||||
// Whether a swipe is active.
|
||||
var active = false;
|
||||
|
||||
pointerTypes = pointerTypes || ['mouse', 'touch', 'pointer'];
|
||||
element.on(getEvents(pointerTypes, 'start'), function(event) {
|
||||
startCoords = getCoordinates(event);
|
||||
active = true;
|
||||
totalX = 0;
|
||||
totalY = 0;
|
||||
lastPos = startCoords;
|
||||
if (eventHandlers['start']) {
|
||||
eventHandlers['start'](startCoords, event);
|
||||
}
|
||||
});
|
||||
var events = getEvents(pointerTypes, 'cancel');
|
||||
if (events) {
|
||||
element.on(events, function(event) {
|
||||
active = false;
|
||||
if (eventHandlers['cancel']) {
|
||||
eventHandlers['cancel'](event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
element.on(getEvents(pointerTypes, 'move'), function(event) {
|
||||
if (!active) return;
|
||||
|
||||
// Android will send a touchcancel if it thinks we're starting to scroll.
|
||||
// So when the total distance (+ or - or both) exceeds 10px in either direction,
|
||||
// we either:
|
||||
// - On totalX > totalY, we send preventDefault() and treat this as a swipe.
|
||||
// - On totalY > totalX, we let the browser handle it as a scroll.
|
||||
|
||||
if (!startCoords) return;
|
||||
var coords = getCoordinates(event);
|
||||
|
||||
totalX += Math.abs(coords.x - lastPos.x);
|
||||
totalY += Math.abs(coords.y - lastPos.y);
|
||||
|
||||
lastPos = coords;
|
||||
|
||||
if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll.
|
||||
if (totalY > totalX) {
|
||||
// Allow native scrolling to take over.
|
||||
active = false;
|
||||
if (eventHandlers['cancel']) {
|
||||
eventHandlers['cancel'](event);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// Prevent the browser from scrolling.
|
||||
event.preventDefault();
|
||||
if (eventHandlers['move']) {
|
||||
eventHandlers['move'](coords, event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
element.on(getEvents(pointerTypes, 'end'), function(event) {
|
||||
if (!active) return;
|
||||
active = false;
|
||||
if (eventHandlers['end']) {
|
||||
eventHandlers['end'](getCoordinates(event), event);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
/* global ngTouch: false,
|
||||
nodeName_: false
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngClick
|
||||
* @deprecated
|
||||
* sinceVersion="v1.5.0"
|
||||
* This directive is deprecated and **disabled** by default.
|
||||
* The directive will receive no further support and might be removed from future releases.
|
||||
* If you need the directive, you can enable it with the {@link ngTouch.$touchProvider $touchProvider#ngClickOverrideEnabled}
|
||||
* function. We also recommend that you migrate to [FastClick](https://github.com/ftlabs/fastclick).
|
||||
* To learn more about the 300ms delay, this [Telerik article](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/)
|
||||
* gives a good overview.
|
||||
*
|
||||
* @description
|
||||
* A more powerful replacement for the default ngClick designed to be used on touchscreen
|
||||
* devices. Most mobile browsers wait about 300ms after a tap-and-release before sending
|
||||
* the click event. This version handles them immediately, and then prevents the
|
||||
* following click event from propagating.
|
||||
*
|
||||
* Requires the {@link ngTouch `ngTouch`} module to be installed.
|
||||
*
|
||||
* This directive can fall back to using an ordinary click event, and so works on desktop
|
||||
* browsers as well as mobile.
|
||||
*
|
||||
* This directive also sets the CSS class `ng-click-active` while the element is being held
|
||||
* down (by a mouse click or touch) so you can restyle the depressed element if you wish.
|
||||
*
|
||||
* @element ANY
|
||||
* @param {expression} ngClick {@link guide/expression Expression} to evaluate
|
||||
* upon tap. (Event object is available as `$event`)
|
||||
*
|
||||
* @example
|
||||
<example module="ngClickExample" deps="angular-touch.js" name="ng-touch-ng-click">
|
||||
<file name="index.html">
|
||||
<button ng-click="count = count + 1" ng-init="count=0">
|
||||
Increment
|
||||
</button>
|
||||
count: {{ count }}
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('ngClickExample', ['ngTouch']);
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
|
||||
var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement',
|
||||
function($parse, $timeout, $rootElement) {
|
||||
var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag.
|
||||
var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers.
|
||||
var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click
|
||||
var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks.
|
||||
|
||||
var ACTIVE_CLASS_NAME = 'ng-click-active';
|
||||
var lastPreventedTime;
|
||||
var touchCoordinates;
|
||||
var lastLabelClickCoordinates;
|
||||
|
||||
|
||||
// TAP EVENTS AND GHOST CLICKS
|
||||
//
|
||||
// Why tap events?
|
||||
// Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're
|
||||
// double-tapping, and then fire a click event.
|
||||
//
|
||||
// This delay sucks and makes mobile apps feel unresponsive.
|
||||
// So we detect touchstart, touchcancel and touchend ourselves and determine when
|
||||
// the user has tapped on something.
|
||||
//
|
||||
// What happens when the browser then generates a click event?
|
||||
// The browser, of course, also detects the tap and fires a click after a delay. This results in
|
||||
// tapping/clicking twice. We do "clickbusting" to prevent it.
|
||||
//
|
||||
// How does it work?
|
||||
// We attach global touchstart and click handlers, that run during the capture (early) phase.
|
||||
// So the sequence for a tap is:
|
||||
// - global touchstart: Sets an "allowable region" at the point touched.
|
||||
// - element's touchstart: Starts a touch
|
||||
// (- touchcancel ends the touch, no click follows)
|
||||
// - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold
|
||||
// too long) and fires the user's tap handler. The touchend also calls preventGhostClick().
|
||||
// - preventGhostClick() removes the allowable region the global touchstart created.
|
||||
// - The browser generates a click event.
|
||||
// - The global click handler catches the click, and checks whether it was in an allowable region.
|
||||
// - If preventGhostClick was called, the region will have been removed, the click is busted.
|
||||
// - If the region is still there, the click proceeds normally. Therefore clicks on links and
|
||||
// other elements without ngTap on them work normally.
|
||||
//
|
||||
// This is an ugly, terrible hack!
|
||||
// Yeah, tell me about it. The alternatives are using the slow click events, or making our users
|
||||
// deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular
|
||||
// encapsulates this ugly logic away from the user.
|
||||
//
|
||||
// Why not just put click handlers on the element?
|
||||
// We do that too, just to be sure. If the tap event caused the DOM to change,
|
||||
// it is possible another element is now in that position. To take account for these possibly
|
||||
// distinct elements, the handlers are global and care only about coordinates.
|
||||
|
||||
// Checks if the coordinates are close enough to be within the region.
|
||||
function hit(x1, y1, x2, y2) {
|
||||
return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD;
|
||||
}
|
||||
|
||||
// Checks a list of allowable regions against a click location.
|
||||
// Returns true if the click should be allowed.
|
||||
// Splices out the allowable region from the list after it has been used.
|
||||
function checkAllowableRegions(touchCoordinates, x, y) {
|
||||
for (var i = 0; i < touchCoordinates.length; i += 2) {
|
||||
if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) {
|
||||
touchCoordinates.splice(i, i + 2);
|
||||
return true; // allowable region
|
||||
}
|
||||
}
|
||||
return false; // No allowable region; bust it.
|
||||
}
|
||||
|
||||
// Global click handler that prevents the click if it's in a bustable zone and preventGhostClick
|
||||
// was called recently.
|
||||
function onClick(event) {
|
||||
if (Date.now() - lastPreventedTime > PREVENT_DURATION) {
|
||||
return; // Too old.
|
||||
}
|
||||
|
||||
var touches = event.touches && event.touches.length ? event.touches : [event];
|
||||
var x = touches[0].clientX;
|
||||
var y = touches[0].clientY;
|
||||
// Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label
|
||||
// and on the input element). Depending on the exact browser, this second click we don't want
|
||||
// to bust has either (0,0), negative coordinates, or coordinates equal to triggering label
|
||||
// click event
|
||||
if (x < 1 && y < 1) {
|
||||
return; // offscreen
|
||||
}
|
||||
if (lastLabelClickCoordinates &&
|
||||
lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) {
|
||||
return; // input click triggered by label click
|
||||
}
|
||||
// reset label click coordinates on first subsequent click
|
||||
if (lastLabelClickCoordinates) {
|
||||
lastLabelClickCoordinates = null;
|
||||
}
|
||||
// remember label click coordinates to prevent click busting of trigger click event on input
|
||||
if (nodeName_(event.target) === 'label') {
|
||||
lastLabelClickCoordinates = [x, y];
|
||||
}
|
||||
|
||||
// Look for an allowable region containing this click.
|
||||
// If we find one, that means it was created by touchstart and not removed by
|
||||
// preventGhostClick, so we don't bust it.
|
||||
if (checkAllowableRegions(touchCoordinates, x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we didn't find an allowable region, bust the click.
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
// Blur focused form elements
|
||||
if (event.target && event.target.blur) {
|
||||
event.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Global touchstart handler that creates an allowable region for a click event.
|
||||
// This allowable region can be removed by preventGhostClick if we want to bust it.
|
||||
function onTouchStart(event) {
|
||||
var touches = event.touches && event.touches.length ? event.touches : [event];
|
||||
var x = touches[0].clientX;
|
||||
var y = touches[0].clientY;
|
||||
touchCoordinates.push(x, y);
|
||||
|
||||
$timeout(function() {
|
||||
// Remove the allowable region.
|
||||
for (var i = 0; i < touchCoordinates.length; i += 2) {
|
||||
if (touchCoordinates[i] === x && touchCoordinates[i + 1] === y) {
|
||||
touchCoordinates.splice(i, i + 2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, PREVENT_DURATION, false);
|
||||
}
|
||||
|
||||
// On the first call, attaches some event handlers. Then whenever it gets called, it creates a
|
||||
// zone around the touchstart where clicks will get busted.
|
||||
function preventGhostClick(x, y) {
|
||||
if (!touchCoordinates) {
|
||||
$rootElement[0].addEventListener('click', onClick, true);
|
||||
$rootElement[0].addEventListener('touchstart', onTouchStart, true);
|
||||
touchCoordinates = [];
|
||||
}
|
||||
|
||||
lastPreventedTime = Date.now();
|
||||
|
||||
checkAllowableRegions(touchCoordinates, x, y);
|
||||
}
|
||||
|
||||
// Actual linking function.
|
||||
return function(scope, element, attr) {
|
||||
var clickHandler = $parse(attr.ngClick),
|
||||
tapping = false,
|
||||
tapElement, // Used to blur the element after a tap.
|
||||
startTime, // Used to check if the tap was held too long.
|
||||
touchStartX,
|
||||
touchStartY;
|
||||
|
||||
function resetState() {
|
||||
tapping = false;
|
||||
element.removeClass(ACTIVE_CLASS_NAME);
|
||||
}
|
||||
|
||||
element.on('touchstart', function(event) {
|
||||
tapping = true;
|
||||
tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement.
|
||||
// Hack for Safari, which can target text nodes instead of containers.
|
||||
if (tapElement.nodeType === 3) {
|
||||
tapElement = tapElement.parentNode;
|
||||
}
|
||||
|
||||
element.addClass(ACTIVE_CLASS_NAME);
|
||||
|
||||
startTime = Date.now();
|
||||
|
||||
// Use jQuery originalEvent
|
||||
var originalEvent = event.originalEvent || event;
|
||||
var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent];
|
||||
var e = touches[0];
|
||||
touchStartX = e.clientX;
|
||||
touchStartY = e.clientY;
|
||||
});
|
||||
|
||||
element.on('touchcancel', function(event) {
|
||||
resetState();
|
||||
});
|
||||
|
||||
element.on('touchend', function(event) {
|
||||
var diff = Date.now() - startTime;
|
||||
|
||||
// Use jQuery originalEvent
|
||||
var originalEvent = event.originalEvent || event;
|
||||
var touches = (originalEvent.changedTouches && originalEvent.changedTouches.length) ?
|
||||
originalEvent.changedTouches :
|
||||
((originalEvent.touches && originalEvent.touches.length) ? originalEvent.touches : [originalEvent]);
|
||||
var e = touches[0];
|
||||
var x = e.clientX;
|
||||
var y = e.clientY;
|
||||
var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2));
|
||||
|
||||
if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) {
|
||||
// Call preventGhostClick so the clickbuster will catch the corresponding click.
|
||||
preventGhostClick(x, y);
|
||||
|
||||
// Blur the focused element (the button, probably) before firing the callback.
|
||||
// This doesn't work perfectly on Android Chrome, but seems to work elsewhere.
|
||||
// I couldn't get anything to work reliably on Android Chrome.
|
||||
if (tapElement) {
|
||||
tapElement.blur();
|
||||
}
|
||||
|
||||
if (!angular.isDefined(attr.disabled) || attr.disabled === false) {
|
||||
element.triggerHandler('click', [event]);
|
||||
}
|
||||
}
|
||||
|
||||
resetState();
|
||||
});
|
||||
|
||||
// Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
|
||||
// something else nearby.
|
||||
element.onclick = function(event) { };
|
||||
|
||||
// Actual click handler.
|
||||
// There are three different kinds of clicks, only two of which reach this point.
|
||||
// - On desktop browsers without touch events, their clicks will always come here.
|
||||
// - On mobile browsers, the simulated "fast" click will call this.
|
||||
// - But the browser's follow-up slow click will be "busted" before it reaches this handler.
|
||||
// Therefore it's safe to use this directive on both mobile and desktop.
|
||||
element.on('click', function(event, touchend) {
|
||||
scope.$apply(function() {
|
||||
clickHandler(scope, {$event: (touchend || event)});
|
||||
});
|
||||
});
|
||||
|
||||
element.on('mousedown', function(event) {
|
||||
element.addClass(ACTIVE_CLASS_NAME);
|
||||
});
|
||||
|
||||
element.on('mousemove mouseup', function(event) {
|
||||
element.removeClass(ACTIVE_CLASS_NAME);
|
||||
});
|
||||
|
||||
};
|
||||
}];
|
||||
|
||||
/* global ngTouch: false */
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngSwipeLeft
|
||||
*
|
||||
* @description
|
||||
* Specify custom behavior when an element is swiped to the left on a touchscreen device.
|
||||
* A leftward swipe is a quick, right-to-left slide of the finger.
|
||||
* Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag
|
||||
* too.
|
||||
*
|
||||
* To disable the mouse click and drag functionality, add `ng-swipe-disable-mouse` to
|
||||
* the `ng-swipe-left` or `ng-swipe-right` DOM Element.
|
||||
*
|
||||
* Requires the {@link ngTouch `ngTouch`} module to be installed.
|
||||
*
|
||||
* @element ANY
|
||||
* @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate
|
||||
* upon left swipe. (Event object is available as `$event`)
|
||||
*
|
||||
* @example
|
||||
<example module="ngSwipeLeftExample" deps="angular-touch.js" name="ng-swipe-left">
|
||||
<file name="index.html">
|
||||
<div ng-show="!showActions" ng-swipe-left="showActions = true">
|
||||
Some list content, like an email in the inbox
|
||||
</div>
|
||||
<div ng-show="showActions" ng-swipe-right="showActions = false">
|
||||
<button ng-click="reply()">Reply</button>
|
||||
<button ng-click="delete()">Delete</button>
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('ngSwipeLeftExample', ['ngTouch']);
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngSwipeRight
|
||||
*
|
||||
* @description
|
||||
* Specify custom behavior when an element is swiped to the right on a touchscreen device.
|
||||
* A rightward swipe is a quick, left-to-right slide of the finger.
|
||||
* Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag
|
||||
* too.
|
||||
*
|
||||
* Requires the {@link ngTouch `ngTouch`} module to be installed.
|
||||
*
|
||||
* @element ANY
|
||||
* @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate
|
||||
* upon right swipe. (Event object is available as `$event`)
|
||||
*
|
||||
* @example
|
||||
<example module="ngSwipeRightExample" deps="angular-touch.js" name="ng-swipe-right">
|
||||
<file name="index.html">
|
||||
<div ng-show="!showActions" ng-swipe-left="showActions = true">
|
||||
Some list content, like an email in the inbox
|
||||
</div>
|
||||
<div ng-show="showActions" ng-swipe-right="showActions = false">
|
||||
<button ng-click="reply()">Reply</button>
|
||||
<button ng-click="delete()">Delete</button>
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('ngSwipeRightExample', ['ngTouch']);
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
|
||||
function makeSwipeDirective(directiveName, direction, eventName) {
|
||||
ngTouch.directive(directiveName, ['$parse', '$swipe', function($parse, $swipe) {
|
||||
// The maximum vertical delta for a swipe should be less than 75px.
|
||||
var MAX_VERTICAL_DISTANCE = 75;
|
||||
// Vertical distance should not be more than a fraction of the horizontal distance.
|
||||
var MAX_VERTICAL_RATIO = 0.3;
|
||||
// At least a 30px lateral motion is necessary for a swipe.
|
||||
var MIN_HORIZONTAL_DISTANCE = 30;
|
||||
|
||||
return function(scope, element, attr) {
|
||||
var swipeHandler = $parse(attr[directiveName]);
|
||||
|
||||
var startCoords, valid;
|
||||
|
||||
function validSwipe(coords) {
|
||||
// Check that it's within the coordinates.
|
||||
// Absolute vertical distance must be within tolerances.
|
||||
// Horizontal distance, we take the current X - the starting X.
|
||||
// This is negative for leftward swipes and positive for rightward swipes.
|
||||
// After multiplying by the direction (-1 for left, +1 for right), legal swipes
|
||||
// (ie. same direction as the directive wants) will have a positive delta and
|
||||
// illegal ones a negative delta.
|
||||
// Therefore this delta must be positive, and larger than the minimum.
|
||||
if (!startCoords) return false;
|
||||
var deltaY = Math.abs(coords.y - startCoords.y);
|
||||
var deltaX = (coords.x - startCoords.x) * direction;
|
||||
return valid && // Short circuit for already-invalidated swipes.
|
||||
deltaY < MAX_VERTICAL_DISTANCE &&
|
||||
deltaX > 0 &&
|
||||
deltaX > MIN_HORIZONTAL_DISTANCE &&
|
||||
deltaY / deltaX < MAX_VERTICAL_RATIO;
|
||||
}
|
||||
|
||||
var pointerTypes = ['touch'];
|
||||
if (!angular.isDefined(attr['ngSwipeDisableMouse'])) {
|
||||
pointerTypes.push('mouse');
|
||||
}
|
||||
$swipe.bind(element, {
|
||||
'start': function(coords, event) {
|
||||
startCoords = coords;
|
||||
valid = true;
|
||||
},
|
||||
'cancel': function(event) {
|
||||
valid = false;
|
||||
},
|
||||
'end': function(coords, event) {
|
||||
if (validSwipe(coords)) {
|
||||
scope.$apply(function() {
|
||||
element.triggerHandler(eventName);
|
||||
swipeHandler(scope, {$event: event});
|
||||
});
|
||||
}
|
||||
}
|
||||
}, pointerTypes);
|
||||
};
|
||||
}]);
|
||||
}
|
||||
|
||||
// Left is negative X-coordinate, right is positive.
|
||||
makeSwipeDirective('ngSwipeLeft', -1, 'swipeleft');
|
||||
makeSwipeDirective('ngSwipeRight', 1, 'swiperight');
|
||||
|
||||
|
||||
|
||||
})(window, window.angular);
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,111 +1,566 @@
|
||||
/*
|
||||
jQuery UI Sortable plugin wrapper
|
||||
* angular-ui-sortable - This directive allows you to jQueryUI Sortable.
|
||||
* @version v0.17.2 - 2017-08-17
|
||||
* @link http://angular-ui.github.com
|
||||
* @license MIT
|
||||
*/
|
||||
(function(window, angular, undefined) {
|
||||
'use strict';
|
||||
/*
|
||||
jQuery UI Sortable plugin wrapper
|
||||
|
||||
@param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config
|
||||
*/
|
||||
angular.module('ui.sortable', [])
|
||||
.value('uiSortableConfig',{})
|
||||
.directive('uiSortable', [ 'uiSortableConfig',
|
||||
function(uiSortableConfig) {
|
||||
@param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config
|
||||
*/
|
||||
angular.module('ui.sortable', [])
|
||||
.value('uiSortableConfig',{
|
||||
// the default for jquery-ui sortable is "> *", we need to restrict this to
|
||||
// ng-repeat items
|
||||
// if the user uses
|
||||
items: '> [ng-repeat],> [data-ng-repeat],> [x-ng-repeat]'
|
||||
})
|
||||
.directive('uiSortable', [
|
||||
'uiSortableConfig', '$timeout', '$log',
|
||||
function(uiSortableConfig, $timeout, $log) {
|
||||
return {
|
||||
require: '?ngModel',
|
||||
require:'?ngModel',
|
||||
scope: {
|
||||
ngModel:'=',
|
||||
uiSortable:'=',
|
||||
////Expression bindings from html.
|
||||
create:'&uiSortableCreate',
|
||||
// helper:'&uiSortableHelper',
|
||||
start:'&uiSortableStart',
|
||||
activate:'&uiSortableActivate',
|
||||
// sort:'&uiSortableSort',
|
||||
// change:'&uiSortableChange',
|
||||
// over:'&uiSortableOver',
|
||||
// out:'&uiSortableOut',
|
||||
beforeStop:'&uiSortableBeforeStop',
|
||||
update:'&uiSortableUpdate',
|
||||
remove:'&uiSortableRemove',
|
||||
receive:'&uiSortableReceive',
|
||||
deactivate:'&uiSortableDeactivate',
|
||||
stop:'&uiSortableStop'
|
||||
},
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
var savedNodes;
|
||||
var helper;
|
||||
|
||||
function combineCallbacks(first,second){
|
||||
if( second && (typeof second === "function") ){
|
||||
return function(e,ui){
|
||||
first(e,ui);
|
||||
second(e,ui);
|
||||
};
|
||||
}
|
||||
return first;
|
||||
function combineCallbacks(first, second){
|
||||
var firstIsFunc = typeof first === 'function';
|
||||
var secondIsFunc = typeof second === 'function';
|
||||
if(firstIsFunc && secondIsFunc) {
|
||||
return function() {
|
||||
first.apply(this, arguments);
|
||||
second.apply(this, arguments);
|
||||
};
|
||||
} else if (secondIsFunc) {
|
||||
return second;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
function getSortableWidgetInstance(element) {
|
||||
// this is a fix to support jquery-ui prior to v1.11.x
|
||||
// otherwise we should be using `element.sortable('instance')`
|
||||
var data = element.data('ui-sortable');
|
||||
if (data && typeof data === 'object' && data.widgetFullName === 'ui-sortable') {
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function patchSortableOption(key, value) {
|
||||
if (callbacks[key]) {
|
||||
if( key === 'stop' ){
|
||||
// call apply after stop
|
||||
value = combineCallbacks(
|
||||
value, function() { scope.$apply(); });
|
||||
|
||||
value = combineCallbacks(value, afterStop);
|
||||
}
|
||||
// wrap the callback
|
||||
value = combineCallbacks(callbacks[key], value);
|
||||
} else if (wrappers[key]) {
|
||||
value = wrappers[key](value);
|
||||
}
|
||||
|
||||
// patch the options that need to have values set
|
||||
if (!value && (key === 'items' || key === 'ui-model-items')) {
|
||||
value = uiSortableConfig.items;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function patchUISortableOptions(newVal, oldVal, sortableWidgetInstance) {
|
||||
function addDummyOptionKey(value, key) {
|
||||
if (!(key in opts)) {
|
||||
// add the key in the opts object so that
|
||||
// the patch function detects and handles it
|
||||
opts[key] = null;
|
||||
}
|
||||
}
|
||||
// for this directive to work we have to attach some callbacks
|
||||
angular.forEach(callbacks, addDummyOptionKey);
|
||||
|
||||
// only initialize it in case we have to
|
||||
// update some options of the sortable
|
||||
var optsDiff = null;
|
||||
|
||||
if (oldVal) {
|
||||
// reset deleted options to default
|
||||
var defaultOptions;
|
||||
angular.forEach(oldVal, function(oldValue, key) {
|
||||
if (!newVal || !(key in newVal)) {
|
||||
if (key in directiveOpts) {
|
||||
if (key === 'ui-floating') {
|
||||
opts[key] = 'auto';
|
||||
} else {
|
||||
opts[key] = patchSortableOption(key, undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!defaultOptions) {
|
||||
defaultOptions = angular.element.ui.sortable().options;
|
||||
}
|
||||
var defaultValue = defaultOptions[key];
|
||||
defaultValue = patchSortableOption(key, defaultValue);
|
||||
|
||||
if (!optsDiff) {
|
||||
optsDiff = {};
|
||||
}
|
||||
optsDiff[key] = defaultValue;
|
||||
opts[key] = defaultValue;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// update changed options
|
||||
angular.forEach(newVal, function(value, key) {
|
||||
// if it's a custom option of the directive,
|
||||
// handle it approprietly
|
||||
if (key in directiveOpts) {
|
||||
if (key === 'ui-floating' && (value === false || value === true) && sortableWidgetInstance) {
|
||||
sortableWidgetInstance.floating = value;
|
||||
}
|
||||
|
||||
opts[key] = patchSortableOption(key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
value = patchSortableOption(key, value);
|
||||
|
||||
if (!optsDiff) {
|
||||
optsDiff = {};
|
||||
}
|
||||
optsDiff[key] = value;
|
||||
opts[key] = value;
|
||||
});
|
||||
|
||||
return optsDiff;
|
||||
}
|
||||
|
||||
function getPlaceholderElement (element) {
|
||||
var placeholder = element.sortable('option','placeholder');
|
||||
|
||||
// placeholder.element will be a function if the placeholder, has
|
||||
// been created (placeholder will be an object). If it hasn't
|
||||
// been created, either placeholder will be false if no
|
||||
// placeholder class was given or placeholder.element will be
|
||||
// undefined if a class was given (placeholder will be a string)
|
||||
if (placeholder && placeholder.element && typeof placeholder.element === 'function') {
|
||||
var result = placeholder.element();
|
||||
// workaround for jquery ui 1.9.x,
|
||||
// not returning jquery collection
|
||||
result = angular.element(result);
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPlaceholderExcludesludes (element, placeholder) {
|
||||
// exact match with the placeholder's class attribute to handle
|
||||
// the case that multiple connected sortables exist and
|
||||
// the placeholder option equals the class of sortable items
|
||||
var notCssSelector = opts['ui-model-items'].replace(/[^,]*>/g, '');
|
||||
var excludes = element.find('[class="' + placeholder.attr('class') + '"]:not(' + notCssSelector + ')');
|
||||
return excludes;
|
||||
}
|
||||
|
||||
function hasSortingHelper (element, ui) {
|
||||
var helperOption = element.sortable('option','helper');
|
||||
return helperOption === 'clone' || (typeof helperOption === 'function' && ui.item.sortable.isCustomHelperUsed());
|
||||
}
|
||||
|
||||
function getSortingHelper (element, ui/*, savedNodes*/) {
|
||||
var result = null;
|
||||
if (hasSortingHelper(element, ui) &&
|
||||
element.sortable( 'option', 'appendTo' ) === 'parent') {
|
||||
// The .ui-sortable-helper element (that's the default class name)
|
||||
result = helper;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// thanks jquery-ui
|
||||
function isFloating (item) {
|
||||
return (/left|right/).test(item.css('float')) || (/inline|table-cell/).test(item.css('display'));
|
||||
}
|
||||
|
||||
function getElementContext(elementScopes, element) {
|
||||
for (var i = 0; i < elementScopes.length; i++) {
|
||||
var c = elementScopes[i];
|
||||
if (c.element[0] === element[0]) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function afterStop(e, ui) {
|
||||
ui.item.sortable._destroy();
|
||||
}
|
||||
|
||||
// return the index of ui.item among the items
|
||||
// we can't just do ui.item.index() because there it might have siblings
|
||||
// which are not items
|
||||
function getItemIndex(item) {
|
||||
return item.parent()
|
||||
.find(opts['ui-model-items'])
|
||||
.index(item);
|
||||
}
|
||||
|
||||
var opts = {};
|
||||
|
||||
var callbacks = {
|
||||
receive: null,
|
||||
remove:null,
|
||||
start:null,
|
||||
stop:null,
|
||||
update:null
|
||||
// directive specific options
|
||||
var directiveOpts = {
|
||||
'ui-floating': undefined,
|
||||
'ui-model-items': uiSortableConfig.items
|
||||
};
|
||||
|
||||
angular.extend(opts, uiSortableConfig);
|
||||
var callbacks = {
|
||||
create: null,
|
||||
start: null,
|
||||
activate: null,
|
||||
// sort: null,
|
||||
// change: null,
|
||||
// over: null,
|
||||
// out: null,
|
||||
beforeStop: null,
|
||||
update: null,
|
||||
remove: null,
|
||||
receive: null,
|
||||
deactivate: null,
|
||||
stop: null
|
||||
};
|
||||
|
||||
if (ngModel) {
|
||||
var wrappers = {
|
||||
helper: null
|
||||
};
|
||||
|
||||
ngModel.$render = function() {
|
||||
element.sortable( "refresh" );
|
||||
};
|
||||
angular.extend(opts, directiveOpts, uiSortableConfig, scope.uiSortable);
|
||||
|
||||
if (!angular.element.fn || !angular.element.fn.jquery) {
|
||||
$log.error('ui.sortable: jQuery should be included before AngularJS!');
|
||||
return;
|
||||
}
|
||||
|
||||
function wireUp () {
|
||||
// When we add or remove elements, we need the sortable to 'refresh'
|
||||
// so it can find the new/removed elements.
|
||||
scope.$watchCollection('ngModel', function() {
|
||||
// Timeout to let ng-repeat modify the DOM
|
||||
$timeout(function() {
|
||||
// ensure that the jquery-ui-sortable widget instance
|
||||
// is still bound to the directive's element
|
||||
if (!!getSortableWidgetInstance(element)) {
|
||||
element.sortable('refresh');
|
||||
}
|
||||
}, 0, false);
|
||||
});
|
||||
|
||||
callbacks.start = function(e, ui) {
|
||||
// Save position of dragged item
|
||||
ui.item.sortable = { index: ui.item.index() };
|
||||
if (opts['ui-floating'] === 'auto') {
|
||||
// since the drag has started, the element will be
|
||||
// absolutely positioned, so we check its siblings
|
||||
var siblings = ui.item.siblings();
|
||||
var sortableWidgetInstance = getSortableWidgetInstance(angular.element(e.target));
|
||||
sortableWidgetInstance.floating = isFloating(siblings);
|
||||
}
|
||||
|
||||
// Save the starting position of dragged item
|
||||
var index = getItemIndex(ui.item);
|
||||
ui.item.sortable = {
|
||||
model: ngModel.$modelValue[index],
|
||||
index: index,
|
||||
source: element,
|
||||
sourceList: ui.item.parent(),
|
||||
sourceModel: ngModel.$modelValue,
|
||||
cancel: function () {
|
||||
ui.item.sortable._isCanceled = true;
|
||||
},
|
||||
isCanceled: function () {
|
||||
return ui.item.sortable._isCanceled;
|
||||
},
|
||||
isCustomHelperUsed: function () {
|
||||
return !!ui.item.sortable._isCustomHelperUsed;
|
||||
},
|
||||
_isCanceled: false,
|
||||
_isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed,
|
||||
_destroy: function () {
|
||||
angular.forEach(ui.item.sortable, function(value, key) {
|
||||
ui.item.sortable[key] = undefined;
|
||||
});
|
||||
},
|
||||
_connectedSortables: [],
|
||||
_getElementContext: function (element) {
|
||||
return getElementContext(this._connectedSortables, element);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
callbacks.activate = function(e, ui) {
|
||||
var isSourceContext = ui.item.sortable.source === element;
|
||||
var savedNodesOrigin = isSourceContext ?
|
||||
ui.item.sortable.sourceList :
|
||||
element;
|
||||
var elementContext = {
|
||||
element: element,
|
||||
scope: scope,
|
||||
isSourceContext: isSourceContext,
|
||||
savedNodesOrigin: savedNodesOrigin
|
||||
};
|
||||
// save the directive's scope so that it is accessible from ui.item.sortable
|
||||
ui.item.sortable._connectedSortables.push(elementContext);
|
||||
|
||||
// We need to make a copy of the current element's contents so
|
||||
// we can restore it after sortable has messed it up.
|
||||
// This is inside activate (instead of start) in order to save
|
||||
// both lists when dragging between connected lists.
|
||||
savedNodes = savedNodesOrigin.contents();
|
||||
helper = ui.helper;
|
||||
|
||||
// If this list has a placeholder (the connected lists won't),
|
||||
// don't inlcude it in saved nodes.
|
||||
var placeholder = getPlaceholderElement(element);
|
||||
if (placeholder && placeholder.length) {
|
||||
var excludes = getPlaceholderExcludesludes(element, placeholder);
|
||||
savedNodes = savedNodes.not(excludes);
|
||||
}
|
||||
};
|
||||
|
||||
callbacks.update = function(e, ui) {
|
||||
// For some reason the reference to ngModel in stop() is wrong
|
||||
ui.item.sortable.resort = ngModel;
|
||||
};
|
||||
// Save current drop position but only if this is not a second
|
||||
// update that happens when moving between lists because then
|
||||
// the value will be overwritten with the old value
|
||||
if (!ui.item.sortable.received) {
|
||||
ui.item.sortable.dropindex = getItemIndex(ui.item);
|
||||
var droptarget = ui.item.parent().closest('[ui-sortable], [data-ui-sortable], [x-ui-sortable]');
|
||||
ui.item.sortable.droptarget = droptarget;
|
||||
ui.item.sortable.droptargetList = ui.item.parent();
|
||||
|
||||
callbacks.receive = function(e, ui) {
|
||||
ui.item.sortable.relocate = true;
|
||||
// added item to array into correct position and set up flag
|
||||
ngModel.$modelValue.splice(ui.item.index(), 0, ui.item.sortable.moved);
|
||||
};
|
||||
var droptargetContext = ui.item.sortable._getElementContext(droptarget);
|
||||
ui.item.sortable.droptargetModel = droptargetContext.scope.ngModel;
|
||||
|
||||
callbacks.remove = function(e, ui) {
|
||||
// copy data into item
|
||||
if (ngModel.$modelValue.length === 1) {
|
||||
ui.item.sortable.moved = ngModel.$modelValue.splice(0, 1)[0];
|
||||
} else {
|
||||
ui.item.sortable.moved = ngModel.$modelValue.splice(ui.item.sortable.index, 1)[0];
|
||||
// Cancel the sort (let ng-repeat do the sort for us)
|
||||
// Don't cancel if this is the received list because it has
|
||||
// already been canceled in the other list, and trying to cancel
|
||||
// here will mess up the DOM.
|
||||
element.sortable('cancel');
|
||||
}
|
||||
|
||||
// Put the nodes back exactly the way they started (this is very
|
||||
// important because ng-repeat uses comment elements to delineate
|
||||
// the start and stop of repeat sections and sortable doesn't
|
||||
// respect their order (even if we cancel, the order of the
|
||||
// comments are still messed up).
|
||||
var sortingHelper = !ui.item.sortable.received && getSortingHelper(element, ui, savedNodes);
|
||||
if (sortingHelper && sortingHelper.length) {
|
||||
// Restore all the savedNodes except from the sorting helper element.
|
||||
// That way it will be garbage collected.
|
||||
savedNodes = savedNodes.not(sortingHelper);
|
||||
}
|
||||
var elementContext = ui.item.sortable._getElementContext(element);
|
||||
savedNodes.appendTo(elementContext.savedNodesOrigin);
|
||||
|
||||
// If this is the target connected list then
|
||||
// it's safe to clear the restored nodes since:
|
||||
// update is currently running and
|
||||
// stop is not called for the target list.
|
||||
if (ui.item.sortable.received) {
|
||||
savedNodes = null;
|
||||
}
|
||||
|
||||
// If received is true (an item was dropped in from another list)
|
||||
// then we add the new item to this list otherwise wait until the
|
||||
// stop event where we will know if it was a sort or item was
|
||||
// moved here from another list
|
||||
if (ui.item.sortable.received && !ui.item.sortable.isCanceled()) {
|
||||
scope.$apply(function () {
|
||||
ngModel.$modelValue.splice(ui.item.sortable.dropindex, 0,
|
||||
ui.item.sortable.moved);
|
||||
});
|
||||
scope.$emit('ui-sortable:moved', ui);
|
||||
}
|
||||
};
|
||||
|
||||
callbacks.stop = function(e, ui) {
|
||||
// digest all prepared changes
|
||||
if (ui.item.sortable.resort && !ui.item.sortable.relocate) {
|
||||
// If the received flag hasn't be set on the item, this is a
|
||||
// normal sort, if dropindex is set, the item was moved, so move
|
||||
// the items in the list.
|
||||
var wasMoved = ('dropindex' in ui.item.sortable) &&
|
||||
!ui.item.sortable.isCanceled();
|
||||
|
||||
// Fetch saved and current position of dropped element
|
||||
var end, start;
|
||||
start = ui.item.sortable.index;
|
||||
end = ui.item.index();
|
||||
if (wasMoved && !ui.item.sortable.received) {
|
||||
|
||||
// Reorder array and apply change to scope
|
||||
ui.item.sortable.resort.$modelValue.splice(end, 0, ui.item.sortable.resort.$modelValue.splice(start, 1)[0]);
|
||||
scope.$apply(function () {
|
||||
ngModel.$modelValue.splice(
|
||||
ui.item.sortable.dropindex, 0,
|
||||
ngModel.$modelValue.splice(ui.item.sortable.index, 1)[0]);
|
||||
});
|
||||
scope.$emit('ui-sortable:moved', ui);
|
||||
} else if (!wasMoved &&
|
||||
!angular.equals(element.contents().toArray(), savedNodes.toArray())) {
|
||||
// if the item was not moved
|
||||
// and the DOM element order has changed,
|
||||
// then restore the elements
|
||||
// so that the ngRepeat's comment are correct.
|
||||
|
||||
var sortingHelper = getSortingHelper(element, ui, savedNodes);
|
||||
if (sortingHelper && sortingHelper.length) {
|
||||
// Restore all the savedNodes except from the sorting helper element.
|
||||
// That way it will be garbage collected.
|
||||
savedNodes = savedNodes.not(sortingHelper);
|
||||
}
|
||||
var elementContext = ui.item.sortable._getElementContext(element);
|
||||
savedNodes.appendTo(elementContext.savedNodesOrigin);
|
||||
}
|
||||
if (ui.item.sortable.resort || ui.item.sortable.relocate) {
|
||||
scope.$apply();
|
||||
|
||||
// It's now safe to clear the savedNodes and helper
|
||||
// since stop is the last callback.
|
||||
savedNodes = null;
|
||||
helper = null;
|
||||
};
|
||||
|
||||
callbacks.receive = function(e, ui) {
|
||||
// An item was dropped here from another list, set a flag on the
|
||||
// item.
|
||||
ui.item.sortable.received = true;
|
||||
};
|
||||
|
||||
callbacks.remove = function(e, ui) {
|
||||
// Workaround for a problem observed in nested connected lists.
|
||||
// There should be an 'update' event before 'remove' when moving
|
||||
// elements. If the event did not fire, cancel sorting.
|
||||
if (!('dropindex' in ui.item.sortable)) {
|
||||
element.sortable('cancel');
|
||||
ui.item.sortable.cancel();
|
||||
}
|
||||
|
||||
// Remove the item from this list's model and copy data into item,
|
||||
// so the next list can retrive it
|
||||
if (!ui.item.sortable.isCanceled()) {
|
||||
scope.$apply(function () {
|
||||
ui.item.sortable.moved = ngModel.$modelValue.splice(
|
||||
ui.item.sortable.index, 1)[0];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
scope.$watch(attrs.uiSortable, function(newVal, oldVal){
|
||||
angular.forEach(newVal, function(value, key){
|
||||
|
||||
if( callbacks[key] ){
|
||||
// wrap the callback
|
||||
value = combineCallbacks( callbacks[key], value );
|
||||
}
|
||||
|
||||
element.sortable('option', key, value);
|
||||
// setup attribute handlers
|
||||
angular.forEach(callbacks, function(value, key) {
|
||||
callbacks[key] = combineCallbacks(callbacks[key],
|
||||
function () {
|
||||
var attrHandler = scope[key];
|
||||
var attrHandlerFn;
|
||||
if (typeof attrHandler === 'function' &&
|
||||
('uiSortable' + key.substring(0,1).toUpperCase() + key.substring(1)).length &&
|
||||
typeof (attrHandlerFn = attrHandler()) === 'function') {
|
||||
attrHandlerFn.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
}, true);
|
||||
|
||||
angular.forEach(callbacks, function(value, key ){
|
||||
|
||||
opts[key] = combineCallbacks(value, opts[key]);
|
||||
});
|
||||
|
||||
// Create sortable
|
||||
|
||||
element.sortable(opts);
|
||||
wrappers.helper = function (inner) {
|
||||
if (inner && typeof inner === 'function') {
|
||||
return function (e, item) {
|
||||
var oldItemSortable = item.sortable;
|
||||
var index = getItemIndex(item);
|
||||
item.sortable = {
|
||||
model: ngModel.$modelValue[index],
|
||||
index: index,
|
||||
source: element,
|
||||
sourceList: item.parent(),
|
||||
sourceModel: ngModel.$modelValue,
|
||||
_restore: function () {
|
||||
angular.forEach(item.sortable, function(value, key) {
|
||||
item.sortable[key] = undefined;
|
||||
});
|
||||
|
||||
item.sortable = oldItemSortable;
|
||||
}
|
||||
};
|
||||
|
||||
var innerResult = inner.apply(this, arguments);
|
||||
item.sortable._restore();
|
||||
item.sortable._isCustomHelperUsed = item !== innerResult;
|
||||
return innerResult;
|
||||
};
|
||||
}
|
||||
return inner;
|
||||
};
|
||||
|
||||
scope.$watchCollection('uiSortable', function(newVal, oldVal) {
|
||||
// ensure that the jquery-ui-sortable widget instance
|
||||
// is still bound to the directive's element
|
||||
var sortableWidgetInstance = getSortableWidgetInstance(element);
|
||||
if (!!sortableWidgetInstance) {
|
||||
var optsDiff = patchUISortableOptions(newVal, oldVal, sortableWidgetInstance);
|
||||
|
||||
if (optsDiff) {
|
||||
element.sortable('option', optsDiff);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
patchUISortableOptions(opts);
|
||||
}
|
||||
|
||||
function init () {
|
||||
if (ngModel) {
|
||||
wireUp();
|
||||
} else {
|
||||
$log.info('ui.sortable: ngModel not provided!', element);
|
||||
}
|
||||
|
||||
// Create sortable
|
||||
element.sortable(opts);
|
||||
}
|
||||
|
||||
function initIfEnabled () {
|
||||
if (scope.uiSortable && scope.uiSortable.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
// Stop Watcher
|
||||
initIfEnabled.cancelWatcher();
|
||||
initIfEnabled.cancelWatcher = angular.noop;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
initIfEnabled.cancelWatcher = angular.noop;
|
||||
|
||||
if (!initIfEnabled()) {
|
||||
initIfEnabled.cancelWatcher = scope.$watch('uiSortable.disabled', initIfEnabled);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
]);
|
||||
|
||||
})(window, window.angular);
|
File diff suppressed because one or more lines are too long
209
docs-web/src/main/webapp/src/lib/angular.ui-validate.js
Normal file
209
docs-web/src/main/webapp/src/lib/angular.ui-validate.js
Normal file
@ -0,0 +1,209 @@
|
||||
/*!
|
||||
* angular-ui-validate
|
||||
* https://github.com/angular-ui/ui-validate
|
||||
* Version: 1.2.3 - 2017-05-18T08:04:45.422Z
|
||||
* License: MIT
|
||||
*/
|
||||
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
/**
|
||||
* General-purpose validator for ngModel.
|
||||
* angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using
|
||||
* an arbitrary validation function requires creation of custom directives for interact with angular's validation mechanism.
|
||||
* The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s).
|
||||
* A validator function will trigger validation on both model and input changes.
|
||||
*
|
||||
* This utility bring 'ui-validate' directives to handle regular validations and 'ui-validate-async' for asynchronous validations.
|
||||
*
|
||||
* @example <input ui-validate=" 'myValidatorFunction($value)' ">
|
||||
* @example <input ui-validate="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }">
|
||||
* @example <input ui-validate="{ foo : '$value > anotherModel' }" ui-validate-watch=" 'anotherModel' ">
|
||||
* @example <input ui-validate="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }" ui-validate-watch=" { foo : 'anotherModel' } ">
|
||||
* @example <input ui-validate-async=" 'myAsyncValidatorFunction($value)' ">
|
||||
* @example <input ui-validate-async="{ foo: 'myAsyncValidatorFunction($value, anotherModel)' }" ui-validate-watch=" 'anotherModel' ">
|
||||
*
|
||||
* @param ui-validate {string|object literal} If strings is passed it should be a scope's function to be used as a validator.
|
||||
* If an object literal is passed a key denotes a validation error key while a value should be a validator function.
|
||||
* In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result.
|
||||
* It is possible for a validator function to return a promise, however promises are better handled by ui-validate-async.
|
||||
*
|
||||
* @param ui-validate-async {string|object literal} If strings is passed it should be a scope's function to be used as a validator.
|
||||
* If an object literal is passed a key denotes a validation error key while a value should be a validator function.
|
||||
* Async validator function should take a value to validate as its argument and should return a promise that resolves if valid and reject if not,
|
||||
* indicating a validation result.
|
||||
* ui-validate-async supports non asyncronous validators. They are wrapped into a promise. Although is recomented to use ui-validate instead, since
|
||||
* all validations declared in ui-validate-async are registered un ngModel.$asyncValidators that runs after ngModel.$validators if and only if
|
||||
* all validators in ngModel.$validators reports as valid.
|
||||
*/
|
||||
angular.module('ui.validate',[])
|
||||
.directive('uiValidate', ['$$uiValidateApplyWatch', '$$uiValidateApplyWatchCollection', function ($$uiValidateApplyWatch, $$uiValidateApplyWatchCollection) {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attrs, ctrl) {
|
||||
var validateFn, validateExpr = scope.$eval(attrs.uiValidate);
|
||||
|
||||
if (!validateExpr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (angular.isString(validateExpr)) {
|
||||
validateExpr = {
|
||||
validator: validateExpr
|
||||
};
|
||||
}
|
||||
|
||||
angular.forEach(validateExpr, function(exprssn, key) {
|
||||
validateFn = function(modelValue, viewValue) {
|
||||
// $value is left for retrocompatibility
|
||||
var expression = scope.$eval(exprssn, {
|
||||
'$value': modelValue,
|
||||
'$modelValue': modelValue,
|
||||
'$viewValue': viewValue,
|
||||
'$name': ctrl.$name
|
||||
});
|
||||
// Keep support for promises for retrocompatibility
|
||||
if (angular.isObject(expression) && angular.isFunction(expression.then)) {
|
||||
expression.then(function() {
|
||||
ctrl.$setValidity(key, true);
|
||||
}, function() {
|
||||
ctrl.$setValidity(key, false);
|
||||
});
|
||||
// Return as valid for now. Validity is updated when promise resolves.
|
||||
return true;
|
||||
} else {
|
||||
return !!expression; // Transform 'undefined' to false (to avoid corrupting the NgModelController and the FormController)
|
||||
}
|
||||
};
|
||||
ctrl.$validators[key] = validateFn;
|
||||
});
|
||||
|
||||
// Support for ui-validate-watch
|
||||
if (attrs.uiValidateWatch) {
|
||||
$$uiValidateApplyWatch(scope, ctrl, scope.$eval(attrs.uiValidateWatch), attrs.uiValidateWatchObjectEquality);
|
||||
}
|
||||
if (attrs.uiValidateWatchCollection) {
|
||||
$$uiValidateApplyWatchCollection(scope, ctrl, scope.$eval(attrs.uiValidateWatchCollection));
|
||||
}
|
||||
}
|
||||
};
|
||||
}])
|
||||
.directive('uiValidateAsync', ['$$uiValidateApplyWatch', '$$uiValidateApplyWatchCollection', '$timeout', '$q', function ($$uiValidateApplyWatch, $$uiValidateApplyWatchCollection, $timeout, $q) {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function (scope, elm, attrs, ctrl) {
|
||||
var validateFn, validateExpr = scope.$eval(attrs.uiValidateAsync);
|
||||
|
||||
if (!validateExpr){ return;}
|
||||
|
||||
if (angular.isString(validateExpr)) {
|
||||
validateExpr = { validatorAsync: validateExpr };
|
||||
}
|
||||
|
||||
angular.forEach(validateExpr, function (exprssn, key) {
|
||||
validateFn = function(modelValue, viewValue) {
|
||||
// $value is left for ease of use
|
||||
var expression = scope.$eval(exprssn, {
|
||||
'$value': modelValue,
|
||||
'$modelValue': modelValue,
|
||||
'$viewValue': viewValue,
|
||||
'$name': ctrl.$name
|
||||
});
|
||||
// Check if it's a promise
|
||||
if (angular.isObject(expression) && angular.isFunction(expression.then)) {
|
||||
return expression;
|
||||
// Support for validate non-async validators
|
||||
} else {
|
||||
return $q(function(resolve, reject) {
|
||||
setTimeout(function() {
|
||||
if (expression) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
};
|
||||
ctrl.$asyncValidators[key] = validateFn;
|
||||
});
|
||||
|
||||
// Support for ui-validate-watch
|
||||
if (attrs.uiValidateWatch){
|
||||
$$uiValidateApplyWatch( scope, ctrl, scope.$eval(attrs.uiValidateWatch), attrs.uiValidateWatchObjectEquality);
|
||||
}
|
||||
if (attrs.uiValidateWatchCollection) {
|
||||
$$uiValidateApplyWatchCollection(scope, ctrl, scope.$eval(attrs.uiValidateWatchCollection));
|
||||
}
|
||||
}
|
||||
};
|
||||
}])
|
||||
.service('$$uiValidateApplyWatch', function () {
|
||||
return function (scope, ctrl, watch, objectEquality) {
|
||||
var watchCallback = function () {
|
||||
ctrl.$validate();
|
||||
};
|
||||
|
||||
//string - update all validators on expression change
|
||||
if (angular.isString(watch)) {
|
||||
scope.$watch(watch, watchCallback, objectEquality);
|
||||
//array - update all validators on change of any expression
|
||||
} else if (angular.isArray(watch)) {
|
||||
angular.forEach(watch, function (expression) {
|
||||
scope.$watch(expression, watchCallback, objectEquality);
|
||||
});
|
||||
//object - update appropriate validator
|
||||
} else if (angular.isObject(watch)) {
|
||||
angular.forEach(watch, function (expression/*, validatorKey*/) {
|
||||
//value is string - look after one expression
|
||||
if (angular.isString(expression)) {
|
||||
scope.$watch(expression, watchCallback, objectEquality);
|
||||
}
|
||||
//value is array - look after all expressions in array
|
||||
if (angular.isArray(expression)) {
|
||||
angular.forEach(expression, function (intExpression) {
|
||||
scope.$watch(intExpression, watchCallback, objectEquality);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
.service('$$uiValidateApplyWatchCollection', function () {
|
||||
return function (scope, ctrl, watch) {
|
||||
var watchCallback = function () {
|
||||
ctrl.$validate();
|
||||
};
|
||||
|
||||
//string - update all validators on expression change
|
||||
if (angular.isString(watch)) {
|
||||
scope.$watchCollection(watch, watchCallback);
|
||||
//array - update all validators on change of any expression
|
||||
} else if (angular.isArray(watch)) {
|
||||
angular.forEach(watch, function (expression) {
|
||||
scope.$watchCollection(expression, watchCallback);
|
||||
});
|
||||
//object - update appropriate validator
|
||||
} else if (angular.isObject(watch)) {
|
||||
angular.forEach(watch, function (expression/*, validatorKey*/) {
|
||||
//value is string - look after one expression
|
||||
if (angular.isString(expression)) {
|
||||
scope.$watchCollection(expression, watchCallback);
|
||||
}
|
||||
//value is array - look after all expressions in array
|
||||
if (angular.isArray(expression)) {
|
||||
angular.forEach(expression, function (intExpression) {
|
||||
scope.$watchCollection(intExpression, watchCallback);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
}());
|
10258
docs-web/src/main/webapp/src/lib/jquery.js
vendored
10258
docs-web/src/main/webapp/src/lib/jquery.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -27,7 +27,7 @@
|
||||
<div class="col-sm-4">
|
||||
<input required ng-maxlength="50" class="form-control" type="text" id="inputTarget"
|
||||
ng-attr-placeholder="{{ 'directive.acledit.search_user_group' | translate }}" name="target" ng-model="acl.target" autocomplete="off"
|
||||
typeahead="target as target.name for target in getTargetAclTypeahead($viewValue) | filter: $viewValue"
|
||||
uib-typeahead="target as target.name for target in getTargetAclTypeahead($viewValue)"
|
||||
typeahead-template-url="partial/docs/directive.typeahead.acl.html"
|
||||
typeahead-wait-ms="200" />
|
||||
</div>
|
||||
|
@ -7,6 +7,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
<input class="form-control" type="text" id="{{ ref }}" ng-attr-placeholder="{{ 'directive.selectrelation.typeahead' | translate }}" ng-model="input" ng-disabled="ngDisabled"
|
||||
autocomplete="off" typeahead="document.title for document in getDocumentTypeahead($viewValue)"
|
||||
autocomplete="off" uib-typeahead="document.title for document in getDocumentTypeahead($viewValue)"
|
||||
typeahead-wait-ms="200" typeahead-on-select="addRelation($item)" />
|
||||
</div>
|
@ -7,5 +7,5 @@
|
||||
</li>
|
||||
</ul>
|
||||
<input class="form-control" type="text" id="{{ ref }}" ng-attr-placeholder="{{ 'directive.selecttag.typeahead' | translate }}" ng-model="input" ng-disabled="ngDisabled"
|
||||
autocomplete="off" typeahead="tag.name for tag in allTags | filter: $viewValue" typeahead-on-select="addTag()" />
|
||||
autocomplete="off" uib-typeahead="tag.name for tag in allTags | filter: $viewValue" typeahead-on-select="addTag()" />
|
||||
</div>
|
@ -3,8 +3,8 @@
|
||||
<div>
|
||||
<div class="well drop-zone">
|
||||
<h3><span class="glyphicon glyphicon-cloud-upload"></span> {{ 'document.default.quick_upload' | translate }}</h3>
|
||||
<div class="row upload-zone" ng-model="dropFiles" ng-file-drop drag-over-class="bg-success"
|
||||
ng-multiple="true" allow-dir="false" ng-file-change="fileDropped($files, $event, $rejectedFiles)">
|
||||
<div class="row upload-zone" ng-model="dropFiles" ngf-drop="fileDropped($files)"
|
||||
ngf-drag-over-class="'bg-success'" ngf-multiple="true" ngf-allow-dir="false">
|
||||
<div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center" ng-repeat="file in files">
|
||||
<div class="thumbnail" ng-class="{ 'thumbnail-checked': file.checked }" ng-if="file.id">
|
||||
<a ng-click="openFile(file)">
|
||||
@ -27,7 +27,7 @@
|
||||
{{ file.status }}
|
||||
</p>
|
||||
<div class="caption">
|
||||
<progressbar value="file.progress" class="progress-info active"></progressbar>
|
||||
<uib-progressbar value="file.progress" class="progress-info active"></uib-progressbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,10 +39,10 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<div class="text-center">
|
||||
<button class="btn btn-primary" ng-file-select
|
||||
ng-file-change="fileDropped($files, $event)"
|
||||
<button class="btn btn-primary" ngf-select
|
||||
ngf-change="fileDropped($files, $event)"
|
||||
input-file-multiple="multiple"
|
||||
ng-multiple="true">
|
||||
ngf-multiple="true">
|
||||
{{ 'document.default.add_files' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -3,10 +3,10 @@
|
||||
<div ng-show="document || !isEdit()">
|
||||
<div class="row" ng-show="fileIsUploading">
|
||||
<h4>{{ 'document.edit.uploading_files' | translate }}</h4>
|
||||
<div class="col-md-6"><progressbar value="fileProgress" class="progress-info active"></progressbar></div>
|
||||
<div class="col-md-6"><uib-progressbar value="fileProgress" class="progress-info active"></uib-progressbar></div>
|
||||
</div>
|
||||
|
||||
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
|
||||
<div uib-alert ng-class="'alert-' + alert.type" ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</div>
|
||||
|
||||
<form name="documentForm" class="form-horizontal">
|
||||
<div class="pull-right btn-group" ng-init="form = documentForm">
|
||||
@ -26,7 +26,7 @@
|
||||
<div class="col-sm-10">
|
||||
<input required ng-maxlength="100" class="form-control" type="text" id="inputTitle"
|
||||
ng-attr-placeholder="{{ 'document.edit.title_placeholder' | translate }}" name="title" ng-model="document.title" autocomplete="off"
|
||||
typeahead="document for document in getTitleTypeahead($viewValue)"
|
||||
uib-typeahead="document for document in getTitleTypeahead($viewValue)"
|
||||
typeahead-wait-ms="200" ng-disabled="fileIsUploading" />
|
||||
</div>
|
||||
</div>
|
||||
@ -40,8 +40,8 @@
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="inputCreateDate">{{ 'document.creation_date' | translate }}</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" id="inputCreateDate" ng-readonly="true" datepicker-popup="yyyy-MM-dd" class="form-control"
|
||||
ng-model="document.create_date" starting-day="1" show-weeks="false" ng-disabled="fileIsUploading" />
|
||||
<input type="text" id="inputCreateDate" ng-readonly="true" uib-datepicker-popup="yyyy-MM-dd" class="form-control"
|
||||
ng-model="document.create_date" datepicker-options="{ startingDay:1, showWeeks: false }" ng-click="datepickerOpened = true" is-open="datepickerOpened" ng-disabled="fileIsUploading" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -70,11 +70,11 @@
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="inputFiles">{{ 'document.edit.new_files' | translate }}</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="file" ng-file-select class="form-control" id="inputFiles" multiple="multiple" ng-model="newFiles" ng-disabled="fileIsUploading"></input>
|
||||
<input type="file" ngf-select class="form-control" id="inputFiles" ngf-multiple="true" ng-model="newFiles" ng-disabled="fileIsUploading"></input>
|
||||
</div>
|
||||
<div class="col-sm-4" ng-if="orphanFiles.length > 0">
|
||||
+ {{ orphanFiles.length }} file{{ orphanFiles.length > 1 ? 's' : '' }}
|
||||
{{ 'document.edit.orphan_files' | translate: '{ count: orphanFiles.length }' }}
|
||||
<div class="col-sm-4" ng-if="orphanFiles.length > 0"
|
||||
translate="document.edit.orphan_files"
|
||||
translate-values="{ count: orphanFiles.length }">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -6,8 +6,8 @@
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="dropdown col-md-2 tag-tree-dropdown" dropdown>
|
||||
<button class="btn btn-block btn-default" dropdown-toggle ng-disabled="disabled">
|
||||
<div class="dropdown col-md-2 tag-tree-dropdown" uib-dropdown>
|
||||
<button class="btn btn-block btn-default" uib-dropdown-toggle ng-disabled="disabled">
|
||||
{{ 'document.tags' | translate }} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu tag-tree">
|
||||
@ -20,14 +20,14 @@
|
||||
<span class="input-group-addon">
|
||||
<span class="glyphicon glyphicon glyphicon-info-sign"
|
||||
tooltip-placement="bottom"
|
||||
tooltip-html-unsafe="before:2012-05<br/>
|
||||
uib-tooltip-html="'before:2012-05<br/>
|
||||
after:2012-05<br/>
|
||||
at:2012-05<br/>
|
||||
tag:car<br/>
|
||||
full:led<br/>
|
||||
shared:yes<br/>
|
||||
lang:fra<br/>
|
||||
by:user1"></span>
|
||||
by:user1'"></span>
|
||||
</span>
|
||||
<input type="search" class="form-control" ng-attr-placeholder="{{ 'document.search' | translate }}" ng-model="search" />
|
||||
<span class="input-group-addon">
|
||||
@ -77,12 +77,13 @@
|
||||
</table>
|
||||
|
||||
<div class="text-center pagination-box">
|
||||
<pagination
|
||||
<ul uib-pagination
|
||||
ng-if="paginationShown"
|
||||
previous-text="{{ 'pagination.previous' | translate }}"
|
||||
next-text="{{ 'pagination.next' | translate }}"
|
||||
first-text="{{ 'pagination.first' | translate }}"
|
||||
last-text="{{ 'pagination.last' | translate }}"
|
||||
total-items="totalDocuments" items-per-page="limit" max-size="5" page="currentPage"></pagination>
|
||||
total-items="totalDocuments" items-per-page="limit" max-size="5" ng-model="currentPage"></ul>
|
||||
<label class="sr-only" for="pagesizeSelect">{{ 'document.page_size' | translate }}</label>
|
||||
<select ng-model="limit" id="pagesizeSelect" class="form-control">
|
||||
<option value="10">{{ 'document.page_size_10' | translate }}</option>
|
||||
@ -91,14 +92,13 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pull-left" title="{{ 'document.upgrade_quota' | translate }}">
|
||||
{{ 'document.quota' | translate: '{ current: userInfo.storage_current / 1000000, percent: userInfo.storage_current / userInfo.storage_quota * 100, total: userInfo.storage_quota / 1000000 }' }}
|
||||
<div class="pull-left" title="{{ 'document.upgrade_quota' | translate }}"
|
||||
translate="document.quota"
|
||||
translate-values="{ current: userInfo.storage_current / 1000000, percent: userInfo.storage_current / userInfo.storage_quota * 100, total: userInfo.storage_quota / 1000000 }">
|
||||
</div>
|
||||
|
||||
<div class="text-right" >
|
||||
<span ng-if="totalDocuments">
|
||||
{{ 'document.count' | translate: '{ count: totalDocuments }' }}
|
||||
</span>
|
||||
<span ng-if="totalDocuments" translate="document.count" translate-values="{ count: totalDocuments }"></span>
|
||||
<span ng-if="!totalDocuments"> </span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,8 +38,7 @@
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div ng-file-drop drag-over-class="bg-success" ng-multiple="true" allow-dir="false" ng-model="dropFiles"
|
||||
ng-file-change="fileDropped($files, $event, $rejectedFiles)">
|
||||
<div ngf-drop="fileDropped($files)" ngf-drag-over-class="'bg-success'" ngf-multiple="true" ngf-allow-dir="false" ng-model="dropFiles">
|
||||
<div class="row upload-zone" ui-sortable="fileSortableOptions" ng-model="files">
|
||||
<div class="col-xs-6 col-sm-4 col-md-4 col-lg-3 text-center" ng-repeat="file in files">
|
||||
<div class="thumbnail" ng-if="file.id">
|
||||
@ -63,7 +62,7 @@
|
||||
{{ file.status }}
|
||||
</p>
|
||||
<div class="caption">
|
||||
<progressbar value="file.progress" class="progress-info active"></progressbar>
|
||||
<uib-progressbar value="file.progress" class="progress-info active"></uib-progressbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,10 +74,10 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button class="btn btn-primary" ng-file-select
|
||||
ng-file-change="fileDropped($files, $event)"
|
||||
<button class="btn btn-primary" ngf-select
|
||||
ngf-change="fileDropped($files, $event)"
|
||||
input-file-multiple="multiple"
|
||||
ng-multiple="true">
|
||||
ngf-multiple="true">
|
||||
{{ 'document.view.content.add_files' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -13,8 +13,8 @@
|
||||
|
||||
<div ng-show="document">
|
||||
<div class="pull-right">
|
||||
<div class="dropdown btn-export" dropdown ng-class="{ 'btn-group': document.writable }">
|
||||
<button class="btn btn-default" dropdown-toggle>
|
||||
<div class="dropdown" uib-dropdown ng-class="{ 'btn-group': document.writable }">
|
||||
<button class="btn btn-default" uib-dropdown-toggle>
|
||||
<span class="glyphicon glyphicon-export"></span>
|
||||
{{ 'export' | translate }}
|
||||
<span class="caret"></span>
|
||||
|
@ -14,8 +14,9 @@
|
||||
<h4>{{ 'group.profile.related_links' | translate }}</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<a ng-href="#/settings/group/edit/{{ group.name }}">
|
||||
{{ 'group.profile.edit_group' | translate: '{ name: group.name }' }}
|
||||
<a ng-href="#/settings/group/edit/{{ group.name }}"
|
||||
translate="group.profile.edit_group"
|
||||
translate-values="{ name: group.name }">
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -33,4 +33,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
|
||||
<div uib-alert ng-repeat="alert in alerts" ng-class="'alert-' + alert.type" close="closeAlert($index)">{{ alert.msg }}</div>
|
@ -40,7 +40,7 @@
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="inputLogo">{{ 'settings.config.logo' | translate }}</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="file" ng-file-select accept="image/gif,image/png,image/jpg,image/jpeg"
|
||||
<input type="file" ngf-select ngf-accept="'image/gif,image/png,image/jpg,image/jpeg'"
|
||||
class="form-control" id="inputLogo" ng-model="logo" ng-disabled="sendingImage" />
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
@ -54,7 +54,7 @@
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="inputBackground">{{ 'settings.config.background_image' | translate }}</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="file" ng-file-select accept="image/gif,image/png,image/jpg,image/jpeg"
|
||||
<input type="file" ngf-select ngf-accept="'image/gif,image/png,image/jpg,image/jpeg'"
|
||||
class="form-control" id="inputBackground" ng-model="background" ng-disabled="sendingImage" />
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
|
@ -25,7 +25,7 @@
|
||||
<div class="col-sm-7">
|
||||
<input name="parent" type="text" id="inputParent" class="form-control" autocomplete="off"
|
||||
ng-attr-placeholder="{{ 'settings.group.edit.search_group' | translate }}" ng-model="group.parent"
|
||||
typeahead="group for group in getGroupTypeahead($viewValue)"
|
||||
uib-typeahead="group for group in getGroupTypeahead($viewValue)"
|
||||
typeahead-wait-ms="200" typeahead-editable="false" />
|
||||
</div>
|
||||
</div>
|
||||
@ -50,7 +50,7 @@
|
||||
<div class="col-sm-7">
|
||||
<input name="member" type="text" id="inputMember" class="form-control" ng-model="member"
|
||||
ng-attr-placeholder="{{ 'settings.group.edit.search_user' | translate }}"
|
||||
typeahead="user for user in getUserTypeahead($viewValue)" typeahead-on-select="addMember($item)"
|
||||
uib-typeahead="user for user in getUserTypeahead($viewValue)" typeahead-on-select="addMember($item)"
|
||||
typeahead-wait-ms="200" typeahead-editable="false" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,20 +3,20 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>{{ 'settings.menu_personal_settings' | translate }}</strong></div>
|
||||
<ul class="list-group">
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/account" href="#/settings/account">{{ 'settings.menu_user_account' | translate }}</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/security" href="#/settings/security">{{ 'settings.menu_two_factor_auth' | translate }}</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/session" href="#/settings/session">{{ 'settings.menu_opened_sessions' | translate }}</a>
|
||||
<a class="list-group-item" ui-sref-active="{ active: 'settings.account.**' }" href="#/settings/account">{{ 'settings.menu_user_account' | translate }}</a>
|
||||
<a class="list-group-item" ui-sref-active="{ active: 'settings.security.**' }" href="#/settings/security">{{ 'settings.menu_two_factor_auth' | translate }}</a>
|
||||
<a class="list-group-item" ui-sref-active="{ active: 'settings.session.**' }" href="#/settings/session">{{ 'settings.menu_opened_sessions' | translate }}</a>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default" ng-show="isAdmin">
|
||||
<div class="panel-heading"><strong>{{ 'settings.menu_general_settings' | translate }}</strong></div>
|
||||
<ul class="list-group">
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/user.*" href="#/settings/user">{{ 'settings.menu_users' | translate }}</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/group.*" href="#/settings/group">{{ 'settings.menu_groups' | translate }}</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/vocabulary.*" href="#/settings/vocabulary">{{ 'settings.menu_vocabularies' | translate }}</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/config" href="#/settings/config">{{ 'settings.menu_configuration' | translate }}</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/log" href="#/settings/log">{{ 'settings.menu_server_logs' | translate }}</a>
|
||||
<a class="list-group-item" ui-sref-active="{ active: 'settings.user.**' }" href="#/settings/user">{{ 'settings.menu_users' | translate }}</a>
|
||||
<a class="list-group-item" ui-sref-active="{ active: 'settings.group.**' }" href="#/settings/group">{{ 'settings.menu_groups' | translate }}</a>
|
||||
<a class="list-group-item" ui-sref-active="{ active: 'settings.vocabulary.**' }" href="#/settings/vocabulary">{{ 'settings.menu_vocabularies' | translate }}</a>
|
||||
<a class="list-group-item" ui-sref-active="{ active: 'settings.config.**' }" href="#/settings/config">{{ 'settings.menu_configuration' | translate }}</a>
|
||||
<a class="list-group-item" ui-sref-active="{ active: 'settings.log.**' }" href="#/settings/log">{{ 'settings.menu_server_logs' | translate }}</a>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,9 +12,9 @@
|
||||
<h4>{{ 'user.profile.quota_used' | translate }}</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="progress" title="{{ 'user.profile.percent_used' | translate: '{ percent: user.storage_current / user.storage_quota * 100 }' }}">
|
||||
<div class="progress" translate-attr="{ title: 'user.profile.percent_used' }" translate-values="{ percent: user.storage_current / user.storage_quota * 100 }">
|
||||
<div class="progress-bar" ng-style="{ 'width': (user.storage_current / user.storage_quota * 100) + '%' }">
|
||||
<span class="sr-only">{{ 'user.profile.percent_used' | translate: '{ percent: user.storage_current / user.storage_quota * 100 }' }}</span>
|
||||
<span class="sr-only" translate="user.profile.percent_used" translate-values="{ percent: user.storage_current / user.storage_quota * 100 }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -23,13 +23,15 @@
|
||||
<h4>{{ 'user.profile.related_links' | translate }}</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<a ng-href="#/document/search/by:{{ user.username }}">
|
||||
{{ 'user.profile.document_created' | translate: '{ username: user.username }' }}
|
||||
<a ng-href="#/document/search/by:{{ user.username }}"
|
||||
translate="user.profile.document_created"
|
||||
translate-values="{ username: user.username }">
|
||||
</a>
|
||||
</li>
|
||||
<li ng-if="userInfo.base_functions.indexOf('ADMIN') != -1">
|
||||
<a ng-href="#/settings/user/edit/{{ user.username }}">
|
||||
{{ 'user.profile.edit_user' | translate: '{ username: user.username }' }}
|
||||
<a ng-href="#/settings/user/edit/{{ user.username }}"
|
||||
translate="user.profile.edit_user"
|
||||
translate-values="{ username: user.username }">
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
@ -1,38 +1,40 @@
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<div class="pull-right">
|
||||
<div class="dropdown" dropdown>
|
||||
<button class="btn btn-default" dropdown-toggle>
|
||||
<span class="glyphicon glyphicon-export"></span>
|
||||
Export
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a ng-href="../api/file/zip?id={{ document.id }}&share={{ $stateParams.shareId }}" title="Download all files">
|
||||
<span class="glyphicon glyphicon glyphicon-compressed"></span>
|
||||
Download files
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="exportPdf()" title="Export document to PDF" class="pointer">
|
||||
<span class="glyphicon glyphicon glyphicon-save-file"></span>
|
||||
Export to PDF
|
||||
</a>
|
||||
</li>
|
||||
<div>
|
||||
<div class="pull-right">
|
||||
<div class="dropdown" uib-dropdown>
|
||||
<button class="btn btn-default" uib-dropdown-toggle>
|
||||
<span class="glyphicon glyphicon-export"></span>
|
||||
Export
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a ng-href="../api/file/zip?id={{ document.id }}&share={{ $stateParams.shareId }}" title="Download all files">
|
||||
<span class="glyphicon glyphicon glyphicon-compressed"></span>
|
||||
Download files
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="exportPdf()" title="Export document to PDF" class="pointer">
|
||||
<span class="glyphicon glyphicon glyphicon-save-file"></span>
|
||||
Export to PDF
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{ document.title }} <small>{{ document.create_date | date: 'yyyy-MM-dd' }}</small>
|
||||
</h1>
|
||||
<ul class="list-inline">
|
||||
<li ng-repeat="tag in document.tags"><span class="label label-info" ng-style="{ 'background': tag.color }">{{ tag.name }}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{{ document.title }} <small>{{ document.create_date | date: 'yyyy-MM-dd' }}</small>
|
||||
</h1>
|
||||
<ul class="list-inline">
|
||||
<li ng-repeat="tag in document.tags"><span class="label label-info" ng-style="{ 'background': tag.color }">{{ tag.name }}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p ng-bind-html="document.description | newline"></p>
|
||||
<dl class="dl-horizontal">
|
||||
<dt ng-if="document.subject">Subject</dt>
|
||||
@ -54,7 +56,7 @@
|
||||
<dt>Contributors</dt>
|
||||
<dd>
|
||||
<span ng-repeat="contributor in document.contributors">
|
||||
<span class="btn btn-default btn-xs">
|
||||
<span class="btn btn-link btn-xs">
|
||||
<a href="mailto:{{ contributor.email }}">
|
||||
{{ contributor.username }}
|
||||
</a>
|
||||
|
@ -30,7 +30,6 @@
|
||||
<script src="lib/angular.touch.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-router.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-bootstrap.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.ui-utils.js" type="text/javascript"></script>
|
||||
<script src="lib/angular.restangular.js" type="text/javascript"></script>
|
||||
<script src="app/share/app.js" type="text/javascript"></script>
|
||||
<script src="app/share/controller/Main.js" type="text/javascript"></script>
|
||||
|
@ -4549,6 +4549,11 @@ fieldset[disabled] .navbar-default .btn-link:focus {
|
||||
.navbar-inverse .navbar-nav > .active > a:focus {
|
||||
color: #fff;
|
||||
}
|
||||
.navbar-inverse .navbar-nav > .active2 > a,
|
||||
.navbar-inverse .navbar-nav > .active2 > a:hover,
|
||||
.navbar-inverse .navbar-nav > .active2 > a:focus {
|
||||
color: #fff;
|
||||
}
|
||||
.navbar-inverse .navbar-nav > .disabled > a,
|
||||
.navbar-inverse .navbar-nav > .disabled > a:hover,
|
||||
.navbar-inverse .navbar-nav > .disabled > a:focus {
|
||||
|
@ -300,13 +300,6 @@ input[readonly].share-link {
|
||||
}
|
||||
}
|
||||
|
||||
// Export dropdown
|
||||
.btn-export {
|
||||
.dropdown-menu {
|
||||
left: -62px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Styling for the ngProgress itself */
|
||||
#ngProgress {
|
||||
margin: 0;
|
||||
|
@ -1,21 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.config = {
|
||||
seleniumServerJar: '../node_modules/selenium/lib/runner/selenium-server-standalone-2.20.0.jar',
|
||||
framework: 'jasmine',
|
||||
rootElement: 'html',
|
||||
baseUrl: 'http://localhost:9999/docs-web/src/?protractor',
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
|
||||
specs: [
|
||||
'specs/**/*.js'
|
||||
],
|
||||
|
||||
jasmineNodeOpts: {
|
||||
isVerbose: true,
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000
|
||||
}
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
describe('document', function () {
|
||||
it('should create and delete a document', function () {
|
||||
browser.get('');
|
||||
|
||||
// Login as admin
|
||||
element(by.model('user.username')).sendKeys('admin');
|
||||
element(by.model('user.password')).sendKeys('admin');
|
||||
element(by.css('.login-box button[type="submit"]')).click();
|
||||
|
||||
// Create a document
|
||||
element(by.partialLinkText('Add a document')).click();
|
||||
element(by.model('document.title')).sendKeys('My test document');
|
||||
element(by.buttonText('Add')).click();
|
||||
|
||||
// Open the last document
|
||||
element(by.css('.table-documents tbody tr:nth-child(1)')).click();
|
||||
|
||||
// Delete the document
|
||||
element(by.partialButtonText('Delete')).click();
|
||||
element(by.partialButtonText('OK')).click();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user