feat: use panel width event instead of ResizeObserver

This commit is contained in:
Antonella Sgarlatta
2021-05-31 12:59:35 -03:00
parent 7ac5856205
commit b7c2fa0b60
9 changed files with 185 additions and 110 deletions

View File

@@ -10,7 +10,7 @@ import { AppState } from '@/ui_models/app_state';
type Props = {
application: WebApplication;
appState: AppState;
tagsRef: RefObject<HTMLButtonElement[]>
tagsRef: RefObject<HTMLButtonElement[]>;
};
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
@@ -25,7 +25,7 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
const [hintVisible, setHintVisible] = useState(true);
const getActiveNoteTagResults = (query: string) => {
const { activeNote } = appState.notes;
const { activeNote } = appState.activeNote;
return application.searchTags(query, activeNote);
};
@@ -41,10 +41,13 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
setTagResults(getActiveNoteTagResults(''));
};
const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => {
setDropdownVisible(visible);
clearResults();
});
const [closeOnBlur, setLockCloseOnBlur] = useCloseOnBlur(
dropdownRef,
(visible: boolean) => {
setDropdownVisible(visible);
clearResults();
}
);
const showDropdown = () => {
const { clientHeight } = document.documentElement;
@@ -61,7 +64,7 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
const onTagOptionClick = async (tag: SNTag) => {
setLockCloseOnBlur(true);
await appState.notes.addTagToActiveNote(tag);
await appState.activeNote.addTagToActiveNote(tag);
inputRef.current.focus();
setTagResults(getActiveNoteTagResults(searchQuery));
setLockCloseOnBlur(false);
@@ -69,7 +72,7 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
const createAndAddNewTag = async () => {
const newTag = await application.findOrCreateTag(searchQuery);
await appState.notes.addTagToActiveNote(newTag);
await appState.activeNote.addTagToActiveNote(newTag);
clearResults();
inputRef.current.focus();
};
@@ -84,7 +87,9 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
};
useEffect(() => {
setHintVisible(searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery));
setHintVisible(
searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)
);
}, [tagResults, searchQuery]);
return (
@@ -100,7 +105,12 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
onBlur={closeOnBlur}
onFocus={showDropdown}
onKeyUp={(event) => {
if (event.key === 'Backspace' && searchQuery === '' && tagsRef.current && tagsRef.current.length > 1) {
if (
event.key === 'Backspace' &&
searchQuery === '' &&
tagsRef.current &&
tagsRef.current.length > 1
) {
tagsRef.current[tagsRef.current.length - 1].focus();
}
}}
@@ -128,7 +138,8 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
<span
key={index}
className={
substring.toLowerCase() === searchQuery.toLowerCase()
substring.toLowerCase() ===
searchQuery.toLowerCase()
? 'font-bold whitespace-pre-wrap'
: 'whitespace-pre-wrap'
}
@@ -151,13 +162,12 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
onClick={onTagHintClick}
onBlur={closeOnBlur}
>
<span>
Create new tag:
</span>
<span
className="bg-contrast rounded text-xs color-text p-1 flex ml-2"
>
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
<span>Create new tag:</span>
<span className="bg-contrast rounded text-xs color-text p-1 flex ml-2">
<Icon
type="hashtag"
className="sn-icon--small color-neutral mr-1"
/>
{searchQuery}
</span>
</button>

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 { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { useEffect, useRef, useState } from 'preact/hooks';
import { SNTag } from '@standardnotes/snjs';
type Props = {
@@ -13,14 +13,14 @@ type Props = {
};
const TAGS_ROW_RIGHT_MARGIN = 92;
const TAGS_ROW_HEIGHT = 32;
const TAGS_ROW_HEIGHT = 36;
const MIN_OVERFLOW_TOP = 76;
const TAG_RIGHT_MARGIN = 8;
const TAGS_RIGHT_MARGIN = 8;
const NoteTags = observer(({ application, appState }: Props) => {
const { activeNoteTags } = appState.notes;
const [tagsContainerMaxWidth, setTagsContainerMaxWidth] =
useState<number | 'auto'>('auto');
const { tags, tagsContainerPosition, tagsContainerMaxWidth } =
appState.activeNote;
const [overflowedTagsCount, setOverflowedTagsCount] = useState(0);
const [overflowCountPosition, setOverflowCountPosition] = useState(0);
const [tagsContainerCollapsed, setTagsContainerCollapsed] = useState(true);
@@ -32,23 +32,28 @@ const NoteTags = observer(({ application, appState }: Props) => {
tagsRef.current = [];
const onTagBackspacePress = async (tag: SNTag) => {
await appState.notes.removeTagFromActiveNote(tag);
await appState.activeNote.removeTagFromActiveNote(tag);
if (tagsRef.current.length > 1) {
tagsRef.current[tagsRef.current.length - 1].focus();
}
};
const reloadOverflowCount = useCallback(() => {
const editorElement = document.getElementById('editor-column');
const expandTags = () => {
setContainerHeight(tagsContainerRef.current.scrollHeight);
setTagsContainerCollapsed(false);
};
useEffect(() => {
appState.activeNote.reloadTagsContainerLayout();
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
(tagsContainerPosition ?? 0) +
TAGS_RIGHT_MARGIN
);
}
overflowCount += 1;
@@ -59,34 +64,12 @@ const NoteTags = observer(({ application, appState }: Props) => {
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]);
}, [
appState.activeNote,
tags,
tagsContainerCollapsed,
tagsContainerPosition,
]);
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`;
@@ -99,7 +82,7 @@ const NoteTags = observer(({ application, appState }: Props) => {
>
<div
ref={tagsContainerRef}
className={`absolute flex flex-wrap ${
className={`absolute flex flex-wrap pl-1 -ml-1 ${
tagsContainerCollapsed ? 'overflow-hidden' : ''
}`}
style={{
@@ -108,7 +91,7 @@ const NoteTags = observer(({ application, appState }: Props) => {
marginRight: TAGS_ROW_RIGHT_MARGIN,
}}
>
{activeNoteTags.map((tag, index) => (
{tags.map((tag: SNTag, index: number) => (
<button
className={`${tagClass} pl-1`}
style={{ maxWidth: tagsContainerMaxWidth }}

View File

@@ -38,6 +38,7 @@ interface PanelResizerScope {
index: number
minWidth: number
onResizeFinish: () => ResizeFinishCallback
onWidthEvent?: () => void
panelId: string
property: PanelSide
}
@@ -53,6 +54,7 @@ class PanelResizerCtrl implements PanelResizerScope {
index!: number
minWidth!: number
onResizeFinish!: () => ResizeFinishCallback
onWidthEvent?: () => () => void
panelId!: string
property!: PanelSide
@@ -102,6 +104,7 @@ class PanelResizerCtrl implements PanelResizerScope {
$onDestroy() {
(this.onResizeFinish as any) = undefined;
(this.onWidthEvent as any) = undefined;
(this.control as any) = undefined;
window.removeEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
document.removeEventListener(MouseEventType.Move, this.onMouseMove);
@@ -246,6 +249,9 @@ class PanelResizerCtrl implements PanelResizerScope {
this.handleLeftEvent(event);
} else {
this.handleWidthEvent(event);
if (this.onWidthEvent) {
this.onWidthEvent()();
}
}
}
@@ -387,6 +393,7 @@ export class PanelResizer extends WebDirective {
index: '=',
minWidth: '=',
onResizeFinish: '&',
onWidthEvent: '&',
panelId: '=',
property: '='
};

View File

@@ -0,0 +1,100 @@
import {
SNNote,
ContentType,
SNTag,
} from '@standardnotes/snjs';
import {
action,
makeObservable,
observable,
} from 'mobx';
import { WebApplication } from '../application';
import { AppState } from './app_state';
export class ActiveNoteState {
tags: SNTag[] = [];
tagsContainerPosition? = 0;
tagsContainerMaxWidth: number | 'auto' = 'auto';
constructor(
private application: WebApplication,
private appState: AppState,
appEventListeners: (() => void)[]
) {
makeObservable(this, {
tags: observable,
tagsContainerPosition: observable,
tagsContainerMaxWidth: observable,
setTagsContainerPosition: action,
setTagsContainerMaxWidth: action,
reloadTags: action,
});
this.tagsContainerPosition = document
.getElementById('editor-column')
?.getBoundingClientRect().left;
appEventListeners.push(
application.streamItems(
ContentType.Tag,
() => {
this.reloadTags();
}
)
);
}
get activeNote(): SNNote | undefined {
return this.appState.notes.activeEditor?.note;
}
setTagsContainerPosition(position: number): void {
this.tagsContainerPosition = position;
}
setTagsContainerMaxWidth(width: number): void {
this.tagsContainerMaxWidth = width;
}
reloadTags(): void {
const { activeNote } = this;
if (activeNote) {
this.tags = this.application.getSortedTagsForNote(activeNote);
}
}
reloadTagsContainerLayout(): void {
const editorElementId = 'editor-column';
const { clientWidth } = document.documentElement;
const editorPosition =
document.getElementById(editorElementId)?.getBoundingClientRect()
.left ?? 0;
this.appState.activeNote.setTagsContainerPosition(editorPosition);
this.appState.activeNote.setTagsContainerMaxWidth(
clientWidth - editorPosition
);
}
async addTagToActiveNote(tag: SNTag): Promise<void> {
const { activeNote } = this;
if (activeNote) {
await this.application.changeItem(tag.uuid, (mutator) => {
mutator.addItemAsRelationship(activeNote);
});
this.application.sync();
this.reloadTags();
}
}
async removeTagFromActiveNote(tag: SNTag): Promise<void> {
const { activeNote } = this;
if (activeNote) {
await this.application.changeItem(tag.uuid, (mutator) => {
mutator.removeItemAsRelationship(activeNote);
});
this.application.sync();
this.reloadTags();
}
}
}

View File

@@ -16,6 +16,7 @@ import { Bridge } from '@/services/bridge';
import { storage, StorageKey } from '@/services/localStorage';
import { AccountMenuState } from './account_menu_state';
import { ActionsMenuState } from './actions_menu_state';
import { ActiveNoteState } from './active_note_state';
import { NoAccountWarningState } from './no_account_warning_state';
import { SyncState } from './sync_state';
import { SearchOptionsState } from './search_options_state';
@@ -62,6 +63,7 @@ export class AppState {
showBetaWarning: boolean;
readonly accountMenu = new AccountMenuState();
readonly actionsMenu = new ActionsMenuState();
readonly activeNote: ActiveNoteState;
readonly noAccountWarning: NoAccountWarningState;
readonly sync = new SyncState();
readonly searchOptions: SearchOptionsState;
@@ -81,8 +83,14 @@ export class AppState {
this.$timeout = $timeout;
this.$rootScope = $rootScope;
this.application = application;
this.activeNote = new ActiveNoteState(
application,
this,
this.appEventObserverRemovers
);
this.notes = new NotesState(
this.application,
application,
this,
async () => {
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
},

View File

@@ -18,6 +18,7 @@ import {
} from 'mobx';
import { WebApplication } from '../application';
import { Editor } from '../editor';
import { AppState } from './app_state';
export class NotesState {
lastSelectedNote: SNNote | undefined;
@@ -29,10 +30,10 @@ export class NotesState {
};
contextMenuMaxHeight: number | 'auto' = 'auto';
showProtectedWarning = false;
activeNoteTags: SNTag[] = [];
constructor(
private application: WebApplication,
private appState: AppState,
private onActiveEditorChanged: () => Promise<void>,
appEventListeners: (() => void)[]
) {
@@ -41,12 +42,10 @@ export class NotesState {
contextMenuOpen: observable,
contextMenuPosition: observable,
showProtectedWarning: observable,
activeNoteTags: observable,
selectedNotesCount: computed,
trashedNotesCount: computed,
reloadActiveNoteTags: action,
setContextMenuOpen: action,
setContextMenuPosition: action,
setContextMenuMaxHeight: action,
@@ -65,24 +64,12 @@ export class NotesState {
});
})
);
appEventListeners.push(
application.streamItems(
ContentType.Tag,
() => {
this.reloadActiveNoteTags();
}
)
);
}
get activeEditor(): Editor | undefined {
return this.application.editorGroup.editors[0];
}
get activeNote(): SNNote | undefined {
return this.activeEditor?.note;
}
get selectedNotesCount(): number {
return Object.keys(this.selectedNotes).length;
}
@@ -163,13 +150,6 @@ export class NotesState {
}
}
reloadActiveNoteTags(): void {
const { activeNote } = this;
if (activeNote) {
this.activeNoteTags = this.application.getSortedTagsForNote(activeNote);
}
}
private async openEditor(noteUuid: string): Promise<void> {
if (this.activeEditor?.note?.uuid === noteUuid) {
return;
@@ -187,7 +167,7 @@ export class NotesState {
this.activeEditor.setNote(note);
}
this.reloadActiveNoteTags();
this.appState.activeNote.reloadTags();
await this.onActiveEditorChanged();
if (note.waitingForKey) {
@@ -368,36 +348,12 @@ export class NotesState {
isTagInSelectedNotes(tag: SNTag): boolean {
const selectedNotes = Object.values(this.selectedNotes);
return selectedNotes.every((note) =>
this.application
.getAppState()
this.appState
.getNoteTags(note)
.find((noteTag) => noteTag.uuid === tag.uuid)
);
}
async addTagToActiveNote(tag: SNTag): Promise<void> {
const { activeNote } = this;
if (activeNote) {
await this.application.changeItem(tag.uuid, (mutator) => {
mutator.addItemAsRelationship(activeNote);
});
this.application.sync();
this.reloadActiveNoteTags();
}
}
async removeTagFromActiveNote(tag: SNTag): Promise<void> {
const { activeNote } = this;
if (activeNote) {
await this.application.changeItem(tag.uuid, (mutator) => {
mutator.removeItemAsRelationship(activeNote);
});
this.application.sync();
this.reloadActiveNoteTags();
}
}
setShowProtectedWarning(show: boolean): void {
this.showProtectedWarning = show;
}

View File

@@ -169,5 +169,6 @@
default-width="300"
hoverable="true"
on-resize-finish="self.onPanelResize"
on-width-event="self.onPanelWidthEvent"
panel-id="'notes-column'"
)

View File

@@ -93,6 +93,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
};
this.onWindowResize = this.onWindowResize.bind(this);
this.onPanelResize = this.onPanelResize.bind(this);
this.onPanelWidthEvent = this.onPanelWidthEvent.bind(this);
window.addEventListener('resize', this.onWindowResize, true);
this.registerKeyboardShortcuts();
this.autorun(async () => {
@@ -133,6 +134,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
window.removeEventListener('resize', this.onWindowResize, true);
(this.onWindowResize as any) = undefined;
(this.onPanelResize as any) = undefined;
(this.onPanelWidthEvent as any) = undefined;
this.newNoteKeyObserver();
this.nextNoteKeyObserver();
this.previousNoteKeyObserver();
@@ -408,7 +410,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
await this.appState.createEditor(title);
await this.flushUI();
await this.reloadNotes();
await this.appState.notes.reloadActiveNoteTags();
await this.appState.activeNote.reloadTags();
}
async handleTagChange(tag: SNTag) {
@@ -643,7 +645,7 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
onPanelResize(
newWidth: number,
_: number,
newLeft: number,
__: boolean,
isCollapsed: boolean
) {
@@ -657,6 +659,10 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
);
}
onPanelWidthEvent(): void {
this.appState.activeNote.reloadTagsContainerLayout();
}
paginate() {
this.notesToDisplay += this.pageSize;
this.reloadNotes();

View File

@@ -61,6 +61,10 @@
margin-right: 0.25rem;
}
.-ml-1 {
margin-left: -0.25rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;