diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 326674086..79af1fa4f 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -58,22 +58,20 @@ angular.module('app.frontend') } let associatedEditor = this.editorForNote(note); - if(associatedEditor) { + if(associatedEditor && associatedEditor != this.selectedEditor) { // 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; - } else { - onReady(); - } - - // Activate new editor if it's different from the one currently activated - if(associatedEditor) { - // switch after timeout, so that note data isnt posted to current editor + // switch after timeout, so that note data isnt posted to current editor $timeout(() => { this.selectedEditor = associatedEditor; onReady(); }) + } else if(associatedEditor) { + // Same editor as currently active + onReady(); } else { + // No editor this.selectedEditor = null; onReady(); } @@ -106,7 +104,6 @@ angular.module('app.frontend') } this.selectEditor = function(editor) { - console.log("selectEditor", editor); this.showEditorMenu = false; if(editor) { @@ -419,18 +416,6 @@ angular.module('app.frontend') this.selectedEditor = 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){ @@ -441,21 +426,10 @@ angular.module('app.frontend') 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(data.type == "container") { 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); } } } diff --git a/app/assets/javascripts/app/frontend/controllers/footer.js b/app/assets/javascripts/app/frontend/controllers/footer.js index 07001176c..315272cd3 100644 --- a/app/assets/javascripts/app/frontend/controllers/footer.js +++ b/app/assets/javascripts/app/frontend/controllers/footer.js @@ -121,46 +121,49 @@ angular.module('app.frontend') componentManager.registerHandler({identifier: "roomBar", areas: ["rooms"], activationHandler: (component) => { if(component.active) { + // Show room, if it was not activated manually (in the event of event from componentManager) + if(!component.showRoom) { + this.selectRoom(component); + } $timeout(() => { - var iframe = componentManager.iframeForComponent(component); - if(iframe) { - var lastSize = component.getRoomLastSize(); - if(lastSize) { - componentManager.handleSetSizeEvent(component, lastSize); - } - iframe.onload = function() { - componentManager.registerComponentWindow(component, iframe.contentWindow); - }.bind(this); + var lastSize = component.getRoomLastSize(); + if(lastSize) { + componentManager.handleSetSizeEvent(component, lastSize); } }); } }, actionHandler: (component, action, data) => { if(action == "set-size") { - componentManager.handleSetSizeEvent(component, data); component.setRoomLastSize(data); } }}); this.selectRoom = function(room) { - room.show = !room.show; // Allows us to send messages to component modal directive if(!room.directiveController) { - room.directiveController = {}; + room.directiveController = {onDismiss: () => { + room.showRoom = false; + }}; } - if(!room.show) { - room.directiveController.dismiss(); - } + // Make sure to call dismiss() before setting new showRoom value + // This way the directive stays alive long enough to deactivate the associated component + // (The directive's life is at the mercy of "ng-if" => "room.showRoom") + if(room.showRoom) { + room.directiveController.dismiss(() => { - console.log("Show", room.show); + }); + } else { + room.showRoom = true; + } } // Handle singleton ProLink instance singletonManager.registerSingleton({content_type: "SN|Component", package_info: {identifier: "org.standardnotes.prolink"}}, (resolvedSingleton) => { - console.log("Roombar received resolved ProLink", resolvedSingleton); + }, (valueCallback) => { - console.log("Creating prolink"); + // Safe to create. Create and return object. let url = window._prolink_package_url; packageManager.installPackage(url, (component) => { diff --git a/app/assets/javascripts/app/frontend/controllers/tags.js b/app/assets/javascripts/app/frontend/controllers/tags.js index 549649f80..e053b5b4e 100644 --- a/app/assets/javascripts/app/frontend/controllers/tags.js +++ b/app/assets/javascripts/app/frontend/controllers/tags.js @@ -63,19 +63,9 @@ angular.module('app.frontend') componentManager.registerHandler({identifier: "tags", areas: ["tags-list"], activationHandler: function(component){ this.component = component; - if(component.active) { - $timeout(function(){ - var iframe = document.getElementById("tags-list-iframe"); - iframe.onload = function() { - componentManager.registerComponentWindow(this.component, iframe.contentWindow); - }.bind(this); - }.bind(this)); - } - }.bind(this), contextRequestHandler: function(component){ return null; }.bind(this), actionHandler: function(component, action, data){ - if(action === "select-item") { var tag = modelManager.findItem(data.item.uuid); if(tag) { @@ -86,7 +76,6 @@ angular.module('app.frontend') else if(action === "clear-selection") { this.selectTag(this.allTag); } - }.bind(this)}); this.setAllTag = function(allTag) { diff --git a/app/assets/javascripts/app/frontend/models/app/component.js b/app/assets/javascripts/app/frontend/models/app/component.js index 5287f412e..42781cfa7 100644 --- a/app/assets/javascripts/app/frontend/models/app/component.js +++ b/app/assets/javascripts/app/frontend/models/app/component.js @@ -73,6 +73,14 @@ class Component extends Item { return "SN|Component"; } + computedUrl() { + if(this.offlineOnly || (isDesktopApplication() && this.local_url)) { + return this.local_url.replace("sn://", this.desktopManager.getApplicationDataPath() + "/"); + } else { + return this.url || this.hosted_url; + } + } + isEditor() { return this.area == "editor-editor"; } diff --git a/app/assets/javascripts/app/frontend/models/app/theme.js b/app/assets/javascripts/app/frontend/models/app/theme.js index 38ced50ce..7d03cb2c3 100644 --- a/app/assets/javascripts/app/frontend/models/app/theme.js +++ b/app/assets/javascripts/app/frontend/models/app/theme.js @@ -1,27 +1,9 @@ -class Theme extends Item { +class Theme extends Component { constructor(json_obj) { super(json_obj); - } - mapContentToLocalProperties(content) { - super.mapContentToLocalProperties(content) - this.url = content.url; - this.name = content.name; - } - - structureParams() { - var params = { - url: this.url, - name: this.name - }; - - _.merge(params, super.structureParams()); - return params; - } - - toJSON() { - return {uuid: this.uuid} + this.area = "themes"; } get content_type() { diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index 6c2be7612..46cff40fc 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -296,7 +296,6 @@ angular.module('app.frontend') let prefsContentType = "SN|UserPreferences"; singletonManager.registerSingleton({content_type: prefsContentType}, (resolvedSingleton) => { - console.log("AuthManager received resolved UserPreferences", resolvedSingleton); this.userPreferences = resolvedSingleton; this.userPreferencesDidChange(); }, (valueCallback) => { @@ -304,7 +303,6 @@ angular.module('app.frontend') var prefs = new Item({content_type: prefsContentType}); modelManager.addItem(prefs); prefs.setDirty(true); - console.log("Created new prefs", prefs); $rootScope.sync(); valueCallback(prefs); }); diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 2b73b1580..916e44df6 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -3,12 +3,11 @@ let ClientDataDomain = "org.standardnotes.sn.components"; class ComponentManager { - constructor($rootScope, modelManager, syncManager, desktopManager, themeManager, $timeout, $compile) { + constructor($rootScope, modelManager, syncManager, desktopManager, $timeout, $compile) { this.$compile = $compile; this.$rootScope = $rootScope; this.modelManager = modelManager; this.syncManager = syncManager; - this.themeManager = themeManager; this.desktopManager = desktopManager; this.timeout = $timeout; this.streamObservers = []; @@ -21,9 +20,9 @@ class ComponentManager { this.handlers = []; - $rootScope.$on("theme-changed", function(){ - this.postThemeToComponents(); - }.bind(this)) + // $rootScope.$on("theme-changed", function(){ + // this.postThemeToAllComponents(); + // }.bind(this)) window.addEventListener("message", function(event){ if(this.loggingEnabled) { @@ -32,7 +31,7 @@ class ComponentManager { this.handleMessage(this.componentForSessionKey(event.data.sessionKey), event.data); }.bind(this), false); - this.modelManager.addItemSyncObserver("component-manager", "*", function(allItems, validItems, deletedItems, source) { + this.modelManager.addItemSyncObserver("component-manager", "*", (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 @@ -42,7 +41,9 @@ class ComponentManager { return; } - var syncedComponents = allItems.filter(function(item){return item.content_type === "SN|Component" }); + var syncedComponents = allItems.filter(function(item) { + return item.content_type === "SN|Component" || item.content_type == "SN|Theme" + }); /* We only want to sync if the item source is Retrieved, not MappingSourceRemoteSaved to avoid recursion caused by the component being modified and saved after it is updated. @@ -77,9 +78,10 @@ class ComponentManager { } ]; - this.runWithPermissions(observer.component, requiredPermissions, observer.originalMessage.permissions, function(){ + this.runWithPermissions(observer.component, requiredPermissions, () => { + console.log("Stream observer, sending items", relevantItems); this.sendItemsInReply(observer.component, relevantItems, observer.originalMessage); - }.bind(this)) + }) } var requiredContextPermissions = [ @@ -89,36 +91,43 @@ class ComponentManager { ]; 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)) { - continue; - } + for(let handler of this.handlers) { + if(!handler.areas.includes(observer.component.area) && !handler.areas.includes("*")) { + continue; + } + if(handler.contextRequestHandler) { var itemInContext = handler.contextRequestHandler(observer.component); if(itemInContext) { var matchingItem = _.find(allItems, {uuid: itemInContext.uuid}); if(matchingItem) { - this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage, source); + this.runWithPermissions(observer.component, requiredContextPermissions, () => { + this.sendContextItemInReply(observer.component, matchingItem, observer.originalMessage, source); + }) } } } - }.bind(this)) + } } - }.bind(this)) + }); } - postThemeToComponents() { + postThemeToAllComponents() { for(var component of this.components) { - if(!component.active || !component.window) { + if(component.area == "themes" || !component.active || !component.window) { continue; } this.postThemeToComponent(component); } } + getActiveTheme() { + return this.componentsForArea("themes").find((theme) => {return theme.active}); + } + postThemeToComponent(component) { + var activeTheme = this.getActiveTheme(); var data = { - themes: [this.themeManager.currentTheme ? this.themeManager.currentTheme.url : null] + themes: [activeTheme ? activeTheme.computedUrl() : null] } this.sendMessageToComponent(component, {action: "themes", data: data}) @@ -126,7 +135,7 @@ class ComponentManager { contextItemDidChangeInArea(area) { for(let handler of this.handlers) { - if(handler.areas.includes(area) === false) { + if(handler.areas.includes(area) === false && !handler.areas.includes("*")) { continue; } var observers = this.contextStreamObservers.filter(function(observer){ @@ -134,8 +143,10 @@ class ComponentManager { }) for(let observer of observers) { - var itemInContext = handler.contextRequestHandler(observer.component); - this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage); + if(handler.contextRequestHandler) { + var itemInContext = handler.contextRequestHandler(observer.component); + this.sendContextItemInReply(observer.component, itemInContext, observer.originalMessage); + } } } } @@ -176,7 +187,7 @@ class ComponentManager { } get components() { - return this.modelManager.itemsForContentType("SN|Component"); + return this.modelManager.allItemsMatchingTypes(["SN|Component", "SN|Theme"]); } componentsForArea(area) { @@ -220,7 +231,7 @@ class ComponentManager { save-context-client-data get-context-client-data install-local-component - open-component + toggle-activate-component */ if(message.action === "stream-items") { @@ -237,14 +248,14 @@ class ComponentManager { this.handleSaveItemsMessage(component, message); } else if(message.action === "install-local-component") { this.handleInstallLocalComponentMessage(component, message); - } else if(message.action === "open-component") { - let openComponent = this.modelManager.findItem(message.data.uuid); - this.openModalComponent(openComponent); + } else if(message.action === "toggle-activate-component") { + let componentToToggle = this.modelManager.findItem(message.data.uuid); + this.handleToggleComponentMessage(component, componentToToggle, message); } // Notify observers for(let handler of this.handlers) { - if(handler.areas.includes(component.area)) { + if(handler.areas.includes(component.area) || handler.areas.includes("*")) { this.timeout(function(){ handler.actionHandler(component, message.action, message.data); }) @@ -254,7 +265,7 @@ class ComponentManager { removePrivatePropertiesFromResponseItems(responseItems, includeUrls) { // Don't allow component to overwrite these properties. - var privateProperties = ["appData", "autoupdate", "permissions", "active"]; + var privateProperties = ["appData", "autoupdate", "permissions", "active", "encrypted"]; if(includeUrls) { privateProperties = privateProperties.concat(["url", "hosted_url", "local_url"]); } @@ -278,7 +289,7 @@ class ComponentManager { } ]; - this.runWithPermissions(component, requiredPermissions, message.permissions, function(){ + this.runWithPermissions(component, requiredPermissions, () => { if(!_.find(this.streamObservers, {identifier: component.uuid})) { // for pushing laster as changes come in this.streamObservers.push({ @@ -289,14 +300,13 @@ class ComponentManager { }) } - // push immediately now var items = []; for(var contentType of message.data.content_types) { items = items.concat(this.modelManager.itemsForContentType(contentType)); } this.sendItemsInReply(component, items, message); - }.bind(this)); + }); } handleStreamContextItemMessage(component, message) { @@ -307,7 +317,7 @@ class ComponentManager { } ]; - this.runWithPermissions(component, requiredPermissions, message.permissions, function(){ + this.runWithPermissions(component, requiredPermissions, function(){ if(!_.find(this.contextStreamObservers, {identifier: component.uuid})) { // for pushing laster as changes come in this.contextStreamObservers.push({ @@ -318,27 +328,49 @@ class ComponentManager { } // push immediately now - for(let handler of this.handlers) { - if(handler.areas.includes(component.area) === false) { - continue; - } + for(let handler of this.handlersForArea(component.area)) { var itemInContext = handler.contextRequestHandler(component); this.sendContextItemInReply(component, itemInContext, message); } }.bind(this)) } - handleSaveItemsMessage(component, message) { - var requiredContentTypes = _.uniq(message.data.items.map((i) => {return i.content_type})).sort(); - var requiredPermissions = [ - { - name: "stream-items", - content_types: requiredContentTypes + isItemWithinComponentContextJurisdiction(item, component) { + for(let handler of this.handlersForArea(component.area)) { + var itemInContext = handler.contextRequestHandler(component); + if(itemInContext.uuid == item.uuid) { + return true; } - ]; + } + return false; + } - this.runWithPermissions(component, requiredPermissions, message.permissions, () => { - var responseItems = message.data.items; + handlersForArea(area) { + return this.handlers.filter((candidate) => {return candidate.areas.includes(area)}); + } + + handleSaveItemsMessage(component, message) { + var responseItems = message.data.items; + var requiredPermissions; + + // Check if you're just trying to save the context item, which requires only stream-context-item permissions + if(responseItems.length == 1 && this.isItemWithinComponentContextJurisdiction(responseItems[0], component)) { + requiredPermissions = [ + { + name: "stream-context-item" + } + ]; + } else { + var requiredContentTypes = _.uniq(responseItems.map((i) => {return i.content_type})).sort(); + requiredPermissions = [ + { + name: "stream-items", + content_types: requiredContentTypes + } + ]; + } + + this.runWithPermissions(component, requiredPermissions, () => { this.removePrivatePropertiesFromResponseItems(responseItems, {includeUrls: true}); @@ -373,7 +405,7 @@ class ComponentManager { } ]; - this.runWithPermissions(component, requiredPermissions, message.permissions, () => { + this.runWithPermissions(component, requiredPermissions, () => { var responseItem = message.data.item; this.removePrivatePropertiesFromResponseItems([responseItem]); var item = this.modelManager.createItem(responseItem); @@ -397,7 +429,7 @@ class ComponentManager { } ]; - this.runWithPermissions(component, requiredPermissions, message.permissions, () => { + this.runWithPermissions(component, requiredPermissions, () => { var items = message.data.items; var noun = items.length == 1 ? "item" : "items"; if(confirm(`Are you sure you want to delete ${items.length} ${noun}?`)) { @@ -419,10 +451,8 @@ class ComponentManager { } ]; - this.runWithPermissions(component, requiredPermissions, message.permissions, () => { - console.log("Received install-local-component event"); + this.runWithPermissions(component, requiredPermissions, () => { this.desktopManager.installOfflineComponentFromData(message.data, (response) => { - console.log("componentManager: installed component:", response); var component = this.modelManager.mapResponseItemsToLocalModels([response], ModelManager.MappingSourceComponentRetrieved)[0]; // Save updated URL component.setDirty(true); @@ -432,68 +462,73 @@ class ComponentManager { } handleSetComponentDataMessage(component, message) { - var requiredPermissions = [ - { - name: "stream-items", - content_types: [component.content_type] - } - ]; - - this.runWithPermissions(component, requiredPermissions, message.permissions, () => { + // A component setting its own data does not require special permissions + this.runWithPermissions(component, [], () => { component.componentData = message.data.componentData; component.setDirty(true); this.syncManager.sync(); }); } - runWithPermissions(component, requiredPermissions, requestedPermissions, runFunction) { - var acquiredPermissions = component.permissions; - - var requestedMatchesRequired = true; - - for(var required of requiredPermissions) { - var matching = _.find(requestedPermissions, required); - var matching = requestedPermissions.filter((p) => { - var matchesContentTypes = true; - if(p.content_types) { - matchesContentTypes = JSON.stringify(p.content_types.sort()) == JSON.stringify(required.content_types.sort()); - } - return p.name == required.name && matchesContentTypes; - })[0]; - - if(!matching) { - /* Required permissions can be 1 content type, and requestedPermisisons may send an array of content types. - In the case of an array, we can just check to make sure that requiredPermissions content type is found in the array - */ - matching = requestedPermissions.filter((requested) => { - return Array.isArray(requested.content_types) && requested.content_types.containsSubset(required.content_types); - }); - - console.log("Matching 2nd chance", matching); - - if(!matching) { - requestedMatchesRequired = false; - break; + handleToggleComponentMessage(sourceComponent, targetComponent, message) { + if(targetComponent.area == "modal") { + this.openModalComponent(targetComponent); + } else { + if(targetComponent.active) { + this.deactivateComponent(targetComponent); + } else { + if(targetComponent.content_type == "SN|Theme") { + // Deactive currently active theme + var activeTheme = this.getActiveTheme(); + if(activeTheme) { + this.deactivateComponent(activeTheme); + } } + this.activateComponent(targetComponent); } } + } - if(!requestedMatchesRequired) { - // Error with Component permissions request - console.error("You are requesting permissions", requestedPermissions, "when you need to be requesting", requiredPermissions, ". Component:", component); - return false; - } - + runWithPermissions(component, requiredPermissions, runFunction) { if(!component.permissions) { component.permissions = []; } - var acquiredMatchesRequested = angular.toJson(component.permissions.sort()) === angular.toJson(requestedPermissions.sort()); + var acquiredPermissions = component.permissions; + var acquiredMatchesRequired = true; - if(!acquiredMatchesRequested) { - this.promptForPermissions(component, requestedPermissions, function(approved){ + for(var required of requiredPermissions) { + var matching = acquiredPermissions.find((candidate) => { + var matchesContentTypes = true; + if(candidate.content_types && required.content_types) { + matchesContentTypes = JSON.stringify(candidate.content_types.sort()) == JSON.stringify(required.content_types.sort()); + } + return candidate.name == required.name && matchesContentTypes; + }); + + if(!matching) { + /* Required permissions can be 1 content type, and requestedPermisisons may send an array of content types. + In the case of an array, we can just check to make sure that requiredPermissions content type is found in the array + */ + matching = acquiredPermissions.find((candidate) => { + return Array.isArray(candidate.content_types) + && Array.isArray(required.content_types) + && candidate.content_types.containsSubset(required.content_types); + }); + + if(!matching) { + acquiredMatchesRequired = false; + break; + } + } + } + + // var acquiredMatchesRequested = angular.toJson(component.permissions.sort()) === angular.toJson(requestedPermissions.sort()); + + if(!acquiredMatchesRequired) { + this.promptForPermissions(component, requiredPermissions, function(approved){ if(approved) { runFunction(); } @@ -503,18 +538,22 @@ class ComponentManager { } } - promptForPermissions(component, requestedPermissions, callback) { + promptForPermissions(component, permissions, callback) { // since these calls are asyncronous, multiple dialogs may be requested at the same time. We only want to present one and trigger all callbacks based on one modal result var existingDialog = _.find(this.permissionDialogs, {component: component}); var scope = this.$rootScope.$new(true); scope.component = component; - scope.permissions = requestedPermissions; + scope.permissions = permissions; scope.actionBlock = callback; scope.callback = function(approved) { if(approved) { - component.permissions = requestedPermissions; + for(var permission of permissions) { + if(!component.permissions.includes(permission)) { + component.permissions.push(permission); + } + } component.setDirty(true); this.syncManager.sync(); } @@ -544,6 +583,9 @@ class ComponentManager { openModalComponent(component) { var scope = this.$rootScope.$new(true); scope.component = component; + scope.onDismiss = () => { + + } var el = this.$compile( "" )(scope); angular.element(document.body).append(el); } @@ -591,7 +633,7 @@ class ComponentManager { component.active = true; for(var handler of this.handlers) { - if(handler.areas.includes(component.area)) { + if(handler.areas.includes(component.area) || handler.areas.includes("*")) { handler.activationHandler(component); } } @@ -604,6 +646,10 @@ class ComponentManager { if(!this.activeComponents.includes(component)) { this.activeComponents.push(component); } + + if(component.area == "themes") { + this.postThemeToAllComponents(); + } } registerHandler(handler) { @@ -640,12 +686,13 @@ class ComponentManager { } deactivateComponent(component) { + console.log("Deactivating component", component); var didChange = component.active != false; component.active = false; component.sessionKey = null; for(var handler of this.handlers) { - if(handler.areas.includes(component.area)) { + if(handler.areas.includes(component.area) || handler.areas.includes("*")) { handler.activationHandler(component); } } @@ -664,6 +711,10 @@ class ComponentManager { this.contextStreamObservers = this.contextStreamObservers.filter(function(o){ return o.component !== component; }) + + if(component.area == "themes") { + this.postThemeToAllComponents(); + } } deleteComponent(component) { @@ -713,14 +764,6 @@ class ComponentManager { component.ignoreEvents = !on; } - urlForComponent(component) { - if(isDesktopApplication() && component.local_url) { - return component.local_url.replace("sn://", this.desktopManager.getApplicationDataPath() + "/"); - } else { - return component.url || component.hosted_url; - } - } - iframeForComponent(component) { for(var frame of document.getElementsByTagName("iframe")) { var componentId = frame.dataset.componentId; diff --git a/app/assets/javascripts/app/services/directives/views/componentModal.js b/app/assets/javascripts/app/services/directives/views/componentModal.js index 72502e854..a02cb1f00 100644 --- a/app/assets/javascripts/app/services/directives/views/componentModal.js +++ b/app/assets/javascripts/app/services/directives/views/componentModal.js @@ -6,8 +6,8 @@ class ComponentModal { this.scope = { show: "=", component: "=", - controller: "=", - callback: "=" + callback: "=", + onDismiss: "&" }; } @@ -18,42 +18,23 @@ class ComponentModal { controller($scope, $timeout, componentManager) { 'ngInject'; - let identifier = "modal-" + $scope.component.uuid; - - $scope.component.directiveController.dismiss = function() { - $scope.component.show = false; - componentManager.deactivateComponent($scope.component); - componentManager.deregisterHandler(identifier); - $scope.el.remove(); - } - - $scope.dismiss = function() { - $scope.component.directiveController.dismiss(); - } - - $scope.url = function() { - return componentManager.urlForComponent($scope.component); - } - - componentManager.registerHandler({identifier: identifier, areas: ["modal"], activationHandler: (component) => { - if(component.active) { - $timeout(function(){ - var iframe = componentManager.iframeForComponent(component); - if(iframe) { - iframe.onload = function() { - componentManager.registerComponentWindow(component, iframe.contentWindow); - }.bind(this); - } - }.bind(this)); + if($scope.component.directiveController) { + $scope.component.directiveController.dismiss = function(callback) { + $scope.dismiss(callback); } - }, - actionHandler: function(component, action, data) { - if(action == "set-size") { - componentManager.handleSetSizeEvent(component, data); - } - }.bind(this)}); + } + + $scope.dismiss = function(callback) { + var onDismiss = $scope.component.directiveController && $scope.component.directiveController.onDismiss(); + // Setting will null out compinent-view's component, which will handle deactivation + $scope.component = null; + $timeout(() => { + $scope.el.remove(); + onDismiss && onDismiss(); + callback && callback(); + }) + } - componentManager.activateComponent($scope.component); } } diff --git a/app/assets/javascripts/app/services/directives/views/componentView.js b/app/assets/javascripts/app/services/directives/views/componentView.js index d3f75fa5e..f6ddf5db7 100644 --- a/app/assets/javascripts/app/services/directives/views/componentView.js +++ b/app/assets/javascripts/app/services/directives/views/componentView.js @@ -1,18 +1,41 @@ class ComponentView { - constructor() { + constructor(componentManager, $timeout) { this.restrict = "E"; this.templateUrl = "frontend/directives/component-view.html"; this.scope = { component: "=" }; + + this.componentManager = componentManager; + this.timeout = $timeout; } link($scope, el, attrs, ctrl) { $scope.el = el; + let identifier = "component-view-" + Math.random(); + + this.componentManager.registerHandler({identifier: identifier, areas: ["*"], activationHandler: (component) => { + if(component.active) { + this.timeout(function(){ + var iframe = this.componentManager.iframeForComponent(component); + if(iframe) { + iframe.onload = function() { + this.componentManager.registerComponentWindow(component, iframe.contentWindow); + }.bind(this); + } + }.bind(this)); + } + }, + actionHandler: function(component, action, data) { + if(action == "set-size") { + this.componentManager.handleSetSizeEvent(component, data); + } + }.bind(this)}); + $scope.$watch('component', function(component, prevComponent){ - console.log("Component View Setting Component", component); + // console.log("Component View Setting Component", component); ctrl.componentValueChanging(component, prevComponent); }); } @@ -20,6 +43,8 @@ class ComponentView { controller($scope, $timeout, componentManager, desktopManager) { 'ngInject'; + console.log("Creating New Component View"); + this.componentValueChanging = (component, prevComponent) => { if(prevComponent && component !== prevComponent) { // Deactive old component @@ -33,39 +58,15 @@ class ComponentView { } } - let identifier = "component-view-" + Math.random(); - - $scope.url = function() { - if($scope.component.offlineOnly) { - return $scope.component.local_url; + $scope.$on("$destroy", function() { + console.log("DESTROY COMPONENT VIEW"); + componentManager.deregisterHandler($scope.identifier); + if($scope.component) { + componentManager.deactivateComponent($scope.component); } - - if(desktopManager.isDesktop && $scope.component.local_url) { - return $scope.component.local_url; - } - - return $scope.component.hosted_url || $scope.component.url; - } - - componentManager.registerHandler({identifier: identifier, areas: ["*"], activationHandler: (component) => { - if(component.active) { - $timeout(function(){ - var iframe = componentManager.iframeForComponent(component); - if(iframe) { - iframe.onload = function() { - componentManager.registerComponentWindow(component, iframe.contentWindow); - }.bind(this); - } - }.bind(this)); - } - }, - actionHandler: function(component, action, data) { - if(action == "set-size") { - componentManager.handleSetSizeEvent(component, data); - } - }.bind(this)}); + }); } } -angular.module('app.frontend').directive('componentView', () => new ComponentView); +angular.module('app.frontend').directive('componentView', (componentManager, $timeout) => new ComponentView(componentManager, $timeout)); diff --git a/app/assets/javascripts/app/services/directives/views/editorMenu.js b/app/assets/javascripts/app/services/directives/views/editorMenu.js index 78e4005ab..63a95af1c 100644 --- a/app/assets/javascripts/app/services/directives/views/editorMenu.js +++ b/app/assets/javascripts/app/services/directives/views/editorMenu.js @@ -30,7 +30,6 @@ class EditorMenu { } $scope.toggleDefaultForEditor = function(editor) { - console.log("Toggling editor", editor); if($scope.defaultEditor == editor) { $scope.removeEditorDefault(editor); } else { diff --git a/app/assets/javascripts/app/services/directives/views/permissionsModal.js b/app/assets/javascripts/app/services/directives/views/permissionsModal.js index d08de104e..5988bae86 100644 --- a/app/assets/javascripts/app/services/directives/views/permissionsModal.js +++ b/app/assets/javascripts/app/services/directives/views/permissionsModal.js @@ -30,8 +30,6 @@ class PermissionsModal { controller($scope, modelManager) { - console.log("permissions", $scope.permissions); - $scope.formattedPermissions = $scope.permissions.map(function(permission){ if(permission.name === "stream-items") { var types = permission.content_types.map(function(type){ diff --git a/app/assets/javascripts/app/services/themeManager.js b/app/assets/javascripts/app/services/themeManager.js index 0e799ba08..248a67cab 100644 --- a/app/assets/javascripts/app/services/themeManager.js +++ b/app/assets/javascripts/app/services/themeManager.js @@ -1,54 +1,65 @@ class ThemeManager { - constructor(modelManager, syncManager, $rootScope, storageManager) { + constructor(modelManager, syncManager, $rootScope, storageManager, componentManager) { this.syncManager = syncManager; this.modelManager = modelManager; this.$rootScope = $rootScope; this.storageManager = storageManager; + + componentManager.registerHandler({identifier: "themeManager", areas: ["themes"], activationHandler: (component) => { + if(component.active) { + this.activateTheme(component); + } else { + this.deactivateTheme(component); + } + }}); } get themes() { return this.modelManager.itemsForContentType("SN|Theme"); } + /* activeTheme: computed property that returns saved theme currentTheme: stored variable that allows other classes to watch changes */ - get activeTheme() { - var activeThemeId = this.storageManager.getItem("activeTheme"); - if(!activeThemeId) { - return null; - } - - var theme = _.find(this.themes, {uuid: activeThemeId}); - return theme; - } + // get activeTheme() { + // var activeThemeId = this.storageManager.getItem("activeTheme"); + // if(!activeThemeId) { + // return null; + // } + // + // var theme = _.find(this.themes, {uuid: activeThemeId}); + // return theme; + // } activateInitialTheme() { - var theme = this.activeTheme; - if(theme) { - this.activateTheme(theme); - } + // var theme = this.activeTheme; + // if(theme) { + // this.activateTheme(theme); + // } } - submitTheme(url) { - var name = this.displayNameForThemeFile(this.fileNameFromPath(url)); - var theme = this.modelManager.createItem({content_type: "SN|Theme", url: url, name: name}); - this.modelManager.addItem(theme); - theme.setDirty(true); - this.syncManager.sync(); - } + // submitTheme(url) { + // var name = this.displayNameForThemeFile(this.fileNameFromPath(url)); + // var theme = this.modelManager.createItem({content_type: "SN|Theme", url: url, name: name}); + // this.modelManager.addItem(theme); + // theme.setDirty(true); + // this.syncManager.sync(); + // } activateTheme(theme) { - var activeTheme = this.activeTheme; - if(activeTheme) { - this.deactivateTheme(activeTheme); + if(this.activeTheme && this.activeTheme !== theme) { + this.deactivateTheme(this.activeTheme); } + + var url = theme.computedUrl(); + var link = document.createElement("link"); - link.href = theme.url; + link.href = url; link.type = "text/css"; link.rel = "stylesheet"; link.media = "screen,print"; @@ -72,10 +83,6 @@ class ThemeManager { this.$rootScope.$broadcast("theme-changed"); } - isThemeActive(theme) { - return this.storageManager.getItem("activeTheme") === theme.uuid; - } - fileNameFromPath(filePath) { return filePath.replace(/^.*[\\\/]/, ''); } diff --git a/app/assets/stylesheets/app/_editor.scss b/app/assets/stylesheets/app/_editor.scss index 7b4c08d2b..cca66e8ee 100644 --- a/app/assets/stylesheets/app/_editor.scss +++ b/app/assets/stylesheets/app/_editor.scss @@ -68,7 +68,7 @@ $heading-height: 75px; #note-tags-component-container { height: 50px; - #note-tags-iframe { + iframe { height: 50px; width: 100%; position: absolute; @@ -116,8 +116,8 @@ $heading-height: 75px; #editor-pane-component-stack { width: 100%; - .component { - height: 50px; + .component-stack-item { + // height: 50px; width: 100%; position: relative; &:not(:last-child) { @@ -128,28 +128,6 @@ $heading-height: 75px; border-top: 1px solid $bg-color; } - .exit-button { - width: 15px; - height: 100%; - position: absolute; - right: 0; - background-color: transparent; - cursor: pointer; - display: flex; - align-items: center; - color: rgba(black, 0.7); - text-align: center; - padding-left: 2px; - - .content { - - } - - &:hover { - background-color: rgba(gray, 0.3); - } - } - iframe { width: 100%; } diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss index e4dfa9c71..3e3ff1a59 100644 --- a/app/assets/stylesheets/app/_modals.scss +++ b/app/assets/stylesheets/app/_modals.scss @@ -12,6 +12,10 @@ } } +.panel { + background-color: white; +} + .modal { position: fixed; margin-left: auto; @@ -44,14 +48,12 @@ } } -.modal-iframe-container { +// Optionally use if .component-view container is not flex-based +.component-view-container { flex-grow: 1; display: flex; - - iframe { - flex-grow: 1; - width: 100%; - } + height: 100%; + width: 100%; } .component-view { diff --git a/app/assets/stylesheets/app/_stylekit-sub.scss b/app/assets/stylesheets/app/_stylekit-sub.scss new file mode 100644 index 000000000..4b7bd0603 --- /dev/null +++ b/app/assets/stylesheets/app/_stylekit-sub.scss @@ -0,0 +1,7 @@ +.panel { + color: black; + + a { + color: $blue-color; + } +} diff --git a/app/assets/stylesheets/frontend.css.scss b/app/assets/stylesheets/frontend.css.scss index 529b6ed35..2097406ef 100644 --- a/app/assets/stylesheets/frontend.css.scss +++ b/app/assets/stylesheets/frontend.css.scss @@ -11,5 +11,6 @@ $dark-gray: #2e2e2e; @import "app/menus"; @import "app/modals"; @import "app/lock-screen"; +@import "app/stylekit-sub"; @import "ionicons"; diff --git a/app/assets/templates/frontend/directives/component-modal.html.haml b/app/assets/templates/frontend/directives/component-modal.html.haml index 2d02ce18a..4ab3a5a8a 100644 --- a/app/assets/templates/frontend/directives/component-modal.html.haml +++ b/app/assets/templates/frontend/directives/component-modal.html.haml @@ -1,7 +1,9 @@ -.sn-component - .panel{"ng-attr-id" => "component-{{component.uuid}}"} - .header - %h1.title {{component.name}} - %a.close-button.info{"ng-click" => "dismiss()"} Close - .modal-iframe-container{"ng-attr-id" => "component-{{component.uuid}}"} - %iframe{"ng-src" => "{{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}}"} +.background{"ng-click" => "dismiss()"} + +.content + .sn-component + .panel{"ng-attr-id" => "component-{{component.uuid}}"} + .header + %h1.title {{component.name}} + %a.close-button.info{"ng-click" => "dismiss()"} Close + %component-view.component-view{"component" => "component"} diff --git a/app/assets/templates/frontend/directives/component-view.html.haml b/app/assets/templates/frontend/directives/component-view.html.haml index 60ffbab5d..6025d9e26 100644 --- a/app/assets/templates/frontend/directives/component-view.html.haml +++ b/app/assets/templates/frontend/directives/component-view.html.haml @@ -1,6 +1,6 @@ %iframe{"ng-if" => "component", "ng-attr-id" => "component-{{component.uuid}}", -"ng-src" => "{{url() | trusted}}", "frameBorder" => "0", +"ng-src" => "{{component.computedUrl() | 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}}"} Loading 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 276a370c5..2a6b89a39 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -24,8 +24,8 @@ %h3 %input.bold{"ng-if" => "theme.rename", "ng-model" => "theme.tempName", "ng-keyup" => "$event.keyCode == 13 && submitExtensionRename(theme);", "mb-autofocus" => "true", "should-focus" => "true"} %span{"ng-if" => "!theme.rename"} {{theme.name}} - %a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme); $event.stopPropagation();"} Activate - %a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme); $event.stopPropagation();"} Deactivate + -# %a{"ng-if" => "!themeManager.isThemeActive(theme)", "ng-click" => "themeManager.activateTheme(theme); $event.stopPropagation();"} Activate + -# %a{"ng-if" => "themeManager.isThemeActive(theme)", "ng-click" => "themeManager.deactivateTheme(theme); $event.stopPropagation();"} Deactivate .mt-3{"ng-if" => "theme.showDetails"} .link-group %a{"ng-click" => "renameExtension(theme); $event.stopPropagation();"} Rename diff --git a/app/assets/templates/frontend/directives/permissions-modal.html.haml b/app/assets/templates/frontend/directives/permissions-modal.html.haml index cfd7d8661..d88280a3a 100644 --- a/app/assets/templates/frontend/directives/permissions-modal.html.haml +++ b/app/assets/templates/frontend/directives/permissions-modal.html.haml @@ -5,7 +5,7 @@ .panel .header %h1.title Activate Extension - %a.close-button.info Cancel + %a.close-button.info{"ng-click" => "deny()"} Cancel .content .panel-section .panel-row @@ -14,7 +14,6 @@ would like to interact with your %span{"ng-repeat" => "permission in formattedPermissions"} {{permission}}. - -# %p.wrap URL: {{component.runningUrl}} .panel-row %p diff --git a/app/assets/templates/frontend/editor.html.haml b/app/assets/templates/frontend/editor.html.haml index 9541746bf..45a54105f 100644 --- a/app/assets/templates/frontend/editor.html.haml +++ b/app/assets/templates/frontend/editor.html.haml @@ -8,8 +8,8 @@ #save-status{"ng-class" => "{'red bold': ctrl.saveError, 'orange bold': ctrl.syncTakingTooLong}", "ng-bind-html" => "ctrl.noteStatus"} .editor-tags - #note-tags-component-container{"ng-if" => "ctrl.tagsComponent && ctrl.tagsComponent.active"} - %iframe#note-tags-iframe{"ng-src" => "{{ctrl.componentManager.urlForComponent(ctrl.tagsComponent) | trusted}}", "frameBorder" => "0", "sandbox" => "allow-scripts", "data-component-id" => "{{ctrl.tagsComponent.uuid}}"} + #note-tags-component-container{"ng-if" => "ctrl.tagsComponent"} + %component-view.component-view{ "component" => "ctrl.tagsComponent"} %input.tags-input{"ng-if" => "!(ctrl.tagsComponent && ctrl.tagsComponent.active)", "type" => "text", "ng-keyup" => "$event.keyCode == 13 && $event.target.blur();", "ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"} @@ -42,8 +42,7 @@ .editor-content#editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting"} %panel-resizer.left{"panel-id" => "'editor-content'", "on-resize-finish" => "ctrl.onPanelResizeFinish","control" => "ctrl.resizeControl", "min-width" => 300, "property" => "'left'", "hoverable" => "true"} - -# ng-show is required here (as opposed to ng-if) in order for the component-view to receive events such as nulling ctrl.selectorEditor - %component-view{"ng-show" => "ctrl.selectedEditor", "component" => "ctrl.selectedEditor", "class" => "component-view"} + %component-view.component-view{"ng-if" => "ctrl.selectedEditor", "component" => "ctrl.selectedEditor"} %textarea.editable#note-text-editor{"ng-if" => "!ctrl.selectedEditor", "ng-model" => "ctrl.note.text", "ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()", "dir" => "auto"} {{ctrl.onSystemEditorLoad()}} @@ -53,6 +52,4 @@ %p.medium-padding{"style" => "padding-top: 0 !important;"} There was an error decrypting this item. Ensure you are running the latest version of this app, then sign out and sign back in to try again. #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.disableComponentForCurrentItem(component, true)"} × - %iframe{"ng-src" => "{{ctrl.componentManager.urlForComponent(component) | 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}}"} + %component-view.component-view.component-stack-item{"ng-repeat" => "component in ctrl.componentStack", "ng-if" => "component.active", "component" => "component"} diff --git a/app/assets/templates/frontend/footer.html.haml b/app/assets/templates/frontend/footer.html.haml index dc8b87d7f..4ed1d1de5 100644 --- a/app/assets/templates/frontend/footer.html.haml +++ b/app/assets/templates/frontend/footer.html.haml @@ -22,7 +22,7 @@ .item{"ng-repeat" => "room in ctrl.rooms"} .column{"ng-click" => "ctrl.selectRoom(room)"} .label {{room.name}} - %component-modal{"ng-if" => "room.show", "component" => "room"} + %component-modal{"ng-if" => "room.showRoom", "component" => "room", "controller" => "room.directiveController"} .right diff --git a/app/assets/templates/frontend/tags.html.haml b/app/assets/templates/frontend/tags.html.haml index d9cea08ee..e751732f8 100644 --- a/app/assets/templates/frontend/tags.html.haml +++ b/app/assets/templates/frontend/tags.html.haml @@ -1,5 +1,6 @@ .section.tags#tags-column - %iframe#tags-list-iframe{"ng-if" => "ctrl.component && ctrl.component.active", "ng-src" => "{{ctrl.componentManager.urlForComponent(ctrl.component) | trusted}}", "frameBorder" => "0", "style" => "width: 100%; height: 100%;", "sandbox" => "allow-scripts"} + .component-view-container{"ng-if" => "ctrl.component"} + %component-view.component-view{"component" => "ctrl.component"} #tags-content.content{"ng-if" => "!(ctrl.component && ctrl.component.active)"} #tags-title-bar.section-title-bar .section-title-bar-header diff --git a/vendor/assets/stylesheets/stylekit.css b/vendor/assets/stylesheets/stylekit.css index 906073401..326f4b600 100644 --- a/vendor/assets/stylesheets/stylekit.css +++ b/vendor/assets/stylesheets/stylekit.css @@ -6,9 +6,12 @@ .sn-component .panel { box-shadow: 0px 2px 13px #C8C8C8; border-radius: 0.7rem; - overflow: scroll; display: flex; flex-direction: column; + overflow: hidden; +} +.sn-component .panel a:hover { + text-decoration: underline; } .sn-component .panel.static { box-shadow: none; @@ -28,15 +31,16 @@ .sn-component .panel .header .close-button { font-weight: bold; } -.sn-component .panel .footer { +.sn-component .panel .footer, .sn-component .panel .panel-footer { padding: 1rem 2rem; - border-top: 1px solid #F1F1F1; + border-top: 1px solid #E1E1E1; + box-sizing: border-box; } -.sn-component .panel .footer .left { +.sn-component .panel .footer .left, .sn-component .panel .panel-footer .left { text-align: right; display: block; } -.sn-component .panel .footer .right { +.sn-component .panel .footer .right, .sn-component .panel .panel-footer .right { text-align: right; display: block; } @@ -44,75 +48,88 @@ padding: 1.6rem 2rem; padding-bottom: 0; flex-grow: 1; + overflow: scroll; } .sn-component .panel .content p { color: #454545; line-height: 1.3; } -.sn-component .panel .content .label, .sn-component .panel .content .panel-section .subtitle { +.sn-component .panel .content .label, .sn-component .panel .content .panel-section .subtitle, .sn-component .panel-section .panel .content .subtitle { font-weight: bold; } -.sn-component .panel .content .panel-section { +.sn-component .panel-section { padding-bottom: 1.6rem; -} -.sn-component .panel .content .panel-section .panel-row { display: flex; - justify-content: space-between; - align-items: center; - padding-top: 0.4rem; + flex-direction: column; } -.sn-component .panel .content .panel-section .panel-row.centered { - justify-content: center; +.sn-component .panel-section.no-bottom-pad { + padding-bottom: 0; } -.sn-component .panel .content .panel-section .panel-row .panel-column { - width: 100%; -} -.sn-component .panel .content .panel-section .panel-row:not(:last-child) { - padding-bottom: 0.4rem; -} -.sn-component .panel .content .panel-section .panel-row:not(:last-child).condensed { - padding-top: 0.2rem; - padding-bottom: 0.2rem; -} -.sn-component .panel .content .panel-section .panel-row p { - margin: 0; - padding: 0; -} -.sn-component .panel .content .panel-section.hero { +.sn-component .panel-section.hero { text-align: center; } -.sn-component .panel .content .panel-section p:last-child { +.sn-component .panel-section p:last-child { margin-bottom: 0; } -.sn-component .panel .content .panel-section:not(:last-child) { +.sn-component .panel-section:not(:last-child) { margin-bottom: 1.5rem; border-bottom: 1px solid #DDDDDD; } -.sn-component .panel .content .panel-section:last-child { +.sn-component .panel-section:not(:last-child).no-border { + border-bottom: none; +} +.sn-component .panel-section:last-child { margin-bottom: 0.5rem; } -.sn-component .panel .content .panel-section .outer-title { +.sn-component .panel-section .outer-title { border-bottom: 1px solid #DDDDDD; padding-bottom: 0.9rem; margin-top: 2.1rem; margin-bottom: 15px; } -.sn-component .panel .content .panel-section .subtitle { +.sn-component .panel-section .subtitle { margin-top: -0.5rem; } -.sn-component .panel .content .panel-section .subtitle.subtle { +.sn-component .panel-section .subtitle.subtle { font-weight: normal; opacity: 0.6; } -.sn-component .panel .content .panel-form { +.sn-component .panel-row { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 0.4rem; +} +.sn-component .panel-row.centered { + justify-content: center; +} +.sn-component .panel-row .panel-column { width: 100%; } -.sn-component .panel .content .panel-form.half { +.sn-component .panel-row.default-padding, .sn-component .panel-row:not(:last-child) { + padding-bottom: 0.4rem; +} +.sn-component .panel-row.default-padding.condensed, .sn-component .panel-row:not(:last-child).condensed { + padding-top: 0.2rem; + padding-bottom: 0.2rem; +} +.sn-component .panel-row p { + margin: 0; + padding: 0; +} +.sn-component .panel-form { + width: 100%; +} +.sn-component .panel-form.half { width: 50%; } -.sn-component .panel .content .panel-form .form-submit { +.sn-component .panel-form .form-submit { margin-top: 0.15rem; } +.sn-component .right-aligned { + justify-content: flex-end; + text-align: right; +} .sn-component .menu-panel { box-shadow: 0px 4px 4px #C8C8C8; border-radius: 0.6rem; @@ -156,14 +173,14 @@ .sn-component .menu-panel .row .column .left { display: flex; } -.sn-component .menu-panel .row .button .label, .sn-component .menu-panel .row .box .label, .sn-component .menu-panel .row .circle .label, .sn-component .menu-panel .row .button .panel .content .panel-section .subtitle, .sn-component .panel .content .panel-section .menu-panel .row .button .subtitle, .sn-component .menu-panel .row .box .panel .content .panel-section .subtitle, .sn-component .panel .content .panel-section .menu-panel .row .box .subtitle, .sn-component .menu-panel .row .circle .panel .content .panel-section .subtitle, .sn-component .panel .content .panel-section .menu-panel .row .circle .subtitle { +.sn-component .menu-panel .row .button .label, .sn-component .menu-panel .row .box .label, .sn-component .menu-panel .row .circle .label, .sn-component .menu-panel .row .button .panel-section .subtitle, .sn-component .panel-section .menu-panel .row .button .subtitle, .sn-component .menu-panel .row .box .panel-section .subtitle, .sn-component .panel-section .menu-panel .row .box .subtitle, .sn-component .menu-panel .row .circle .panel-section .subtitle, .sn-component .panel-section .menu-panel .row .circle .subtitle { font-size: 0.8rem; font-weight: normal; } .sn-component .menu-panel .row:hover { background-color: #efefef; } -.sn-component .menu-panel .row .label, .sn-component .menu-panel .row .panel .content .panel-section .subtitle, .sn-component .panel .content .panel-section .menu-panel .row .subtitle { +.sn-component .menu-panel .row .label, .sn-component .menu-panel .row .panel-section .subtitle, .sn-component .panel-section .menu-panel .row .subtitle { font-size: 1rem; font-weight: bold; } @@ -270,6 +287,9 @@ .sn-component .horizontal-group > *:not(:first-child), .sn-component .input-group > *:not(:first-child) { margin-left: 0.9rem; } +.sn-component .border-bottom { + border-bottom: 1px solid #DDDDDD; +} .sn-component .checkbox-group { padding-top: 0.5rem; padding-bottom: 0.3rem; @@ -322,7 +342,7 @@ text-align: center; border: 1px solid; } -.sn-component .button .label, .sn-component .box .label, .sn-component .circle .label, .sn-component .button .panel .content .panel-section .subtitle, .sn-component .panel .content .panel-section .button .subtitle, .sn-component .box .panel .content .panel-section .subtitle, .sn-component .panel .content .panel-section .box .subtitle, .sn-component .circle .panel .content .panel-section .subtitle, .sn-component .panel .content .panel-section .circle .subtitle { +.sn-component .button .label, .sn-component .box .label, .sn-component .circle .label, .sn-component .button .panel-section .subtitle, .sn-component .panel-section .button .subtitle, .sn-component .box .panel-section .subtitle, .sn-component .panel-section .box .subtitle, .sn-component .circle .panel-section .subtitle, .sn-component .panel-section .circle .subtitle { font-weight: bold; display: block; text-align: center; @@ -602,10 +622,10 @@ .sn-component .app-bar .item.no-pointer { cursor: default; } -.sn-component .app-bar .item:hover > .label:not(.subtle), .sn-component .app-bar .panel .content .panel-section .item:hover > .subtitle:not(.subtle), .sn-component .panel .content .panel-section .app-bar .item:hover > .subtitle:not(.subtle), .sn-component .app-bar .item:hover > .sublabel:not(.subtle), .sn-component .app-bar .item:hover > .column > .label:not(.subtle), .sn-component .app-bar .panel .content .panel-section .item:hover > .column > .subtitle:not(.subtle), .sn-component .panel .content .panel-section .app-bar .item:hover > .column > .subtitle:not(.subtle), .sn-component .app-bar .item:hover > .column > .sublabel:not(.subtle) { +.sn-component .app-bar .item:hover > .label:not(.subtle), .sn-component .app-bar .panel-section .item:hover > .subtitle:not(.subtle), .sn-component .panel-section .app-bar .item:hover > .subtitle:not(.subtle), .sn-component .app-bar .item:hover > .sublabel:not(.subtle), .sn-component .app-bar .item:hover > .column > .label:not(.subtle), .sn-component .app-bar .panel-section .item:hover > .column > .subtitle:not(.subtle), .sn-component .panel-section .app-bar .item:hover > .column > .subtitle:not(.subtle), .sn-component .app-bar .item:hover > .column > .sublabel:not(.subtle) { color: #086DD6; } -.sn-component .app-bar .item > .label, .sn-component .app-bar .panel .content .panel-section .item > .subtitle, .sn-component .panel .content .panel-section .app-bar .item > .subtitle, .sn-component .app-bar .item > .column > .label, .sn-component .app-bar .panel .content .panel-section .item > .column > .subtitle, .sn-component .panel .content .panel-section .app-bar .item > .column > .subtitle { +.sn-component .app-bar .item > .label, .sn-component .app-bar .panel-section .item > .subtitle, .sn-component .panel-section .app-bar .item > .subtitle, .sn-component .app-bar .item > .column > .label, .sn-component .app-bar .panel-section .item > .column > .subtitle, .sn-component .panel-section .app-bar .item > .column > .subtitle { font-weight: bold; font-size: 0.9rem; white-space: nowrap;