diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 8634475d1..20f80d8ca 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -20,24 +20,10 @@ 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 = []; @@ -58,140 +44,25 @@ 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(associatedEditor) { + componentManager.activateComponent(associatedEditor); + } else if(this.editorComponent) { + componentManager.deactivateComponent(this.editorComponent); + this.editorComponent = null; } + this.noteReady = true; if(note.safeText().length == 0 && note.dummy) { this.focusTitle(100); @@ -206,62 +77,27 @@ 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, '*'); + this.selectedEditor = function(editorComponent) { + this.showEditorMenu = false; + if(editorComponent) { + this.enableComponentForCurrentItem(editorComponent); + } else { + // Use plain system editor + if(this.editorComponent) { + this.disableComponentForCurrentItem(this.editorComponent); + } } - } - - function themeData() { - return { - themes: [themeManager.currentTheme ? themeManager.currentTheme.url : null] - } - } - - this.postThemeToExternalEditor = function() { - this.postDataToExternalEditor(themeData()) - } - - this.postNoteToExternalEditor = function() { - if(!this.editor) { - return; - } - - var data = { - text: this.note.text, - data: this.editor.dataForKey(this.note.uuid), - id: this.note.uuid, - } - _.merge(data, themeData()); - this.postDataToExternalEditor(data); - } + this.editorComponent = editorComponent; + }.bind(this) this.hasAvailableExtensions = function() { return extensionManager.extensionsInContextOfItem(this.note).length > 0; @@ -295,20 +131,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 +160,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 +235,6 @@ angular.module('app.frontend') } this.clickedEditNote = function() { - this.editorMode = 'edit'; this.focusEditor(100); } @@ -454,18 +300,121 @@ 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") { + this.showSavingStatus(); + } else if(action == "save-success") { + $timeout(this.showAllChangesSavedStatus.bind(this), 200); + } else { + $timeout(this.showErrorStatus.bind(this), 200); + } + } + } + }.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.enableComponentForCurrentItem = function(component) { + componentManager.activateComponent(component); + componentManager.associateComponentWithItem(component, this.note); + componentManager.setEventFlowForComponent(component, 1); + } + + 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 +424,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/models/app/component.js b/app/assets/javascripts/app/frontend/models/app/component.js index c0108469c..d7fa7f48f 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,28 @@ class Component extends Item { return "SN|Component"; } + + /* + 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..e0c51b000 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -11,7 +11,7 @@ class ComponentManager { this.contextStreamObservers = []; this.activeComponents = []; - // this.loggingEnabled = true; + this.loggingEnabled = true; this.permissionDialogs = []; @@ -140,9 +140,9 @@ 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; }) } @@ -219,7 +219,12 @@ class ComponentManager { _.merge(item.content, responseItem.content); 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) { @@ -378,7 +383,7 @@ 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; } @@ -466,15 +471,23 @@ class ComponentManager { return component.active; } - disableComponentForItem(component, item) { + disassociateComponentWithItem(component, item) { if(component.disassociatedItemIds.indexOf(item.uuid) !== -1) { return; } + _.pull(component.associatedItemIds, item.uuid); component.disassociatedItemIds.push(item.uuid); component.setDirty(true); this.syncManager.sync(); } + associateComponentWithItem(component, item) { + _.pull(component.disassociatedItemIds, item.uuid); + component.associatedItemIds.push(item.uuid); + component.setDirty(true); + this.syncManager.sync(); + } + enableComponentsForItem(components, item) { for(var component of components) { _.pull(component.disassociatedItemIds, item.uuid); 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/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/templates/frontend/directives/editor-menu.html.haml b/app/assets/templates/frontend/directives/editor-menu.html.haml index b6165f264..b028399c7 100644 --- a/app/assets/templates/frontend/directives/editor-menu.html.haml +++ b/app/assets/templates/frontend/directives/editor-menu.html.haml @@ -1,17 +1,17 @@ %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 {{editor.name}} 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}}"}