Merge pull request #165 from standardnotes/editor-components

Editor components
This commit is contained in:
Mo Bitar
2017-11-20 17:31:27 +00:00
committed by GitHub
20 changed files with 489 additions and 406 deletions

View File

@@ -20,32 +20,14 @@ angular.module('app.frontend')
ctrl.noteDidChange(note, oldNote);
}
});
scope.$watch('ctrl.note.text', function(newText){
if(!ctrl.note) {
return;
}
// ignore this change if it originated from here
if(ctrl.changingTextFromEditor) {
ctrl.changingTextFromEditor = false;
return;
}
ctrl.postNoteToExternalEditor(ctrl.note);
})
}
}
})
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, editorManager, themeManager, componentManager, storageManager) {
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, extensionManager, syncManager, modelManager, themeManager, componentManager, storageManager) {
this.componentManager = componentManager;
this.componentStack = [];
$rootScope.$on("theme-changed", function(){
this.postThemeToExternalEditor();
}.bind(this))
$rootScope.$on("sync:taking-too-long", function(){
this.syncTakingTooLong = true;
}.bind(this));
@@ -58,140 +40,30 @@ 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") {
if(data.item.content_type == "Tag") {
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 if(!event.data.api) {
// console.log("Received message", event.data);
var id = event.data.id;
var text = event.data.text;
var data = event.data.data;
if(this.note.uuid === id) {
// to ignore $watch events
this.changingTextFromEditor = true;
this.note.text = text;
if(data) {
var changesMade = this.editor.setData(id, data);
if(changesMade) {
this.editor.setDirty(true);
}
}
this.changesMade();
}
}
}.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.reloadComponentContext();
}
this.setNote = function(note, oldNote) {
var currentEditor = this.editor;
this.editor = null;
this.showExtensions = false;
this.showMenu = false;
this.loadTagsString();
var setEditor = function(editor) {
this.editor = editor;
this.postNoteToExternalEditor();
this.noteReady = true;
}.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
$timeout(function(){
setEditor(editor);
}.bind(this));
} else {
// switch immediately
setEditor(editor);
}
} else {
this.editor = null;
this.noteReady = true;
let associatedEditor = this.editorForNote(note);
if(this.editorComponent && this.editorComponent != associatedEditor) {
// Deactivate old editor
componentManager.deactivateComponent(this.editorComponent);
}
// Activate new editor if it's different from the one currently activated
if(associatedEditor && associatedEditor != this.editorComponent) {
this.enableComponent(associatedEditor);
}
this.editorComponent = associatedEditor;
this.noteReady = true;
if(note.safeText().length == 0 && note.dummy) {
this.focusTitle(100);
@@ -206,62 +78,44 @@ angular.module('app.frontend')
}
}
this.selectedEditor = function(editor) {
this.showEditorMenu = false;
if(this.editor && editor !== this.editor) {
this.editor.removeItemAsRelationship(this.note);
this.editor.setDirty(true);
}
editor.addItemAsRelationship(this.note);
editor.setDirty(true);
syncManager.sync();
this.editor = editor;
}.bind(this)
this.editorForNote = function(note) {
var editors = modelManager.itemsForContentType("SN|Editor");
let editors = componentManager.componentsForArea("editor-editor");
for(var editor of editors) {
if(_.includes(editor.notes, note)) {
if(editor.isActiveForItem(note)) {
return editor;
}
}
return _.find(editors, {default: true});
}
this.postDataToExternalEditor = function(data) {
var externalEditorElement = document.getElementById("editor-iframe");
if(externalEditorElement) {
externalEditorElement.contentWindow.postMessage(data, '*');
// No editor found for note. Use default editor, if note does not prefer system editor
if(!note.getAppDataItem("prefersPlainEditor")) {
return editors.filter((e) => {return e.isDefaultEditor()})[0];
}
}
function themeData() {
return {
themes: [themeManager.currentTheme ? themeManager.currentTheme.url : null]
}
}
this.selectedEditor = function(editorComponent) {
this.showEditorMenu = false;
this.postThemeToExternalEditor = function() {
this.postDataToExternalEditor(themeData())
}
this.postNoteToExternalEditor = function() {
if(!this.editor) {
return;
if(this.editorComponent && this.editorComponent !== editorComponent) {
// This disassociates the editor from the note, but the component itself still needs to be deactivated
this.disableComponentForCurrentItem(this.editorComponent);
// Now deactivate the component
componentManager.deactivateComponent(this.editorComponent);
}
var data = {
text: this.note.text,
data: this.editor.dataForKey(this.note.uuid),
id: this.note.uuid,
if(editorComponent) {
this.note.setAppDataItem("prefersPlainEditor", false);
this.note.setDirty(true);
this.enableComponent(editorComponent);
this.associateComponentWithCurrentItem(editorComponent);
} else {
// Note prefers plain editor
this.note.setAppDataItem("prefersPlainEditor", true);
this.note.setDirty(true);
syncManager.sync();
}
_.merge(data, themeData());
this.postDataToExternalEditor(data);
}
this.editorComponent = editorComponent;
}.bind(this)
this.hasAvailableExtensions = function() {
return extensionManager.extensionsInContextOfItem(this.note).length > 0;
@@ -295,20 +149,16 @@ angular.module('app.frontend')
if(success) {
if(statusTimeout) $timeout.cancel(statusTimeout);
statusTimeout = $timeout(function(){
var status = "All changes saved";
if(authManager.offline()) {
status += " (offline)";
}
this.saveError = false;
this.syncTakingTooLong = false;
this.noteStatus = $sce.trustAsHtml(status);
this.showAllChangesSavedStatus();
}.bind(this), 200)
} else {
if(statusTimeout) $timeout.cancel(statusTimeout);
statusTimeout = $timeout(function(){
this.saveError = true;
this.syncTakingTooLong = false;
this.noteStatus = $sce.trustAsHtml("Error syncing<br>(changes saved offline)")
this.showErrorStatus();
}.bind(this), 200)
}
}.bind(this));
@@ -328,11 +178,26 @@ angular.module('app.frontend')
if(saveTimeout) $timeout.cancel(saveTimeout);
if(statusTimeout) $timeout.cancel(statusTimeout);
saveTimeout = $timeout(function(){
this.noteStatus = $sce.trustAsHtml("Saving...");
this.showSavingStatus();
this.saveNote();
}.bind(this), 275)
}
this.showSavingStatus = function() {
this.noteStatus = $sce.trustAsHtml("Saving...");
}
this.showAllChangesSavedStatus = function() {
var status = "All changes saved";
if(authManager.offline()) {
status += " (offline)";
}
this.noteStatus = $sce.trustAsHtml(status);
}
this.showErrorStatus = function() {
this.noteStatus = $sce.trustAsHtml("Error syncing<br>(changes saved offline)")
}
this.contentChanged = function() {
this.changesMade();
@@ -388,7 +253,6 @@ angular.module('app.frontend')
}
this.clickedEditNote = function() {
this.editorMode = 'edit';
this.focusEditor(100);
}
@@ -454,18 +318,131 @@ angular.module('app.frontend')
Components
*/
let alertKey = "displayed-component-disable-alert";
componentManager.registerHandler({identifier: "editor", areas: ["note-tags", "editor-stack", "editor-editor"], activationHandler: function(component){
this.disableComponent = function(component) {
componentManager.disableComponentForItem(component, this.note);
componentManager.setEventFlowForComponent(component, false);
if(!storageManager.getItem(alertKey)) {
if(component.area === "note-tags") {
// Autocomplete Tags
this.tagsComponent = component.active ? component : null;
} else if(component.area == "editor-stack") {
// Stack
if(component.active) {
if(!_.find(this.componentStack, component)) {
this.componentStack.push(component);
}
} else {
_.pull(this.componentStack, component);
}
} else {
// Editor
if(component.active && this.note && component.isActiveForItem(this.note)) {
this.editorComponent = component;
} else {
this.editorComponent = null;
}
}
if(component.active) {
$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") {
if(data.item.content_type == "Tag") {
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);
}
else if(action === "save-items" || action === "save-success" || action == "save-error") {
if(data.items.map((item) => {return item.uuid}).includes(this.note.uuid)) {
if(action == "save-items") {
if(this.componentSaveTimeout) $timeout.cancel(this.componentSaveTimeout);
this.componentSaveTimeout = $timeout(this.showSavingStatus.bind(this), 10);
}
else {
if(this.componentStatusTimeout) $timeout.cancel(this.componentStatusTimeout);
if(action == "save-success") {
this.componentStatusTimeout = $timeout(this.showAllChangesSavedStatus.bind(this), 400);
} else {
this.componentStatusTimeout = $timeout(this.showErrorStatus.bind(this), 400);
}
}
}
}
}.bind(this)});
this.reloadComponentContext = function() {
for(var component of this.componentStack) {
componentManager.setEventFlowForComponent(component, component.isActiveForItem(this.note));
}
componentManager.contextItemDidChangeInArea("note-tags");
componentManager.contextItemDidChangeInArea("editor-stack");
componentManager.contextItemDidChangeInArea("editor-editor");
}
this.enableComponent = function(component) {
componentManager.activateComponent(component);
componentManager.setEventFlowForComponent(component, 1);
}
this.associateComponentWithCurrentItem = function(component) {
componentManager.associateComponentWithItem(component, this.note);
}
let alertKey = "displayed-component-disable-alert";
this.disableComponentForCurrentItem = function(component, showAlert) {
componentManager.disassociateComponentWithItem(component, this.note);
componentManager.setEventFlowForComponent(component, 0);
if(showAlert && !storageManager.getItem(alertKey)) {
alert("This component will be disabled for this note. You can re-enable this component in the 'Menu' of the editor pane.");
storageManager.setItem(alertKey, true);
}
}
this.hasDisabledComponents = function() {
this.hasDisabledStackComponents = function() {
for(var component of this.componentStack) {
if(component.ignoreEvents) {
return true;
@@ -475,7 +452,7 @@ angular.module('app.frontend')
return false;
}
this.restoreDisabledComponents = function() {
this.restoreDisabledStackComponents = function() {
var relevantComponents = this.componentStack.filter(function(component){
return component.ignoreEvents;
})

View File

@@ -1,6 +1,6 @@
angular.module('app.frontend')
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager,
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager) {
dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager) {
storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());

View File

@@ -49,9 +49,11 @@ angular.module('app.frontend')
this.selectFirstNote(false);
}.bind(this))
this.notesToDisplay = 20;
this.DefaultNotesToDisplayValue = 20;
this.notesToDisplay = this.DefaultNotesToDisplayValue;
this.paginate = function() {
this.notesToDisplay += 20
this.notesToDisplay += this.DefaultNotesToDisplayValue
}
this.optionsSubtitle = function() {
@@ -77,6 +79,14 @@ angular.module('app.frontend')
}
this.tagDidChange = function(tag, oldTag) {
var scrollable = document.getElementById("notes-scrollable");
if(scrollable) {
scrollable.scrollTop = 0;
scrollable.scrollLeft = 0;
}
this.notesToDisplay = this.DefaultNotesToDisplayValue;
this.showMenu = false;
if(this.selectedNote && this.selectedNote.dummy) {

View File

@@ -151,17 +151,17 @@ class Item {
App Data
*/
setAppDataItem(key, value) {
var data = this.appData[AppDomain];
setDomainDataItem(key, value, domain) {
var data = this.appData[domain];
if(!data) {
data = {}
}
data[key] = value;
this.appData[AppDomain] = data;
this.appData[domain] = data;
}
getAppDataItem(key) {
var data = this.appData[AppDomain];
getDomainDataItem(key, domain) {
var data = this.appData[domain];
if(data) {
return data[key];
} else {
@@ -169,6 +169,14 @@ class Item {
}
}
setAppDataItem(key, value) {
this.setDomainDataItem(key, value, AppDomain);
}
getAppDataItem(key) {
return this.getDomainDataItem(key, AppDomain);
}
get pinned() {
return this.getAppDataItem("pinned");
}

View File

@@ -10,6 +10,10 @@ class Component extends Item {
if(!this.disassociatedItemIds) {
this.disassociatedItemIds = [];
}
if(!this.associatedItemIds) {
this.associatedItemIds = [];
}
}
mapContentToLocalProperties(content) {
@@ -28,6 +32,9 @@ class Component extends Item {
// items that have requested a component to be disabled in its context
this.disassociatedItemIds = content.disassociatedItemIds || [];
// items that have requested a component to be enabled in its context
this.associatedItemIds = content.associatedItemIds || [];
}
structureParams() {
@@ -38,7 +45,8 @@ class Component extends Item {
permissions: this.permissions,
active: this.active,
componentData: this.componentData,
disassociatedItemIds: this.disassociatedItemIds
disassociatedItemIds: this.disassociatedItemIds,
associatedItemIds: this.associatedItemIds,
};
_.merge(params, super.structureParams());
@@ -53,7 +61,36 @@ class Component extends Item {
return "SN|Component";
}
isEditor() {
return this.area == "editor-editor";
}
isDefaultEditor() {
return this.getAppDataItem("defaultEditor") == true;
}
/*
An associative component depends on being explicitly activated for a given item, compared to a dissaciative component,
which is enabled by default in areas unrelated to a certain item.
*/
static associativeAreas() {
return ["editor-editor"];
}
isAssociative() {
return Component.associativeAreas().includes(this.area);
}
associateWithItem(item) {
this.associatedItemIds.push(item.uuid);
}
isActiveForItem(item) {
return this.disassociatedItemIds.indexOf(item.uuid) === -1;
if(this.isAssociative()) {
return this.associatedItemIds.indexOf(item.uuid) !== -1;
} else {
return this.disassociatedItemIds.indexOf(item.uuid) === -1;
}
}
}

View File

@@ -1,3 +1,6 @@
/* This domain will be used to save context item client data */
let ClientDataDomain = "org.standardnotes.sn.components";
class ComponentManager {
constructor($rootScope, modelManager, syncManager, themeManager, $timeout, $compile) {
@@ -28,7 +31,15 @@ class ComponentManager {
this.handleMessage(this.componentForSessionKey(event.data.sessionKey), event.data);
}.bind(this), false);
this.modelManager.addItemSyncObserver("component-manager", "*", function(allItems, validItems, deletedItems) {
this.modelManager.addItemSyncObserver("component-manager", "*", function(allItems, validItems, deletedItems, source) {
/* If the source of these new or updated items is from a Component itself saving items, we don't need to notify
components again of the same item. Regarding notifying other components than the issuing component, other mapping sources
will take care of that, like ModelManager.MappingSourceRemoteSaved
*/
if(source == ModelManager.MappingSourceComponentRetrieved) {
return;
}
var syncedComponents = allItems.filter(function(item){return item.content_type === "SN|Component" });
for(var component of syncedComponents) {
@@ -62,23 +73,23 @@ class ComponentManager {
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) {
if(!handler.areas.includes(observer.component.area)) {
continue;
}
var itemInContext = handler.contextRequestHandler(observer.component);
if(itemInContext) {
var matchingItem = _.find(allItems, {uuid: itemInContext.uuid});
if(matchingItem) {
this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage);
this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage, source);
}
}
}
}.bind(this))
}
}.bind(this))
}
@@ -115,24 +126,37 @@ class ComponentManager {
}
}
jsonForItem(item) {
jsonForItem(item, component, source) {
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();
params.clientData = item.getDomainDataItem(component.url, ClientDataDomain) || {};
/* This means the this function is being triggered through a remote Saving response, which should not update
actual local content values. The reason is, Save responses may be delayed, and a user may have changed some values
in between the Save was initiated, and the time it completes. So we only want to update actual content values (and not just metadata)
when its another source, like ModelManager.MappingSourceRemoteRetrieved.
*/
if(source && source == ModelManager.MappingSourceRemoteSaved) {
params.isMetadataUpdate = true;
}
this.removePrivatePropertiesFromResponseItems([params]);
return params;
}
sendItemsInReply(component, items, message) {
sendItemsInReply(component, items, message, source) {
if(this.loggingEnabled) {console.log("Web|componentManager|sendItemsInReply", component, items, message)};
var response = {items: {}};
var mapped = items.map(function(item) {
return this.jsonForItem(item);
return this.jsonForItem(item, component, source);
}.bind(this));
response.items = mapped;
this.replyToMessage(component, message, response);
}
sendContextItemInReply(component, item, originalMessage) {
var response = {item: this.jsonForItem(item)};
sendContextItemInReply(component, item, originalMessage, source) {
if(this.loggingEnabled) {console.log("Web|componentManager|sendContextItemInReply", component, item, originalMessage)};
var response = {item: this.jsonForItem(item, component, source)};
this.replyToMessage(component, originalMessage, response);
}
@@ -140,12 +164,18 @@ class ComponentManager {
return this.modelManager.itemsForContentType("SN|Component");
}
componentsForStack(stack) {
componentsForArea(area) {
return this.components.filter(function(component){
return component.area === stack;
return component.area === area;
})
}
componentForUrl(url) {
return this.components.filter(function(component){
return component.url === url;
})[0];
}
componentForSessionKey(key) {
return _.find(this.components, {sessionKey: key});
}
@@ -172,6 +202,8 @@ class ComponentManager {
create-item
delete-items
set-component-data
save-context-client-data
get-context-client-data
*/
if(message.action === "stream-items") {
@@ -202,24 +234,44 @@ class ComponentManager {
}
else if(message.action === "create-item") {
var item = this.modelManager.createItem(message.data.item);
var responseItem = message.data.item;
this.removePrivatePropertiesFromResponseItems([responseItem]);
var item = this.modelManager.createItem(responseItem);
if(responseItem.clientData) {
item.setDomainDataItem(component.url, responseItem.clientData, ClientDataDomain);
}
this.modelManager.addItem(item);
this.modelManager.resolveReferencesForItem(item);
item.setDirty(true);
this.syncManager.sync();
this.replyToMessage(component, message, {item: this.jsonForItem(item)})
this.replyToMessage(component, message, {item: this.jsonForItem(item, component)})
}
else if(message.action === "save-items") {
var responseItems = message.data.items;
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems);
this.removePrivatePropertiesFromResponseItems(responseItems);
/*
We map the items here because modelManager is what updates the UI. If you were to instead get the items directly,
this would update them server side via sync, but would never make its way back to the UI.
*/
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved);
for(var item of localItems) {
var responseItem = _.find(responseItems, {uuid: item.uuid});
_.merge(item.content, responseItem.content);
if(responseItem.clientData) {
item.setDomainDataItem(component.url, responseItem.clientData, ClientDataDomain);
}
item.setDirty(true);
}
this.syncManager.sync();
this.syncManager.sync((response) => {
// Allow handlers to be notified when a save begins and ends, to update the UI
var saveMessage = Object.assign({}, message);
saveMessage.action = response && response.error ? "save-error" : "save-success";
this.handleMessage(component, saveMessage);
});
}
for(let handler of this.handlers) {
@@ -231,6 +283,21 @@ class ComponentManager {
}
}
removePrivatePropertiesFromResponseItems(responseItems) {
// Don't allow component to overwrite these properties.
let privateProperties = ["appData"];
for(var responseItem of responseItems) {
// Do not pass in actual items here, otherwise that would be destructive.
// Instead, generic JS/JSON objects should be passed.
console.assert(typeof responseItem.setDirty !== 'function');
for(var prop of privateProperties) {
delete responseItem[prop];
}
}
}
handleStreamItemsMessage(component, message) {
var requiredPermissions = [
{
@@ -378,10 +445,13 @@ class ComponentManager {
sendMessageToComponent(component, message) {
if(component.ignoreEvents && message.action !== "component-registered") {
if(this.loggingEnabled) {
console.log("Component disabled for current item, not sending any messages.");
console.log("Component disabled for current item, not sending any messages.", component.name);
}
return;
}
if(this.loggingEnabled) {
console.log("Web|sendMessageToComponent", component, message);
}
component.window.postMessage(message, "*");
}
@@ -415,7 +485,9 @@ class ComponentManager {
this.syncManager.sync();
}
this.activeComponents.push(component);
if(!this.activeComponents.includes(component)) {
this.activeComponents.push(component);
}
}
registerHandler(handler) {
@@ -424,6 +496,15 @@ class ComponentManager {
// Called by other views when the iframe is ready
registerComponentWindow(component, componentWindow) {
if(component.window === componentWindow) {
if(this.loggingEnabled) {
console.log("Web|componentManager", "attempting to re-register same component window.")
}
}
if(this.loggingEnabled) {
console.log("Web|componentManager|registerComponentWindow", component);
}
component.window = componentWindow;
component.sessionKey = Neeto.crypto.generateUUID();
this.sendMessageToComponent(component, {action: "component-registered", sessionKey: component.sessionKey, componentData: component.componentData});
@@ -466,11 +547,28 @@ class ComponentManager {
return component.active;
}
disableComponentForItem(component, item) {
disassociateComponentWithItem(component, item) {
_.pull(component.associatedItemIds, item.uuid);
if(component.disassociatedItemIds.indexOf(item.uuid) !== -1) {
return;
}
component.disassociatedItemIds.push(item.uuid);
component.setDirty(true);
this.syncManager.sync();
}
associateComponentWithItem(component, item) {
_.pull(component.disassociatedItemIds, item.uuid);
if(component.associatedItemIds.includes(item.uuid)) {
return;
}
component.associatedItemIds.push(item.uuid);
component.setDirty(true);
this.syncManager.sync();
}

View File

@@ -242,7 +242,7 @@ class AccountMenu {
$scope.importJSONData = function(data, password, callback) {
var onDataReady = function(errorCount) {
var items = modelManager.mapResponseItemsToLocalModels(data.items);
var items = modelManager.mapResponseItemsToLocalModels(data.items, ModelManager.MappingSourceFileImport);
items.forEach(function(item){
item.setDirty(true);
item.deleted = false;

View File

@@ -9,14 +9,17 @@ class EditorMenu {
};
}
controller($scope, editorManager) {
controller($scope, componentManager) {
'ngInject';
$scope.formData = {};
$scope.editorManager = editorManager;
$scope.editors = componentManager.componentsForArea("editor-editor");
$scope.selectEditor = function($event, editor) {
editor.conflict_of = null; // clear conflict if applicable
if(editor) {
editor.conflict_of = null; // clear conflict if applicable
}
$scope.callback()(editor);
}

View File

@@ -7,14 +7,13 @@ class GlobalExtensionsMenu {
};
}
controller($scope, extensionManager, syncManager, modelManager, themeManager, editorManager, componentManager) {
controller($scope, extensionManager, syncManager, modelManager, themeManager, componentManager) {
'ngInject';
$scope.formData = {};
$scope.extensionManager = extensionManager;
$scope.themeManager = themeManager;
$scope.editorManager = editorManager;
$scope.componentManager = componentManager;
$scope.serverExtensions = modelManager.itemsForContentType("SF|Extension");
@@ -120,23 +119,6 @@ class GlobalExtensionsMenu {
}
// Editors
$scope.deleteEditor = function(editor) {
if(confirm("Are you sure you want to delete this editor?")) {
editorManager.deleteEditor(editor);
}
}
$scope.setDefaultEditor = function(editor) {
editorManager.setDefaultEditor(editor);
}
$scope.removeDefaultEditor = function(editor) {
editorManager.removeDefaultEditor(editor);
}
// Components
$scope.revokePermissions = function(component) {
@@ -151,6 +133,23 @@ class GlobalExtensionsMenu {
}
}
$scope.makeEditorDefault = function(component) {
var currentDefault = componentManager.componentsForArea("editor-editor").filter((e) => {return e.isDefaultEditor()})[0];
if(currentDefault) {
currentDefault.setAppDataItem("defaultEditor", false);
currentDefault.setDirty(true);
}
component.setAppDataItem("defaultEditor", true);
component.setDirty(true);
syncManager.sync();
}
$scope.removeEditorDefault = function(component) {
component.setAppDataItem("defaultEditor", false);
component.setDirty(true);
syncManager.sync();
}
// Installation
$scope.submitInstallLink = function() {
@@ -219,11 +218,6 @@ class GlobalExtensionsMenu {
}
}
$scope.handleEditorLink = function(link, completion) {
editorManager.addNewEditorFromURL(link);
completion();
}
}
}

View File

@@ -57,7 +57,8 @@ class PermissionsModal {
} else if(permission.name === "stream-context-item") {
var mapping = {
"editor-stack" : "working note",
"note-tags" : "working note"
"note-tags" : "working note",
"editor-editor": "working note"
}
return "Access to " + mapping[$scope.component.area];
}

View File

@@ -1,100 +0,0 @@
class EditorManager {
constructor($rootScope, modelManager, syncManager) {
this.syncManager = syncManager;
this.modelManager = modelManager;
this.editorType = "SN|Editor";
this._systemEditor = {
systemEditor: true,
name: "Plain"
}
$rootScope.$on("sync:completed", function(){
// we want to wait for sync completion before creating a syncable system editor
// we need to sync the system editor so that we can assign note preferences to it
// that is, when a user selects Plain for a note, we need to remember that
if(this.systemEditor.uuid) {
return;
}
var liveSysEditor = _.find(this.allEditors, {systemEditor: true});
if(liveSysEditor) {
this._systemEditor = liveSysEditor;
} else {
this._systemEditor = modelManager.createItem({
content_type: this.editorType,
systemEditor: true,
name: "Plain"
})
modelManager.addItem(this._systemEditor);
this._systemEditor.setDirty(true);
syncManager.sync();
}
}.bind(this))
}
get allEditors() {
return this.modelManager.itemsForContentType(this.editorType);
}
get externalEditors() {
return this.allEditors.filter(function(editor){
return !editor.systemEditor;
})
}
get systemEditors() {
return [this.systemEditor];
}
get systemEditor() {
return this._systemEditor;
}
get defaultEditor() {
return _.find(this.externalEditors, {default: true});
}
editorForUrl(url) {
return this.externalEditors.filter(function(editor){return editor.url == url})[0];
}
setDefaultEditor(editor) {
var defaultEditor = this.defaultEditor;
if(defaultEditor) {
defaultEditor.default = false;
defaultEditor.setDirty(true);
}
editor.default = true;
editor.setDirty(true);
this.syncManager.sync();
}
removeDefaultEditor(editor) {
editor.default = false;
editor.setDirty(true);
this.syncManager.sync();
}
addNewEditorFromURL(url) {
var name = getParameterByName("name", url);
var editor = this.modelManager.createItem({
content_type: this.editorType,
url: url,
name: name
})
this.modelManager.addItem(editor);
editor.setDirty(true);
this.syncManager.sync();
}
deleteEditor(editor) {
this.modelManager.setItemToBeDeleted(editor);
this.syncManager.sync();
}
}
angular.module('app.frontend').service('editorManager', EditorManager);

View File

@@ -161,7 +161,7 @@ class ExtensionManager {
action.error = false;
var items = response.items || [response.item];
EncryptionHelper.decryptMultipleItems(items, this.authManager.keys());
items = this.modelManager.mapResponseItemsToLocalModels(items);
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
for(var item of items) {
item.setDirty(true);
}

View File

@@ -0,0 +1,61 @@
class MigrationManager {
constructor($rootScope, modelManager, syncManager, componentManager) {
this.$rootScope = $rootScope;
this.modelManager = modelManager;
this.syncManager = syncManager;
this.componentManager = componentManager;
this.migrators = [];
this.addEditorToComponentMigrator();
this.modelManager.addItemSyncObserver("migration-manager", "*", (allItems, validItems, deletedItems) => {
for(var migrator of this.migrators) {
var items = allItems.filter((item) => {return item.content_type == migrator.content_type});
if(items.length > 0) {
migrator.handler(items);
}
}
});
}
/*
Migrate SN|Editor to SN|Component. Editors are deprecated as of November 2017. Editors using old APIs must
convert to using the new component API.
*/
addEditorToComponentMigrator() {
this.migrators.push({
content_type: "SN|Editor",
handler: (editors) => {
// Convert editors to components
for(var editor of editors) {
// If there's already a component for this url, then skip this editor
if(editor.url && !this.componentManager.componentForUrl(editor.url)) {
var component = this.modelManager.createItem({
content_type: "SN|Component",
url: editor.url,
name: editor.name,
area: "editor-editor"
})
component.setAppDataItem("data", editor.data);
component.setDirty(true);
this.modelManager.addItem(component);
}
}
for(let editor of editors) {
this.modelManager.setItemToBeDeleted(editor);
}
this.syncManager.sync();
}
})
}
}
angular.module('app.frontend').service('migrationManager', MigrationManager);

View File

@@ -1,6 +1,13 @@
class ModelManager {
constructor(storageManager) {
ModelManager.MappingSourceRemoteRetrieved = "MappingSourceRemoteRetrieved";
ModelManager.MappingSourceRemoteSaved = "MappingSourceRemoteSaved";
ModelManager.MappingSourceLocalRetrieved = "MappingSourceLocalRetrieved";
ModelManager.MappingSourceComponentRetrieved = "MappingSourceComponentRetrieved";
ModelManager.MappingSourceRemoteActionRetrieved = "MappingSourceRemoteActionRetrieved"; /* aciton-based Extensions like note history */
ModelManager.MappingSourceFileImport = "MappingSourceFileImport";
this.storageManager = storageManager;
this.notes = [];
this.tags = [];
@@ -96,11 +103,11 @@ class ModelManager {
return tag;
}
mapResponseItemsToLocalModels(items) {
return this.mapResponseItemsToLocalModelsOmittingFields(items, null);
mapResponseItemsToLocalModels(items, source) {
return this.mapResponseItemsToLocalModelsOmittingFields(items, null, source);
}
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
mapResponseItemsToLocalModelsOmittingFields(items, omitFields, source) {
var models = [], processedObjects = [], modelsToNotifyObserversOf = [];
// first loop should add and process items
@@ -151,12 +158,12 @@ class ModelManager {
}
}
this.notifySyncObserversOfModels(modelsToNotifyObserversOf);
this.notifySyncObserversOfModels(modelsToNotifyObserversOf, source);
return models;
}
notifySyncObserversOfModels(models) {
notifySyncObserversOfModels(models, source) {
for(var observer of this.itemSyncObservers) {
var allRelevantItems = models.filter(function(item){return item.content_type == observer.type || observer.type == "*"});
var validItems = [], deletedItems = [];
@@ -169,7 +176,7 @@ class ModelManager {
}
if(allRelevantItems.length > 0) {
observer.callback(allRelevantItems, validItems, deletedItems);
observer.callback(allRelevantItems, validItems, deletedItems, source);
}
}
}

View File

@@ -47,7 +47,7 @@ class SyncManager {
loadLocalItems(callback) {
var params = this.storageManager.getAllModels(function(items){
var items = this.handleItemsResponse(items, null);
var items = this.handleItemsResponse(items, null, ModelManager.MappingSourceLocalRetrieved);
Item.sortItemsByDate(items);
callback(items);
}.bind(this))
@@ -267,7 +267,7 @@ class SyncManager {
// Map retrieved items to local data
var retrieved
= this.handleItemsResponse(response.retrieved_items, null);
= this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved);
// Append items to master list of retrieved items for this ongoing sync operation
this.allRetreivedItems = this.allRetreivedItems.concat(retrieved);
@@ -279,7 +279,7 @@ class SyncManager {
// Map saved items to local data
var saved =
this.handleItemsResponse(response.saved_items, omitFields);
this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved);
// Create copies of items or alternate their uuids if neccessary
var unsaved = response.unsaved;
@@ -355,10 +355,10 @@ class SyncManager {
}
}
handleItemsResponse(responseItems, omitFields) {
handleItemsResponse(responseItems, omitFields, source) {
var keys = this.authManager.keys() || this.passcodeManager.keys();
EncryptionHelper.decryptMultipleItems(responseItems, keys);
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields, source);
return items;
}

View File

@@ -16,7 +16,8 @@ h2 {
position: relative;
width: 100%;
padding: 5px;
background-color: #d8d7d9;
background-color: #f1f1f1;
border-top: 1px solid rgba(black, 0.04);
height: $footer-height;
max-height: $footer-height;
z-index: 100;

View File

@@ -1,18 +1,18 @@
%ul.dropdown-menu.sectioned-menu
.header
.title System Editors
.title System Editor
%ul
%li.menu-item{"ng-repeat" => "editor in editorManager.systemEditors", "ng-click" => "selectEditor($event, editor)"}
%span.pull-left.mr-10{"ng-if" => "selectedEditor === editor"} ✓
%label.menu-item-title.pull-left {{editor.name}}
%li.menu-item{"ng-click" => "selectEditor($event, null)"}
%span.pull-left.mr-10{"ng-if" => "selectedEditor == null"} ✓
%label.menu-item-title.pull-left Plain
%div{"ng-if" => "editorManager.externalEditors.length > 0"}
%div{"ng-if" => "editors.length > 0"}
.header
.title External Editors
.subtitle Can access your current note decrypted.
%ul
%li.menu-item{"ng-repeat" => "editor in editorManager.externalEditors", "ng-click" => "selectEditor($event, editor)"}
%li.menu-item{"ng-repeat" => "editor in editors", "ng-click" => "selectEditor($event, editor)"}
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
%label.menu-item-title
%span.inline.tinted.mr-10{"ng-if" => "selectedEditor === editor"} ✓
{{editor.name}}
%span.inline.tinted{"style" => "margin-left: 8px;", "ng-if" => "selectedEditor === editor"} ✓

View File

@@ -4,7 +4,7 @@
.float-group.h20
%h1.tinted.pull-left Extensions
%a.block.pull-right.dashboard-link{"href" => "https://dashboard.standardnotes.org", "target" => "_blank"} Open Dashboard
%div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !editorManager.externalEditors.length"}
%div.clear{"ng-if" => "!extensionManager.extensions.length && !themeManager.themes.length && !componentManager.components.length"}
%p Customize your experience with editors, themes, and actions.
.tinted-box.mt-10
%h3 Available as part of the Extended subscription.
@@ -34,25 +34,6 @@
%p.small.selectable.wrap{"ng-if" => "theme.showLink"}
{{theme.url}}
%div{"ng-if" => "editorManager.externalEditors.length > 0"}
.header.container.section-margin
%h2 Editors
%p{"style" => "margin-top: 3px;"} Choose "Editor" in the note menu to use an editor for a specific note.
%ul
%li{"ng-repeat" => "editor in editorManager.externalEditors | orderBy: 'name'", "ng-click" => "clickedExtension(editor)"}
.container
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
%h3
%input.bold{"ng-if" => "editor.rename", "ng-model" => "editor.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(editor);", "mb-autofocus" => "true", "should-focus" => "true"}
%span{"ng-if" => "!editor.rename"} {{editor.name}}
%div.mt-5{"ng-if" => "editor.showDetails"}
.link-group
%a{"ng-if" => "!editor.default", "ng-click" => "setDefaultEditor(editor); $event.stopPropagation();"} Make Default
%a.tinted{"ng-if" => "editor.default", "ng-click" => "removeDefaultEditor(editor); $event.stopPropagation();"} Remove as Default
%a{"ng-click" => "renameExtension(editor); $event.stopPropagation();"} Rename
%a{"ng-click" => "editor.showUrl = !editor.showUrl; $event.stopPropagation();"} Show Link
%a.red{ "ng-click" => "deleteEditor(editor); $event.stopPropagation();"} Delete
.wrap.mt-5.selectable{"ng-if" => "editor.showUrl"} {{editor.url}}
%div{"ng-if" => "extensionManager.extensions.length"}
.header.container.section-margin
@@ -118,8 +99,13 @@
%h3
%input.bold{"ng-if" => "component.rename", "ng-model" => "component.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(component);", "mb-autofocus" => "true", "should-focus" => "true"}
%span{"ng-if" => "!component.rename"} {{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.isEditor()"}
%a{"ng-if" => "!component.isDefaultEditor()", "ng-click" => "makeEditorDefault(component); $event.stopPropagation();"} Make Default
%a{"ng-if" => "component.isDefaultEditor()", "ng-click" => "removeEditorDefault(component); $event.stopPropagation();"} Remove Default
%div{"ng-if" => "!component.isEditor()"}
%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
.mt-3{"ng-if" => "component.showDetails"}
.link-group
%a{"ng-click" => "renameExtension(component); $event.stopPropagation();"} Rename

View File

@@ -34,21 +34,21 @@
%i.icon.ion-arrow-expand
Toggle Fullscreen
%li{"ng-if" => "ctrl.hasDisabledComponents()"}
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledComponents()"} Restore Disabled Components
%li{"ng-if" => "ctrl.hasDisabledStackComponents()"}
%label{"ng-click" => "ctrl.selectedMenuItem($event); ctrl.restoreDisabledStackComponents()"} 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
%editor-menu{"ng-if" => "ctrl.showEditorMenu", "callback" => "ctrl.selectedEditor", "selected-editor" => "ctrl.editor"}
%editor-menu{"ng-if" => "ctrl.showEditorMenu", "callback" => "ctrl.selectedEditor", "selected-editor" => "ctrl.editorComponent"}
%li{"ng-class" => "{'selected' : ctrl.showExtensions}", "ng-if" => "ctrl.hasAvailableExtensions()", "click-outside" => "ctrl.showExtensions = false;", "is-open" => "ctrl.showExtensions"}
%label{"ng-click" => "ctrl.showExtensions = !ctrl.showExtensions; ctrl.showMenu = false; ctrl.showEditorMenu = false;"} Actions
%contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
.editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting", "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.editorComponent && ctrl.editorComponent.active", "ng-src" => "{{ctrl.editorComponent.url | trusted}}", "data-component-id" => "{{ctrl.editorComponent.uuid}}", "frameBorder" => "0", "style" => "width: 100%;"}
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.editorComponent", "ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-model" => "ctrl.note.text",
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()", "dir" => "auto"}
{{ctrl.onSystemEditorLoad()}}
@@ -58,5 +58,5 @@
#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)"} ×
.exit-button.body-text-color{"ng-if" => "component.showExit", "ng-click" => "ctrl.disableComponentForCurrentItem(component, true)"} ×
%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

@@ -38,7 +38,7 @@
Show archived notes
.scrollable
.infinite-scroll{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
.infinite-scroll#notes-scrollable{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy| limitTo:ctrl.notesToDisplay)) track by note.uuid",
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
%strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy