feat: add delete tag button and refactor NoteTag to separate component

This commit is contained in:
Antonella Sgarlatta
2021-06-01 20:55:54 -03:00
parent a071d4c9d0
commit 684a3fb0bf
6 changed files with 154 additions and 63 deletions

View File

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

View File

@@ -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<HTMLButtonElement[]>;
tag: SNTag;
overflowed: boolean;
maxWidth: number | 'auto';
};
export const NoteTag: FunctionalComponent<Props> = ({
appState,
index,
tagsRef,
tag,
overflowed,
maxWidth,
}) => {
const [showDeleteButton, setShowDeleteButton] = useState(false);
const deleteTagRef = useRef<HTMLButtonElement>();
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 (
<button
ref={(element) => {
if (element && tagsRef.current) {
tagsRef.current[index] = element;
}
}}
className="sn-tag pl-1 pr-2 mr-2"
style={{ maxWidth }}
onClick={onTagClick}
onKeyUp={(event) => {
if (event.key === 'Backspace') {
deleteTag();
}
}}
tabIndex={overflowed ? -1 : 0}
onFocus={onFocus}
onBlur={onBlur}
>
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
{tag.title}
</span>
{showDeleteButton && (
<button
ref={deleteTagRef}
type="button"
className="ml-2 -mr-1 border-0 p-0 bg-transparent cursor-pointer flex"
onFocus={onFocus}
onBlur={onBlur}
onClick={deleteTag}
>
<Icon
type="close"
className="sn-icon--small color-neutral hover:color-info"
/>
</button>
)}
</button>
);
};

View File

