refactor: Move notes_view to React (#761)

This commit is contained in:
Aman Harwara
2021-12-21 20:31:11 +05:30
committed by GitHub
parent f120af3b43
commit 270fcbc3bc
20 changed files with 1495 additions and 1142 deletions

View File

@@ -6,7 +6,10 @@
ng-if='!self.state.needsUnlock && self.state.ready'
)
tags-view(application='self.application')
notes-view(application='self.application')
notes-view(
application='self.application'
app-state='self.appState'
)
editor-group-view.flex-grow(application='self.application')
footer-view(

View File

@@ -4,6 +4,5 @@ export { ApplicationView } from './application/application_view';
export { EditorGroupView } from './editor_group/editor_group_view';
export { EditorView } from './editor/editor_view';
export { FooterView } from './footer/footer_view';
export { NotesView } from './notes/notes_view';
export { TagsView } from './tags/tags_view';
export { ChallengeModal } from './challenge_modal/challenge_modal';

View File

@@ -1,115 +0,0 @@
#notes-column.sn-component.section.notes(aria-label='Notes')
.content
#notes-title-bar.section-title-bar
.p-4
.section-title-bar-header
.sk-h2.font-semibold.title {{self.state.panelTitle}}
.sk-button.contrast.wide(
ng-click='self.createNewNote()',
title='Create a new note in the selected tag'
aria-label="Create new note"
)
.sk-label
i.icon.ion-plus.add-button
.filter-section(role='search')
input#search-bar.filter-bar(
type="text"
ng-ref='self.searchBarInput'
ng-focus='self.onSearchInputFocus()'
ng-blur='self.onSearchInputBlur()',
ng-change='self.filterTextChanged()',
ng-keyup='$event.keyCode == 13 && self.onFilterEnter();',
ng-model='self.state.noteFilter.text',
placeholder='Search',
select-on-focus='true',
title='Searches notes in the currently selected tag'
)
#search-clear-button(
ng-click='self.clearFilterText();',
ng-show='self.state.noteFilter.text'
aria-role="button"
) ✕
search-options(
class="ml-2"
app-state='self.appState'
)
no-account-warning(
application='self.application'
app-state='self.appState'
)
#notes-menu-bar.sn-component
.sk-app-bar.no-edges
.left
.sk-app-bar-item(
ng-class="{'selected' : self.state.mutable.showMenu}",
ng-click='self.state.mutable.showMenu = !self.state.mutable.showMenu'
)
.sk-app-bar-item-column
.sk-label
| Options
.sk-app-bar-item-column
.sk-sublabel {{self.optionsSubtitle()}}
notes-list-options-menu(
ng-if='self.state.mutable.showMenu'
app-state='self.appState'
application='self.application'
set-show-menu-false='self.setShowMenuFalse'
)
p.empty-notes-list.faded(
ng-if="self.state.completedFullSync && !self.state.renderedNotes.length"
) No notes.
p.empty-notes-list.faded(
ng-if="!self.state.completedFullSync && !self.state.renderedNotes.length"
) Loading notes…
.scrollable(ng-if="self.state.renderedNotes.length")
#notes-scrollable.infinite-scroll(
can-load='true',
infinite-scroll='self.paginate()',
threshold='200'
)
.note(
ng-attr-id='note-{{note.uuid}}'
ng-repeat='note in self.state.renderedNotes track by note.uuid'
ng-class="{'selected' : self.isNoteSelected(note.uuid) }"
ng-click='self.selectNote(note, true)'
)
.note-flags.flex.flex-wrap(ng-show='self.noteFlags[note.uuid].length > 0')
.flag(ng-class='flag.class', ng-repeat='flag in self.noteFlags[note.uuid]')
.label {{flag.text}}
.name(ng-show='note.title')
| {{note.title}}
.note-preview(
ng-if=`
!self.state.hideNotePreview &&
!note.hidePreview &&
!note.protected`
)
.html-preview(
ng-bind-html='note.preview_html',
ng-show='note.preview_html'
)
.plain-preview(
ng-show='!note.preview_html && note.preview_plain'
) {{note.preview_plain}}
.default-preview(
ng-show='!note.preview_html && !note.preview_plain'
) {{note.text}}
.bottom-info.faded(ng-show='!self.state.hideDate || note.protected')
span(ng-if="note.protected")
| Protected{{self.state.hideDate ? '' : ' • '}}
span(ng-show="!self.state.hideDate && self.state.sortBy == 'userModifiedDate'")
| Modified {{note.updatedAtString || 'Now'}}
span(ng-show="!self.state.hideDate && self.state.sortBy != 'userModifiedDate'")
| {{note.createdAtString || 'Now'}}
.tags-string(ng-if='!self.state.hideTags && self.state.renderedNotesTags[$index]')
.faded {{self.state.renderedNotesTags[$index]}}
panel-resizer(
collapsable="true"
control="self.panelPuppet"
default-width="300"
hoverable="true"
on-resize-finish="self.onPanelResize"
on-width-event="self.onPanelWidthEvent"
panel-id="'notes-column'"
)

