refactor: extract shared logic to active note state
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { SNTag } from '@standardnotes/snjs';
|
||||
import { FunctionalComponent, RefObject } from 'preact';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { Icon } from './Icon';
|
||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
||||
@@ -10,15 +10,13 @@ import { AppState } from '@/ui_models/app_state';
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
tagsRef: RefObject<HTMLButtonElement[]>;
|
||||
};
|
||||
|
||||
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
||||
application,
|
||||
appState,
|
||||
tagsRef,
|
||||
}) => {
|
||||
const { tags, tagsContainerMaxWidth, tagsOverflowed } = appState.activeNote;
|
||||
const { tagElements, tags, tagsContainerMaxWidth, tagsOverflowed } = appState.activeNote;
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
@@ -85,13 +83,9 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
||||
};
|
||||
|
||||
const reloadInputOverflowed = useCallback(() => {
|
||||
let overflowed = false;
|
||||
if (!tagsOverflowed && tagsRef.current && tagsRef.current.length > 0) {
|
||||
const firstTagTop = tagsRef.current[0].offsetTop;
|
||||
overflowed = inputRef.current.offsetTop > firstTagTop;
|
||||
}
|
||||
const overflowed = !tagsOverflowed && appState.activeNote.isElementOverflowed(inputRef.current);
|
||||
appState.activeNote.setInputOverflowed(overflowed);
|
||||
}, [appState.activeNote, tagsOverflowed, tagsRef]);
|
||||
}, [appState.activeNote, tagsOverflowed]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadInputOverflowed();
|
||||
@@ -124,10 +118,9 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
||||
if (
|
||||
event.key === 'Backspace' &&
|
||||
searchQuery === '' &&
|
||||
tagsRef.current &&
|
||||
tagsRef.current.length > 1
|
||||
tagElements.length > 0
|
||||
) {
|
||||
tagsRef.current[tagsRef.current.length - 1].focus();
|
||||
tagElements[tagElements.length - 1]?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { Icon } from './Icon';
|
||||
import { FunctionalComponent, RefObject } from 'preact';
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import { useCallback, useRef, useState } from 'preact/hooks';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { SNTag } from '@standardnotes/snjs/dist/@types';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
index: number;
|
||||
tagsRef: RefObject<HTMLButtonElement[]>;
|
||||
tag: SNTag;
|
||||
overflowed: boolean;
|
||||
maxWidth: number | 'auto';
|
||||
overflowButtonRef: RefObject<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export const NoteTag: FunctionalComponent<Props> = ({
|
||||
appState,
|
||||
index,
|
||||
tagsRef,
|
||||
tag,
|
||||
overflowed,
|
||||
maxWidth,
|
||||
}) => {
|
||||
export const NoteTag: FunctionalComponent<Props> = ({ appState, tag, overflowButtonRef }) => {
|
||||
const {
|
||||
tags,
|
||||
tagsContainerMaxWidth,
|
||||
} = appState.activeNote;
|
||||
|
||||
const [overflowed, setOverflowed] = useState(false);
|
||||
const [showDeleteButton, setShowDeleteButton] = useState(false);
|
||||
|
||||
const deleteTagRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const deleteTag = async () => {
|
||||
await appState.activeNote.removeTagFromActiveNote(tag);
|
||||
const previousTag = appState.activeNote.getPreviousTag(tag);
|
||||
|
||||
if (index > 0 && tagsRef.current) {
|
||||
tagsRef.current[index - 1].focus();
|
||||
if (previousTag) {
|
||||
const previousTagElement = appState.activeNote.getTagElement(previousTag);
|
||||
previousTagElement?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,21 +42,33 @@ export const NoteTag: FunctionalComponent<Props> = ({
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
appState.activeNote.setTagFocused(false);
|
||||
if ((event.relatedTarget as Node) !== deleteTagRef.current) {
|
||||
const relatedTarget = event.relatedTarget as Node;
|
||||
if (relatedTarget === overflowButtonRef.current) {
|
||||
(event.target as HTMLButtonElement).focus();
|
||||
} else if (relatedTarget !== deleteTagRef.current) {
|
||||
appState.activeNote.setTagFocused(false);
|
||||
setShowDeleteButton(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reloadOverflowed = useCallback(() => {
|
||||
const overflowed = appState.activeNote.isTagOverflowed(tag);
|
||||
setOverflowed(overflowed);
|
||||
}, [appState.activeNote, tag]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadOverflowed();
|
||||
}, [reloadOverflowed, tags, tagsContainerMaxWidth]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={(element) => {
|
||||
if (element && tagsRef.current) {
|
||||
tagsRef.current[index] = element;
|
||||
if (element) {
|
||||
appState.activeNote.setTagElement(tag, element);
|
||||
}
|
||||
}}
|
||||
className="sn-tag pl-1 pr-2 mr-2"
|
||||
style={{ maxWidth }}
|
||||
style={{ maxWidth: tagsContainerMaxWidth }}
|
||||
onClick={onTagClick}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { toDirective, useCloseOnClickOutside } from './utils';
|
||||
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 = {
|
||||
@@ -15,7 +14,9 @@ type Props = {
|
||||
const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
||||
const {
|
||||
inputOverflowed,
|
||||
overflowCountPosition,
|
||||
overflowedTagsCount,
|
||||
tagElements,
|
||||
tags,
|
||||
tagsContainerMaxWidth,
|
||||
tagsContainerExpanded,
|
||||
@@ -23,15 +24,9 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
||||
} = appState.activeNote;
|
||||
|
||||
const [expandedContainerHeight, setExpandedContainerHeight] = useState(0);
|
||||
const [lastVisibleTagIndex, setLastVisibleTagIndex] =
|
||||
useState<number | null>(null);
|
||||
const [overflowCountPosition, setOverflowCountPosition] = useState(0);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const tagsContainerRef = useRef<HTMLDivElement>();
|
||||
const tagsRef = useRef<HTMLButtonElement[]>([]);
|
||||
|
||||
tagsRef.current = [];
|
||||
const overflowButtonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => {
|
||||
if (tagsContainerExpanded) {
|
||||
@@ -39,88 +34,29 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
||||
}
|
||||
});
|
||||
|
||||
const isTagOverflowed = useCallback(
|
||||
(tagElement?: HTMLButtonElement): boolean | undefined => {
|
||||
if (!tagElement) {
|
||||
return;
|
||||
}
|
||||
if (tagsContainerExpanded) {
|
||||
return false;
|
||||
}
|
||||
const firstTagTop = tagsRef.current[0].offsetTop;
|
||||
return tagElement.offsetTop > firstTagTop;
|
||||
},
|
||||
[tagsContainerExpanded]
|
||||
);
|
||||
|
||||
const reloadLastVisibleTagIndex = useCallback(() => {
|
||||
if (tagsContainerExpanded) {
|
||||
return tags.length - 1;
|
||||
}
|
||||
const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) =>
|
||||
isTagOverflowed(tagElement)
|
||||
);
|
||||
if (firstOverflowedTagIndex > -1) {
|
||||
setLastVisibleTagIndex(firstOverflowedTagIndex - 1);
|
||||
} else {
|
||||
setLastVisibleTagIndex(null);
|
||||
}
|
||||
}, [isTagOverflowed, tags, tagsContainerExpanded]);
|
||||
|
||||
const reloadExpandedContainersHeight = useCallback(() => {
|
||||
const reloadExpandedContainerHeight = useCallback(() => {
|
||||
setExpandedContainerHeight(tagsContainerRef.current.scrollHeight);
|
||||
}, []);
|
||||
|
||||
const reloadOverflowCount = useCallback(() => {
|
||||
const count = tagsRef.current.filter((tagElement) =>
|
||||
isTagOverflowed(tagElement)
|
||||
).length;
|
||||
appState.activeNote.setOverflowedTagsCount(count);
|
||||
}, [appState.activeNote, isTagOverflowed]);
|
||||
|
||||
const reloadOverflowCountPosition = useCallback(() => {
|
||||
if (tagsContainerExpanded || !lastVisibleTagIndex) {
|
||||
return;
|
||||
}
|
||||
if (tagsRef.current[lastVisibleTagIndex]) {
|
||||
const {
|
||||
offsetLeft: lastVisibleTagLeft,
|
||||
clientWidth: lastVisibleTagWidth,
|
||||
} = tagsRef.current[lastVisibleTagIndex];
|
||||
setOverflowCountPosition(lastVisibleTagLeft + lastVisibleTagWidth);
|
||||
}
|
||||
}, [lastVisibleTagIndex, tagsContainerExpanded]);
|
||||
|
||||
const expandTags = () => {
|
||||
appState.activeNote.setTagsContainerExpanded(true);
|
||||
};
|
||||
|
||||
const reloadTagsContainerLayout = useCallback(() => {
|
||||
useEffect(() => {
|
||||
appState.activeNote.reloadTagsContainerLayout();
|
||||
reloadLastVisibleTagIndex();
|
||||
reloadExpandedContainersHeight();
|
||||
reloadOverflowCount();
|
||||
reloadOverflowCountPosition();
|
||||
reloadExpandedContainerHeight();
|
||||
}, [
|
||||
appState.activeNote,
|
||||
reloadLastVisibleTagIndex,
|
||||
reloadExpandedContainersHeight,
|
||||
reloadOverflowCount,
|
||||
reloadOverflowCountPosition,
|
||||
reloadExpandedContainerHeight,
|
||||
tags,
|
||||
tagsContainerMaxWidth,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadTagsContainerLayout();
|
||||
}, [reloadTagsContainerLayout, tags, tagsContainerMaxWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
let tagResizeObserver: ResizeObserver;
|
||||
if (ResizeObserver) {
|
||||
tagResizeObserver = new ResizeObserver(() => {
|
||||
reloadTagsContainerLayout();
|
||||
appState.activeNote.reloadTagsContainerLayout();
|
||||
reloadExpandedContainerHeight();
|
||||
});
|
||||
tagsRef.current.forEach((tagElement) =>
|
||||
tagResizeObserver.observe(tagElement)
|
||||
tagElements.forEach(
|
||||
(tagElement) => tagElement && tagResizeObserver.observe(tagElement)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,14 +65,13 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
||||
tagResizeObserver.disconnect();
|
||||
}
|
||||
};
|
||||
}, [reloadTagsContainerLayout]);
|
||||
}, [appState.activeNote, reloadExpandedContainerHeight, tagElements]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex transition-height duration-150 relative ${
|
||||
inputOverflowed ? 'h-18' : 'h-9'
|
||||
}`}
|
||||
ref={containerRef}
|
||||
style={tagsContainerExpanded ? { height: expandedContainerHeight } : {}}
|
||||
>
|
||||
<div
|
||||
@@ -148,31 +83,22 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
||||
maxWidth: tagsContainerMaxWidth,
|
||||
}}
|
||||
>
|
||||
{tags.map((tag: SNTag, index: number) => (
|
||||
{tags.map((tag) => (
|
||||
<NoteTag
|
||||
key={tag.uuid}
|
||||
appState={appState}
|
||||
tagsRef={tagsRef}
|
||||
index={index}
|
||||
tag={tag}
|
||||
maxWidth={tagsContainerMaxWidth}
|
||||
overflowed={
|
||||
!tagsContainerExpanded &&
|
||||
!!lastVisibleTagIndex &&
|
||||
index > lastVisibleTagIndex
|
||||
}
|
||||
overflowButtonRef={overflowButtonRef}
|
||||
/>
|
||||
))}
|
||||
<AutocompleteTagInput
|
||||
application={application}
|
||||
appState={appState}
|
||||
tagsRef={tagsRef}
|
||||
/>
|
||||
<AutocompleteTagInput application={application} appState={appState} />
|
||||
</div>
|
||||
{tagsOverflowed && (
|
||||
<button
|
||||
ref={overflowButtonRef}
|
||||
type="button"
|
||||
className="sn-tag ml-1 px-2 absolute"
|
||||
onClick={expandTags}
|
||||
className="sn-tag ml-1 absolute"
|
||||
onClick={() => appState.activeNote.setTagsContainerExpanded(true)}
|
||||
style={{ left: overflowCountPosition }}
|
||||
>
|
||||
+{overflowedTagsCount}
|
||||
|
||||
Reference in New Issue
Block a user