refactor: Move notes_view to React (#761)
This commit is contained in:
@@ -26,7 +26,6 @@ import {
|
|||||||
EditorGroupView,
|
EditorGroupView,
|
||||||
EditorView,
|
EditorView,
|
||||||
TagsView,
|
TagsView,
|
||||||
NotesView,
|
|
||||||
FooterView,
|
FooterView,
|
||||||
ChallengeModal,
|
ChallengeModal,
|
||||||
} from '@/views';
|
} from '@/views';
|
||||||
@@ -81,6 +80,7 @@ import { PurchaseFlowDirective } from './purchaseFlow';
|
|||||||
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu';
|
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu';
|
||||||
import { ComponentViewDirective } from '@/components/ComponentView';
|
import { ComponentViewDirective } from '@/components/ComponentView';
|
||||||
import { TagsListDirective } from '@/components/TagsList';
|
import { TagsListDirective } from '@/components/TagsList';
|
||||||
|
import { NotesViewDirective } from './components/NotesView';
|
||||||
import { PinNoteButtonDirective } from '@/components/PinNoteButton';
|
import { PinNoteButtonDirective } from '@/components/PinNoteButton';
|
||||||
|
|
||||||
function reloadHiddenFirefoxTab(): boolean {
|
function reloadHiddenFirefoxTab(): boolean {
|
||||||
@@ -137,7 +137,6 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
.directive('editorGroupView', () => new EditorGroupView())
|
.directive('editorGroupView', () => new EditorGroupView())
|
||||||
.directive('editorView', () => new EditorView())
|
.directive('editorView', () => new EditorView())
|
||||||
.directive('tagsView', () => new TagsView())
|
.directive('tagsView', () => new TagsView())
|
||||||
.directive('notesView', () => new NotesView())
|
|
||||||
.directive('footerView', () => new FooterView());
|
.directive('footerView', () => new FooterView());
|
||||||
|
|
||||||
// Directives - Functional
|
// Directives - Functional
|
||||||
@@ -186,6 +185,7 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
.directive('tags', TagsListDirective)
|
.directive('tags', TagsListDirective)
|
||||||
.directive('preferences', PreferencesDirective)
|
.directive('preferences', PreferencesDirective)
|
||||||
.directive('purchaseFlow', PurchaseFlowDirective)
|
.directive('purchaseFlow', PurchaseFlowDirective)
|
||||||
|
.directive('notesView', NotesViewDirective)
|
||||||
.directive('pinNoteButton', PinNoteButtonDirective);
|
.directive('pinNoteButton', PinNoteButtonDirective);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
|
|||||||
|
|
||||||
type Props = { appState: AppState };
|
type Props = { appState: AppState };
|
||||||
|
|
||||||
const NoAccountWarning = observer(({ appState }: Props) => {
|
export const NoAccountWarning = observer(({ appState }: Props) => {
|
||||||
const canShow = appState.noAccountWarning.show;
|
const canShow = appState.noAccountWarning.show;
|
||||||
if (!canShow) {
|
if (!canShow) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
107
app/assets/javascripts/components/NotesList.tsx
Normal file
107
app/assets/javascripts/components/NotesList.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { KeyboardKey } from '@/services/ioService';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { DisplayOptions } from '@/ui_models/app_state/notes_view_state';
|
||||||
|
import { SNNote } from '@standardnotes/snjs';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { NotesListItem } from './NotesListItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appState: AppState;
|
||||||
|
notes: SNNote[];
|
||||||
|
selectedNotes: Record<string, SNNote>;
|
||||||
|
displayOptions: DisplayOptions;
|
||||||
|
paginate: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FOCUSABLE_BUT_NOT_TABBABLE = -1;
|
||||||
|
const NOTES_LIST_SCROLL_THRESHOLD = 200;
|
||||||
|
|
||||||
|
export const NotesList: FunctionComponent<Props> = observer(
|
||||||
|
({ appState, notes, selectedNotes, displayOptions, paginate }) => {
|
||||||
|
const { selectPreviousNote, selectNextNote } = appState.notesView;
|
||||||
|
const { hideTags, hideDate, hideNotePreview, sortBy } = displayOptions;
|
||||||
|
|
||||||
|
const tagsStringForNote = (note: SNNote): string => {
|
||||||
|
if (hideTags) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const selectedTag = appState.selectedTag;
|
||||||
|
if (!selectedTag) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const tags = appState.getNoteTags(note);
|
||||||
|
if (!selectedTag.isSmartTag && tags.length === 1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return tags.map((tag) => `#${tag.title}`).join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||||
|
appState.notes.setContextMenuClickLocation({
|
||||||
|
x: posX,
|
||||||
|
y: posY,
|
||||||
|
});
|
||||||
|
appState.notes.reloadContextMenuLayout();
|
||||||
|
appState.notes.setContextMenuOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContextMenu = async (note: SNNote, posX: number, posY: number) => {
|
||||||
|
await appState.notes.selectNote(note.uuid, true);
|
||||||
|
if (selectedNotes[note.uuid]) {
|
||||||
|
openNoteContextMenu(posX, posY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScroll = (e: Event) => {
|
||||||
|
const offset = NOTES_LIST_SCROLL_THRESHOLD;
|
||||||
|
const element = e.target as HTMLElement;
|
||||||
|
if (
|
||||||
|
element.scrollTop + element.offsetHeight >=
|
||||||
|
element.scrollHeight - offset
|
||||||
|
) {
|
||||||
|
paginate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === KeyboardKey.Up) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectPreviousNote();
|
||||||
|
} else if (e.key === KeyboardKey.Down) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectNextNote();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="infinite-scroll"
|
||||||
|
id="notes-scrollable"
|
||||||
|
onScroll={onScroll}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
|
>
|
||||||
|
{notes.map((note) => (
|
||||||
|
<NotesListItem
|
||||||
|
key={note.uuid}
|
||||||
|
note={note}
|
||||||
|
tags={tagsStringForNote(note)}
|
||||||
|
selected={!!selectedNotes[note.uuid]}
|
||||||
|
hideDate={hideDate}
|
||||||
|
hidePreview={hideNotePreview}
|
||||||
|
hideTags={hideTags}
|
||||||
|
sortedBy={sortBy}
|
||||||
|
onClick={() => {
|
||||||
|
appState.notes.selectNote(note.uuid, true);
|
||||||
|
}}
|
||||||
|
onContextMenu={(e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onContextMenu(note, e.clientX, e.clientY);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
142
app/assets/javascripts/components/NotesListItem.tsx
Normal file
142
app/assets/javascripts/components/NotesListItem.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { CollectionSort, SNNote } from '@standardnotes/snjs';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
note: SNNote;
|
||||||
|
tags: string;
|
||||||
|
hideDate: boolean;
|
||||||
|
hidePreview: boolean;
|
||||||
|
hideTags: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
onContextMenu: (e: MouseEvent) => void;
|
||||||
|
selected: boolean;
|
||||||
|
sortedBy?: CollectionSort;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NoteFlag = {
|
||||||
|
text: string;
|
||||||
|
class: 'info' | 'neutral' | 'warning' | 'success' | 'danger';
|
||||||
|
};
|
||||||
|
|
||||||
|
const flagsForNote = (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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotesListItem: FunctionComponent<Props> = ({
|
||||||
|
hideDate,
|
||||||
|
hidePreview,
|
||||||
|
hideTags,
|
||||||
|
note,
|
||||||
|
onClick,
|
||||||
|
onContextMenu,
|
||||||
|
selected,
|
||||||
|
sortedBy,
|
||||||
|
tags,
|
||||||
|
}) => {
|
||||||
|
const flags = flagsForNote(note);
|
||||||
|
const showModifiedDate = sortedBy === CollectionSort.UpdatedAt;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`note ${selected ? 'selected' : ''}`}
|
||||||
|
id={`note-${note.uuid}`}
|
||||||
|
onClick={onClick}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
|
>
|
||||||
|
{flags && flags.length > 0 ? (
|
||||||
|
<div className="note-flags flex flex-wrap">
|
||||||
|
{flags.map((flag) => (
|
||||||
|
<div className={`flag ${flag.class}`}>
|
||||||
|
<div className="label">{flag.text}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="name">{note.title}</div>
|
||||||
|
{!hidePreview && !note.hidePreview && !note.protected ? (
|
||||||
|
<div className="note-preview">
|
||||||
|
{note.preview_html ? (
|
||||||
|
<div
|
||||||
|
className="html-preview"
|
||||||
|
dangerouslySetInnerHTML={{ __html: note.preview_html }}
|
||||||
|
></div>
|
||||||
|
) : null}
|
||||||
|
{!note.preview_html && note.preview_plain ? (
|
||||||
|
<div className="plain-preview">{note.preview_plain}</div>
|
||||||
|
) : null}
|
||||||
|
{!note.preview_html && !note.preview_plain ? (
|
||||||
|
<div className="default-preview">{note.text}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!hideDate || note.protected ? (
|
||||||
|
<div className="bottom-info faded">
|
||||||
|
{note.protected ? (
|
||||||
|
<span>Protected {hideDate ? '' : ' • '}</span>
|
||||||
|
) : null}
|
||||||
|
{!hideDate && showModifiedDate ? (
|
||||||
|
<span>Modified {note.updatedAtString || 'Now'}</span>
|
||||||
|
) : null}
|
||||||
|
{!hideDate && !showModifiedDate ? (
|
||||||
|
<span>{note.createdAtString || 'Now'}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!hideTags && (
|
||||||
|
<div className="tags-string">
|
||||||
|
<div className="faded">{tags}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,11 +10,11 @@ import { toDirective, useCloseOnClickOutside } from './utils';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
setShowMenuFalse: () => void;
|
closeDisplayOptionsMenu: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NotesListOptionsMenu: FunctionComponent<Props> = observer(
|
export const NotesListOptionsMenu: FunctionComponent<Props> = observer(
|
||||||
({ setShowMenuFalse, application }) => {
|
({ closeDisplayOptionsMenu, application }) => {
|
||||||
const menuClassName =
|
const menuClassName =
|
||||||
'sn-dropdown sn-dropdown--animated min-w-70 overflow-y-auto \
|
'sn-dropdown sn-dropdown--animated min-w-70 overflow-y-auto \
|
||||||
border-1 border-solid border-main text-sm z-index-dropdown-menu \
|
border-1 border-solid border-main text-sm z-index-dropdown-menu \
|
||||||
@@ -112,13 +112,13 @@ flex flex-col py-2 bottom-0 left-2 absolute';
|
|||||||
|
|
||||||
useCloseOnClickOutside(menuRef as any, (open: boolean) => {
|
useCloseOnClickOutside(menuRef as any, (open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setShowMenuFalse();
|
closeDisplayOptionsMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menuRef} className={menuClassName}>
|
<div ref={menuRef} className={menuClassName}>
|
||||||
<Menu a11yLabel="Sort by" closeMenu={setShowMenuFalse}>
|
<Menu a11yLabel="Sort by" closeMenu={closeDisplayOptionsMenu}>
|
||||||
<div className="px-3 my-1 text-xs font-semibold color-text uppercase">
|
<div className="px-3 my-1 text-xs font-semibold color-text uppercase">
|
||||||
Sort by
|
Sort by
|
||||||
</div>
|
</div>
|
||||||
@@ -246,7 +246,7 @@ flex flex-col py-2 bottom-0 left-2 absolute';
|
|||||||
export const NotesListOptionsDirective = toDirective<Props>(
|
export const NotesListOptionsDirective = toDirective<Props>(
|
||||||
NotesListOptionsMenu,
|
NotesListOptionsMenu,
|
||||||
{
|
{
|
||||||
setShowMenuFalse: '=',
|
closeDisplayOptionsMenu: '=',
|
||||||
state: '&',
|
state: '&',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
256
app/assets/javascripts/components/NotesView.tsx
Normal file
256
app/assets/javascripts/components/NotesView.tsx
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import {
|
||||||
|
PanelSide,
|
||||||
|
ResizeFinishCallback,
|
||||||
|
} from '@/directives/views/panelResizer';
|
||||||
|
import { KeyboardKey, KeyboardModifier } from '@/services/ioService';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { PANEL_NAME_NOTES } from '@/views/constants';
|
||||||
|
import { PrefKey } from '@standardnotes/snjs';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
|
import { NoAccountWarning } from './NoAccountWarning';
|
||||||
|
import { NotesList } from './NotesList';
|
||||||
|
import { NotesListOptionsMenu } from './NotesListOptionsMenu';
|
||||||
|
import { PanelResizer } from './PanelResizer';
|
||||||
|
import { SearchOptions } from './SearchOptions';
|
||||||
|
import { toDirective } from './utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
appState: AppState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotesView: FunctionComponent<Props> = observer(
|
||||||
|
({ application, appState }) => {
|
||||||
|
const notesViewPanelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
completedFullSync,
|
||||||
|
createNewNote,
|
||||||
|
displayOptions,
|
||||||
|
noteFilterText,
|
||||||
|
optionsSubtitle,
|
||||||
|
panelTitle,
|
||||||
|
renderedNotes,
|
||||||
|
selectedNotes,
|
||||||
|
setNoteFilterText,
|
||||||
|
showDisplayOptionsMenu,
|
||||||
|
toggleDisplayOptionsMenu,
|
||||||
|
searchBarElement,
|
||||||
|
selectNextNote,
|
||||||
|
selectPreviousNote,
|
||||||
|
onFilterEnter,
|
||||||
|
handleFilterTextChanged,
|
||||||
|
onSearchInputBlur,
|
||||||
|
clearFilterText,
|
||||||
|
paginate,
|
||||||
|
} = appState.notesView;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleFilterTextChanged();
|
||||||
|
}, [noteFilterText, handleFilterTextChanged]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
const newNoteKeyObserver = application.io.addKeyObserver({
|
||||||
|
key: 'n',
|
||||||
|
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl],
|
||||||
|
onKeyDown: (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
createNewNote();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextNoteKeyObserver = application.io.addKeyObserver({
|
||||||
|
key: KeyboardKey.Down,
|
||||||
|
elements: [
|
||||||
|
document.body,
|
||||||
|
...(searchBarElement ? [searchBarElement] : []),
|
||||||
|
],
|
||||||
|
onKeyDown: () => {
|
||||||
|
if (searchBarElement === document.activeElement) {
|
||||||
|
searchBarElement?.blur();
|
||||||
|
}
|
||||||
|
selectNextNote();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const previousNoteKeyObserver = application.io.addKeyObserver({
|
||||||
|
key: KeyboardKey.Up,
|
||||||
|
element: document.body,
|
||||||
|
onKeyDown: () => {
|
||||||
|
selectPreviousNote();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchKeyObserver = application.io.addKeyObserver({
|
||||||
|
key: 'f',
|
||||||
|
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Shift],
|
||||||
|
onKeyDown: () => {
|
||||||
|
if (searchBarElement) {
|
||||||
|
searchBarElement.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
newNoteKeyObserver();
|
||||||
|
nextNoteKeyObserver();
|
||||||
|
previousNoteKeyObserver();
|
||||||
|
searchKeyObserver();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
application.io,
|
||||||
|
createNewNote,
|
||||||
|
searchBarElement,
|
||||||
|
selectNextNote,
|
||||||
|
selectPreviousNote,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onNoteFilterTextChange = (e: Event) => {
|
||||||
|
setNoteFilterText((e.target as HTMLInputElement).value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNoteFilterKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === KeyboardKey.Enter) {
|
||||||
|
onFilterEnter();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const panelResizeFinishCallback: ResizeFinishCallback = (
|
||||||
|
_w,
|
||||||
|
_l,
|
||||||
|
_mw,
|
||||||
|
isCollapsed
|
||||||
|
) => {
|
||||||
|
appState.noteTags.reloadTagsContainerMaxWidth();
|
||||||
|
appState.panelDidResize(PANEL_NAME_NOTES, isCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const panelWidthEventCallback = () => {
|
||||||
|
appState.noteTags.reloadTagsContainerMaxWidth();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="notes-column"
|
||||||
|
className="sn-component section notes"
|
||||||
|
aria-label="Notes"
|
||||||
|
ref={notesViewPanelRef}
|
||||||
|
>
|
||||||
|
<div className="content">
|
||||||
|
<div id="notes-title-bar" className="section-title-bar">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="section-title-bar-header">
|
||||||
|
<div className="sk-h2 font-semibold title">{panelTitle}</div>
|
||||||
|
<button
|
||||||
|
className="sk-button contrast wide"
|
||||||
|
title="Create a new note in the selected tag"
|
||||||
|
aria-label="Create new note"
|
||||||
|
onClick={() => createNewNote()}
|
||||||
|
>
|
||||||
|
<div className="sk-label">
|
||||||
|
<i className="ion-plus add-button" aria-hidden></i>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="filter-section" role="search">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-bar"
|
||||||
|
className="filter-bar"
|
||||||
|
placeholder="Search"
|
||||||
|
title="Searches notes in the currently selected tag"
|
||||||
|
value={noteFilterText}
|
||||||
|
onChange={onNoteFilterTextChange}
|
||||||
|
onKeyUp={onNoteFilterKeyUp}
|
||||||
|
onBlur={() => onSearchInputBlur()}
|
||||||
|
/>
|
||||||
|
{noteFilterText ? (
|
||||||
|
<button
|
||||||
|
onClick={clearFilterText}
|
||||||
|
aria-role="button"
|
||||||
|
id="search-clear-button"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<div className="ml-2">
|
||||||
|
<SearchOptions
|
||||||
|
application={application}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NoAccountWarning appState={appState} />
|
||||||
|
</div>
|
||||||
|
<div id="notes-menu-bar" className="sn-component">
|
||||||
|
<div className="sk-app-bar no-edges">
|
||||||
|
<div className="left">
|
||||||
|
<div
|
||||||
|
className={`sk-app-bar-item ${
|
||||||
|
showDisplayOptionsMenu ? 'selected' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() =>
|
||||||
|
toggleDisplayOptionsMenu(!showDisplayOptionsMenu)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="sk-app-bar-item-column">
|
||||||
|
<div className="sk-label">Options</div>
|
||||||
|
</div>
|
||||||
|
<div className="sk-app-bar-item-column">
|
||||||
|
<div className="sk-sublabel">{optionsSubtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showDisplayOptionsMenu && (
|
||||||
|
<NotesListOptionsMenu
|
||||||
|
application={application}
|
||||||
|
closeDisplayOptionsMenu={() =>
|
||||||
|
toggleDisplayOptionsMenu(false)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{completedFullSync && !renderedNotes.length ? (
|
||||||
|
<p className="empty-notes-list faded">No notes.</p>
|
||||||
|
) : null}
|
||||||
|
{!completedFullSync && !renderedNotes.length ? (
|
||||||
|
<p className="empty-notes-list faded">Loading notes...</p>
|
||||||
|
) : null}
|
||||||
|
{renderedNotes.length ? (
|
||||||
|
<NotesList
|
||||||
|
notes={renderedNotes}
|
||||||
|
selectedNotes={selectedNotes}
|
||||||
|
appState={appState}
|
||||||
|
displayOptions={displayOptions}
|
||||||
|
paginate={paginate}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{notesViewPanelRef.current && (
|
||||||
|
<PanelResizer
|
||||||
|
application={application}
|
||||||
|
collapsable={true}
|
||||||
|
defaultWidth={300}
|
||||||
|
panel={document.querySelector('notes-view') as HTMLDivElement}
|
||||||
|
prefKey={PrefKey.NotesPanelWidth}
|
||||||
|
side={PanelSide.Right}
|
||||||
|
resizeFinishCallback={panelResizeFinishCallback}
|
||||||
|
widthEventCallback={panelWidthEventCallback}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotesViewDirective = toDirective<Props>(NotesView);
|
||||||
60
app/assets/javascripts/components/PanelResizer.tsx
Normal file
60
app/assets/javascripts/components/PanelResizer.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
PanelResizerProps,
|
||||||
|
PanelResizerState,
|
||||||
|
} from '@/ui_models/panel_resizer';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
export const PanelResizer: FunctionComponent<PanelResizerProps> = observer(
|
||||||
|
({
|
||||||
|
alwaysVisible,
|
||||||
|
application,
|
||||||
|
defaultWidth,
|
||||||
|
hoverable,
|
||||||
|
collapsable,
|
||||||
|
minWidth,
|
||||||
|
panel,
|
||||||
|
prefKey,
|
||||||
|
resizeFinishCallback,
|
||||||
|
side,
|
||||||
|
widthEventCallback,
|
||||||
|
}) => {
|
||||||
|
const [panelResizerState] = useState(
|
||||||
|
() =>
|
||||||
|
new PanelResizerState({
|
||||||
|
alwaysVisible,
|
||||||
|
application,
|
||||||
|
defaultWidth,
|
||||||
|
hoverable,
|
||||||
|
collapsable,
|
||||||
|
minWidth,
|
||||||
|
panel,
|
||||||
|
prefKey,
|
||||||
|
resizeFinishCallback,
|
||||||
|
side,
|
||||||
|
widthEventCallback,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const panelResizerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (panelResizerRef.current) {
|
||||||
|
panelResizerState.setMinWidth(panelResizerRef.current.offsetWidth + 2);
|
||||||
|
}
|
||||||
|
}, [panelResizerState]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`panel-resizer ${panelResizerState.side} ${
|
||||||
|
panelResizerState.hoverable ? 'hoverable' : ''
|
||||||
|
} ${panelResizerState.alwaysVisible ? 'alwaysVisible' : ''} ${
|
||||||
|
panelResizerState.pressed ? 'dragging' : ''
|
||||||
|
} ${panelResizerState.collapsed ? 'collapsed' : ''}`}
|
||||||
|
onMouseDown={panelResizerState.onMouseDown}
|
||||||
|
onDblClick={panelResizerState.onDblClick}
|
||||||
|
ref={panelResizerRef}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -17,7 +17,7 @@ type Props = {
|
|||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchOptions = observer(({ appState }: Props) => {
|
export const SearchOptions = observer(({ appState }: Props) => {
|
||||||
const { searchOptions } = appState;
|
const { searchOptions } = appState;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import angular from 'angular';
|
|||||||
import template from '%/directives/panel-resizer.pug';
|
import template from '%/directives/panel-resizer.pug';
|
||||||
import { debounce } from '@/utils';
|
import { debounce } from '@/utils';
|
||||||
|
|
||||||
enum PanelSide {
|
export enum PanelSide {
|
||||||
Right = 'right',
|
Right = 'right',
|
||||||
Left = 'left'
|
Left = 'left',
|
||||||
}
|
}
|
||||||
enum MouseEventType {
|
enum MouseEventType {
|
||||||
Move = 'mousemove',
|
Move = 'mousemove',
|
||||||
Down = 'mousedown',
|
Down = 'mousedown',
|
||||||
Up = 'mouseup'
|
Up = 'mouseup',
|
||||||
}
|
}
|
||||||
enum CssClass {
|
enum CssClass {
|
||||||
Hoverable = 'hoverable',
|
Hoverable = 'hoverable',
|
||||||
@@ -22,64 +22,63 @@ enum CssClass {
|
|||||||
}
|
}
|
||||||
const WINDOW_EVENT_RESIZE = 'resize';
|
const WINDOW_EVENT_RESIZE = 'resize';
|
||||||
|
|
||||||
type ResizeFinishCallback = (
|
export type ResizeFinishCallback = (
|
||||||
lastWidth: number,
|
lastWidth: number,
|
||||||
lastLeft: number,
|
lastLeft: number,
|
||||||
isMaxWidth: boolean,
|
isMaxWidth: boolean,
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
||||||
) => void
|
) => void;
|
||||||
|
|
||||||
interface PanelResizerScope {
|
interface PanelResizerScope {
|
||||||
alwaysVisible: boolean
|
alwaysVisible: boolean;
|
||||||
collapsable: boolean
|
collapsable: boolean;
|
||||||
control: PanelPuppet
|
control: PanelPuppet;
|
||||||
defaultWidth: number
|
defaultWidth: number;
|
||||||
hoverable: boolean
|
hoverable: boolean;
|
||||||
index: number
|
index: number;
|
||||||
minWidth: number
|
minWidth: number;
|
||||||
onResizeFinish: () => ResizeFinishCallback
|
onResizeFinish: () => ResizeFinishCallback;
|
||||||
onWidthEvent?: () => void
|
onWidthEvent?: () => void;
|
||||||
panelId: string
|
panelId: string;
|
||||||
property: PanelSide
|
property: PanelSide;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PanelResizerCtrl implements PanelResizerScope {
|
class PanelResizerCtrl implements PanelResizerScope {
|
||||||
|
|
||||||
/** @scope */
|
/** @scope */
|
||||||
alwaysVisible!: boolean
|
alwaysVisible!: boolean;
|
||||||
collapsable!: boolean
|
collapsable!: boolean;
|
||||||
control!: PanelPuppet
|
control!: PanelPuppet;
|
||||||
defaultWidth!: number
|
defaultWidth!: number;
|
||||||
hoverable!: boolean
|
hoverable!: boolean;
|
||||||
index!: number
|
index!: number;
|
||||||
minWidth!: number
|
minWidth!: number;
|
||||||
onResizeFinish!: () => ResizeFinishCallback
|
onResizeFinish!: () => ResizeFinishCallback;
|
||||||
onWidthEvent?: () => () => void
|
onWidthEvent?: () => () => void;
|
||||||
panelId!: string
|
panelId!: string;
|
||||||
property!: PanelSide
|
property!: PanelSide;
|
||||||
|
|
||||||
$compile: ng.ICompileService
|
$compile: ng.ICompileService;
|
||||||
$element: JQLite
|
$element: JQLite;
|
||||||
$timeout: ng.ITimeoutService
|
$timeout: ng.ITimeoutService;
|
||||||
panel!: HTMLElement
|
panel!: HTMLElement;
|
||||||
resizerColumn!: HTMLElement
|
resizerColumn!: HTMLElement;
|
||||||
currentMinWidth = 0
|
currentMinWidth = 0;
|
||||||
pressed = false
|
pressed = false;
|
||||||
startWidth = 0
|
startWidth = 0;
|
||||||
lastDownX = 0
|
lastDownX = 0;
|
||||||
collapsed = false
|
collapsed = false;
|
||||||
lastWidth = 0
|
lastWidth = 0;
|
||||||
startLeft = 0
|
startLeft = 0;
|
||||||
lastLeft = 0
|
lastLeft = 0;
|
||||||
appFrame?: DOMRect
|
appFrame?: DOMRect;
|
||||||
widthBeforeLastDblClick = 0
|
widthBeforeLastDblClick = 0;
|
||||||
overlay?: JQLite
|
overlay?: JQLite;
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
$compile: ng.ICompileService,
|
$compile: ng.ICompileService,
|
||||||
$element: JQLite,
|
$element: JQLite,
|
||||||
$timeout: ng.ITimeoutService,
|
$timeout: ng.ITimeoutService
|
||||||
) {
|
) {
|
||||||
this.$compile = $compile;
|
this.$compile = $compile;
|
||||||
this.$element = $element;
|
this.$element = $element;
|
||||||
@@ -109,7 +108,10 @@ class PanelResizerCtrl implements PanelResizerScope {
|
|||||||
window.removeEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
|
window.removeEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
|
||||||
document.removeEventListener(MouseEventType.Move, this.onMouseMove);
|
document.removeEventListener(MouseEventType.Move, this.onMouseMove);
|
||||||
document.removeEventListener(MouseEventType.Up, this.onMouseUp);
|
document.removeEventListener(MouseEventType.Up, this.onMouseUp);
|
||||||
this.resizerColumn.removeEventListener(MouseEventType.Down, this.onMouseDown);
|
this.resizerColumn.removeEventListener(
|
||||||
|
MouseEventType.Down,
|
||||||
|
this.onMouseDown
|
||||||
|
);
|
||||||
(this.handleResize as any) = undefined;
|
(this.handleResize as any) = undefined;
|
||||||
(this.onMouseMove as any) = undefined;
|
(this.onMouseMove as any) = undefined;
|
||||||
(this.onMouseUp as any) = undefined;
|
(this.onMouseUp as any) = undefined;
|
||||||
@@ -140,7 +142,7 @@ class PanelResizerCtrl implements PanelResizerScope {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.resizerColumn = this.$element[0];
|
this.resizerColumn = this.$element[0];
|
||||||
this.currentMinWidth = this.minWidth || (this.resizerColumn.offsetWidth + 2);
|
this.currentMinWidth = this.minWidth || this.resizerColumn.offsetWidth + 2;
|
||||||
this.pressed = false;
|
this.pressed = false;
|
||||||
this.startWidth = this.panel.scrollWidth;
|
this.startWidth = this.panel.scrollWidth;
|
||||||
this.lastDownX = 0;
|
this.lastDownX = 0;
|
||||||
@@ -313,7 +315,8 @@ class PanelResizerCtrl implements PanelResizerScope {
|
|||||||
width = parentRect.width;
|
width = parentRect.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxWidth = this.appFrame!.width - this.panel.getBoundingClientRect().x;
|
const maxWidth =
|
||||||
|
this.appFrame!.width - this.panel.getBoundingClientRect().x;
|
||||||
if (width > maxWidth) {
|
if (width > maxWidth) {
|
||||||
width = maxWidth;
|
width = maxWidth;
|
||||||
}
|
}
|
||||||
@@ -356,7 +359,9 @@ class PanelResizerCtrl implements PanelResizerScope {
|
|||||||
if (this.overlay) {
|
if (this.overlay) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(this as any);
|
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(
|
||||||
|
this as any
|
||||||
|
);
|
||||||
angular.element(document.body).prepend(this.overlay);
|
angular.element(document.body).prepend(this.overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +400,7 @@ export class PanelResizer extends WebDirective {
|
|||||||
onResizeFinish: '&',
|
onResizeFinish: '&',
|
||||||
onWidthEvent: '&',
|
onWidthEvent: '&',
|
||||||
panelId: '=',
|
panelId: '=',
|
||||||
property: '='
|
property: '=',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export enum KeyboardKey {
|
|||||||
Backspace = 'Backspace',
|
Backspace = 'Backspace',
|
||||||
Up = 'ArrowUp',
|
Up = 'ArrowUp',
|
||||||
Down = 'ArrowDown',
|
Down = 'ArrowDown',
|
||||||
|
Enter = 'Enter',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KeyboardModifier {
|
export enum KeyboardModifier {
|
||||||
@@ -51,7 +52,9 @@ export class IOService {
|
|||||||
(this.handleWindowBlur as unknown) = undefined;
|
(this.handleWindowBlur as unknown) = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
private addActiveModifier = (
|
||||||
|
modifier: KeyboardModifier | undefined
|
||||||
|
): void => {
|
||||||
if (!modifier) {
|
if (!modifier) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -73,14 +76,16 @@ export class IOService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
private removeActiveModifier = (
|
||||||
|
modifier: KeyboardModifier | undefined
|
||||||
|
): void => {
|
||||||
if (!modifier) {
|
if (!modifier) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.activeModifiers.delete(modifier);
|
this.activeModifiers.delete(modifier);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleKeyDown = (event: KeyboardEvent): void => {
|
handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
for (const modifier of this.modifiersForEvent(event)) {
|
for (const modifier of this.modifiersForEvent(event)) {
|
||||||
@@ -91,7 +96,7 @@ export class IOService {
|
|||||||
|
|
||||||
handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
|
handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
|
||||||
this.addActiveModifier(modifier);
|
this.addActiveModifier(modifier);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleKeyUp = (event: KeyboardEvent): void => {
|
handleKeyUp = (event: KeyboardEvent): void => {
|
||||||
for (const modifier of this.modifiersForEvent(event)) {
|
for (const modifier of this.modifiersForEvent(event)) {
|
||||||
@@ -102,7 +107,7 @@ export class IOService {
|
|||||||
|
|
||||||
handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
|
handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
|
||||||
this.removeActiveModifier(modifier);
|
this.removeActiveModifier(modifier);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleWindowBlur = (): void => {
|
handleWindowBlur = (): void => {
|
||||||
for (const modifier of this.activeModifiers) {
|
for (const modifier of this.activeModifiers) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from 'mobx';
|
} from 'mobx';
|
||||||
import { ActionsMenuState } from './actions_menu_state';
|
import { ActionsMenuState } from './actions_menu_state';
|
||||||
import { NotesState } from './notes_state';
|
import { NotesState } from './notes_state';
|
||||||
|
import { NotesViewState } from './notes_view_state';
|
||||||
import { NoteTagsState } from './note_tags_state';
|
import { NoteTagsState } from './note_tags_state';
|
||||||
import { NoAccountWarningState } from './no_account_warning_state';
|
import { NoAccountWarningState } from './no_account_warning_state';
|
||||||
import { PreferencesState } from './preferences_state';
|
import { PreferencesState } from './preferences_state';
|
||||||
@@ -86,6 +87,7 @@ export class AppState {
|
|||||||
readonly searchOptions: SearchOptionsState;
|
readonly searchOptions: SearchOptionsState;
|
||||||
readonly notes: NotesState;
|
readonly notes: NotesState;
|
||||||
readonly tags: TagsState;
|
readonly tags: TagsState;
|
||||||
|
readonly notesView: NotesViewState;
|
||||||
isSessionsModalVisible = false;
|
isSessionsModalVisible = false;
|
||||||
|
|
||||||
private appEventObserverRemovers: (() => void)[] = [];
|
private appEventObserverRemovers: (() => void)[] = [];
|
||||||
@@ -127,6 +129,11 @@ export class AppState {
|
|||||||
this.appEventObserverRemovers
|
this.appEventObserverRemovers
|
||||||
);
|
);
|
||||||
this.purchaseFlow = new PurchaseFlowState(application);
|
this.purchaseFlow = new PurchaseFlowState(application);
|
||||||
|
this.notesView = new NotesViewState(
|
||||||
|
application,
|
||||||
|
this,
|
||||||
|
this.appEventObserverRemovers
|
||||||
|
);
|
||||||
this.addAppEventObserver();
|
this.addAppEventObserver();
|
||||||
this.streamNotesAndTags();
|
this.streamNotesAndTags();
|
||||||
this.onVisibilityChange = () => {
|
this.onVisibilityChange = () => {
|
||||||
|
|||||||
541
app/assets/javascripts/ui_models/app_state/notes_view_state.ts
Normal file
541
app/assets/javascripts/ui_models/app_state/notes_view_state.ts
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
import {
|
||||||
|
ApplicationEvent,
|
||||||
|
CollectionSort,
|
||||||
|
ContentType,
|
||||||
|
findInArray,
|
||||||
|
NotesDisplayCriteria,
|
||||||
|
PrefKey,
|
||||||
|
SNNote,
|
||||||
|
SNTag,
|
||||||
|
UuidString,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
|
import {
|
||||||
|
action,
|
||||||
|
autorun,
|
||||||
|
computed,
|
||||||
|
makeObservable,
|
||||||
|
observable,
|
||||||
|
reaction,
|
||||||
|
} from 'mobx';
|
||||||
|
import { AppState, AppStateEvent } from '.';
|
||||||
|
import { WebApplication } from '../application';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
export type DisplayOptions = {
|
||||||
|
sortBy: CollectionSort;
|
||||||
|
sortReverse: boolean;
|
||||||
|
hidePinned: boolean;
|
||||||
|
showArchived: boolean;
|
||||||
|
showTrashed: boolean;
|
||||||
|
hideProtected: boolean;
|
||||||
|
hideTags: boolean;
|
||||||
|
hideNotePreview: boolean;
|
||||||
|
hideDate: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NotesViewState {
|
||||||
|
completedFullSync = false;
|
||||||
|
noteFilterText = '';
|
||||||
|
notes: SNNote[] = [];
|
||||||
|
notesToDisplay = 0;
|
||||||
|
pageSize = 0;
|
||||||
|
panelTitle = 'All Notes';
|
||||||
|
renderedNotes: SNNote[] = [];
|
||||||
|
searchSubmitted = false;
|
||||||
|
selectedNotes: Record<UuidString, SNNote> = {};
|
||||||
|
showDisplayOptionsMenu = false;
|
||||||
|
displayOptions = {
|
||||||
|
sortBy: CollectionSort.CreatedAt,
|
||||||
|
sortReverse: false,
|
||||||
|
hidePinned: false,
|
||||||
|
showArchived: false,
|
||||||
|
showTrashed: false,
|
||||||
|
hideProtected: false,
|
||||||
|
hideTags: true,
|
||||||
|
hideDate: false,
|
||||||
|
hideNotePreview: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private application: WebApplication,
|
||||||
|
private appState: AppState,
|
||||||
|
appObservers: (() => void)[]
|
||||||
|
) {
|
||||||
|
this.resetPagination();
|
||||||
|
|
||||||
|
appObservers.push(
|
||||||
|
application.streamItems(ContentType.Note, () => {
|
||||||
|
this.reloadNotes();
|
||||||
|
const activeNote = this.appState.notes.activeEditor?.note;
|
||||||
|
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.selectedNotes[activeNote.uuid]) {
|
||||||
|
this.selectNote(activeNote);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectFirstNote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
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();
|
||||||
|
this.reloadNotes();
|
||||||
|
if (findInArray(tags, 'uuid', this.appState.selectedTag?.uuid)) {
|
||||||
|
/** Tag title could have changed */
|
||||||
|
this.reloadPanelTitle();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
application.addEventObserver(async () => {
|
||||||
|
this.reloadPreferences();
|
||||||
|
}, ApplicationEvent.PreferencesChanged),
|
||||||
|
application.addEventObserver(async () => {
|
||||||
|
this.appState.closeAllEditors();
|
||||||
|
this.selectFirstNote();
|
||||||
|
this.setCompletedFullSync(false);
|
||||||
|
}, ApplicationEvent.SignedIn),
|
||||||
|
application.addEventObserver(async () => {
|
||||||
|
this.reloadNotes();
|
||||||
|
if (
|
||||||
|
this.notes.length === 0 &&
|
||||||
|
this.appState.selectedTag?.isAllTag &&
|
||||||
|
this.noteFilterText === ''
|
||||||
|
) {
|
||||||
|
this.createPlaceholderNote();
|
||||||
|
}
|
||||||
|
this.setCompletedFullSync(true);
|
||||||
|
}, ApplicationEvent.CompletedFullSync),
|
||||||
|
autorun(() => {
|
||||||
|
if (appState.notes.selectedNotes) {
|
||||||
|
this.syncSelectedNotes();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
reaction(
|
||||||
|
() => [
|
||||||
|
appState.searchOptions.includeProtectedContents,
|
||||||
|
appState.searchOptions.includeArchived,
|
||||||
|
appState.searchOptions.includeTrashed,
|
||||||
|
],
|
||||||
|
() => {
|
||||||
|
this.reloadNotesDisplayOptions();
|
||||||
|
this.reloadNotes();
|
||||||
|
}
|
||||||
|
),
|
||||||
|
appState.addObserver(async (eventName) => {
|
||||||
|
if (eventName === AppStateEvent.TagChanged) {
|
||||||
|
this.handleTagChange();
|
||||||
|
} else if (eventName === AppStateEvent.ActiveEditorChanged) {
|
||||||
|
this.handleEditorChange();
|
||||||
|
} else if (eventName === AppStateEvent.EditorFocused) {
|
||||||
|
this.toggleDisplayOptionsMenu(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
makeObservable(this, {
|
||||||
|
completedFullSync: observable,
|
||||||
|
displayOptions: observable.struct,
|
||||||
|
noteFilterText: observable,
|
||||||
|
notes: observable,
|
||||||
|
notesToDisplay: observable,
|
||||||
|
panelTitle: observable,
|
||||||
|
renderedNotes: observable,
|
||||||
|
selectedNotes: observable,
|
||||||
|
showDisplayOptionsMenu: observable,
|
||||||
|
|
||||||
|
reloadNotes: action,
|
||||||
|
reloadPanelTitle: action,
|
||||||
|
reloadPreferences: action,
|
||||||
|
resetPagination: action,
|
||||||
|
setCompletedFullSync: action,
|
||||||
|
setNoteFilterText: action,
|
||||||
|
syncSelectedNotes: action,
|
||||||
|
toggleDisplayOptionsMenu: action,
|
||||||
|
onFilterEnter: action,
|
||||||
|
handleFilterTextChanged: action,
|
||||||
|
|
||||||
|
optionsSubtitle: computed,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onresize = () => {
|
||||||
|
this.resetPagination(true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setCompletedFullSync = (completed: boolean) => {
|
||||||
|
this.completedFullSync = completed;
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleDisplayOptionsMenu = (enabled: boolean) => {
|
||||||
|
this.showDisplayOptionsMenu = enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
get searchBarElement() {
|
||||||
|
return document.getElementById(ELEMENT_ID_SEARCH_BAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isFiltering(): boolean {
|
||||||
|
return !!this.noteFilterText && this.noteFilterText.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeEditorNote() {
|
||||||
|
return this.appState.notes.activeEditor?.note;
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadPanelTitle = () => {
|
||||||
|
let title = this.panelTitle;
|
||||||
|
if (this.isFiltering) {
|
||||||
|
const resultCount = this.notes.length;
|
||||||
|
title = `${resultCount} search results`;
|
||||||
|
} else if (this.appState.selectedTag) {
|
||||||
|
title = `${this.appState.selectedTag.title}`;
|
||||||
|
}
|
||||||
|
this.panelTitle = title;
|
||||||
|
};
|
||||||
|
|
||||||
|
reloadNotes = () => {
|
||||||
|
const tag = this.appState.selectedTag;
|
||||||
|
if (!tag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notes = this.application.getDisplayableItems(
|
||||||
|
ContentType.Note
|
||||||
|
) as SNNote[];
|
||||||
|
const renderedNotes = notes.slice(0, this.notesToDisplay);
|
||||||
|
|
||||||
|
this.notes = notes;
|
||||||
|
this.renderedNotes = renderedNotes;
|
||||||
|
this.reloadPanelTitle();
|
||||||
|
};
|
||||||
|
|
||||||
|
reloadNotesDisplayOptions = () => {
|
||||||
|
const tag = this.appState.selectedTag;
|
||||||
|
|
||||||
|
const searchText = this.noteFilterText.toLowerCase();
|
||||||
|
const isSearching = searchText.length;
|
||||||
|
let includeArchived: boolean;
|
||||||
|
let includeTrashed: boolean;
|
||||||
|
|
||||||
|
if (isSearching) {
|
||||||
|
includeArchived = this.appState.searchOptions.includeArchived;
|
||||||
|
includeTrashed = this.appState.searchOptions.includeTrashed;
|
||||||
|
} else {
|
||||||
|
includeArchived = this.displayOptions.showArchived ?? false;
|
||||||
|
includeTrashed = this.displayOptions.showTrashed ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const criteria = NotesDisplayCriteria.Create({
|
||||||
|
sortProperty: this.displayOptions.sortBy as CollectionSort,
|
||||||
|
sortDirection: this.displayOptions.sortReverse ? 'asc' : 'dsc',
|
||||||
|
tags: tag ? [tag] : [],
|
||||||
|
includeArchived,
|
||||||
|
includeTrashed,
|
||||||
|
includePinned: !this.displayOptions.hidePinned,
|
||||||
|
includeProtected: !this.displayOptions.hideProtected,
|
||||||
|
searchQuery: {
|
||||||
|
query: searchText,
|
||||||
|
includeProtectedNoteText:
|
||||||
|
this.appState.searchOptions.includeProtectedContents,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.application.setNotesDisplayCriteria(criteria);
|
||||||
|
};
|
||||||
|
|
||||||
|
reloadPreferences = () => {
|
||||||
|
const freshDisplayOptions = {} as DisplayOptions;
|
||||||
|
const currentSortBy = this.displayOptions.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;
|
||||||
|
}
|
||||||
|
freshDisplayOptions.sortBy = sortBy;
|
||||||
|
freshDisplayOptions.sortReverse = this.application.getPreference(
|
||||||
|
PrefKey.SortNotesReverse,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
freshDisplayOptions.showArchived = this.application.getPreference(
|
||||||
|
PrefKey.NotesShowArchived,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
freshDisplayOptions.showTrashed = this.application.getPreference(
|
||||||
|
PrefKey.NotesShowTrashed,
|
||||||
|
false
|
||||||
|
) as boolean;
|
||||||
|
freshDisplayOptions.hidePinned = this.application.getPreference(
|
||||||
|
PrefKey.NotesHidePinned,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
freshDisplayOptions.hideProtected = this.application.getPreference(
|
||||||
|
PrefKey.NotesHideProtected,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
freshDisplayOptions.hideNotePreview = this.application.getPreference(
|
||||||
|
PrefKey.NotesHideNotePreview,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
freshDisplayOptions.hideDate = this.application.getPreference(
|
||||||
|
PrefKey.NotesHideDate,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
freshDisplayOptions.hideTags = this.application.getPreference(
|
||||||
|
PrefKey.NotesHideTags,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const displayOptionsChanged =
|
||||||
|
freshDisplayOptions.sortBy !== this.displayOptions.sortBy ||
|
||||||
|
freshDisplayOptions.sortReverse !== this.displayOptions.sortReverse ||
|
||||||
|
freshDisplayOptions.hidePinned !== this.displayOptions.hidePinned ||
|
||||||
|
freshDisplayOptions.showArchived !== this.displayOptions.showArchived ||
|
||||||
|
freshDisplayOptions.showTrashed !== this.displayOptions.showTrashed ||
|
||||||
|
freshDisplayOptions.hideProtected !== this.displayOptions.hideProtected ||
|
||||||
|
freshDisplayOptions.hideTags !== this.displayOptions.hideTags;
|
||||||
|
this.displayOptions = freshDisplayOptions;
|
||||||
|
if (displayOptionsChanged) {
|
||||||
|
this.reloadNotesDisplayOptions();
|
||||||
|
}
|
||||||
|
this.reloadNotes();
|
||||||
|
if (freshDisplayOptions.sortBy !== currentSortBy) {
|
||||||
|
this.selectFirstNote();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createNewNote = async (focusNewNote = true) => {
|
||||||
|
this.appState.notes.unselectNotes();
|
||||||
|
let title = `Note ${this.notes.length + 1}`;
|
||||||
|
if (this.isFiltering) {
|
||||||
|
title = this.noteFilterText;
|
||||||
|
}
|
||||||
|
await this.appState.createEditor(title);
|
||||||
|
this.reloadNotes();
|
||||||
|
this.appState.noteTags.reloadTags();
|
||||||
|
const noteTitleEditorElement = document.getElementById('note-title-editor');
|
||||||
|
if (focusNewNote) {
|
||||||
|
noteTitleEditorElement?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createPlaceholderNote = () => {
|
||||||
|
const selectedTag = this.appState.selectedTag;
|
||||||
|
if (selectedTag && selectedTag.isSmartTag && !selectedTag.isAllTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.createNewNote(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
get optionsSubtitle(): string {
|
||||||
|
let base = '';
|
||||||
|
if (this.displayOptions.sortBy === CollectionSort.CreatedAt) {
|
||||||
|
base += ' Date Added';
|
||||||
|
} else if (this.displayOptions.sortBy === CollectionSort.UpdatedAt) {
|
||||||
|
base += ' Date Modified';
|
||||||
|
} else if (this.displayOptions.sortBy === CollectionSort.Title) {
|
||||||
|
base += ' Title';
|
||||||
|
}
|
||||||
|
if (this.displayOptions.showArchived) {
|
||||||
|
base += ' | + Archived';
|
||||||
|
}
|
||||||
|
if (this.displayOptions.showTrashed) {
|
||||||
|
base += ' | + Trashed';
|
||||||
|
}
|
||||||
|
if (this.displayOptions.hidePinned) {
|
||||||
|
base += ' | – Pinned';
|
||||||
|
}
|
||||||
|
if (this.displayOptions.hideProtected) {
|
||||||
|
base += ' | – Protected';
|
||||||
|
}
|
||||||
|
if (this.displayOptions.sortReverse) {
|
||||||
|
base += ' | Reversed';
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
paginate = () => {
|
||||||
|
this.notesToDisplay += this.pageSize;
|
||||||
|
this.reloadNotes();
|
||||||
|
if (this.searchSubmitted) {
|
||||||
|
this.application.getDesktopService().searchText(this.noteFilterText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
getFirstNonProtectedNote = () => {
|
||||||
|
return this.notes.find((note) => !note.protected);
|
||||||
|
};
|
||||||
|
|
||||||
|
get notesListScrollContainer() {
|
||||||
|
return document.getElementById(ELEMENT_ID_SCROLL_CONTAINER);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectNote = async (
|
||||||
|
note: SNNote,
|
||||||
|
userTriggered?: boolean,
|
||||||
|
scrollIntoView = true
|
||||||
|
): Promise<void> => {
|
||||||
|
await this.appState.notes.selectNote(note.uuid, userTriggered);
|
||||||
|
if (scrollIntoView) {
|
||||||
|
const noteElement = document.getElementById(`note-${note.uuid}`);
|
||||||
|
noteElement?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
selectFirstNote = () => {
|
||||||
|
const note = this.getFirstNonProtectedNote();
|
||||||
|
if (note) {
|
||||||
|
this.selectNote(note, false, false);
|
||||||
|
this.resetScrollPosition();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
selectNextNote = () => {
|
||||||
|
const displayableNotes = this.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, false, false);
|
||||||
|
} else {
|
||||||
|
this.appState.closeActiveEditor();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
selectPreviousNote = () => {
|
||||||
|
const displayableNotes = this.notes;
|
||||||
|
if (this.activeEditorNote) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setNoteFilterText = (text: string) => {
|
||||||
|
this.noteFilterText = text;
|
||||||
|
};
|
||||||
|
|
||||||
|
syncSelectedNotes = () => {
|
||||||
|
this.selectedNotes = this.appState.notes.selectedNotes;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEditorChange = async () => {
|
||||||
|
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.noteFilterText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resetScrollPosition = () => {
|
||||||
|
if (this.notesListScrollContainer) {
|
||||||
|
this.notesListScrollContainer.scrollTop = 0;
|
||||||
|
this.notesListScrollContainer.scrollLeft = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTagChange = () => {
|
||||||
|
this.resetScrollPosition();
|
||||||
|
this.toggleDisplayOptionsMenu(false);
|
||||||
|
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();
|
||||||
|
this.reloadNotes();
|
||||||
|
|
||||||
|
if (this.notes.length > 0) {
|
||||||
|
this.selectFirstNote();
|
||||||
|
} else if (dbLoaded) {
|
||||||
|
if (
|
||||||
|
this.activeEditorNote &&
|
||||||
|
!this.notes.includes(this.activeEditorNote)
|
||||||
|
) {
|
||||||
|
this.appState.closeActiveEditor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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.noteFilterText);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFilterTextChanged = () => {
|
||||||
|
if (this.searchSubmitted) {
|
||||||
|
this.searchSubmitted = false;
|
||||||
|
}
|
||||||
|
this.reloadNotesDisplayOptions();
|
||||||
|
this.reloadNotes();
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchInputBlur = () => {
|
||||||
|
this.appState.searchOptions.refreshIncludeProtectedContents();
|
||||||
|
};
|
||||||
|
|
||||||
|
clearFilterText = () => {
|
||||||
|
this.setNoteFilterText('');
|
||||||
|
this.onFilterEnter();
|
||||||
|
this.handleFilterTextChanged();
|
||||||
|
this.resetPagination();
|
||||||
|
};
|
||||||
|
}
|
||||||
291
app/assets/javascripts/ui_models/panel_resizer.ts
Normal file
291
app/assets/javascripts/ui_models/panel_resizer.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import {
|
||||||
|
PanelSide,
|
||||||
|
ResizeFinishCallback,
|
||||||
|
} from '@/directives/views/panelResizer';
|
||||||
|
import { debounce } from '@/utils';
|
||||||
|
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs';
|
||||||
|
import { action, computed, makeObservable, observable } from 'mobx';
|
||||||
|
import { WebApplication } from './application';
|
||||||
|
|
||||||
|
export type PanelResizerProps = {
|
||||||
|
alwaysVisible?: boolean;
|
||||||
|
application: WebApplication;
|
||||||
|
collapsable: boolean;
|
||||||
|
defaultWidth?: number;
|
||||||
|
hoverable?: boolean;
|
||||||
|
minWidth?: number;
|
||||||
|
panel: HTMLDivElement;
|
||||||
|
prefKey: PrefKey;
|
||||||
|
resizeFinishCallback?: ResizeFinishCallback;
|
||||||
|
side: PanelSide;
|
||||||
|
widthEventCallback?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PanelResizerState {
|
||||||
|
private application: WebApplication;
|
||||||
|
alwaysVisible: boolean;
|
||||||
|
collapsable: boolean;
|
||||||
|
collapsed = false;
|
||||||
|
currentMinWidth = 0;
|
||||||
|
defaultWidth: number;
|
||||||
|
hoverable: boolean;
|
||||||
|
lastDownX = 0;
|
||||||
|
lastLeft = 0;
|
||||||
|
lastWidth = 0;
|
||||||
|
panel: HTMLDivElement;
|
||||||
|
pressed = false;
|
||||||
|
prefKey: PrefKey;
|
||||||
|
resizeFinishCallback?: ResizeFinishCallback;
|
||||||
|
side: PanelSide;
|
||||||
|
startLeft = 0;
|
||||||
|
startWidth = 0;
|
||||||
|
widthBeforeLastDblClick = 0;
|
||||||
|
widthEventCallback?: () => void;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
alwaysVisible,
|
||||||
|
application,
|
||||||
|
defaultWidth,
|
||||||
|
hoverable,
|
||||||
|
collapsable,
|
||||||
|
minWidth,
|
||||||
|
panel,
|
||||||
|
prefKey,
|
||||||
|
resizeFinishCallback,
|
||||||
|
side,
|
||||||
|
widthEventCallback,
|
||||||
|
}: PanelResizerProps) {
|
||||||
|
this.alwaysVisible = alwaysVisible ?? false;
|
||||||
|
this.application = application;
|
||||||
|
this.collapsable = collapsable ?? false;
|
||||||
|
this.collapsed = false;
|
||||||
|
this.currentMinWidth = minWidth ?? 0;
|
||||||
|
this.defaultWidth = defaultWidth ?? 0;
|
||||||
|
this.hoverable = hoverable ?? true;
|
||||||
|
this.lastDownX = 0;
|
||||||
|
this.lastLeft = this.startLeft;
|
||||||
|
this.lastWidth = this.startWidth;
|
||||||
|
this.panel = panel;
|
||||||
|
this.prefKey = prefKey;
|
||||||
|
this.pressed = false;
|
||||||
|
this.side = side;
|
||||||
|
this.startLeft = this.panel.offsetLeft;
|
||||||
|
this.startWidth = this.panel.scrollWidth;
|
||||||
|
this.widthBeforeLastDblClick = 0;
|
||||||
|
this.widthEventCallback = widthEventCallback;
|
||||||
|
this.resizeFinishCallback = resizeFinishCallback;
|
||||||
|
|
||||||
|
application.addEventObserver(async () => {
|
||||||
|
const changedWidth = application.getPreference(prefKey) as number;
|
||||||
|
if (changedWidth !== this.lastWidth) this.setWidth(changedWidth, true);
|
||||||
|
}, ApplicationEvent.PreferencesChanged);
|
||||||
|
|
||||||
|
makeObservable(this, {
|
||||||
|
pressed: observable,
|
||||||
|
collapsed: observable,
|
||||||
|
|
||||||
|
onMouseUp: action,
|
||||||
|
onMouseDown: action,
|
||||||
|
onDblClick: action,
|
||||||
|
handleWidthEvent: action,
|
||||||
|
handleLeftEvent: action,
|
||||||
|
setWidth: action,
|
||||||
|
setMinWidth: action,
|
||||||
|
reloadDefaultValues: action,
|
||||||
|
|
||||||
|
appFrame: computed,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||||
|
document.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||||
|
if (this.side === PanelSide.Right) {
|
||||||
|
window.addEventListener(
|
||||||
|
'resize',
|
||||||
|
debounce(this.handleResize.bind(this), 250)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get appFrame() {
|
||||||
|
return document.getElementById('app')?.getBoundingClientRect() as DOMRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParentRect() {
|
||||||
|
return (this.panel.parentNode as HTMLElement).getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
isAtMaxWidth = () => {
|
||||||
|
return (
|
||||||
|
Math.round(this.lastWidth + this.lastLeft) ===
|
||||||
|
Math.round(this.getParentRect().width)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
isCollapsed() {
|
||||||
|
return this.lastWidth <= this.currentMinWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadDefaultValues = () => {
|
||||||
|
this.startWidth = this.isAtMaxWidth()
|
||||||
|
? this.getParentRect().width
|
||||||
|
: this.panel.scrollWidth;
|
||||||
|
this.lastWidth = this.startWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
finishSettingWidth = () => {
|
||||||
|
if (!this.collapsable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collapsed = this.isCollapsed();
|
||||||
|
};
|
||||||
|
|
||||||
|
setWidth = (width: number, finish = false) => {
|
||||||
|
if (width < this.currentMinWidth) {
|
||||||
|
width = this.currentMinWidth;
|
||||||
|
}
|
||||||
|
const parentRect = this.getParentRect();
|
||||||
|
if (width > parentRect.width) {
|
||||||
|
width = parentRect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWidth = this.appFrame.width - this.panel.getBoundingClientRect().x;
|
||||||
|
if (width > maxWidth) {
|
||||||
|
width = maxWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.round(width + this.lastLeft) === Math.round(parentRect.width)) {
|
||||||
|
this.panel.style.width = `calc(100% - ${this.lastLeft}px)`;
|
||||||
|
} else {
|
||||||
|
this.panel.style.width = width + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastWidth = width;
|
||||||
|
|
||||||
|
if (finish) {
|
||||||
|
this.finishSettingWidth();
|
||||||
|
if (this.resizeFinishCallback) {
|
||||||
|
this.resizeFinishCallback(
|
||||||
|
this.lastWidth,
|
||||||
|
this.lastLeft,
|
||||||
|
this.isAtMaxWidth(),
|
||||||
|
this.isCollapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.application.setPreference(this.prefKey, this.lastWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
setLeft = (left: number) => {
|
||||||
|
this.panel.style.left = left + 'px';
|
||||||
|
this.lastLeft = left;
|
||||||
|
};
|
||||||
|
|
||||||
|
onDblClick = () => {
|
||||||
|
const collapsed = this.isCollapsed();
|
||||||
|
if (collapsed) {
|
||||||
|
this.setWidth(this.widthBeforeLastDblClick || this.defaultWidth);
|
||||||
|
} else {
|
||||||
|
this.widthBeforeLastDblClick = this.lastWidth;
|
||||||
|
this.setWidth(this.currentMinWidth);
|
||||||
|
}
|
||||||
|
this.application.setPreference(this.prefKey, this.lastWidth);
|
||||||
|
this.finishSettingWidth();
|
||||||
|
if (this.resizeFinishCallback) {
|
||||||
|
this.resizeFinishCallback(
|
||||||
|
this.lastWidth,
|
||||||
|
this.lastLeft,
|
||||||
|
this.isAtMaxWidth(),
|
||||||
|
this.isCollapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleWidthEvent(event?: MouseEvent) {
|
||||||
|
if (this.widthEventCallback) {
|
||||||
|
this.widthEventCallback();
|
||||||
|
}
|
||||||
|
let x;
|
||||||
|
if (event) {
|
||||||
|
x = event.clientX;
|
||||||
|
} else {
|
||||||
|
/** Coming from resize event */
|
||||||
|
x = 0;
|
||||||
|
this.lastDownX = 0;
|
||||||
|
}
|
||||||
|
const deltaX = x - this.lastDownX;
|
||||||
|
const newWidth = this.startWidth + deltaX;
|
||||||
|
this.setWidth(newWidth, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLeftEvent(event: MouseEvent) {
|
||||||
|
const panelRect = this.panel.getBoundingClientRect();
|
||||||
|
const x = event.clientX || panelRect.x;
|
||||||
|
let deltaX = x - this.lastDownX;
|
||||||
|
let newLeft = this.startLeft + deltaX;
|
||||||
|
if (newLeft < 0) {
|
||||||
|
newLeft = 0;
|
||||||
|
deltaX = -this.startLeft;
|
||||||
|
}
|
||||||
|
const parentRect = this.getParentRect();
|
||||||
|
let newWidth = this.startWidth - deltaX;
|
||||||
|
if (newWidth < this.currentMinWidth) {
|
||||||
|
newWidth = this.currentMinWidth;
|
||||||
|
}
|
||||||
|
if (newWidth > parentRect.width) {
|
||||||
|
newWidth = parentRect.width;
|
||||||
|
}
|
||||||
|
if (newLeft + newWidth > parentRect.width) {
|
||||||
|
newLeft = parentRect.width - newWidth;
|
||||||
|
}
|
||||||
|
this.setLeft(newLeft);
|
||||||
|
this.setWidth(newWidth, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize = () => {
|
||||||
|
this.reloadDefaultValues();
|
||||||
|
this.handleWidthEvent();
|
||||||
|
this.finishSettingWidth();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMouseDown = (event: MouseEvent) => {
|
||||||
|
this.pressed = true;
|
||||||
|
this.lastDownX = event.clientX;
|
||||||
|
this.startWidth = this.panel.scrollWidth;
|
||||||
|
this.startLeft = this.panel.offsetLeft;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMouseUp = () => {
|
||||||
|
if (!this.pressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pressed = false;
|
||||||
|
const isMaxWidth = this.isAtMaxWidth();
|
||||||
|
if (this.resizeFinishCallback) {
|
||||||
|
this.resizeFinishCallback(
|
||||||
|
this.lastWidth,
|
||||||
|
this.lastLeft,
|
||||||
|
isMaxWidth,
|
||||||
|
this.isCollapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.finishSettingWidth();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMouseMove(event: MouseEvent) {
|
||||||
|
if (!this.pressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.side === PanelSide.Left) {
|
||||||
|
this.handleLeftEvent(event);
|
||||||
|
} else {
|
||||||
|
this.handleWidthEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMinWidth = (minWidth?: number) => {
|
||||||
|
this.currentMinWidth = minWidth ?? this.currentMinWidth;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@
|
|||||||
ng-if='!self.state.needsUnlock && self.state.ready'
|
ng-if='!self.state.needsUnlock && self.state.ready'
|
||||||
)
|
)
|
||||||
tags-view(application='self.application')
|
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')
|
editor-group-view.flex-grow(application='self.application')
|
||||||
|
|
||||||
footer-view(
|
footer-view(
|
||||||
|
|||||||
@@ -4,6 +4,5 @@ export { ApplicationView } from './application/application_view';
|
|||||||
export { EditorGroupView } from './editor_group/editor_group_view';
|
export { EditorGroupView } from './editor_group/editor_group_view';
|
||||||
export { EditorView } from './editor/editor_view';
|
export { EditorView } from './editor/editor_view';
|
||||||
export { FooterView } from './footer/footer_view';
|
export { FooterView } from './footer/footer_view';
|
||||||
export { NotesView } from './notes/notes_view';
|
|
||||||
export { TagsView } from './tags/tags_view';
|
export { TagsView } from './tags/tags_view';
|
||||||
export { ChallengeModal } from './challenge_modal/challenge_modal';
|
export { ChallengeModal } from './challenge_modal/challenge_modal';
|
||||||
|
|||||||
@@ -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'"
|
|
||||||
)
|
|
||||||
@@ -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: '='
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section.tags,
|
.section.tags,
|
||||||
.section.notes {
|
notes-view {
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
animation: fade-out 1.25s forwards;
|
animation: fade-out 1.25s forwards;
|
||||||
transition: width 1.25s;
|
transition: width 1.25s;
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
width: 0px !important;
|
width: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section.notes:hover {
|
notes-view:hover {
|
||||||
flex: initial;
|
flex: initial;
|
||||||
width: 0px !important;
|
width: 0px !important;
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
.disable-focus-mode {
|
.disable-focus-mode {
|
||||||
.section.tags,
|
.section.tags,
|
||||||
.section.notes {
|
notes-view {
|
||||||
transition: width 1.25s;
|
transition: width 1.25s;
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
animation: fade-in 1.25s forwards;
|
animation: fade-in 1.25s forwards;
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ $footer-height: 2rem;
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
panel-resizer {
|
panel-resizer, .panel-resizer {
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: $z-index-panel-resizer;
|
z-index: $z-index-panel-resizer;
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
notes-view {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
#notes-column,
|
#notes-column,
|
||||||
.notes {
|
.notes {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
border-left: 1px solid var(--sn-stylekit-border-color);
|
border-left: 1px solid var(--sn-stylekit-border-color);
|
||||||
border-right: 1px solid var(--sn-stylekit-border-color);
|
border-right: 1px solid var(--sn-stylekit-border-color);
|
||||||
|
|
||||||
font-size: var(--sn-stylekit-font-size-h2);
|
font-size: var(--sn-stylekit-font-size-h2);
|
||||||
|
|
||||||
width: 350px;
|
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
@@ -71,6 +76,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#search-clear-button {
|
#search-clear-button {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 17px;
|
width: 17px;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
|
|||||||
Reference in New Issue
Block a user