feat: make tags container expandable

This commit is contained in:
Antonella Sgarlatta
2021-05-27 21:17:39 -03:00
parent 48562e8b26
commit 7ac5856205
4 changed files with 136 additions and 30 deletions

View File

@@ -10,13 +10,13 @@ import { AppState } from '@/ui_models/app_state';
type Props = {
application: WebApplication;
appState: AppState;
lastTagRef: RefObject<HTMLButtonElement>;
tagsRef: RefObject<HTMLButtonElement[]>
};
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
application,
appState,
lastTagRef,
tagsRef,
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [dropdownVisible, setDropdownVisible] = useState(false);
@@ -100,8 +100,8 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
onBlur={closeOnBlur}
onFocus={showDropdown}
onKeyUp={(event) => {
if (event.key === 'Backspace' && searchQuery === '') {
lastTagRef.current?.focus();
if (event.key === 'Backspace' && searchQuery === '' && tagsRef.current && tagsRef.current.length > 1) {
tagsRef.current[tagsRef.current.length - 1].focus();
}
}}
/>

View File

@@ -4,7 +4,7 @@ import { toDirective } from './utils';
import { Icon } from './Icon';
import { AutocompleteTagInput } from './AutocompleteTagInput';
import { WebApplication } from '@/ui_models/application';
import { useRef } from 'preact/hooks';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { SNTag } from '@standardnotes/snjs';
type Props = {
@@ -12,35 +12,142 @@ type Props = {
appState: AppState;
};
const TAGS_ROW_RIGHT_MARGIN = 92;
const TAGS_ROW_HEIGHT = 32;
const MIN_OVERFLOW_TOP = 76;
const TAG_RIGHT_MARGIN = 8;
const NoteTags = observer(({ application, appState }: Props) => {
const { activeNoteTags } = appState.notes;
const lastTagRef = useRef<HTMLButtonElement>();
const [tagsContainerMaxWidth, setTagsContainerMaxWidth] =
useState<number | 'auto'>('auto');
const [overflowedTagsCount, setOverflowedTagsCount] = useState(0);
const [overflowCountPosition, setOverflowCountPosition] = useState(0);
const [tagsContainerCollapsed, setTagsContainerCollapsed] = useState(true);
const [containerHeight, setContainerHeight] = useState(TAGS_ROW_HEIGHT);
const containerRef = useRef<HTMLDivElement>();
const tagsContainerRef = useRef<HTMLDivElement>();
const tagsRef = useRef<HTMLButtonElement[]>([]);
tagsRef.current = [];
const onTagBackspacePress = async (tag: SNTag) => {
await appState.notes.removeTagFromActiveNote(tag);
lastTagRef.current?.focus();
if (tagsRef.current.length > 1) {
tagsRef.current[tagsRef.current.length - 1].focus();
}
};
const reloadOverflowCount = useCallback(() => {
const editorElement = document.getElementById('editor-column');
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
);
}
overflowCount += 1;
}
}
setOverflowedTagsCount(overflowCount);
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]);
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`;
return (
<div className="flex flex-wrap">
{activeNoteTags.map((tag, index) => (
<div
className="flex"
ref={containerRef}
style={{ height: containerHeight }}
>
<div
ref={tagsContainerRef}
className={`absolute flex flex-wrap ${
tagsContainerCollapsed ? 'overflow-hidden' : ''
}`}
style={{
maxWidth: tagsContainerMaxWidth,
height: TAGS_ROW_HEIGHT,
marginRight: TAGS_ROW_RIGHT_MARGIN,
}}
>
{activeNoteTags.map((tag, index) => (
<button
className={`${tagClass} pl-1`}
style={{ maxWidth: tagsContainerMaxWidth }}
ref={(element) => {
if (element) {
tagsRef.current[index] = element;
}
}}
onKeyUp={(event) => {
if (event.key === 'Backspace') {
onTagBackspacePress(tag);
}
}}
>
<Icon
type="hashtag"
className="sn-icon--small color-neutral mr-1"
/>
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
{tag.title}
</span>
</button>
))}
<AutocompleteTagInput
application={application}
appState={appState}
tagsRef={tagsRef}
/>
</div>
{overflowedTagsCount > 1 && tagsContainerCollapsed && (
<button
className={`bg-contrast border-0 rounded text-xs color-text py-1 pl-1 pr-2 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);
}
}}
type="button"
className={`${tagClass} pl-2 absolute`}
style={{ left: overflowCountPosition }}
onClick={expandTags}
>
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
<span className="max-w-xs whitespace-nowrap overflow-hidden overflow-ellipsis">
{tag.title}
</span>
+{overflowedTagsCount}
</button>
))}
<AutocompleteTagInput application={application} appState={appState} lastTagRef={lastTagRef} />
)}
</div>
);
});

View File

@@ -54,7 +54,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
}}
onBlur={closeOnBlur}
ref={buttonRef}
className="sn-icon-button"
className="sn-icon-button mt-2"
>
<VisuallyHidden>Actions</VisuallyHidden>
<Icon type="more" className="block" />

View File

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