diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 8634475d1..009da5dd7 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -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
(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
(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; }) diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index f72cedcfd..830479df3 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -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()); diff --git a/app/assets/javascripts/app/frontend/controllers/notes.js b/app/assets/javascripts/app/frontend/controllers/notes.js index 05a1e4cd7..2f35f5d98 100644 --- a/app/assets/javascripts/app/frontend/controllers/notes.js +++ b/app/assets/javascripts/app/frontend/controllers/notes.js @@ -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) { diff --git a/app/assets/javascripts/app/frontend/models/api/item.js b/app/assets/javascripts/app/frontend/models/api/item.js index af65d5aa3..1c3dcff1d 100644 --- a/app/assets/javascripts/app/frontend/models/api/item.js +++ b/app/assets/javascripts/app/frontend/models/api/item.js @@ -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"); } diff --git a/app/assets/javascripts/app/frontend/models/app/component.js b/app/assets/javascripts/app/frontend/models/app/component.js index c0108469c..ac0c30d0e 100644 --- a/app/assets/javascripts/app/frontend/models/app/component.js +++ b/app/assets/javascripts/app/frontend/models/app/component.js @@ -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; + } } } diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 026aeb79a..c22cdbd85 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -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(); } diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index 68806f322..b8a2b7ad6 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -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; diff --git a/app/assets/javascripts/app/services/directives/views/editorMenu.js b/app/assets/javascripts/app/services/directives/views/editorMenu.js index c10ff2c20..008c5fadc 100644 --- a/app/assets/javascripts/app/services/directives/views/editorMenu.js +++ b/app/assets/javascripts/app/services/directives/views/editorMenu.js @@ -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); } diff --git a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js index fa29c8d4d..f046d48d5 100644 --- a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js +++ b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js @@ -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(); - } - } } diff --git a/app/assets/javascripts/app/services/directives/views/permissionsModal.js b/app/assets/javascripts/app/services/directives/views/permissionsModal.js index ce72dfbdb..e2a1f0a07 100644 --- a/app/assets/javascripts/app/services/directives/views/permissionsModal.js +++ b/app/assets/javascripts/app/services/directives/views/permissionsModal.js @@ -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]; } diff --git a/app/assets/javascripts/app/services/editorManager.js b/app/assets/javascripts/app/services/editorManager.js deleted file mode 100644 index 1b9ee579b..000000000 --- a/app/assets/javascripts/app/services/editorManager.js +++ /dev/null @@ -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); diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index 8a84d2188..ff7b66cd1 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -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); } diff --git a/app/assets/javascripts/app/services/migrationManager.js b/app/assets/javascripts/app/services/migrationManager.js new file mode 100644 index 000000000..bb139860c --- /dev/null +++ b/app/assets/javascripts/app/services/migrationManager.js @@ -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); diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index 20ee7fa80..41861d037 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -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); } } } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index cac99a8dd..8e5bc90e6 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -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; } diff --git a/app/assets/stylesheets/app/_footer.scss b/app/assets/stylesheets/app/_footer.scss index f42b9c457..ac85969c9 100644 --- a/app/assets/stylesheets/app/_footer.scss +++ b/app/assets/stylesheets/app/_footer.scss @@ -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; diff --git a/app/assets/templates/frontend/directives/editor-menu.html.haml b/app/assets/templates/frontend/directives/editor-menu.html.haml index b6165f264..cad2f2d0b 100644 --- a/app/assets/templates/frontend/directives/editor-menu.html.haml +++ b/app/assets/templates/frontend/directives/editor-menu.html.haml @@ -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"} ✓ diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index 3a55fab07..fe8965304 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -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 diff --git a/app/assets/templates/frontend/editor.html.haml b/app/assets/templates/frontend/editor.html.haml index 3da918fdd..a33cb2a5a 100644 --- a/app/assets/templates/frontend/editor.html.haml +++ b/app/assets/templates/frontend/editor.html.haml @@ -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}}"} diff --git a/app/assets/templates/frontend/notes.html.haml b/app/assets/templates/frontend/notes.html.haml index 020cfa552..810763227 100644 --- a/app/assets/templates/frontend/notes.html.haml +++ b/app/assets/templates/frontend/notes.html.haml @@ -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