Components system
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
class BaseCtrl {
|
class BaseCtrl {
|
||||||
constructor($rootScope, $scope, syncManager, dbManager, analyticsManager) {
|
constructor($rootScope, $scope, syncManager, dbManager, analyticsManager, componentManager) {
|
||||||
dbManager.openDatabase(null, function(){
|
dbManager.openDatabase(null, function(){
|
||||||
// new database, delete syncToken so that items can be refetched entirely from server
|
// new database, delete syncToken so that items can be refetched entirely from server
|
||||||
syncManager.clearSyncToken();
|
syncManager.clearSyncToken();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ angular.module('app.frontend')
|
|||||||
link:function(scope, elem, attrs, ctrl) {
|
link:function(scope, elem, attrs, ctrl) {
|
||||||
scope.$watch('ctrl.note', function(note, oldNote){
|
scope.$watch('ctrl.note', function(note, oldNote){
|
||||||
if(note) {
|
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(){
|
$rootScope.$on("theme-changed", function(){
|
||||||
this.postThemeToExternalEditor();
|
this.postThemeToExternalEditor();
|
||||||
@@ -51,10 +54,76 @@ angular.module('app.frontend')
|
|||||||
this.loadTagsString();
|
this.loadTagsString();
|
||||||
}.bind(this));
|
}.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){
|
window.addEventListener("message", function(event){
|
||||||
if(event.data.status) {
|
if(event.data.status) {
|
||||||
this.postNoteToExternalEditor();
|
this.postNoteToExternalEditor();
|
||||||
} else {
|
} else if(!event.data.api) {
|
||||||
// console.log("Received message", event.data);
|
// console.log("Received message", event.data);
|
||||||
var id = event.data.id;
|
var id = event.data.id;
|
||||||
var text = event.data.text;
|
var text = event.data.text;
|
||||||
@@ -75,8 +144,16 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}.bind(this), false);
|
}.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.setNote = function(note, oldNote) {
|
||||||
this.noteReady = false;
|
|
||||||
var currentEditor = this.editor;
|
var currentEditor = this.editor;
|
||||||
this.editor = null;
|
this.editor = null;
|
||||||
this.showExtensions = false;
|
this.showExtensions = false;
|
||||||
@@ -90,6 +167,11 @@ angular.module('app.frontend')
|
|||||||
}.bind(this)
|
}.bind(this)
|
||||||
|
|
||||||
var editor = this.editorForNote(note);
|
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(editor) {
|
||||||
if(currentEditor !== editor) {
|
if(currentEditor !== editor) {
|
||||||
// switch after timeout, so that note data isnt posted to current 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.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) {
|
this.updateTagsFromTagsString = function($event) {
|
||||||
$event.target.blur();
|
$event.target.blur();
|
||||||
|
|
||||||
@@ -314,4 +417,40 @@ angular.module('app.frontend')
|
|||||||
this.updateTags()(this.note, tags);
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ angular.module('app.frontend')
|
|||||||
themeManager.activateInitialTheme();
|
themeManager.activateInitialTheme();
|
||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
|
|
||||||
|
|
||||||
syncManager.sync(null);
|
syncManager.sync(null);
|
||||||
// refresh every 30s
|
// refresh every 30s
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
@@ -56,6 +57,7 @@ angular.module('app.frontend')
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
$scope.updateTagsForNote = function(note, stringTags) {
|
$scope.updateTagsForNote = function(note, stringTags) {
|
||||||
|
console.log("Updating tags", stringTags);
|
||||||
var toRemove = [];
|
var toRemove = [];
|
||||||
for(var tag of note.tags) {
|
for(var tag of note.tags) {
|
||||||
if(stringTags.indexOf(tag.title) === -1) {
|
if(stringTags.indexOf(tag.title) === -1) {
|
||||||
|
|||||||
@@ -33,10 +33,39 @@ angular.module('app.frontend')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.controller('TagsCtrl', function (modelManager, $timeout) {
|
.controller('TagsCtrl', function ($rootScope, modelManager, $timeout, componentManager) {
|
||||||
|
|
||||||
var initialLoad = true;
|
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.setAllTag = function(allTag) {
|
||||||
this.selectTag(this.allTag);
|
this.selectTag(this.allTag);
|
||||||
}
|
}
|
||||||
|
|||||||
59
app/assets/javascripts/app/frontend/models/app/component.js
Normal file
59
app/assets/javascripts/app/frontend/models/app/component.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,30 +22,26 @@ class ItemParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
paramsForSync() {
|
paramsForSync() {
|
||||||
return this.__params(null, false);
|
return this.__params();
|
||||||
}
|
}
|
||||||
|
|
||||||
__params() {
|
__params() {
|
||||||
let encryptionVersion = "001";
|
let encryptionVersion = "001";
|
||||||
|
|
||||||
var itemCopy = _.cloneDeep(this.item);
|
|
||||||
|
|
||||||
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
|
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};
|
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()) {
|
if(this.keys && !this.item.doNotEncrypt()) {
|
||||||
EncryptionHelper.encryptItem(itemCopy, this.keys, encryptionVersion);
|
var encryptedParams = EncryptionHelper.encryptItem(this.item, this.keys, encryptionVersion);
|
||||||
params.content = itemCopy.content;
|
_.merge(params, encryptedParams);
|
||||||
params.enc_item_key = itemCopy.enc_item_key;
|
|
||||||
if(encryptionVersion === "001") {
|
if(encryptionVersion !== "001") {
|
||||||
params.auth_hash = itemCopy.auth_hash;
|
|
||||||
} else {
|
|
||||||
params.auth_hash = null;
|
params.auth_hash = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
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) {
|
if(!this.forExportFile) {
|
||||||
params.enc_item_key = null;
|
params.enc_item_key = null;
|
||||||
params.auth_hash = null;
|
params.auth_hash = null;
|
||||||
|
|||||||
499
app/assets/javascripts/app/services/componentManager.js
Normal file
499
app/assets/javascripts/app/services/componentManager.js
Normal 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);
|
||||||
@@ -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);
|
||||||
@@ -7,7 +7,7 @@ class GlobalExtensionsMenu {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
controller($scope, extensionManager, syncManager, modelManager, themeManager, editorManager) {
|
controller($scope, extensionManager, syncManager, modelManager, themeManager, editorManager, componentManager) {
|
||||||
'ngInject';
|
'ngInject';
|
||||||
|
|
||||||
$scope.formData = {};
|
$scope.formData = {};
|
||||||
@@ -15,6 +15,7 @@ class GlobalExtensionsMenu {
|
|||||||
$scope.extensionManager = extensionManager;
|
$scope.extensionManager = extensionManager;
|
||||||
$scope.themeManager = themeManager;
|
$scope.themeManager = themeManager;
|
||||||
$scope.editorManager = editorManager;
|
$scope.editorManager = editorManager;
|
||||||
|
$scope.componentManager = componentManager;
|
||||||
|
|
||||||
$scope.selectedAction = function(action, extension) {
|
$scope.selectedAction = function(action, extension) {
|
||||||
extensionManager.executeAction(action, extension, null, function(response){
|
extensionManager.executeAction(action, extension, null, function(response){
|
||||||
@@ -69,6 +70,21 @@ class GlobalExtensionsMenu {
|
|||||||
editorManager.removeDefaultEditor(editor);
|
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
|
// Installation
|
||||||
|
|
||||||
$scope.submitInstallLink = function() {
|
$scope.submitInstallLink = function() {
|
||||||
@@ -93,7 +109,11 @@ class GlobalExtensionsMenu {
|
|||||||
$scope.handleEditorLink(link, completion);
|
$scope.handleEditorLink(link, completion);
|
||||||
} else if(link.indexOf(".css") != -1 || type == "theme") {
|
} else if(link.indexOf(".css") != -1 || type == "theme") {
|
||||||
$scope.handleThemeLink(link, completion);
|
$scope.handleThemeLink(link, completion);
|
||||||
} else {
|
} else if(type == "component") {
|
||||||
|
$scope.handleComponentLink(link, completion);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
$scope.handleActionLink(link, completion);
|
$scope.handleActionLink(link, completion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,6 +132,11 @@ class GlobalExtensionsMenu {
|
|||||||
completion();
|
completion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.handleComponentLink = function(link, completion) {
|
||||||
|
componentManager.installComponent(link);
|
||||||
|
completion();
|
||||||
|
}
|
||||||
|
|
||||||
$scope.handleActionLink = function(link, completion) {
|
$scope.handleActionLink = function(link, completion) {
|
||||||
if(link) {
|
if(link) {
|
||||||
extensionManager.addExtension(link, function(response){
|
extensionManager.addExtension(link, function(response){
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ class EncryptionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static encryptItem(item, keys, version) {
|
static encryptItem(item, keys, version) {
|
||||||
|
var params = {};
|
||||||
// encrypt item key
|
// encrypt item key
|
||||||
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
var item_key = Neeto.crypto.generateRandomEncryptionKey();
|
||||||
if(version === "001") {
|
if(version === "001") {
|
||||||
// legacy
|
// 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 {
|
} 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
|
// encrypt content
|
||||||
@@ -32,10 +33,11 @@ class EncryptionHelper {
|
|||||||
var ciphertext = this._private_encryptString(JSON.stringify(item.createContentJSONFromProperties()), ek, ak, version);
|
var ciphertext = this._private_encryptString(JSON.stringify(item.createContentJSONFromProperties()), ek, ak, version);
|
||||||
if(version === "001") {
|
if(version === "001") {
|
||||||
var authHash = Neeto.crypto.hmac256(ciphertext, ak);
|
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) {
|
static encryptionComponentsFromString(string, baseKey, encryptionKey, authKey) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class ModelManager {
|
|||||||
this.itemChangeObservers = [];
|
this.itemChangeObservers = [];
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this._extensions = [];
|
this._extensions = [];
|
||||||
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme"];
|
this.acceptableContentTypes = ["Note", "Tag", "Extension", "SN|Editor", "SN|Theme", "SN|Component"];
|
||||||
}
|
}
|
||||||
|
|
||||||
get allItems() {
|
get allItems() {
|
||||||
@@ -71,29 +71,34 @@ class ModelManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
|
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
|
||||||
var models = [], processedObjects = [];
|
var models = [], processedObjects = [], allModels = [];
|
||||||
|
|
||||||
// first loop should add and process items
|
// first loop should add and process items
|
||||||
for (var json_obj of items) {
|
for (var json_obj of items) {
|
||||||
json_obj = _.omit(json_obj, omitFields || [])
|
json_obj = _.omit(json_obj, omitFields || [])
|
||||||
var item = this.findItem(json_obj["uuid"]);
|
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(json_obj["deleted"] == true || !_.includes(this.acceptableContentTypes, json_obj["content_type"])) {
|
||||||
if(item) {
|
if(item) {
|
||||||
|
allModels.push(item);
|
||||||
this.removeItemLocally(item)
|
this.removeItemLocally(item)
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_.omit(json_obj, omitFields);
|
|
||||||
|
|
||||||
if(!item) {
|
if(!item) {
|
||||||
item = this.createItem(json_obj);
|
item = this.createItem(json_obj);
|
||||||
} else {
|
|
||||||
item.updateFromJSON(json_obj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addItem(item);
|
this.addItem(item);
|
||||||
|
|
||||||
|
allModels.push(item);
|
||||||
models.push(item);
|
models.push(item);
|
||||||
processedObjects.push(json_obj);
|
processedObjects.push(json_obj);
|
||||||
}
|
}
|
||||||
@@ -106,14 +111,14 @@ class ModelManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notifySyncObserversOfModels(models);
|
this.notifySyncObserversOfModels(allModels);
|
||||||
|
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
notifySyncObserversOfModels(models) {
|
notifySyncObserversOfModels(models) {
|
||||||
for(var observer of this.itemSyncObservers) {
|
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) {
|
if(relevantItems.length > 0) {
|
||||||
observer.callback(relevantItems);
|
observer.callback(relevantItems);
|
||||||
}
|
}
|
||||||
@@ -144,7 +149,11 @@ class ModelManager {
|
|||||||
item = new Editor(json_obj);
|
item = new Editor(json_obj);
|
||||||
} else if(json_obj.content_type == "SN|Theme") {
|
} else if(json_obj.content_type == "SN|Theme") {
|
||||||
item = new Theme(json_obj);
|
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);
|
item = new Item(json_obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,14 +25,19 @@ $heading-height: 75px;
|
|||||||
#editor-title-bar {
|
#editor-title-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
height: $heading-height;
|
height: auto;
|
||||||
min-height: $heading-height;
|
// height: $heading-height;
|
||||||
|
// min-height: $heading-height;
|
||||||
|
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
&.fullscreen {
|
&.fullscreen {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -76,7 +81,19 @@ $heading-height: 75px;
|
|||||||
.editor-tags {
|
.editor-tags {
|
||||||
clear: left;
|
clear: left;
|
||||||
width: 100%;
|
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 {
|
.tags-input {
|
||||||
background-color: transparent;
|
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
89
app/assets/stylesheets/app/_permissions-modal.scss
Normal file
89
app/assets/stylesheets/app/_permissions-modal.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
flex: 1 10%;
|
flex: 1 10%;
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
width: 180px;
|
||||||
|
|
||||||
$tags-title-bar-height: 55px;
|
$tags-title-bar-height: 55px;
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ $dark-gray: #2e2e2e;
|
|||||||
@import "app/editor";
|
@import "app/editor";
|
||||||
@import "app/extensions";
|
@import "app/extensions";
|
||||||
@import "app/menus";
|
@import "app/menus";
|
||||||
|
@import "app/permissions-modal";
|
||||||
|
|||||||
@@ -100,6 +100,23 @@
|
|||||||
%p.wrap.selectable.small{"ng-if" => "extension.showURL"} {{extension.url}}
|
%p.wrap.selectable.small{"ng-if" => "extension.showURL"} {{extension.url}}
|
||||||
%a.block.mt-5{"ng-click" => "deleteActionExtension(extension); $event.stopPropagation();"} Remove extension
|
%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
|
.container.section-margin
|
||||||
%h2.blue Install
|
%h2.blue Install
|
||||||
%p.faded Enter an install link
|
%p.faded Enter an install link
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
#save-status{"ng-class" => "{'red bold': ctrl.saveError, 'orange bold': ctrl.syncTakingTooLong}", "ng-bind-html" => "ctrl.noteStatus"}
|
#save-status{"ng-class" => "{'red bold': ctrl.saveError, 'orange bold': ctrl.syncTakingTooLong}", "ng-bind-html" => "ctrl.noteStatus"}
|
||||||
|
|
||||||
.editor-tags
|
.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)"}
|
"ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"}
|
||||||
%ul.section-menu-bar{"ng-if" => "ctrl.note"}
|
%ul.section-menu-bar{"ng-if" => "ctrl.note"}
|
||||||
%li{"ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
|
%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
|
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleFullScreen()"} Toggle Fullscreen
|
||||||
%li
|
%li
|
||||||
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()"} Delete Note
|
%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"}
|
%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
|
%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"}
|
%contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
|
||||||
|
|
||||||
.editor-content{"ng-if" => "ctrl.noteReady", "ng-class" => "{'fullscreen' : ctrl.fullscreen }"}
|
.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",
|
%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()"}
|
"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}}"}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.section.tags
|
.section.tags#tags-column
|
||||||
#tags-content.content
|
%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
|
#tags-title-bar.section-title-bar
|
||||||
.title Tags
|
.title Tags
|
||||||
.add-button#tag-add-button{"ng-click" => "ctrl.clickedAddNewTag()"} +
|
.add-button#tag-add-button{"ng-click" => "ctrl.clickedAddNewTag()"} +
|
||||||
{{ctrl.test}}
|
|
||||||
|
|
||||||
.scrollable
|
.scrollable
|
||||||
.tag{"ng-if" => "ctrl.allTag", "ng-click" => "ctrl.selectTag(ctrl.allTag)", "ng-class" => "{'selected' : ctrl.selectedTag == ctrl.allTag}"}
|
.tag{"ng-if" => "ctrl.allTag", "ng-click" => "ctrl.selectTag(ctrl.allTag)", "ng-class" => "{'selected' : ctrl.selectedTag == ctrl.allTag}"}
|
||||||
|
|||||||
Reference in New Issue
Block a user