refactor: Move notes_view to React (#761)
This commit is contained in:
@@ -26,7 +26,6 @@ import {
|
||||
EditorGroupView,
|
||||
EditorView,
|
||||
TagsView,
|
||||
NotesView,
|
||||
FooterView,
|
||||
ChallengeModal,
|
||||
} from '@/views';
|
||||
@@ -81,6 +80,7 @@ import { PurchaseFlowDirective } from './purchaseFlow';
|
||||
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu/QuickSettingsMenu';
|
||||
import { ComponentViewDirective } from '@/components/ComponentView';
|
||||
import { TagsListDirective } from '@/components/TagsList';
|
||||
import { NotesViewDirective } from './components/NotesView';
|
||||
import { PinNoteButtonDirective } from '@/components/PinNoteButton';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
@@ -137,7 +137,6 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('editorGroupView', () => new EditorGroupView())
|
||||
.directive('editorView', () => new EditorView())
|
||||
.directive('tagsView', () => new TagsView())
|
||||
.directive('notesView', () => new NotesView())
|
||||
.directive('footerView', () => new FooterView());
|
||||
|
||||
// Directives - Functional
|
||||
@@ -186,6 +185,7 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('tags', TagsListDirective)
|
||||
.directive('preferences', PreferencesDirective)
|
||||
.directive('purchaseFlow', PurchaseFlowDirective)
|
||||
.directive('notesView', NotesViewDirective)
|
||||
.directive('pinNoteButton', PinNoteButtonDirective);
|
||||
|
||||
// Filters
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
|
||||
|
||||
type Props = { appState: AppState };
|
||||
|
||||
const NoAccountWarning = observer(({ appState }: Props) => {
|
||||
export const NoAccountWarning = observer(({ appState }: Props) => {
|
||||
const canShow = appState.noAccountWarning.show;
|
||||
if (!canShow) {
|
||||
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 = {
|
||||
application: WebApplication;
|
||||
setShowMenuFalse: () => void;
|
||||
closeDisplayOptionsMenu: () => void;
|
||||
};
|
||||
|
||||
export const NotesListOptionsMenu: FunctionComponent<Props> = observer(
|
||||
({ setShowMenuFalse, application }) => {
|
||||
({ closeDisplayOptionsMenu, application }) => {
|
||||
const menuClassName =
|
||||
'sn-dropdown sn-dropdown--animated min-w-70 overflow-y-auto \
|
||||
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) => {
|
||||
if (!open) {
|
||||
setShowMenuFalse();
|
||||
closeDisplayOptionsMenu();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<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">
|
||||
Sort by
|
||||
</div>
|
||||
@@ -246,7 +246,7 @@ flex flex-col py-2 bottom-0 left-2 absolute';
|
||||
export const NotesListOptionsDirective = toDirective<Props>(
|
||||
NotesListOptionsMenu,
|
||||
{
|
||||
setShowMenuFalse: '=',
|
||||
closeDisplayOptionsMenu: '=',
|
||||
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;
|
||||
};
|
||||
|
||||
const SearchOptions = observer(({ appState }: Props) => {
|
||||
export const SearchOptions = observer(({ appState }: Props) => {
|
||||
const { searchOptions } = appState;
|
||||
|
||||
const {
|
||||
|
||||
@@ -3,14 +3,14 @@ import angular from 'angular';
|
||||
import template from '%/directives/panel-resizer.pug';
|
||||
import { debounce } from '@/utils';
|
||||
|
||||
enum PanelSide {
|
||||
export enum PanelSide {
|
||||
Right = 'right',
|
||||
Left = 'left'
|
||||
Left = 'left',
|
||||
}
|
||||
enum MouseEventType {
|
||||
Move = 'mousemove',
|
||||
Down = 'mousedown',
|
||||
Up = 'mouseup'
|
||||
Up = 'mouseup',
|
||||
}
|
||||
enum CssClass {
|
||||
Hoverable = 'hoverable',
|
||||
@@ -22,64 +22,63 @@ enum CssClass {
|
||||
}
|
||||
const WINDOW_EVENT_RESIZE = 'resize';
|
||||
|
||||
type ResizeFinishCallback = (
|
||||
export type ResizeFinishCallback = (
|
||||
lastWidth: number,
|
||||
lastLeft: number,
|
||||
isMaxWidth: boolean,
|
||||
isCollapsed: boolean
|
||||
) => void
|
||||
) => void;
|
||||
|
||||
interface PanelResizerScope {
|
||||
alwaysVisible: boolean
|
||||
collapsable: boolean
|
||||
control: PanelPuppet
|
||||
defaultWidth: number
|
||||
hoverable: boolean
|
||||
index: number
|
||||
minWidth: number
|
||||
onResizeFinish: () => ResizeFinishCallback
|
||||
onWidthEvent?: () => void
|
||||
panelId: string
|
||||
property: PanelSide
|
||||
alwaysVisible: boolean;
|
||||
collapsable: boolean;
|
||||
control: PanelPuppet;
|
||||
defaultWidth: number;
|
||||
hoverable: boolean;
|
||||
index: number;
|
||||
minWidth: number;
|
||||
onResizeFinish: () => ResizeFinishCallback;
|
||||
onWidthEvent?: () => void;
|
||||
panelId: string;
|
||||
property: PanelSide;
|
||||
}
|
||||
|
||||
class PanelResizerCtrl implements PanelResizerScope {
|
||||
|
||||
/** @scope */
|
||||
alwaysVisible!: boolean
|
||||
collapsable!: boolean
|
||||
control!: PanelPuppet
|
||||
defaultWidth!: number
|
||||
hoverable!: boolean
|
||||
index!: number
|
||||
minWidth!: number
|
||||
onResizeFinish!: () => ResizeFinishCallback
|
||||
onWidthEvent?: () => () => void
|
||||
panelId!: string
|
||||
property!: PanelSide
|
||||
alwaysVisible!: boolean;
|
||||
collapsable!: boolean;
|
||||
control!: PanelPuppet;
|
||||
defaultWidth!: number;
|
||||
hoverable!: boolean;
|
||||
index!: number;
|
||||
minWidth!: number;
|
||||
onResizeFinish!: () => ResizeFinishCallback;
|
||||
onWidthEvent?: () => () => void;
|
||||
panelId!: string;
|
||||
property!: PanelSide;
|
||||
|
||||
$compile: ng.ICompileService
|
||||
$element: JQLite
|
||||
$timeout: ng.ITimeoutService
|
||||
panel!: HTMLElement
|
||||
resizerColumn!: HTMLElement
|
||||
currentMinWidth = 0
|
||||
pressed = false
|
||||
startWidth = 0
|
||||
lastDownX = 0
|
||||
collapsed = false
|
||||
lastWidth = 0
|
||||
startLeft = 0
|
||||
lastLeft = 0
|
||||
appFrame?: DOMRect
|
||||
widthBeforeLastDblClick = 0
|
||||
overlay?: JQLite
|
||||
$compile: ng.ICompileService;
|
||||
$element: JQLite;
|
||||
$timeout: ng.ITimeoutService;
|
||||
panel!: HTMLElement;
|
||||
resizerColumn!: HTMLElement;
|
||||
currentMinWidth = 0;
|
||||
pressed = false;
|
||||
startWidth = 0;
|
||||
lastDownX = 0;
|
||||
collapsed = false;
|
||||
lastWidth = 0;
|
||||
startLeft = 0;
|
||||
lastLeft = 0;
|
||||
appFrame?: DOMRect;
|
||||
widthBeforeLastDblClick = 0;
|
||||
overlay?: JQLite;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$compile: ng.ICompileService,
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
this.$compile = $compile;
|
||||
this.$element = $element;
|
||||
@@ -109,7 +108,10 @@ class PanelResizerCtrl implements PanelResizerScope {
|
||||
window.removeEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
|
||||
document.removeEventListener(MouseEventType.Move, this.onMouseMove);
|
||||
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.onMouseMove as any) = undefined;
|
||||
(this.onMouseUp as any) = undefined;
|
||||
@@ -140,7 +142,7 @@ class PanelResizerCtrl implements PanelResizerScope {
|
||||
return;
|
||||
}
|
||||
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.startWidth = this.panel.scrollWidth;
|
||||
this.lastDownX = 0;
|
||||
@@ -313,7 +315,8 @@ class PanelResizerCtrl implements PanelResizerScope {
|
||||
width = parentRect.width;
|
||||
}
|
||||
|
||||
const maxWidth = this.appFrame!.width - this.panel.getBoundingClientRect().x;
|
||||
const maxWidth =
|
||||
this.appFrame!.width - this.panel.getBoundingClientRect().x;
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth;
|
||||
}
|
||||
@@ -356,7 +359,9 @@ class PanelResizerCtrl implements PanelResizerScope {
|
||||
if (this.overlay) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -395,7 +400,7 @@ export class PanelResizer extends WebDirective {
|
||||
onResizeFinish: '&',
|
||||
onWidthEvent: '&',
|
||||
panelId: '=',
|
||||
property: '='
|
||||
property: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum KeyboardKey {
|
||||
Backspace = 'Backspace',
|
||||
Up = 'ArrowUp',
|
||||
Down = 'ArrowDown',
|
||||
Enter = 'Enter',
|
||||
}
|
||||
|
||||
export enum KeyboardModifier {
|
||||
@@ -51,7 +52,9 @@ export class IOService {
|
||||
(this.handleWindowBlur as unknown) = undefined;
|
||||
}
|
||||
|
||||
private addActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
private addActiveModifier = (
|
||||
modifier: KeyboardModifier | undefined
|
||||
): void => {
|
||||
if (!modifier) {
|
||||
return;
|
||||
}
|
||||
@@ -73,14 +76,16 @@ export class IOService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => {
|
||||
private removeActiveModifier = (
|
||||
modifier: KeyboardModifier | undefined
|
||||
): void => {
|
||||
if (!modifier) {
|
||||
return;
|
||||
}
|
||||
this.activeModifiers.delete(modifier);
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (event: KeyboardEvent): void => {
|
||||
for (const modifier of this.modifiersForEvent(event)) {
|
||||
@@ -91,7 +96,7 @@ export class IOService {
|
||||
|
||||
handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.addActiveModifier(modifier);
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyUp = (event: KeyboardEvent): void => {
|
||||
for (const modifier of this.modifiersForEvent(event)) {
|
||||
@@ -102,7 +107,7 @@ export class IOService {
|
||||
|
||||
handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => {
|
||||
this.removeActiveModifier(modifier);
|
||||
}
|
||||
};
|
||||
|
||||
handleWindowBlur = (): void => {
|
||||
for (const modifier of this.activeModifiers) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from 'mobx';
|
||||
import { ActionsMenuState } from './actions_menu_state';
|
||||
import { NotesState } from './notes_state';
|
||||
import { NotesViewState } from './notes_view_state';
|
||||
import { NoteTagsState } from './note_tags_state';
|
||||
import { NoAccountWarningState } from './no_account_warning_state';
|
||||
import { PreferencesState } from './preferences_state';
|
||||
@@ -86,6 +87,7 @@ export class AppState {
|
||||
readonly searchOptions: SearchOptionsState;
|
||||
readonly notes: NotesState;
|
||||
readonly tags: TagsState;
|
||||
readonly notesView: NotesViewState;
|
||||
isSessionsModalVisible = false;
|
||||
|
||||
private appEventObserverRemovers: (() => void)[] = [];
|
||||
@@ -127,6 +129,11 @@ export class AppState {
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.purchaseFlow = new PurchaseFlowState(application);
|
||||
this.notesView = new NotesViewState(
|
||||
application,
|
||||
this,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.addAppEventObserver();
|
||||
this.streamNotesAndTags();
|
||||
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'
|
||||
)
|
||||
tags-view(application='self.application')
|
||||
notes-view(application='self.application')
|
||||
notes-view(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
editor-group-view.flex-grow(application='self.application')
|
||||
|
||||
footer-view(
|
||||
|
||||
@@ -4,6 +4,5 @@ export { ApplicationView } from './application/application_view';
|
||||
export { EditorGroupView } from './editor_group/editor_group_view';
|
||||
export { EditorView } from './editor/editor_view';
|
||||
export { FooterView } from './footer/footer_view';
|
||||
export { NotesView } from './notes/notes_view';
|
||||
export { TagsView } from './tags/tags_view';
|
||||
export { ChallengeModal } from './challenge_modal/challenge_modal';
|
||||
|
||||
@@ -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.notes {
|
||||
notes-view {
|
||||
will-change: opacity;
|
||||
animation: fade-out 1.25s forwards;
|
||||
transition: width 1.25s;
|
||||
@@ -50,7 +50,7 @@
|
||||
width: 0px !important;
|
||||
}
|
||||
|
||||
.section.notes:hover {
|
||||
notes-view:hover {
|
||||
flex: initial;
|
||||
width: 0px !important;
|
||||
}
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
.disable-focus-mode {
|
||||
.section.tags,
|
||||
.section.notes {
|
||||
notes-view {
|
||||
transition: width 1.25s;
|
||||
will-change: opacity;
|
||||
animation: fade-in 1.25s forwards;
|
||||
|
||||
@@ -136,7 +136,7 @@ $footer-height: 2rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
panel-resizer {
|
||||
panel-resizer, .panel-resizer {
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: $z-index-panel-resizer;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
notes-view {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
#notes-column,
|
||||
.notes {
|
||||
width: 100%;
|
||||
|
||||
border-left: 1px solid var(--sn-stylekit-border-color);
|
||||
border-right: 1px solid var(--sn-stylekit-border-color);
|
||||
|
||||
font-size: var(--sn-stylekit-font-size-h2);
|
||||
|
||||
width: 350px;
|
||||
flex-grow: 0;
|
||||
user-select: none;
|
||||
|
||||
@@ -71,6 +76,8 @@
|
||||
}
|
||||
|
||||
#search-clear-button {
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
|
||||
Reference in New Issue
Block a user