Cleaned folder heirarchy

This commit is contained in:
Mo Bitar
2018-01-19 12:42:44 -06:00
parent 5be2402f65
commit 5d43697ed8
86 changed files with 75 additions and 91 deletions

View File

@@ -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();
}
});
}
}
}]);

View File

@@ -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;
}
})
}
}
}]);

View File

@@ -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;
}
}
};
});

View File

@@ -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});
});
});
}
};
});

View File

@@ -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);
}
});
}
};
}
]);

View File

@@ -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]);
}
};
});

View File

@@ -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)
}
});
}
};
}]);

View 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);

View 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);

View File

@@ -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);

View 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));

View 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);

View File

@@ -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);

View 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);

View 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);

View File

@@ -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);