Component management improvements, removal of dependence on noteReady flag

This commit is contained in:
Mo Bitar
2020-04-15 10:26:55 -05:00
parent a2303aa7af
commit 0d44a2ff64
17 changed files with 1618 additions and 1119 deletions

View File

@@ -1,5 +1,5 @@
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { SNComponent } from 'snjs'; import { SNComponent, ComponentAction, LiveItem } from 'snjs';
import { WebDirective } from './../../types'; import { WebDirective } from './../../types';
import template from '%/directives/component-view.pug'; import template from '%/directives/component-view.pug';
import { isDesktopApplication } from '../../utils'; import { isDesktopApplication } from '../../utils';
@@ -7,38 +7,37 @@ import { isDesktopApplication } from '../../utils';
* The maximum amount of time we'll wait for a component * The maximum amount of time we'll wait for a component
* to load before displaying error * to load before displaying error
*/ */
const MAX_LOAD_THRESHOLD = 4000; const MaxLoadThreshold = 4000;
const VisibilityChangeKey = 'visibilitychange';
const VISIBILITY_CHANGE_LISTENER_KEY = 'visibilitychange';
interface ComponentViewScope { interface ComponentViewScope {
component: SNComponent componentUuid: string
onLoad?: (component: SNComponent) => void onLoad?: (component: SNComponent) => void
manualDealloc: boolean
application: WebApplication application: WebApplication
} }
class ComponentViewCtrl implements ComponentViewScope { class ComponentViewCtrl implements ComponentViewScope {
$rootScope: ng.IRootScopeService /** @scope */
$timeout: ng.ITimeoutService
componentValid = true
cleanUpOn: () => void
unregisterComponentHandler!: () => void
component!: SNComponent
onLoad?: (component: SNComponent) => void onLoad?: (component: SNComponent) => void
manualDealloc = false componentUuid!: string
application!: WebApplication application!: WebApplication
unregisterDesktopObserver!: () => void liveComponent!: LiveItem<SNComponent>
didRegisterObservers = false
lastComponentValue?: SNComponent private $rootScope: ng.IRootScopeService
issueLoading = false private $timeout: ng.ITimeoutService
reloading = false private componentValid = true
expired = false private cleanUpOn: () => void
loading = false private unregisterComponentHandler!: () => void
didAttemptReload = false private unregisterDesktopObserver!: () => void
error: 'offline-restricted' | 'url-missing' | undefined private didRegisterObservers = false
loadTimeout: any private issueLoading = false
public reloading = false
private expired = false
private loading = false
private didAttemptReload = false
public error: 'offline-restricted' | 'url-missing' | undefined
private loadTimeout: any
/* @ngInject */ /* @ngInject */
constructor( constructor(
@@ -51,7 +50,6 @@ class ComponentViewCtrl implements ComponentViewScope {
this.cleanUpOn = $scope.$on('ext-reload-complete', () => { this.cleanUpOn = $scope.$on('ext-reload-complete', () => {
this.reloadStatus(false); this.reloadStatus(false);
}); });
/** To allow for registering events */ /** To allow for registering events */
this.onVisibilityChange = this.onVisibilityChange.bind(this); this.onVisibilityChange = this.onVisibilityChange.bind(this);
} }
@@ -61,49 +59,56 @@ class ComponentViewCtrl implements ComponentViewScope {
(this.cleanUpOn as any) = undefined; (this.cleanUpOn as any) = undefined;
this.unregisterComponentHandler(); this.unregisterComponentHandler();
(this.unregisterComponentHandler as any) = undefined; (this.unregisterComponentHandler as any) = undefined;
if (this.component && !this.manualDealloc) {
/* application and componentManager may be destroyed if this onDestroy is part of
the entire application being destroyed rather than part of just a single component
view being removed */
if (this.application && this.application.componentManager) {
this.application.componentManager.deregisterComponent(this.component);
}
}
this.unregisterDesktopObserver(); this.unregisterDesktopObserver();
(this.unregisterDesktopObserver as any) = undefined; (this.unregisterDesktopObserver as any) = undefined;
document.removeEventListener( this.liveComponent.deinit();
VISIBILITY_CHANGE_LISTENER_KEY, (this.liveComponent as any) = undefined;
this.onVisibilityChange
);
(this.component as any) = undefined;
this.onLoad = undefined;
(this.application as any) = undefined; (this.application as any) = undefined;
(this.onVisibilityChange as any) = undefined; (this.onVisibilityChange as any) = undefined;
this.onLoad = undefined;
document.removeEventListener(
VisibilityChangeKey,
this.onVisibilityChange
);
} }
$onChanges() { $onInit() {
if (!this.didRegisterObservers) { this.liveComponent = new LiveItem(this.componentUuid, this.application);
this.didRegisterObservers = true; this.loadComponent();
this.registerComponentHandlers(); this.registerComponentHandlers();
this.registerPackageUpdateObserver(); this.registerPackageUpdateObserver();
}
const newComponent = this.component;
const oldComponent = this.lastComponentValue;
this.lastComponentValue = newComponent;
if (oldComponent && oldComponent !== newComponent) {
this.application.componentManager!.deregisterComponent(
oldComponent
);
}
if (newComponent && newComponent !== oldComponent) {
this.application.componentManager!.registerComponent(
newComponent
)
this.reloadStatus();
}
} }
registerPackageUpdateObserver() { get component() {
return this.liveComponent?.item;
}
private loadComponent() {
if (!this.component) {
throw 'Component view is missing component';
}
if (!this.component.active) {
throw 'Component view component must be active';
}
const iframe = this.application.componentManager!.iframeForComponent(
this.component
);
if (!iframe) {
return;
}
this.loading = true;
if (this.loadTimeout) {
this.$timeout.cancel(this.loadTimeout);
}
this.loadTimeout = this.$timeout(() => {
this.handleIframeLoadTimeout();
}, MaxLoadThreshold);
iframe.onload = (event) => {
this.handleIframeLoad(iframe);
};
}
private registerPackageUpdateObserver() {
this.unregisterDesktopObserver = this.application.getDesktopService() this.unregisterDesktopObserver = this.application.getDesktopService()
.registerUpdateObserver((component: SNComponent) => { .registerUpdateObserver((component: SNComponent) => {
if (component === this.component && component.active) { if (component === this.component && component.active) {
@@ -112,27 +117,19 @@ class ComponentViewCtrl implements ComponentViewScope {
}); });
} }
registerComponentHandlers() { private registerComponentHandlers() {
this.unregisterComponentHandler = this.application.componentManager!.registerHandler({ this.unregisterComponentHandler = this.application.componentManager!.registerHandler({
identifier: 'component-view-' + Math.random(), identifier: 'component-view-' + Math.random(),
areas: [this.component.area], areas: [this.component.area],
activationHandler: (component) => {
if (component !== this.component) {
return;
}
this.$timeout(() => {
this.handleActivation();
});
},
actionHandler: (component, action, data) => { actionHandler: (component, action, data) => {
if (action === 'set-size') { if (action === ComponentAction.SetSize) {
this.application.componentManager!.handleSetSizeEvent(component, data); this.application.componentManager!.handleSetSizeEvent(component, data);
} }
} }
}); });
} }
onVisibilityChange() { private onVisibilityChange() {
if (document.visibilityState === 'hidden') { if (document.visibilityState === 'hidden') {
return; return;
} }
@@ -141,13 +138,13 @@ class ComponentViewCtrl implements ComponentViewScope {
} }
} }
async reloadComponent() { public async reloadComponent() {
this.componentValid = false; this.componentValid = false;
await this.application.componentManager!.reloadComponent(this.component); await this.application.componentManager!.reloadComponent(this.component);
this.reloadStatus(); this.reloadStatus();
} }
reloadStatus(doManualReload = true) { public reloadStatus(doManualReload = true) {
this.reloading = true; this.reloading = true;
const component = this.component; const component = this.component;
const previouslyValid = this.componentValid; const previouslyValid = this.componentValid;
@@ -183,36 +180,12 @@ class ComponentViewCtrl implements ComponentViewScope {
if (this.expired && doManualReload) { if (this.expired && doManualReload) {
this.$rootScope.$broadcast('reload-ext-dat'); this.$rootScope.$broadcast('reload-ext-dat');
} }
this.$timeout(() => { this.$timeout(() => {
this.reloading = false; this.reloading = false;
}, 500); }, 500);
} }
handleActivation() { private async handleIframeLoadTimeout() {
if (!this.component || !this.component.active) {
return;
}
const iframe = this.application.componentManager!.iframeForComponent(
this.component
);
if (!iframe) {
return;
}
this.loading = true;
if (this.loadTimeout) {
this.$timeout.cancel(this.loadTimeout);
}
this.loadTimeout = this.$timeout(() => {
this.handleIframeLoadTimeout();
}, MAX_LOAD_THRESHOLD);
iframe.onload = (event) => {
this.handleIframeLoad(iframe);
};
}
async handleIframeLoadTimeout() {
if (this.loading) { if (this.loading) {
this.loading = false; this.loading = false;
this.issueLoading = true; this.issueLoading = true;
@@ -221,14 +194,14 @@ class ComponentViewCtrl implements ComponentViewScope {
this.reloadComponent(); this.reloadComponent();
} else { } else {
document.addEventListener( document.addEventListener(
VISIBILITY_CHANGE_LISTENER_KEY, VisibilityChangeKey,
this.onVisibilityChange this.onVisibilityChange
); );
} }
} }
} }
async handleIframeLoad(iframe: HTMLIFrameElement) { private async handleIframeLoad(iframe: HTMLIFrameElement) {
let desktopError = false; let desktopError = false;
if (isDesktopApplication()) { if (isDesktopApplication()) {
try { try {
@@ -252,11 +225,7 @@ class ComponentViewCtrl implements ComponentViewScope {
}, avoidFlickerTimeout); }, avoidFlickerTimeout);
} }
disableActiveTheme() { public getUrl() {
this.application.getThemeService().deactivateAllThemes();
}
getUrl() {
const url = this.application.componentManager!.urlForComponent(this.component); const url = this.application.componentManager!.urlForComponent(this.component);
return url; return url;
} }
@@ -268,9 +237,8 @@ export class ComponentView extends WebDirective {
this.restrict = 'E'; this.restrict = 'E';
this.template = template; this.template = template;
this.scope = { this.scope = {
component: '=', componentUuid: '=',
onLoad: '=?', onLoad: '=?',
manualDealloc: '=?',
application: '=' application: '='
}; };
this.controller = ComponentViewCtrl; this.controller = ComponentViewCtrl;

View File

@@ -15,7 +15,7 @@ interface EditorMenuScope {
class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope { class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
callback!: (component: SNComponent) => void callback!: () => (component: SNComponent) => void
selectedEditor!: SNComponent selectedEditor!: SNComponent
currentItem!: SNItem currentItem!: SNItem
application!: WebApplication application!: WebApplication
@@ -52,7 +52,7 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
} }
} }
this.$timeout(() => { this.$timeout(() => {
this.callback(component); this.callback()(component);
}); });
} }

View File

@@ -1,3 +1,4 @@
import { ComponentGroup } from './component_group';
import { EditorGroup } from '@/ui_models/editor_group'; import { EditorGroup } from '@/ui_models/editor_group';
import { InputModalScope } from '@/directives/views/inputModal'; import { InputModalScope } from '@/directives/views/inputModal';
import { PasswordWizardType, PasswordWizardScope } from '@/types'; import { PasswordWizardType, PasswordWizardScope } from '@/types';
@@ -13,7 +14,7 @@ import {
import angular from 'angular'; import angular from 'angular';
import { getPlatformString } from '@/utils'; import { getPlatformString } from '@/utils';
import { AlertService } from '@/services/alertService'; import { AlertService } from '@/services/alertService';
import { WebDeviceInterface } from '@/web_device_interface'; import { WebDeviceInterface } from '@/interface';
import { import {
AppState, AppState,
DesktopManager, DesktopManager,
@@ -46,6 +47,7 @@ export class WebApplication extends SNApplication {
private webServices!: WebServices private webServices!: WebServices
private currentAuthenticationElement?: JQLite private currentAuthenticationElement?: JQLite
public editorGroup: EditorGroup public editorGroup: EditorGroup
public componentGroup: ComponentGroup
/* @ngInject */ /* @ngInject */
constructor( constructor(
@@ -74,6 +76,7 @@ export class WebApplication extends SNApplication {
this.onDeinit = onDeinit; this.onDeinit = onDeinit;
deviceInterface.setApplication(this); deviceInterface.setApplication(this);
this.editorGroup = new EditorGroup(this); this.editorGroup = new EditorGroup(this);
this.componentGroup = new ComponentGroup(this);
} }
/** @override */ /** @override */
@@ -90,6 +93,7 @@ export class WebApplication extends SNApplication {
this.onDeinit = undefined; this.onDeinit = undefined;
this.$compile = undefined; this.$compile = undefined;
this.editorGroup.deinit(); this.editorGroup.deinit();
this.componentGroup.deinit();
(this.scope! as any).application = undefined; (this.scope! as any).application = undefined;
this.scope!.$destroy(); this.scope!.$destroy();
this.scope = undefined; this.scope = undefined;

View File

@@ -0,0 +1,100 @@
import { dictToArray } from '../utils';
import { SNComponent, ComponentArea, removeFromArray } from 'snjs';
import { WebApplication } from './application';
/** Areas that only allow a single component to be active */
const SingleComponentAreas = [
ComponentArea.Editor,
ComponentArea.NoteTags,
ComponentArea.TagsList
]
export class ComponentGroup {
private application: WebApplication
changeObservers: any[] = []
activeComponents: Partial<Record<string, SNComponent>> = {}
constructor(application: WebApplication) {
this.application = application;
}
get componentManager() {
return this.application.componentManager!;
}
public deinit() {
(this.application as any) = undefined;
for (const component of this.allActiveComponents()) {
this.componentManager.deregisterComponent(component);
}
}
async activateComponent(component: SNComponent) {
if (this.activeComponents[component.uuid]) {
return;
}
if (SingleComponentAreas.includes(component.area)) {
const currentActive = this.activeComponentForArea(component.area);
if (currentActive) {
await this.deactivateComponent(currentActive, false);
}
}
this.activeComponents[component.uuid] = component;
await this.componentManager.activateComponent(component);
this.notifyObservers();
}
async deactivateComponent(component: SNComponent, notify = true) {
if (!this.activeComponents[component.uuid]) {
return;
}
delete this.activeComponents[component.uuid];
await this.componentManager.deactivateComponent(component);
if(notify) {
this.notifyObservers();
}
}
async deactivateComponentForArea(area: ComponentArea) {
const component = this.activeComponentForArea(area);
if (component) {
return this.deactivateComponent(component);
}
}
activeComponentForArea(area: ComponentArea) {
return this.activeComponentsForArea(area)[0];
}
activeComponentsForArea(area: ComponentArea) {
const all = dictToArray(this.activeComponents);
return all.filter((c) => c.area === area);
}
allComponentsForArea(area: ComponentArea) {
return this.componentManager.componentsForArea(area);
}
private allActiveComponents() {
return dictToArray(this.activeComponents);
}
/**
* Notifies observer when the active editor has changed.
*/
public addChangeObserver(callback: () => void) {
this.changeObservers.push(callback);
return () => {
removeFromArray(this.changeObservers, callback);
}
}
private notifyObservers() {
for (const observer of this.changeObservers) {
observer();
}
}
}

View File

@@ -11,8 +11,8 @@ export function isNullOrUndefined(value: any) {
return value === null || value === undefined; return value === null || value === undefined;
} }
export function dictToArray(dict: any) { export function dictToArray<T>(dict: Record<any, T>) {
return Object.keys(dict).map((key) => dict[key]); return Object.keys(dict).map((key) => dict[key]!);
} }
export function getPlatformString() { export function getPlatformString() {

View File

@@ -31,9 +31,9 @@
) {{self.state.noteStatus.message}} ) {{self.state.noteStatus.message}}
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}} .desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
.editor-tags .editor-tags
#note-tags-component-container(ng-if='self.state.tagsComponent') #note-tags-component-container(ng-if='self.activeTagsComponent')
component-view.component-view( component-view.component-view(
component='self.state.tagsComponent', component-uuid='self.activeTagsComponent.uuid',
ng-class="{'locked' : self.noteLocked}", ng-class="{'locked' : self.noteLocked}",
ng-style="self.noteLocked && {'pointer-events' : 'none'}", ng-style="self.noteLocked && {'pointer-events' : 'none'}",
application='self.application' application='self.application'
@@ -41,7 +41,7 @@
input.tags-input( input.tags-input(
ng-blur='self.onTagsInputBlur()', ng-blur='self.onTagsInputBlur()',
ng-disabled='self.noteLocked', ng-disabled='self.noteLocked',
ng-if='!(self.state.tagsComponent && self.state.tagsComponent.active)', ng-if='!self.activeTagsComponent',
ng-keyup='$event.keyCode == 13 && $event.target.blur();', ng-keyup='$event.keyCode == 13 && $event.target.blur();',
ng-model='self.editorValues.tagsInputValue', ng-model='self.editorValues.tagsInputValue',
placeholder='#tags', placeholder='#tags',
@@ -133,18 +133,18 @@
action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeyMonospace)", action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeyMonospace)",
circle="self.state.monospaceEnabled ? 'success' : 'neutral'", circle="self.state.monospaceEnabled ? 'success' : 'neutral'",
desc="'Toggles the font style for the default editor'", desc="'Toggles the font style for the default editor'",
disabled='self.state.selectedEditor', disabled='self.activeEditorComponent',
label="'Monospace Font'", label="'Monospace Font'",
subtitle="self.state.selectedEditor ? 'Not available with editor extensions' : null" subtitle="self.activeEditorComponent ? 'Not available with editor extensions' : null"
) )
menu-row( menu-row(
action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeySpellcheck)", action="self.selectedMenuItem(true); self.toggleWebPrefKey(self.prefKeySpellcheck)",
circle="self.state.spellcheck ? 'success' : 'neutral'", circle="self.state.spellcheck ? 'success' : 'neutral'",
desc="'Toggles spellcheck for the default editor'", desc="'Toggles spellcheck for the default editor'",
disabled='self.state.selectedEditor', disabled='self.activeEditorComponent',
label="'Spellcheck'", label="'Spellcheck'",
subtitle=` subtitle=`
self.state.selectedEditor self.activeEditorComponent
? 'Not available with editor extensions' ? 'Not available with editor extensions'
: (self.state.isDesktop ? 'May degrade editor performance' : null) : (self.state.isDesktop ? 'May degrade editor performance' : null)
`) `)
@@ -163,10 +163,10 @@
) )
.sk-label Editor .sk-label Editor
editor-menu( editor-menu(
callback='self.editorMenuOnSelect()', callback='self.editorMenuOnSelect',
current-item='self.note', current-item='self.note',
ng-if='self.state.showEditorMenu', ng-if='self.state.showEditorMenu',
selected-editor='self.state.selectedEditor', selected-editor='self.activeEditorComponent',
application='self.application' application='self.application'
) )
.sk-app-bar-item( .sk-app-bar-item(
@@ -205,8 +205,8 @@
property="'left'" property="'left'"
) )
component-view.component-view( component-view.component-view(
component='self.state.selectedEditor', component-uuid='self.activeEditorComponent.uuid',
ng-if='self.state.selectedEditor', ng-if='self.activeEditorComponent',
on-load='self.onEditorLoad', on-load='self.onEditorLoad',
application='self.application' application='self.application'
) )
@@ -216,7 +216,7 @@
ng-change='self.contentChanged()', ng-change='self.contentChanged()',
ng-click='self.clickedTextArea()', ng-click='self.clickedTextArea()',
ng-focus='self.onContentFocus()', ng-focus='self.onContentFocus()',
ng-if='!self.state.selectedEditor', ng-if='!self.activeEditorComponent',
ng-model='self.editorValues.text', ng-model='self.editorValues.text',
ng-model-options='{ debounce: self.state.editorDebounce}', ng-model-options='{ debounce: self.state.editorDebounce}',
ng-readonly='self.noteLocked', ng-readonly='self.noteLocked',
@@ -236,11 +236,11 @@
| There was an error decrypting this item. Ensure you are running the | 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. | latest version of this app, then sign out and sign back in to try again.
#editor-pane-component-stack(ng-show='self.note') #editor-pane-component-stack(ng-show='self.note')
#component-stack-menu-bar.sk-app-bar.no-edges(ng-if='self.state.componentStack.length') #component-stack-menu-bar.sk-app-bar.no-edges(ng-if='self.state.allStackComponents.length')
.left .left
.sk-app-bar-item( .sk-app-bar-item(
ng-repeat='component in self.state.allStackComponents track by component.uuid'
ng-click='self.toggleStackComponentForCurrentItem(component)', ng-click='self.toggleStackComponentForCurrentItem(component)',
ng-repeat='component in self.state.componentStack track by component.uuid'
) )
.sk-app-bar-item-column .sk-app-bar-item-column
.sk-circle.small( .sk-circle.small(
@@ -250,10 +250,9 @@
.sk-label {{component.name}} .sk-label {{component.name}}
.sn-component .sn-component
component-view.component-view.component-stack-item( component-view.component-view.component-stack-item(
component='component', ng-repeat='component in self.activeStackComponents track by component.uuid',
component-uuid='component.uuid',
manual-dealloc='true', manual-dealloc='true',
ng-if='component.active',
ng-repeat='component in self.state.componentStack track by component.uuid',
ng-show='!component.hidden', ng-show='!component.hidden',
application='self.application' application='self.application'
) )

View File

@@ -58,15 +58,16 @@ type NoteStatus = {
} }
type EditorState = { type EditorState = {
allStackComponents: SNComponent[]
activeEditorComponent?: SNComponent
activeTagsComponent?: SNComponent
activeStackComponents: SNComponent[]
saveError?: any saveError?: any
editorComponent?: SNComponent
noteStatus?: NoteStatus noteStatus?: NoteStatus
tagsAsStrings?: string tagsAsStrings?: string
marginResizersEnabled?: boolean marginResizersEnabled?: boolean
monospaceEnabled?: boolean monospaceEnabled?: boolean
isDesktop?: boolean isDesktop?: boolean
tagsComponent?: SNComponent
componentStack?: SNComponent[]
syncTakingTooLong: boolean syncTakingTooLong: boolean
showExtensions: boolean showExtensions: boolean
noteReady: boolean noteReady: boolean
@@ -106,6 +107,7 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
private removeTrashKeyObserver?: any private removeTrashKeyObserver?: any
private removeDeleteKeyObserver?: any private removeDeleteKeyObserver?: any
private removeTabObserver?: any private removeTabObserver?: any
private removeComponentObserver: any
prefKeyMonospace: string prefKeyMonospace: string
prefKeySpellcheck: string prefKeySpellcheck: string
@@ -133,6 +135,8 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
} }
deinit() { deinit() {
this.removeComponentObserver();
this.removeComponentObserver = undefined;
this.removeAltKeyObserver(); this.removeAltKeyObserver();
this.removeAltKeyObserver = undefined; this.removeAltKeyObserver = undefined;
this.removeTrashKeyObserver(); this.removeTrashKeyObserver();
@@ -173,20 +177,29 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
this.editorValues.text = note.text; this.editorValues.text = note.text;
this.reloadTagsString(); this.reloadTagsString();
} }
});
this.removeComponentObserver = this.componentGroup.addChangeObserver(() => {
this.setEditorState({
activeEditorComponent: this.componentGroup.activeComponentForArea(ComponentArea.Editor),
activeTagsComponent: this.componentGroup.activeComponentForArea(ComponentArea.NoteTags),
activeStackComponents: this.componentGroup.activeComponentsForArea(ComponentArea.EditorStack)
})
}) })
} }
/** @override */ /** @override */
getInitialState() { getInitialState() {
return { return {
componentStack: [], allStackComponents: [],
activeStackComponents: [],
editorDebounce: EDITOR_DEBOUNCE, editorDebounce: EDITOR_DEBOUNCE,
isDesktop: isDesktopApplication(), isDesktop: isDesktopApplication(),
spellcheck: true, spellcheck: true,
noteReady: true,
mutable: { mutable: {
tagsString: '' tagsString: ''
} }
}; } as Partial<EditorState>;
} }
async setEditorState(state: Partial<EditorState>) { async setEditorState(state: Partial<EditorState>) {
@@ -238,6 +251,22 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
} }
} }
get activeEditorComponent() {
return this.getState().activeEditorComponent;
}
get activeTagsComponent() {
return this.getState().activeTagsComponent;
}
get activeStackComponents() {
return this.getState().activeStackComponents;
}
get componentGroup() {
return this.application.componentGroup;
}
async handleEditorNoteChange() { async handleEditorNoteChange() {
const note = this.editor.note; const note = this.editor.note;
await this.setEditorState({ await this.setEditorState({
@@ -248,31 +277,7 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
}); });
this.editorValues.title = note.title; this.editorValues.title = note.title;
this.editorValues.text = note.text; this.editorValues.text = note.text;
if (!note) { await this.reloadComponentEditorState();
this.setEditorState({
noteReady: false
});
return;
}
const associatedEditor = this.editorForNote(note);
if (associatedEditor && associatedEditor !== this.getState().editorComponent) {
/**
* 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.setEditorState({
noteReady: false,
editorComponent: associatedEditor
});
} else if (!associatedEditor) {
/** No editor */
this.setEditorState({
editorComponent: undefined
});
}
await this.setEditorState({
noteReady: true,
});
this.reloadTagsString(); this.reloadTagsString();
this.reloadPreferences(); this.reloadPreferences();
if (note.safeText().length === 0) { if (note.safeText().length === 0) {
@@ -281,6 +286,19 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
this.reloadComponentContext(); this.reloadComponentContext();
} }
async reloadComponentEditorState() {
const associatedEditor = this.application.componentManager!.editorForNote(this.note)
if (associatedEditor && associatedEditor !== this.activeEditorComponent) {
/** 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 */
await this.componentGroup.activateComponent(associatedEditor);
} else if (!associatedEditor) {
/** No editor */
await this.componentGroup.deactivateComponentForArea(ComponentArea.Editor);
}
return associatedEditor;
}
/** /**
* Because note.locked accesses note.content.appData, * Because note.locked accesses note.content.appData,
@@ -334,10 +352,7 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
return; return;
} }
/** Find the most recent editor for note */ /** Find the most recent editor for note */
const editor = this.editorForNote(this.note); const editor = this.reloadComponentEditorState();
this.setEditorState({
editorComponent: editor
});
if (!editor) { if (!editor) {
this.reloadFont(); this.reloadFont();
} }
@@ -345,10 +360,6 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
); );
} }
editorForNote(note: SNNote) {
return this.application.componentManager!.editorForNote(note);
}
setMenuState(menu: string, state: boolean) { setMenuState(menu: string, state: boolean) {
this.setEditorState({ this.setEditorState({
[menu]: state [menu]: state
@@ -376,42 +387,39 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
this.setEditorState(menuState); this.setEditorState(menuState);
} }
editorMenuOnSelect(component: SNComponent) { async editorMenuOnSelect(component?: SNComponent) {
if (!component || component.area === 'editor-editor') { this.setMenuState('showEditorMenu', false);
/** If plain editor or other editor */ if (!component) {
this.setMenuState('showEditorMenu', false); if (!this.note.prefersPlainEditor) {
const editor = component; await this.application.changeItem(this.note.uuid, (mutator) => {
if (this.getState().editorComponent && editor !== this.getState().editorComponent) { const noteMutator = mutator as NoteMutator;
this.disassociateComponentWithCurrentNote(this.getState().editorComponent!); noteMutator.prefersPlainEditor = true;
})
} }
const note = this.note; if(this.activeEditorComponent?.isExplicitlyEnabledForItem(this.note)) {
if (editor) { await this.disassociateComponentWithCurrentNote(this.activeEditorComponent);
const prefersPlain = note.prefersPlainEditor;
if (prefersPlain) {
this.application.changeItem(note.uuid, (mutator) => {
const noteMutator = mutator as NoteMutator;
noteMutator.prefersPlainEditor = false;
})
}
this.associateComponentWithCurrentNote(editor);
} else {
/** Note prefers plain editor */
if (!note.prefersPlainEditor) {
this.application.changeItem(note.uuid, (mutator) => {
const noteMutator = mutator as NoteMutator;
noteMutator.prefersPlainEditor = true;
})
}
this.reloadFont();
} }
await this.componentGroup.deactivateComponentForArea(ComponentArea.Editor);
this.setEditorState({ this.reloadFont();
editorComponent: editor }
}); else if (component.area === ComponentArea.Editor) {
} else if (component.area === 'editor-stack') { const currentEditor = this.activeEditorComponent;
this.toggleStackComponentForCurrentItem(component); if (currentEditor && component !== currentEditor) {
await this.disassociateComponentWithCurrentNote(currentEditor);
}
const prefersPlain = this.note.prefersPlainEditor;
if (prefersPlain) {
await this.application.changeItem(this.note.uuid, (mutator) => {
const noteMutator = mutator as NoteMutator;
noteMutator.prefersPlainEditor = false;
})
}
await this.associateComponentWithCurrentNote(component);
await this.componentGroup.activateComponent(component);
}
else if (component.area === ComponentArea.EditorStack) {
await this.toggleStackComponentForCurrentItem(component);
} }
/** Dirtying can happen above */ /** Dirtying can happen above */
this.application.sync(); this.application.sync();
} }
@@ -980,55 +988,16 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
ComponentArea.Editor ComponentArea.Editor
], ],
activationHandler: (component) => { activationHandler: (component) => {
if (component.area === 'note-tags') { if (component.area === ComponentArea.EditorStack) {
this.setEditorState({
tagsComponent: component.active ? component : undefined
});
} else if (component.area === 'editor-editor') {
if (
component === this.getState().editorComponent &&
!component.active
) {
this.setEditorState({ editorComponent: undefined });
}
else if (this.getState().editorComponent) {
if (this.getState().editorComponent!.active && this.note) {
if (
component.isExplicitlyEnabledForItem(this.note)
&& !this.getState().editorComponent!.isExplicitlyEnabledForItem(this.note)
) {
this.setEditorState({ editorComponent: component });
}
}
}
else if (this.note) {
const enableable = (
component.isExplicitlyEnabledForItem(this.note)
|| component.isDefaultEditor()
);
if (
component.active
&& enableable
) {
this.setEditorState({ editorComponent: component });
} else {
/**
* Not a candidate, and no qualified editor.
* Disable the current editor.
*/
this.setEditorState({ editorComponent: undefined });
}
}
} else if (component.area === 'editor-stack') {
this.reloadComponentContext(); this.reloadComponentContext();
} }
}, },
contextRequestHandler: (component) => { contextRequestHandler: (component) => {
const currentEditor = this.activeEditorComponent;
if ( if (
component === this.getState().editorComponent || component === currentEditor ||
component === this.getState().tagsComponent || component === this.activeTagsComponent ||
this.getState().componentStack!.includes(component) this.activeStackComponents.includes(component)
) { ) {
return this.note; return this.note;
} }
@@ -1040,7 +1009,10 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
}, },
actionHandler: (component, action, data) => { actionHandler: (component, action, data) => {
if (action === ComponentAction.SetSize) { if (action === ComponentAction.SetSize) {
const setSize = function (element: HTMLElement, size: { width: number, height: number }) { const setSize = (
element: HTMLElement,
size: { width: number, height: number }
) => {
const widthString = typeof size.width === 'string' const widthString = typeof size.width === 'string'
? size.width ? size.width
: `${data.width}px`; : `${data.width}px`;
@@ -1089,16 +1061,15 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
.sort((a, b) => { .sort((a, b) => {
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
}); });
this.setEditorState({ this.setEditorState({
componentStack: components allStackComponents: components
}); });
} }
reloadComponentContext() { reloadComponentContext() {
this.reloadComponentStackArray(); this.reloadComponentStackArray();
if (this.note) { if (this.note) {
for (const component of this.getState().componentStack!) { for (const component of this.getState().allStackComponents!) {
if (component.active) { if (component.active) {
this.application.componentManager!.setComponentHidden( this.application.componentManager!.setComponentHidden(
component, component,
@@ -1113,33 +1084,34 @@ class EditorViewCtrl extends PureViewCtrl implements EditorViewScope {
this.application.componentManager!.contextItemDidChangeInArea(ComponentArea.Editor); this.application.componentManager!.contextItemDidChangeInArea(ComponentArea.Editor);
} }
toggleStackComponentForCurrentItem(component: SNComponent) { async toggleStackComponentForCurrentItem(component: SNComponent) {
const hidden = this.application.componentManager!.isComponentHidden(component); const hidden = this.application.componentManager!.isComponentHidden(component);
if (hidden || !component.active) { if (hidden || !component.active) {
this.application.componentManager!.setComponentHidden(component, false); this.application.componentManager!.setComponentHidden(component, false);
this.associateComponentWithCurrentNote(component); await this.associateComponentWithCurrentNote(component);
if (!component.active) { if (!component.active) {
this.application.componentManager!.activateComponent(component); this.application.componentManager!.activateComponent(component);
} }
this.application.componentManager!.contextItemDidChangeInArea(ComponentArea.EditorStack); this.application.componentManager!.contextItemDidChangeInArea(ComponentArea.EditorStack);
} else { } else {
this.application.componentManager!.setComponentHidden(component, true); this.application.componentManager!.setComponentHidden(component, true);
this.disassociateComponentWithCurrentNote(component); await this.disassociateComponentWithCurrentNote(component);
} }
this.application.sync();
} }
disassociateComponentWithCurrentNote(component: SNComponent) { async disassociateComponentWithCurrentNote(component: SNComponent) {
const note = this.note; const note = this.note;
this.application.changeAndSaveItem(component.uuid, (m) => { return this.application.changeItem(component.uuid, (m) => {
const mutator = m as ComponentMutator; const mutator = m as ComponentMutator;
mutator.removeAssociatedItemId(note.uuid); mutator.removeAssociatedItemId(note.uuid);
mutator.disassociateWithItem(note); mutator.disassociateWithItem(note);
}) })
} }
associateComponentWithCurrentNote(component: SNComponent) { async associateComponentWithCurrentNote(component: SNComponent) {
const note = this.note; const note = this.note;
this.application.changeAndSaveItem(component.uuid, (m) => { return this.application.changeItem(component.uuid, (m) => {
const mutator = m as ComponentMutator; const mutator = m as ComponentMutator;
mutator.removeDisassociatedItemId(note.uuid); mutator.removeDisassociatedItemId(note.uuid);
mutator.associateWithItem(note); mutator.associateWithItem(note);

View File

@@ -1,7 +1,7 @@
#tags-column.sn-component.section.tags(aria-label='Tags') #tags-column.sn-component.section.tags(aria-label='Tags')
.component-view-container(ng-if='self.component.active') .component-view-container(ng-if='self.component.active')
component-view.component-view( component-view.component-view(
component='self.component', component-uuid='self.component.uuid',
application='self.application' application='self.application'
) )
#tags-content.content(ng-if='!(self.component && self.component.active)') #tags-content.content(ng-if='!(self.component && self.component.active)')

View File

@@ -11,6 +11,6 @@
| {{ctrl.component.name}} | {{ctrl.component.name}}
a.sk-a.info.close-button(ng-click="ctrl.dismiss()") Close a.sk-a.info.close-button(ng-click="ctrl.dismiss()") Close
component-view.component-view( component-view.component-view(
component="ctrl.component", component-uuid="ctrl.component.uuid",
application='ctrl.application' application='ctrl.application'
) )

View File

@@ -22,7 +22,7 @@
style="white-space: pre-wrap; font-size: 16px;" style="white-space: pre-wrap; font-size: 16px;"
) {{ctrl.content.text}} ) {{ctrl.content.text}}
component-view.component-view( component-view.component-view(
component="ctrl.editor" component-uuid="ctrl.editor.uuid"
ng-if="ctrl.editor", ng-if="ctrl.editor",
application='ctrl.application' application='ctrl.application'
) )

View File

@@ -0,0 +1,29 @@
import { DeviceInterface, SNApplication } from 'snjs';
export declare class WebDeviceInterface extends DeviceInterface {
private database;
constructor(namespace: string, timeout: any);
setApplication(application: SNApplication): void;
deinit(): void;
getRawStorageValue(key: string): Promise<string | null>;
getAllRawStorageKeyValues(): Promise<{
key: string;
value: any;
}[]>;
setRawStorageValue(key: string, value: any): Promise<void>;
removeRawStorageValue(key: string): Promise<void>;
removeAllRawStorageValues(): Promise<void>;
openDatabase(): Promise<{
isNewDatabase?: boolean | undefined;
} | undefined>;
private getDatabaseKeyPrefix;
private keyForPayloadId;
getAllRawDatabasePayloads(): Promise<any[]>;
saveRawDatabasePayload(payload: any): Promise<void>;
saveRawDatabasePayloads(payloads: any[]): Promise<void>;
removeRawDatabasePayloadWithId(id: string): Promise<void>;
removeAllRawDatabasePayloads(): Promise<void>;
getKeychainValue(): Promise<any>;
setKeychainValue(value: any): Promise<void>;
clearKeychainValue(): Promise<void>;
openUrl(url: string): void;
}

View File

@@ -1,4 +1,5 @@
/// <reference types="angular" /> /// <reference types="angular" />
import { ComponentGroup } from './component_group';
import { EditorGroup } from '@/ui_models/editor_group'; import { EditorGroup } from '@/ui_models/editor_group';
import { PasswordWizardType } from '@/types'; import { PasswordWizardType } from '@/types';
import { SNApplication, Challenge, ChallengeOrchestrator, ProtectedAction } from 'snjs'; import { SNApplication, Challenge, ChallengeOrchestrator, ProtectedAction } from 'snjs';
@@ -21,27 +22,19 @@ export declare class WebApplication extends SNApplication {
private webServices; private webServices;
private currentAuthenticationElement?; private currentAuthenticationElement?;
editorGroup: EditorGroup; editorGroup: EditorGroup;
componentGroup: ComponentGroup;
constructor($compile: ng.ICompileService, $timeout: ng.ITimeoutService, scope: ng.IScope, onDeinit: (app: WebApplication) => void); constructor($compile: ng.ICompileService, $timeout: ng.ITimeoutService, scope: ng.IScope, onDeinit: (app: WebApplication) => void);
/** @override */ /** @override */
deinit(): void; deinit(): void;
setWebServices(services: WebServices): void; setWebServices(services: WebServices): void;
/** @access public */
getAppState(): AppState; getAppState(): AppState;
/** @access public */
getDesktopService(): DesktopManager; getDesktopService(): DesktopManager;
/** @access public */
getLockService(): LockManager; getLockService(): LockManager;
/** @access public */
getArchiveService(): ArchiveManager; getArchiveService(): ArchiveManager;
/** @access public */
getNativeExtService(): NativeExtManager; getNativeExtService(): NativeExtManager;
/** @access public */
getStatusService(): StatusManager; getStatusService(): StatusManager;
/** @access public */
getThemeService(): ThemeManager; getThemeService(): ThemeManager;
/** @access public */
getPrefsService(): PreferencesManager; getPrefsService(): PreferencesManager;
/** @access public */
getKeyboardService(): KeyboardManager; getKeyboardService(): KeyboardManager;
checkForSecurityUpdate(): Promise<boolean>; checkForSecurityUpdate(): Promise<boolean>;
presentPasswordWizard(type: PasswordWizardType): void; presentPasswordWizard(type: PasswordWizardType): void;

View File

@@ -0,0 +1,23 @@
import { SNComponent, ComponentArea } from 'snjs';
import { WebApplication } from './application';
export declare class ComponentGroup {
private application;
changeObservers: any[];
activeComponents: Partial<Record<string, SNComponent>>;
constructor(application: WebApplication);
get componentManager(): import("../../../../../snjs/dist/@types").SNComponentManager;
deinit(): void;
registerComponentHandler(): void;
activateComponent(component: SNComponent): Promise<void>;
deactivateComponent(component: SNComponent): Promise<void>;
deactivateComponentForArea(area: ComponentArea): Promise<void>;
activeComponentForArea(area: ComponentArea): SNComponent;
activeComponentsForArea(area: ComponentArea): SNComponent[];
allComponentsForArea(area: ComponentArea): SNComponent[];
private allActiveComponents;
/**
* Notifies observer when the active editor has changed.
*/
addChangeObserver(callback: any): void;
private notifyObservers;
}

View File

@@ -1,6 +1,6 @@
export declare function getParameterByName(name: string, url: string): string | null; export declare function getParameterByName(name: string, url: string): string | null;
export declare function isNullOrUndefined(value: any): boolean; export declare function isNullOrUndefined(value: any): boolean;
export declare function dictToArray(dict: any): any[]; export declare function dictToArray<T>(dict: Record<any, T>): NonNullable<T>[];
export declare function getPlatformString(): string; export declare function getPlatformString(): string;
export declare function dateToLocalizedString(date: Date): string; export declare function dateToLocalizedString(date: Date): string;
/** Via https://davidwalsh.name/javascript-debounce-function */ /** Via https://davidwalsh.name/javascript-debounce-function */

2113
dist/javascripts/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long