refactor: extract shared logic to active note state

This commit is contained in:
Antonella Sgarlatta
2021-06-02 17:57:37 -03:00
parent 3db87099e0
commit 6fb68d2255
8 changed files with 184 additions and 164 deletions

View File

@@ -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();
}
}}
/>

View File

@@ -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') {

View File

@@ -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}