Merge branch 'develop' into feature/remove-batch-manager

This commit is contained in:
Antonella Sgarlatta
2021-06-08 18:59:39 -03:00
committed by GitHub
24 changed files with 1026 additions and 371 deletions

View File

@@ -64,6 +64,7 @@ import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNot
import { NotesContextMenuDirective } from './components/NotesContextMenu';
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
import { IconDirective } from './components/Icon';
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
function reloadHiddenFirefoxTab(): boolean {
/**
@@ -159,7 +160,8 @@ const startApplication: StartApplication = async function startApplication(
.directive('multipleSelectedNotesPanel', MultipleSelectedNotesDirective)
.directive('notesContextMenu', NotesContextMenuDirective)
.directive('notesOptionsPanel', NotesOptionsPanelDirective)
.directive('icon', IconDirective);
.directive('icon', IconDirective)
.directive('noteTagsContainer', NoteTagsContainerDirective);
// Filters
angular.module('app').filter('trusted', ['$sce', trusted]);

View File

@@ -0,0 +1,79 @@
import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { useRef, useEffect } from 'preact/hooks';
import { Icon } from './Icon';
type Props = {
appState: AppState;
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
};
export const AutocompleteTagHint = observer(
({ appState, closeOnBlur }: Props) => {
const { autocompleteTagHintFocused } = appState.noteTags;
const hintRef = useRef<HTMLButtonElement>();
const { autocompleteSearchQuery, autocompleteTagResults } =
appState.noteTags;
const onTagHintClick = async () => {
await appState.noteTags.createAndAddNewTag();
};
const onFocus = () => {
appState.noteTags.setAutocompleteTagHintFocused(true);
};
const onBlur = (event: FocusEvent) => {
closeOnBlur(event);
appState.noteTags.setAutocompleteTagHintFocused(false);
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
if (autocompleteTagResults.length > 0) {
const lastTagResult =
autocompleteTagResults[autocompleteTagResults.length - 1];
appState.noteTags.setFocusedTagResultUuid(lastTagResult.uuid);
} else {
appState.noteTags.setAutocompleteInputFocused(true);
}
}
};
useEffect(() => {
if (autocompleteTagHintFocused) {
hintRef.current.focus();
}
}, [appState.noteTags, autocompleteTagHintFocused]);
return (
<>
{autocompleteTagResults.length > 0 && (
<div className="h-1px my-2 bg-border"></div>
)}
<button
ref={hintRef}
type="button"
className="sn-dropdown-item"
onClick={onTagHintClick}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
>
<span>Create new tag:</span>
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
<Icon
type="hashtag"
className="sn-icon--small color-neutral mr-1"
/>
<span className="max-w-40 whitespace-nowrap overflow-hidden overflow-ellipsis">
{autocompleteSearchQuery}
</span>
</span>
</button>
</>
);
}
);

View File

@@ -0,0 +1,140 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
import { useCloseOnBlur } from './utils';
import { AppState } from '@/ui_models/app_state';
import { AutocompleteTagResult } from './AutocompleteTagResult';
import { AutocompleteTagHint } from './AutocompleteTagHint';
import { observer } from 'mobx-react-lite';
type Props = {
appState: AppState;
};
export const AutocompleteTagInput = observer(({ appState }: Props) => {
const {
autocompleteInputFocused,
autocompleteSearchQuery,
autocompleteTagHintVisible,
autocompleteTagResults,
tags,
tagsContainerMaxWidth,
} = appState.noteTags;
const [dropdownVisible, setDropdownVisible] = useState(false);
const [dropdownMaxHeight, setDropdownMaxHeight] =
useState<number | 'auto'>('auto');
const dropdownRef = useRef<HTMLDivElement>();
const inputRef = useRef<HTMLInputElement>();
const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => {
setDropdownVisible(visible);
appState.noteTags.clearAutocompleteSearch();
});
const showDropdown = () => {
const { clientHeight } = document.documentElement;
const inputRect = inputRef.current.getBoundingClientRect();
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2);
setDropdownVisible(true);
};
const onSearchQueryChange = (event: Event) => {
const query = (event.target as HTMLInputElement).value;
appState.noteTags.setAutocompleteSearchQuery(query);
appState.noteTags.searchActiveNoteAutocompleteTags();
};
const onFormSubmit = async (event: Event) => {
event.preventDefault();
await appState.noteTags.createAndAddNewTag();
};
const onKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case 'Backspace':
case 'ArrowLeft':
if (autocompleteSearchQuery === '' && tags.length > 0) {
appState.noteTags.setFocusedTagUuid(tags[tags.length - 1].uuid);
}
break;
case 'ArrowDown':
event.preventDefault();
if (autocompleteTagResults.length > 0) {
appState.noteTags.setFocusedTagResultUuid(autocompleteTagResults[0].uuid);
} else if (autocompleteTagHintVisible) {
appState.noteTags.setAutocompleteTagHintFocused(true);
}
break;
default:
return;
}
};
const onFocus = () => {
showDropdown();
appState.noteTags.setAutocompleteInputFocused(true);
};
const onBlur = (event: FocusEvent) => {
closeOnBlur(event);
appState.noteTags.setAutocompleteInputFocused(false);
};
useEffect(() => {
appState.noteTags.searchActiveNoteAutocompleteTags();
}, [appState.noteTags]);
useEffect(() => {
if (autocompleteInputFocused) {
inputRef.current.focus();
appState.noteTags.setAutocompleteInputFocused(false);
}
}, [appState.noteTags, autocompleteInputFocused]);
return (
<form
onSubmit={onFormSubmit}
className={`${tags.length > 0 ? 'mt-2' : ''}`}
>
<Disclosure open={dropdownVisible} onChange={showDropdown}>
<input
ref={inputRef}
className={`${tags.length > 0 ? 'w-80' : 'w-70 mr-10'} bg-default text-xs
color-text no-border h-7 focus:outline-none focus:shadow-none focus:border-bottom`}
value={autocompleteSearchQuery}
onChange={onSearchQueryChange}
type="text"
placeholder="Add tag"
onBlur={onBlur}
onFocus={onFocus}
onKeyDown={onKeyDown}
/>
{dropdownVisible && (autocompleteTagResults.length > 0 || autocompleteTagHintVisible) && (
<DisclosurePanel
ref={dropdownRef}
className={`${tags.length > 0 ? 'w-80' : 'w-70 mr-10'} sn-dropdown flex flex-col py-2 absolute`}
style={{ maxHeight: dropdownMaxHeight, maxWidth: tagsContainerMaxWidth }}
>
<div className="overflow-y-scroll">
{autocompleteTagResults.map((tagResult) => (
<AutocompleteTagResult
key={tagResult.uuid}
appState={appState}
tagResult={tagResult}
closeOnBlur={closeOnBlur}
/>
))}
</div>
{autocompleteTagHintVisible && (
<AutocompleteTagHint
appState={appState}
closeOnBlur={closeOnBlur}
/>
)}
</DisclosurePanel>
)}
</Disclosure>
</form>
);
});

View File

@@ -0,0 +1,109 @@
import { AppState } from '@/ui_models/app_state';
import { SNTag } from '@standardnotes/snjs';
import { observer } from 'mobx-react-lite';
import { useEffect, useRef } from 'preact/hooks';
import { Icon } from './Icon';
type Props = {
appState: AppState;
tagResult: SNTag;
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
};
export const AutocompleteTagResult = observer(
({ appState, tagResult, closeOnBlur }: Props) => {
const {
autocompleteSearchQuery,
autocompleteTagHintVisible,
autocompleteTagResults,
focusedTagResultUuid,
} = appState.noteTags;
const tagResultRef = useRef<HTMLButtonElement>();
const onTagOptionClick = async (tag: SNTag) => {
await appState.noteTags.addTagToActiveNote(tag);
appState.noteTags.clearAutocompleteSearch();
appState.noteTags.setAutocompleteInputFocused(true);
};
const onKeyDown = (event: KeyboardEvent) => {
const tagResultIndex = appState.noteTags.getTagIndex(
tagResult,
autocompleteTagResults
);
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
if (tagResultIndex === 0) {
appState.noteTags.setAutocompleteInputFocused(true);
} else {
appState.noteTags.focusPreviousTagResult(tagResult);
}
break;
case 'ArrowDown':
event.preventDefault();
if (
tagResultIndex === autocompleteTagResults.length - 1 &&
autocompleteTagHintVisible
) {
appState.noteTags.setAutocompleteTagHintFocused(true);
} else {
appState.noteTags.focusNextTagResult(tagResult);
}
break;
default:
return;
}
};
const onFocus = () => {
appState.noteTags.setFocusedTagResultUuid(tagResult.uuid);
};
const onBlur = (event: FocusEvent) => {
closeOnBlur(event);
appState.noteTags.setFocusedTagResultUuid(undefined);
};
useEffect(() => {
if (focusedTagResultUuid === tagResult.uuid) {
tagResultRef.current.focus();
appState.noteTags.setFocusedTagResultUuid(undefined);
}
}, [appState.noteTags, focusedTagResultUuid, tagResult]);
return (
<button
ref={tagResultRef}
type="button"
className="sn-dropdown-item"
onClick={() => onTagOptionClick(tagResult)}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
>
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
{autocompleteSearchQuery === ''
? tagResult.title
: tagResult.title
.split(new RegExp(`(${autocompleteSearchQuery})`, 'gi'))
.map((substring, index) => (
<span
key={index}
className={`${
substring.toLowerCase() ===
autocompleteSearchQuery.toLowerCase()
? 'font-bold whitespace-pre-wrap'
: 'whitespace-pre-wrap '
}`}
>
{substring}
</span>
))}
</span>
</button>
);
}
);

View File

@@ -0,0 +1,112 @@
import { Icon } from './Icon';
import { useEffect, useRef, useState } from 'preact/hooks';
import { AppState } from '@/ui_models/app_state';
import { SNTag } from '@standardnotes/snjs/dist/@types';
import { observer } from 'mobx-react-lite';
type Props = {
appState: AppState;
tag: SNTag;
};
export const NoteTag = observer(({ appState, tag }: Props) => {
const { focusedTagUuid, tags } = appState.noteTags;
const [showDeleteButton, setShowDeleteButton] = useState(false);
const [tagClicked, setTagClicked] = useState(false);
const deleteTagRef = useRef<HTMLButtonElement>();
const tagRef = useRef<HTMLButtonElement>();
const deleteTag = () => {
appState.noteTags.focusPreviousTag(tag);
appState.noteTags.removeTagFromActiveNote(tag);
};
const onDeleteTagClick = (event: MouseEvent) => {
event.stopImmediatePropagation();
event.stopPropagation();
deleteTag();
};
const onTagClick = (event: MouseEvent) => {
if (tagClicked && event.target !== deleteTagRef.current) {
setTagClicked(false);
appState.setSelectedTag(tag);
} else {
setTagClicked(true);
}
};
const onFocus = () => {
appState.noteTags.setFocusedTagUuid(tag.uuid);
setShowDeleteButton(true);
};
const onBlur = (event: FocusEvent) => {
const relatedTarget = event.relatedTarget as Node;
if (relatedTarget !== deleteTagRef.current) {
appState.noteTags.setFocusedTagUuid(undefined);
setShowDeleteButton(false);
}
};
const onKeyDown = (event: KeyboardEvent) => {
const tagIndex = appState.noteTags.getTagIndex(tag, tags);
switch (event.key) {
case 'Backspace':
deleteTag();
break;
case 'ArrowLeft':
appState.noteTags.focusPreviousTag(tag);
break;
case 'ArrowRight':
if (tagIndex === tags.length - 1) {
appState.noteTags.setAutocompleteInputFocused(true);
} else {
appState.noteTags.focusNextTag(tag);
}
break;
default:
return;
}
};
useEffect(() => {
if (focusedTagUuid === tag.uuid) {
tagRef.current.focus();
appState.noteTags.setFocusedTagUuid(undefined);
}
}, [appState.noteTags, focusedTagUuid, tag]);
return (
<button
ref={tagRef}
className="sn-tag pl-1 pr-2 mr-2"
onClick={onTagClick}
onKeyDown={onKeyDown}
onFocus={onFocus}
onBlur={onBlur}
>
<Icon type="hashtag" className="sn-icon--small color-info mr-1" />
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis max-w-290px">
{tag.title}
</span>
{showDeleteButton && (
<button
ref={deleteTagRef}
type="button"
className="ml-2 -mr-1 border-0 p-0 bg-transparent cursor-pointer flex"
onFocus={onFocus}
onBlur={onBlur}
onClick={onDeleteTagClick}
>
<Icon
type="close"
className="sn-icon--small color-neutral hover:color-info"
/>
</button>
)}
</button>
);
});

View File

@@ -0,0 +1,41 @@
import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { toDirective } from './utils';
import { AutocompleteTagInput } from './AutocompleteTagInput';
import { NoteTag } from './NoteTag';
import { useEffect } from 'preact/hooks';
type Props = {
appState: AppState;
};
const NoteTagsContainer = observer(({ appState }: Props) => {
const {
tags,
tagsContainerMaxWidth,
} = appState.noteTags;
useEffect(() => {
appState.noteTags.reloadTagsContainerMaxWidth();
}, [appState.noteTags]);
return (
<div
className="bg-default flex flex-wrap min-w-80 -mt-1 -mr-2"
style={{
maxWidth: tagsContainerMaxWidth,
}}
>
{tags.map((tag) => (
<NoteTag
key={tag.uuid}
appState={appState}
tag={tag}
/>
))}
<AutocompleteTagInput appState={appState} />
</div>
);
});
export const NoteTagsContainerDirective = toDirective<Props>(NoteTagsContainer);

View File

@@ -1,8 +1,8 @@
import { AppState } from '@/ui_models/app_state';
import { toDirective, useCloseOnBlur } from './utils';
import { toDirective, useCloseOnBlur, useCloseOnClickOutside } from './utils';
import { observer } from 'mobx-react-lite';
import { NotesOptions } from './NotesOptions';
import { useCallback, useEffect, useRef } from 'preact/hooks';
import { useRef } from 'preact/hooks';
type Props = {
appState: AppState;
@@ -15,23 +15,15 @@ const NotesContextMenu = observer(({ appState }: Props) => {
(open: boolean) => appState.notes.setContextMenuOpen(open)
);
const closeOnClickOutside = useCallback((event: MouseEvent) => {
if (!contextMenuRef.current?.contains(event.target as Node)) {
appState.notes.setContextMenuOpen(false);
}
}, [appState]);
useEffect(() => {
document.addEventListener('click', closeOnClickOutside);
return () => {
document.removeEventListener('click', closeOnClickOutside);
};
}, [closeOnClickOutside]);
useCloseOnClickOutside(
contextMenuRef,
(open: boolean) => appState.notes.setContextMenuOpen(open)
);
return appState.notes.contextMenuOpen ? (
<div
ref={contextMenuRef}
className="sn-dropdown max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
className="sn-dropdown max-h-120 max-w-xs flex flex-col py-2 overflow-y-scroll fixed"
style={{
...appState.notes.contextMenuPosition,
maxHeight: appState.notes.contextMenuMaxHeight,

View File

@@ -52,10 +52,6 @@ export const NotesOptions = observer(
const tagsButtonRef = useRef<HTMLButtonElement>();
const iconClass = 'color-neutral mr-2';
const buttonClass =
'flex items-center border-0 focus:inner-ring-info ' +
'cursor-pointer hover:bg-contrast color-text bg-transparent px-3 ' +
'text-left';
useEffect(() => {
if (onSubmenuChange) {
@@ -136,14 +132,14 @@ export const NotesOptions = observer(
{appState.tags.tagsCount > 0 && (
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
<DisclosureButton
onKeyUp={(event) => {
onKeyDown={(event) => {
if (event.key === 'Escape') {
setTagsMenuOpen(false);
}
}}
onBlur={closeOnBlur}
ref={tagsButtonRef}
className={`${buttonClass} py-1.5 justify-between`}
className="sn-dropdown-item justify-between"
>
<div className="flex items-center">
<Icon type="hashtag" className={iconClass} />
@@ -152,7 +148,7 @@ export const NotesOptions = observer(
<Icon type="chevron-right" className="color-neutral" />
</DisclosureButton>
<DisclosurePanel
onKeyUp={(event) => {
onKeyDown={(event) => {
if (event.key === 'Escape') {
setTagsMenuOpen(false);
tagsButtonRef.current.focus();
@@ -163,12 +159,12 @@ export const NotesOptions = observer(
maxHeight: tagsMenuMaxHeight,
position: 'fixed',
}}
className="sn-dropdown flex flex-col py-2 max-h-120 max-w-80 fixed overflow-y-scroll"
className="sn-dropdown flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-scroll"
>
{appState.tags.tags.map((tag) => (
<button
key={tag.title}
className={`${buttonClass} py-2 max-w-80`}
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
onBlur={closeOnBlur}
onClick={() => {
appState.notes.isTagInSelectedNotes(tag)
@@ -194,7 +190,7 @@ export const NotesOptions = observer(
{unpinned && (
<button
onBlur={closeOnBlur}
className={`${buttonClass} py-1.5`}
className="sn-dropdown-item"
onClick={() => {
appState.notes.setPinSelectedNotes(true);
}}
@@ -206,7 +202,7 @@ export const NotesOptions = observer(
{pinned && (
<button
onBlur={closeOnBlur}
className={`${buttonClass} py-1.5`}
className="sn-dropdown-item"
onClick={() => {
appState.notes.setPinSelectedNotes(false);
}}
@@ -218,7 +214,7 @@ export const NotesOptions = observer(
{unarchived && (
<button
onBlur={closeOnBlur}
className={`${buttonClass} py-1.5`}
className="sn-dropdown-item"
onClick={() => {
appState.notes.setArchiveSelectedNotes(true);
}}
@@ -230,7 +226,7 @@ export const NotesOptions = observer(
{archived && (
<button
onBlur={closeOnBlur}
className={`${buttonClass} py-1.5`}
className="sn-dropdown-item"
onClick={() => {
appState.notes.setArchiveSelectedNotes(false);
}}
@@ -242,7 +238,7 @@ export const NotesOptions = observer(
{notTrashed && (
<button
onBlur={closeOnBlur}
className={`${buttonClass} py-1.5`}
className="sn-dropdown-item"
onClick={async () => {
await appState.notes.setTrashSelectedNotes(true);
}}
@@ -255,7 +251,7 @@ export const NotesOptions = observer(
<>
<button
onBlur={closeOnBlur}
className={`${buttonClass} py-1.5`}
className="sn-dropdown-item"
onClick={async () => {
await appState.notes.setTrashSelectedNotes(false);
}}
@@ -265,7 +261,7 @@ export const NotesOptions = observer(
</button>
<button
onBlur={closeOnBlur}
className={`${buttonClass} py-1.5`}
className="sn-dropdown-item"
onClick={async () => {
await appState.notes.deleteNotesPermanently();
}}
@@ -275,7 +271,7 @@ export const NotesOptions = observer(
</button>
<button
onBlur={closeOnBlur}
className={`${buttonClass} py-1.5`}
className="sn-dropdown-item"
onClick={async () => {
await appState.notes.emptyTrash();
}}

View File

@@ -47,7 +47,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
}}
>
<DisclosureButton
onKeyUp={(event) => {
onKeyDown={(event) => {
if (event.key === 'Escape' && !submenuOpen) {
setOpen(false);
}
@@ -59,28 +59,28 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
<VisuallyHidden>Actions</VisuallyHidden>
<Icon type="more" className="block" />
</DisclosureButton>
<DisclosurePanel
onKeyUp={(event) => {
if (event.key === 'Escape' && !submenuOpen) {
setOpen(false);
buttonRef.current.focus();
}
}}
ref={panelRef}
style={{
...position,
maxHeight
}}
className="sn-dropdown sn-dropdown--animated max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
>
{open && (
<NotesOptions
appState={appState}
closeOnBlur={closeOnBlur}
onSubmenuChange={onSubmenuChange}
/>
)}
</DisclosurePanel>
<DisclosurePanel
onKeyDown={(event) => {
if (event.key === 'Escape' && !submenuOpen) {
setOpen(false);
buttonRef.current.focus();
}
}}
ref={panelRef}
style={{
...position,
maxHeight,
}}
className="sn-dropdown sn-dropdown--animated max-h-120 max-w-xs flex flex-col py-2 overflow-y-scroll fixed"
>
{open && (
<NotesOptions
appState={appState}
closeOnBlur={closeOnBlur}
onSubmenuChange={onSubmenuChange}
/>
)}
</DisclosurePanel>
</Disclosure>
);
});

View File

@@ -42,7 +42,7 @@ export const Switch: FunctionalComponent<SwitchProps> = (
<span
aria-hidden
className={`sn-switch-handle ${
checked ? 'sn-switch-handle-right' : ''
checked ? 'sn-switch-handle--right' : ''
}`}
/>
</CustomCheckboxContainer>

View File

@@ -1,5 +1,6 @@
import { FunctionComponent, h, render } from 'preact';
import { StateUpdater, useCallback, useState } from 'preact/hooks';
import { useEffect } from 'react';
/**
* @returns a callback that will close a dropdown if none of its children has
@@ -30,6 +31,26 @@ export function useCloseOnBlur(
];
}
export function useCloseOnClickOutside(
container: { current: HTMLDivElement },
setOpen: (open: boolean) => void
): void {
const closeOnClickOutside = useCallback((event: { target: EventTarget | null }) => {
if (
!container.current?.contains(event.target as Node)
) {
setOpen(false);
}
}, [container, setOpen]);
useEffect(() => {
document.addEventListener('click', closeOnClickOutside);
return () => {
document.removeEventListener('click', closeOnClickOutside);
};
}, [closeOnClickOutside]);
}
export function toDirective<Props>(
component: FunctionComponent<Props>,
scope: Record<string, '=' | '&' | '@'> = {}

View File

@@ -38,6 +38,7 @@ interface PanelResizerScope {
index: number
minWidth: number
onResizeFinish: () => ResizeFinishCallback
onWidthEvent?: () => void
panelId: string
property: PanelSide
}
@@ -53,6 +54,7 @@ class PanelResizerCtrl implements PanelResizerScope {
index!: number
minWidth!: number
onResizeFinish!: () => ResizeFinishCallback
onWidthEvent?: () => () => void
panelId!: string
property!: PanelSide
@@ -102,6 +104,7 @@ class PanelResizerCtrl implements PanelResizerScope {
$onDestroy() {
(this.onResizeFinish as any) = undefined;
(this.onWidthEvent as any) = undefined;
(this.control as any) = undefined;
window.removeEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
document.removeEventListener(MouseEventType.Move, this.onMouseMove);
@@ -250,6 +253,9 @@ class PanelResizerCtrl implements PanelResizerScope {
}
handleWidthEvent(event?: MouseEvent) {
if (this.onWidthEvent && this.onWidthEvent()) {
this.onWidthEvent()();
}
let x;
if (event) {
x = event!.clientX;
@@ -387,6 +393,7 @@ export class PanelResizer extends WebDirective {
index: '=',
minWidth: '=',
onResizeFinish: '&',
onWidthEvent: '&',
panelId: '=',
property: '='
};

View File

@@ -16,6 +16,7 @@ import { Bridge } from '@/services/bridge';
import { storage, StorageKey } from '@/services/localStorage';
import { AccountMenuState } from './account_menu_state';
import { ActionsMenuState } from './actions_menu_state';
import { NoteTagsState } from './note_tags_state';
import { NoAccountWarningState } from './no_account_warning_state';
import { SyncState } from './sync_state';
import { SearchOptionsState } from './search_options_state';
@@ -63,6 +64,7 @@ export class AppState {
readonly accountMenu = new AccountMenuState();
readonly actionsMenu = new ActionsMenuState();
readonly noAccountWarning: NoAccountWarningState;
readonly noteTags: NoteTagsState;
readonly sync = new SyncState();
readonly searchOptions: SearchOptionsState;
readonly notes: NotesState;
@@ -82,12 +84,18 @@ export class AppState {
this.$rootScope = $rootScope;
this.application = application;
this.notes = new NotesState(
this.application,
application,
this,
async () => {
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
},
this.appEventObserverRemovers,
);
this.noteTags = new NoteTagsState(
application,
this,
this.appEventObserverRemovers
);
this.tags = new TagsState(
application,
this.appEventObserverRemovers,

View File

@@ -0,0 +1,208 @@
import { SNNote, ContentType, SNTag, UuidString } from '@standardnotes/snjs';
import { action, computed, makeObservable, observable } from 'mobx';
import { WebApplication } from '../application';
import { AppState } from './app_state';
export class NoteTagsState {
autocompleteInputFocused = false;
autocompleteSearchQuery = '';
autocompleteTagHintFocused = false;
autocompleteTagResults: SNTag[] = [];
focusedTagResultUuid: UuidString | undefined = undefined;
focusedTagUuid: UuidString | undefined = undefined;
tags: SNTag[] = [];
tagsContainerMaxWidth: number | 'auto' = 0;
constructor(
private application: WebApplication,
private appState: AppState,
appEventListeners: (() => void)[]
) {
makeObservable(this, {
autocompleteInputFocused: observable,
autocompleteSearchQuery: observable,
autocompleteTagHintFocused: observable,
autocompleteTagResults: observable,
focusedTagUuid: observable,
focusedTagResultUuid: observable,
tags: observable,
tagsContainerMaxWidth: observable,
autocompleteTagHintVisible: computed,
clearAutocompleteSearch: action,
focusNextTag: action,
focusPreviousTag: action,
setAutocompleteInputFocused: action,
setAutocompleteSearchQuery: action,
setAutocompleteTagHintFocused: action,
setAutocompleteTagResults: action,
setFocusedTagResultUuid: action,
setFocusedTagUuid: action,
setTags: action,
setTagsContainerMaxWidth: action,
reloadTags: action,
});
appEventListeners.push(
application.streamItems(ContentType.Tag, () => {
this.reloadTags();
})
);
}
get activeNote(): SNNote | undefined {
return this.appState.notes.activeEditor?.note;
}
get autocompleteTagHintVisible(): boolean {
return (
this.autocompleteSearchQuery !== '' &&
!this.autocompleteTagResults.some(
(tagResult) => tagResult.title === this.autocompleteSearchQuery
)
);
}
setAutocompleteInputFocused(focused: boolean): void {
this.autocompleteInputFocused = focused;
}
setAutocompleteSearchQuery(query: string): void {
this.autocompleteSearchQuery = query;
}
setAutocompleteTagHintFocused(focused: boolean): void {
this.autocompleteTagHintFocused = focused;
}
setAutocompleteTagResults(results: SNTag[]): void {
this.autocompleteTagResults = results;
}
setFocusedTagUuid(tagUuid: UuidString | undefined): void {
this.focusedTagUuid = tagUuid;
}
setFocusedTagResultUuid(tagUuid: UuidString | undefined): void {
this.focusedTagResultUuid = tagUuid;
}
setTags(tags: SNTag[]): void {
this.tags = tags;
}
setTagsContainerMaxWidth(width: number): void {
this.tagsContainerMaxWidth = width;
}
clearAutocompleteSearch(): void {
this.setAutocompleteSearchQuery('');
this.searchActiveNoteAutocompleteTags();
}
async createAndAddNewTag(): Promise<void> {
const newTag = await this.application.findOrCreateTag(
this.autocompleteSearchQuery
);
await this.addTagToActiveNote(newTag);
this.clearAutocompleteSearch();
}
focusNextTag(tag: SNTag): void {
const nextTagIndex = this.getTagIndex(tag, this.tags) + 1;
if (nextTagIndex > -1 && this.tags.length > nextTagIndex) {
const nextTag = this.tags[nextTagIndex];
this.setFocusedTagUuid(nextTag.uuid);
}
}
focusNextTagResult(tagResult: SNTag): void {
const nextTagResultIndex =
this.getTagIndex(tagResult, this.autocompleteTagResults) + 1;
if (
nextTagResultIndex > -1 &&
this.autocompleteTagResults.length > nextTagResultIndex
) {
const nextTagResult = this.autocompleteTagResults[nextTagResultIndex];
this.setFocusedTagResultUuid(nextTagResult.uuid);
}
}
focusPreviousTag(tag: SNTag): void {
const previousTagIndex = this.getTagIndex(tag, this.tags) - 1;
if (previousTagIndex > -1 && this.tags.length > previousTagIndex) {
const previousTag = this.tags[previousTagIndex];
this.setFocusedTagUuid(previousTag.uuid);
}
}
focusPreviousTagResult(tagResult: SNTag): void {
const previousTagResultIndex =
this.getTagIndex(tagResult, this.autocompleteTagResults) - 1;
if (
previousTagResultIndex > -1 &&
this.autocompleteTagResults.length > previousTagResultIndex
) {
const previousTagResult =
this.autocompleteTagResults[previousTagResultIndex];
this.setFocusedTagResultUuid(previousTagResult.uuid);
}
}
searchActiveNoteAutocompleteTags(): void {
const newResults = this.application.searchTags(
this.autocompleteSearchQuery,
this.activeNote
);
this.setAutocompleteTagResults(newResults);
}
getTagIndex(tag: SNTag, tagsArr: SNTag[]): number {
return tagsArr.findIndex((t) => t.uuid === tag.uuid);
}
reloadTags(): void {
const { activeNote } = this;
if (activeNote) {
const tags = this.application.getSortedTagsForNote(activeNote);
this.setTags(tags);
}
}
reloadTagsContainerMaxWidth(): void {
const EDITOR_ELEMENT_ID = 'editor-column';
const editorWidth = document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth;
if (editorWidth) {
this.setTagsContainerMaxWidth(editorWidth);
}
}
async addTagToActiveNote(tag: SNTag): Promise<void> {
const { activeNote } = this;
if (activeNote) {
const parentChainTags = this.application.getTagParentChain(tag);
const tagsToAdd = [...parentChainTags, tag];
await Promise.all(
tagsToAdd.map(async (tag) => {
await this.application.changeItem(tag.uuid, (mutator) => {
mutator.addItemAsRelationship(activeNote);
});
})
);
this.application.sync();
this.reloadTags();
}
}
async removeTagFromActiveNote(tag: SNTag): Promise<void> {
const { activeNote } = this;
if (activeNote) {
await this.application.changeItem(tag.uuid, (mutator) => {
mutator.removeItemAsRelationship(activeNote);
});
this.application.sync();
this.reloadTags();
}
}
}

View File

@@ -18,6 +18,7 @@ import {
} from 'mobx';
import { WebApplication } from '../application';
import { Editor } from '../editor';
import { AppState } from './app_state';
export class NotesState {
lastSelectedNote: SNNote | undefined;
@@ -32,6 +33,7 @@ export class NotesState {
constructor(
private application: WebApplication,
private appState: AppState,
private onActiveEditorChanged: () => Promise<void>,
appEventListeners: (() => void)[]
) {
@@ -168,6 +170,8 @@ export class NotesState {
} else {
this.activeEditor.setNote(note);
}
this.appState.noteTags.reloadTags();
await this.onActiveEditorChanged();
if (note.waitingForKey) {
@@ -326,11 +330,17 @@ export class NotesState {
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
const selectedNotes = Object.values(this.selectedNotes);
await this.application.changeItem(tag.uuid, (mutator) => {
for (const note of selectedNotes) {
mutator.addItemAsRelationship(note);
}
});
const parentChainTags = this.application.getTagParentChain(tag);
const tagsToAdd = [...parentChainTags, tag];
await Promise.all(
tagsToAdd.map(async (tag) => {
await this.application.changeItem(tag.uuid, (mutator) => {
for (const note of selectedNotes) {
mutator.addItemAsRelationship(note);
}
});
})
);
this.application.sync();
}
@@ -342,13 +352,13 @@ export class NotesState {
}
});
this.application.sync();
}
isTagInSelectedNotes(tag: SNTag): boolean {
const selectedNotes = Object.values(this.selectedNotes);
return selectedNotes.every((note) =>
this.application
.getAppState()
this.appState
.getNoteTags(note)
.find((noteTag) => noteTag.uuid === tag.uuid)
);

View File

@@ -24,50 +24,37 @@
ng-if="self.showLockedIcon"
)
| {{self.lockText}}
#editor-title-bar.section-title-bar.flex.items-center.justify-between.w-full(
#editor-title-bar.section-title-bar.w-full(
ng-show='self.note && !self.note.errorDecrypting'
)
div.flex-grow(
ng-class="{'locked' : self.noteLocked}"
)
.title
input#note-title-editor.input(
ng-blur='self.onTitleBlur()',
ng-change='self.onTitleChange()',
ng-disabled='self.noteLocked',
ng-focus='self.onTitleFocus()',
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
ng-model='self.editorValues.title',
select-on-focus='true',
spellcheck='false'
)
.editor-tags
#note-tags-component-container(ng-if='self.state.tagsComponent && !self.note.errorDecrypting')
component-view.component-view(
component-uuid='self.state.tagsComponent.uuid',
ng-style="self.notesLocked && {'pointer-events' : 'none'}",
application='self.application'
)
input.tags-input(
ng-blur='self.onTagsInputBlur()',
ng-disabled='self.noteLocked',
ng-if='!self.state.tagsComponent',
ng-keyup='$event.keyCode == 13 && $event.target.blur();',
ng-model='self.editorValues.tagsInputValue',
placeholder='#tags',
spellcheck='false',
type='text'
)
div.flex.items-center
#save-status
.message(
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
) {{self.state.noteStatus.message}}
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
notes-options-panel(
app-state='self.appState',
ng-if='self.appState.notes.selectedNotesCount > 0'
div.flex.items-center.justify-between.h-8
div.flex-grow(
ng-class="{'locked' : self.noteLocked}"
)
.title.overflow-auto
input#note-title-editor.input(
ng-blur='self.onTitleBlur()',
ng-change='self.onTitleChange()',
ng-disabled='self.noteLocked',
ng-focus='self.onTitleFocus()',
ng-keyup='$event.keyCode == 13 && self.onTitleEnter($event)',
ng-model='self.editorValues.title',
select-on-focus='true',
spellcheck='false'
)
div.flex.items-center
#save-status
.message(
ng-class="{'warning sk-bold': self.state.syncTakingTooLong, 'danger sk-bold': self.state.saveError}"
) {{self.state.noteStatus.message}}
.desc(ng-show='self.state.noteStatus.desc') {{self.state.noteStatus.desc}}
notes-options-panel(
app-state='self.appState',
ng-if='self.appState.notes.selectedNotesCount > 0'
)
note-tags-container(
app-state='self.appState'
)
.sn-component(ng-if='self.note')
#editor-menu-bar.sk-app-bar.no-edges
.left

View File

@@ -1,7 +1,5 @@
import {
STRING_ARCHIVE_LOCKED_ATTEMPT,
STRING_SAVING_WHILE_DOCUMENT_HIDDEN,
STRING_UNARCHIVE_LOCKED_ATTEMPT,
} from './../../strings';
import { Editor } from '@/ui_models/editor';
import { WebApplication } from '@/ui_models/application';
@@ -14,15 +12,12 @@ import {
ContentType,
SNComponent,
SNNote,
SNTag,
NoteMutator,
Uuids,
ComponentArea,
ComponentAction,
PrefKey,
ComponentMutator,
} from '@standardnotes/snjs';
import find from 'lodash/find';
import { isDesktopApplication } from '@/utils';
import { KeyboardModifier, KeyboardKey } from '@/services/ioService';
import template from './editor-view.pug';
@@ -36,9 +31,8 @@ import {
STRING_DELETE_LOCKED_ATTEMPT,
STRING_EDIT_LOCKED_ATTEMPT,
StringDeleteNote,
StringEmptyTrash,
} from '@/strings';
import { alertDialog, confirmDialog } from '@/services/alertService';
import { confirmDialog } from '@/services/alertService';
const NOTE_PREVIEW_CHAR_LIMIT = 80;
const MINIMUM_STATUS_DURATION = 400;
@@ -50,7 +44,6 @@ const ElementIds = {
NoteTextEditor: 'note-text-editor',
NoteTitleEditor: 'note-title-editor',
EditorContent: 'editor-content',
NoteTagsComponentContainer: 'note-tags-component-container',
};
type NoteStatus = {
@@ -61,10 +54,8 @@ type NoteStatus = {
type EditorState = {
stackComponents: SNComponent[];
editorComponent?: SNComponent;
tagsComponent?: SNComponent;
saveError?: any;
noteStatus?: NoteStatus;
tagsAsStrings?: string;
marginResizersEnabled?: boolean;
monospaceFont?: boolean;
isDesktop?: boolean;
@@ -83,14 +74,11 @@ type EditorState = {
/** Setting to true then false will allow the main content textarea to be destroyed
* then re-initialized. Used when reloading spellcheck status. */
textareaUnloading: boolean;
/** Fields that can be directly mutated by the template */
mutable: any;
};
type EditorValues = {
title: string;
text: string;
tagsInputValue?: string;
};
function copyEditorValues(values: EditorValues) {
@@ -117,13 +105,10 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
public editorValues: EditorValues = { title: '', text: '' };
onEditorLoad?: () => void;
private tags: SNTag[] = [];
private removeAltKeyObserver?: any;
private removeTrashKeyObserver?: any;
private removeTabObserver?: any;
private removeTagsObserver!: () => void;
private removeComponentsObserver!: () => void;
prefKeyMonospace: string;
@@ -153,9 +138,7 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
deinit() {
this.editor.clearNoteChangeListener();
this.removeTagsObserver();
this.removeComponentsObserver();
(this.removeTagsObserver as any) = undefined;
(this.removeComponentsObserver as any) = undefined;
this.removeAltKeyObserver();
this.removeAltKeyObserver = undefined;
@@ -172,7 +155,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
this.statusTimeout = undefined;
(this.onPanelResizeFinish as any) = undefined;
(this.editorMenuOnSelect as any) = undefined;
this.tags = [];
super.deinit();
}
@@ -194,7 +176,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
if (isPayloadSourceRetrieved(source!)) {
this.editorValues.title = note.title;
this.editorValues.text = note.text;
this.reloadTags();
}
if (!this.editorValues.title) {
this.editorValues.title = note.title;
@@ -237,9 +218,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
noteStatus: undefined,
editorUnloading: false,
textareaUnloading: false,
mutable: {
tagsString: '',
},
} as EditorState;
}
@@ -302,10 +280,8 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
this.editorValues.title = note.title;
this.editorValues.text = note.text;
this.reloadEditor();
this.reloadTags();
this.reloadPreferences();
this.reloadStackComponents();
this.reloadNoteTagsComponent();
if (note.dirty) {
this.showSavingStatus();
}
@@ -335,13 +311,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
}
streamItems() {
this.removeTagsObserver = this.application.streamItems(
ContentType.Tag,
() => {
this.reloadTags();
}
);
this.removeComponentsObserver = this.application.streamItems(
ContentType.Component,
async (_items, source) => {
@@ -350,7 +319,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
}
if (!this.note) return;
this.reloadStackComponents();
this.reloadNoteTagsComponent();
this.reloadEditor();
}
);
@@ -487,7 +455,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
const title = editorValues.title;
const text = editorValues.text;
const isTemplate = this.editor.isTemplateNote;
const selectedTag = this.appState.selectedTag;
if (document.hidden) {
this.application.alertService.alert(STRING_SAVING_WHILE_DOCUMENT_HIDDEN);
return;
@@ -499,14 +466,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
if (isTemplate) {
await this.editor.insertTemplatedNote();
}
if (
!selectedTag?.isSmartTag &&
!selectedTag?.hasRelationshipWithItem(note)
) {
await this.application.changeItem(selectedTag!.uuid, (mutator) => {
mutator.addItemAsRelationship(note);
});
}
if (!this.application.findItem(note.uuid)) {
this.application.alertService.alert(STRING_INVALID_NOTE);
return;
@@ -697,105 +656,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
this.application.deleteItem(note);
}
async reloadTags() {
if (!this.note) {
return;
}
const tags = this.appState.getNoteTags(this.note);
if (tags.length !== this.tags.length) {
this.reloadTagsString(tags);
} else {
/** Check that all tags are the same */
for (let i = 0; i < tags.length; i++) {
const localTag = this.tags[i];
const tag = tags[i];
if (tag.title !== localTag.title || tag.uuid !== localTag.uuid) {
this.reloadTagsString(tags);
break;
}
}
}
this.tags = tags;
}
private async reloadTagsString(tags: SNTag[]) {
const string = SNTag.arrayToDisplayString(tags);
await this.flushUI();
this.editorValues.tagsInputValue = string;
}
private addTag(tag: SNTag) {
const tags = this.appState.getNoteTags(this.note);
const strings = tags.map((currentTag) => {
return currentTag.title;
});
strings.push(tag.title);
this.saveTagsFromStrings(strings);
}
removeTag(tag: SNTag) {
const tags = this.appState.getNoteTags(this.note);
const strings = tags
.map((currentTag) => {
return currentTag.title;
})
.filter((title) => {
return title !== tag.title;
});
this.saveTagsFromStrings(strings);
}
onTagsInputBlur() {
this.saveTagsFromStrings();
this.focusEditor();
}
public async saveTagsFromStrings(strings?: string[]) {
if (
!strings &&
this.editorValues.tagsInputValue === this.state.tagsAsStrings
) {
return;
}
if (!strings) {
strings = this.editorValues
.tagsInputValue!.split('#')
.filter((string) => {
return string.length > 0;
})
.map((string) => {
return string.trim();
});
}
const note = this.note;
const currentTags = this.appState.getNoteTags(note);
const removeTags = [];
for (const tag of currentTags) {
if (strings.indexOf(tag.title) === -1) {
removeTags.push(tag);
}
}
for (const tag of removeTags) {
await this.application.changeItem(tag.uuid, (mutator) => {
mutator.removeItemAsRelationship(note);
});
}
const newRelationships: SNTag[] = [];
for (const title of strings) {
const existingRelationship = find(currentTags, { title: title });
if (!existingRelationship) {
newRelationships.push(await this.application.findOrCreateTag(title));
}
}
if (newRelationships.length > 0) {
await this.application.changeItems(Uuids(newRelationships), (mutator) => {
mutator.addItemAsRelationship(note);
});
}
this.application.sync();
this.reloadTags();
}
async onPanelResizeFinish(width: number, left: number, isMaxWidth: boolean) {
if (isMaxWidth) {
await this.application.setPreference(PrefKey.EditorWidth, null);
@@ -900,7 +760,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
{
identifier: 'editor',
areas: [
ComponentArea.NoteTags,
ComponentArea.EditorStack,
ComponentArea.Editor,
],
@@ -908,7 +767,6 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
const currentEditor = this.state.editorComponent;
if (
componentUuid === currentEditor?.uuid ||
componentUuid === this.state.tagsComponent?.uuid ||
Uuids(this.state.stackComponents).includes(componentUuid)
) {
return this.note;
@@ -919,78 +777,10 @@ class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
this.closeAllMenus();
}
},
actionHandler: (component, action, data) => {
if (action === ComponentAction.SetSize) {
const setSize = (
element: HTMLElement,
size: { width: string | number; height: string | number }
) => {
const widthString =
typeof size.width === 'string' ? size.width : `${data.width}px`;
const heightString =
typeof size.height === 'string'
? size.height
: `${data.height}px`;
element.setAttribute(
'style',
`width: ${widthString}; height: ${heightString};`
);
};
if (data.type === 'container') {
if (component.area === ComponentArea.NoteTags) {
const container = document.getElementById(
ElementIds.NoteTagsComponentContainer
);
setSize(container!, {
width: data.width!,
height: data.height!,
});
}
}
} else if (action === ComponentAction.AssociateItem) {
if (data.item!.content_type === ContentType.Tag) {
const tag = this.application.findItem(data.item!.uuid) as SNTag;
this.addTag(tag);
}
} else if (action === ComponentAction.DeassociateItem) {
const tag = this.application.findItem(data.item!.uuid) as SNTag;
this.removeTag(tag);
} else if (action === ComponentAction.SaveSuccess) {
const savedUuid = data.item ? data.item.uuid : data.items![0].uuid;
if (savedUuid === this.note.uuid) {
const selectedTag = this.appState.selectedTag;
if (
!selectedTag?.isSmartTag &&
!selectedTag?.hasRelationshipWithItem(this.note)
) {
this.application.changeAndSaveItem(
selectedTag!.uuid,
(mutator) => {
mutator.addItemAsRelationship(this.note);
}
);
}
}
}
},
}
);
}
async reloadNoteTagsComponent() {
const [
tagsComponent,
] = this.application.componentManager!.componentsForArea(
ComponentArea.NoteTags
);
await this.setState({
tagsComponent: tagsComponent?.active ? tagsComponent : undefined,
});
this.application.componentManager!.contextItemDidChangeInArea(
ComponentArea.NoteTags
);
}
async reloadStackComponents() {
const stackComponents = sortAlphabetically(
this.application

View File

@@ -1,4 +1,3 @@
import { WebApplication } from '@/ui_models/application';
import { WebDirective } from './../../types';
import template from './editor-group-view.pug';
import { Editor } from '@/ui_models/editor';
@@ -15,7 +14,7 @@ class EditorGroupViewCtrl extends PureViewCtrl<unknown, {
super($timeout);
this.state = {
showMultipleSelectedNotes: false
}
};
}
$onInit() {

View File

@@ -169,5 +169,6 @@
default-width="300"
hoverable="true"
on-resize-finish="self.onPanelResize"
on-width-event="self.onPanelWidthEvent"
panel-id="'notes-column'"
)

View File

@@ -93,6 +93,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
};
this.onWindowResize = this.onWindowResize.bind(this);
this.onPanelResize = this.onPanelResize.bind(this);
this.onPanelWidthEvent = this.onPanelWidthEvent.bind(this);
window.addEventListener('resize', this.onWindowResize, true);
this.registerKeyboardShortcuts();
this.autorun(async () => {
@@ -133,6 +134,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
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();
@@ -408,6 +410,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
await this.appState.createEditor(title);
await this.flushUI();
await this.reloadNotes();
await this.appState.noteTags.reloadTags();
}
async handleTagChange(tag: SNTag) {
@@ -642,10 +645,11 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
onPanelResize(
newWidth: number,
_: number,
newLeft: number,
__: boolean,
isCollapsed: boolean
) {
this.appState.noteTags.reloadTagsContainerMaxWidth();
this.application.setPreference(
PrefKey.NotesPanelWidth,
newWidth
@@ -656,6 +660,10 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
);
}
onPanelWidthEvent(): void {
this.appState.noteTags.reloadTagsContainerMaxWidth();
}
paginate() {
this.notesToDisplay += this.pageSize;
this.reloadNotes();

View File

@@ -86,34 +86,6 @@ $heading-height: 75px;
width: 100%;
overflow: visible;
position: relative;
#note-tags-component-container {
height: 50px;
overflow: auto; // Required for expired sub to not overflow
.component-view {
// see comment under main .component-view css defintion
position: inherit;
}
iframe {
height: 50px;
width: 100%;
position: absolute; // Required for autocomplete window to show
}
}
.tags-input {
background-color: transparent;
color: var(--sn-stylekit-foreground-color);
width: 100%;
border: none;
&:focus {
outline: 0;
box-shadow: none;
}
}
}
}

