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

View File

@@ -4,7 +4,7 @@ import { toDirective } from './utils';
import { Icon } from './Icon'; 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 { useRef } from 'preact/hooks'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { SNTag } from '@standardnotes/snjs'; import { SNTag } from '@standardnotes/snjs';
type Props = { type Props = {
@@ -12,35 +12,142 @@ type Props = {
appState: AppState; 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 NoteTags = observer(({ application, appState }: Props) => {
const { activeNoteTags } = appState.notes; 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) => { const onTagBackspacePress = async (tag: SNTag) => {
await appState.notes.removeTagFromActiveNote(tag); 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 ( return (
<div className="flex flex-wrap"> <div
{activeNoteTags.map((tag, index) => ( 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 <button
className={`bg-contrast border-0 rounded text-xs color-text py-1 pl-1 pr-2 flex items-center type="button"
mt-2 mr-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`} className={`${tagClass} pl-2 absolute`}
ref={index === activeNoteTags.length - 1 ? lastTagRef : undefined} style={{ left: overflowCountPosition }}
onKeyUp={(event) => { onClick={expandTags}
if (event.key === 'Backspace') {
onTagBackspacePress(tag);
}
}}
> >
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" /> +{overflowedTagsCount}
<span className="max-w-xs whitespace-nowrap overflow-hidden overflow-ellipsis">
{tag.title}
</span>
</button> </button>
))} )}
<AutocompleteTagInput application={application} appState={appState} lastTagRef={lastTagRef} />
</div> </div>
); );
}); });

View File

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

View File

@@ -24,7 +24,7 @@
ng-if="self.showLockedIcon" ng-if="self.showLockedIcon"
) )
| {{self.lockText}} | {{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' ng-show='self.note && !self.note.errorDecrypting'
) )
div.flex-grow( div.flex-grow(
@@ -41,11 +41,10 @@
select-on-focus='true', select-on-focus='true',
spellcheck='false' spellcheck='false'
) )
.editor-tags note-tags(
note-tags( application='self.application'
application='self.application' app-state='self.appState'
app-state='self.appState' )
)
div.flex.items-center div.flex.items-center
#save-status #save-status
.message( .message(