View File

@@ -1,955 +0,0 @@
import { PanelPuppet, WebDirective } from './../../types';
import template from './notes-view.pug';
import {
ApplicationEvent,
ContentType,
removeFromArray,
SNNote,
SNTag,
PrefKey,
findInArray,
CollectionSort,
UuidString,
NotesDisplayCriteria
} from '@standardnotes/snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { AppStateEvent } from '@/ui_models/app_state';
import { KeyboardKey, KeyboardModifier } from '@/services/ioService';
import {
PANEL_NAME_NOTES
} from '@/views/constants';
type NotesCtrlState = {
panelTitle: string
notes: SNNote[]
renderedNotes: SNNote[]
renderedNotesTags: string[],
selectedNotes: Record<UuidString, SNNote>,
sortBy?: string
sortReverse?: boolean
showArchived?: boolean
hidePinned?: boolean
hideNotePreview?: boolean
hideDate?: boolean
hideTags: boolean
noteFilter: {
text: string;
}
searchOptions: {
includeProtectedContents: boolean;
includeArchived: boolean;
includeTrashed: boolean;
}
mutable: { showMenu: boolean }
completedFullSync: boolean
[PrefKey.TagsPanelWidth]?: number
[PrefKey.NotesPanelWidth]?: number
[PrefKey.EditorWidth]?: number
[PrefKey.EditorLeft]?: number
[PrefKey.EditorMonospaceEnabled]?: boolean
[PrefKey.EditorSpellcheck]?: boolean
[PrefKey.EditorResizersEnabled]?: boolean
[PrefKey.NotesShowTrashed]?: boolean
[PrefKey.NotesHideProtected]?: boolean
}
type NoteFlag = {
text: string
class: 'info' | 'neutral' | 'warning' | 'success' | 'danger'
}
/**
* This is the height of a note cell with nothing but the title,
* which *is* a display option
*/
const MIN_NOTE_CELL_HEIGHT = 51.0;
const DEFAULT_LIST_NUM_NOTES = 20;
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
private panelPuppet?: PanelPuppet
private reloadNotesPromise?: any
private notesToDisplay = 0
private pageSize = 0
private searchSubmitted = false
private newNoteKeyObserver: any
private nextNoteKeyObserver: any
private previousNoteKeyObserver: any
private searchKeyObserver: any
private noteFlags: Partial<Record<UuidString, NoteFlag[]>> = {}
private removeObservers: Array<() => void> = [];
private rightClickListeners: Map<UuidString, (e: MouseEvent) => void> = new Map();
/* @ngInject */
constructor($timeout: ng.ITimeoutService,) {
super($timeout);
this.resetPagination();
}
$onInit() {
super.$onInit();
this.panelPuppet = {
onReady: () => this.reloadPanelWidth()
};
this.onWindowResize = this.onWindowResize.bind(this);
this.onPanelResize = this.onPanelResize.bind(this);
this.onPanelWidthEvent = this.onPanelWidthEvent.bind(this);
this.setShowMenuFalse = this.setShowMenuFalse.bind(this);
window.addEventListener('resize', this.onWindowResize, true);
this.registerKeyboardShortcuts();
this.autorun(async () => {
const {
includeProtectedContents,
includeArchived,
includeTrashed,
} = this.appState.searchOptions;
await this.setState({
searchOptions: {
includeProtectedContents,
includeArchived,
includeTrashed,
}
});
if (this.state.noteFilter.text) {
this.reloadNotesDisplayOptions();
this.reloadNotes();
}
});
this.autorun(() => {
this.setState({
selectedNotes: this.appState.notes.selectedNotes,
});
});
}
onWindowResize() {
this.resetPagination(true);
}
deinit() {
for (const remove of this.removeObservers) remove();
this.removeObservers.length = 0;
this.removeRightClickListeners();
this.panelPuppet!.onReady = undefined;
this.panelPuppet = undefined;
window.removeEventListener('resize', this.onWindowResize, true);
(this.onWindowResize as any) = undefined;
(this.onPanelResize as any) = undefined;
(this.onPanelWidthEvent as any) = undefined;
this.newNoteKeyObserver();
this.nextNoteKeyObserver();
this.previousNoteKeyObserver();
this.searchKeyObserver();
this.newNoteKeyObserver = undefined;
this.nextNoteKeyObserver = undefined;
this.previousNoteKeyObserver = undefined;
this.searchKeyObserver = undefined;
super.deinit();
}
async setNotesState(state: Partial<NotesCtrlState>) {
return this.setState(state);
}
getInitialState(): NotesCtrlState {
return {
notes: [],
renderedNotes: [],
renderedNotesTags: [],
selectedNotes: {},
mutable: { showMenu: false },
noteFilter: {
text: '',
},
searchOptions: {
includeArchived: false,
includeProtectedContents: false,
includeTrashed: false,
},
panelTitle: '',
completedFullSync: false,
hideTags: true
};
}
async onAppLaunch() {
super.onAppLaunch();
this.streamNotesAndTags();
this.reloadPreferences();
}
/** @override */
onAppStateEvent(eventName: AppStateEvent, data?: any) {
if (eventName === AppStateEvent.TagChanged) {
this.handleTagChange(this.selectedTag!);
} else if (eventName === AppStateEvent.ActiveEditorChanged) {
this.handleEditorChange();
} else if (eventName === AppStateEvent.EditorFocused) {
this.setShowMenuFalse();
}
}
private get activeEditorNote() {
return this.appState.notes.activeEditor?.note;
}
/** @template */
public isNoteSelected(uuid: UuidString) {
return !!this.state.selectedNotes[uuid];
}
public get editorNotes() {
return this.appState.getEditors().map((editor) => editor.note);
}
/** @override */
async onAppEvent(eventName: ApplicationEvent) {
switch (eventName) {
case ApplicationEvent.PreferencesChanged:
this.reloadPreferences();
break;
case ApplicationEvent.SignedIn:
this.appState.closeAllEditors();
this.selectFirstNote();
this.setState({
completedFullSync: false,
});
break;
case ApplicationEvent.CompletedFullSync:
this.getMostValidNotes().then((notes) => {
if (notes.length === 0 && this.selectedTag?.isAllTag && this.state.noteFilter.text === '') {
this.createPlaceholderNote();
}
});
this.setState({
completedFullSync: true,
});
break;
}
}
/**
* Access the current state notes without awaiting any potential reloads
* that may be in progress. This is the sync alternative to `async getMostValidNotes`
*/
private getPossiblyStaleNotes() {
return this.state.notes;
}
/**
* Access the current state notes after waiting for any pending reloads.
* This returns the most up to date notes, but is the asyncronous counterpart
* to `getPossiblyStaleNotes`
*/
private async getMostValidNotes() {
await this.reloadNotesPromise;
return this.getPossiblyStaleNotes();
}
/**
* Triggered programatically to create a new placeholder note
* when conditions allow for it. This is as opposed to creating a new note
* as part of user interaction (pressing the + button).
*/
private async createPlaceholderNote() {
const selectedTag = this.selectedTag!;
if (selectedTag.isSmartTag && !selectedTag.isAllTag) {
return;
}
return this.createNewNote(false);
}
streamNotesAndTags() {
this.removeObservers.push(this.application.streamItems(
[ContentType.Note],
async (items) => {
const notes = items as SNNote[];
/** Note has changed values, reset its flags */
for (const note of notes) {
if (note.deleted) {
continue;
}
this.loadFlagsForNote(note);
}
/** If a note changes, it will be queried against the existing filter;
* we dont need to reload display options */
await this.reloadNotes();
const activeNote = this.activeEditorNote;
if (this.application.getAppState().notes.selectedNotesCount < 2) {
if (activeNote) {
const discarded = activeNote.deleted || activeNote.trashed;
if (
discarded &&
!this.appState?.selectedTag?.isTrashTag &&
!this.appState?.searchOptions.includeTrashed
) {
this.selectNextOrCreateNew();
} else if (!this.state.selectedNotes[activeNote.uuid]) {
this.selectNote(activeNote);
}
} else {
this.selectFirstNote();
}
}
}
));
this.removeObservers.push(this.application.streamItems(
[ContentType.Tag],
async (items) => {
const tags = items as SNTag[];
/** A tag could have changed its relationships, so we need to reload the filter */
this.reloadNotesDisplayOptions();
await this.reloadNotes();
if (findInArray(tags, 'uuid', this.appState.selectedTag?.uuid)) {
/** Tag title could have changed */
this.reloadPanelTitle();
}
}
));
}
private async openNotesContextMenu(e: MouseEvent, note: SNNote) {
e.preventDefault();
if (!this.state.selectedNotes[note.uuid]) {
await this.selectNote(note, true);
}
if (this.state.selectedNotes[note.uuid]) {
this.appState.notes.setContextMenuClickLocation({
x: e.clientX,
y: e.clientY,
});
this.appState.notes.reloadContextMenuLayout();
this.appState.notes.setContextMenuOpen(true);
}
}
private removeRightClickListeners() {
for (const [noteUuid, listener] of this.rightClickListeners.entries()) {
document
.getElementById(`note-${noteUuid}`)
?.removeEventListener('contextmenu', listener);
}
this.rightClickListeners.clear();
}
private addRightClickListeners() {
for (const [noteUuid, listener] of this.rightClickListeners.entries()) {
if (!this.state.renderedNotes.find(note => note.uuid === noteUuid)) {
document
.getElementById(`note-${noteUuid}`)
?.removeEventListener('contextmenu', listener);
this.rightClickListeners.delete(noteUuid);
}
}
for (const note of this.state.renderedNotes) {
if (!this.rightClickListeners.has(note.uuid)) {
const listener = async (e: MouseEvent): Promise<void> => {
return await this.openNotesContextMenu(e, note);
};
document
.getElementById(`note-${note.uuid}`)
?.addEventListener('contextmenu', listener);
this.rightClickListeners.set(note.uuid, listener);
}
}
}
async selectNote(note: SNNote, userTriggered?: boolean): Promise<void> {
await this.appState.notes.selectNote(note.uuid, userTriggered);
}
async createNewNote(focusNewNote = true) {
this.appState.notes.unselectNotes();
let title = `Note ${this.state.notes.length + 1}`;
if (this.isFiltering()) {
title = this.state.noteFilter.text;
}
await this.appState.createEditor(title);
await this.flushUI();
await this.reloadNotes();
await this.appState.noteTags.reloadTags();
const noteTitleEditorElement = document.getElementById('note-title-editor');
if (focusNewNote) {
noteTitleEditorElement?.focus();
}
}
async handleTagChange(tag: SNTag) {
this.resetScrollPosition();
this.setShowMenuFalse();
await this.setNoteFilterText('');
this.application.getDesktopService().searchText();
this.resetPagination();
/* Capture db load state before beginning reloadNotes,
since this status may change during reload */
const dbLoaded = this.application.isDatabaseLoaded();
this.reloadNotesDisplayOptions();
await this.reloadNotes();
if (this.state.notes.length > 0) {
this.selectFirstNote();
} else if (dbLoaded) {
if (
this.activeEditorNote &&
!this.state.notes.includes(this.activeEditorNote!)
) {
this.appState.closeActiveEditor();
}
}
}
resetScrollPosition() {
const scrollable = document.getElementById(ELEMENT_ID_SCROLL_CONTAINER);
if (scrollable) {
scrollable.scrollTop = 0;
scrollable.scrollLeft = 0;
}
}
async removeNoteFromList(note: SNNote) {
const notes = this.state.notes;
removeFromArray(notes, note);
await this.setNotesState({
notes: notes,
renderedNotes: notes.slice(0, this.notesToDisplay)
});
}
private async reloadNotes() {
this.reloadNotesPromise = this.performReloadNotes();
return this.reloadNotesPromise;
}
/**
* Note that reloading display options destroys the current index and rebuilds it,
* so call sparingly. The runtime complexity of destroying and building
* an index is roughly O(n^2).
*/
private reloadNotesDisplayOptions() {
const tag = this.appState.selectedTag;
const searchText = this.state.noteFilter.text.toLowerCase();
const isSearching = searchText.length;
let includeArchived: boolean;
let includeTrashed: boolean;
if (isSearching) {
includeArchived = this.state.searchOptions.includeArchived;
includeTrashed = this.state.searchOptions.includeTrashed;
} else {
includeArchived = this.state.showArchived ?? false;
includeTrashed = this.state.showTrashed ?? false;
}
const criteria = NotesDisplayCriteria.Create({
sortProperty: this.state.sortBy as CollectionSort,
sortDirection: this.state.sortReverse ? 'asc' : 'dsc',
tags: tag ? [tag] : [],
includeArchived,
includeTrashed,
includePinned: !this.state.hidePinned,
includeProtected: !this.state.hideProtected,
searchQuery: {
query: searchText,
includeProtectedNoteText: this.state.searchOptions.includeProtectedContents
}
});
this.application.setNotesDisplayCriteria(criteria);
}
private get selectedTag() {
return this.application.getAppState().getSelectedTag();
}
private async performReloadNotes() {
const tag = this.appState.selectedTag!;
if (!tag) {
return;
}
const notes = this.application.getDisplayableItems(
ContentType.Note
) as SNNote[];
const renderedNotes = notes.slice(0, this.notesToDisplay);
const renderedNotesTags = this.notesTagsList(renderedNotes);
await this.setNotesState({
notes,
renderedNotesTags,
renderedNotes,
});
this.reloadPanelTitle();
this.addRightClickListeners();
}
private notesTagsList(notes: SNNote[]): string[] {
if (this.state.hideTags) {
return [];
} else {
const selectedTag = this.appState.selectedTag;
if (!selectedTag) {
return [];
} else if (selectedTag?.isSmartTag) {
return notes.map((note) =>
this.appState
.getNoteTags(note)
.map((tag) => '#' + tag.title)
.join(' ')
);
} else {
/**
* Displaying a normal tag, hide the note's tag when there's only one
*/
return notes.map((note) => {
const tags = this.appState.getNoteTags(note);
if (tags.length === 1) return '';
return tags.map((tag) => '#' + tag.title).join(' ');
});
}
}
}
setShowMenuFalse() {
this.setNotesState({
mutable: {
...this.state.mutable,
showMenu: false
}
});
}
async handleEditorChange() {
const activeNote = this.appState.getActiveEditor()?.note;
if (activeNote && activeNote.conflictOf) {
this.application.changeAndSaveItem(activeNote.uuid, (mutator) => {
mutator.conflictOf = undefined;
});
}
if (this.isFiltering()) {
this.application.getDesktopService().searchText(this.state.noteFilter.text);
}
}
async reloadPreferences() {
const viewOptions = {} as NotesCtrlState;
const prevSortValue = this.state.sortBy;
let sortBy = this.application.getPreference(
PrefKey.SortNotesBy,
CollectionSort.CreatedAt
);
if (
sortBy === CollectionSort.UpdatedAt ||
(sortBy as string) === "client_updated_at"
) {
/** Use UserUpdatedAt instead */
sortBy = CollectionSort.UpdatedAt;
}
viewOptions.sortBy = sortBy;
viewOptions.sortReverse = this.application.getPreference(
PrefKey.SortNotesReverse,
false
);
viewOptions.showArchived = this.application.getPreference(
PrefKey.NotesShowArchived,
false
);
viewOptions.showTrashed = this.application.getPreference(
PrefKey.NotesShowTrashed,
false
) as boolean;
viewOptions.hidePinned = this.application.getPreference(
PrefKey.NotesHidePinned,
false
);
viewOptions.hideProtected = this.application.getPreference(
PrefKey.NotesHideProtected,
false
);
viewOptions.hideNotePreview = this.application.getPreference(
PrefKey.NotesHideNotePreview,
false
);
viewOptions.hideDate = this.application.getPreference(
PrefKey.NotesHideDate,
false
);
viewOptions.hideTags = this.application.getPreference(
PrefKey.NotesHideTags,
true,
);
const state = this.state;
const displayOptionsChanged = (
viewOptions.sortBy !== state.sortBy ||
viewOptions.sortReverse !== state.sortReverse ||
viewOptions.hidePinned !== state.hidePinned ||
viewOptions.showArchived !== state.showArchived ||
viewOptions.showTrashed !== state.showTrashed ||
viewOptions.hideProtected !== state.hideProtected ||
viewOptions.hideTags !== state.hideTags
);
await this.setNotesState({
...viewOptions
});
this.reloadPanelWidth();
if (displayOptionsChanged) {
this.reloadNotesDisplayOptions();
}
await this.reloadNotes();
if (prevSortValue && prevSortValue !== sortBy) {
this.selectFirstNote();
}
}
reloadPanelWidth() {
const width = this.application.getPreference(
PrefKey.NotesPanelWidth
);
if (width && this.panelPuppet!.ready) {
this.panelPuppet!.setWidth!(width);
if (this.panelPuppet!.isCollapsed!()) {
this.application.getAppState().panelDidResize(
PANEL_NAME_NOTES,
this.panelPuppet!.isCollapsed!()
);
}
}
}
onPanelResize(
newWidth: number,
newLeft: number,
__: boolean,
isCollapsed: boolean
) {
this.appState.noteTags.reloadTagsContainerMaxWidth();
this.application.setPreference(
PrefKey.NotesPanelWidth,
newWidth
);
this.application.getAppState().panelDidResize(
PANEL_NAME_NOTES,
isCollapsed
);
}
onPanelWidthEvent(): void {
this.appState.noteTags.reloadTagsContainerMaxWidth();
}
paginate() {
this.notesToDisplay += this.pageSize;
this.reloadNotes();
if (this.searchSubmitted) {
this.application.getDesktopService().searchText(this.state.noteFilter.text);
}
}
resetPagination(keepCurrentIfLarger = false) {
const clientHeight = document.documentElement.clientHeight;
this.pageSize = Math.ceil(clientHeight / MIN_NOTE_CELL_HEIGHT);
if (this.pageSize === 0) {
this.pageSize = DEFAULT_LIST_NUM_NOTES;
}
if (keepCurrentIfLarger && this.notesToDisplay > this.pageSize) {
return;
}
this.notesToDisplay = this.pageSize;
}
reloadPanelTitle() {
let title;
if (this.isFiltering()) {
const resultCount = this.state.notes.length;
title = `${resultCount} search results`;
} else if (this.appState.selectedTag) {
title = `${this.appState.selectedTag.title}`;
}
this.setNotesState({
panelTitle: title
});
}
optionsSubtitle() {
let base = "";
if (this.state.sortBy === CollectionSort.CreatedAt) {
base += " Date Added";
} else if (this.state.sortBy === CollectionSort.UpdatedAt) {
base += " Date Modified";
} else if (this.state.sortBy === CollectionSort.Title) {
base += " Title";
}
if (this.state.showArchived) {
base += " | + Archived";
}
if (this.state.showTrashed) {
base += " | + Trashed";
}
if (this.state.hidePinned) {
base += " | Pinned";
}
if (this.state.hideProtected) {
base += " | Protected";
}
if (this.state.sortReverse) {
base += " | Reversed";
}
return base;
}
loadFlagsForNote(note: SNNote) {
const flags = [] as NoteFlag[];
if (note.pinned) {
flags.push({
text: "Pinned",
class: 'info'
});
}
if (note.archived) {
flags.push({
text: "Archived",
class: 'warning'
});
}
if (note.locked) {
flags.push({
text: "Editing Disabled",
class: 'neutral'
});
}
if (note.trashed) {
flags.push({
text: "Deleted",
class: 'danger'
});
}
if (note.conflictOf) {
flags.push({
text: "Conflicted Copy",
class: 'danger'
});
}
if (note.errorDecrypting) {
if (note.waitingForKey) {
flags.push({
text: "Waiting For Keys",
class: 'info'
});
} else {
flags.push({
text: "Missing Keys",
class: 'danger'
});
}
}
if (note.deleted) {
flags.push({
text: "Deletion Pending Sync",
class: 'danger'
});
}
this.noteFlags[note.uuid] = flags;
}
getFirstNonProtectedNote() {
return this.state.notes.find(note => !note.protected);
}
selectFirstNote() {
const note = this.getFirstNonProtectedNote();
if (note) {
this.selectNote(note);
}
}
selectNextNote() {
const displayableNotes = this.state.notes;
const currentIndex = displayableNotes.findIndex((candidate) => {
return candidate.uuid === this.activeEditorNote?.uuid;
});
if (currentIndex + 1 < displayableNotes.length) {
const nextNote = displayableNotes[currentIndex + 1];
this.selectNote(nextNote);
const nextNoteElement = document.getElementById(`note-${nextNote.uuid}`);
nextNoteElement?.focus();
}
}
selectNextOrCreateNew() {
const note = this.getFirstNonProtectedNote();
if (note) {
this.selectNote(note);
} else {
this.appState.closeActiveEditor();
}
}
selectPreviousNote() {
const displayableNotes = this.state.notes;
const currentIndex = displayableNotes.indexOf(this.activeEditorNote!);
if (currentIndex - 1 >= 0) {
const previousNote = displayableNotes[currentIndex - 1];
this.selectNote(previousNote);
const previousNoteElement = document.getElementById(`note-${previousNote.uuid}`);
previousNoteElement?.focus();
return true;
} else {
return false;
}
}
isFiltering() {
return this.state.noteFilter.text &&
this.state.noteFilter.text.length > 0;
}
async setNoteFilterText(text: string) {
await this.setNotesState({
noteFilter: {
...this.state.noteFilter,
text: text
}
});
}
async clearFilterText() {
await this.setNoteFilterText('');
this.onFilterEnter();
this.filterTextChanged();
this.resetPagination();
}
async filterTextChanged() {
if (this.searchSubmitted) {
this.searchSubmitted = false;
}
this.reloadNotesDisplayOptions();
await this.reloadNotes();
}
async onSearchInputBlur() {
this.appState.searchOptions.refreshIncludeProtectedContents();
}
onFilterEnter() {
/**
* For Desktop, performing a search right away causes
* input to lose focus. We wait until user explicity hits
* enter before highlighting desktop search results.
*/
this.searchSubmitted = true;
this.application.getDesktopService().searchText(this.state.noteFilter.text);
}
selectedMenuItem() {
this.setShowMenuFalse();
}
togglePrefKey(key: PrefKey) {
this.application.setPreference(
key,
!this.state[key]
);
}
selectedSortByCreated() {
this.setSortBy(CollectionSort.CreatedAt);
}
selectedSortByUpdated() {
this.setSortBy(CollectionSort.UpdatedAt);
}
selectedSortByTitle() {
this.setSortBy(CollectionSort.Title);
}
toggleReverseSort() {
this.selectedMenuItem();
this.application.setPreference(
PrefKey.SortNotesReverse,
!this.state.sortReverse
);
}
setSortBy(type: CollectionSort) {
this.application.setPreference(
PrefKey.SortNotesBy,
type
);
}
getSearchBar() {
return document.getElementById(ELEMENT_ID_SEARCH_BAR)!;
}
registerKeyboardShortcuts() {
/**
* In the browser we're not allowed to override cmd/ctrl + n, so we have to
* use Control modifier as well. These rules don't apply to desktop, but
* probably better to be consistent.
*/
this.newNoteKeyObserver = this.application.io.addKeyObserver({
key: 'n',
modifiers: [
KeyboardModifier.Meta,
KeyboardModifier.Ctrl
],
onKeyDown: (event) => {
event.preventDefault();
this.createNewNote();
}
});
this.nextNoteKeyObserver = this.application.io.addKeyObserver({
key: KeyboardKey.Down,
elements: [
document.body,
this.getSearchBar()
],
onKeyDown: () => {
const searchBar = this.getSearchBar();
if (searchBar === document.activeElement) {
searchBar.blur();
}
this.selectNextNote();
}
});
this.previousNoteKeyObserver = this.application.io.addKeyObserver({
key: KeyboardKey.Up,
element: document.body,
onKeyDown: () => {
this.selectPreviousNote();
}
});
this.searchKeyObserver = this.application.io.addKeyObserver({
key: "f",
modifiers: [
KeyboardModifier.Meta,
KeyboardModifier.Shift
],
onKeyDown: () => {
const searchBar = this.getSearchBar();
if (searchBar) { searchBar.focus(); }
}
});
}
}
export class NotesView extends WebDirective {
constructor() {
super();
this.template = template;
this.replace = true;
this.controller = NotesViewCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
application: '='
};
}
}