@@ -1,18 +1,18 @@
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { toDirective, useCloseOnClickOutside } from './utils'; import { toDirective, useCloseOnClickOutside } from './utils';
import { Icon } from './Icon';
import { AutocompleteTagInput } from './AutocompleteTagInput'; import { AutocompleteTagInput } from './AutocompleteTagInput';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { SNTag } from '@standardnotes/snjs'; import { SNTag } from '@standardnotes/snjs';
import { NoteTag } from './NoteTag';
type Props = { type Props = {
application: WebApplication; application: WebApplication;
appState: AppState; appState: AppState;
}; };
const NoteTags = observer(({ application, appState }: Props) => { const NoteTagsContainer = observer(({ application, appState }: Props) => {
const { const {
overflowedTagsCount, overflowedTagsCount,
tags, tags,
@@ -29,31 +29,15 @@ const NoteTags = observer(({ application, appState }: Props) => {
const containerRef = useRef<HTMLDivElement>(); const containerRef = useRef<HTMLDivElement>();
const tagsContainerRef = useRef<HTMLDivElement>(); const tagsContainerRef = useRef<HTMLDivElement>();
const tagsRef = useRef<HTMLButtonElement[]>([]); const tagsRef = useRef<HTMLButtonElement[]>([]);
const overflowButtonRef = useRef<HTMLButtonElement>();
tagsRef.current = []; tagsRef.current = [];
useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => { useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => {
if (overflowButtonRef.current || tagsContainerExpanded) { if (tagsContainerExpanded) {
appState.activeNote.setTagsContainerExpanded(expanded); 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( const isTagOverflowed = useCallback(
(tagElement?: HTMLButtonElement): boolean | undefined => { (tagElement?: HTMLButtonElement): boolean | undefined => {
if (!tagElement) { if (!tagElement) {
@@ -144,10 +128,7 @@ const NoteTags = observer(({ application, appState }: Props) => {
tagResizeObserver.disconnect(); tagResizeObserver.disconnect();
} }
}; };
}, [reloadTagsContainerLayout, tags]); }, [reloadTagsContainerLayout]);
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 ( return (
<div <div
@@ -164,38 +145,18 @@ const NoteTags = observer(({ application, appState }: Props) => {
maxWidth: tagsContainerMaxWidth, maxWidth: tagsContainerMaxWidth,
}} }}
> >
{tags.map((tag: SNTag, index: number) => { {tags.map((tag: SNTag, index: number) => (
const overflowed = <NoteTag
!tagsContainerExpanded && appState={appState}
lastVisibleTagIndex && tagsRef={tagsRef}
index > lastVisibleTagIndex; index={index}
return ( tag={tag}
<button maxWidth={tagsContainerMaxWidth}
className={`${tagClass} pl-1 mr-2`} overflowed={!tagsContainerExpanded &&
style={{ maxWidth: tagsContainerMaxWidth }} !!lastVisibleTagIndex &&
ref={(element) => { index > lastVisibleTagIndex}
if (element) { />
tagsRef.current[index] = element; ))}
}
}}
onClick={() => onTagClick(tag)}
onKeyUp={(event) => {
if (event.key === 'Backspace') {
onTagBackspacePress(tag, index);
}
}}
tabIndex={overflowed ? -1 : 0}
>
<Icon
type="hashtag"
className="sn-icon--small color-neutral mr-1"
/>
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
{tag.title}
</span>
</button>
);
})}
<AutocompleteTagInput <AutocompleteTagInput
application={application} application={application}
appState={appState} appState={appState}
@@ -205,9 +166,8 @@ const NoteTags = observer(({ application, appState }: Props) => {
</div> </div>
{tagsOverflowed && ( {tagsOverflowed && (
<button <button
ref={overflowButtonRef}
type="button" type="button"
className={`${tagClass} pl-2 ml-1 absolute`} className="sn-tag ml-1 px-2 absolute"
onClick={expandTags} onClick={expandTags}
style={{ left: overflowCountPosition }} style={{ left: overflowCountPosition }}
> >
@@ -218,4 +178,4 @@ const NoteTags = observer(({ application, appState }: Props) => {
); );
}); });
export const NoteTagsDirective = toDirective<Props>(NoteTags); export const NoteTagsContainerDirective = toDirective<Props>(NoteTagsContainer);

View File

@@ -17,6 +17,7 @@ export class ActiveNoteState {
tagsContainerMaxWidth: number | 'auto' = 0; tagsContainerMaxWidth: number | 'auto' = 0;
tagsContainerExpanded = false; tagsContainerExpanded = false;
overflowedTagsCount = 0; overflowedTagsCount = 0;
tagFocused = false;
constructor( constructor(
private application: WebApplication, private application: WebApplication,
@@ -28,12 +29,14 @@ export class ActiveNoteState {
tagsContainerMaxWidth: observable, tagsContainerMaxWidth: observable,
tagsContainerExpanded: observable, tagsContainerExpanded: observable,
overflowedTagsCount: observable, overflowedTagsCount: observable,
tagFocused: observable,
tagsOverflowed: computed, tagsOverflowed: computed,
setTagsContainerMaxWidth: action, setTagsContainerMaxWidth: action,
setTagsContainerExpanded: action, setTagsContainerExpanded: action,
setOverflowedTagsCount: action, setOverflowedTagsCount: action,
setTagFocused: action,
reloadTags: action, reloadTags: action,
}); });
@@ -67,6 +70,10 @@ export class ActiveNoteState {
this.overflowedTagsCount = count; this.overflowedTagsCount = count;
} }
setTagFocused(focused: boolean): void {
this.tagFocused = focused;
}
reloadTags(): void { reloadTags(): void {
const { activeNote } = this; const { activeNote } = this;
if (activeNote) { if (activeNote) {
@@ -79,13 +86,14 @@ export class ActiveNoteState {
const defaultFontSize = window.getComputedStyle( const defaultFontSize = window.getComputedStyle(
document.documentElement document.documentElement
).fontSize; ).fontSize;
const containerMargins = parseFloat(defaultFontSize) * 4; const containerMargins = parseFloat(defaultFontSize) * 6;
const deleteButtonMargin = this.tagFocused ? parseFloat(defaultFontSize) * 1.25 : 0;
const editorWidth = const editorWidth =
document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth; document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth;
if (editorWidth) { if (editorWidth) {
this.appState.activeNote.setTagsContainerMaxWidth( this.appState.activeNote.setTagsContainerMaxWidth(
editorWidth - containerMargins editorWidth - containerMargins + deleteButtonMargin
); );
} }
} }

View File

@@ -52,7 +52,7 @@
app-state='self.appState', app-state='self.appState',
ng-if='self.appState.notes.selectedNotesCount > 0' ng-if='self.appState.notes.selectedNotesCount > 0'
) )
note-tags( note-tags-container(
application='self.application' application='self.application'
app-state='self.appState' app-state='self.appState'
) )

View File

@@ -69,6 +69,10 @@
margin-left: -0.25rem; margin-left: -0.25rem;
} }
.-mr-1 {
margin-right: -0.25rem;
}
.py-1 { .py-1 {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
@@ -82,6 +86,16 @@
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.py-1\.5 { .py-1\.5 {
padding-top: 0.375rem; padding-top: 0.375rem;
padding-bottom: 0.375rem; padding-bottom: 0.375rem;
@@ -407,3 +421,21 @@
@extend .py-2; @extend .py-2;
} }
} }
.sn-tag {
@extend .h-6;
@extend .bg-contrast;
@extend .border-0;
@extend .rounded;
@extend .text-xs;
@extend .color-text;
@extend .py-1;
@extend .py-2;
@extend .pr-2;
@extend .flex;
@extend .items-center;
@extend .mt-2;
@extend .cursor-pointer;
@extend .hover\:bg-secondary-contrast;
@extend .focus\:bg-secondary-contrast;
}