refactor: Move notes_view to React (#761)

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

View File

@@ -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

View File

@@ -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;

View 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>
);
}
);

View 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>
);
};

View File

@@ -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: '&',
}
);

View 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);

View 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>
);
}
);

View File

@@ -17,7 +17,7 @@ type Props = {
application: WebApplication;
};
const SearchOptions = observer(({ appState }: Props) => {
export const SearchOptions = observer(({ appState }: Props) => {
const { searchOptions } = appState;
const {

View File

@@ -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: '=',
};
}
}

View File

@@ -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) {

View File

@@ -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 = () => {

View 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();
};
}

View 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;
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;