Components system

This commit is contained in:
Mo Bitar
2017-06-15 11:44:20 -05:00
parent b759b2b770
commit de21d110d9
19 changed files with 1071 additions and 39 deletions

View File

@@ -1,5 +1,5 @@
class BaseCtrl {
constructor($rootScope, $scope, syncManager, dbManager, analyticsManager) {
constructor($rootScope, $scope, syncManager, dbManager, analyticsManager, componentManager) {
dbManager.openDatabase(null, function(){
// new database, delete syncToken so that items can be refetched entirely from server
syncManager.clearSyncToken();

View File

@@ -17,7 +17,7 @@ angular.module('app.frontend')
link:function(scope, elem, attrs, ctrl) {
scope.$watch('ctrl.note', function(note, oldNote){
if(note) {
ctrl.setNote(note, oldNote);
ctrl.noteDidChange(note, oldNote);
}
});
@@ -37,7 +37,10 @@ angular.module('app.frontend')
}
}
})
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, editorManager, themeManager) {
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, editorManager, themeManager, componentManager) {
this.componentManager = componentManager;
this.componentStack = [];
$rootScope.$on("theme-changed", function(){
this.postThemeToExternalEditor();
@@ -51,10 +54,76 @@ angular.module('app.frontend')
this.loadTagsString();
}.bind(this));
componentManager.registerHandler({identifier: "editor", areas: ["note-tags", "editor-stack"], activationHandler: function(component){
if(!component.active) {
return;
}
if(component.area === "note-tags") {
this.tagsComponent = component;
} else {
// stack
if(!_.find(this.componentStack, component)) {
this.componentStack.push(component);
}
}
$timeout(function(){
var iframe = componentManager.iframeForComponent(component);
if(iframe) {
iframe.onload = function() {
componentManager.registerComponentWindow(component, iframe.contentWindow);
}.bind(this);
}
}.bind(this));
}.bind(this), contextRequestHandler: function(component){
return this.note;
}.bind(this), actionHandler: function(component, action, data){
if(action === "set-size") {
var setSize = function(element, size) {
var widthString = typeof size.width === 'string' ? size.width : `${data.width}px`;
var heightString = typeof size.height === 'string' ? size.height : `${data.height}px`;
element.setAttribute("style", `width:${widthString}; height:${heightString}; `);
}
if(data.type === "content") {
var iframe = componentManager.iframeForComponent(component);
var width = data.width;
var height = data.height;
iframe.width = width;
iframe.height = height;
setSize(iframe, data);
} else {
if(component.area == "note-tags") {
var container = document.getElementById("note-tags-component-container");
setSize(container, data);
} else {
var container = document.getElementById("component-" + component.uuid);
setSize(container, data);
}
}
}
else if(action === "associate-item") {
var tag = modelManager.findItem(data.item.uuid);
this.addTag(tag);
}
else if(action === "deassociate-item") {
var tag = modelManager.findItem(data.item.uuid);
this.removeTag(tag);
}
}.bind(this)});
window.addEventListener("message", function(event){
if(event.data.status) {
this.postNoteToExternalEditor();
} else {
} else if(!event.data.api) {
// console.log("Received message", event.data);
var id = event.data.id;
var text = event.data.text;
@@ -75,8 +144,16 @@ angular.module('app.frontend')
}
}.bind(this), false);
this.noteDidChange = function(note, oldNote) {
this.setNote(note, oldNote);
for(var component of this.componentStack) {
componentManager.setEventFlowForComponent(component, component.isActiveForItem(this.note));
}
componentManager.contextItemDidChangeInArea("note-tags");
componentManager.contextItemDidChangeInArea("editor-stack");
}
this.setNote = function(note, oldNote) {
this.noteReady = false;
var currentEditor = this.editor;
this.editor = null;
this.showExtensions = false;
@@ -90,6 +167,11 @@ angular.module('app.frontend')
}.bind(this)
var editor = this.editorForNote(note);
if(editor && !editor.systemEditor) {
// setting note to not ready will remove the editor from view in a flash,
// so we only want to do this if switching between external editors
this.noteReady = false;
}
if(editor) {
if(currentEditor !== editor) {
// switch after timeout, so that note data isnt posted to current editor
@@ -299,6 +381,27 @@ angular.module('app.frontend')
this.tagsString = string;
}
this.addTag = function(tag) {
var tags = this.note.tags;
var strings = tags.map(function(_tag){
return _tag.title;
})
strings.push(tag.title);
this.updateTags()(this.note, strings);
this.loadTagsString();
}
this.removeTag = function(tag) {
var tags = this.note.tags;
var strings = tags.map(function(_tag){
return _tag.title;
}).filter(function(_tag){
return _tag !== tag.title;
})
this.updateTags()(this.note, strings);
this.loadTagsString();
}
this.updateTagsFromTagsString = function($event) {
$event.target.blur();
@@ -314,4 +417,40 @@ angular.module('app.frontend')
this.updateTags()(this.note, tags);
}
/* Components */
let alertKey = "displayed-component-disable-alert";
this.disableComponent = function(component) {
componentManager.disableComponentForItem(component, this.note);
componentManager.setEventFlowForComponent(component, false);
if(!localStorage.getItem(alertKey)) {
alert("This component will be disabled for this note. You can re-enable this component in the 'Menu' of the editor pane.");
localStorage.setItem(alertKey, true);
}
}
this.hasDisabledComponents = function() {
for(var component of this.componentStack) {
if(component.ignoreEvents) {
return true;
}
}
return false;
}
this.restoreDisabledComponents = function() {
var relevantComponents = this.componentStack.filter(function(component){
return component.ignoreEvents;
})
componentManager.enableComponentsForItem(relevantComponents, this.note);
for(var component of relevantComponents) {
componentManager.setEventFlowForComponent(component, true);
componentManager.contextItemDidChangeInArea("editor-stack");
}
}
});

View File

@@ -37,6 +37,7 @@ angular.module('app.frontend')
themeManager.activateInitialTheme();
$scope.$apply();
syncManager.sync(null);
// refresh every 30s
setInterval(function () {
@@ -56,6 +57,7 @@ angular.module('app.frontend')
*/
$scope.updateTagsForNote = function(note, stringTags) {
console.log("Updating tags", stringTags);
var toRemove = [];
for(var tag of note.tags) {
if(stringTags.indexOf(tag.title) === -1) {

View File

@@ -33,10 +33,39 @@ angular.module('app.frontend')
}
}
})
.controller('TagsCtrl', function (modelManager, $timeout) {
.controller('TagsCtrl', function ($rootScope, modelManager, $timeout, componentManager) {
var initialLoad = true;
componentManager.registerHandler({identifier: "tags", areas: ["tags-list"], activationHandler: function(component){
this.component = component;
if(component.active) {
$timeout(function(){
var iframe = document.getElementById("tags-list-iframe");
iframe.onload = function() {
componentManager.registerComponentWindow(this.component, iframe.contentWindow);
}.bind(this);
}.bind(this));
}
}.bind(this), contextRequestHandler: function(component){
return null;
}.bind(this), actionHandler: function(component, action, data){
if(action === "select-item") {
var tag = modelManager.findItem(data.item.uuid);
if(tag) {
this.selectTag(tag);
}
}
else if(action === "clear-selection") {
this.selectTag(this.allTag);
}
}.bind(this)});
this.setAllTag = function(allTag) {
this.selectTag(this.allTag);
}

View File

@@ -0,0 +1,59 @@
class Component extends Item {
constructor(json_obj) {
super(json_obj);
if(!this.componentData) {
this.componentData = {};
}
if(!this.disassociatedItemIds) {
this.disassociatedItemIds = [];
}
}
mapContentToLocalProperties(contentObject) {
super.mapContentToLocalProperties(contentObject)
this.url = contentObject.url;
this.name = contentObject.name;
// the location in the view this component is located in. Valid values are currently tags-list, note-tags, and editor-stack`
this.area = contentObject.area;
this.permissions = contentObject.permissions;
this.active = contentObject.active;
// custom data that a component can store in itself
this.componentData = contentObject.componentData || {};
// items that have requested a component to be disabled in its context
this.disassociatedItemIds = contentObject.disassociatedItemIds || [];
}
structureParams() {
var params = {
url: this.url,
name: this.name,
area: this.area,
permissions: this.permissions,
active: this.active,
componentData: this.componentData,
disassociatedItemIds: this.disassociatedItemIds
};
_.merge(params, super.structureParams());
return params;
}
toJSON() {
return {uuid: this.uuid}
}
get content_type() {
return "SN|Component";
}
isActiveForItem(item) {
return this.disassociatedItemIds.indexOf(item.uuid) === -1;
}
}

View File

@@ -22,30 +22,26 @@ class ItemParams {
}
paramsForSync() {
return this.__params(null, false);
return this.__params();
}
__params() {
let encryptionVersion = "001";
var itemCopy = _.cloneDeep(this.item);
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at};
if(this.keys && !this.item.doNotEncrypt()) {
EncryptionHelper.encryptItem(itemCopy, this.keys, encryptionVersion);
params.content = itemCopy.content;
params.enc_item_key = itemCopy.enc_item_key;
if(encryptionVersion === "001") {
params.auth_hash = itemCopy.auth_hash;
} else {
var encryptedParams = EncryptionHelper.encryptItem(this.item, this.keys, encryptionVersion);
_.merge(params, encryptedParams);
if(encryptionVersion !== "001") {
params.auth_hash = null;
}
}
else {
params.content = this.forExportFile ? itemCopy.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(itemCopy.createContentJSONFromProperties()));
params.content = this.forExportFile ? this.item.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(this.item.createContentJSONFromProperties()));
if(!this.forExportFile) {
params.enc_item_key = null;
params.auth_hash = null;

View File

@@ -0,0 +1,499 @@
class ComponentManager {
constructor($rootScope, modelManager, syncManager, themeManager, $timeout, $compile) {
this.$compile = $compile;
this.$rootScope = $rootScope;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.themeManager = themeManager;
this.timeout = $timeout;
this.streamObservers = [];
this.contextStreamObservers = [];
this.activeComponents = [];
this.permissionDialogs = [];
this.handlers = [];
$rootScope.$on("theme-changed", function(){
this.postThemeToComponents();
}.bind(this))
window.addEventListener("message", function(event){
if(this.loggingEnabled) {
console.log("Web app: received message", event);
}
this.handleMessage(this.componentForSessionKey(event.data.sessionKey), event.data);
}.bind(this), false);
this.modelManager.addItemSyncObserver("component-manager", "*", function(items) {
var syncedComponents = items.filter(function(item){return item.content_type === "SN|Component" });
for(var component of syncedComponents) {
var activeComponent = _.find(this.activeComponents, {uuid: component.uuid});
if(component.active && !activeComponent) {
this.activateComponent(component);
} else if(!component.active && activeComponent) {
this.deactivateComponent(component);
}
}
for(let observer of this.streamObservers) {
var relevantItems = items.filter(function(item){
return observer.contentTypes.indexOf(item.content_type) !== -1;
})
var requiredPermissions = [
{
name: "stream-items",
content_types: observer.contentTypes.sort()
}
];
this.runWithPermissions(observer.component, requiredPermissions, observer.originalMessage.permissions, function(){
this.sendItemsInReply(observer.component, relevantItems, observer.originalMessage);
}.bind(this))
}
var requiredContextPermissions = [
{
name: "stream-context-item"
}
];
for(let observer of this.contextStreamObservers) {
this.runWithPermissions(observer.component, requiredContextPermissions, observer.originalMessage.permissions, function(){
for(let handler of this.handlers) {
if(handler.areas.includes(observer.component.area) === false) {
continue;
}
var itemInContext = handler.contextRequestHandler(observer.component);
if(itemInContext) {
var matchingItem = _.find(items, {uuid: itemInContext.uuid});
if(matchingItem) {
this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage);
}
}
}
}.bind(this))
}
}.bind(this))
}
postThemeToComponents() {
for(var component of this.components) {
if(!component.active || !component.window) {
continue;
}
this.postThemeToComponent(component);
}
}
postThemeToComponent(component) {
var data = {
themes: [this.themeManager.currentTheme ? this.themeManager.currentTheme.url : null]
}
this.sendMessageToComponent(component, {action: "themes", data: data})
}
contextItemDidChangeInArea(area) {
for(let handler of this.handlers) {
if(handler.areas.includes(area) === false) {
continue;
}
var observers = this.contextStreamObservers.filter(function(observer){
return observer.component.area === area;
})
for(let observer of observers) {
var itemInContext = handler.contextRequestHandler(observer.component);
this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage);
}
}
}
jsonForItem(item) {
var params = {uuid: item.uuid, content_type: item.content_type, created_at: item.created_at, updated_at: item.updated_at, deleted: item.deleted};
params.content = item.createContentJSONFromProperties();
return params;
}
sendItemsInReply(component, items, message) {
var response = {items: {}};
var mapped = items.map(function(item) {
return this.jsonForItem(item);
}.bind(this));
response.items = mapped;
this.replyToMessage(component, message, response);
}
sendContextItemInReply(component, item, originalMessage) {
var response = {item: this.jsonForItem(item)};
this.replyToMessage(component, originalMessage, response);
}
get components() {
return this.modelManager.itemsForContentType("SN|Component");
}
componentsForStack(stack) {
return this.components.filter(function(component){
return component.area === stack;
})
}
componentForSessionKey(key) {
return _.find(this.components, {sessionKey: key});
}
handleMessage(component, message) {
if(!component) {
if(this.loggingEnabled) {
console.log("Component not defined, returning");
}
return;
}
/**
Possible Messages:
set-size
stream-items
stream-context-item
save-items
select-item
associate-item
deassociate-item
clear-selection
create-item
delete-items
set-component-data
*/
if(message.action === "stream-items") {
this.handleStreamItemsMessage(component, message);
}
else if(message.action === "stream-context-item") {
this.handleStreamContextItemMessage(component, message);
}
else if(message.action === "set-component-data") {
component.componentData = message.data.componentData;
component.setDirty(true);
this.syncManager.sync();
}
else if(message.action === "delete-items") {
var items = message.data.items;
var noun = items.length == 1 ? "item" : "items";
if(confirm(`Are you sure you want to delete ${items.length} ${noun}?`)) {
for(var item of items) {
var model = this.modelManager.findItem(item.uuid);
this.modelManager.setItemToBeDeleted(model);
}
this.syncManager.sync();
}
}
else if(message.action === "create-item") {
var item = this.modelManager.createItem(message.data.item);
this.modelManager.addItem(item);
item.setDirty(true);
this.syncManager.sync();
this.replyToMessage(component, message, {item: this.jsonForItem(item)})
}
else if(message.action === "save-items") {
var responseItems = message.data.items;
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems);
for(var item of localItems) {
var responseItem = _.find(responseItems, {uuid: item.uuid});
_.merge(item.content, responseItem.content);
item.setDirty(true);
}
this.syncManager.sync();
}
for(let handler of this.handlers) {
if(handler.areas.includes(component.area)) {
this.timeout(function(){
handler.actionHandler(component, message.action, message.data);
})
}
}
}
handleStreamItemsMessage(component, message) {
var requiredPermissions = [
{
name: "stream-items",
content_types: message.data.content_types.sort()
}
];
this.runWithPermissions(component, requiredPermissions, message.permissions, function(){
if(!_.find(this.streamObservers, {identifier: component.url})) {
// for pushing laster as changes come in
this.streamObservers.push({
identifier: component.url,
component: component,
originalMessage: message,
contentTypes: message.data.content_types
})
}
// push immediately now
var items = [];
for(var contentType of message.data.content_types) {
items = items.concat(this.modelManager.itemsForContentType(contentType));
}
this.sendItemsInReply(component, items, message);
}.bind(this));
}
handleStreamContextItemMessage(component, message) {
var requiredPermissions = [
{
name: "stream-context-item"
}
];
this.runWithPermissions(component, requiredPermissions, message.permissions, function(){
if(!_.find(this.contextStreamObservers, {identifier: component.url})) {
// for pushing laster as changes come in
this.contextStreamObservers.push({
identifier: component.url,
component: component,
originalMessage: message
})
}
// push immediately now
for(let handler of this.handlers) {
if(handler.areas.includes(component.area) === false) {
continue;
}
var itemInContext = handler.contextRequestHandler(component);
this.sendContextItemInReply(component, itemInContext, message);
}
}.bind(this))
}
runWithPermissions(component, requiredPermissions, requestedPermissions, runFunction) {
var acquiredPermissions = component.permissions;
var requestedMatchesRequired = true;
for(var required of requiredPermissions) {
var matching = _.find(requestedPermissions, required);
if(!matching) {
requestedMatchesRequired = false;
break;
}
}
if(!requestedMatchesRequired) {
// Error with Component permissions request
console.error("You are requesting permissions", requestedPermissions, "when you need to be requesting", requiredPermissions, ". Component:", component);
return;
}
if(!component.permissions) {
component.permissions = [];
}
var acquiredMatchesRequested = angular.toJson(component.permissions.sort()) === angular.toJson(requestedPermissions.sort());
if(!acquiredMatchesRequested) {
this.promptForPermissions(component, requestedPermissions, function(approved){
if(approved) {
runFunction();
}
});
} else {
runFunction();
}
}
promptForPermissions(component, requestedPermissions, callback) {
// since these calls are asyncronous, multiple dialogs may be requested at the same time. We only want to present one and trigger all callbacks based on one modal result
var existingDialog = _.find(this.permissionDialogs, {component: component});
component.trusted = component.url.startsWith("https://standardnotes.org") || component.url.startsWith("https://extensions.standardnotes.org");
var scope = this.$rootScope.$new(true);
scope.component = component;
scope.permissions = requestedPermissions;
scope.actionBlock = callback;
scope.callback = function(approved) {
if(approved) {
component.permissions = requestedPermissions;
component.setDirty(true);
this.syncManager.sync();
}
for(var existing of this.permissionDialogs) {
if(existing.component === component && existing.actionBlock) {
existing.actionBlock(approved);
}
}
this.permissionDialogs = this.permissionDialogs.filter(function(dialog){
return dialog.component !== component;
})
}.bind(this);
this.permissionDialogs.push(scope);
if(!existingDialog) {
var el = this.$compile( "<permissions-modal component='component' permissions='permissions' callback='callback' class='permissions-modal'></permissions-modal>" )(scope);
angular.element(document.body).append(el);
} else {
console.log("Existing dialog, not presenting.");
}
}
replyToMessage(component, originalMessage, replyData) {
var reply = {
action: "reply",
original: originalMessage,
data: replyData
}
this.sendMessageToComponent(component, reply);
}
sendMessageToComponent(component, message) {
if(component.ignoreEvents && message.action !== "component-registered") {
if(this.loggingEnabled) {
console.log("Component disabled for current item, not sending any messages.");
}
return;
}
component.window.postMessage(message, "*");
}
installComponent(url) {
var name = getParameterByName("name", url);
var area = getParameterByName("area", url);
var component = this.modelManager.createItem({
content_type: "SN|Component",
url: url,
name: name,
area: area
})
this.modelManager.addItem(component);
component.setDirty(true);
this.syncManager.sync();
}
activateComponent(component) {
var didChange = component.active != true;
component.active = true;
for(var handler of this.handlers) {
if(handler.areas.includes(component.area)) {
handler.activationHandler(component);
}
}
if(didChange) {
component.setDirty(true);
this.syncManager.sync();
}
this.activeComponents.push(component);
}
registerHandler(handler) {
this.handlers.push(handler);
}
// Called by other views when the iframe is ready
registerComponentWindow(component, componentWindow) {
component.window = componentWindow;
component.sessionKey = Neeto.crypto.generateUUID();
this.sendMessageToComponent(component, {action: "component-registered", sessionKey: component.sessionKey, componentData: component.componentData});
this.postThemeToComponent(component);
}
deactivateComponent(component) {
var didChange = component.active != false;
component.active = false;
component.sessionKey = null;
for(var handler of this.handlers) {
if(handler.areas.includes(component.area)) {
handler.activationHandler(component);
}
}
if(didChange) {
component.setDirty(true);
this.syncManager.sync();
}
_.pull(this.activeComponents, component);
this.streamObservers = this.streamObservers.filter(function(o){
return o.component !== component;
})
this.contextStreamObservers = this.contextStreamObservers.filter(function(o){
return o.component !== component;
})
}
deleteComponent(component) {
this.modelManager.setItemToBeDeleted(component);
this.syncManager.sync();
}
isComponentActive(component) {
return component.active;
}
disableComponentForItem(component, item) {
if(component.disassociatedItemIds.indexOf(item.uuid) !== -1) {
return;
}
component.disassociatedItemIds.push(item.uuid);
component.setDirty(true);
this.syncManager.sync();
}
enableComponentsForItem(components, item) {
for(var component of components) {
_.pull(component.disassociatedItemIds, item.uuid);
component.setDirty(true);
}
this.syncManager.sync();
}
setEventFlowForComponent(component, on) {
component.ignoreEvents = !on;
}
iframeForComponent(component) {
for(var frame of document.getElementsByTagName("iframe")) {
var componentId = frame.dataset.componentId;
if(componentId === component.uuid) {
return frame;
}
}
}
}
angular.module('app.frontend').service('componentManager', ComponentManager);

View File

@@ -0,0 +1,69 @@
class PermissionsModal {
constructor() {
this.restrict = "E";
this.templateUrl = "frontend/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();
}
$scope.formattedPermissions = $scope.permissions.map(function(permission){
if(permission.name === "stream-items") {
var title = "Access to ";
var types = permission.content_types.map(function(type){
return (type + "s").toLowerCase();
})
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 " + typesString;
} else if(types.length == 2) {
typesString = typesString + " and " + type;
}
} else {
typesString += separator + type;
}
}
return title + typesString;
} else if(permission.name === "stream-context-item") {
var mapping = {
"editor-stack" : "working note",
"note-tags" : "working note"
}
return "Access to " + mapping[$scope.component.area];
}
})
}
}
angular.module('app.frontend').directive('permissionsModal', () => new PermissionsModal);

