fix: adjust layout for all zoom values and font sizes
This commit is contained in:
@@ -12,28 +12,25 @@ type Props = {
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TAGS_ROW_HEIGHT = 36;
|
|
||||||
const MIN_OVERFLOW_TOP = 76;
|
|
||||||
const TAG_RIGHT_MARGIN = 8;
|
|
||||||
|
|
||||||
const NoteTags = observer(({ application, appState }: Props) => {
|
const NoteTags = observer(({ application, appState }: Props) => {
|
||||||
const {
|
const {
|
||||||
overflowedTagsCount,
|
overflowedTagsCount,
|
||||||
tags,
|
tags,
|
||||||
tagsContainerPosition,
|
|
||||||
tagsContainerMaxWidth,
|
tagsContainerMaxWidth,
|
||||||
tagsContainerExpanded,
|
tagsContainerExpanded,
|
||||||
tagsOverflowed,
|
tagsOverflowed,
|
||||||
} = appState.activeNote;
|
} = appState.activeNote;
|
||||||
|
|
||||||
const [containerHeight, setContainerHeight] =
|
const [expandedContainerHeight, setExpandedContainerHeight] = useState(0);
|
||||||
useState(TAGS_ROW_HEIGHT);
|
const [lastVisibleTagIndex, setLastVisibleTagIndex] =
|
||||||
|
useState<number | null>(null);
|
||||||
const [overflowCountPosition, setOverflowCountPosition] = useState(0);
|
const [overflowCountPosition, setOverflowCountPosition] = useState(0);
|
||||||
|
|
||||||
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>();
|
const overflowButtonRef = useRef<HTMLButtonElement>();
|
||||||
|
|
||||||
tagsRef.current = [];
|
tagsRef.current = [];
|
||||||
|
|
||||||
useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => {
|
useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => {
|
||||||
@@ -42,16 +39,16 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onTagBackspacePress = async (tag: SNTag) => {
|
const onTagBackspacePress = async (tag: SNTag, index: number) => {
|
||||||
await appState.activeNote.removeTagFromActiveNote(tag);
|
await appState.activeNote.removeTagFromActiveNote(tag);
|
||||||
|
|
||||||
if (tagsRef.current.length > 1) {
|
if (index > 0) {
|
||||||
tagsRef.current[tagsRef.current.length - 1].focus();
|
tagsRef.current[index - 1].focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTagClick = (clickedTag: SNTag) => {
|
const onTagClick = (clickedTag: SNTag) => {
|
||||||
const tagIndex = tags.findIndex(tag => tag.uuid === clickedTag.uuid);
|
const tagIndex = tags.findIndex((tag) => tag.uuid === clickedTag.uuid);
|
||||||
if (tagsRef.current[tagIndex] === document.activeElement) {
|
if (tagsRef.current[tagIndex] === document.activeElement) {
|
||||||
appState.setSelectedTag(clickedTag);
|
appState.setSelectedTag(clickedTag);
|
||||||
}
|
}
|
||||||
@@ -65,31 +62,29 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
|||||||
if (tagsContainerExpanded) {
|
if (tagsContainerExpanded) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP;
|
const firstTagTop = tagsRef.current[0].offsetTop;
|
||||||
|
return tagElement.offsetTop > firstTagTop;
|
||||||
},
|
},
|
||||||
[tagsContainerExpanded]
|
[tagsContainerExpanded]
|
||||||
);
|
);
|
||||||
|
|
||||||
const reloadOverflowCountPosition = useCallback(() => {
|
const reloadLastVisibleTagIndex = useCallback(() => {
|
||||||
|
if (tagsContainerExpanded) {
|
||||||
|
return tags.length - 1;
|
||||||
|
}
|
||||||
const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) =>
|
const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) =>
|
||||||
isTagOverflowed(tagElement)
|
isTagOverflowed(tagElement)
|
||||||
);
|
);
|
||||||
if (tagsContainerExpanded || firstOverflowedTagIndex < 1) {
|
if (firstOverflowedTagIndex > -1) {
|
||||||
return;
|
setLastVisibleTagIndex(firstOverflowedTagIndex - 1);
|
||||||
|
} else {
|
||||||
|
setLastVisibleTagIndex(null);
|
||||||
}
|
}
|
||||||
const previousTagRect =
|
}, [isTagOverflowed, tags, tagsContainerExpanded]);
|
||||||
tagsRef.current[firstOverflowedTagIndex - 1].getBoundingClientRect();
|
|
||||||
const position =
|
|
||||||
previousTagRect.right - (tagsContainerPosition ?? 0) + TAG_RIGHT_MARGIN;
|
|
||||||
setOverflowCountPosition(position);
|
|
||||||
}, [isTagOverflowed, tagsContainerExpanded, tagsContainerPosition]);
|
|
||||||
|
|
||||||
const reloadContainersHeight = useCallback(() => {
|
const reloadExpandedContainersHeight = useCallback(() => {
|
||||||
const containerHeight = tagsContainerExpanded
|
setExpandedContainerHeight(tagsContainerRef.current.scrollHeight);
|
||||||
? tagsContainerRef.current.scrollHeight
|
}, []);
|
||||||
: TAGS_ROW_HEIGHT;
|
|
||||||
setContainerHeight(containerHeight);
|
|
||||||
}, [tagsContainerExpanded]);
|
|
||||||
|
|
||||||
const reloadOverflowCount = useCallback(() => {
|
const reloadOverflowCount = useCallback(() => {
|
||||||
const count = tagsRef.current.filter((tagElement) =>
|
const count = tagsRef.current.filter((tagElement) =>
|
||||||
@@ -98,72 +93,108 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
|||||||
appState.activeNote.setOverflowedTagsCount(count);
|
appState.activeNote.setOverflowedTagsCount(count);
|
||||||
}, [appState.activeNote, isTagOverflowed]);
|
}, [appState.activeNote, isTagOverflowed]);
|
||||||
|
|
||||||
|
const reloadOverflowCountPosition = useCallback(() => {
|
||||||
|
if (tagsContainerExpanded || !lastVisibleTagIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { offsetLeft: lastVisibleTagLeft, clientWidth: lastVisibleTagWidth } =
|
||||||
|
tagsRef.current[lastVisibleTagIndex];
|
||||||
|
console.log(tagsRef.current[0].offsetLeft);
|
||||||
|
setOverflowCountPosition(lastVisibleTagLeft + lastVisibleTagWidth);
|
||||||
|
}, [lastVisibleTagIndex, tagsContainerExpanded]);
|
||||||
|
|
||||||
const expandTags = () => {
|
const expandTags = () => {
|
||||||
appState.activeNote.setTagsContainerExpanded(true);
|
appState.activeNote.setTagsContainerExpanded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const reloadTagsContainerLayout = useCallback(() => {
|
||||||
appState.activeNote.reloadTagsContainerLayout();
|
appState.activeNote.reloadTagsContainerLayout();
|
||||||
reloadOverflowCountPosition();
|
reloadLastVisibleTagIndex();
|
||||||
reloadContainersHeight();
|
reloadExpandedContainersHeight();
|
||||||
reloadOverflowCount();
|
reloadOverflowCount();
|
||||||
|
reloadOverflowCountPosition();
|
||||||
}, [
|
}, [
|
||||||
appState.activeNote,
|
appState.activeNote,
|
||||||
reloadOverflowCountPosition,
|
reloadLastVisibleTagIndex,
|
||||||
reloadContainersHeight,
|
reloadExpandedContainersHeight,
|
||||||
reloadOverflowCount,
|
reloadOverflowCount,
|
||||||
tags,
|
reloadOverflowCountPosition,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadTagsContainerLayout;
|
||||||
|
}, [
|
||||||
|
reloadTagsContainerLayout,
|
||||||
|
tags,
|
||||||
|
tagsContainerMaxWidth,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let tagResizeObserver: ResizeObserver;
|
||||||
|
if (ResizeObserver) {
|
||||||
|
tagResizeObserver = new ResizeObserver(() => {
|
||||||
|
reloadTagsContainerLayout();
|
||||||
|
});
|
||||||
|
tagsRef.current.forEach((tagElement) => tagResizeObserver.observe(tagElement));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (tagResizeObserver) {
|
||||||
|
tagResizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [reloadTagsContainerLayout]);
|
||||||
|
|
||||||
const tagClass = `h-6 bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center
|
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`;
|
mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex transition-height duration-150"
|
className="flex transition-height duration-150 h-9 relative"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
style={{ height: containerHeight }}
|
style={tagsContainerExpanded ? { height: expandedContainerHeight } : {}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={tagsContainerRef}
|
ref={tagsContainerRef}
|
||||||
className={`absolute flex flex-wrap pl-1 -ml-1 ${
|
className={`absolute bg-default h-9 flex flex-wrap pl-1 -ml-1 ${
|
||||||
tagsContainerExpanded ? '' : 'overflow-hidden'
|
tagsContainerExpanded ? '' : 'overflow-y-hidden'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
maxWidth: tagsContainerMaxWidth,
|
maxWidth: tagsContainerMaxWidth,
|
||||||
height: TAGS_ROW_HEIGHT,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tags.map((tag: SNTag, index: number) => (
|
{tags.map((tag: SNTag, index: number) => {
|
||||||
<button
|
const overflowed =
|
||||||
className={`${tagClass} pl-1 mr-2 transition-opacity duration-150 ${
|
!tagsContainerExpanded &&
|
||||||
isTagOverflowed(tagsRef.current[index])
|
lastVisibleTagIndex &&
|
||||||
? 'opacity-0'
|
index > lastVisibleTagIndex;
|
||||||
: 'opacity-1'
|
return (
|
||||||
}`}
|
<button
|
||||||
style={{ maxWidth: tagsContainerMaxWidth }}
|
className={`${tagClass} pl-1 mr-2`}
|
||||||
ref={(element) => {
|
style={{ maxWidth: tagsContainerMaxWidth }}
|
||||||
if (element) {
|
ref={(element) => {
|
||||||
tagsRef.current[index] = element;
|
if (element) {
|
||||||
}
|
tagsRef.current[index] = element;
|
||||||
}}
|
}
|
||||||
onClick={() => onTagClick(tag)}
|
}}
|
||||||
onKeyUp={(event) => {
|
onClick={() => onTagClick(tag)}
|
||||||
if (event.key === 'Backspace') {
|
onKeyUp={(event) => {
|
||||||
onTagBackspacePress(tag);
|
if (event.key === 'Backspace') {
|
||||||
}
|
onTagBackspacePress(tag, index);
|
||||||
}}
|
}
|
||||||
tabIndex={isTagOverflowed(tagsRef.current[index]) ? -1 : 0}
|
}}
|
||||||
>
|
tabIndex={overflowed ? -1 : 0}
|
||||||
<Icon
|
>
|
||||||
type="hashtag"
|
<Icon
|
||||||
className="sn-icon--small color-neutral mr-1"
|
type="hashtag"
|
||||||
/>
|
className="sn-icon--small color-neutral mr-1"
|
||||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
/>
|
||||||
{tag.title}
|
<span className="whitespace-nowrap overflow-y-hidden overflow-ellipsis">
|
||||||
</span>
|
{tag.title}
|
||||||
</button>
|
</span>
|
||||||
))}
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<AutocompleteTagInput
|
<AutocompleteTagInput
|
||||||
application={application}
|
application={application}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
@@ -175,9 +206,9 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
|||||||
<button
|
<button
|
||||||
ref={overflowButtonRef}
|
ref={overflowButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
className={`${tagClass} pl-2 absolute`}
|
className={`${tagClass} pl-2 ml-1 absolute`}
|
||||||
style={{ left: overflowCountPosition }}
|
|
||||||
onClick={expandTags}
|
onClick={expandTags}
|
||||||
|
style={{ left: overflowCountPosition }}
|
||||||
>
|
>
|
||||||
+{overflowedTagsCount}
|
+{overflowedTagsCount}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { AppState } from './app_state';
|
|||||||
|
|
||||||
export class ActiveNoteState {
|
export class ActiveNoteState {
|
||||||
tags: SNTag[] = [];
|
tags: SNTag[] = [];
|
||||||
tagsContainerPosition? = 0;
|
|
||||||
tagsContainerMaxWidth: number | 'auto' = 'auto';
|
tagsContainerMaxWidth: number | 'auto' = 'auto';
|
||||||
tagsContainerExpanded = false;
|
tagsContainerExpanded = false;
|
||||||
overflowedTagsCount = 0;
|
overflowedTagsCount = 0;
|
||||||
@@ -26,24 +25,18 @@ export class ActiveNoteState {
|
|||||||
) {
|
) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
tags: observable,
|
tags: observable,
|
||||||
tagsContainerPosition: observable,
|
|
||||||
tagsContainerMaxWidth: observable,
|
tagsContainerMaxWidth: observable,
|
||||||
tagsContainerExpanded: observable,
|
tagsContainerExpanded: observable,
|
||||||
overflowedTagsCount: observable,
|
overflowedTagsCount: observable,
|
||||||
|
|
||||||
tagsOverflowed: computed,
|
tagsOverflowed: computed,
|
||||||
|
|
||||||
setTagsContainerPosition: action,
|
|
||||||
setTagsContainerMaxWidth: action,
|
setTagsContainerMaxWidth: action,
|
||||||
setTagsContainerExpanded: action,
|
setTagsContainerExpanded: action,
|
||||||
setOverflowedTagsCount: action,
|
setOverflowedTagsCount: action,
|
||||||
reloadTags: action,
|
reloadTags: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tagsContainerPosition = document
|
|
||||||
.getElementById('editor-column')
|
|
||||||
?.getBoundingClientRect().left;
|
|
||||||
|
|
||||||
appEventListeners.push(
|
appEventListeners.push(
|
||||||
application.streamItems(
|
application.streamItems(
|
||||||
ContentType.Tag,
|
ContentType.Tag,
|
||||||
@@ -62,10 +55,6 @@ export class ActiveNoteState {
|
|||||||
return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded;
|
return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTagsContainerPosition(position: number): void {
|
|
||||||
this.tagsContainerPosition = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTagsContainerMaxWidth(width: number): void {
|
setTagsContainerMaxWidth(width: number): void {
|
||||||
this.tagsContainerMaxWidth = width;
|
this.tagsContainerMaxWidth = width;
|
||||||
}
|
}
|
||||||
@@ -86,16 +75,19 @@ export class ActiveNoteState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reloadTagsContainerLayout(): void {
|
reloadTagsContainerLayout(): void {
|
||||||
const MARGIN = 72;
|
|
||||||
const EDITOR_ELEMENT_ID = 'editor-column';
|
const EDITOR_ELEMENT_ID = 'editor-column';
|
||||||
const { clientWidth } = document.documentElement;
|
const defaultFontSize = window.getComputedStyle(
|
||||||
const editorPosition =
|
document.documentElement
|
||||||
document.getElementById(EDITOR_ELEMENT_ID)?.getBoundingClientRect()
|
).fontSize;
|
||||||
.left ?? 0;
|
const containerMargins = parseFloat(defaultFontSize) * 4;
|
||||||
this.appState.activeNote.setTagsContainerPosition(editorPosition);
|
const editorWidth =
|
||||||
this.appState.activeNote.setTagsContainerMaxWidth(
|
document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth;
|
||||||
clientWidth - editorPosition - MARGIN
|
|
||||||
);
|
if (editorWidth) {
|
||||||
|
this.appState.activeNote.setTagsContainerMaxWidth(
|
||||||
|
editorWidth - containerMargins
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTagToActiveNote(tag: SNTag): Promise<void> {
|
async addTagToActiveNote(tag: SNTag): Promise<void> {
|
||||||
|
|||||||
@@ -57,6 +57,10 @@
|
|||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ml-1 {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mr-1 {
|
.mr-1 {
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -192,6 +196,10 @@
|
|||||||
height: 2rem;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-9 {
|
||||||
|
height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-10 {
|
.h-10 {
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
@@ -212,8 +220,8 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overflow-hidden {
|
.overflow-y-hidden {
|
||||||
overflow: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overflow-ellipsis {
|
.overflow-ellipsis {
|
||||||
@@ -260,6 +268,10 @@
|
|||||||
width: 20rem;
|
width: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A button that is just an icon. Separated from .sn-button because there
|
* A button that is just an icon. Separated from .sn-button because there
|
||||||
* is almost no style overlap.
|
* is almost no style overlap.
|
||||||
|
|||||||
Reference in New Issue
Block a user