feat: add delete tag button and refactor NoteTag to separate component
This commit is contained in:
@@ -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]);
|
||||||
|
|||||||
91
app/assets/javascripts/components/NoteTag.tsx
Normal file
91
app/assets/javascripts/components/NoteTag.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user