diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 16b54ebf4..9cc5b8d46 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -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]); diff --git a/app/assets/javascripts/components/AutocompleteTagHint.tsx b/app/assets/javascripts/components/AutocompleteTagHint.tsx new file mode 100644 index 000000000..d4bfd6c6c --- /dev/null +++ b/app/assets/javascripts/components/AutocompleteTagHint.tsx @@ -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(); + + 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 && ( +
+ )} + + + ); + } +); diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx new file mode 100644 index 000000000..e7d242798 --- /dev/null +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -0,0 +1,143 @@ +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('auto'); + + const dropdownRef = useRef(); + const inputRef = useRef(); + + 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(); + if (autocompleteSearchQuery !== '') { + 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 ( +
0 ? 'mt-2' : ''}`} + > + + 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) && ( + 0 ? 'w-80' : 'w-70 mr-10'} sn-dropdown flex flex-col py-2 absolute`} + style={{ maxHeight: dropdownMaxHeight, maxWidth: tagsContainerMaxWidth }} + onBlur={closeOnBlur} + > +
+ {autocompleteTagResults.map((tagResult) => ( + + ))} +
+ {autocompleteTagHintVisible && ( + + )} +
+ )} +
+
+ ); +}); diff --git a/app/assets/javascripts/components/AutocompleteTagResult.tsx b/app/assets/javascripts/components/AutocompleteTagResult.tsx new file mode 100644 index 000000000..0a7cf9cb8 --- /dev/null +++ b/app/assets/javascripts/components/AutocompleteTagResult.tsx @@ -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(); + + 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 ( + + ); + } +); diff --git a/app/assets/javascripts/components/ConfirmSignoutModal.tsx b/app/assets/javascripts/components/ConfirmSignoutModal.tsx index b432caf83..6d1b9ef8f 100644 --- a/app/assets/javascripts/components/ConfirmSignoutModal.tsx +++ b/app/assets/javascripts/components/ConfirmSignoutModal.tsx @@ -23,9 +23,7 @@ const ConfirmSignoutContainer = observer((props: Props) => { }); const ConfirmSignoutModal = observer(({ application, appState }: Props) => { - const [deleteLocalBackups, setDeleteLocalBackups] = useState( - application.hasAccount() - ); + const [deleteLocalBackups, setDeleteLocalBackups] = useState(false); const cancelRef = useRef(); function close() { diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx new file mode 100644 index 000000000..3afcc3d35 --- /dev/null +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -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(); + + const tagRef = useRef(); + + 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 ( + + )} + + ); +}); diff --git a/app/assets/javascripts/components/NoteTagsContainer.tsx b/app/assets/javascripts/components/NoteTagsContainer.tsx new file mode 100644 index 000000000..8d94cdf42 --- /dev/null +++ b/app/assets/javascripts/components/NoteTagsContainer.tsx @@ -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 ( +
+ {tags.map((tag) => ( + + ))} + +
+ ); +}); + +export const NoteTagsContainerDirective = toDirective(NoteTagsContainer); diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index b215f0463..abd4fa354 100644 --- a/app/assets/javascripts/components/NotesContextMenu.tsx +++ b/app/assets/javascripts/components/NotesContextMenu.tsx @@ -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 ? (
(); 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 && ( { + 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" >
@@ -152,7 +148,7 @@ export const NotesOptions = observer( { + 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 min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto" > {appState.tags.tags.map((tag) => (
-
+
{refreshing ? ( <>
diff --git a/app/assets/javascripts/components/Switch.tsx b/app/assets/javascripts/components/Switch.tsx index 89bb8574c..c504df77c 100644 --- a/app/assets/javascripts/components/Switch.tsx +++ b/app/assets/javascripts/components/Switch.tsx @@ -42,7 +42,7 @@ export const Switch: FunctionalComponent = ( diff --git a/app/assets/javascripts/components/utils.ts b/app/assets/javascripts/components/utils.ts index 13e025192..fd78955f8 100644 --- a/app/assets/javascripts/components/utils.ts +++ b/app/assets/javascripts/components/utils.ts @@ -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( component: FunctionComponent, scope: Record = {} diff --git a/app/assets/javascripts/directives/views/panelResizer.ts b/app/assets/javascripts/directives/views/panelResizer.ts index c39f40743..791e3fcb9 100644 --- a/app/assets/javascripts/directives/views/panelResizer.ts +++ b/app/assets/javascripts/directives/views/panelResizer.ts @@ -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: '=' }; diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index ba8c7ea11..c57d88fb3 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -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, diff --git a/app/assets/javascripts/ui_models/app_state/note_tags_state.ts b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts new file mode 100644 index 000000000..f263fcb7d --- /dev/null +++ b/app/assets/javascripts/ui_models/app_state/note_tags_state.ts @@ -0,0 +1,214 @@ +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 { + 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 { + 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 { + const { activeNote } = this; + if (activeNote) { + const descendantTags = this.application.getTagDescendants(tag); + const tagsToRemove = [...descendantTags, tag]; + await Promise.all( + tagsToRemove.map(async (tag) => { + await this.application.changeItem(tag.uuid, (mutator) => { + mutator.removeItemAsRelationship(activeNote); + }); + }) + ); + this.application.sync(); + this.reloadTags(); + } + } +} diff --git a/app/assets/javascripts/ui_models/app_state/notes_state.ts b/app/assets/javascripts/ui_models/app_state/notes_state.ts index 3a4e79724..daa9881aa 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -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, 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,29 +330,40 @@ export class NotesState { async addTagToSelectedNotes(tag: SNTag): Promise { 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(); } async removeTagFromSelectedNotes(tag: SNTag): Promise { const selectedNotes = Object.values(this.selectedNotes); - await this.application.changeItem(tag.uuid, (mutator) => { - for (const note of selectedNotes) { - mutator.removeItemAsRelationship(note); - } - }); + const descendantTags = this.application.getTagDescendants(tag); + const tagsToRemove = [...descendantTags, tag]; + await Promise.all( + tagsToRemove.map(async (tag) => { + await this.application.changeItem(tag.uuid, (mutator) => { + for (const note of selectedNotes) { + mutator.removeItemAsRelationship(note); + } + }); + }) + ); 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) ); diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index f5ef81437..bf37859cb 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -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 diff --git a/app/assets/javascripts/views/editor/editor_view.ts b/app/assets/javascripts/views/editor/editor_view.ts index 7a2b9fa46..16cc59035 100644 --- a/app/assets/javascripts/views/editor/editor_view.ts +++ b/app/assets/javascripts/views/editor/editor_view.ts @@ -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 { 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 { 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 { 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 { 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 { noteStatus: undefined, editorUnloading: false, textareaUnloading: false, - mutable: { - tagsString: '', - }, } as EditorState; } @@ -302,10 +280,8 @@ class EditorViewCtrl extends PureViewCtrl { 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 { } 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 { } if (!this.note) return; this.reloadStackComponents(); - this.reloadNoteTagsComponent(); this.reloadEditor(); } ); @@ -487,7 +455,6 @@ class EditorViewCtrl extends PureViewCtrl { 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 { 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 { 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 { { identifier: 'editor', areas: [ - ComponentArea.NoteTags, ComponentArea.EditorStack, ComponentArea.Editor, ], @@ -908,7 +767,6 @@ class EditorViewCtrl extends PureViewCtrl { 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 { 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 diff --git a/app/assets/javascripts/views/editor_group/editor_group_view.ts b/app/assets/javascripts/views/editor_group/editor_group_view.ts index 405db34f8..df7ded8e6 100644 --- a/app/assets/javascripts/views/editor_group/editor_group_view.ts +++ b/app/assets/javascripts/views/editor_group/editor_group_view.ts @@ -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 { }; 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 { 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 { 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 { 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 { ); } + onPanelWidthEvent(): void { + this.appState.noteTags.reloadTagsContainerMaxWidth(); + } + paginate() { this.notesToDisplay += this.pageSize; this.reloadNotes(); diff --git a/app/assets/javascripts/views/tags/tags_view.ts b/app/assets/javascripts/views/tags/tags_view.ts index d7ce1bbc8..2c63341cb 100644 --- a/app/assets/javascripts/views/tags/tags_view.ts +++ b/app/assets/javascripts/views/tags/tags_view.ts @@ -289,10 +289,10 @@ class TagsViewCtrl extends PureViewCtrl { async saveTag($event: Event, tag: SNTag) { ($event.target! as HTMLInputElement).blur(); - if (!this.titles[tag.uuid]?.length) { - return this.undoCreateTag(tag); - } if (this.getState().templateTag) { + if (!this.titles[tag.uuid]?.length) { + return this.undoCreateTag(tag); + } return this.saveNewTag(); } else { return this.saveTagRename(tag); @@ -314,6 +314,9 @@ class TagsViewCtrl extends PureViewCtrl { if (newTitle.length === 0) { this.titles[tag.uuid] = this.editingOriginalName; this.editingOriginalName = undefined; + await this.setState({ + editingTag: undefined + }); return; } const existingTag = this.application.findTagByTitle(newTitle); @@ -345,6 +348,7 @@ class TagsViewCtrl extends PureViewCtrl { this.application.alertService!.alert( "A tag with this name already exists." ); + this.undoCreateTag(newTag); return; } const insertedTag = await this.application.insertItem(newTag); diff --git a/app/assets/stylesheets/_editor.scss b/app/assets/stylesheets/_editor.scss index 45ad2a88c..5a1f8cf4f 100644 --- a/app/assets/stylesheets/_editor.scss +++ b/app/assets/stylesheets/_editor.scss @@ -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; - } - } } } diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 3d3d6cb25..7120ea12d 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -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,11 +57,63 @@ 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; } +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + .outline-none { outline: none; } @@ -70,6 +126,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 +150,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 +179,10 @@ line-height: 2.25rem; } +.w-0 { + width: 0; +} + .w-3\.5 { width: 0.875rem; } @@ -123,18 +195,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,32 +235,52 @@ 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; +} + +.h-90vh { + height: 90vh; +} + .max-h-120 { max-height: 30rem; } +.min-h-5 { + min-height: 1.25rem; +} + .fixed { position: fixed; } -.overflow-y-scroll { - overflow-y: scroll; +.overflow-y-auto { + overflow-y: auto; } -.items-start { - align-items: flex-start; -} - -.whitespace-nowrap { - white-space: nowrap; +.overflow-auto { + overflow: auto; } .overflow-hidden { @@ -183,6 +291,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 +351,15 @@ @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 .rounded; @extend .box-shadow; @@ -238,6 +378,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 +419,7 @@ top: 50%; transform: translate(0px, -50%); - &.sn-switch-handle-right { + &.sn-switch-handle--right { transform: translate( calc(2rem - 1.125rem), -50% @@ -286,3 +430,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; +} diff --git a/app/assets/stylesheets/_tags.scss b/app/assets/stylesheets/_tags.scss index 95060e799..300d5af52 100644 --- a/app/assets/stylesheets/_tags.scss +++ b/app/assets/stylesheets/_tags.scss @@ -72,6 +72,8 @@ } > .title { + @extend .focus\:outline-none; + @extend .focus\:shadow-none; width: 80%; background-color: transparent; font-weight: 600; diff --git a/package.json b/package.json index 0fd868b94..8fae7c363 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.8.0", + "version": "3.8.1", "license": "AGPL-3.0-or-later", "repository": { "type": "git", @@ -57,7 +57,7 @@ "serve-static": "^1.14.1", "sn-stylekit": "5.1.0", "ts-loader": "^8.0.17", - "typescript": "^4.1.5", + "typescript": "4.2.3", "typescript-eslint": "0.0.1-alpha.0", "webpack": "^4.44.1", "webpack-cli": "^3.3.12", @@ -71,7 +71,7 @@ "@reach/checkbox": "^0.13.2", "@reach/dialog": "^0.13.0", "@standardnotes/sncrypto-web": "1.2.10", - "@standardnotes/snjs": "2.5.0", + "@standardnotes/snjs": "2.6.3", "mobx": "^6.1.6", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12" diff --git a/yarn.lock b/yarn.lock index 81c84c425..a14d4d07a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1936,10 +1936,10 @@ "@standardnotes/sncrypto-common" "^1.2.7" libsodium-wrappers "^0.7.8" -"@standardnotes/snjs@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.5.0.tgz#fcd45f8c6884fcc204633be33366b59ede71c5b1" - integrity sha512-VWThoZhymoCOqRkZjXj3vDhQGAPMt+KUrB/FyYZkl+9jVCMX6NIGziLb8fThFaZzoyC/qp5BuoceZlbyrggOnw== +"@standardnotes/snjs@2.6.3": + version "2.6.3" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.6.3.tgz#7677899c050b0616d994423fd4ec9caf03394f35" + integrity sha512-5pWh+BPVPpd6JlP3avo2puGk9EWUaH0+6Y1fN9rMR8oLZ2oc8Dffiy5S4TLNm8zL4q504oMlm1/ALkwwZpKLEQ== dependencies: "@standardnotes/auth" "^2.0.0" "@standardnotes/sncrypto-common" "^1.2.9" @@ -8491,10 +8491,10 @@ typescript-eslint@0.0.1-alpha.0: resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-0.0.1-alpha.0.tgz#285d68a4e96588295cd436278801bcb6a6b916c1" integrity sha512-1hNKM37dAWML/2ltRXupOq2uqcdRQyDFphl+341NTPXFLLLiDhErXx8VtaSLh3xP7SyHZdcCgpt9boYYVb3fQg== -typescript@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== +typescript@4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" + integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== uglify-js@3.4.x: version "3.4.10"