View File

@@ -40,6 +40,10 @@
border-color: var(--sn-stylekit-background-color);
}
.focus\:border-bottom:focus {
border-bottom: 2px solid var(--sn-stylekit-info-color);
}
.grid {
display: grid;
}
@@ -53,7 +57,54 @@
margin-bottom: 0.5rem;
}
.py-1\.5 {
.ml-1 {
margin-left: 0.25rem;
}
.mr-1 {
margin-right: 0.25rem;
}
.mr-10 {
margin-right: 2.5rem;
}
.-mt-1 {
margin-top: -0.25rem;
}
.-mr-1 {
margin-right: -0.25rem;
}
.-mr-2 {
margin-right: -0.5rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.pl-1 {
padding-left: 0.25rem;
}
.pr-2 {
padding-right: 0.5rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.py-1\.5 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
@@ -70,6 +121,10 @@
color: var(--sn-stylekit-danger-color);
}
.color-info {
color: var(--sn-stylekit-info-color);
}
.ring-info {
box-shadow: 0 0 0 2px var(--sn-stylekit-info-color);
}
@@ -90,6 +145,14 @@
@extend .color-text;
}
.hover\:bg-secondary-contrast:hover {
@extend .bg-secondary-contrast;
}
.focus\:bg-secondary-contrast:focus {
@extend .bg-secondary-contrast;
}
.focus\:inner-ring-info:focus {
@extend .inner-ring-info;
}
@@ -111,6 +174,10 @@
line-height: 2.25rem;
}
.w-0 {
width: 0;
}
.w-3\.5 {
width: 0.875rem;
}
@@ -123,18 +190,34 @@
width: 2rem;
}
.max-w-60 {
max-width: 15rem;
.max-w-290px {
max-width: 290px;
}
.max-w-80 {
.max-w-xs {
max-width: 20rem;
}
.max-w-40 {
max-width: 10rem;
}
.min-w-5 {
min-width: 1.25rem;
}
.min-w-40 {
min-width: 10rem;
}
.h-1px {
height: 1px;
}
.h-0 {
height: 0;
}
.h-3\.5 {
height: 0.875rem;
}
@@ -147,18 +230,38 @@
height: 1.25rem;
}
.h-6 {
height: 1.5rem;
}
.h-7 {
height: 1.75rem;
}
.h-8 {
height: 2rem;
}
.h-9 {
height: 2.25rem;
}
.h-10 {
height: 2.5rem;
}
.h-18 {
height: 4.5rem;
}
.max-h-120 {
max-height: 30rem;
}
.min-h-5 {
min-height: 1.25rem;
}
.fixed {
position: fixed;
}
@@ -167,12 +270,8 @@
overflow-y: scroll;
}
.items-start {
align-items: flex-start;
}
.whitespace-nowrap {
white-space: nowrap;
.overflow-auto {
overflow: auto;
}
.overflow-hidden {
@@ -183,6 +282,34 @@
text-overflow: ellipsis;
}
.items-start {
align-items: flex-start;
}
.p-2 {
padding: 0.5rem;
}
.flex-wrap {
flex-wrap: wrap;
}
.whitespace-pre-wrap {
white-space: pre-wrap;
}
.whitespace-nowrap {
white-space: nowrap;
}
.w-80 {
width: 20rem;
}
.w-70 {
width: 17.5rem;
}
/**
* A button that is just an icon. Separated from .sn-button because there
* is almost no style overlap.
@@ -215,11 +342,16 @@
@extend .h-5;
@extend .w-5;
@extend .fill-current;
&.sn-icon--small {
@extend .h-3\.5 ;
@extend .w-3\.5 ;
}
}
.sn-dropdown {
@extend .bg-default;
@extend .min-w-80;
// @extend .min-w-80;
@extend .rounded;
@extend .box-shadow;
@@ -238,6 +370,10 @@
@extend .duration-150;
@extend .slide-down-animation;
}
&.sn-dropdown--small {
@extend .min-w-40;
}
}
/** Lesser specificity will give priority to reach's styles */
@@ -275,7 +411,7 @@
top: 50%;
transform: translate(0px, -50%);
&.sn-switch-handle-right {
&.sn-switch-handle--right {
transform: translate(
calc(2rem - 1.125rem),
-50%
@@ -286,3 +422,40 @@
.sn-component .sk-app-bar .sk-app-bar-item {
justify-content: flex-start;
}
.sn-dropdown-item {
@extend .flex;
@extend .items-center;
@extend .border-0;
@extend .focus\:inner-ring-info;
@extend .cursor-pointer;
@extend .hover\:bg-contrast;
@extend .color-text;
@extend .bg-transparent;
@extend .px-3;
@extend .py-1\.5;
@extend .text-left;
@extend .w-full;
&.sn-dropdown-item--no-icon {
@extend .py-2;
}
}
.sn-tag {
@extend .h-6;
@extend .bg-contrast;
@extend .border-0;
@extend .rounded;
@extend .text-xs;
@extend .color-text;
@extend .py-1;
@extend .py-2;
@extend .pr-2;
@extend .flex;
@extend .items-center;
@extend .mt-2;
@extend .cursor-pointer;
@extend .hover\:bg-secondary-contrast;
@extend .focus\:bg-secondary-contrast;
}