View File

@@ -7,7 +7,7 @@ class GlobalExtensionsMenu {
};
}
controller($scope, extensionManager, syncManager, modelManager, themeManager, editorManager) {
controller($scope, extensionManager, syncManager, modelManager, themeManager, editorManager, componentManager) {
'ngInject';
$scope.formData = {};
@@ -15,6 +15,7 @@ class GlobalExtensionsMenu {
$scope.extensionManager = extensionManager;
$scope.themeManager = themeManager;
$scope.editorManager = editorManager;
$scope.componentManager = componentManager;
$scope.selectedAction = function(action, extension) {
extensionManager.executeAction(action, extension, null, function(response){
@@ -69,6 +70,21 @@ class GlobalExtensionsMenu {
editorManager.removeDefaultEditor(editor);
}
// 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() {
@@ -93,7 +109,11 @@ class GlobalExtensionsMenu {
$scope.handleEditorLink(link, completion);
} else if(link.indexOf(".css") != -1 || type == "theme") {
$scope.handleThemeLink(link, completion);
} else {
} else if(type == "component") {
$scope.handleComponentLink(link, completion);
}
else {
$scope.handleActionLink(link, completion);
}
}
@@ -112,6 +132,11 @@ class GlobalExtensionsMenu {
completion();
}
$scope.handleComponentLink = function(link, completion) {
componentManager.installComponent(link);
completion();
}
$scope.handleActionLink = function(link, completion) {
if(link) {
extensionManager.addExtension(link, function(response){

View File

@@ -17,13 +17,14 @@ class EncryptionHelper {
}
static encryptItem(item, keys, version) {
var params = {};
// encrypt item key
var item_key = Neeto.crypto.generateRandomEncryptionKey();
if(version === "001") {
// legacy
item.enc_item_key = Neeto.crypto.encryptText(item_key, keys.mk, null);
params.enc_item_key = Neeto.crypto.encryptText(item_key, keys.mk, null);
} else {
item.enc_item_key = this._private_encryptString(item_key, keys.encryptionKey, keys.authKey, version);
params.enc_item_key = this._private_encryptString(item_key, keys.encryptionKey, keys.authKey, version);
}
// encrypt content
@@ -32,10 +33,11 @@ class EncryptionHelper {
var ciphertext = this._private_encryptString(JSON.stringify(item.createContentJSONFromProperties()), ek, ak, version);
if(version === "001") {
var authHash = Neeto.crypto.hmac256(ciphertext, ak);
item.auth_hash = authHash;
params.auth_hash = authHash;
}
item.content = ciphertext;
params.content = ciphertext;
return params;
}
static encryptionComponentsFromString(string, baseKey, encryptionKey, authKey) {

View File

@@ -8,7 +8,7 @@ class ModelManager {
this.itemChangeObservers = [];
this.items = [];
this._extensions = [];
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme"];
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme", "SN|Component"];
}
get allItems() {
@@ -71,29 +71,34 @@ class ModelManager {
}
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
var models = [], processedObjects = [];
var models = [], processedObjects = [], allModels = [];
// first loop should add and process items
for (var json_obj of items) {
json_obj = _.omit(json_obj, omitFields || [])
var item = this.findItem(json_obj["uuid"]);
_.omit(json_obj, omitFields);
if(item) {
item.updateFromJSON(json_obj);
}
if(json_obj["deleted"] == true || !_.includes(this.acceptableContentTypes, json_obj["content_type"])) {
if(item) {
allModels.push(item);
this.removeItemLocally(item)
}
continue;
}
_.omit(json_obj, omitFields);
if(!item) {
item = this.createItem(json_obj);
} else {
item.updateFromJSON(json_obj);
}
this.addItem(item);
allModels.push(item);
models.push(item);
processedObjects.push(json_obj);
}
@@ -106,14 +111,14 @@ class ModelManager {
}
}
this.notifySyncObserversOfModels(models);
this.notifySyncObserversOfModels(allModels);
return models;
}
notifySyncObserversOfModels(models) {
for(var observer of this.itemSyncObservers) {
var relevantItems = models.filter(function(item){return item.content_type == observer.type});
var relevantItems = models.filter(function(item){return item.content_type == observer.type || observer.type == "*"});
if(relevantItems.length > 0) {
observer.callback(relevantItems);
}
@@ -144,7 +149,11 @@ class ModelManager {
item = new Editor(json_obj);
} else if(json_obj.content_type == "SN|Theme") {
item = new Theme(json_obj);
} else {
} else if(json_obj.content_type == "SN|Component") {
item = new Component(json_obj);
}
else {
item = new Item(json_obj);
}

View File

@@ -25,14 +25,19 @@ $heading-height: 75px;
#editor-title-bar {
width: 100%;
padding: 20px;
padding-bottom: 10px;
background-color: white;
border-bottom: none;
z-index: 100;
height: $heading-height;
min-height: $heading-height;
height: auto;
// height: $heading-height;
// min-height: $heading-height;
padding-right: 10px;
overflow: visible;
&.fullscreen {
position: relative;
}
@@ -76,7 +81,19 @@ $heading-height: 75px;
.editor-tags {
clear: left;
width: 100%;
height: 25px;
// height: 25px;
overflow: visible;
position: relative;
#note-tags-component-container {
height: 50px;
#note-tags-iframe {
height: 50px;
width: 100%;
position: absolute;
}
}
.tags-input {
background-color: transparent;
@@ -125,3 +142,46 @@ $heading-height: 75px;
}
}
}
#editor-pane-component-stack {
width: 100%;
.component {
height: 50px;
width: 100%;
position: relative;
&:not(:last-child) {
border-bottom: 1px solid $bg-color;
}
&:first-child {
border-top: 1px solid $bg-color;
}
.exit-button {
width: 15px;
height: 100%;
position: absolute;
right: 0;
background-color: transparent;
cursor: pointer;
display: flex;
align-items: center;
color: rgba(black, 0.7);
text-align: center;
padding-left: 2px;
.content {
}
&:hover {
background-color: rgba(gray, 0.3);
}
}
iframe {
width: 100%;
}
}
}

View File

@@ -0,0 +1,89 @@
.permissions-modal {
position: fixed;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 10000;
width: 100vw;
height: 100vh;
background-color: rgba(gray, 0.3);
color: black;
font-size: 16px;
display: flex;
align-items: center;
.background {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
}
.content {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-color: white;
width: 700px;
// height: 500px;
margin: auto;
padding: 10px 30px;
padding-bottom: 30px;
// position: absolute;
// top: 0; left: 0; bottom: 0; right: 0;
overflow-y: scroll;
p {
margin-bottom: 8px;
margin-top: 0;
}
h3 {
margin-bottom: 0;
}
h4 {
margin-bottom: 6px;
}
}
.learn-more {
margin-top: 20px;
line-height: 1.3;
}
.status {
color: orange;
&.trusted {
color: $blue-color;
}
}
.buttons {
margin-top: 35px;
}
button.standard {
border-radius: 1px;
font-weight: bold;
padding: 6px 20px;
display: inline-block;
&:hover {
text-decoration: underline;
}
&.blue {
background-color: $blue-color;
color: white;
}
&.white {
color: black;
background-color: white;
border: 1px solid gray;
}
}
}

View File

@@ -2,6 +2,7 @@
flex: 1 10%;
max-width: 180px;
min-width: 100px;
width: 180px;
$tags-title-bar-height: 55px;

View File

@@ -9,3 +9,4 @@ $dark-gray: #2e2e2e;
@import "app/editor";
@import "app/extensions";
@import "app/menus";
@import "app/permissions-modal";

View File

@@ -100,6 +100,23 @@
%p.wrap.selectable.small{"ng-if" => "extension.showURL"} {{extension.url}}
%a.block.mt-5{"ng-click" => "deleteActionExtension(extension); $event.stopPropagation();"} Remove extension
%div{"ng-if" => "componentManager.components.length > 0"}
.container.no-bottom.section-margin
%h2 Components
%ul
%li{"ng-repeat" => "component in componentManager.components", "ng-click" => "component.showDetails = !component.showDetails"}
.container
%h3 {{component.name}}
%a{"ng-if" => "!componentManager.isComponentActive(component)", "ng-click" => "componentManager.activateComponent(component); $event.stopPropagation();"} Activate
%a{"ng-if" => "componentManager.isComponentActive(component)", "ng-click" => "componentManager.deactivateComponent(component); $event.stopPropagation();"} Deactivate
%div{"ng-if" => "component.showDetails"}
.link-group
%a.red{"ng-click" => "deleteComponent(component); $event.stopPropagation();"} Delete
%a{"ng-click" => "component.showLink = !component.showLink; $event.stopPropagation();"} Show Link
%a{"ng-if" => "component.permissions.length", "ng-click" => "revokePermissions(component); $event.stopPropagation();"} Revoke Permissions
%p.small.selectable.wrap{"ng-if" => "component.showLink"}
{{component.url}}
.container.section-margin
%h2.blue Install
%p.faded Enter an install link

View File

@@ -0,0 +1,25 @@
.background{"ng-click" => "dismiss()"}
.content
%h3 The following extension has requested these permissions:
%h4 Extension
%p Name: {{component.name}}
%p.wrap URL: {{component.url}}
%h4 Permissions
.permission{"ng-repeat" => "permission in formattedPermissions"}
%p {{permission}}
%h4 Status
%p.status{"ng-class" => "{'trusted' : component.trusted}"} {{component.trusted ? 'Trusted' : 'Untrusted'}}
.learn-more
%h4 Details
%p
Extensions use an offline messaging system to communicate. With <i>Trusted</i> extensions, data is never sent remotely without your consent. Learn more about extension permissions at
%a{"href" => "https://standardnotes.org/permissions", "target" => "_blank"} https://standardnotes.org/permissions.
.buttons
%button.standard.white{"ng-click" => "deny()"} Deny
%button.standard.blue{"ng-click" => "accept()"} Accept

View File

@@ -8,7 +8,9 @@
#save-status{"ng-class" => "{'red bold': ctrl.saveError, 'orange bold': ctrl.syncTakingTooLong}", "ng-bind-html" => "ctrl.noteStatus"}
.editor-tags
%input.tags-input{"type" => "text", "ng-keyup" => "$event.keyCode == 13 && $event.target.blur();",
#note-tags-component-container{"ng-if" => "ctrl.tagsComponent && ctrl.tagsComponent.active"}
%iframe#note-tags-iframe{"ng-src" => "{{ctrl.tagsComponent.url | trusted}}", "frameBorder" => "0", "sandbox" => "allow-scripts", "data-component-id" => "{{ctrl.tagsComponent.uuid}}"}
%input.tags-input{"ng-if" => "!(ctrl.tagsComponent && ctrl.tagsComponent.active)", "type" => "text", "ng-keyup" => "$event.keyCode == 13 && $event.target.blur();",
"ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"}
%ul.section-menu-bar{"ng-if" => "ctrl.note"}
%li{"ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
@@ -19,6 +21,8 @@
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleFullScreen()"} Toggle Fullscreen
%li
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()"} Delete Note
%li{"ng-if" => "ctrl.hasDisabledComponents()"}
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledComponents()"} Restore Disabled Components
%li{"ng-class" => "{'selected' : ctrl.showEditorMenu}", "click-outside" => "ctrl.showEditorMenu = false;", "is-open" => "ctrl.showEditorMenu"}
%label{"ng-click" => "ctrl.showEditorMenu = !ctrl.showEditorMenu; ctrl.showMenu = false; ctrl.showExtensions = false;"} Editor
@@ -29,6 +33,12 @@
%contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
.editor-content{"ng-if" => "ctrl.noteReady", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"}
%iframe#editor-iframe{"ng-if" => "ctrl.editor && !ctrl.editor.systemEditor", "ng-src" => "{{ctrl.editor.url | trusted}}", "frameBorder" => "0", "style" => "width: 100%;"}
%iframe#editor-iframe{"ng-if" => "ctrl.editor && !ctrl.editor.systemEditor", "ng-src" => "{{ctrl.editor.url | trusted}}", "frameBorder" => "0", "style" => "width: 100%;", "sandbox" => "allow-scripts"}
Loading
%textarea.editable#note-text-editor{"ng-if" => "!ctrl.editor || ctrl.editor.systemEditor", "ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-model" => "ctrl.note.text",
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()"}
#editor-pane-component-stack
.component.component-stack-border{"ng-repeat" => "component in ctrl.componentStack", "ng-if" => "component.active", "ng-show" => "!component.ignoreEvents", "id" => "{{'component-' + component.uuid}}", "ng-mouseover" => "component.showExit = true", "ng-mouseleave" => "component.showExit = false"}
.exit-button.body-text-color{"ng-if" => "component.showExit", "ng-click" => "ctrl.disableComponent(component)"} ×
%iframe#note-tags-iframe{"ng-src" => "{{component.url | trusted}}", "frameBorder" => "0", "sandbox" => "allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-modals", "data-component-id" => "{{component.uuid}}"}

View File

@@ -1,9 +1,9 @@
.section.tags
#tags-content.content
.section.tags#tags-column
%iframe#tags-list-iframe{"ng-if" => "ctrl.component && ctrl.component.active", "ng-src" => "{{ctrl.component.url | trusted}}", "frameBorder" => "0", "style" => "width: 100%; height: 100%;", "sandbox" => "allow-scripts"}
#tags-content.content{"ng-if" => "!(ctrl.component && ctrl.component.active)"}
#tags-title-bar.section-title-bar
.title Tags
.add-button#tag-add-button{"ng-click" => "ctrl.clickedAddNewTag()"} +
{{ctrl.test}}
.scrollable
.tag{"ng-if" => "ctrl.allTag", "ng-click" => "ctrl.selectTag(ctrl.allTag)", "ng-class" => "{'selected' : ctrl.selectedTag == ctrl.allTag}"}