From 90250d22a3d3f60cd7e47d4380ddd6f27b45c2bf Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 19 May 2021 17:39:18 -0300 Subject: [PATCH 01/96] feat: modify current tags to v4 style --- app/assets/javascripts/app.ts | 4 +++- .../javascripts/components/NoteTags.tsx | 23 ++++++++++++++++++ .../ui_models/app_state/notes_state.ts | 7 ++++++ .../javascripts/views/editor/editor-view.pug | 24 +++++++++++-------- app/assets/stylesheets/_editor.scss | 1 - app/assets/stylesheets/_sn.scss | 21 ++++++++++++++++ 6 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/components/NoteTags.tsx diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index 8adffd559..eead8b4e8 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 { NoteTagsDirective } from './components/NoteTags'; function reloadHiddenFirefoxTab(): boolean { /** @@ -157,7 +158,8 @@ const startApplication: StartApplication = async function startApplication( .directive('multipleSelectedNotesPanel', MultipleSelectedNotesDirective) .directive('notesContextMenu', NotesContextMenuDirective) .directive('notesOptionsPanel', NotesOptionsPanelDirective) - .directive('icon', IconDirective); + .directive('icon', IconDirective) + .directive('noteTags', NoteTagsDirective); // Filters angular.module('app').filter('trusted', ['$sce', trusted]); diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx new file mode 100644 index 000000000..8b78e0ff6 --- /dev/null +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -0,0 +1,23 @@ +import { AppState } from "@/ui_models/app_state"; +import { observer } from "mobx-react-lite"; +import { toDirective } from "./utils"; +import { Icon } from "./Icon"; + +type Props = { + appState: AppState; +} + +const NoteTags = observer(({ appState }: Props) => { + return ( +
+ {appState.notes.activeNoteTags.map(tag => ( + + + {tag.title} + + ))} +
+ ); +}); + +export const NoteTagsDirective = toDirective(NoteTags); \ No newline at end of file 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 6688e4e97..d8dcccb02 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -29,6 +29,7 @@ export class NotesState { }; contextMenuMaxHeight: number | 'auto' = 'auto'; showProtectedWarning = false; + activeNoteTags: SNTag[] = []; constructor( private application: WebApplication, @@ -40,6 +41,7 @@ export class NotesState { contextMenuOpen: observable, contextMenuPosition: observable, showProtectedWarning: observable, + activeNoteTags: observable, selectedNotesCount: computed, trashedNotesCount: computed, @@ -164,6 +166,11 @@ export class NotesState { } else { this.activeEditor.setNote(note); } + + runInAction(() => { + this.activeNoteTags = this.application.getAppState().getNoteTags(note); + }); + await this.onActiveEditorChanged(); if (note.waitingForKey) { diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index f5ef81437..dabb895c3 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -30,7 +30,7 @@ div.flex-grow( ng-class="{'locked' : self.noteLocked}" ) - .title + .title.overflow-auto input#note-title-editor.input( ng-blur='self.onTitleBlur()', ng-change='self.onTitleChange()', @@ -48,16 +48,20 @@ 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 + note-tags( + app-state='self.appState' ) + 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( diff --git a/app/assets/stylesheets/_editor.scss b/app/assets/stylesheets/_editor.scss index 45ad2a88c..ca4d681af 100644 --- a/app/assets/stylesheets/_editor.scss +++ b/app/assets/stylesheets/_editor.scss @@ -106,7 +106,6 @@ $heading-height: 75px; .tags-input { background-color: transparent; color: var(--sn-stylekit-foreground-color); - width: 100%; border: none; &:focus { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 9a4f00026..d4d8cd236 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -53,6 +53,14 @@ margin-bottom: 0.5rem; } +.mr-1 { + margin-right: 0.25rem; +} + +.p-1 { + padding: 0.25rem; +} + .py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; @@ -167,10 +175,18 @@ overflow-y: scroll; } +.overflow-auto { + overflow: auto; +} + .items-start { align-items: flex-start; } +.p-2 { + padding: 0.5rem; +} + /** * A button that is just an icon. Separated from .sn-button because there * is almost no style overlap. @@ -205,6 +221,11 @@ @extend .fill-current; } +.sn-icon.small { + @extend .h-3\.5; + @extend .w-3\.5; +} + .sn-dropdown { @extend .bg-default; @extend .min-w-80; From 905e5ab193ea40c50f0966bdc001ace7c52cbb3d Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 20 May 2021 17:10:10 -0300 Subject: [PATCH 02/96] styles: fix class names --- app/assets/stylesheets/_sn.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index d4d8cd236..8f474af45 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -61,7 +61,7 @@ padding: 0.25rem; } -.py-1\.5 { +.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; } @@ -222,8 +222,8 @@ } .sn-icon.small { - @extend .h-3\.5; - @extend .w-3\.5; + @extend .h-3\.5 ; + @extend .w-3\.5 ; } .sn-dropdown { From be6893b8dc7da0c932801a3e91f38c00a471aac3 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 17:06:09 -0300 Subject: [PATCH 03/96] feat: add autocomplete tags input and dropdown --- .../components/AutocompleteTagInput.tsx | 91 +++++++++++++++++++ .../javascripts/components/NoteTags.tsx | 24 +++-- .../javascripts/views/editor/editor-view.pug | 1 + app/assets/stylesheets/_sn.scss | 16 ++++ 4 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/components/AutocompleteTagInput.tsx diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx new file mode 100644 index 000000000..9eb089953 --- /dev/null +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -0,0 +1,91 @@ +import { WebApplication } from '@/ui_models/application'; +import { SNTag } from '@standardnotes/snjs'; +import { FunctionalComponent } from 'preact'; +import { useRef, useState } from 'preact/hooks'; +import { Icon } from './Icon'; +import { Disclosure, DisclosurePanel } from '@reach/disclosure'; +import { useCloseOnBlur } from './utils'; + +type Props = { + application: WebApplication; +}; + +export const AutocompleteTagInput: FunctionalComponent = ({ + application, +}) => { + const [searchQuery, setSearchQuery] = useState(''); + const [tagResults, setTagResults] = useState(() => { + return application.searchTags(''); + }); + const [dropdownVisible, setDropdownVisible] = useState(false); + + const dropdownRef = useRef(); + const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => + setDropdownVisible(visible) + ); + + const onSearchQueryChange = (event: Event) => { + const query = (event.target as HTMLInputElement).value; + const tags = application.searchTags(query); + + setSearchQuery(query); + setTagResults(tags); + setDropdownVisible(tags.length > 0); + }; + + return ( +
event.preventDefault()} className="mt-2"> + setDropdownVisible(true)} + > + { + if (tagResults.length > 0) { + setDropdownVisible(true); + } + }} + /> + {dropdownVisible && ( + + {tagResults.map((tag) => { + return ( + + ); + })} + + )} + +
+ ); +}; diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 8b78e0ff6..015185ef5 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -1,23 +1,27 @@ -import { AppState } from "@/ui_models/app_state"; -import { observer } from "mobx-react-lite"; -import { toDirective } from "./utils"; -import { Icon } from "./Icon"; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { toDirective } from './utils'; +import { Icon } from './Icon'; +import { AutocompleteTagInput } from './AutocompleteTagInput'; +import { WebApplication } from '@/ui_models/application'; type Props = { + application: WebApplication; appState: AppState; -} +}; -const NoteTags = observer(({ appState }: Props) => { +const NoteTags = observer(({ application, appState }: Props) => { return ( -
- {appState.notes.activeNoteTags.map(tag => ( - +
+ {appState.notes.activeNoteTags.map((tag) => ( + {tag.title} ))} +
); }); -export const NoteTagsDirective = toDirective(NoteTags); \ No newline at end of file +export const NoteTagsDirective = toDirective(NoteTags); diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index dabb895c3..3a86f047f 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -50,6 +50,7 @@ ) div.flex note-tags( + application='self.application' app-state='self.appState' ) input.tags-input( diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 8f474af45..c6e092940 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; } @@ -155,6 +159,10 @@ height: 1.25rem; } +.h-7 { + height: 1.75rem; +} + .h-8 { height: 2rem; } @@ -187,6 +195,14 @@ padding: 0.5rem; } +.flex-wrap { + flex-wrap: wrap; +} + +.whitespace-pre-wrap { + white-space: pre-wrap; +} + /** * A button that is just an icon. Separated from .sn-button because there * is almost no style overlap. From f23e4a4b446de578e57a9f3e59326eedd85675a1 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 17:21:53 -0300 Subject: [PATCH 04/96] chore(version-snjs): 2.3.2 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1887b5cbe..adb2ff66c 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@reach/checkbox": "^0.13.2", "@reach/dialog": "^0.13.0", "@standardnotes/sncrypto-web": "1.2.10", - "@standardnotes/snjs": "2.3.0", + "@standardnotes/snjs": "2.3.2", "mobx": "^6.1.6", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12" diff --git a/yarn.lock b/yarn.lock index bd05813de..9b66222c8 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.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.3.0.tgz#52f6b5458e348e77642f922dd4302ad8c6d28914" - integrity sha512-4xlLcVKJznhqCTKWy4IhYPbnxc3k66fzBeTdWJqZZ/n0vOT2l1/ybCRrPP0os/7NFCtK3CqInApbzZP6xXUhuA== +"@standardnotes/snjs@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.3.2.tgz#9dc13df769681f81bc4540aee475461445a36cda" + integrity sha512-JxoDn489nYs9zlEoki075oZNjyyztG6Uf0zIyPYd5Ye6vR04aVtZyIONYkI9XVU5b9Uy0xdJ5I59SwJJbmD75Q== dependencies: "@standardnotes/auth" "^2.0.0" "@standardnotes/sncrypto-common" "^1.2.9" From b2d15be1845498a656e2300fbf2dbbc3bc0e1783 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 17:22:54 -0300 Subject: [PATCH 05/96] feat: omit active note tags from dropdown --- .../components/AutocompleteTagInput.tsx | 17 +++++++++++++---- app/assets/javascripts/components/NoteTags.tsx | 2 +- .../ui_models/app_state/notes_state.ts | 4 ++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 9eb089953..a6e6148d8 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -5,20 +5,29 @@ import { useRef, useState } from 'preact/hooks'; import { Icon } from './Icon'; import { Disclosure, DisclosurePanel } from '@reach/disclosure'; import { useCloseOnBlur } from './utils'; +import { AppState } from '@/ui_models/app_state'; type Props = { application: WebApplication; + appState: AppState; }; export const AutocompleteTagInput: FunctionalComponent = ({ application, + appState, }) => { const [searchQuery, setSearchQuery] = useState(''); - const [tagResults, setTagResults] = useState(() => { - return application.searchTags(''); - }); const [dropdownVisible, setDropdownVisible] = useState(false); + const getActiveNoteTagResults = (query: string) => { + const { activeNote } = appState.notes; + return application.searchTags(query, activeNote); + }; + + const [tagResults, setTagResults] = useState(() => { + return getActiveNoteTagResults(''); + }); + const dropdownRef = useRef(); const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => setDropdownVisible(visible) @@ -26,7 +35,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const onSearchQueryChange = (event: Event) => { const query = (event.target as HTMLInputElement).value; - const tags = application.searchTags(query); + const tags = getActiveNoteTagResults(query); setSearchQuery(query); setTagResults(tags); diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 015185ef5..87906b381 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -19,7 +19,7 @@ const NoteTags = observer(({ application, appState }: Props) => { {tag.title}
))} - +
); }); 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 d8dcccb02..18297eb27 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -70,6 +70,10 @@ export class NotesState { return this.application.editorGroup.editors[0]; } + get activeNote(): SNNote | undefined { + return this.activeEditor?.note; + } + get selectedNotesCount(): number { return Object.keys(this.selectedNotes).length; } From c230cdee812d320a6ea55b4274f486f9544e0c8c Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 17:23:44 -0300 Subject: [PATCH 06/96] fix: add missing key --- app/assets/javascripts/components/NoteTags.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 87906b381..534b4964d 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -14,7 +14,7 @@ const NoteTags = observer(({ application, appState }: Props) => { return (
{appState.notes.activeNoteTags.map((tag) => ( - + {tag.title} From e2585200ac992a28a891c2e913f259f7810bebfb Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 18:28:13 -0300 Subject: [PATCH 07/96] fix: make dropdown height adjust to screen --- .../components/AutocompleteTagInput.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index a6e6148d8..52b241b81 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -18,6 +18,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ }) => { const [searchQuery, setSearchQuery] = useState(''); const [dropdownVisible, setDropdownVisible] = useState(false); + const [dropdownMaxHeight, setDropdownMaxHeight] = useState('auto'); const getActiveNoteTagResults = (query: string) => { const { activeNote } = appState.notes; @@ -28,11 +29,19 @@ export const AutocompleteTagInput: FunctionalComponent = ({ return getActiveNoteTagResults(''); }); + const inputRef = useRef(); const dropdownRef = useRef(); const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => setDropdownVisible(visible) ); + 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; const tags = getActiveNoteTagResults(query); @@ -46,9 +55,10 @@ export const AutocompleteTagInput: FunctionalComponent = ({
event.preventDefault()} className="mt-2"> setDropdownVisible(true)} + onChange={showDropdown} > = ({ onBlur={closeOnBlur} onFocus={() => { if (tagResults.length > 0) { - setDropdownVisible(true); + showDropdown(); } }} /> {dropdownVisible && ( {tagResults.map((tag) => { return ( From a8d6080a6f1a2c2968e8ed152fb4afb259614bbc Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 19:07:07 -0300 Subject: [PATCH 08/96] feat: add tag on dropdown option click --- .../components/AutocompleteTagInput.tsx | 11 ++++----- .../ui_models/app_state/notes_state.ts | 23 +++++++++++++++---- .../javascripts/views/editor/editor-view.pug | 19 ++++----------- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 52b241b81..1dd5b7cf2 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -18,7 +18,8 @@ export const AutocompleteTagInput: FunctionalComponent = ({ }) => { const [searchQuery, setSearchQuery] = useState(''); const [dropdownVisible, setDropdownVisible] = useState(false); - const [dropdownMaxHeight, setDropdownMaxHeight] = useState('auto'); + const [dropdownMaxHeight, setDropdownMaxHeight] = + useState('auto'); const getActiveNoteTagResults = (query: string) => { const { activeNote } = appState.notes; @@ -38,7 +39,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const showDropdown = () => { const { clientHeight } = document.documentElement; const inputRect = inputRef.current.getBoundingClientRect(); - setDropdownMaxHeight(clientHeight - inputRect.bottom - 32*2); + setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2); setDropdownVisible(true); }; @@ -53,10 +54,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ return ( event.preventDefault()} className="mt-2"> - + = ({ key={tag.uuid} className={`flex items-center border-0 focus:inner-ring-info cursor-pointer hover:bg-contrast color-text bg-transparent px-3 text-left py-1.5`} + onClick={() => appState.notes.addTagToActiveNote(tag)} onBlur={closeOnBlur} > 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 18297eb27..bc8c75b50 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -46,6 +46,7 @@ export class NotesState { selectedNotesCount: computed, trashedNotesCount: computed, + reloadActiveNoteTags: action, setContextMenuOpen: action, setContextMenuPosition: action, setContextMenuMaxHeight: action, @@ -154,6 +155,13 @@ export class NotesState { } } + async reloadActiveNoteTags(): Promise { + const { activeNote } = this; + if (activeNote) { + this.activeNoteTags = this.application.getAppState().getNoteTags(activeNote); + } + } + private async openEditor(noteUuid: string): Promise { if (this.activeEditor?.note?.uuid === noteUuid) { return; @@ -171,10 +179,7 @@ export class NotesState { this.activeEditor.setNote(note); } - runInAction(() => { - this.activeNoteTags = this.application.getAppState().getNoteTags(note); - }); - + this.reloadActiveNoteTags(); await this.onActiveEditorChanged(); if (note.waitingForKey) { @@ -361,6 +366,16 @@ export class NotesState { ); } + async addTagToActiveNote(tag: SNTag): Promise { + const { activeNote } = this; + if (activeNote) { + await this.application.changeItem(tag.uuid, (mutator) => { + mutator.addItemAsRelationship(activeNote); + }); + this.reloadActiveNoteTags(); + } + } + setShowProtectedWarning(show: boolean): void { this.showProtectedWarning = show; } diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index 3a86f047f..073a0a47d 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -48,21 +48,10 @@ ng-style="self.notesLocked && {'pointer-events' : 'none'}", application='self.application' ) - div.flex - note-tags( - application='self.application' - app-state='self.appState' - ) - 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' - ) + note-tags( + application='self.application' + app-state='self.appState' + ) div.flex.items-center #save-status .message( From c05220af7ac90a9968bcf197de423f2ee8acca1b Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 19:07:21 -0300 Subject: [PATCH 09/96] feat: remove previous tag input --- app/assets/javascripts/views/notes/notes_view.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/views/notes/notes_view.ts b/app/assets/javascripts/views/notes/notes_view.ts index cfbbaf40b..78d276d43 100644 --- a/app/assets/javascripts/views/notes/notes_view.ts +++ b/app/assets/javascripts/views/notes/notes_view.ts @@ -407,6 +407,7 @@ class NotesViewCtrl extends PureViewCtrl { await this.appState.createEditor(title); await this.flushUI(); await this.reloadNotes(); + await this.appState.notes.reloadActiveNoteTags(); } async handleTagChange(tag: SNTag) { From b6aaa49e505885c83de31796271a6ab77f38a6cf Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 19:13:46 -0300 Subject: [PATCH 10/96] fix: reload tag results after adding tag --- .../components/AutocompleteTagInput.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 1dd5b7cf2..1ece32124 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -43,13 +43,21 @@ export const AutocompleteTagInput: FunctionalComponent = ({ setDropdownVisible(true); }; + const reloadTags = (query: string) => { + const tags = getActiveNoteTagResults(query); + setTagResults(tags); + }; + const onSearchQueryChange = (event: Event) => { const query = (event.target as HTMLInputElement).value; - const tags = getActiveNoteTagResults(query); - + reloadTags(query); setSearchQuery(query); - setTagResults(tags); - setDropdownVisible(tags.length > 0); + setDropdownVisible(tagResults.length > 0); + }; + + const onOptionClick = async (tag: SNTag) => { + await appState.notes.addTagToActiveNote(tag); + reloadTags(searchQuery); }; return ( @@ -80,7 +88,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ key={tag.uuid} className={`flex items-center border-0 focus:inner-ring-info cursor-pointer hover:bg-contrast color-text bg-transparent px-3 text-left py-1.5`} - onClick={() => appState.notes.addTagToActiveNote(tag)} + onClick={() => onOptionClick(tag)} onBlur={closeOnBlur} > From 447c0109f0aefdc1b578cbf96e96b7752af467de Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 24 May 2021 19:16:17 -0300 Subject: [PATCH 11/96] fix: keep tag title in dropdown in its original case --- app/assets/javascripts/components/AutocompleteTagInput.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 1ece32124..4a2203d84 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -93,13 +93,12 @@ export const AutocompleteTagInput: FunctionalComponent = ({ > {tag.title - .toLowerCase() .split(new RegExp(`(${searchQuery})`, 'gi')) .map((substring, index) => ( Date: Tue, 25 May 2021 11:49:37 -0300 Subject: [PATCH 12/96] fix: remove async from reloadActiveNoteTags --- app/assets/javascripts/ui_models/app_state/notes_state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bc8c75b50..0205bbde6 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -155,7 +155,7 @@ export class NotesState { } } - async reloadActiveNoteTags(): Promise { + reloadActiveNoteTags(): void { const { activeNote } = this; if (activeNote) { this.activeNoteTags = this.application.getAppState().getNoteTags(activeNote); From cdf8f6065583eb49feb00ff7e13ae6a4996c910b Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 25 May 2021 15:57:51 -0300 Subject: [PATCH 13/96] refactor: extract Tag to its own component --- app/assets/javascripts/components/NoteTags.tsx | 7 ++----- app/assets/javascripts/components/Tag.tsx | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/components/Tag.tsx diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 534b4964d..f91bcc509 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -1,7 +1,7 @@ import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; import { toDirective } from './utils'; -import { Icon } from './Icon'; +import { Tag } from './Tag'; import { AutocompleteTagInput } from './AutocompleteTagInput'; import { WebApplication } from '@/ui_models/application'; @@ -14,10 +14,7 @@ const NoteTags = observer(({ application, appState }: Props) => { return (
{appState.notes.activeNoteTags.map((tag) => ( - - - {tag.title} - + ))}
diff --git a/app/assets/javascripts/components/Tag.tsx b/app/assets/javascripts/components/Tag.tsx new file mode 100644 index 000000000..53f4d1709 --- /dev/null +++ b/app/assets/javascripts/components/Tag.tsx @@ -0,0 +1,16 @@ +import { FunctionalComponent } from 'preact'; +import { Icon } from './Icon'; + +type TagProps = { + title: string; + className?: string; +}; + +export const Tag: FunctionalComponent = ({ title, className }) => ( + + + {title} + +); From eb89fe4a012fc4e832da1fc630c65a15c6b9312b Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 25 May 2021 16:09:14 -0300 Subject: [PATCH 14/96] styles: make class names BEM and extract sn-dropdown-item style --- .../javascripts/components/NotesOptions.tsx | 24 +++++++-------- .../javascripts/components/SearchOptions.tsx | 2 +- app/assets/javascripts/components/Switch.tsx | 2 +- app/assets/javascripts/components/Tag.tsx | 2 +- app/assets/stylesheets/_sn.scss | 30 +++++++++++++++---- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/components/NotesOptions.tsx b/app/assets/javascripts/components/NotesOptions.tsx index 8c80f5f54..874d04d7e 100644 --- a/app/assets/javascripts/components/NotesOptions.tsx +++ b/app/assets/javascripts/components/NotesOptions.tsx @@ -52,10 +52,6 @@ export const NotesOptions = observer( const tagsButtonRef = useRef(); 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) { @@ -144,7 +140,7 @@ export const NotesOptions = observer( }} onBlur={closeOnBlur} ref={tagsButtonRef} - className={`${buttonClass} py-1.5 justify-between`} + className="sn-dropdown-item justify-between" >
@@ -169,7 +165,7 @@ export const NotesOptions = observer( {appState.tags.tags.map((tag) => ( ); })} + {searchQuery !== '' && ( + <> + {tagResults.length > 0 && ( +
+ )} + + + )} )} From 4d67c484f9a0ac774a79cb948ad18872d9d24649 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 25 May 2021 16:47:21 -0300 Subject: [PATCH 17/96] fix: focus input after option click --- .../javascripts/components/AutocompleteTagInput.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index fb672bcee..347c49de8 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -33,7 +33,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const inputRef = useRef(); const dropdownRef = useRef(); - const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => { + const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => { setDropdownVisible(visible); setSearchQuery(''); setTagResults(getActiveNoteTagResults('')); @@ -46,20 +46,17 @@ export const AutocompleteTagInput: FunctionalComponent = ({ setDropdownVisible(true); }; - const reloadTags = (query: string) => { - setTagResults(getActiveNoteTagResults(query)); - }; - const onSearchQueryChange = (event: Event) => { const query = (event.target as HTMLInputElement).value; - reloadTags(query); + setTagResults(getActiveNoteTagResults(query)); setSearchQuery(query); }; const onOptionClick = async (tag: SNTag) => { + setLockCloseOnBlur(true); await appState.notes.addTagToActiveNote(tag); - setSearchQuery(''); - reloadTags(searchQuery); + inputRef.current.focus(); + setLockCloseOnBlur(false); }; return ( From 5c5523fd0a1f675f9cc04c08f228c812ed8e8b93 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 25 May 2021 16:48:45 -0300 Subject: [PATCH 18/96] fix: reload tags after adding or removing them from context menu --- app/assets/javascripts/ui_models/app_state/notes_state.ts | 2 ++ 1 file changed, 2 insertions(+) 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 53cbd5a7f..69c227c62 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -344,6 +344,7 @@ export class NotesState { } }); this.application.sync(); + this.reloadActiveNoteTags(); } async removeTagFromSelectedNotes(tag: SNTag): Promise { @@ -354,6 +355,7 @@ export class NotesState { } }); this.application.sync(); + this.reloadActiveNoteTags(); } isTagInSelectedNotes(tag: SNTag): boolean { From 4c6bfb8519e5c42175365ff2b829c505a4a5edf7 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 25 May 2021 17:25:57 -0300 Subject: [PATCH 19/96] feat: create and add tag to note on hint click --- .../javascripts/components/AutocompleteTagInput.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 347c49de8..3a107af74 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -52,13 +52,19 @@ export const AutocompleteTagInput: FunctionalComponent = ({ setSearchQuery(query); }; - const onOptionClick = async (tag: SNTag) => { + const onTagOptionClick = async (tag: SNTag) => { setLockCloseOnBlur(true); await appState.notes.addTagToActiveNote(tag); inputRef.current.focus(); setLockCloseOnBlur(false); }; + const onTagHintClick = async () => { + const newTag = await application.findOrCreateTag(searchQuery); + await appState.notes.addTagToActiveNote(newTag); + setSearchQuery(''); + }; + return ( event.preventDefault()} className="mt-2"> @@ -82,7 +88,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ ); })} - {searchQuery !== '' && ( + {hintVisible && ( <> {tagResults.length > 0 && (
From f7bc9e0b0ac0d10435e0a3e458cd5795918b2144 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 16:31:53 -0300 Subject: [PATCH 25/96] feat: make hint fixed on bottom of dropdown --- .../components/AutocompleteTagInput.tsx | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index cfce139d2..d4469d64c 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -97,36 +97,38 @@ export const AutocompleteTagInput: FunctionalComponent = ({ {dropdownVisible && ( - {tagResults.map((tag) => { - return ( - - ); - })} +
+ {tagResults.map((tag) => { + return ( + + ); + })} +
{hintVisible && ( <> {tagResults.length > 0 && ( From 90cc806e1564a9ade6a6e359883df766c3c8b24f Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 16:32:29 -0300 Subject: [PATCH 26/96] feat: highlight tag on click --- .../components/AutocompleteTagInput.tsx | 8 ++++++-- app/assets/javascripts/components/NoteTags.tsx | 10 ++++++++-- app/assets/javascripts/components/Tag.tsx | 16 ---------------- app/assets/stylesheets/_sn.scss | 8 ++++++++ 4 files changed, 22 insertions(+), 20 deletions(-) delete mode 100644 app/assets/javascripts/components/Tag.tsx diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index d4469d64c..9ae7e7ca5 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -6,7 +6,6 @@ import { Icon } from './Icon'; import { Disclosure, DisclosurePanel } from '@reach/disclosure'; import { useCloseOnBlur } from './utils'; import { AppState } from '@/ui_models/app_state'; -import { Tag } from './Tag'; type Props = { application: WebApplication; @@ -142,7 +141,12 @@ export const AutocompleteTagInput: FunctionalComponent = ({ Create new tag: - + + + {searchQuery} + )} diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index f91bcc509..2c32909cc 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -1,7 +1,7 @@ import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; import { toDirective } from './utils'; -import { Tag } from './Tag'; +import { Icon } from './Icon'; import { AutocompleteTagInput } from './AutocompleteTagInput'; import { WebApplication } from '@/ui_models/application'; @@ -14,7 +14,13 @@ const NoteTags = observer(({ application, appState }: Props) => { return (
{appState.notes.activeNoteTags.map((tag) => ( - + ))}
diff --git a/app/assets/javascripts/components/Tag.tsx b/app/assets/javascripts/components/Tag.tsx deleted file mode 100644 index 37b370056..000000000 --- a/app/assets/javascripts/components/Tag.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { FunctionalComponent } from 'preact'; -import { Icon } from './Icon'; - -type TagProps = { - title: string; - className?: string; -}; - -export const Tag: FunctionalComponent = ({ title, className }) => ( - - - {title} - -); diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index c819eca91..839edf5cf 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -102,6 +102,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; } From d6f1cc3730a6ff01cb844637406a411df01728db Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 17:38:20 -0300 Subject: [PATCH 27/96] feat: focus last tag when pressing backspace on input --- .../javascripts/components/AutocompleteTagInput.tsx | 9 ++++++++- app/assets/javascripts/components/NoteTags.tsx | 9 +++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 9ae7e7ca5..0150834d1 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -1,6 +1,6 @@ import { WebApplication } from '@/ui_models/application'; import { SNTag } from '@standardnotes/snjs'; -import { FunctionalComponent } from 'preact'; +import { FunctionalComponent, RefObject } from 'preact'; import { useRef, useState } from 'preact/hooks'; import { Icon } from './Icon'; import { Disclosure, DisclosurePanel } from '@reach/disclosure'; @@ -10,11 +10,13 @@ import { AppState } from '@/ui_models/app_state'; type Props = { application: WebApplication; appState: AppState; + lastTagRef: RefObject; }; export const AutocompleteTagInput: FunctionalComponent = ({ application, appState, + lastTagRef, }) => { const [searchQuery, setSearchQuery] = useState(''); const [dropdownVisible, setDropdownVisible] = useState(false); @@ -92,6 +94,11 @@ export const AutocompleteTagInput: FunctionalComponent = ({ type="text" onBlur={closeOnBlur} onFocus={showDropdown} + onKeyUp={(event) => { + if (event.key === 'Backspace') { + lastTagRef.current?.focus(); + } + }} /> {dropdownVisible && ( { + const { activeNoteTags } = appState.notes; + const lastTagRef = useRef(); + return (
- {appState.notes.activeNoteTags.map((tag) => ( + {activeNoteTags.map((tag, index) => ( ))} - +
); }); From 69c9247cd9bd8a4715a520d4593be7504b433618 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 17:49:09 -0300 Subject: [PATCH 28/96] feat: remove tag on backspace press --- app/assets/javascripts/components/NoteTags.tsx | 11 +++++++++++ .../javascripts/ui_models/app_state/notes_state.ts | 14 +++++++++++++- .../views/editor_group/editor_group_view.ts | 3 +-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 6c4390dc0..83b2d2759 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -5,6 +5,7 @@ import { Icon } from './Icon'; import { AutocompleteTagInput } from './AutocompleteTagInput'; import { WebApplication } from '@/ui_models/application'; import { useRef } from 'preact/hooks'; +import { SNTag } from '@standardnotes/snjs'; type Props = { application: WebApplication; @@ -15,6 +16,11 @@ const NoteTags = observer(({ application, appState }: Props) => { const { activeNoteTags } = appState.notes; const lastTagRef = useRef(); + const onTagBackspacePress = async (tag: SNTag) => { + await appState.notes.removeTagFromActiveNote(tag); + lastTagRef.current?.focus(); + }; + return (
{activeNoteTags.map((tag, index) => ( @@ -22,6 +28,11 @@ const NoteTags = observer(({ application, appState }: Props) => { className={`bg-contrast border-0 rounded text-xs color-text p-1 flex items-center mt-2 mr-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`} ref={index === activeNoteTags.length - 1 ? lastTagRef : undefined} + onKeyUp={(event) => { + if (event.key === 'Backspace') { + onTagBackspacePress(tag); + } + }} > {tag.title} 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 08bb56d29..5bcf531c1 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -158,7 +158,7 @@ export class NotesState { reloadActiveNoteTags(): void { const { activeNote } = this; if (activeNote) { - this.activeNoteTags = this.application.getSortedTagsForNote(activeNote) + this.activeNoteTags = this.application.getSortedTagsForNote(activeNote); } } @@ -379,6 +379,18 @@ export class NotesState { } } + async removeTagFromActiveNote(tag: SNTag): Promise { + const { activeNote } = this; + if (activeNote) { + await this.application.changeItem(tag.uuid, (mutator) => { + mutator.removeItemAsRelationship(activeNote); + }); + this.application.sync(); + this.reloadActiveNoteTags(); + } + } + + setShowProtectedWarning(show: boolean): void { this.showProtectedWarning = show; } 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 Date: Wed, 26 May 2021 17:51:37 -0300 Subject: [PATCH 29/96] fix: make dropdown items full width --- app/assets/stylesheets/_sn.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 839edf5cf..eaf7b5e38 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -329,6 +329,7 @@ @extend .px-3; @extend .py-1\.5; @extend .text-left; + @extend .w-full; &.sn-dropdown-item--no-icon { @extend .py-2; From 9aa2021e1173ab1c626baddece42dde196ba8bf0 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 18:03:29 -0300 Subject: [PATCH 30/96] fix: add streamItems event listener for tags --- .../javascripts/ui_models/app_state/notes_state.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 5bcf531c1..c7d64a718 100644 --- a/app/assets/javascripts/ui_models/app_state/notes_state.ts +++ b/app/assets/javascripts/ui_models/app_state/notes_state.ts @@ -65,6 +65,14 @@ export class NotesState { }); }) ); + appEventListeners.push( + application.streamItems( + ContentType.Tag, + () => { + this.reloadActiveNoteTags(); + } + ) + ); } get activeEditor(): Editor | undefined { @@ -344,7 +352,6 @@ export class NotesState { } }); this.application.sync(); - this.reloadActiveNoteTags(); } async removeTagFromSelectedNotes(tag: SNTag): Promise { @@ -355,7 +362,7 @@ export class NotesState { } }); this.application.sync(); - this.reloadActiveNoteTags(); + } isTagInSelectedNotes(tag: SNTag): boolean { From f03987016f0f2b5fd0cd55d2c25aab617ac1d730 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 18:12:52 -0300 Subject: [PATCH 31/96] fix: reload tag results after adding tag --- app/assets/javascripts/components/AutocompleteTagInput.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 0150834d1..03b84f232 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -65,6 +65,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ setLockCloseOnBlur(true); await appState.notes.addTagToActiveNote(tag); inputRef.current.focus(); + setTagResults(getActiveNoteTagResults(searchQuery)); setLockCloseOnBlur(false); }; From 0d4456bc53e050c75a61a97f8a9bdd83a0d625bc Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 18:48:31 -0300 Subject: [PATCH 32/96] refactor: remove legacy tags code from editor view --- .../javascripts/views/editor/editor_view.ts | 212 +----------------- 1 file changed, 1 insertion(+), 211 deletions(-) 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 From 68bbcc6820155bf7e96c2451c482f96896e77483 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 18:56:56 -0300 Subject: [PATCH 33/96] refactor: remove tags component from view --- .../javascripts/views/editor/editor-view.pug | 6 ----- app/assets/stylesheets/_editor.scss | 27 ------------------- 2 files changed, 33 deletions(-) diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index 073a0a47d..780517482 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -42,12 +42,6 @@ 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' - ) note-tags( application='self.application' app-state='self.appState' diff --git a/app/assets/stylesheets/_editor.scss b/app/assets/stylesheets/_editor.scss index ca4d681af..5a1f8cf4f 100644 --- a/app/assets/stylesheets/_editor.scss +++ b/app/assets/stylesheets/_editor.scss @@ -86,33 +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); - border: none; - - &:focus { - outline: 0; - box-shadow: none; - } - } } } From 2c86958bf295bef1d4653ccf89ce0c7eea53d5df Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 26 May 2021 19:04:12 -0300 Subject: [PATCH 34/96] fix: make hint visible and focus input after creating new tag --- .../javascripts/components/AutocompleteTagInput.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 03b84f232..94061fe3e 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -1,7 +1,7 @@ import { WebApplication } from '@/ui_models/application'; import { SNTag } from '@standardnotes/snjs'; import { FunctionalComponent, RefObject } from 'preact'; -import { useRef, useState } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { Icon } from './Icon'; import { Disclosure, DisclosurePanel } from '@reach/disclosure'; import { useCloseOnBlur } from './utils'; @@ -55,9 +55,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const onSearchQueryChange = (event: Event) => { const query = (event.target as HTMLInputElement).value; - const tags = getActiveNoteTagResults(query); - setTagResults(tags); - setHintVisible(query !== '' && !tags.some((tag) => tag.title === query)); + setTagResults(getActiveNoteTagResults(query)); setSearchQuery(query); }; @@ -73,6 +71,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const newTag = await application.findOrCreateTag(searchQuery); await appState.notes.addTagToActiveNote(newTag); clearResults(); + inputRef.current.focus(); }; const onTagHintClick = async () => { @@ -84,6 +83,10 @@ export const AutocompleteTagInput: FunctionalComponent = ({ await createAndAddNewTag(); }; + useEffect(() => { + setHintVisible(searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)); + }, [tagResults, searchQuery]); + return ( @@ -142,6 +145,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({
)} ))} diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index f7ec94559..c95842c85 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -204,6 +204,14 @@ overflow: auto; } +.overflow-hidden { + overflow: hidden; +} + +.overflow-ellipsis { + text-overflow: ellipsis; +} + .items-start { align-items: flex-start; } @@ -220,6 +228,10 @@ white-space: pre-wrap; } +.whitespace-nowrap { + white-space: nowrap; +} + /** * A button that is just an icon. Separated from .sn-button because there * is almost no style overlap. From 48562e8b26c95075b4e083c1148eedfb6f044396 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 27 May 2021 14:12:12 -0300 Subject: [PATCH 41/96] styles: reduce tags max width --- app/assets/javascripts/components/NoteTags.tsx | 2 +- app/assets/javascripts/components/NotesContextMenu.tsx | 2 +- app/assets/javascripts/components/NotesOptions.tsx | 2 +- app/assets/javascripts/components/NotesOptionsPanel.tsx | 2 +- app/assets/stylesheets/_sn.scss | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 6327eea30..ad6fb70fd 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -35,7 +35,7 @@ const NoteTags = observer(({ application, appState }: Props) => { }} > - + {tag.title} diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index b215f0463..4940207eb 100644 --- a/app/assets/javascripts/components/NotesContextMenu.tsx +++ b/app/assets/javascripts/components/NotesContextMenu.tsx @@ -31,7 +31,7 @@ const NotesContextMenu = observer(({ appState }: Props) => { return appState.notes.contextMenuOpen ? (
{appState.tags.tags.map((tag) => ( + ))} + +
+ {overflowedTagsCount > 1 && tagsContainerCollapsed && ( - ))} - + )}
); }); diff --git a/app/assets/javascripts/components/NotesOptionsPanel.tsx b/app/assets/javascripts/components/NotesOptionsPanel.tsx index d9dea3b72..2b5a3bdbd 100644 --- a/app/assets/javascripts/components/NotesOptionsPanel.tsx +++ b/app/assets/javascripts/components/NotesOptionsPanel.tsx @@ -54,7 +54,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => { }} onBlur={closeOnBlur} ref={buttonRef} - className="sn-icon-button" + className="sn-icon-button mt-2" > Actions diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index 780517482..87cd64c80 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -24,7 +24,7 @@ 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.flex.items-start.justify-between.w-full( ng-show='self.note && !self.note.errorDecrypting' ) div.flex-grow( @@ -41,11 +41,10 @@ select-on-focus='true', spellcheck='false' ) - .editor-tags - note-tags( - application='self.application' - app-state='self.appState' - ) + note-tags( + application='self.application' + app-state='self.appState' + ) div.flex.items-center #save-status .message( From b7c2fa0b60a77195b8c8f0c7f6e62084437df248 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 31 May 2021 12:59:35 -0300 Subject: [PATCH 43/96] feat: use panel width event instead of ResizeObserver --- .../components/AutocompleteTagInput.tsx | 46 ++++---- .../javascripts/components/NoteTags.tsx | 65 +++++------- .../directives/views/panelResizer.ts | 7 ++ .../ui_models/app_state/active_note_state.ts | 100 ++++++++++++++++++ .../ui_models/app_state/app_state.ts | 10 +- .../ui_models/app_state/notes_state.ts | 52 +-------- .../javascripts/views/notes/notes-view.pug | 1 + .../javascripts/views/notes/notes_view.ts | 10 +- app/assets/stylesheets/_sn.scss | 4 + 9 files changed, 185 insertions(+), 110 deletions(-) create mode 100644 app/assets/javascripts/ui_models/app_state/active_note_state.ts diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 78bc1718a..2417e804f 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -10,7 +10,7 @@ import { AppState } from '@/ui_models/app_state'; type Props = { application: WebApplication; appState: AppState; - tagsRef: RefObject + tagsRef: RefObject; }; export const AutocompleteTagInput: FunctionalComponent = ({ @@ -25,7 +25,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const [hintVisible, setHintVisible] = useState(true); const getActiveNoteTagResults = (query: string) => { - const { activeNote } = appState.notes; + const { activeNote } = appState.activeNote; return application.searchTags(query, activeNote); }; @@ -41,10 +41,13 @@ export const AutocompleteTagInput: FunctionalComponent = ({ setTagResults(getActiveNoteTagResults('')); }; - const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => { - setDropdownVisible(visible); - clearResults(); - }); + const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur( + dropdownRef, + (visible: boolean) => { + setDropdownVisible(visible); + clearResults(); + } + ); const showDropdown = () => { const { clientHeight } = document.documentElement; @@ -61,7 +64,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const onTagOptionClick = async (tag: SNTag) => { setLockCloseOnBlur(true); - await appState.notes.addTagToActiveNote(tag); + await appState.activeNote.addTagToActiveNote(tag); inputRef.current.focus(); setTagResults(getActiveNoteTagResults(searchQuery)); setLockCloseOnBlur(false); @@ -69,7 +72,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const createAndAddNewTag = async () => { const newTag = await application.findOrCreateTag(searchQuery); - await appState.notes.addTagToActiveNote(newTag); + await appState.activeNote.addTagToActiveNote(newTag); clearResults(); inputRef.current.focus(); }; @@ -84,7 +87,9 @@ export const AutocompleteTagInput: FunctionalComponent = ({ }; useEffect(() => { - setHintVisible(searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)); + setHintVisible( + searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery) + ); }, [tagResults, searchQuery]); return ( @@ -100,7 +105,12 @@ export const AutocompleteTagInput: FunctionalComponent = ({ onBlur={closeOnBlur} onFocus={showDropdown} onKeyUp={(event) => { - if (event.key === 'Backspace' && searchQuery === '' && tagsRef.current && tagsRef.current.length > 1) { + if ( + event.key === 'Backspace' && + searchQuery === '' && + tagsRef.current && + tagsRef.current.length > 1 + ) { tagsRef.current[tagsRef.current.length - 1].focus(); } }} @@ -128,7 +138,8 @@ export const AutocompleteTagInput: FunctionalComponent = ({ = ({ onClick={onTagHintClick} onBlur={closeOnBlur} > - - Create new tag: - - - + Create new tag: + + {searchQuery} diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index a1c6cd798..1eaffc0e4 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -4,7 +4,7 @@ import { toDirective } from './utils'; import { Icon } from './Icon'; import { AutocompleteTagInput } from './AutocompleteTagInput'; import { WebApplication } from '@/ui_models/application'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { SNTag } from '@standardnotes/snjs'; type Props = { @@ -13,14 +13,14 @@ type Props = { }; const TAGS_ROW_RIGHT_MARGIN = 92; -const TAGS_ROW_HEIGHT = 32; +const TAGS_ROW_HEIGHT = 36; const MIN_OVERFLOW_TOP = 76; -const TAG_RIGHT_MARGIN = 8; +const TAGS_RIGHT_MARGIN = 8; const NoteTags = observer(({ application, appState }: Props) => { - const { activeNoteTags } = appState.notes; - const [tagsContainerMaxWidth, setTagsContainerMaxWidth] = - useState('auto'); + const { tags, tagsContainerPosition, tagsContainerMaxWidth } = + appState.activeNote; + const [overflowedTagsCount, setOverflowedTagsCount] = useState(0); const [overflowCountPosition, setOverflowCountPosition] = useState(0); const [tagsContainerCollapsed, setTagsContainerCollapsed] = useState(true); @@ -32,23 +32,28 @@ const NoteTags = observer(({ application, appState }: Props) => { tagsRef.current = []; const onTagBackspacePress = async (tag: SNTag) => { - await appState.notes.removeTagFromActiveNote(tag); + await appState.activeNote.removeTagFromActiveNote(tag); if (tagsRef.current.length > 1) { tagsRef.current[tagsRef.current.length - 1].focus(); } }; - const reloadOverflowCount = useCallback(() => { - const editorElement = document.getElementById('editor-column'); + const expandTags = () => { + setContainerHeight(tagsContainerRef.current.scrollHeight); + setTagsContainerCollapsed(false); + }; + + useEffect(() => { + appState.activeNote.reloadTagsContainerLayout(); let overflowCount = 0; for (const [index, tagElement] of tagsRef.current.entries()) { if (tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP) { if (overflowCount === 0) { setOverflowCountPosition( tagsRef.current[index - 1].getBoundingClientRect().right - - (editorElement ? editorElement.getBoundingClientRect().left : 0) + - TAG_RIGHT_MARGIN + (tagsContainerPosition ?? 0) + + TAGS_RIGHT_MARGIN ); } overflowCount += 1; @@ -59,34 +64,12 @@ const NoteTags = observer(({ application, appState }: Props) => { if (!tagsContainerCollapsed) { setContainerHeight(tagsContainerRef.current.scrollHeight); } - }, [tagsContainerCollapsed]); - - const expandTags = () => { - setContainerHeight(tagsContainerRef.current.scrollHeight); - setTagsContainerCollapsed(false); - }; - - useEffect(() => { - const editorElement = document.getElementById('editor-column'); - const resizeObserver = new ResizeObserver((entries) => { - const entry = entries[0]; - const { width } = entry.contentRect; - setTagsContainerMaxWidth(width); - reloadOverflowCount(); - }); - - if (editorElement) { - resizeObserver.observe(editorElement); - } - - return () => { - resizeObserver.disconnect(); - }; - }, [reloadOverflowCount]); - - useEffect(() => { - reloadOverflowCount(); - }, [activeNoteTags, reloadOverflowCount]); + }, [ + appState.activeNote, + tags, + tagsContainerCollapsed, + tagsContainerPosition, + ]); const tagClass = `bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center mt-2 mr-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; @@ -99,7 +82,7 @@ const NoteTags = observer(({ application, appState }: Props) => { >
{ marginRight: TAGS_ROW_RIGHT_MARGIN, }} > - {activeNoteTags.map((tag, index) => ( + {tags.map((tag: SNTag, index: number) => ( )}
From 70e4425e20a3dc0334e56646c8146a12851876ca Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 31 May 2021 14:42:03 -0300 Subject: [PATCH 45/96] feat: set tabindex depending on overflowed tags --- app/assets/javascripts/components/AutocompleteTagInput.tsx | 3 +++ app/assets/javascripts/components/NoteTags.tsx | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 2417e804f..413b85c42 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -11,12 +11,14 @@ type Props = { application: WebApplication; appState: AppState; tagsRef: RefObject; + tabIndex: number; }; export const AutocompleteTagInput: FunctionalComponent = ({ application, appState, tagsRef, + tabIndex, }) => { const [searchQuery, setSearchQuery] = useState(''); const [dropdownVisible, setDropdownVisible] = useState(false); @@ -102,6 +104,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ onChange={onSearchQueryChange} type="text" placeholder="Add tag" + tabIndex={tabIndex} onBlur={closeOnBlur} onFocus={showDropdown} onKeyUp={(event) => { diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 3c2e2e4ba..4c34c40d4 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -99,6 +99,8 @@ const NoteTags = observer(({ application, appState }: Props) => { const tagClass = `bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center mt-2 mr-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; + const overflowedTags = tagsContainerCollapsed && overflowCount > 0; + return (
{ onTagBackspacePress(tag); } }} + tabIndex={isTagOverflowed(tagsRef.current[index]) ? -1 : 0} > { application={application} appState={appState} tagsRef={tagsRef} + tabIndex={overflowedTags ? -1 : 0} />
{overflowCount > 1 && tagsContainerCollapsed && ( From b54de00b40102eb4b9de35f96c947b15c073cecb Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 31 May 2021 16:27:17 -0300 Subject: [PATCH 46/96] fix: fix tags container width --- .../javascripts/components/NoteTags.tsx | 36 ++++++------ .../components/NotesOptionsPanel.tsx | 2 +- .../ui_models/app_state/active_note_state.ts | 28 ++++++++- .../javascripts/views/editor/editor-view.pug | 57 ++++++++++--------- 4 files changed, 73 insertions(+), 50 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 4c34c40d4..217b10620 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -12,20 +12,23 @@ type Props = { appState: AppState; }; -const TAGS_ROW_RIGHT_MARGIN = 92; const TAGS_ROW_HEIGHT = 36; const MIN_OVERFLOW_TOP = 76; -const TAGS_RIGHT_MARGIN = 8; +const TAG_RIGHT_MARGIN = 8; const NoteTags = observer(({ application, appState }: Props) => { - const { tags, tagsContainerPosition, tagsContainerMaxWidth } = - appState.activeNote; + const { + overflowedTagsCount, + tags, + tagsContainerPosition, + tagsContainerMaxWidth, + tagsContainerCollapsed, + tagsOverflowed, + } = appState.activeNote; - const [tagsContainerCollapsed, setTagsContainerCollapsed] = useState(true); const [tagsContainerHeight, setTagsContainerHeight] = useState(TAGS_ROW_HEIGHT); const [overflowCountPosition, setOverflowCountPosition] = useState(0); - const [overflowCount, setOverflowCount] = useState(0); const tagsContainerRef = useRef(); const tagsRef = useRef([]); @@ -39,7 +42,7 @@ const NoteTags = observer(({ application, appState }: Props) => { }; const expandTags = () => { - setTagsContainerCollapsed(false); + appState.activeNote.setTagsContainerCollapsed(false); }; const isTagOverflowed = useCallback( @@ -65,7 +68,7 @@ const NoteTags = observer(({ application, appState }: Props) => { const previousTagRect = tagsRef.current[firstOverflowedTagIndex - 1].getBoundingClientRect(); const position = - previousTagRect.right - (tagsContainerPosition ?? 0) + TAGS_RIGHT_MARGIN; + previousTagRect.right - (tagsContainerPosition ?? 0) + TAG_RIGHT_MARGIN; setOverflowCountPosition(position); }, [isTagOverflowed, tagsContainerCollapsed, tagsContainerPosition]); @@ -80,8 +83,8 @@ const NoteTags = observer(({ application, appState }: Props) => { const count = tagsRef.current.filter((tagElement) => isTagOverflowed(tagElement) ).length; - setOverflowCount(count); - }, [isTagOverflowed]); + appState.activeNote.setOverflowedTagsCount(count); + }, [appState.activeNote, isTagOverflowed]); useEffect(() => { appState.activeNote.reloadTagsContainerLayout(); @@ -97,9 +100,7 @@ const NoteTags = observer(({ application, appState }: Props) => { ]); const tagClass = `bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center - mt-2 mr-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; - - const overflowedTags = tagsContainerCollapsed && overflowCount > 0; + mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; return (
@@ -111,12 +112,11 @@ const NoteTags = observer(({ application, appState }: Props) => { style={{ maxWidth: tagsContainerMaxWidth, height: TAGS_ROW_HEIGHT, - marginRight: TAGS_ROW_RIGHT_MARGIN, }} > {tags.map((tag: SNTag, index: number) => (
- {overflowCount > 1 && tagsContainerCollapsed && ( + {overflowedTagsCount > 1 && tagsContainerCollapsed && ( )}
diff --git a/app/assets/javascripts/components/NotesOptionsPanel.tsx b/app/assets/javascripts/components/NotesOptionsPanel.tsx index 2b5a3bdbd..d9dea3b72 100644 --- a/app/assets/javascripts/components/NotesOptionsPanel.tsx +++ b/app/assets/javascripts/components/NotesOptionsPanel.tsx @@ -54,7 +54,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => { }} onBlur={closeOnBlur} ref={buttonRef} - className="sn-icon-button mt-2" + className="sn-icon-button" > Actions diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 253d88ca7..3e91cd318 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -5,6 +5,7 @@ import { } from '@standardnotes/snjs'; import { action, + computed, makeObservable, observable, } from 'mobx'; @@ -15,6 +16,8 @@ export class ActiveNoteState { tags: SNTag[] = []; tagsContainerPosition? = 0; tagsContainerMaxWidth: number | 'auto' = 'auto'; + tagsContainerCollapsed = true; + overflowedTagsCount = 0; constructor( private application: WebApplication, @@ -25,9 +28,15 @@ export class ActiveNoteState { tags: observable, tagsContainerPosition: observable, tagsContainerMaxWidth: observable, + tagsContainerCollapsed: observable, + overflowedTagsCount: observable, + + tagsOverflowed: computed, setTagsContainerPosition: action, setTagsContainerMaxWidth: action, + setTagsContainerCollapsed: action, + setOverflowedTagsCount: action, reloadTags: action, }); @@ -49,6 +58,10 @@ export class ActiveNoteState { return this.appState.notes.activeEditor?.note; } + get tagsOverflowed(): boolean { + return this.overflowedTagsCount > 0 && this.tagsContainerCollapsed; + } + setTagsContainerPosition(position: number): void { this.tagsContainerPosition = position; } @@ -57,6 +70,14 @@ export class ActiveNoteState { this.tagsContainerMaxWidth = width; } + setTagsContainerCollapsed(collapsed: boolean): void { + this.tagsContainerCollapsed = collapsed; + } + + setOverflowedTagsCount(count: number): void { + this.overflowedTagsCount = count; + } + reloadTags(): void { const { activeNote } = this; if (activeNote) { @@ -65,14 +86,15 @@ export class ActiveNoteState { } reloadTagsContainerLayout(): void { - const editorElementId = 'editor-column'; + const MARGIN = this.tagsContainerCollapsed ? 68 : 24; + const EDITOR_ELEMENT_ID = 'editor-column'; const { clientWidth } = document.documentElement; const editorPosition = - document.getElementById(editorElementId)?.getBoundingClientRect() + document.getElementById(EDITOR_ELEMENT_ID)?.getBoundingClientRect() .left ?? 0; this.appState.activeNote.setTagsContainerPosition(editorPosition); this.appState.activeNote.setTagsContainerMaxWidth( - clientWidth - editorPosition + clientWidth - editorPosition - MARGIN ); } diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index 87cd64c80..11c55c533 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -24,37 +24,38 @@ ng-if="self.showLockedIcon" ) | {{self.lockText}} - #editor-title-bar.section-title-bar.flex.items-start.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}" + div.flex.items-start.justify-between + 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( + application='self.application' + app-state='self.appState' ) - .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' - ) - note-tags( - application='self.application' - app-state='self.appState' - ) - 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' - ) .sn-component(ng-if='self.note') #editor-menu-bar.sk-app-bar.no-edges .left From b5906ecf788fdc1832ea6b205aefb1cb4bef0038 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 31 May 2021 17:58:46 -0300 Subject: [PATCH 47/96] feat: collapse tags on click outside --- .../javascripts/components/NoteTags.tsx | 50 ++++++++++++------- .../components/NotesContextMenu.tsx | 20 +++----- app/assets/javascripts/components/utils.ts | 21 ++++++++ .../ui_models/app_state/active_note_state.ts | 14 +++--- 4 files changed, 66 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 217b10620..ab7a84620 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -1,6 +1,6 @@ import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; -import { toDirective } from './utils'; +import { toDirective, useCloseOnClickOutside } from './utils'; import { Icon } from './Icon'; import { AutocompleteTagInput } from './AutocompleteTagInput'; import { WebApplication } from '@/ui_models/application'; @@ -22,7 +22,7 @@ const NoteTags = observer(({ application, appState }: Props) => { tags, tagsContainerPosition, tagsContainerMaxWidth, - tagsContainerCollapsed, + tagsContainerExpanded, tagsOverflowed, } = appState.activeNote; @@ -30,8 +30,19 @@ const NoteTags = observer(({ application, appState }: Props) => { useState(TAGS_ROW_HEIGHT); const [overflowCountPosition, setOverflowCountPosition] = useState(0); + const containerRef = useRef(); const tagsContainerRef = useRef(); const tagsRef = useRef([]); + const overflowButtonRef = useRef(); + + useCloseOnClickOutside( + tagsContainerRef, + (expanded: boolean) => { + if (overflowButtonRef.current || tagsContainerExpanded) { + appState.activeNote.setTagsContainerExpanded(expanded); + } + } + ); const onTagBackspacePress = async (tag: SNTag) => { await appState.activeNote.removeTagFromActiveNote(tag); @@ -41,28 +52,24 @@ const NoteTags = observer(({ application, appState }: Props) => { } }; - const expandTags = () => { - appState.activeNote.setTagsContainerCollapsed(false); - }; - const isTagOverflowed = useCallback( (tagElement?: HTMLButtonElement): boolean | undefined => { if (!tagElement) { return; } - if (!tagsContainerCollapsed) { + if (tagsContainerExpanded) { return false; } return tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP; }, - [tagsContainerCollapsed] + [tagsContainerExpanded] ); const reloadOverflowCountPosition = useCallback(() => { const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) => isTagOverflowed(tagElement) ); - if (!tagsContainerCollapsed || firstOverflowedTagIndex < 1) { + if (tagsContainerExpanded || firstOverflowedTagIndex < 1) { return; } const previousTagRect = @@ -70,14 +77,14 @@ const NoteTags = observer(({ application, appState }: Props) => { const position = previousTagRect.right - (tagsContainerPosition ?? 0) + TAG_RIGHT_MARGIN; setOverflowCountPosition(position); - }, [isTagOverflowed, tagsContainerCollapsed, tagsContainerPosition]); + }, [isTagOverflowed, tagsContainerExpanded, tagsContainerPosition]); const reloadTagsContainerHeight = useCallback(() => { - const height = tagsContainerCollapsed - ? TAGS_ROW_HEIGHT - : tagsContainerRef.current.scrollHeight; + const height = tagsContainerExpanded + ? tagsContainerRef.current.scrollHeight + : TAGS_ROW_HEIGHT; setTagsContainerHeight(height); - }, [tagsContainerCollapsed]); + }, [tagsContainerExpanded]); const reloadOverflowCount = useCallback(() => { const count = tagsRef.current.filter((tagElement) => @@ -86,6 +93,10 @@ const NoteTags = observer(({ application, appState }: Props) => { appState.activeNote.setOverflowedTagsCount(count); }, [appState.activeNote, isTagOverflowed]); + const setTagsContainerExpanded = (expanded: boolean) => { + appState.activeNote.setTagsContainerExpanded(expanded); + }; + useEffect(() => { appState.activeNote.reloadTagsContainerLayout(); reloadOverflowCountPosition(); @@ -103,11 +114,11 @@ const NoteTags = observer(({ application, appState }: Props) => { mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; return ( -
+
{ tabIndex={tagsOverflowed ? -1 : 0} />
- {overflowedTagsCount > 1 && tagsContainerCollapsed && ( + {tagsOverflowed && ( diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index 4940207eb..fa21ac7eb 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,18 +15,10 @@ 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 ? (
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/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 3e91cd318..521090f70 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -16,7 +16,7 @@ export class ActiveNoteState { tags: SNTag[] = []; tagsContainerPosition? = 0; tagsContainerMaxWidth: number | 'auto' = 'auto'; - tagsContainerCollapsed = true; + tagsContainerExpanded = false; overflowedTagsCount = 0; constructor( @@ -28,14 +28,14 @@ export class ActiveNoteState { tags: observable, tagsContainerPosition: observable, tagsContainerMaxWidth: observable, - tagsContainerCollapsed: observable, + tagsContainerExpanded: observable, overflowedTagsCount: observable, tagsOverflowed: computed, setTagsContainerPosition: action, setTagsContainerMaxWidth: action, - setTagsContainerCollapsed: action, + setTagsContainerExpanded: action, setOverflowedTagsCount: action, reloadTags: action, }); @@ -59,7 +59,7 @@ export class ActiveNoteState { } get tagsOverflowed(): boolean { - return this.overflowedTagsCount > 0 && this.tagsContainerCollapsed; + return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded; } setTagsContainerPosition(position: number): void { @@ -70,8 +70,8 @@ export class ActiveNoteState { this.tagsContainerMaxWidth = width; } - setTagsContainerCollapsed(collapsed: boolean): void { - this.tagsContainerCollapsed = collapsed; + setTagsContainerExpanded(expanded: boolean): void { + this.tagsContainerExpanded = expanded; } setOverflowedTagsCount(count: number): void { @@ -86,7 +86,7 @@ export class ActiveNoteState { } reloadTagsContainerLayout(): void { - const MARGIN = this.tagsContainerCollapsed ? 68 : 24; + const MARGIN = this.tagsContainerExpanded ? 68 : 24; const EDITOR_ELEMENT_ID = 'editor-column'; const { clientWidth } = document.documentElement; const editorPosition = From bf614447950053b9a2086800032029bb047d4817 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 31 May 2021 18:12:33 -0300 Subject: [PATCH 48/96] styles: add animation for tags expanding/collapsing --- .../javascripts/components/NoteTags.tsx | 23 +++++++++---------- app/assets/stylesheets/_sn.scss | 4 ++++ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index ab7a84620..ce9ad8de1 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -35,14 +35,11 @@ const NoteTags = observer(({ application, appState }: Props) => { const tagsRef = useRef([]); const overflowButtonRef = useRef(); - useCloseOnClickOutside( - tagsContainerRef, - (expanded: boolean) => { - if (overflowButtonRef.current || tagsContainerExpanded) { - appState.activeNote.setTagsContainerExpanded(expanded); - } + useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => { + if (overflowButtonRef.current || tagsContainerExpanded) { + appState.activeNote.setTagsContainerExpanded(expanded); } - ); + }); const onTagBackspacePress = async (tag: SNTag) => { await appState.activeNote.removeTagFromActiveNote(tag); @@ -114,15 +111,17 @@ const NoteTags = observer(({ application, appState }: Props) => { mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; return ( -
+
{tags.map((tag: SNTag, index: number) => ( diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 4d4746c82..56744c242 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -236,6 +236,10 @@ white-space: nowrap; } +.transition-height { + transition-property: height; +} + /** * A button that is just an icon. Separated from .sn-button because there * is almost no style overlap. From 6d1f6c6f24375df531886f6ebbdf017c475ce95d Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 31 May 2021 18:57:49 -0300 Subject: [PATCH 49/96] fix: revert to using fixed margin when setting container width --- app/assets/javascripts/ui_models/app_state/active_note_state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 521090f70..4fd33550c 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -86,7 +86,7 @@ export class ActiveNoteState { } reloadTagsContainerLayout(): void { - const MARGIN = this.tagsContainerExpanded ? 68 : 24; + const MARGIN = 72; const EDITOR_ELEMENT_ID = 'editor-column'; const { clientWidth } = document.documentElement; const editorPosition = From 31fbf2ce35f860050fb95f595d290e491ecf1263 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 31 May 2021 19:26:26 -0300 Subject: [PATCH 50/96] fix: fix tags dropdown width and tags container height --- .../components/AutocompleteTagInput.tsx | 4 +-- .../javascripts/components/NoteTags.tsx | 27 +++++++++++++------ app/assets/stylesheets/_sn.scss | 8 ++++++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 413b85c42..0ae19226a 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -99,7 +99,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ = ({ {dropdownVisible && (
diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index ce9ad8de1..6a071eb5d 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -26,6 +26,8 @@ const NoteTags = observer(({ application, appState }: Props) => { tagsOverflowed, } = appState.activeNote; + const [containerHeight, setContainerHeight] = + useState(TAGS_ROW_HEIGHT); const [tagsContainerHeight, setTagsContainerHeight] = useState(TAGS_ROW_HEIGHT); const [overflowCountPosition, setOverflowCountPosition] = useState(0); @@ -76,11 +78,18 @@ const NoteTags = observer(({ application, appState }: Props) => { setOverflowCountPosition(position); }, [isTagOverflowed, tagsContainerExpanded, tagsContainerPosition]); - const reloadTagsContainerHeight = useCallback(() => { - const height = tagsContainerExpanded + const reloadContainersHeight = useCallback(() => { + const containerHeight = tagsContainerExpanded ? tagsContainerRef.current.scrollHeight : TAGS_ROW_HEIGHT; - setTagsContainerHeight(height); + setContainerHeight(containerHeight); + + const firstTagTop = tagsRef.current[0].getBoundingClientRect().top; + const lastTagBottom = tagsRef.current[tagsRef.current.length - 1].getBoundingClientRect().bottom; + const tagsContainerHeight = tagsContainerExpanded + ? lastTagBottom - firstTagTop + : TAGS_ROW_HEIGHT; + setTagsContainerHeight(tagsContainerHeight); }, [tagsContainerExpanded]); const reloadOverflowCount = useCallback(() => { @@ -97,28 +106,30 @@ const NoteTags = observer(({ application, appState }: Props) => { useEffect(() => { appState.activeNote.reloadTagsContainerLayout(); reloadOverflowCountPosition(); - reloadTagsContainerHeight(); + reloadContainersHeight(); reloadOverflowCount(); }, [ appState.activeNote, reloadOverflowCountPosition, - reloadTagsContainerHeight, + reloadContainersHeight, reloadOverflowCount, tags, ]); - const tagClass = `bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center + const tagClass = `h-6 bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; return (
Date: Mon, 31 May 2021 19:29:08 -0300 Subject: [PATCH 51/96] styles: fix hint tag styles --- app/assets/javascripts/components/AutocompleteTagInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 0ae19226a..d853526e7 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -166,7 +166,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ onBlur={closeOnBlur} > Create new tag: - + Date: Mon, 31 May 2021 19:47:21 -0300 Subject: [PATCH 52/96] feat: clicking on tag sets it as active --- app/assets/javascripts/components/NoteTags.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 6a071eb5d..259835ca2 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -51,6 +51,13 @@ const NoteTags = observer(({ application, appState }: Props) => { } }; + const onTagClick = (clickedTag: SNTag) => { + const tagIndex = tags.findIndex(tag => tag.uuid === clickedTag.uuid); + if (tagsRef.current[tagIndex] === document.activeElement) { + appState.setSelectedTag(clickedTag); + } + }; + const isTagOverflowed = useCallback( (tagElement?: HTMLButtonElement): boolean | undefined => { if (!tagElement) { @@ -99,8 +106,8 @@ const NoteTags = observer(({ application, appState }: Props) => { appState.activeNote.setOverflowedTagsCount(count); }, [appState.activeNote, isTagOverflowed]); - const setTagsContainerExpanded = (expanded: boolean) => { - appState.activeNote.setTagsContainerExpanded(expanded); + const expandTags = () => { + appState.activeNote.setTagsContainerExpanded(true); }; useEffect(() => { @@ -144,6 +151,7 @@ const NoteTags = observer(({ application, appState }: Props) => { tagsRef.current[index] = element; } }} + onClick={() => onTagClick(tag)} onKeyUp={(event) => { if (event.key === 'Backspace') { onTagBackspacePress(tag); @@ -173,9 +181,7 @@ const NoteTags = observer(({ application, appState }: Props) => { type="button" className={`${tagClass} pl-2 absolute`} style={{ left: overflowCountPosition }} - onClick={() => { - setTagsContainerExpanded(true); - }} + onClick={expandTags} > +{overflowedTagsCount} From 1aebe44dcdd602903bb4f906459d68f2c9575aca Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 1 Jun 2021 12:17:06 -0300 Subject: [PATCH 53/96] feat: use opacity for overflowed tags animation --- .../javascripts/components/NoteTags.tsx | 19 +++++++------------ app/assets/stylesheets/_sn.scss | 12 ++++++++++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 259835ca2..112877416 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -28,8 +28,6 @@ const NoteTags = observer(({ application, appState }: Props) => { const [containerHeight, setContainerHeight] = useState(TAGS_ROW_HEIGHT); - const [tagsContainerHeight, setTagsContainerHeight] = - useState(TAGS_ROW_HEIGHT); const [overflowCountPosition, setOverflowCountPosition] = useState(0); const containerRef = useRef(); @@ -90,13 +88,6 @@ const NoteTags = observer(({ application, appState }: Props) => { ? tagsContainerRef.current.scrollHeight : TAGS_ROW_HEIGHT; setContainerHeight(containerHeight); - - const firstTagTop = tagsRef.current[0].getBoundingClientRect().top; - const lastTagBottom = tagsRef.current[tagsRef.current.length - 1].getBoundingClientRect().bottom; - const tagsContainerHeight = tagsContainerExpanded - ? lastTagBottom - firstTagTop - : TAGS_ROW_HEIGHT; - setTagsContainerHeight(tagsContainerHeight); }, [tagsContainerExpanded]); const reloadOverflowCount = useCallback(() => { @@ -134,17 +125,21 @@ const NoteTags = observer(({ application, appState }: Props) => { >
{tags.map((tag: SNTag, index: number) => ( - ))} + {tags.map((tag: SNTag, index: number) => { + const overflowed = + !tagsContainerExpanded && + lastVisibleTagIndex && + index > lastVisibleTagIndex; + return ( + + ); + })} { diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 4fd33550c..2b8c10db8 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -14,7 +14,6 @@ import { AppState } from './app_state'; export class ActiveNoteState { tags: SNTag[] = []; - tagsContainerPosition? = 0; tagsContainerMaxWidth: number | 'auto' = 'auto'; tagsContainerExpanded = false; overflowedTagsCount = 0; @@ -26,24 +25,18 @@ export class ActiveNoteState { ) { makeObservable(this, { tags: observable, - tagsContainerPosition: observable, tagsContainerMaxWidth: observable, tagsContainerExpanded: observable, overflowedTagsCount: observable, tagsOverflowed: computed, - setTagsContainerPosition: action, setTagsContainerMaxWidth: action, setTagsContainerExpanded: action, setOverflowedTagsCount: action, reloadTags: action, }); - this.tagsContainerPosition = document - .getElementById('editor-column') - ?.getBoundingClientRect().left; - appEventListeners.push( application.streamItems( ContentType.Tag, @@ -62,10 +55,6 @@ export class ActiveNoteState { return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded; } - setTagsContainerPosition(position: number): void { - this.tagsContainerPosition = position; - } - setTagsContainerMaxWidth(width: number): void { this.tagsContainerMaxWidth = width; } @@ -86,16 +75,19 @@ export class ActiveNoteState { } reloadTagsContainerLayout(): void { - const MARGIN = 72; const EDITOR_ELEMENT_ID = 'editor-column'; - const { clientWidth } = document.documentElement; - const editorPosition = - document.getElementById(EDITOR_ELEMENT_ID)?.getBoundingClientRect() - .left ?? 0; - this.appState.activeNote.setTagsContainerPosition(editorPosition); - this.appState.activeNote.setTagsContainerMaxWidth( - clientWidth - editorPosition - MARGIN - ); + const defaultFontSize = window.getComputedStyle( + document.documentElement + ).fontSize; + const containerMargins = parseFloat(defaultFontSize) * 4; + const editorWidth = + document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth; + + if (editorWidth) { + this.appState.activeNote.setTagsContainerMaxWidth( + editorWidth - containerMargins + ); + } } async addTagToActiveNote(tag: SNTag): Promise { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 32091c59a..c3e7e0f4f 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -57,6 +57,10 @@ margin-bottom: 0.5rem; } +.ml-1 { + margin-left: 0.25rem; +} + .mr-1 { margin-right: 0.25rem; } @@ -192,6 +196,10 @@ height: 2rem; } +.h-9 { + height: 2.25rem; +} + .h-10 { height: 2.5rem; } @@ -212,8 +220,8 @@ overflow: auto; } -.overflow-hidden { - overflow: hidden; +.overflow-y-hidden { + overflow-y: hidden; } .overflow-ellipsis { @@ -260,6 +268,10 @@ width: 20rem; } +.relative { + position: relative; +} + /** * A button that is just an icon. Separated from .sn-button because there * is almost no style overlap. From 8bcd8c31c90c8971eb8e54461f3920677bf8a95a Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 1 Jun 2021 18:36:20 -0300 Subject: [PATCH 57/96] fix: fix typo and remove log --- app/assets/javascripts/components/NoteTags.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 1edf7e28e..a77e1894d 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -99,7 +99,6 @@ const NoteTags = observer(({ application, appState }: Props) => { } const { offsetLeft: lastVisibleTagLeft, clientWidth: lastVisibleTagWidth } = tagsRef.current[lastVisibleTagIndex]; - console.log(tagsRef.current[0].offsetLeft); setOverflowCountPosition(lastVisibleTagLeft + lastVisibleTagWidth); }, [lastVisibleTagIndex, tagsContainerExpanded]); @@ -122,7 +121,7 @@ const NoteTags = observer(({ application, appState }: Props) => { ]); useEffect(() => { - reloadTagsContainerLayout; + reloadTagsContainerLayout(); }, [ reloadTagsContainerLayout, tags, From 19a85a1cc0425162591f3c433501a8c92f62a476 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 1 Jun 2021 18:48:22 -0300 Subject: [PATCH 58/96] chore(deps): upgrade typescript --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 10df113dd..67dd3f9db 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 2c9d723f8..9896080d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From af3bed850e0545bfac0488fcbbc8c4d34eb90a79 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 1 Jun 2021 19:08:52 -0300 Subject: [PATCH 59/96] fix: use ellipsis for tags text in dropdown --- .../components/AutocompleteTagInput.tsx | 38 +++++++++++-------- .../javascripts/components/NoteTags.tsx | 4 +- app/assets/stylesheets/_sn.scss | 12 +++++- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 0fcc89edd..799cafcf2 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -135,22 +135,28 @@ export const AutocompleteTagInput: FunctionalComponent = ({ onBlur={closeOnBlur} tabIndex={tabIndex} > - - {tag.title - .split(new RegExp(`(${searchQuery})`, 'gi')) - .map((substring, index) => ( - - {substring} - - ))} + + + {searchQuery === '' ? ( + tag.title + ) : ( + tag.title + .split(new RegExp(`(${searchQuery})`, 'gi')) + .map((substring, index) => ( + + {substring} + + )) + )} + ); })} diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index a77e1894d..8720bffd6 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -156,7 +156,7 @@ const NoteTags = observer(({ application, appState }: Props) => {
{ type="hashtag" className="sn-icon--small color-neutral mr-1" /> - + {tag.title} diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index c3e7e0f4f..b6d7e68d7 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -168,6 +168,10 @@ max-width: 20rem; } +.min-w-5 { + min-width: 1.25rem; +} + .h-1px { height: 1px; } @@ -208,6 +212,10 @@ max-height: 30rem; } +.min-h-5 { + min-height: 1.25rem; +} + .fixed { position: fixed; } @@ -220,8 +228,8 @@ overflow: auto; } -.overflow-y-hidden { - overflow-y: hidden; +.overflow-hidden { + overflow: hidden; } .overflow-ellipsis { From a071d4c9d025b91f89119586aa9b81a7615a60d1 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 1 Jun 2021 19:50:03 -0300 Subject: [PATCH 60/96] fix: check if ref is present before setting overflow position --- app/assets/javascripts/components/NoteTags.tsx | 8 +++++--- .../javascripts/ui_models/app_state/active_note_state.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 8720bffd6..5effdac7c 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -97,9 +97,11 @@ const NoteTags = observer(({ application, appState }: Props) => { if (tagsContainerExpanded || !lastVisibleTagIndex) { return; } - const { offsetLeft: lastVisibleTagLeft, clientWidth: lastVisibleTagWidth } = + if (tagsRef.current[lastVisibleTagIndex]) { + const { offsetLeft: lastVisibleTagLeft, clientWidth: lastVisibleTagWidth } = tagsRef.current[lastVisibleTagIndex]; - setOverflowCountPosition(lastVisibleTagLeft + lastVisibleTagWidth); + setOverflowCountPosition(lastVisibleTagLeft + lastVisibleTagWidth); + } }, [lastVisibleTagIndex, tagsContainerExpanded]); const expandTags = () => { @@ -142,7 +144,7 @@ const NoteTags = observer(({ application, appState }: Props) => { tagResizeObserver.disconnect(); } }; - }, [reloadTagsContainerLayout]); + }, [reloadTagsContainerLayout, tags]); const tagClass = `h-6 bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 2b8c10db8..f853f7fa6 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -14,7 +14,7 @@ import { AppState } from './app_state'; export class ActiveNoteState { tags: SNTag[] = []; - tagsContainerMaxWidth: number | 'auto' = 'auto'; + tagsContainerMaxWidth: number | 'auto' = 0; tagsContainerExpanded = false; overflowedTagsCount = 0; From 684a3fb0bf4236c8ec556470216ed39d6fe7b187 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Tue, 1 Jun 2021 20:55:54 -0300 Subject: [PATCH 61/96] feat: add delete tag button and refactor NoteTag to separate component --- app/assets/javascripts/app.ts | 4 +- app/assets/javascripts/components/NoteTag.tsx | 91 +++++++++++++++++++ .../{NoteTags.tsx => NoteTagsContainer.tsx} | 76 ++++------------ .../ui_models/app_state/active_note_state.ts | 12 ++- .../javascripts/views/editor/editor-view.pug | 2 +- app/assets/stylesheets/_sn.scss | 32 +++++++ 6 files changed, 154 insertions(+), 63 deletions(-) create mode 100644 app/assets/javascripts/components/NoteTag.tsx rename app/assets/javascripts/components/{NoteTags.tsx => NoteTagsContainer.tsx} (69%) diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index eead8b4e8..5680c8704 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -64,7 +64,7 @@ import { MultipleSelectedNotesDirective } from './components/MultipleSelectedNot import { NotesContextMenuDirective } from './components/NotesContextMenu'; import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel'; import { IconDirective } from './components/Icon'; -import { NoteTagsDirective } from './components/NoteTags'; +import { NoteTagsContainerDirective } from './components/NoteTagsContainer'; function reloadHiddenFirefoxTab(): boolean { /** @@ -159,7 +159,7 @@ const startApplication: StartApplication = async function startApplication( .directive('notesContextMenu', NotesContextMenuDirective) .directive('notesOptionsPanel', NotesOptionsPanelDirective) .directive('icon', IconDirective) - .directive('noteTags', NoteTagsDirective); + .directive('noteTagsContainer', NoteTagsContainerDirective); // Filters angular.module('app').filter('trusted', ['$sce', trusted]); diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx new file mode 100644 index 000000000..f3f5a0d8d --- /dev/null +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -0,0 +1,91 @@ +import { Icon } from './Icon'; +import { FunctionalComponent, RefObject } from 'preact'; +import { useRef, useState } from 'preact/hooks'; +import { AppState } from '@/ui_models/app_state'; +import { SNTag } from '@standardnotes/snjs/dist/@types'; + +type Props = { + appState: AppState; + index: number; + tagsRef: RefObject; + tag: SNTag; + overflowed: boolean; + maxWidth: number | 'auto'; +}; + +export const NoteTag: FunctionalComponent = ({ + appState, + index, + tagsRef, + tag, + overflowed, + maxWidth, +}) => { + const [showDeleteButton, setShowDeleteButton] = useState(false); + const deleteTagRef = useRef(); + + const deleteTag = async () => { + await appState.activeNote.removeTagFromActiveNote(tag); + + if (index > 0 && tagsRef.current) { + tagsRef.current[index - 1].focus(); + } + }; + + const onTagClick = () => { + appState.setSelectedTag(tag); + }; + + const onFocus = () => { + appState.activeNote.setTagFocused(true); + setShowDeleteButton(true); + }; + + const onBlur = (event: FocusEvent) => { + appState.activeNote.setTagFocused(false); + if ((event.relatedTarget as Node) !== deleteTagRef.current) { + setShowDeleteButton(false); + } + }; + + return ( + + )} + + ); +}; diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTagsContainer.tsx similarity index 69% rename from app/assets/javascripts/components/NoteTags.tsx rename to app/assets/javascripts/components/NoteTagsContainer.tsx index 5effdac7c..0b5724417 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTagsContainer.tsx @@ -1,18 +1,18 @@ import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; import { toDirective, useCloseOnClickOutside } from './utils'; -import { Icon } from './Icon'; import { AutocompleteTagInput } from './AutocompleteTagInput'; import { WebApplication } from '@/ui_models/application'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { SNTag } from '@standardnotes/snjs'; +import { NoteTag } from './NoteTag'; type Props = { application: WebApplication; appState: AppState; }; -const NoteTags = observer(({ application, appState }: Props) => { +const NoteTagsContainer = observer(({ application, appState }: Props) => { const { overflowedTagsCount, tags, @@ -29,31 +29,15 @@ const NoteTags = observer(({ application, appState }: Props) => { const containerRef = useRef(); const tagsContainerRef = useRef(); const tagsRef = useRef([]); - const overflowButtonRef = useRef(); tagsRef.current = []; useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => { - if (overflowButtonRef.current || tagsContainerExpanded) { + if (tagsContainerExpanded) { appState.activeNote.setTagsContainerExpanded(expanded); } }); - const onTagBackspacePress = async (tag: SNTag, index: number) => { - await appState.activeNote.removeTagFromActiveNote(tag); - - if (index > 0) { - tagsRef.current[index - 1].focus(); - } - }; - - const onTagClick = (clickedTag: SNTag) => { - const tagIndex = tags.findIndex((tag) => tag.uuid === clickedTag.uuid); - if (tagsRef.current[tagIndex] === document.activeElement) { - appState.setSelectedTag(clickedTag); - } - }; - const isTagOverflowed = useCallback( (tagElement?: HTMLButtonElement): boolean | undefined => { if (!tagElement) { @@ -144,10 +128,7 @@ const NoteTags = observer(({ application, appState }: Props) => { tagResizeObserver.disconnect(); } }; - }, [reloadTagsContainerLayout, tags]); - - const tagClass = `h-6 bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center - mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; + }, [reloadTagsContainerLayout]); return (
{ maxWidth: tagsContainerMaxWidth, }} > - {tags.map((tag: SNTag, index: number) => { - const overflowed = - !tagsContainerExpanded && - lastVisibleTagIndex && - index > lastVisibleTagIndex; - return ( - - ); - })} + {tags.map((tag: SNTag, index: number) => ( + lastVisibleTagIndex} + /> + ))} {
{tagsOverflowed && ( + {contextMenuOpen && ( +
- - + +
)} - + ); }; diff --git a/app/assets/javascripts/components/NoteTagsContainer.tsx b/app/assets/javascripts/components/NoteTagsContainer.tsx index aa4cee774..8627df267 100644 --- a/app/assets/javascripts/components/NoteTagsContainer.tsx +++ b/app/assets/javascripts/components/NoteTagsContainer.tsx @@ -88,7 +88,6 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => { key={tag.uuid} appState={appState} tag={tag} - overflowButtonRef={overflowButtonRef} /> ))} diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 049c47392..30cbe2b88 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -190,6 +190,10 @@ min-width: 1.25rem; } +.min-w-40 { + min-width: 10rem; +} + .h-1px { height: 1px; } @@ -366,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 */ From 595b44dfee66390ae0cdb561d34d9d5d0aa21f6e Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 18:38:54 -0300 Subject: [PATCH 66/96] fix: reload layout when double clicking on panel resizer --- .../javascripts/directives/views/panelResizer.ts | 15 +++++++++------ app/assets/javascripts/views/notes/notes-view.pug | 2 +- app/assets/javascripts/views/notes/notes_view.ts | 7 ++++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/directives/views/panelResizer.ts b/app/assets/javascripts/directives/views/panelResizer.ts index 8372ff235..9b8c93451 100644 --- a/app/assets/javascripts/directives/views/panelResizer.ts +++ b/app/assets/javascripts/directives/views/panelResizer.ts @@ -54,7 +54,7 @@ class PanelResizerCtrl implements PanelResizerScope { index!: number minWidth!: number onResizeFinish!: () => ResizeFinishCallback - onMouseMoveEvent?: () => () => void + onWidthEvent?: () => () => void panelId!: string property!: PanelSide @@ -104,7 +104,7 @@ class PanelResizerCtrl implements PanelResizerScope { $onDestroy() { (this.onResizeFinish as any) = undefined; - (this.onMouseMoveEvent 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); @@ -189,6 +189,9 @@ class PanelResizerCtrl implements PanelResizerScope { addDoubleClickHandler() { this.resizerColumn.ondblclick = () => { this.$timeout(() => { + if (this.onWidthEvent) { + this.onWidthEvent()(); + } const preClickCollapseState = this.isCollapsed(); if (preClickCollapseState) { this.setWidth(this.widthBeforeLastDblClick || this.defaultWidth); @@ -245,9 +248,6 @@ class PanelResizerCtrl implements PanelResizerScope { return; } event.preventDefault(); - if (this.onMouseMoveEvent) { - this.onMouseMoveEvent()(); - } if (this.property && this.property === PanelSide.Left) { this.handleLeftEvent(event); } else { @@ -256,6 +256,9 @@ class PanelResizerCtrl implements PanelResizerScope { } handleWidthEvent(event?: MouseEvent) { + if (this.onWidthEvent) { + this.onWidthEvent()(); + } let x; if (event) { x = event!.clientX; @@ -393,7 +396,7 @@ export class PanelResizer extends WebDirective { index: '=', minWidth: '=', onResizeFinish: '&', - onMouseMoveEvent: '&', + onWidthEvent: '&', panelId: '=', property: '=' }; diff --git a/app/assets/javascripts/views/notes/notes-view.pug b/app/assets/javascripts/views/notes/notes-view.pug index b14c3f4e7..3bf428d6d 100644 --- a/app/assets/javascripts/views/notes/notes-view.pug +++ b/app/assets/javascripts/views/notes/notes-view.pug @@ -169,6 +169,6 @@ default-width="300" hoverable="true" on-resize-finish="self.onPanelResize" - on-mouse-move-event="self.onPanelMouseMoveEvent" + on-width-event="self.onPanelWidthEvent" panel-id="'notes-column'" ) diff --git a/app/assets/javascripts/views/notes/notes_view.ts b/app/assets/javascripts/views/notes/notes_view.ts index 993405207..2b9e3d204 100644 --- a/app/assets/javascripts/views/notes/notes_view.ts +++ b/app/assets/javascripts/views/notes/notes_view.ts @@ -93,7 +93,7 @@ class NotesViewCtrl extends PureViewCtrl { }; this.onWindowResize = this.onWindowResize.bind(this); this.onPanelResize = this.onPanelResize.bind(this); - this.onPanelMouseMoveEvent = this.onPanelMouseMoveEvent.bind(this); + this.onPanelWidthEvent = this.onPanelWidthEvent.bind(this); window.addEventListener('resize', this.onWindowResize, true); this.registerKeyboardShortcuts(); this.autorun(async () => { @@ -134,7 +134,7 @@ class NotesViewCtrl extends PureViewCtrl { window.removeEventListener('resize', this.onWindowResize, true); (this.onWindowResize as any) = undefined; (this.onPanelResize as any) = undefined; - (this.onPanelMouseMoveEvent as any) = undefined; + (this.onPanelWidthEvent as any) = undefined; this.newNoteKeyObserver(); this.nextNoteKeyObserver(); this.previousNoteKeyObserver(); @@ -649,6 +649,7 @@ class NotesViewCtrl extends PureViewCtrl { __: boolean, isCollapsed: boolean ) { + this.appState.activeNote.reloadTagsContainerMaxWidth(); this.application.setPreference( PrefKey.NotesPanelWidth, newWidth @@ -659,7 +660,7 @@ class NotesViewCtrl extends PureViewCtrl { ); } - onPanelMouseMoveEvent(): void { + onPanelWidthEvent(): void { this.appState.activeNote.reloadTagsContainerMaxWidth(); } From f9c2b19eacf84e5461b269ec81b58163bd703c37 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 18:59:27 -0300 Subject: [PATCH 67/96] fix: reload tabIndex after tags expansion --- app/assets/javascripts/components/NoteTag.tsx | 3 ++- .../javascripts/ui_models/app_state/active_note_state.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index 6d15c04d8..cf86641a6 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -14,6 +14,7 @@ type Props = { export const NoteTag: FunctionalComponent = ({ appState, tag }) => { const { tags, + tagsContainerExpanded, tagsContainerMaxWidth, } = appState.activeNote; @@ -48,7 +49,7 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { useEffect(() => { reloadOverflowed(); - }, [reloadOverflowed, tags, tagsContainerMaxWidth]); + }, [reloadOverflowed, tags, tagsContainerExpanded, tagsContainerMaxWidth]); const contextMenuListener = (event: MouseEvent) => { event.preventDefault(); diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 254b0da38..c61de8564 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -134,6 +134,9 @@ export class ActiveNoteState { } isTagOverflowed(tag: SNTag): boolean { + if (this.tagsContainerExpanded) { + return false; + } const tagElement = this.getTagElement(tag); return tagElement ? this.isElementOverflowed(tagElement) : false; } From 54fbb606eb2f3f8de5a4b66b324cda02c79daee4 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 18:59:43 -0300 Subject: [PATCH 68/96] fix: fix focus on previous tag after tag deletion --- app/assets/javascripts/components/NoteTag.tsx | 10 ++-------- .../ui_models/app_state/active_note_state.ts | 5 +++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index cf86641a6..09260453d 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -28,14 +28,8 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { const [closeOnBlur] = useCloseOnBlur(contextMenuRef, setContextMenuOpen); useCloseOnClickOutside(contextMenuRef, setContextMenuOpen); - const deleteTag = async () => { - await appState.activeNote.removeTagFromActiveNote(tag); - const previousTag = appState.activeNote.getPreviousTag(tag); - - if (previousTag) { - const previousTagElement = appState.activeNote.getTagElement(previousTag); - previousTagElement?.focus(); - } + const deleteTag = () => { + appState.activeNote.removeTagFromActiveNote(tag); }; const onTagClick = () => { diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index c61de8564..5c7223a4f 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -203,10 +203,15 @@ export class ActiveNoteState { async removeTagFromActiveNote(tag: SNTag): Promise { const { activeNote } = this; if (activeNote) { + const previousTag = this.getPreviousTag(tag); await this.application.changeItem(tag.uuid, (mutator) => { mutator.removeItemAsRelationship(activeNote); }); this.application.sync(); + if (previousTag) { + const previousTagElement = this.getTagElement(previousTag); + previousTagElement?.focus(); + } this.reloadTags(); } } From 02f3c7c26cf850a994965e0d52f950a3fec3fea3 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 19:50:38 -0300 Subject: [PATCH 69/96] feat: remove overflowed tags feature --- .../components/AutocompleteTagInput.tsx | 20 +---- app/assets/javascripts/components/NoteTag.tsx | 15 +--- .../components/NoteTagsContainer.tsx | 77 ++--------------- .../ui_models/app_state/active_note_state.ts | 86 ------------------- 4 files changed, 8 insertions(+), 190 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 7a33af117..549674b51 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -1,7 +1,7 @@ import { WebApplication } from '@/ui_models/application'; import { SNTag } from '@standardnotes/snjs'; import { FunctionalComponent } from 'preact'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { Icon } from './Icon'; import { Disclosure, DisclosurePanel } from '@reach/disclosure'; import { useCloseOnBlur } from './utils'; @@ -16,7 +16,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ application, appState, }) => { - const { tagElements, tags, tagsContainerMaxWidth, tagsOverflowed } = appState.activeNote; + const { tagElements } = appState.activeNote; const [searchQuery, setSearchQuery] = useState(''); const [dropdownVisible, setDropdownVisible] = useState(false); @@ -82,19 +82,6 @@ export const AutocompleteTagInput: FunctionalComponent = ({ await createAndAddNewTag(); }; - const reloadInputOverflowed = useCallback(() => { - const overflowed = !tagsOverflowed && appState.activeNote.isElementOverflowed(inputRef.current); - appState.activeNote.setInputOverflowed(overflowed); - }, [appState.activeNote, tagsOverflowed]); - - useEffect(() => { - reloadInputOverflowed(); - }, [ - reloadInputOverflowed, - tagsContainerMaxWidth, - tags, - ]); - useEffect(() => { setHintVisible( searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery) @@ -111,7 +98,6 @@ export const AutocompleteTagInput: FunctionalComponent = ({ onChange={onSearchQueryChange} type="text" placeholder="Add tag" - tabIndex={tagsOverflowed ? -1 : 0} onBlur={closeOnBlur} onFocus={showDropdown} onKeyUp={(event) => { @@ -139,7 +125,6 @@ export const AutocompleteTagInput: FunctionalComponent = ({ className="sn-dropdown-item" onClick={() => onTagOptionClick(tag)} onBlur={closeOnBlur} - tabIndex={tagsOverflowed ? -1 : 0} > @@ -177,7 +162,6 @@ export const AutocompleteTagInput: FunctionalComponent = ({ className="sn-dropdown-item" onClick={onTagHintClick} onBlur={closeOnBlur} - tabIndex={tagsOverflowed ? -1 : 0} > Create new tag: diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index 09260453d..228fdb369 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -1,6 +1,6 @@ import { Icon } from './Icon'; import { FunctionalComponent } from 'preact'; -import { useCallback, useRef, useState } from 'preact/hooks'; +import { useRef, useState } from 'preact/hooks'; import { AppState } from '@/ui_models/app_state'; import { SNTag } from '@standardnotes/snjs/dist/@types'; import { useEffect } from 'react'; @@ -13,12 +13,9 @@ type Props = { export const NoteTag: FunctionalComponent = ({ appState, tag }) => { const { - tags, - tagsContainerExpanded, tagsContainerMaxWidth, } = appState.activeNote; - const [overflowed, setOverflowed] = useState(false); const [contextMenuOpen, setContextMenuOpen] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState({ top: 0, left: 0 }); @@ -36,15 +33,6 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { appState.setSelectedTag(tag); }; - const reloadOverflowed = useCallback(() => { - const overflowed = appState.activeNote.isTagOverflowed(tag); - setOverflowed(overflowed); - }, [appState.activeNote, tag]); - - useEffect(() => { - reloadOverflowed(); - }, [reloadOverflowed, tags, tagsContainerExpanded, tagsContainerMaxWidth]); - const contextMenuListener = (event: MouseEvent) => { event.preventDefault(); setContextMenuPosition({ @@ -78,7 +66,6 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { deleteTag(); } }} - tabIndex={overflowed ? -1 : 0} onBlur={closeOnBlur} > diff --git a/app/assets/javascripts/components/NoteTagsContainer.tsx b/app/assets/javascripts/components/NoteTagsContainer.tsx index 8627df267..f029e888c 100644 --- a/app/assets/javascripts/components/NoteTagsContainer.tsx +++ b/app/assets/javascripts/components/NoteTagsContainer.tsx @@ -1,10 +1,10 @@ import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; -import { toDirective, useCloseOnClickOutside } from './utils'; +import { toDirective } from './utils'; import { AutocompleteTagInput } from './AutocompleteTagInput'; import { WebApplication } from '@/ui_models/application'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { NoteTag } from './NoteTag'; +import { useEffect } from 'preact/hooks'; type Props = { application: WebApplication; @@ -13,72 +13,17 @@ type Props = { const NoteTagsContainer = observer(({ application, appState }: Props) => { const { - inputOverflowed, - overflowCountPosition, - overflowedTagsCount, - tagElements, tags, tagsContainerMaxWidth, - tagsContainerExpanded, - tagsOverflowed, } = appState.activeNote; - const [expandedContainerHeight, setExpandedContainerHeight] = useState(0); - - const tagsContainerRef = useRef(); - const overflowButtonRef = useRef(); - - useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => { - if (tagsContainerExpanded) { - appState.activeNote.setTagsContainerExpanded(expanded); - } - }); - - const reloadExpandedContainerHeight = useCallback(() => { - setExpandedContainerHeight(tagsContainerRef.current.scrollHeight); - }, []); - useEffect(() => { - appState.activeNote.reloadTagsContainerLayout(); - reloadExpandedContainerHeight(); - }, [ - appState.activeNote, - reloadExpandedContainerHeight, - tags, - tagsContainerMaxWidth, - ]); - - useEffect(() => { - let tagResizeObserver: ResizeObserver; - if (ResizeObserver) { - tagResizeObserver = new ResizeObserver(() => { - appState.activeNote.reloadTagsContainerLayout(); - reloadExpandedContainerHeight(); - }); - tagElements.forEach( - (tagElement) => tagElement && tagResizeObserver.observe(tagElement) - ); - } - - return () => { - if (tagResizeObserver) { - tagResizeObserver.disconnect(); - } - }; - }, [appState.activeNote, reloadExpandedContainerHeight, tagElements]); + appState.activeNote.reloadTagsContainerMaxWidth(); + }, [appState.activeNote]); return ( -
{ ))}
- {tagsOverflowed && ( - - )} -
); }); diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 5c7223a4f..0982b7411 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -5,7 +5,6 @@ import { } from '@standardnotes/snjs'; import { action, - computed, makeObservable, observable, } from 'mobx'; @@ -13,14 +12,9 @@ import { WebApplication } from '../application'; import { AppState } from './app_state'; export class ActiveNoteState { - inputOverflowed = false; - overflowCountPosition = 0; - overflowedTagsCount = 0; tagElements: (HTMLButtonElement | undefined)[] = []; - tagFocused = false; tags: SNTag[] = []; tagsContainerMaxWidth: number | 'auto' = 0; - tagsContainerExpanded = false; constructor( private application: WebApplication, @@ -28,24 +22,12 @@ export class ActiveNoteState { appEventListeners: (() => void)[] ) { makeObservable(this, { - inputOverflowed: observable, - overflowCountPosition: observable, - overflowedTagsCount: observable, tagElements: observable, - tagFocused: observable, tags: observable, - tagsContainerExpanded: observable, tagsContainerMaxWidth: observable, - tagsOverflowed: computed, - - setInputOverflowed: action, - setOverflowCountPosition: action, - setOverflowedTagsCount: action, setTagElement: action, - setTagFocused: action, setTags: action, - setTagsContainerExpanded: action, setTagsContainerMaxWidth: action, reloadTags: action, }); @@ -60,23 +42,6 @@ export class ActiveNoteState { get activeNote(): SNNote | undefined { return this.appState.notes.activeEditor?.note; } - - get tagsOverflowed(): boolean { - return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded; - } - - setInputOverflowed(overflowed: boolean): void { - this.inputOverflowed = overflowed; - } - - setOverflowCountPosition(position: number): void { - this.overflowCountPosition = position; - } - - setOverflowedTagsCount(count: number): void { - this.overflowedTagsCount = count; - } - setTagElement(tag: SNTag, element: HTMLButtonElement): void { const tagIndex = this.getTagIndex(tag); if (tagIndex > -1) { @@ -84,10 +49,6 @@ export class ActiveNoteState { } } - setTagFocused(focused: boolean): void { - this.tagFocused = focused; - } - setTagElements(elements: (HTMLButtonElement | undefined)[]): void { this.tagElements = elements; } @@ -96,10 +57,6 @@ export class ActiveNoteState { this.tags = tags; } - setTagsContainerExpanded(expanded: boolean): void { - this.tagsContainerExpanded = expanded; - } - setTagsContainerMaxWidth(width: number): void { this.tagsContainerMaxWidth = width; } @@ -122,25 +79,6 @@ export class ActiveNoteState { } } - isElementOverflowed(element: HTMLElement): boolean { - if ( - this.tagElements.length === 0 || - !this.tagElements[0] - ) { - return false; - } - const firstTagTop = this.tagElements[0].offsetTop; - return element.offsetTop > firstTagTop; - } - - isTagOverflowed(tag: SNTag): boolean { - if (this.tagsContainerExpanded) { - return false; - } - const tagElement = this.getTagElement(tag); - return tagElement ? this.isElementOverflowed(tagElement) : false; - } - reloadTags(): void { const { activeNote } = this; if (activeNote) { @@ -151,24 +89,6 @@ export class ActiveNoteState { } } - reloadOverflowCountPosition(): void { - const lastVisibleTagIndex = this.tagElements.findIndex(tagElement => tagElement && this.isElementOverflowed(tagElement)) - 1; - if (lastVisibleTagIndex > -1 && this.tagElements.length > lastVisibleTagIndex) { - const lastVisibleTagElement = this.tagElements[lastVisibleTagIndex]; - if (lastVisibleTagElement) { - const { offsetLeft, offsetWidth } = lastVisibleTagElement; - this.setOverflowCountPosition(offsetLeft + offsetWidth); - } - } - } - - reloadOverflowedTagsCount(): void { - const count = this.tagElements.filter((tagElement) => - tagElement && this.isElementOverflowed(tagElement) - ).length; - this.setOverflowedTagsCount(count); - } - reloadTagsContainerMaxWidth(): void { const EDITOR_ELEMENT_ID = 'editor-column'; const defaultFontSize = parseFloat(window.getComputedStyle( @@ -183,12 +103,6 @@ export class ActiveNoteState { } } - reloadTagsContainerLayout(): void { - this.reloadTagsContainerMaxWidth(); - this.reloadOverflowedTagsCount(); - this.reloadOverflowCountPosition(); - } - async addTagToActiveNote(tag: SNTag): Promise { const { activeNote } = this; if (activeNote) { From 9513392f6cfec6124bbadfdaa4f84b85035fe9d9 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 19:51:13 -0300 Subject: [PATCH 70/96] styles: reduce spacing when note has no tags --- app/assets/javascripts/components/AutocompleteTagInput.tsx | 4 ++-- app/assets/javascripts/views/editor/editor-view.pug | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 549674b51..4fbfab6a0 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -16,7 +16,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ application, appState, }) => { - const { tagElements } = appState.activeNote; + const { tagElements, tags } = appState.activeNote; const [searchQuery, setSearchQuery] = useState(''); const [dropdownVisible, setDropdownVisible] = useState(false); @@ -89,7 +89,7 @@ export const AutocompleteTagInput: FunctionalComponent = ({ }, [tagResults, searchQuery]); return ( - + 0 ? 'mt-2' : ''}`}> Date: Wed, 2 Jun 2021 19:57:11 -0300 Subject: [PATCH 71/96] styles: change tags icon color --- app/assets/javascripts/components/NoteTag.tsx | 2 +- app/assets/stylesheets/_sn.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index 228fdb369..ec703d78c 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -68,7 +68,7 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { }} onBlur={closeOnBlur} > - + {tag.title} diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 30cbe2b88..c816550f8 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -113,6 +113,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); } From 093acd151954949e1c961f3580d26b236b25732b Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 20:00:42 -0300 Subject: [PATCH 72/96] feat: show button to remove note on focus --- app/assets/javascripts/components/NoteTag.tsx | 110 +++++++----------- 1 file changed, 45 insertions(+), 65 deletions(-) diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index ec703d78c..d3c7a937d 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -3,8 +3,6 @@ import { FunctionalComponent } from 'preact'; import { useRef, useState } from 'preact/hooks'; import { AppState } from '@/ui_models/app_state'; import { SNTag } from '@standardnotes/snjs/dist/@types'; -import { useEffect } from 'react'; -import { useCloseOnBlur, useCloseOnClickOutside } from './utils'; type Props = { appState: AppState; @@ -16,14 +14,8 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { tagsContainerMaxWidth, } = appState.activeNote; - const [contextMenuOpen, setContextMenuOpen] = useState(false); - const [contextMenuPosition, setContextMenuPosition] = useState({ top: 0, left: 0 }); - - const contextMenuRef = useRef(); - const tagRef = useRef(); - - const [closeOnBlur] = useCloseOnBlur(contextMenuRef, setContextMenuOpen); - useCloseOnClickOutside(contextMenuRef, setContextMenuOpen); + const [showDeleteButton, setShowDeleteButton] = useState(false); + const deleteTagRef = useRef(); const deleteTag = () => { appState.activeNote.removeTagFromActiveNote(tag); @@ -33,66 +25,54 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { appState.setSelectedTag(tag); }; - const contextMenuListener = (event: MouseEvent) => { - event.preventDefault(); - setContextMenuPosition({ - top: event.clientY, - left: event.clientX, - }); - setContextMenuOpen(true); + const onFocus = () => { + setShowDeleteButton(true); }; - useEffect(() => { - tagRef.current.addEventListener('contextmenu', contextMenuListener); - return () => { - tagRef.current.removeEventListener('contextmenu', contextMenuListener); - }; - }, []); + const onBlur = (event: FocusEvent) => { + const relatedTarget = event.relatedTarget as Node; + if (relatedTarget !== deleteTagRef.current) { + setShowDeleteButton(false); + } + }; return ( - <> - - {contextMenuOpen && ( -
{ + if (element) { + appState.activeNote.setTagElement(tag, element); + } + }} + className="sn-tag pl-1 pr-2 mr-2" + style={{ maxWidth: tagsContainerMaxWidth }} + onClick={onTagClick} + onKeyUp={(event) => { + if (event.key === 'Backspace') { + deleteTag(); + } + }} + onFocus={onFocus} + onBlur={onBlur} + > + + + {tag.title} + + {showDeleteButton && ( + -
+ + )} - + ); }; From 4faebe4ffd16850d1e5b9a61d56e383741f72b7d Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 20:05:34 -0300 Subject: [PATCH 73/96] fix: onWidthEvent error --- app/assets/javascripts/directives/views/panelResizer.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/javascripts/directives/views/panelResizer.ts b/app/assets/javascripts/directives/views/panelResizer.ts index 9b8c93451..791e3fcb9 100644 --- a/app/assets/javascripts/directives/views/panelResizer.ts +++ b/app/assets/javascripts/directives/views/panelResizer.ts @@ -189,9 +189,6 @@ class PanelResizerCtrl implements PanelResizerScope { addDoubleClickHandler() { this.resizerColumn.ondblclick = () => { this.$timeout(() => { - if (this.onWidthEvent) { - this.onWidthEvent()(); - } const preClickCollapseState = this.isCollapsed(); if (preClickCollapseState) { this.setWidth(this.widthBeforeLastDblClick || this.defaultWidth); @@ -256,7 +253,7 @@ class PanelResizerCtrl implements PanelResizerScope { } handleWidthEvent(event?: MouseEvent) { - if (this.onWidthEvent) { + if (this.onWidthEvent && this.onWidthEvent()) { this.onWidthEvent()(); } let x; From 434ea3258e26d3fbcc50d641e41a2dd25238e380 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 20:08:02 -0300 Subject: [PATCH 74/96] fix: tags container margins --- .../javascripts/ui_models/app_state/active_note_state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 0982b7411..abfee2432 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -94,11 +94,11 @@ export class ActiveNoteState { const defaultFontSize = parseFloat(window.getComputedStyle( document.documentElement ).fontSize); - const overflowMargin = defaultFontSize * 5; + const margins = defaultFontSize * 1.5; const editorWidth = document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth; if (editorWidth) { this.appState.activeNote.setTagsContainerMaxWidth( - editorWidth - overflowMargin + editorWidth - margins ); } } From 672331faaae53cf7b30522135d3d57948afccc92 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Wed, 2 Jun 2021 20:18:59 -0300 Subject: [PATCH 75/96] feat: navigate tags with arrow keys --- app/assets/javascripts/components/NoteTag.tsx | 27 +++++++++++++++---- .../ui_models/app_state/active_note_state.ts | 25 ++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index d3c7a937d..ef304ce0a 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -36,6 +36,27 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { } }; + const onKeyUp = (event: KeyboardEvent) => { + let previousTagElement; + let nextTagElement; + + switch (event.key) { + case "Backspace": + deleteTag(); + break; + case "ArrowLeft": + previousTagElement = appState.activeNote.getPreviousTagElement(tag); + previousTagElement?.focus(); + break; + case "ArrowRight": + nextTagElement = appState.activeNote.getNextTagElement(tag); + nextTagElement?.focus(); + break; + default: + return; + } + }; + return ( + + ); + } +); diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index 4fbfab6a0..fa8efc6fc 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -1,53 +1,35 @@ -import { WebApplication } from '@/ui_models/application'; -import { SNTag } from '@standardnotes/snjs'; -import { FunctionalComponent } from 'preact'; import { useEffect, useRef, useState } from 'preact/hooks'; -import { Icon } from './Icon'; 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 = { - application: WebApplication; appState: AppState; }; -export const AutocompleteTagInput: FunctionalComponent = ({ - application, - appState, -}) => { - const { tagElements, tags } = appState.activeNote; +export const AutocompleteTagInput = observer(({ appState }: Props) => { + const { + autocompleteSearchQuery, + autocompleteTagHintVisible, + autocompleteTagResults, + tagElements, + tags, + } = appState.activeNote; - const [searchQuery, setSearchQuery] = useState(''); const [dropdownVisible, setDropdownVisible] = useState(false); const [dropdownMaxHeight, setDropdownMaxHeight] = useState('auto'); - const [hintVisible, setHintVisible] = useState(true); - - const getActiveNoteTagResults = (query: string) => { - const { activeNote } = appState.activeNote; - return application.searchTags(query, activeNote); - }; - - const [tagResults, setTagResults] = useState(() => { - return getActiveNoteTagResults(''); - }); const inputRef = useRef(); const dropdownRef = useRef(); - const clearResults = () => { - setSearchQuery(''); - setTagResults(getActiveNoteTagResults('')); - }; - - const [closeOnBlur] = useCloseOnBlur( - dropdownRef, - (visible: boolean) => { - setDropdownVisible(visible); - clearResults(); - } - ); + const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => { + setDropdownVisible(visible); + appState.activeNote.clearAutocompleteSearch(); + }); const showDropdown = () => { const { clientHeight } = document.documentElement; @@ -58,43 +40,29 @@ export const AutocompleteTagInput: FunctionalComponent = ({ const onSearchQueryChange = (event: Event) => { const query = (event.target as HTMLInputElement).value; - setTagResults(getActiveNoteTagResults(query)); - setSearchQuery(query); - }; - - const onTagOptionClick = async (tag: SNTag) => { - await appState.activeNote.addTagToActiveNote(tag); - clearResults(); - }; - - const createAndAddNewTag = async () => { - const newTag = await application.findOrCreateTag(searchQuery); - await appState.activeNote.addTagToActiveNote(newTag); - clearResults(); - }; - - const onTagHintClick = async () => { - await createAndAddNewTag(); + appState.activeNote.setAutocompleteSearchQuery(query); + appState.activeNote.searchActiveNoteAutocompleteTags(); }; const onFormSubmit = async (event: Event) => { event.preventDefault(); - await createAndAddNewTag(); + await appState.activeNote.createAndAddNewTag(); }; useEffect(() => { - setHintVisible( - searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery) - ); - }, [tagResults, searchQuery]); + appState.activeNote.searchActiveNoteAutocompleteTags(); + }, [appState.activeNote]); return ( - 0 ? 'mt-2' : ''}`}> + 0 ? 'mt-2' : ''}`} + > = ({ onKeyUp={(event) => { if ( event.key === 'Backspace' && - searchQuery === '' && + autocompleteSearchQuery === '' && tagElements.length > 0 ) { tagElements[tagElements.length - 1]?.focus(); @@ -117,66 +85,24 @@ export const AutocompleteTagInput: FunctionalComponent = ({ style={{ maxHeight: dropdownMaxHeight }} >
- {tagResults.map((tag) => { - return ( - - ); - })} + {autocompleteTagResults.map((tagResult) => ( + + ))}
- {hintVisible && ( - <> - {tagResults.length > 0 && ( -
- )} - - + {autocompleteTagHintVisible && ( + )} )}
); -}; +}); diff --git a/app/assets/javascripts/components/AutocompleteTagResult.tsx b/app/assets/javascripts/components/AutocompleteTagResult.tsx new file mode 100644 index 000000000..f61830f16 --- /dev/null +++ b/app/assets/javascripts/components/AutocompleteTagResult.tsx @@ -0,0 +1,59 @@ +import { AppState } from '@/ui_models/app_state'; +import { SNTag } from '@standardnotes/snjs'; +import { observer } from 'mobx-react-lite'; +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 } = appState.activeNote; + + const onTagOptionClick = async (tag: SNTag) => { + await appState.activeNote.addTagToActiveNote(tag); + appState.activeNote.clearAutocompleteSearch(); + }; + + return ( + + ); + } +); diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index 36c9bba8a..a0bf2f73c 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -1,18 +1,16 @@ import { Icon } from './Icon'; -import { FunctionalComponent } from 'preact'; import { 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: FunctionalComponent = ({ appState, tag }) => { - const { - tagsContainerMaxWidth, - } = appState.activeNote; +export const NoteTag = observer(({ appState, tag }: Props) => { + const { tagsContainerMaxWidth } = appState.activeNote; const [showDeleteButton, setShowDeleteButton] = useState(false); const deleteTagRef = useRef(); @@ -41,14 +39,14 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { let nextTagElement; switch (event.key) { - case "Backspace": + case 'Backspace': deleteTag(); break; - case "ArrowLeft": + case 'ArrowLeft': previousTagElement = appState.activeNote.getPreviousTagElement(tag); previousTagElement?.focus(); break; - case "ArrowRight": + case 'ArrowRight': nextTagElement = appState.activeNote.getNextTagElement(tag); nextTagElement?.focus(); break; @@ -92,4 +90,4 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => { )} ); -}; +}); diff --git a/app/assets/javascripts/components/NoteTagsContainer.tsx b/app/assets/javascripts/components/NoteTagsContainer.tsx index ff0d2e087..dbf73fceb 100644 --- a/app/assets/javascripts/components/NoteTagsContainer.tsx +++ b/app/assets/javascripts/components/NoteTagsContainer.tsx @@ -2,16 +2,14 @@ import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; import { toDirective } from './utils'; import { AutocompleteTagInput } from './AutocompleteTagInput'; -import { WebApplication } from '@/ui_models/application'; import { NoteTag } from './NoteTag'; import { useEffect } from 'preact/hooks'; type Props = { - application: WebApplication; appState: AppState; }; -const NoteTagsContainer = observer(({ application, appState }: Props) => { +const NoteTagsContainer = observer(({ appState }: Props) => { const { tags, tagsContainerMaxWidth, @@ -35,7 +33,7 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => { tag={tag} /> ))} - +
); }); diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 775120d21..c7a0dc124 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -1,17 +1,12 @@ -import { - SNNote, - ContentType, - SNTag, -} from '@standardnotes/snjs'; -import { - action, - makeObservable, - observable, -} from 'mobx'; +import { SNNote, ContentType, SNTag } from '@standardnotes/snjs'; +import { action, computed, makeObservable, observable } from 'mobx'; import { WebApplication } from '../application'; import { AppState } from './app_state'; export class ActiveNoteState { + autocompleteSearchQuery = ''; + autocompleteTagResultElements: (HTMLButtonElement | undefined)[] = []; + autocompleteTagResults: SNTag[] = []; tagElements: (HTMLButtonElement | undefined)[] = []; tags: SNTag[] = []; tagsContainerMaxWidth: number | 'auto' = 0; @@ -22,11 +17,22 @@ export class ActiveNoteState { appEventListeners: (() => void)[] ) { makeObservable(this, { + autocompleteSearchQuery: observable, + autocompleteTagResultElements: observable, + autocompleteTagResults: observable, tagElements: observable, tags: observable, tagsContainerMaxWidth: observable, + autocompleteTagHintVisible: computed, + + clearAutocompleteSearch: action, + setAutocompleteSearchQuery: action, + setAutocompleteTagResultElement: action, + setAutocompleteTagResultElements: action, + setAutocompleteTagResults: action, setTagElement: action, + setTagElements: action, setTags: action, setTagsContainerMaxWidth: action, reloadTags: action, @@ -43,8 +49,41 @@ export class ActiveNoteState { return this.appState.notes.activeEditor?.note; } + get autocompleteTagHintVisible(): boolean { + return ( + this.autocompleteSearchQuery !== '' && + !this.autocompleteTagResults.some( + (tagResult) => tagResult.title === this.autocompleteSearchQuery + ) + ); + } + + setAutocompleteSearchQuery(query: string): void { + this.autocompleteSearchQuery = query; + } + + setAutocompleteTagResultElement( + tagResult: SNTag, + element: HTMLButtonElement + ): void { + const tagIndex = this.getTagIndex(tagResult, this.autocompleteTagResults); + if (tagIndex > -1) { + this.autocompleteTagResultElements.splice(tagIndex, 1, element); + } + } + + setAutocompleteTagResultElements( + elements: (HTMLButtonElement | undefined)[] + ): void { + this.autocompleteTagResultElements = elements; + } + + setAutocompleteTagResults(results: SNTag[]): void { + this.autocompleteTagResults = results; + } + setTagElement(tag: SNTag, element: HTMLButtonElement): void { - const tagIndex = this.getTagIndex(tag); + const tagIndex = this.getTagIndex(tag, this.tags); if (tagIndex > -1) { this.tagElements.splice(tagIndex, 1, element); } @@ -62,19 +101,38 @@ export class ActiveNoteState { this.tagsContainerMaxWidth = width; } - getTagIndex(tag: SNTag): number { - return this.tags.findIndex(t => t.uuid === tag.uuid); + clearAutocompleteSearch(): void { + this.setAutocompleteSearchQuery(''); + this.searchActiveNoteAutocompleteTags(); + } + + async createAndAddNewTag(): Promise { + const newTag = await this.application.findOrCreateTag(this.autocompleteSearchQuery); + await this.addTagToActiveNote(newTag); + this.clearAutocompleteSearch(); + } + + 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); } getPreviousTagElement(tag: SNTag): HTMLButtonElement | undefined { - const previousTagIndex = this.getTagIndex(tag) - 1; + const previousTagIndex = this.getTagIndex(tag, this.tags) - 1; if (previousTagIndex > -1 && this.tagElements.length > previousTagIndex) { return this.tagElements[previousTagIndex]; } } getNextTagElement(tag: SNTag): HTMLButtonElement | undefined { - const nextTagIndex = this.getTagIndex(tag) + 1; + const nextTagIndex = this.getTagIndex(tag, this.tags) + 1; if (nextTagIndex > -1 && this.tagElements.length > nextTagIndex) { return this.tagElements[nextTagIndex]; } @@ -94,9 +152,7 @@ export class ActiveNoteState { const EDITOR_ELEMENT_ID = 'editor-column'; const editorWidth = document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth; if (editorWidth) { - this.appState.activeNote.setTagsContainerMaxWidth( - editorWidth - ); + this.setTagsContainerMaxWidth(editorWidth); } } diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index 9cb2f8e4c..dcaa5b45d 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -53,7 +53,6 @@ ng-if='self.appState.notes.selectedNotesCount > 0' ) note-tags-container( - application='self.application' app-state='self.appState' ) .sn-component(ng-if='self.note') From 386ca341789c502a9e8b8e48accf040ad6260ba2 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 3 Jun 2021 13:53:49 -0300 Subject: [PATCH 80/96] refactor: rename state --- .../javascripts/components/AutocompleteTagHint.tsx | 4 ++-- .../components/AutocompleteTagInput.tsx | 14 +++++++------- .../components/AutocompleteTagResult.tsx | 8 ++++---- app/assets/javascripts/components/NoteTag.tsx | 10 +++++----- .../javascripts/components/NoteTagsContainer.tsx | 6 +++--- .../javascripts/ui_models/app_state/app_state.ts | 14 +++++++------- .../{active_note_state.ts => note_tags_state.ts} | 2 +- .../javascripts/ui_models/app_state/notes_state.ts | 2 +- app/assets/javascripts/views/notes/notes_view.ts | 6 +++--- 9 files changed, 33 insertions(+), 33 deletions(-) rename app/assets/javascripts/ui_models/app_state/{active_note_state.ts => note_tags_state.ts} (99%) diff --git a/app/assets/javascripts/components/AutocompleteTagHint.tsx b/app/assets/javascripts/components/AutocompleteTagHint.tsx index 65630b4f4..ec5905000 100644 --- a/app/assets/javascripts/components/AutocompleteTagHint.tsx +++ b/app/assets/javascripts/components/AutocompleteTagHint.tsx @@ -10,10 +10,10 @@ type Props = { export const AutocompleteTagHint = observer( ({ appState, closeOnBlur }: Props) => { const { autocompleteSearchQuery, autocompleteTagResults } = - appState.activeNote; + appState.noteTags; const onTagHintClick = async () => { - await appState.activeNote.createAndAddNewTag(); + await appState.noteTags.createAndAddNewTag(); }; return ( diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index fa8efc6fc..f887d0d0a 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -17,7 +17,7 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { autocompleteTagResults, tagElements, tags, - } = appState.activeNote; + } = appState.noteTags; const [dropdownVisible, setDropdownVisible] = useState(false); const [dropdownMaxHeight, setDropdownMaxHeight] = @@ -28,7 +28,7 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => { setDropdownVisible(visible); - appState.activeNote.clearAutocompleteSearch(); + appState.noteTags.clearAutocompleteSearch(); }); const showDropdown = () => { @@ -40,18 +40,18 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { const onSearchQueryChange = (event: Event) => { const query = (event.target as HTMLInputElement).value; - appState.activeNote.setAutocompleteSearchQuery(query); - appState.activeNote.searchActiveNoteAutocompleteTags(); + appState.noteTags.setAutocompleteSearchQuery(query); + appState.noteTags.searchActiveNoteAutocompleteTags(); }; const onFormSubmit = async (event: Event) => { event.preventDefault(); - await appState.activeNote.createAndAddNewTag(); + await appState.noteTags.createAndAddNewTag(); }; useEffect(() => { - appState.activeNote.searchActiveNoteAutocompleteTags(); - }, [appState.activeNote]); + appState.noteTags.searchActiveNoteAutocompleteTags(); + }, [appState.noteTags]); return (
{ - const { autocompleteSearchQuery } = appState.activeNote; + const { autocompleteSearchQuery } = appState.noteTags; const onTagOptionClick = async (tag: SNTag) => { - await appState.activeNote.addTagToActiveNote(tag); - appState.activeNote.clearAutocompleteSearch(); + await appState.noteTags.addTagToActiveNote(tag); + appState.noteTags.clearAutocompleteSearch(); }; return ( diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 94dc079c1..0a03d1d58 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -190,6 +190,10 @@ max-width: 20rem; } +.max-w-40 { + max-width: 10rem; +} + .min-w-5 { min-width: 1.25rem; } From 3d0c8d5cce18c999a6245ba979b54591e5dc6efb Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Thu, 3 Jun 2021 17:45:19 -0300 Subject: [PATCH 86/96] fix: avoid event propagation when deleting a tag --- app/assets/javascripts/components/NoteTag.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index 31a9dd941..f078c8a06 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -18,8 +18,14 @@ export const NoteTag = observer(({ appState, tag }: Props) => { appState.noteTags.removeTagFromActiveNote(tag); }; - const onTagClick = () => { - if (tagClicked) { + 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 { @@ -78,7 +84,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => { className="ml-2 -mr-1 border-0 p-0 bg-transparent cursor-pointer flex" onFocus={onFocus} onBlur={onBlur} - onClick={deleteTag} + onClick={onDeleteTagClick} > Date: Thu, 3 Jun 2021 17:45:43 -0300 Subject: [PATCH 87/96] refactor: store refs in components --- .../components/AutocompleteTagInput.tsx | 49 +++--- .../components/AutocompleteTagResult.tsx | 39 +++-- app/assets/javascripts/components/NoteTag.tsx | 31 +++- .../ui_models/app_state/note_tags_state.ts | 141 ++++++++---------- 4 files changed, 139 insertions(+), 121 deletions(-) diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx index a4a57e49b..5756301da 100644 --- a/app/assets/javascripts/components/AutocompleteTagInput.tsx +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -12,12 +12,10 @@ type Props = { export const AutocompleteTagInput = observer(({ appState }: Props) => { const { + autocompleteInputFocused, autocompleteSearchQuery, autocompleteTagHintVisible, autocompleteTagResults, - autocompleteTagResultElements, - autocompleteInputElement, - tagElements, tags, } = appState.noteTags; @@ -26,6 +24,7 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { useState('auto'); const dropdownRef = useRef(); + const inputRef = useRef(); const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => { setDropdownVisible(visible); @@ -33,11 +32,9 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { }); const showDropdown = () => { - if (autocompleteInputElement) { - const { clientHeight } = document.documentElement; - const inputRect = autocompleteInputElement.getBoundingClientRect(); - setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2); - } + const { clientHeight } = document.documentElement; + const inputRect = inputRef.current.getBoundingClientRect(); + setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2); setDropdownVisible(true); }; @@ -55,14 +52,15 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { const onKeyDown = (event: KeyboardEvent) => { switch (event.key) { case 'Backspace': - if (autocompleteSearchQuery === '' && tagElements.length > 0) { - tagElements[tagElements.length - 1]?.focus(); + case 'ArrowLeft': + if (autocompleteSearchQuery === '' && tags.length > 0) { + appState.noteTags.setFocusedTagUuid(tags[tags.length - 1].uuid); } break; case 'ArrowDown': event.preventDefault(); - if (autocompleteTagResultElements.length > 0) { - autocompleteTagResultElements[0]?.focus(); + if (autocompleteTagResults.length > 0) { + appState.noteTags.setFocusedTagResultUuid(autocompleteTagResults[0].uuid); } break; default: @@ -70,10 +68,27 @@ export const AutocompleteTagInput = observer(({ appState }: Props) => { } }; + 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 ( { > { - if (element) { - appState.noteTags.setAutocompleteInputElement(element); - } - }} + ref={inputRef} className="w-80 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={closeOnBlur} - onFocus={showDropdown} + onBlur={onBlur} + onFocus={onFocus} onKeyDown={onKeyDown} /> {dropdownVisible && ( diff --git a/app/assets/javascripts/components/AutocompleteTagResult.tsx b/app/assets/javascripts/components/AutocompleteTagResult.tsx index 4dd36aab0..6a82555ac 100644 --- a/app/assets/javascripts/components/AutocompleteTagResult.tsx +++ b/app/assets/javascripts/components/AutocompleteTagResult.tsx @@ -1,6 +1,7 @@ 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 = { @@ -11,7 +12,9 @@ type Props = { export const AutocompleteTagResult = observer( ({ appState, tagResult, closeOnBlur }: Props) => { - const { autocompleteInputElement, autocompleteSearchQuery, autocompleteTagResults } = appState.noteTags; + const { autocompleteSearchQuery, autocompleteTagResults, focusedTagResultUuid } = appState.noteTags; + + const tagResultRef = useRef(); const onTagOptionClick = async (tag: SNTag) => { await appState.noteTags.addTagToActiveNote(tag); @@ -24,34 +27,44 @@ export const AutocompleteTagResult = observer( case 'ArrowUp': event.preventDefault(); if (tagResultIndex === 0) { - autocompleteInputElement?.focus(); + appState.noteTags.setAutocompleteInputFocused(true); } else { - appState.noteTags.getPreviousAutocompleteTagResultElement(tagResult)?.focus(); + appState.noteTags.focusPreviousTagResult(tagResult); } break; case 'ArrowDown': event.preventDefault(); - appState.noteTags.getNextAutocompleteTagResultElement(tagResult)?.focus(); + 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 (