diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 05b5dbd3d..039f62df9 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -30,8 +30,8 @@ import { AccountSwitcher } from './views/account_switcher/account_switcher'; import { ApplicationGroupView, ApplicationView, - EditorGroupView, - EditorView, + NoteGroupViewDirective, + NoteViewDirective, TagsView, FooterView, ChallengeModal, @@ -141,8 +141,8 @@ const startApplication: StartApplication = async function startApplication( .module('app') .directive('applicationGroupView', () => new ApplicationGroupView()) .directive('applicationView', () => new ApplicationView()) - .directive('editorGroupView', () => new EditorGroupView()) - .directive('editorView', () => new EditorView()) + .directive('noteGroupView', () => new NoteGroupViewDirective()) + .directive('noteView', () => new NoteViewDirective()) .directive('tagsView', () => new TagsView()) .directive('footerView', () => new FooterView()); diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index 4d1ce9806..642d9f8c0 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -2,7 +2,7 @@ import { Bridge } from '@/services/bridge'; import { storage, StorageKey } from '@/services/localStorage'; import { WebApplication } from '@/ui_models/application'; import { AccountMenuState } from '@/ui_models/app_state/account_menu_state'; -import { Editor } from '@/ui_models/editor'; +import { NoteViewController } from '@/views/note_view/note_view_controller'; import { isDesktopApplication } from '@/utils'; import { ApplicationEvent, @@ -226,9 +226,9 @@ export class AppState { storage.set(StorageKey.ShowBetaWarning, true); } - async createEditor(title?: string) { + async openNewNote(title?: string) { if (!this.multiEditorSupport) { - this.closeActiveEditor(); + this.closeActiveNoteController(); } const activeTagUuid = this.selectedTag ? this.selectedTag.isSmartTag @@ -236,37 +236,37 @@ export class AppState { : this.selectedTag.uuid : undefined; - await this.application.editorGroup.createEditor( + await this.application.noteControllerGroup.createNoteView( undefined, title, activeTagUuid ); } - getActiveEditor() { - return this.application.editorGroup.editors[0]; + getActiveNoteController() { + return this.application.noteControllerGroup.noteControllers[0]; } - getEditors() { - return this.application.editorGroup.editors; + getNoteControllers() { + return this.application.noteControllerGroup.noteControllers; } - closeEditor(editor: Editor) { - this.application.editorGroup.closeEditor(editor); + closeNoteController(controller: NoteViewController) { + this.application.noteControllerGroup.closeNoteView(controller); } - closeActiveEditor() { - this.application.editorGroup.closeActiveEditor(); + closeActiveNoteController() { + this.application.noteControllerGroup.closeActiveNoteView(); } - closeAllEditors() { - this.application.editorGroup.closeAllEditors(); + closeAllNoteControllers() { + this.application.noteControllerGroup.closeAllNoteViews(); } - editorForNote(note: SNNote) { - for (const editor of this.getEditors()) { - if (editor.note.uuid === note.uuid) { - return editor; + noteControllerForNote(note: SNNote) { + for (const controller of this.getNoteControllers()) { + if (controller.note.uuid === note.uuid) { + return controller; } } } @@ -275,31 +275,31 @@ export class AppState { this.application.streamItems( [ContentType.Note, ContentType.Tag], async (items, source) => { - /** Close any editors for deleted/trashed/archived notes */ + /** Close any note controllers for deleted/trashed/archived notes */ if (source === PayloadSource.PreSyncSave) { const notes = items.filter( (candidate) => candidate.content_type === ContentType.Note ) as SNNote[]; for (const note of notes) { - const editor = this.editorForNote(note); - if (!editor) { + const noteController = this.noteControllerForNote(note); + if (!noteController) { continue; } if (note.deleted) { - this.closeEditor(editor); + this.closeNoteController(noteController); } else if ( note.trashed && !this.selectedTag?.isTrashTag && !this.searchOptions.includeTrashed ) { - this.closeEditor(editor); + this.closeNoteController(noteController); } else if ( note.archived && !this.selectedTag?.isArchiveTag && !this.searchOptions.includeArchived && !this.application.getPreference(PrefKey.NotesShowArchived, false) ) { - this.closeEditor(editor); + this.closeNoteController(noteController); } } } diff --git a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts index b49699c50..f24a08936 100644 --- a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts +++ b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts @@ -48,7 +48,7 @@ export class NoteTagsState { } get activeNote(): SNNote | undefined { - return this.appState.notes.activeEditor?.note; + return this.appState.notes.activeNoteController?.note; } get autocompleteTagHintVisible(): boolean { diff --git a/app/assets/javascripts/ui_models/app_state/notes_state.ts b/app/assets/javascripts/ui_models/app_state/notes_state.ts index f38ea99ed..44dda28e1 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -17,7 +17,7 @@ import { runInAction, } from 'mobx'; import { WebApplication } from '../application'; -import { Editor } from '../editor'; +import { NoteViewController } from '@/views/note_view/note_view_controller'; import { AppState } from './app_state'; export class NotesState { @@ -68,8 +68,8 @@ export class NotesState { ); } - get activeEditor(): Editor | undefined { - return this.application.editorGroup.editors[0]; + get activeNoteController(): NoteViewController | undefined { + return this.application.noteControllerGroup.noteControllers[0]; } get selectedNotesCount(): number { @@ -151,13 +151,13 @@ export class NotesState { } if (this.selectedNotesCount === 1) { - await this.openEditor(Object.keys(this.selectedNotes)[0]); + await this.openNote(Object.keys(this.selectedNotes)[0]); } } } - private async openEditor(noteUuid: string): Promise { - if (this.activeEditor?.note?.uuid === noteUuid) { + private async openNote(noteUuid: string): Promise { + if (this.activeNoteController?.note?.uuid === noteUuid) { return; } @@ -167,11 +167,11 @@ export class NotesState { return; } - if (this.activeEditor) { - this.application.editorGroup.closeActiveEditor(); + if (this.activeNoteController) { + this.application.noteControllerGroup.closeActiveNoteView(); } - await this.application.editorGroup.createEditor(noteUuid); + await this.application.noteControllerGroup.createNoteView(noteUuid); this.appState.noteTags.reloadTags(); await this.onActiveEditorChanged(); diff --git a/app/assets/javascripts/ui_models/app_state/notes_view_state.ts b/app/assets/javascripts/ui_models/app_state/notes_view_state.ts index 62a5f6047..e0be2a0e0 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_view_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_view_state.ts @@ -70,7 +70,7 @@ export class NotesViewState { appObservers.push( application.streamItems(ContentType.Note, () => { this.reloadNotes(); - const activeNote = this.appState.notes.activeEditor?.note; + const activeNote = this.appState.notes.activeNoteController?.note; if (this.application.getAppState().notes.selectedNotesCount < 2) { if (activeNote) { const discarded = activeNote.deleted || activeNote.trashed; @@ -102,7 +102,7 @@ export class NotesViewState { this.reloadPreferences(); }, ApplicationEvent.PreferencesChanged), application.addEventObserver(async () => { - this.appState.closeAllEditors(); + this.appState.closeAllNoteControllers(); this.selectFirstNote(); this.setCompletedFullSync(false); }, ApplicationEvent.SignedIn), @@ -111,7 +111,8 @@ export class NotesViewState { if ( this.notes.length === 0 && this.appState.selectedTag?.isAllTag && - this.noteFilterText === '' + this.noteFilterText === '' && + !this.appState.notes.activeNoteController ) { this.createPlaceholderNote(); } @@ -191,7 +192,7 @@ export class NotesViewState { } get activeEditorNote() { - return this.appState.notes.activeEditor?.note; + return this.appState.notes.activeNoteController?.note; } reloadPanelTitle = () => { @@ -318,19 +319,15 @@ export class NotesViewState { } }; - createNewNote = async (focusNewNote = true) => { + createNewNote = async () => { this.appState.notes.unselectNotes(); let title = `Note ${this.notes.length + 1}`; if (this.isFiltering) { title = this.noteFilterText; } - await this.appState.createEditor(title); + await this.appState.openNewNote(title); this.reloadNotes(); this.appState.noteTags.reloadTags(); - const noteTitleEditorElement = document.getElementById('note-title-editor'); - if (focusNewNote) { - noteTitleEditorElement?.focus(); - } }; createPlaceholderNote = () => { @@ -338,7 +335,7 @@ export class NotesViewState { if (selectedTag && selectedTag.isSmartTag && !selectedTag.isAllTag) { return; } - return this.createNewNote(false); + return this.createNewNote(); }; get optionsSubtitle(): string { @@ -436,7 +433,7 @@ export class NotesViewState { if (note) { this.selectNote(note, false, false); } else { - this.appState.closeActiveEditor(); + this.appState.closeActiveNoteController(); } }; @@ -467,7 +464,7 @@ export class NotesViewState { }; handleEditorChange = async () => { - const activeNote = this.appState.getActiveEditor()?.note; + const activeNote = this.appState.getActiveNoteController()?.note; if (activeNote && activeNote.conflictOf) { this.application.changeAndSaveItem(activeNote.uuid, (mutator) => { mutator.conflictOf = undefined; @@ -505,7 +502,7 @@ export class NotesViewState { this.activeEditorNote && !this.notes.includes(this.activeEditorNote) ) { - this.appState.closeActiveEditor(); + this.appState.closeActiveNoteController(); } } }; diff --git a/app/assets/javascripts/ui_models/application.ts b/app/assets/javascripts/ui_models/application.ts index a0709260d..d574a938b 100644 --- a/app/assets/javascripts/ui_models/application.ts +++ b/app/assets/javascripts/ui_models/application.ts @@ -10,7 +10,7 @@ import { StatusManager } from '@/services/statusManager'; import { ThemeManager } from '@/services/themeManager'; import { PasswordWizardScope, PasswordWizardType } from '@/types'; import { AppState } from '@/ui_models/app_state'; -import { EditorGroup } from '@/ui_models/editor_group'; +import { NoteGroupController } from '@/views/note_group_view/note_group_controller'; import { AppVersion } from '@/version'; import { WebDeviceInterface } from '@/web_device_interface'; import { @@ -36,7 +36,7 @@ export class WebApplication extends SNApplication { private scope?: angular.IScope; private webServices!: WebServices; private currentAuthenticationElement?: angular.IRootElementService; - public editorGroup: EditorGroup; + public noteControllerGroup: NoteGroupController; /* @ngInject */ constructor( @@ -66,7 +66,7 @@ export class WebApplication extends SNApplication { this.$compile = $compile; this.scope = scope; deviceInterface.setApplication(this); - this.editorGroup = new EditorGroup(this); + this.noteControllerGroup = new NoteGroupController(this); this.presentPermissionsDialog = this.presentPermissionsDialog.bind(this); } @@ -80,7 +80,7 @@ export class WebApplication extends SNApplication { } this.webServices = {} as WebServices; (this.$compile as unknown) = undefined; - this.editorGroup.deinit(); + this.noteControllerGroup.deinit(); (this.scope as any).application = undefined; this.scope!.$destroy(); this.scope = undefined; diff --git a/app/assets/javascripts/ui_models/editor_group.ts b/app/assets/javascripts/ui_models/editor_group.ts deleted file mode 100644 index 577478036..000000000 --- a/app/assets/javascripts/ui_models/editor_group.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { removeFromArray, UuidString } from '@standardnotes/snjs'; -import { Editor } from './editor'; -import { WebApplication } from './application'; - -type EditorGroupChangeCallback = () => void; - -export class EditorGroup { - public editors: Editor[] = []; - private application: WebApplication; - changeObservers: EditorGroupChangeCallback[] = []; - - constructor(application: WebApplication) { - this.application = application; - } - - public deinit() { - (this.application as unknown) = undefined; - for (const editor of this.editors) { - this.deleteEditor(editor); - } - } - - async createEditor( - noteUuid?: string, - noteTitle?: string, - noteTag?: UuidString - ) { - const editor = new Editor(this.application, noteUuid, noteTitle, noteTag); - await editor.initialize(); - this.editors.push(editor); - this.notifyObservers(); - } - - deleteEditor(editor: Editor) { - editor.deinit(); - removeFromArray(this.editors, editor); - } - - closeEditor(editor: Editor) { - this.deleteEditor(editor); - this.notifyObservers(); - } - - closeActiveEditor() { - const activeEditor = this.activeEditor; - if (activeEditor) { - this.deleteEditor(activeEditor); - } - } - - closeAllEditors() { - for (const editor of this.editors) { - this.deleteEditor(editor); - } - } - - get activeEditor() { - return this.editors[0]; - } - - /** - * Notifies observer when the active editor has changed. - */ - public addChangeObserver(callback: EditorGroupChangeCallback) { - this.changeObservers.push(callback); - if (this.activeEditor) { - callback(); - } - return () => { - removeFromArray(this.changeObservers, callback); - }; - } - - private notifyObservers() { - for (const observer of this.changeObservers) { - observer(); - } - } -} diff --git a/app/assets/javascripts/views/application/application-view.pug b/app/assets/javascripts/views/application/application-view.pug index e39519b6c..01630f2b5 100644 --- a/app/assets/javascripts/views/application/application-view.pug +++ b/app/assets/javascripts/views/application/application-view.pug @@ -10,7 +10,7 @@ application='self.application' app-state='self.appState' ) - editor-group-view.flex-grow(application='self.application') + note-group-view.flex-grow(application='self.application') footer-view( ng-if='!self.state.needsUnlock && self.state.launched' diff --git a/app/assets/javascripts/views/editor_group/editor_group_view.ts b/app/assets/javascripts/views/editor_group/editor_group_view.ts deleted file mode 100644 index df7ded8e6..000000000 --- a/app/assets/javascripts/views/editor_group/editor_group_view.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { WebDirective } from './../../types'; -import template from './editor-group-view.pug'; -import { Editor } from '@/ui_models/editor'; -import { PureViewCtrl } from '../abstract/pure_view_ctrl'; - -class EditorGroupViewCtrl extends PureViewCtrl { - - public editors: Editor[] = [] - - /* @ngInject */ - constructor($timeout: ng.ITimeoutService,) { - super($timeout); - this.state = { - showMultipleSelectedNotes: false - }; - } - - $onInit() { - this.application.editorGroup.addChangeObserver(() => { - this.editors = this.application.editorGroup.editors; - }); - this.autorun(() => { - this.setState({ - showMultipleSelectedNotes: this.appState.notes.selectedNotesCount > 1 - }); - }); - } -} - -export class EditorGroupView extends WebDirective { - constructor() { - super(); - this.template = template; - this.controller = EditorGroupViewCtrl; - this.controllerAs = 'self'; - this.bindToController = true; - this.scope = { - application: '=' - }; - } -} diff --git a/app/assets/javascripts/views/index.ts b/app/assets/javascripts/views/index.ts index a68bf862d..82223ae4c 100644 --- a/app/assets/javascripts/views/index.ts +++ b/app/assets/javascripts/views/index.ts @@ -1,8 +1,8 @@ export { PureViewCtrl } from './abstract/pure_view_ctrl'; export { ApplicationGroupView } from './application_group/application_group_view'; export { ApplicationView } from './application/application_view'; -export { EditorGroupView } from './editor_group/editor_group_view'; -export { EditorView } from './editor/editor_view'; +export { NoteGroupViewDirective } from './note_group_view/note_group_view'; +export { NoteViewDirective } from './note_view/note_view'; export { FooterView } from './footer/footer_view'; export { TagsView } from './tags/tags_view'; export { ChallengeModal } from './challenge_modal/challenge_modal'; diff --git a/app/assets/javascripts/views/editor_group/editor-group-view.pug b/app/assets/javascripts/views/note_group_view/note-group-view.pug similarity index 75% rename from app/assets/javascripts/views/editor_group/editor-group-view.pug rename to app/assets/javascripts/views/note_group_view/note-group-view.pug index 2947a1dbe..4baafa105 100644 --- a/app/assets/javascripts/views/editor_group/editor-group-view.pug +++ b/app/assets/javascripts/views/note_group_view/note-group-view.pug @@ -6,9 +6,9 @@ ) .flex-grow.h-full( ng-if='!self.state.showMultipleSelectedNotes' - ng-repeat='editor in self.editors' + ng-repeat='controller in self.controllers' ) - editor-view( + note-view( application='self.application' - editor='editor' + controller='controller' ) diff --git a/app/assets/javascripts/views/note_group_view/note_group_controller.ts b/app/assets/javascripts/views/note_group_view/note_group_controller.ts new file mode 100644 index 000000000..c625fc47c --- /dev/null +++ b/app/assets/javascripts/views/note_group_view/note_group_controller.ts @@ -0,0 +1,84 @@ +import { removeFromArray, UuidString } from '@standardnotes/snjs'; +import { NoteViewController } from '@/views/note_view/note_view_controller'; +import { WebApplication } from '@/ui_models/application'; + +type NoteControllerGroupChangeCallback = () => void; + +export class NoteGroupController { + public noteControllers: NoteViewController[] = []; + private application: WebApplication; + changeObservers: NoteControllerGroupChangeCallback[] = []; + + constructor(application: WebApplication) { + this.application = application; + } + + public deinit() { + (this.application as unknown) = undefined; + for (const controller of this.noteControllers) { + this.deleteNoteView(controller); + } + } + + async createNoteView( + noteUuid?: string, + noteTitle?: string, + noteTag?: UuidString + ) { + const controller = new NoteViewController( + this.application, + noteUuid, + noteTitle, + noteTag + ); + await controller.initialize(); + this.noteControllers.push(controller); + this.notifyObservers(); + } + + deleteNoteView(controller: NoteViewController) { + controller.deinit(); + removeFromArray(this.noteControllers, controller); + } + + closeNoteView(controller: NoteViewController) { + this.deleteNoteView(controller); + this.notifyObservers(); + } + + closeActiveNoteView() { + const activeController = this.activeNoteViewController; + if (activeController) { + this.deleteNoteView(activeController); + } + } + + closeAllNoteViews() { + for (const controller of this.noteControllers) { + this.deleteNoteView(controller); + } + } + + get activeNoteViewController() { + return this.noteControllers[0]; + } + + /** + * Notifies observer when the active controller has changed. + */ + public addChangeObserver(callback: NoteControllerGroupChangeCallback) { + this.changeObservers.push(callback); + if (this.activeNoteViewController) { + callback(); + } + return () => { + removeFromArray(this.changeObservers, callback); + }; + } + + private notifyObservers() { + for (const observer of this.changeObservers) { + observer(); + } + } +} diff --git a/app/assets/javascripts/views/note_group_view/note_group_view.ts b/app/assets/javascripts/views/note_group_view/note_group_view.ts new file mode 100644 index 000000000..8d598d1aa --- /dev/null +++ b/app/assets/javascripts/views/note_group_view/note_group_view.ts @@ -0,0 +1,45 @@ +import { WebDirective } from './../../types'; +import template from './note-group-view.pug'; +import { NoteViewController } from '@/views/note_view/note_view_controller'; +import { PureViewCtrl } from '../abstract/pure_view_ctrl'; + +class NoteGroupView extends PureViewCtrl< + unknown, + { + showMultipleSelectedNotes: boolean; + } +> { + public controllers: NoteViewController[] = []; + + /* @ngInject */ + constructor($timeout: ng.ITimeoutService) { + super($timeout); + this.state = { + showMultipleSelectedNotes: false, + }; + } + + $onInit() { + this.application.noteControllerGroup.addChangeObserver(() => { + this.controllers = this.application.noteControllerGroup.noteControllers; + }); + this.autorun(() => { + this.setState({ + showMultipleSelectedNotes: this.appState.notes.selectedNotesCount > 1, + }); + }); + } +} + +export class NoteGroupViewDirective extends WebDirective { + constructor() { + super(); + this.template = template; + this.controller = NoteGroupView; + this.controllerAs = 'self'; + this.bindToController = true; + this.scope = { + application: '=', + }; + } +} diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/note_view/note-view.pug similarity index 99% rename from app/assets/javascripts/views/editor/editor-view.pug rename to app/assets/javascripts/views/note_view/note-view.pug index 3f2f73966..1c851734f 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/note_view/note-view.pug @@ -114,7 +114,7 @@ component-view.component-view( component-viewer='self.state.editorComponentViewer', ng-if='self.state.editorComponentViewer', - on-load='self.onEditorLoad', + on-load='self.onEditorComponentLoad', request-reload='self.editorComponentViewerRequestsReload' application='self.application' app-state='self.appState' diff --git a/app/assets/javascripts/views/editor/editor_view.test.ts b/app/assets/javascripts/views/note_view/note_view.test.ts similarity index 97% rename from app/assets/javascripts/views/editor/editor_view.test.ts rename to app/assets/javascripts/views/note_view/note_view.test.ts index d70a3c9d0..c93066854 100644 --- a/app/assets/javascripts/views/editor/editor_view.test.ts +++ b/app/assets/javascripts/views/note_view/note_view.test.ts @@ -2,19 +2,19 @@ * @jest-environment jsdom */ -import { EditorViewCtrl } from '@Views/editor/editor_view'; +import { NoteView } from '@Views/note_view/note_view'; import { ApplicationEvent, ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, } from '@standardnotes/snjs/'; describe('editor-view', () => { - let ctrl: EditorViewCtrl; + let ctrl: NoteView; let setShowProtectedWarningSpy: jest.SpyInstance; beforeEach(() => { const $timeout = {} as jest.Mocked; - ctrl = new EditorViewCtrl($timeout); + ctrl = new NoteView($timeout); setShowProtectedWarningSpy = jest.spyOn(ctrl, 'setShowProtectedOverlay'); diff --git a/app/assets/javascripts/views/editor/editor_view.ts b/app/assets/javascripts/views/note_view/note_view.ts similarity index 94% rename from app/assets/javascripts/views/editor/editor_view.ts rename to app/assets/javascripts/views/note_view/note_view.ts index dbc8b4d3b..cd3857605 100644 --- a/app/assets/javascripts/views/editor/editor_view.ts +++ b/app/assets/javascripts/views/note_view/note_view.ts @@ -1,5 +1,5 @@ import { STRING_SAVING_WHILE_DOCUMENT_HIDDEN } from './../../strings'; -import { Editor } from '@/ui_models/editor'; +import { NoteViewController } from '@/views/note_view/note_view_controller'; import { WebApplication } from '@/ui_models/application'; import { PanelPuppet, WebDirective } from '@/types'; import angular from 'angular'; @@ -21,9 +21,9 @@ import { ItemMutator, ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, } from '@standardnotes/snjs'; -import { isDesktopApplication } from '@/utils'; +import { debounce, isDesktopApplication } from '@/utils'; import { KeyboardModifier, KeyboardKey } from '@/services/ioService'; -import template from './editor-view.pug'; +import template from './note-view.pug'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { EventSource } from '@/ui_models/app_state'; import { @@ -90,10 +90,10 @@ function sortAlphabetically(array: SNComponent[]): SNComponent[] { ); } -export class EditorViewCtrl extends PureViewCtrl { +export class NoteView extends PureViewCtrl { /** Passed through template */ readonly application!: WebApplication; - readonly editor!: Editor; + readonly controller!: NoteViewController; private leftPanelPuppet?: PanelPuppet; private rightPanelPuppet?: PanelPuppet; @@ -101,7 +101,7 @@ export class EditorViewCtrl extends PureViewCtrl { private statusTimeout?: ng.IPromise; private lastEditorFocusEventSource?: EventSource; public editorValues: EditorValues = { title: '', text: '' }; - onEditorLoad?: () => void; + onEditorComponentLoad?: () => void; private scrollPosition = 0; private removeTrashKeyObserver?: () => void; @@ -127,9 +127,13 @@ export class EditorViewCtrl extends PureViewCtrl { this.resetScrollPosition = this.resetScrollPosition.bind(this); this.editorComponentViewerRequestsReload = this.editorComponentViewerRequestsReload.bind(this); - this.onEditorLoad = () => { + this.onEditorComponentLoad = () => { this.application.getDesktopService().redoSearch(); }; + this.debounceReloadEditorComponent = debounce( + this.debounceReloadEditorComponent.bind(this), + 25 + ); } deinit() { @@ -144,7 +148,7 @@ export class EditorViewCtrl extends PureViewCtrl { this.removeTabObserver = undefined; this.leftPanelPuppet = undefined; this.rightPanelPuppet = undefined; - this.onEditorLoad = undefined; + this.onEditorComponentLoad = undefined; this.saveTimeout = undefined; this.statusTimeout = undefined; (this.onPanelResizeFinish as unknown) = undefined; @@ -157,14 +161,14 @@ export class EditorViewCtrl extends PureViewCtrl { } get note() { - return this.editor.note; + return this.controller.note; } $onInit() { super.$onInit(); this.registerKeyboardShortcuts(); - this.editor.setOnNoteValueChange((note, source) => { - this.onNoteChanges(note, source); + this.controller.setOnNoteInnerValueChange((note, source) => { + this.onNoteInnerChange(note, source); }); this.autorun(() => { this.setState({ @@ -180,12 +184,14 @@ export class EditorViewCtrl extends PureViewCtrl { this.reloadPreferences(); - if (this.note.dirty) { - this.showSavingStatus(); + if (this.controller.isTemplateNote) { + this.$timeout(() => { + this.focusTitle(); + }); } } - private onNoteChanges(note: SNNote, source: PayloadSource): void { + private onNoteInnerChange(note: SNNote, source: PayloadSource): void { if (note.uuid !== this.note.uuid) { throw Error('Editor received changes for non-current note'); } @@ -214,6 +220,7 @@ export class EditorViewCtrl extends PureViewCtrl { ) { this.showSavingStatus(); } else if ( + this.state.noteStatus && note.lastSyncEnd!.getTime() > note.lastSyncBegan!.getTime() ) { this.showAllChangesSavedStatus(); @@ -376,7 +383,7 @@ export class EditorViewCtrl extends PureViewCtrl { } if (!this.note) return; await this.reloadStackComponents(); - await this.reloadEditorComponent(); + this.debounceReloadEditorComponent(); } ); } @@ -403,13 +410,22 @@ export class EditorViewCtrl extends PureViewCtrl { }); } + /** + * Calling reloadEditorComponent successively without waiting for state to settle + * can result in componentViewers being dealloced twice + */ + debounceReloadEditorComponent() { + this.reloadEditorComponent(); + } + private async reloadEditorComponent() { const newEditor = this.application.componentManager.editorForNote( this.note ); /** Editors cannot interact with template notes so the note must be inserted */ - if (newEditor && this.editor.isTemplateNote) { - await this.editor.insertTemplatedNote(); + if (newEditor && this.controller.isTemplateNote) { + await this.controller.insertTemplatedNote(); + this.associateComponentWithCurrentNote(newEditor); } const currentComponentViewer = this.state.editorComponentViewer; @@ -463,8 +479,8 @@ export class EditorViewCtrl extends PureViewCtrl { const transactions: TransactionalMutation[] = []; this.setMenuState('showEditorMenu', false); - if (this.appState.getActiveEditor()?.isTemplateNote) { - await this.appState.getActiveEditor().insertTemplatedNote(); + if (this.appState.getActiveNoteController()?.isTemplateNote) { + await this.appState.getActiveNoteController().insertTemplatedNote(); } if (this.note.locked) { this.application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT); @@ -550,7 +566,7 @@ export class EditorViewCtrl extends PureViewCtrl { ) { const title = editorValues.title; const text = editorValues.text; - const isTemplate = this.editor.isTemplateNote; + const isTemplate = this.controller.isTemplateNote; if (document.hidden) { this.application.alertService.alert(STRING_SAVING_WHILE_DOCUMENT_HIDDEN); return; @@ -560,7 +576,7 @@ export class EditorViewCtrl extends PureViewCtrl { return; } if (isTemplate) { - await this.editor.insertTemplatedNote(); + await this.controller.insertTemplatedNote(); } if (!this.application.findItem(note.uuid)) { this.application.alertService.alert(STRING_INVALID_NOTE); @@ -598,7 +614,7 @@ export class EditorViewCtrl extends PureViewCtrl { this.saveTimeout = this.$timeout(() => { this.application.sync(); if (closeAfterSync) { - this.appState.closeEditor(this.editor); + this.appState.closeNoteController(this.controller); } }, syncDebouceMs); } @@ -703,7 +719,7 @@ export class EditorViewCtrl extends PureViewCtrl { } async deleteNote(permanently: boolean) { - if (this.editor.isTemplateNote) { + if (this.controller.isTemplateNote) { this.application.alertService.alert(STRING_DELETE_PLACEHOLDER_ATTEMPT); return; } @@ -1023,17 +1039,17 @@ export class EditorViewCtrl extends PureViewCtrl { } } -export class EditorView extends WebDirective { +export class NoteViewDirective extends WebDirective { constructor() { super(); this.restrict = 'E'; this.scope = { - editor: '=', + controller: '=', application: '=', }; this.template = template; this.replace = true; - this.controller = EditorViewCtrl; + this.controller = NoteView; this.controllerAs = 'self'; this.bindToController = true; } diff --git a/app/assets/javascripts/ui_models/editor.ts b/app/assets/javascripts/views/note_view/note_view_controller.ts similarity index 68% rename from app/assets/javascripts/ui_models/editor.ts rename to app/assets/javascripts/views/note_view/note_view_controller.ts index 41e2e63d6..5b6a81d14 100644 --- a/app/assets/javascripts/ui_models/editor.ts +++ b/app/assets/javascripts/views/note_view/note_view_controller.ts @@ -5,9 +5,9 @@ import { UuidString, SNTag, } from '@standardnotes/snjs'; -import { WebApplication } from './application'; +import { WebApplication } from '@/ui_models/application'; -export class Editor { +export class NoteViewController { public note!: SNNote; private application: WebApplication; private onNoteValueChange?: (note: SNNote, source: PayloadSource) => void; @@ -28,7 +28,21 @@ export class Editor { async initialize(): Promise { if (!this.note) { - await this.createTemplateNote(this.defaultTitle, this.defaultTag); + const note = (await this.application.createTemplateItem( + ContentType.Note, + { + text: '', + title: this.defaultTitle, + references: [], + } + )) as SNNote; + if (this.defaultTag) { + const tag = this.application.findItem(this.defaultTag) as SNTag; + await this.application.addTagHierarchyToNote(note, tag); + } + this.isTemplateNote = true; + this.note = note; + this.onNoteValueChange?.(this.note, this.note.payload.source); } this.streamItems(); } @@ -67,29 +81,10 @@ export class Editor { } /** - * Reverts the editor to a blank state, removing any existing note from view, - * and creating a placeholder note. - */ - async createTemplateNote(defaultTitle?: string, noteTag?: UuidString) { - const note = (await this.application.createTemplateItem(ContentType.Note, { - text: '', - title: defaultTitle, - references: [], - })) as SNNote; - if (noteTag) { - const tag = this.application.findItem(noteTag) as SNTag; - await this.application.addTagHierarchyToNote(note, tag); - } - this.isTemplateNote = true; - this.note = note; - this.onNoteValueChange?.(this.note, this.note.payload.source); - } - - /** - * Register to be notified when the editor's note's values change + * Register to be notified when the controller's note's inner values change * (and thus a new object reference is created) */ - public setOnNoteValueChange( + public setOnNoteInnerValueChange( callback: (note: SNNote, source: PayloadSource) => void ) { this.onNoteValueChange = callback; diff --git a/package.json b/package.json index 027066f3f..de6783cdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.9.10", + "version": "3.9.11", "license": "AGPL-3.0-or-later", "repository": { "type": "git", @@ -84,10 +84,10 @@ "@reach/checkbox": "^0.16.0", "@reach/dialog": "^0.16.2", "@reach/listbox": "^0.16.2", - "@standardnotes/features": "1.10.2", + "@standardnotes/features": "1.15.1", "@reach/tooltip": "^0.16.2", "@standardnotes/sncrypto-web": "1.5.3", - "@standardnotes/snjs": "2.29.0", + "@standardnotes/snjs": "2.30.0", "mobx": "^6.3.5", "mobx-react-lite": "^3.2.2", "preact": "^10.5.15", diff --git a/yarn.lock b/yarn.lock index a86fd2d27..5a8cea5e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2623,14 +2623,6 @@ dependencies: "@standardnotes/auth" "^3.8.1" -"@standardnotes/features@1.10.2": - version "1.10.2" - resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.10.2.tgz#a0783f66c00e21cb7692edc0cea95ec25a0253a5" - integrity sha512-Zh6EMjli4mL6jlXEhMyU3qYIKFJj5kuhbxtHXiErUGIDy+s1hHY+THFFO53Jdga2+8wgcATWlmSBY7dieVA8uA== - dependencies: - "@standardnotes/auth" "3.8.3" - "@standardnotes/common" "^1.2.1" - "@standardnotes/features@1.11.0": version "1.11.0" resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.11.0.tgz#66e960a20358c5f58b6be4e19226b34df6f4efbf" @@ -2639,6 +2631,14 @@ "@standardnotes/auth" "3.8.3" "@standardnotes/common" "^1.2.1" +"@standardnotes/features@1.15.1": + version "1.15.1" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.15.1.tgz#3880d73fb32ab952359e70fb313abfdf14023c7d" + integrity sha512-QurBvYwfNrcAKLwE7Z/1EsFpZ6Vuwp8E9O7jlMlzf0GyMXY/djCooeXjmoCWed+B/qJ4sdDdtptGyMsjKVeJCw== + dependencies: + "@standardnotes/auth" "3.8.3" + "@standardnotes/common" "1.2.1" + "@standardnotes/settings@^1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.8.0.tgz#d7bd1f35c3b500d12ba73f5f385b1019baae3efc" @@ -2658,10 +2658,10 @@ buffer "^6.0.3" libsodium-wrappers "^0.7.9" -"@standardnotes/snjs@2.29.0": - version "2.29.0" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.29.0.tgz#6c7c6ccd983df4a1a5e2063647eb731304002fd9" - integrity sha512-Y+GpNiFyJtVr2W3nVbC2zljtXpBlqe3cB4+R1REE0V4hnQBaq/HE6PaUd80TnFj99Kl8lowyH/o4bNV3+CjGgg== +"@standardnotes/snjs@2.30.0": + version "2.30.0" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.30.0.tgz#12abda65e8069273347bbf6786d6a99203260023" + integrity sha512-0s7NYtO8edvY3/MNwx/Ic+fIDXiSnaVhdBWlaIOreVHwUl15LkHyN8mptR8QLtHOjOhvaguew+KCvmXXmkNjTA== dependencies: "@standardnotes/auth" "3.8.1" "@standardnotes/common" "1.2.1"