Cleaned folder heirarchy
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
angular
|
||||
.module('app')
|
||||
.directive('snAutofocus', ['$timeout', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
shouldFocus: "="
|
||||
},
|
||||
link : function($scope, $element) {
|
||||
$timeout(function() {
|
||||
if($scope.shouldFocus) {
|
||||
$element[0].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
@@ -0,0 +1,27 @@
|
||||
angular
|
||||
.module('app')
|
||||
.directive('clickOutside', ['$document', function($document) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
replace: false,
|
||||
link : function($scope, $element, attrs) {
|
||||
|
||||
var didApplyClickOutside = false;
|
||||
|
||||
$element.bind('click', function(e) {
|
||||
didApplyClickOutside = false;
|
||||
if (attrs.isOpen) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
$document.bind('click', function() {
|
||||
if(!didApplyClickOutside) {
|
||||
$scope.$apply(attrs.clickOutside);
|
||||
didApplyClickOutside = true;
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}]);
|
||||
@@ -0,0 +1,46 @@
|
||||
angular
|
||||
.module('app')
|
||||
.directive('delayHide', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
show: '=',
|
||||
delay: '@'
|
||||
},
|
||||
link: function(scope, elem, attrs) {
|
||||
var showTimer;
|
||||
|
||||
showElement(false);
|
||||
|
||||
//This is where all the magic happens!
|
||||
// Whenever the scope variable updates we simply
|
||||
// show if it evaluates to 'true' and hide if 'false'
|
||||
scope.$watch('show', function(newVal){
|
||||
newVal ? showSpinner() : hideSpinner();
|
||||
});
|
||||
|
||||
function showSpinner() {
|
||||
if(scope.hidePromise) {
|
||||
$timeout.cancel(scope.hidePromise);
|
||||
scope.hidePromise = null;
|
||||
}
|
||||
showElement(true);
|
||||
}
|
||||
|
||||
function hideSpinner() {
|
||||
scope.hidePromise = $timeout(showElement.bind(this, false), getDelay());
|
||||
}
|
||||
|
||||
function showElement(show) {
|
||||
show ? elem.css({display:''}) : elem.css({display:'none'});
|
||||
}
|
||||
|
||||
function getDelay() {
|
||||
var delay = parseInt(scope.delay);
|
||||
|
||||
return angular.isNumber(delay) ? delay : 200;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
angular
|
||||
.module('app')
|
||||
.directive('fileChange', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
handler: '&'
|
||||
},
|
||||
link: function (scope, element) {
|
||||
element.on('change', function (event) {
|
||||
scope.$apply(function(){
|
||||
scope.handler({files: event.target.files});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
angular.module('app').directive('infiniteScroll', [
|
||||
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
|
||||
return {
|
||||
link: function(scope, elem, attrs) {
|
||||
// elem.css('overflow-x', 'hidden');
|
||||
// elem.css('height', 'inherit');
|
||||
|
||||
var offset = parseInt(attrs.threshold) || 0;
|
||||
var e = elem[0]
|
||||
|
||||
elem.on('scroll', function(){
|
||||
if(scope.$eval(attrs.canLoad) && e.scrollTop + e.offsetHeight >= e.scrollHeight - offset) {
|
||||
scope.$apply(attrs.infiniteScroll);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
@@ -0,0 +1,20 @@
|
||||
angular
|
||||
.module('app')
|
||||
.directive('lowercase', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, modelCtrl) {
|
||||
var lowercase = function(inputValue) {
|
||||
if (inputValue == undefined) inputValue = '';
|
||||
var lowercased = inputValue.toLowerCase();
|
||||
if (lowercased !== inputValue) {
|
||||
modelCtrl.$setViewValue(lowercased);
|
||||
modelCtrl.$render();
|
||||
}
|
||||
return lowercased;
|
||||
}
|
||||
modelCtrl.$parsers.push(lowercase);
|
||||
lowercase(scope[attrs.ngModel]);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
angular
|
||||
.module('app')
|
||||
.directive('selectOnClick', ['$window', function ($window) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
element.on('focus', function () {
|
||||
if (!$window.getSelection().toString()) {
|
||||
// Required for mobile Safari
|
||||
this.setSelectionRange(0, this.value.length)
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
587
app/assets/javascripts/app/directives/views/accountMenu.js
Normal file
587
app/assets/javascripts/app/directives/views/accountMenu.js
Normal file
@@ -0,0 +1,587 @@
|
||||
class AccountMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/account-menu.html";
|
||||
this.scope = {
|
||||
"onSuccessfulAuth" : "&",
|
||||
"closeFunction" : "&"
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
||||
$scope.user = authManager.user;
|
||||
$scope.server = syncManager.serverURL;
|
||||
|
||||
$scope.close = function() {
|
||||
$scope.closeFunction()();
|
||||
}
|
||||
|
||||
$scope.encryptedBackupsAvailable = function() {
|
||||
return authManager.user || passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
$scope.newPasswordData = {};
|
||||
|
||||
$scope.showPasswordChangeForm = function() {
|
||||
$scope.newPasswordData.showForm = true;
|
||||
}
|
||||
|
||||
$scope.submitPasswordChange = function() {
|
||||
|
||||
let newPass = $scope.newPasswordData.newPassword;
|
||||
|
||||
if(!newPass || newPass.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(newPass != $scope.newPasswordData.newPasswordConfirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var email = $scope.user.email;
|
||||
if(!email) {
|
||||
alert("We don't have your email stored. Please log out then log back in to fix this issue.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.newPasswordData.status = "Generating New Keys...";
|
||||
$scope.newPasswordData.showForm = false;
|
||||
|
||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||
syncManager.sync(function(response){
|
||||
authManager.changePassword(email, newPass, function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error changing your password. Please try again.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// re-encrypt all items
|
||||
$scope.newPasswordData.status = "Re-encrypting all items with your new key...";
|
||||
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync(function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.")
|
||||
return;
|
||||
}
|
||||
$scope.newPasswordData.status = "Successfully changed password and re-encrypted all items.";
|
||||
$timeout(function(){
|
||||
alert("Your password has been changed, and your items successfully re-encrypted and synced. You must sign out of all other signed in applications and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.submitMfaForm = function() {
|
||||
var params = {};
|
||||
params[$scope.formData.mfa.payload.mfa_key] = $scope.formData.userMfaCode;
|
||||
$scope.login(params);
|
||||
}
|
||||
|
||||
$scope.submitAuthForm = function() {
|
||||
console.log("Submitting auth form");
|
||||
if(!$scope.formData.email || !$scope.formData.user_password) {
|
||||
return;
|
||||
}
|
||||
if($scope.formData.showLogin) {
|
||||
$scope.login();
|
||||
} else {
|
||||
$scope.register();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.login = function(extraParams) {
|
||||
console.log("Logging in");
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
$timeout(function(){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral, extraParams,
|
||||
(response) => {
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
if(error.tag == "mfa-required" || error.tag == "mfa-invalid") {
|
||||
$timeout(() => {
|
||||
$scope.formData.showLogin = false;
|
||||
$scope.formData.mfa = error;
|
||||
})
|
||||
} else if(!response || (response && !response.didDisplayAlert)) {
|
||||
$timeout(() => {
|
||||
$scope.formData.showLogin = true;
|
||||
$scope.formData.mfa = null;
|
||||
})
|
||||
alert(error.message);
|
||||
}
|
||||
} else {
|
||||
$scope.onAuthSuccess();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.register = function() {
|
||||
let confirmation = $scope.formData.password_conf;
|
||||
if(confirmation !== $scope.formData.user_password) {
|
||||
alert("The two passwords you entered do not match. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.formData.confirmPassword = false;
|
||||
$scope.formData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral ,function(response){
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
alert(error.message);
|
||||
} else {
|
||||
$scope.onAuthSuccess();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.mergeLocalChanged = function() {
|
||||
if(!$scope.formData.mergeLocal) {
|
||||
if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) {
|
||||
$scope.formData.mergeLocal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.onAuthSuccess = function() {
|
||||
var block = function() {
|
||||
$timeout(function(){
|
||||
$scope.onSuccessfulAuth()();
|
||||
syncManager.sync();
|
||||
})
|
||||
}
|
||||
|
||||
if($scope.formData.mergeLocal) {
|
||||
// Allows desktop to make backup file
|
||||
$rootScope.$broadcast("major-data-change");
|
||||
$scope.clearDatabaseAndRewriteAllItems(true, block);
|
||||
}
|
||||
|
||||
else {
|
||||
modelManager.resetLocalMemory();
|
||||
storageManager.clearAllModels(function(){
|
||||
block();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Allows indexeddb unencrypted logs to be deleted
|
||||
// clearAllModels will remove data from backing store, but not from working memory
|
||||
// See: https://github.com/standardnotes/desktop/issues/131
|
||||
$scope.clearDatabaseAndRewriteAllItems = function(alternateUuids, callback) {
|
||||
storageManager.clearAllModels(function(){
|
||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||
callback && callback();
|
||||
}, alternateUuids)
|
||||
});
|
||||
}
|
||||
|
||||
$scope.destroyLocalData = function() {
|
||||
if(!confirm("Are you sure you want to end your session? This will delete all local items and extensions.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.signOut();
|
||||
syncManager.destroyLocalData(function(){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
/* Import/Export */
|
||||
|
||||
$scope.archiveFormData = {encrypted: $scope.encryptedBackupsAvailable() ? true : false};
|
||||
$scope.user = authManager.user;
|
||||
|
||||
$scope.submitImportPassword = function() {
|
||||
$scope.performImport($scope.importData.data, $scope.importData.password);
|
||||
}
|
||||
|
||||
$scope.performImport = function(data, password) {
|
||||
$scope.importData.loading = true;
|
||||
// allow loading indicator to come up with timeout
|
||||
$timeout(function(){
|
||||
$scope.importJSONData(data, password, function(response, errorCount){
|
||||
$timeout(function(){
|
||||
$scope.importData.loading = false;
|
||||
$scope.importData = null;
|
||||
|
||||
// Update UI before showing alert
|
||||
setTimeout(function () {
|
||||
if(!response) {
|
||||
alert("There was an error importing your data. Please try again.");
|
||||
} else {
|
||||
if(errorCount > 0) {
|
||||
var message = `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`;
|
||||
alert(message);
|
||||
} else {
|
||||
alert("Your data was successfully imported.")
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.importFileSelected = function(files) {
|
||||
$scope.importData = {};
|
||||
|
||||
var file = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
var data = JSON.parse(e.target.result);
|
||||
$timeout(function(){
|
||||
if(data.auth_params) {
|
||||
// request password
|
||||
$scope.importData.requestPassword = true;
|
||||
$scope.importData.data = data;
|
||||
} else {
|
||||
$scope.performImport(data, null);
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
alert("Unable to open file. Ensure it is a proper JSON file and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
$scope.importJSONData = function(data, password, callback) {
|
||||
var onDataReady = function(errorCount) {
|
||||
var items = modelManager.mapResponseItemsToLocalModels(data.items, ModelManager.MappingSourceFileImport);
|
||||
items.forEach(function(item){
|
||||
item.setDirty(true);
|
||||
item.deleted = false;
|
||||
item.markAllReferencesDirty();
|
||||
|
||||
// We don't want to activate any components during import process in case of exceptions
|
||||
// breaking up the import proccess
|
||||
if(item.content_type == "SN|Component") {
|
||||
item.active = false;
|
||||
}
|
||||
})
|
||||
|
||||
syncManager.sync((response) => {
|
||||
callback(response, errorCount);
|
||||
}, {additionalFields: ["created_at", "updated_at"]});
|
||||
}.bind(this)
|
||||
|
||||
if(data.auth_params) {
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){
|
||||
try {
|
||||
EncryptionHelper.decryptMultipleItems(data.items, keys, false); /* throws = false as we don't want to interrupt all decryption if just one fails */
|
||||
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
||||
data.items.forEach(function(item){
|
||||
item.enc_item_key = null;
|
||||
item.auth_hash = null;
|
||||
});
|
||||
|
||||
var errorCount = 0;
|
||||
// Don't import items that didn't decrypt properly
|
||||
data.items = data.items.filter(function(item){
|
||||
if(item.errorDecrypting) {
|
||||
errorCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
onDataReady(errorCount);
|
||||
}
|
||||
catch (e) {
|
||||
console.log("Error decrypting", e);
|
||||
alert("There was an error decrypting your items. Make sure the password you entered is correct and try again.");
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
}.bind(this));
|
||||
} else {
|
||||
onDataReady();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Export
|
||||
*/
|
||||
|
||||
function loadZip(callback) {
|
||||
if(window.zip) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = "/assets/zip/zip.js";
|
||||
scriptTag.async = false;
|
||||
var headTag = document.getElementsByTagName('head')[0];
|
||||
headTag.appendChild(scriptTag);
|
||||
scriptTag.onload = function() {
|
||||
zip.workerScriptsPath = "assets/zip/";
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadZippedNotes(notes) {
|
||||
loadZip(function(){
|
||||
|
||||
zip.createWriter(new zip.BlobWriter("application/zip"), function(zipWriter) {
|
||||
|
||||
var index = 0;
|
||||
function nextFile() {
|
||||
var note = notes[index];
|
||||
var blob = new Blob([note.text], {type: 'text/plain'});
|
||||
zipWriter.add(`${note.title}-${note.uuid}.txt`, new zip.BlobReader(blob), function() {
|
||||
index++;
|
||||
if(index < notes.length) {
|
||||
nextFile();
|
||||
} else {
|
||||
zipWriter.close(function(blob) {
|
||||
downloadData(blob, `Notes Txt Archive - ${new Date()}.zip`)
|
||||
zipWriter = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nextFile();
|
||||
}, onerror);
|
||||
})
|
||||
}
|
||||
|
||||
var textFile = null;
|
||||
|
||||
function hrefForData(data) {
|
||||
// If we are replacing a previously generated file we need to
|
||||
// manually revoke the object URL to avoid memory leaks.
|
||||
if (textFile !== null) {
|
||||
window.URL.revokeObjectURL(textFile);
|
||||
}
|
||||
|
||||
textFile = window.URL.createObjectURL(data);
|
||||
|
||||
// returns a URL you can use as a href
|
||||
return textFile;
|
||||
}
|
||||
|
||||
function downloadData(data, fileName) {
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', fileName);
|
||||
link.href = hrefForData(data);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
$scope.downloadDataArchive = function() {
|
||||
// download in Standard File format
|
||||
var keys, authParams, protocolVersion;
|
||||
if($scope.archiveFormData.encrypted) {
|
||||
if(authManager.offline() && passcodeManager.hasPasscode()) {
|
||||
keys = passcodeManager.keys();
|
||||
authParams = passcodeManager.passcodeAuthParams();
|
||||
protocolVersion = authParams.version;
|
||||
} else {
|
||||
keys = authManager.keys();
|
||||
authParams = authManager.getAuthParams();
|
||||
protocolVersion = authManager.protocolVersion();
|
||||
}
|
||||
}
|
||||
var data = $scope.itemsData(keys, authParams, protocolVersion);
|
||||
downloadData(data, `SN Archive - ${new Date()}.txt`);
|
||||
|
||||
// download as zipped plain text files
|
||||
if(!keys) {
|
||||
var notes = modelManager.allItemsMatchingTypes(["Note"]);
|
||||
downloadZippedNotes(notes);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.itemsData = function(keys, authParams, protocolVersion) {
|
||||
let data = modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
|
||||
let blobData = new Blob([data], {type: 'text/json'});
|
||||
return blobData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Advanced
|
||||
|
||||
$scope.reencryptPressed = function() {
|
||||
if(!confirm("Are you sure you want to re-encrypt and sync all your items? This is useful when updates are made to our encryption specification. You should have been instructed to come here from our website.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!confirm("It is highly recommended that you download a backup of your data before proceeding. Press cancel to go back. Note that this procedure can take some time, depending on the number of items you have. Do not close the app during process.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync(function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error re-encrypting your items. You should try syncing again. If all else fails, you should restore your notes from backup.")
|
||||
return;
|
||||
}
|
||||
|
||||
$timeout(function(){
|
||||
alert("Your items have been successfully re-encrypted and synced. You must sign out of all other signed in applications (mobile, desktop, web) and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 002 Update
|
||||
|
||||
$scope.securityUpdateAvailable = function() {
|
||||
var keys = authManager.keys()
|
||||
return keys && !keys.ak;
|
||||
}
|
||||
|
||||
$scope.clickedSecurityUpdate = function() {
|
||||
if(!$scope.securityUpdateData) {
|
||||
$scope.securityUpdateData = {};
|
||||
}
|
||||
$scope.securityUpdateData.showForm = true;
|
||||
}
|
||||
|
||||
$scope.submitSecurityUpdateForm = function() {
|
||||
$scope.securityUpdateData.processing = true;
|
||||
var authParams = authManager.getAuthParams();
|
||||
|
||||
Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: $scope.securityUpdateData.password}, authParams), function(keys){
|
||||
if(keys.mk !== authManager.keys().mk) {
|
||||
alert("Invalid password. Please try again.");
|
||||
$timeout(function(){
|
||||
$scope.securityUpdateData.processing = false;
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.saveKeys(keys);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Encryption Status
|
||||
*/
|
||||
|
||||
$scope.notesAndTagsCount = function() {
|
||||
var items = modelManager.allItemsMatchingTypes(["Note", "Tag"]);
|
||||
return items.length;
|
||||
}
|
||||
|
||||
$scope.encryptionStatusForNotes = function() {
|
||||
var length = $scope.notesAndTagsCount();
|
||||
return length + "/" + length + " notes and tags encrypted";
|
||||
}
|
||||
|
||||
$scope.encryptionEnabled = function() {
|
||||
return passcodeManager.hasPasscode() || !authManager.offline();
|
||||
}
|
||||
|
||||
$scope.encryptionSource = function() {
|
||||
if(!authManager.offline()) {
|
||||
return "Account keys";
|
||||
} else if(passcodeManager.hasPasscode()) {
|
||||
return "Local Passcode";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.encryptionStatusString = function() {
|
||||
if(!authManager.offline()) {
|
||||
return "End-to-end encryption is enabled. Your data is encrypted before being synced to your private account.";
|
||||
} else if(passcodeManager.hasPasscode()) {
|
||||
return "Encryption is enabled. Your data is encrypted using your passcode before being stored on disk.";
|
||||
} else {
|
||||
return "Encryption is not enabled. Sign in, register, or add a passcode lock to enable encryption.";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Passcode Lock
|
||||
*/
|
||||
|
||||
$scope.passcodeOptionAvailable = function() {
|
||||
// If you're signed in with an ephemeral session, passcode lock is unavailable
|
||||
return authManager.offline() || !authManager.isEphemeralSession();
|
||||
}
|
||||
|
||||
$scope.hasPasscode = function() {
|
||||
return passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
$scope.addPasscodeClicked = function() {
|
||||
$scope.formData.showPasscodeForm = true;
|
||||
}
|
||||
|
||||
$scope.submitPasscodeForm = function() {
|
||||
var passcode = $scope.formData.passcode;
|
||||
if(passcode !== $scope.formData.confirmPasscode) {
|
||||
alert("The two passcodes you entered do not match. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
passcodeManager.setPasscode(passcode, () => {
|
||||
$timeout(function(){
|
||||
$scope.formData.showPasscodeForm = false;
|
||||
var offline = authManager.offline();
|
||||
|
||||
if(offline) {
|
||||
// Allows desktop to make backup file
|
||||
$rootScope.$broadcast("major-data-change");
|
||||
$scope.clearDatabaseAndRewriteAllItems(false);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.removePasscodePressed = function() {
|
||||
var signedIn = !authManager.offline();
|
||||
var message = "Are you sure you want to remove your local passcode?";
|
||||
if(!signedIn) {
|
||||
message += " This will remove encryption from your local data.";
|
||||
}
|
||||
if(confirm(message)) {
|
||||
passcodeManager.clearPasscode();
|
||||
|
||||
if(authManager.offline()) {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline();
|
||||
// Don't create backup here, as if the user is temporarily removing the passcode to change it,
|
||||
// we don't want to write unencrypted data to disk.
|
||||
// $rootScope.$broadcast("major-data-change");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.isDesktopApplication = function() {
|
||||
return isDesktopApplication();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').directive('accountMenu', () => new AccountMenu);
|
||||
86
app/assets/javascripts/app/directives/views/actionsMenu.js
Normal file
86
app/assets/javascripts/app/directives/views/actionsMenu.js
Normal file
@@ -0,0 +1,86 @@
|
||||
class ActionsMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/actions-menu.html";
|
||||
this.scope = {
|
||||
item: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, actionsManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.renderData = {};
|
||||
|
||||
$scope.extensions = actionsManager.extensions;
|
||||
|
||||
for(let ext of $scope.extensions) {
|
||||
ext.loading = true;
|
||||
actionsManager.loadExtensionInContextOfItem(ext, $scope.item, function(scopedExtension) {
|
||||
ext.loading = false;
|
||||
})
|
||||
}
|
||||
|
||||
$scope.executeAction = function(action, extension, parentAction) {
|
||||
if(action.verb == "nested") {
|
||||
if(!action.subrows) {
|
||||
action.subrows = $scope.subRowsForAction(action, extension);
|
||||
} else {
|
||||
action.subrows = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
action.running = true;
|
||||
actionsManager.executeAction(action, extension, $scope.item, function(response){
|
||||
action.running = false;
|
||||
$scope.handleActionResponse(action, response);
|
||||
|
||||
// reload extension actions
|
||||
actionsManager.loadExtensionInContextOfItem(extension, $scope.item, function(ext){
|
||||
// keep nested state
|
||||
if(parentAction) {
|
||||
var matchingAction = _.find(ext.actions, {label: parentAction.label});
|
||||
matchingAction.subrows = $scope.subRowsForAction(parentAction, extension);
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.handleActionResponse = function(action, response) {
|
||||
switch (action.verb) {
|
||||
case "render": {
|
||||
var item = response.item;
|
||||
if(item.content_type == "Note") {
|
||||
$scope.renderData.title = item.title;
|
||||
$scope.renderData.text = item.text;
|
||||
$scope.renderData.showRenderModal = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$scope.subRowsForAction = function(parentAction, extension) {
|
||||
if(!parentAction.subactions) {
|
||||
return null;
|
||||
}
|
||||
return parentAction.subactions.map((subaction) => {
|
||||
return {
|
||||
onClick: ($event) => {
|
||||
this.executeAction(subaction, extension, parentAction);
|
||||
$event.stopPropagation();
|
||||
},
|
||||
title: subaction.label,
|
||||
subtitle: subaction.desc,
|
||||
spinnerClass: subaction.running ? 'info' : null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('actionsMenu', () => new ActionsMenu);
|
||||
@@ -0,0 +1,42 @@
|
||||
class ComponentModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/component-modal.html";
|
||||
this.scope = {
|
||||
show: "=",
|
||||
component: "=",
|
||||
callback: "=",
|
||||
onDismiss: "&"
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
$scope.el = el;
|
||||
}
|
||||
|
||||
controller($scope, $timeout, componentManager) {
|
||||
'ngInject';
|
||||
|
||||
if($scope.component.directiveController) {
|
||||
$scope.component.directiveController.dismiss = function(callback) {
|
||||
$scope.dismiss(callback);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.dismiss = function(callback) {
|
||||
var onDismiss = $scope.component.directiveController && $scope.component.directiveController.onDismiss();
|
||||
// Setting will null out compinent-view's component, which will handle deactivation
|
||||
$scope.component = null;
|
||||
$timeout(() => {
|
||||
$scope.el.remove();
|
||||
onDismiss && onDismiss();
|
||||
callback && callback();
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('componentModal', () => new ComponentModal);
|
||||
74
app/assets/javascripts/app/directives/views/componentView.js
Normal file
74
app/assets/javascripts/app/directives/views/componentView.js
Normal file
@@ -0,0 +1,74 @@
|
||||
class ComponentView {
|
||||
|
||||
constructor(componentManager, $timeout) {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/component-view.html";
|
||||
this.scope = {
|
||||
component: "="
|
||||
};
|
||||
|
||||
this.componentManager = componentManager;
|
||||
this.timeout = $timeout;
|
||||
}
|
||||
|
||||
link($scope, el, attrs, ctrl) {
|
||||
$scope.el = el;
|
||||
|
||||
let identifier = "component-view-" + Math.random();
|
||||
|
||||
this.componentManager.registerHandler({identifier: identifier, areas: ["*"], activationHandler: (component) => {
|
||||
if(component.active) {
|
||||
this.timeout(function(){
|
||||
var iframe = this.componentManager.iframeForComponent(component);
|
||||
if(iframe) {
|
||||
iframe.onload = function() {
|
||||
this.componentManager.registerComponentWindow(component, iframe.contentWindow);
|
||||
}.bind(this);
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
actionHandler: function(component, action, data) {
|
||||
if(action == "set-size") {
|
||||
this.componentManager.handleSetSizeEvent(component, data);
|
||||
}
|
||||
}.bind(this)});
|
||||
|
||||
$scope.$watch('component', function(component, prevComponent){
|
||||
ctrl.componentValueChanging(component, prevComponent);
|
||||
});
|
||||
}
|
||||
|
||||
controller($scope, $timeout, componentManager, desktopManager) {
|
||||
'ngInject';
|
||||
|
||||
this.componentValueChanging = (component, prevComponent) => {
|
||||
if(prevComponent && component !== prevComponent) {
|
||||
// Deactive old component
|
||||
componentManager.deactivateComponent(prevComponent);
|
||||
}
|
||||
|
||||
if(component) {
|
||||
componentManager.activateComponent(component);
|
||||
component.runningLocally = $scope.getUrl
|
||||
console.log("Loading", $scope.component.name, $scope.getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
$scope.getUrl = function() {
|
||||
var url = componentManager.urlForComponent($scope.component);
|
||||
$scope.component.runningLocally = url !== ($scope.component.url || $scope.component.hosted_url);
|
||||
return url;
|
||||
}
|
||||
|
||||
$scope.$on("$destroy", function() {
|
||||
componentManager.deregisterHandler($scope.identifier);
|
||||
if($scope.component) {
|
||||
componentManager.deactivateComponent($scope.component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('componentView', (componentManager, $timeout) => new ComponentView(componentManager, $timeout));
|
||||
71
app/assets/javascripts/app/directives/views/editorMenu.js
Normal file
71
app/assets/javascripts/app/directives/views/editorMenu.js
Normal file
@@ -0,0 +1,71 @@
|
||||
class EditorMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/editor-menu.html";
|
||||
this.scope = {
|
||||
callback: "&",
|
||||
selectedEditor: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, componentManager, syncManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.editors = componentManager.componentsForArea("editor-editor");
|
||||
$scope.stack = componentManager.componentsForArea("editor-stack");
|
||||
|
||||
$scope.isDesktop = isDesktopApplication();
|
||||
|
||||
$scope.defaultEditor = $scope.editors.filter((e) => {return e.isDefaultEditor()})[0];
|
||||
|
||||
$scope.selectComponent = function($event, component) {
|
||||
$event.stopPropagation();
|
||||
if(component) {
|
||||
component.conflict_of = null; // clear conflict if applicable
|
||||
}
|
||||
$timeout(() => {
|
||||
$scope.callback()(component);
|
||||
})
|
||||
}
|
||||
|
||||
$scope.toggleDefaultForEditor = function(editor) {
|
||||
if($scope.defaultEditor == editor) {
|
||||
$scope.removeEditorDefault(editor);
|
||||
} else {
|
||||
$scope.makeEditorDefault(editor);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.offlineAvailableForComponent = function(component) {
|
||||
return component.local_url && isDesktopApplication();
|
||||
}
|
||||
|
||||
$scope.makeEditorDefault = function(component) {
|
||||
var currentDefault = componentManager.componentsForArea("editor-editor").filter((e) => {return e.isDefaultEditor()})[0];
|
||||
if(currentDefault) {
|
||||
currentDefault.setAppDataItem("defaultEditor", false);
|
||||
currentDefault.setDirty(true);
|
||||
}
|
||||
component.setAppDataItem("defaultEditor", true);
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
|
||||
$scope.defaultEditor = component;
|
||||
}
|
||||
|
||||
$scope.removeEditorDefault = function(component) {
|
||||
component.setAppDataItem("defaultEditor", false);
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
|
||||
$scope.defaultEditor = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('editorMenu', () => new EditorMenu);
|
||||
@@ -0,0 +1,215 @@
|
||||
class GlobalExtensionsMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/global-extensions-menu.html";
|
||||
this.scope = {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, actionsManager, syncManager, modelManager, themeManager, componentManager, packageManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.actionsManager = actionsManager;
|
||||
$scope.themeManager = themeManager;
|
||||
$scope.componentManager = componentManager;
|
||||
|
||||
$scope.serverExtensions = modelManager.itemsForContentType("SF|Extension");
|
||||
|
||||
$scope.selectedAction = function(action, extension) {
|
||||
actionsManager.executeAction(action, extension, null, function(response){
|
||||
if(response && response.error) {
|
||||
action.error = true;
|
||||
alert("There was an error performing this action. Please try again.");
|
||||
} else {
|
||||
action.error = false;
|
||||
syncManager.sync(null);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.changeExtensionEncryptionFormat = function(encrypted, extension) {
|
||||
extension.encrypted = encrypted;
|
||||
extension.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.deleteActionExtension = function(extension) {
|
||||
if(confirm("Are you sure you want to delete this extension?")) {
|
||||
actionsManager.deleteExtension(extension);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.reloadExtensionsPressed = function() {
|
||||
if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) {
|
||||
actionsManager.refreshExtensionsFromServer();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.deleteTheme = function(theme) {
|
||||
if(confirm("Are you sure you want to delete this theme?")) {
|
||||
themeManager.deactivateTheme(theme);
|
||||
modelManager.setItemToBeDeleted(theme);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.renameExtension = function(extension) {
|
||||
extension.tempName = extension.name;
|
||||
extension.rename = true;
|
||||
}
|
||||
|
||||
$scope.submitExtensionRename = function(extension) {
|
||||
extension.name = extension.tempName;
|
||||
extension.tempName = null;
|
||||
extension.setDirty(true);
|
||||
extension.rename = false;
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.clickedExtension = function(extension) {
|
||||
if(extension.rename) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($scope.currentlyExpandedExtension && $scope.currentlyExpandedExtension !== extension) {
|
||||
$scope.currentlyExpandedExtension.showDetails = false;
|
||||
$scope.currentlyExpandedExtension.rename = false;
|
||||
}
|
||||
|
||||
extension.showDetails = !extension.showDetails;
|
||||
|
||||
if(extension.showDetails) {
|
||||
$scope.currentlyExpandedExtension = extension;
|
||||
}
|
||||
}
|
||||
|
||||
// Server extensions
|
||||
|
||||
$scope.deleteServerExt = function(ext) {
|
||||
if(confirm("Are you sure you want to delete and disable this extension?")) {
|
||||
_.remove($scope.serverExtensions, {uuid: ext.uuid});
|
||||
modelManager.setItemToBeDeleted(ext);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.nameForServerExtension = function(ext) {
|
||||
var url = ext.url;
|
||||
if(!url) {
|
||||
return "Invalid Extension";
|
||||
}
|
||||
if(url.includes("gdrive")) {
|
||||
return "Google Drive Sync";
|
||||
} else if(url.includes("file_attacher")) {
|
||||
return "File Attacher";
|
||||
} else if(url.includes("onedrive")) {
|
||||
return "OneDrive Sync";
|
||||
} else if(url.includes("backup.email_archive")) {
|
||||
return "Daily Email Backups";
|
||||
} else if(url.includes("dropbox")) {
|
||||
return "Dropbox Sync";
|
||||
} else if(url.includes("revisions")) {
|
||||
return "Revision History";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Components
|
||||
|
||||
$scope.revokePermissions = function(component) {
|
||||
component.permissions = [];
|
||||
component.setDirty(true);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$scope.deleteComponent = function(component) {
|
||||
if(confirm("Are you sure you want to delete this component?")) {
|
||||
componentManager.deleteComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Installation
|
||||
|
||||
$scope.submitInstallLink = function() {
|
||||
|
||||
var fullLink = $scope.formData.installLink;
|
||||
if(!fullLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
var completion = function() {
|
||||
$scope.formData.installLink = "";
|
||||
$scope.formData.successfullyInstalled = true;
|
||||
}
|
||||
|
||||
var links = fullLink.split(",");
|
||||
for(var link of links) {
|
||||
var type = getParameterByName("type", link);
|
||||
|
||||
if(type == "sf") {
|
||||
$scope.handleSyncAdapterLink(link, completion);
|
||||
} else if(type == "editor") {
|
||||
$scope.handleEditorLink(link, completion);
|
||||
} else if(link.indexOf(".css") != -1 || type == "theme") {
|
||||
$scope.handleThemeLink(link, completion);
|
||||
} else if(type == "component") {
|
||||
$scope.handleComponentLink(link, completion);
|
||||
} else if(type == "package") {
|
||||
$scope.handlePackageLink(link, completion);
|
||||
}
|
||||
|
||||
else {
|
||||
$scope.handleActionLink(link, completion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.handlePackageLink = function(link, completion) {
|
||||
packageManager.installPackage(link, completion);
|
||||
}
|
||||
|
||||
$scope.handleSyncAdapterLink = function(link, completion) {
|
||||
var params = parametersFromURL(link);
|
||||
params["url"] = link;
|
||||
var ext = new SyncAdapter({content: params});
|
||||
ext.setDirty(true);
|
||||
|
||||
modelManager.addItem(ext);
|
||||
syncManager.sync();
|
||||
$scope.serverExtensions.push(ext);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleThemeLink = function(link, completion) {
|
||||
themeManager.submitTheme(link);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleComponentLink = function(link, completion) {
|
||||
componentManager.installComponent(link);
|
||||
completion();
|
||||
}
|
||||
|
||||
$scope.handleActionLink = function(link, completion) {
|
||||
if(link) {
|
||||
actionsManager.addExtension(link, function(response){
|
||||
if(!response) {
|
||||
alert("Unable to register this extension. Make sure the link is valid and try again.");
|
||||
} else {
|
||||
completion();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('globalExtensionsMenu', () => new GlobalExtensionsMenu);
|
||||
31
app/assets/javascripts/app/directives/views/menuRow.js
Normal file
31
app/assets/javascripts/app/directives/views/menuRow.js
Normal file
@@ -0,0 +1,31 @@
|
||||
class MenuRow {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.transclude = true;
|
||||
this.templateUrl = "directives/menu-row.html";
|
||||
this.scope = {
|
||||
circle: "=",
|
||||
title: "=",
|
||||
subtite: "=",
|
||||
hasButton: "=",
|
||||
buttonText: "=",
|
||||
buttonClass: "=",
|
||||
buttonAction: "&",
|
||||
spinnerClass: "=",
|
||||
subRows: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, componentManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.clickButton = function($event) {
|
||||
$event.stopPropagation();
|
||||
$scope.buttonAction();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').directive('menuRow', () => new MenuRow);
|
||||
201
app/assets/javascripts/app/directives/views/panelResizer.js
Normal file
201
app/assets/javascripts/app/directives/views/panelResizer.js
Normal file
@@ -0,0 +1,201 @@
|
||||
class PanelResizer {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/panel-resizer.html";
|
||||
this.scope = {
|
||||
index: "=",
|
||||
panelId: "=",
|
||||
onResize: "&",
|
||||
onResizeFinish: "&",
|
||||
control: "=",
|
||||
alwaysVisible: "=",
|
||||
minWidth: "=",
|
||||
property: "=",
|
||||
hoverable: "=",
|
||||
collapsable: "="
|
||||
};
|
||||
}
|
||||
|
||||
link(scope, elem, attrs, ctrl) {
|
||||
scope.elem = elem;
|
||||
|
||||
scope.control.setWidth = function(value) {
|
||||
scope.setWidth(value, true);
|
||||
}
|
||||
|
||||
scope.control.setLeft = function(value) {
|
||||
scope.setLeft(value);
|
||||
}
|
||||
}
|
||||
|
||||
controller($scope, $element, modelManager, actionsManager) {
|
||||
'ngInject';
|
||||
|
||||
let panel = document.getElementById($scope.panelId);
|
||||
if(!panel) {
|
||||
console.log("Panel not found for", $scope.panelId);
|
||||
}
|
||||
let resizerColumn = $element[0];
|
||||
let resizerWidth = resizerColumn.offsetWidth;
|
||||
let minWidth = $scope.minWidth || resizerWidth;
|
||||
|
||||
function getParentRect() {
|
||||
return panel.parentNode.getBoundingClientRect();
|
||||
}
|
||||
|
||||
var pressed = false;
|
||||
var startWidth = panel.scrollWidth, startX, lastDownX, collapsed, lastWidth = startWidth, startLeft, lastLeft;
|
||||
let appFrame = document.getElementById("app").getBoundingClientRect();
|
||||
|
||||
if($scope.alwaysVisible) {
|
||||
console.log("Adding always visible", $scope.alwaysVisible);
|
||||
resizerColumn.classList.add("always-visible");
|
||||
}
|
||||
|
||||
if($scope.hoverable) {
|
||||
resizerColumn.classList.add("hoverable");
|
||||
}
|
||||
|
||||
$scope.setWidth = function(width, finish) {
|
||||
if(width < minWidth) {
|
||||
width = minWidth;
|
||||
}
|
||||
|
||||
let parentRect = getParentRect();
|
||||
|
||||
if(width > parentRect.width) {
|
||||
width = parentRect.width;
|
||||
}
|
||||
|
||||
if(width == parentRect.width) {
|
||||
panel.style.width = "100%";
|
||||
panel.style.flexBasis = "100%";
|
||||
} else {
|
||||
panel.style.flexBasis = width + "px";
|
||||
panel.style.width = width + "px";
|
||||
}
|
||||
|
||||
|
||||
lastWidth = width;
|
||||
|
||||
if(finish) {
|
||||
$scope.finishSettingWidth();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.setLeft = function(left) {
|
||||
panel.style.left = left + "px";
|
||||
lastLeft = left;
|
||||
}
|
||||
|
||||
$scope.finishSettingWidth = function() {
|
||||
if(!$scope.collapsable) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(lastWidth <= minWidth) {
|
||||
collapsed = true;
|
||||
} else {
|
||||
collapsed = false;
|
||||
}
|
||||
if(collapsed) {
|
||||
resizerColumn.classList.add("collapsed");
|
||||
} else {
|
||||
resizerColumn.classList.remove("collapsed");
|
||||
}
|
||||
}
|
||||
|
||||
resizerColumn.addEventListener("mousedown", function(event){
|
||||
pressed = true;
|
||||
lastDownX = event.clientX;
|
||||
startWidth = panel.scrollWidth;
|
||||
startLeft = panel.offsetLeft;
|
||||
panel.classList.add("no-selection");
|
||||
|
||||
if($scope.hoverable) {
|
||||
resizerColumn.classList.add("dragging");
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener("mousemove", function(event){
|
||||
if(!pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if($scope.property && $scope.property == 'left') {
|
||||
handleLeftEvent(event);
|
||||
} else {
|
||||
handleWidthEvent(event);
|
||||
}
|
||||
})
|
||||
|
||||
function handleWidthEvent(event) {
|
||||
var rect = panel.getBoundingClientRect();
|
||||
var panelMaxX = rect.left + (startWidth || panel.style.maxWidth);
|
||||
|
||||
var x = event.clientX;
|
||||
|
||||
let deltaX = x - lastDownX;
|
||||
var newWidth = startWidth + deltaX;
|
||||
|
||||
$scope.setWidth(newWidth, false);
|
||||
|
||||
if($scope.onResize()) {
|
||||
$scope.onResize()(lastWidth, panel);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLeftEvent(event) {
|
||||
var panelRect = panel.getBoundingClientRect();
|
||||
var x = event.clientX;
|
||||
let deltaX = x - lastDownX;
|
||||
var newLeft = startLeft + deltaX;
|
||||
if(newLeft < 0) {
|
||||
newLeft = 0;
|
||||
deltaX = -startLeft;
|
||||
}
|
||||
|
||||
let parentRect = getParentRect();
|
||||
|
||||
var newWidth = startWidth - deltaX;
|
||||
if(newWidth < minWidth) {
|
||||
newWidth = minWidth;
|
||||
}
|
||||
|
||||
if(newWidth > parentRect.width) {
|
||||
newWidth = parentRect.width;
|
||||
}
|
||||
|
||||
|
||||
if(newLeft + newWidth > parentRect.width) {
|
||||
newLeft = parentRect.width - newWidth;
|
||||
}
|
||||
|
||||
$scope.setLeft(newLeft, false);
|
||||
$scope.setWidth(newWidth, false);
|
||||
}
|
||||
|
||||
document.addEventListener("mouseup", function(event){
|
||||
if(pressed) {
|
||||
pressed = false;
|
||||
resizerColumn.classList.remove("dragging");
|
||||
panel.classList.remove("no-selection");
|
||||
|
||||
let isMaxWidth = lastWidth == getParentRect().width;
|
||||
|
||||
if($scope.onResizeFinish) {
|
||||
$scope.onResizeFinish()(lastWidth, lastLeft, isMaxWidth);
|
||||
}
|
||||
|
||||
$scope.finishSettingWidth();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('panelResizer', () => new PanelResizer);
|
||||
@@ -0,0 +1,77 @@
|
||||
class PermissionsModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/permissions-modal.html";
|
||||
this.scope = {
|
||||
show: "=",
|
||||
component: "=",
|
||||
permissions: "=",
|
||||
callback: "="
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
|
||||
$scope.dismiss = function() {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
$scope.accept = function() {
|
||||
$scope.callback(true);
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
$scope.deny = function() {
|
||||
$scope.callback(false);
|
||||
$scope.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
controller($scope, modelManager) {
|
||||
|
||||
$scope.formattedPermissions = $scope.permissions.map(function(permission){
|
||||
if(permission.name === "stream-items") {
|
||||
var types = permission.content_types.map(function(type){
|
||||
var desc = modelManager.humanReadableDisplayForContentType(type);
|
||||
if(desc) {
|
||||
return desc + "s";
|
||||
} else {
|
||||
return "items of type " + type;
|
||||
}
|
||||
})
|
||||
var typesString = "";
|
||||
var separator = ", ";
|
||||
|
||||
for(var i = 0;i < types.length;i++) {
|
||||
var type = types[i];
|
||||
if(i == 0) {
|
||||
// first element
|
||||
typesString = typesString + type;
|
||||
} else if(i == types.length - 1) {
|
||||
// last element
|
||||
if(types.length > 2) {
|
||||
typesString += separator + "and " + type;
|
||||
} else if(types.length == 2) {
|
||||
typesString = typesString + " and " + type;
|
||||
}
|
||||
} else {
|
||||
typesString += separator + type;
|
||||
}
|
||||
}
|
||||
|
||||
return typesString;
|
||||
} else if(permission.name === "stream-context-item") {
|
||||
var mapping = {
|
||||
"editor-stack" : "working note",
|
||||
"note-tags" : "working note",
|
||||
"editor-editor": "working note"
|
||||
}
|
||||
return mapping[$scope.component.area];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('permissionsModal', () => new PermissionsModal);
|
||||
Reference in New Issue
Block a user