refactor: Move notes_view to React (#761)
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
} from 'mobx';
|
||||
import { ActionsMenuState } from './actions_menu_state';
|
||||
import { NotesState } from './notes_state';
|
||||
import { NotesViewState } from './notes_view_state';
|
||||
import { NoteTagsState } from './note_tags_state';
|
||||
import { NoAccountWarningState } from './no_account_warning_state';
|
||||
import { PreferencesState } from './preferences_state';
|
||||
@@ -86,6 +87,7 @@ export class AppState {
|
||||
readonly searchOptions: SearchOptionsState;
|
||||
readonly notes: NotesState;
|
||||
readonly tags: TagsState;
|
||||
readonly notesView: NotesViewState;
|
||||
isSessionsModalVisible = false;
|
||||
|
||||
private appEventObserverRemovers: (() => void)[] = [];
|
||||
@@ -127,6 +129,11 @@ export class AppState {
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.purchaseFlow = new PurchaseFlowState(application);
|
||||
this.notesView = new NotesViewState(
|
||||
application,
|
||||
this,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.addAppEventObserver();
|
||||
this.streamNotesAndTags();
|
||||
this.onVisibilityChange = () => {
|
||||
|
||||
541
app/assets/javascripts/ui_models/app_state/notes_view_state.ts
Normal file
541
app/assets/javascripts/ui_models/app_state/notes_view_state.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
import {
|
||||
ApplicationEvent,
|
||||
CollectionSort,
|
||||
ContentType,
|
||||
findInArray,
|
||||
NotesDisplayCriteria,
|
||||
PrefKey,
|
||||
SNNote,
|
||||
SNTag,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs';
|
||||
import {
|
||||
action,
|
||||
autorun,
|
||||
computed,
|
||||
makeObservable,
|
||||
observable,
|
||||
reaction,
|
||||
} from 'mobx';
|
||||
import { AppState, AppStateEvent } from '.';
|
||||
import { WebApplication } from '../application';
|
||||
|
||||
const MIN_NOTE_CELL_HEIGHT = 51.0;
|
||||
const DEFAULT_LIST_NUM_NOTES = 20;
|
||||
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
|
||||
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
|
||||
|
||||
export type DisplayOptions = {
|
||||
sortBy: CollectionSort;
|
||||
sortReverse: boolean;
|
||||
hidePinned: boolean;
|
||||
showArchived: boolean;
|
||||
showTrashed: boolean;
|
||||
hideProtected: boolean;
|
||||
hideTags: boolean;
|
||||
hideNotePreview: boolean;
|
||||
hideDate: boolean;
|
||||
};
|
||||
|
||||
export class NotesViewState {
|
||||
completedFullSync = false;
|
||||
noteFilterText = '';
|
||||
notes: SNNote[] = [];
|
||||
notesToDisplay = 0;
|
||||
pageSize = 0;
|
||||
panelTitle = 'All Notes';
|
||||
renderedNotes: SNNote[] = [];
|
||||
searchSubmitted = false;
|
||||
selectedNotes: Record<UuidString, SNNote> = {};
|
||||
showDisplayOptionsMenu = false;
|
||||
displayOptions = {
|
||||
sortBy: CollectionSort.CreatedAt,
|
||||
sortReverse: false,
|
||||
hidePinned: false,
|
||||
showArchived: false,
|
||||
showTrashed: false,
|
||||
hideProtected: false,
|
||||
hideTags: true,
|
||||
hideDate: false,
|
||||
hideNotePreview: false,
|
||||
};
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private appState: AppState,
|
||||
appObservers: (() => void)[]
|
||||
) {
|
||||
this.resetPagination();
|
||||
|
||||
appObservers.push(
|
||||
application.streamItems(ContentType.Note, () => {
|
||||
this.reloadNotes();
|
||||
const activeNote = this.appState.notes.activeEditor?.note;
|
||||
if (this.application.getAppState().notes.selectedNotesCount < 2) {
|
||||
if (activeNote) {
|
||||
const discarded = activeNote.deleted || activeNote.trashed;
|
||||
if (
|
||||
discarded &&
|
||||
!this.appState?.selectedTag?.isTrashTag &&
|
||||
!this.appState?.searchOptions.includeTrashed
|
||||
) {
|
||||
this.selectNextOrCreateNew();
|
||||
} else if (!this.selectedNotes[activeNote.uuid]) {
|
||||
this.selectNote(activeNote);
|
||||
}
|
||||
} else {
|
||||
this.selectFirstNote();
|
||||
}
|
||||
}
|
||||
}),
|
||||
application.streamItems([ContentType.Tag], async (items) => {
|
||||
const tags = items as SNTag[];
|
||||
/** A tag could have changed its relationships, so we need to reload the filter */
|
||||
this.reloadNotesDisplayOptions();
|
||||
this.reloadNotes();
|
||||
if (findInArray(tags, 'uuid', this.appState.selectedTag?.uuid)) {
|
||||
/** Tag title could have changed */
|
||||
this.reloadPanelTitle();
|
||||
}
|
||||
}),
|
||||
application.addEventObserver(async () => {
|
||||
this.reloadPreferences();
|
||||
}, ApplicationEvent.PreferencesChanged),
|
||||
application.addEventObserver(async () => {
|
||||
this.appState.closeAllEditors();
|
||||
this.selectFirstNote();
|
||||
this.setCompletedFullSync(false);
|
||||
}, ApplicationEvent.SignedIn),
|
||||
application.addEventObserver(async () => {
|
||||
this.reloadNotes();
|
||||
if (
|
||||
this.notes.length === 0 &&
|
||||
this.appState.selectedTag?.isAllTag &&
|
||||
this.noteFilterText === ''
|
||||
) {
|
||||
this.createPlaceholderNote();
|
||||
}
|
||||
this.setCompletedFullSync(true);
|
||||
}, ApplicationEvent.CompletedFullSync),
|
||||
autorun(() => {
|
||||
if (appState.notes.selectedNotes) {
|
||||
this.syncSelectedNotes();
|
||||
}
|
||||
}),
|
||||
reaction(
|
||||
() => [
|
||||
appState.searchOptions.includeProtectedContents,
|
||||
appState.searchOptions.includeArchived,
|
||||
appState.searchOptions.includeTrashed,
|
||||
],
|
||||
() => {
|
||||
this.reloadNotesDisplayOptions();
|
||||
this.reloadNotes();
|
||||
}
|
||||
),
|
||||
appState.addObserver(async (eventName) => {
|
||||
if (eventName === AppStateEvent.TagChanged) {
|
||||
this.handleTagChange();
|
||||
} else if (eventName === AppStateEvent.ActiveEditorChanged) {
|
||||
this.handleEditorChange();
|
||||
} else if (eventName === AppStateEvent.EditorFocused) {
|
||||
this.toggleDisplayOptionsMenu(false);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
makeObservable(this, {
|
||||
completedFullSync: observable,
|
||||
displayOptions: observable.struct,
|
||||
noteFilterText: observable,
|
||||
notes: observable,
|
||||
notesToDisplay: observable,
|
||||
panelTitle: observable,
|
||||
renderedNotes: observable,
|
||||
selectedNotes: observable,
|
||||
showDisplayOptionsMenu: observable,
|
||||
|
||||
reloadNotes: action,
|
||||
reloadPanelTitle: action,
|
||||
reloadPreferences: action,
|
||||
resetPagination: action,
|
||||
setCompletedFullSync: action,
|
||||
setNoteFilterText: action,
|
||||
syncSelectedNotes: action,
|
||||
toggleDisplayOptionsMenu: action,
|
||||
onFilterEnter: action,
|
||||
handleFilterTextChanged: action,
|
||||
|
||||
optionsSubtitle: computed,
|
||||
});
|
||||
|
||||
window.onresize = () => {
|
||||
this.resetPagination(true);
|
||||
};
|
||||
}
|
||||
|
||||
setCompletedFullSync = (completed: boolean) => {
|
||||
this.completedFullSync = completed;
|
||||
};
|
||||
|
||||
toggleDisplayOptionsMenu = (enabled: boolean) => {
|
||||
this.showDisplayOptionsMenu = enabled;
|
||||
};
|
||||
|
||||
get searchBarElement() {
|
||||
return document.getElementById(ELEMENT_ID_SEARCH_BAR);
|
||||
}
|
||||
|
||||
get isFiltering(): boolean {
|
||||
return !!this.noteFilterText && this.noteFilterText.length > 0;
|
||||
}
|
||||
|
||||
get activeEditorNote() {
|
||||
return this.appState.notes.activeEditor?.note;
|
||||
}
|
||||
|
||||
reloadPanelTitle = () => {
|
||||
let title = this.panelTitle;
|
||||
if (this.isFiltering) {
|
||||
const resultCount = this.notes.length;
|
||||
title = `${resultCount} search results`;
|
||||
} else if (this.appState.selectedTag) {
|
||||
title = `${this.appState.selectedTag.title}`;
|
||||
}
|
||||
this.panelTitle = title;
|
||||
};
|
||||
|
||||
reloadNotes = () => {
|
||||
const tag = this.appState.selectedTag;
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
const notes = this.application.getDisplayableItems(
|
||||
ContentType.Note
|
||||
) as SNNote[];
|
||||
const renderedNotes = notes.slice(0, this.notesToDisplay);
|
||||
|
||||
this.notes = notes;
|
||||
this.renderedNotes = renderedNotes;
|
||||
this.reloadPanelTitle();
|
||||
};
|
||||
|
||||
reloadNotesDisplayOptions = () => {
|
||||
const tag = this.appState.selectedTag;
|
||||
|
||||
const searchText = this.noteFilterText.toLowerCase();
|
||||
const isSearching = searchText.length;
|
||||
let includeArchived: boolean;
|
||||
let includeTrashed: boolean;
|
||||
|
||||
if (isSearching) {
|
||||
includeArchived = this.appState.searchOptions.includeArchived;
|
||||
includeTrashed = this.appState.searchOptions.includeTrashed;
|
||||
} else {
|
||||
includeArchived = this.displayOptions.showArchived ?? false;
|
||||
includeTrashed = this.displayOptions.showTrashed ?? false;
|
||||
}
|
||||
|
||||
const criteria = NotesDisplayCriteria.Create({
|
||||
sortProperty: this.displayOptions.sortBy as CollectionSort,
|
||||
sortDirection: this.displayOptions.sortReverse ? 'asc' : 'dsc',
|
||||
tags: tag ? [tag] : [],
|
||||
includeArchived,
|
||||
includeTrashed,
|
||||
includePinned: !this.displayOptions.hidePinned,
|
||||
includeProtected: !this.displayOptions.hideProtected,
|
||||
searchQuery: {
|
||||
query: searchText,
|
||||
includeProtectedNoteText:
|
||||
this.appState.searchOptions.includeProtectedContents,
|
||||
},
|
||||
});
|
||||
this.application.setNotesDisplayCriteria(criteria);
|
||||
};
|
||||
|
||||
reloadPreferences = () => {
|
||||
const freshDisplayOptions = {} as DisplayOptions;
|
||||
const currentSortBy = this.displayOptions.sortBy;
|
||||
let sortBy = this.application.getPreference(
|
||||
PrefKey.SortNotesBy,
|
||||
CollectionSort.CreatedAt
|
||||
);
|
||||
if (
|
||||
sortBy === CollectionSort.UpdatedAt ||
|
||||
(sortBy as string) === 'client_updated_at'
|
||||
) {
|
||||
/** Use UserUpdatedAt instead */
|
||||
sortBy = CollectionSort.UpdatedAt;
|
||||
}
|
||||
freshDisplayOptions.sortBy = sortBy;
|
||||
freshDisplayOptions.sortReverse = this.application.getPreference(
|
||||
PrefKey.SortNotesReverse,
|
||||
false
|
||||
);
|
||||
freshDisplayOptions.showArchived = this.application.getPreference(
|
||||
PrefKey.NotesShowArchived,
|
||||
false
|
||||
);
|
||||
freshDisplayOptions.showTrashed = this.application.getPreference(
|
||||
PrefKey.NotesShowTrashed,
|
||||
false
|
||||
) as boolean;
|
||||
freshDisplayOptions.hidePinned = this.application.getPreference(
|
||||
PrefKey.NotesHidePinned,
|
||||
false
|
||||
);
|
||||
freshDisplayOptions.hideProtected = this.application.getPreference(
|
||||
PrefKey.NotesHideProtected,
|
||||
false
|
||||
);
|
||||
freshDisplayOptions.hideNotePreview = this.application.getPreference(
|
||||
PrefKey.NotesHideNotePreview,
|
||||
false
|
||||
);
|
||||
freshDisplayOptions.hideDate = this.application.getPreference(
|
||||
PrefKey.NotesHideDate,
|
||||
false
|
||||
);
|
||||
freshDisplayOptions.hideTags = this.application.getPreference(
|
||||
PrefKey.NotesHideTags,
|
||||
true
|
||||
);
|
||||
const displayOptionsChanged =
|
||||
freshDisplayOptions.sortBy !== this.displayOptions.sortBy ||
|
||||
freshDisplayOptions.sortReverse !== this.displayOptions.sortReverse ||
|
||||
freshDisplayOptions.hidePinned !== this.displayOptions.hidePinned ||
|
||||
freshDisplayOptions.showArchived !== this.displayOptions.showArchived ||
|
||||
freshDisplayOptions.showTrashed !== this.displayOptions.showTrashed ||
|
||||
freshDisplayOptions.hideProtected !== this.displayOptions.hideProtected ||
|
||||
freshDisplayOptions.hideTags !== this.displayOptions.hideTags;
|
||||
this.displayOptions = freshDisplayOptions;
|
||||
if (displayOptionsChanged) {
|
||||
this.reloadNotesDisplayOptions();
|
||||
}
|
||||
this.reloadNotes();
|
||||
if (freshDisplayOptions.sortBy !== currentSortBy) {
|
||||
this.selectFirstNote();
|
||||
}
|
||||
};
|
||||
|
||||
createNewNote = async (focusNewNote = true) => {
|
||||
this.appState.notes.unselectNotes();
|
||||
let title = `Note ${this.notes.length + 1}`;
|
||||
if (this.isFiltering) {
|
||||
title = this.noteFilterText;
|
||||
}
|
||||
await this.appState.createEditor(title);
|
||||
this.reloadNotes();
|
||||
this.appState.noteTags.reloadTags();
|
||||
const noteTitleEditorElement = document.getElementById('note-title-editor');
|
||||
if (focusNewNote) {
|
||||
noteTitleEditorElement?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
createPlaceholderNote = () => {
|
||||
const selectedTag = this.appState.selectedTag;
|
||||
if (selectedTag && selectedTag.isSmartTag && !selectedTag.isAllTag) {
|
||||
return;
|
||||
}
|
||||
return this.createNewNote(false);
|
||||
};
|
||||
|
||||
get optionsSubtitle(): string {
|
||||
let base = '';
|
||||
if (this.displayOptions.sortBy === CollectionSort.CreatedAt) {
|
||||
base += ' Date Added';
|
||||
} else if (this.displayOptions.sortBy === CollectionSort.UpdatedAt) {
|
||||
base += ' Date Modified';
|
||||
} else if (this.displayOptions.sortBy === CollectionSort.Title) {
|
||||
base += ' Title';
|
||||
}
|
||||
if (this.displayOptions.showArchived) {
|
||||
base += ' | + Archived';
|
||||
}
|
||||
if (this.displayOptions.showTrashed) {
|
||||
base += ' | + Trashed';
|
||||
}
|
||||
if (this.displayOptions.hidePinned) {
|
||||
base += ' | – Pinned';
|
||||
}
|
||||
if (this.displayOptions.hideProtected) {
|
||||
base += ' | – Protected';
|
||||
}
|
||||
if (this.displayOptions.sortReverse) {
|
||||
base += ' | Reversed';
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
paginate = () => {
|
||||
this.notesToDisplay += this.pageSize;
|
||||
this.reloadNotes();
|
||||
if (this.searchSubmitted) {
|
||||
this.application.getDesktopService().searchText(this.noteFilterText);
|
||||
}
|
||||
};
|
||||
|
||||
resetPagination = (keepCurrentIfLarger = false) => {
|
||||
const clientHeight = document.documentElement.clientHeight;
|
||||
this.pageSize = Math.ceil(clientHeight / MIN_NOTE_CELL_HEIGHT);
|
||||
if (this.pageSize === 0) {
|
||||
this.pageSize = DEFAULT_LIST_NUM_NOTES;
|
||||
}
|
||||
if (keepCurrentIfLarger && this.notesToDisplay > this.pageSize) {
|
||||
return;
|
||||
}
|
||||
this.notesToDisplay = this.pageSize;
|
||||
};
|
||||
|
||||
getFirstNonProtectedNote = () => {
|
||||
return this.notes.find((note) => !note.protected);
|
||||
};
|
||||
|
||||
get notesListScrollContainer() {
|
||||
return document.getElementById(ELEMENT_ID_SCROLL_CONTAINER);
|
||||
}
|
||||
|
||||
selectNote = async (
|
||||
note: SNNote,
|
||||
userTriggered?: boolean,
|
||||
scrollIntoView = true
|
||||
): Promise<void> => {
|
||||
await this.appState.notes.selectNote(note.uuid, userTriggered);
|
||||
if (scrollIntoView) {
|
||||
const noteElement = document.getElementById(`note-${note.uuid}`);
|
||||
noteElement?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
selectFirstNote = () => {
|
||||
const note = this.getFirstNonProtectedNote();
|
||||
if (note) {
|
||||
this.selectNote(note, false, false);
|
||||
this.resetScrollPosition();
|
||||
}
|
||||
};
|
||||
|
||||
selectNextNote = () => {
|
||||
const displayableNotes = this.notes;
|
||||
const currentIndex = displayableNotes.findIndex((candidate) => {
|
||||
return candidate.uuid === this.activeEditorNote?.uuid;
|
||||
});
|
||||
if (currentIndex + 1 < displayableNotes.length) {
|
||||
const nextNote = displayableNotes[currentIndex + 1];
|
||||
this.selectNote(nextNote);
|
||||
const nextNoteElement = document.getElementById(`note-${nextNote.uuid}`);
|
||||
nextNoteElement?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
selectNextOrCreateNew = () => {
|
||||
const note = this.getFirstNonProtectedNote();
|
||||
if (note) {
|
||||
this.selectNote(note, false, false);
|
||||
} else {
|
||||
this.appState.closeActiveEditor();
|
||||
}
|
||||
};
|
||||
|
||||
selectPreviousNote = () => {
|
||||
const displayableNotes = this.notes;
|
||||
if (this.activeEditorNote) {
|
||||
const currentIndex = displayableNotes.indexOf(this.activeEditorNote);
|
||||
if (currentIndex - 1 >= 0) {
|
||||
const previousNote = displayableNotes[currentIndex - 1];
|
||||
this.selectNote(previousNote);
|
||||
const previousNoteElement = document.getElementById(
|
||||
`note-${previousNote.uuid}`
|
||||
);
|
||||
previousNoteElement?.focus();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setNoteFilterText = (text: string) => {
|
||||
this.noteFilterText = text;
|
||||
};
|
||||
|
||||
syncSelectedNotes = () => {
|
||||
this.selectedNotes = this.appState.notes.selectedNotes;
|
||||
};
|
||||
|
||||
handleEditorChange = async () => {
|
||||
const activeNote = this.appState.getActiveEditor()?.note;
|
||||
if (activeNote && activeNote.conflictOf) {
|
||||
this.application.changeAndSaveItem(activeNote.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
});
|
||||
}
|
||||
if (this.isFiltering) {
|
||||
this.application.getDesktopService().searchText(this.noteFilterText);
|
||||
}
|
||||
};
|
||||
|
||||
resetScrollPosition = () => {
|
||||
if (this.notesListScrollContainer) {
|
||||
this.notesListScrollContainer.scrollTop = 0;
|
||||
this.notesListScrollContainer.scrollLeft = 0;
|
||||
}
|
||||
};
|
||||
|
||||
handleTagChange = () => {
|
||||
this.resetScrollPosition();
|
||||
this.toggleDisplayOptionsMenu(false);
|
||||
this.setNoteFilterText('');
|
||||
this.application.getDesktopService().searchText();
|
||||
this.resetPagination();
|
||||
|
||||
/* Capture db load state before beginning reloadNotes,
|
||||
since this status may change during reload */
|
||||
const dbLoaded = this.application.isDatabaseLoaded();
|
||||
this.reloadNotesDisplayOptions();
|
||||
this.reloadNotes();
|
||||
|
||||
if (this.notes.length > 0) {
|
||||
this.selectFirstNote();
|
||||
} else if (dbLoaded) {
|
||||
if (
|
||||
this.activeEditorNote &&
|
||||
!this.notes.includes(this.activeEditorNote)
|
||||
) {
|
||||
this.appState.closeActiveEditor();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onFilterEnter = () => {
|
||||
/**
|
||||
* For Desktop, performing a search right away causes
|
||||
* input to lose focus. We wait until user explicity hits
|
||||
* enter before highlighting desktop search results.
|
||||
*/
|
||||
this.searchSubmitted = true;
|
||||
this.application.getDesktopService().searchText(this.noteFilterText);
|
||||
};
|
||||
|
||||
handleFilterTextChanged = () => {
|
||||
if (this.searchSubmitted) {
|
||||
this.searchSubmitted = false;
|
||||
}
|
||||
this.reloadNotesDisplayOptions();
|
||||
this.reloadNotes();
|
||||
};
|
||||
|
||||
onSearchInputBlur = () => {
|
||||
this.appState.searchOptions.refreshIncludeProtectedContents();
|
||||
};
|
||||
|
||||
clearFilterText = () => {
|
||||
this.setNoteFilterText('');
|
||||
this.onFilterEnter();
|
||||
this.handleFilterTextChanged();
|
||||
this.resetPagination();
|
||||
};
|
||||
}
|
||||
291
app/assets/javascripts/ui_models/panel_resizer.ts
Normal file
291
app/assets/javascripts/ui_models/panel_resizer.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import {
|
||||
PanelSide,
|
||||
ResizeFinishCallback,
|
||||
} from '@/directives/views/panelResizer';
|
||||
import { debounce } from '@/utils';
|
||||
import { ApplicationEvent, PrefKey } from '@standardnotes/snjs';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { WebApplication } from './application';
|
||||
|
||||
export type PanelResizerProps = {
|
||||
alwaysVisible?: boolean;
|
||||
application: WebApplication;
|
||||
collapsable: boolean;
|
||||
defaultWidth?: number;
|
||||
hoverable?: boolean;
|
||||
minWidth?: number;
|
||||
panel: HTMLDivElement;
|
||||
prefKey: PrefKey;
|
||||
resizeFinishCallback?: ResizeFinishCallback;
|
||||
side: PanelSide;
|
||||
widthEventCallback?: () => void;
|
||||
};
|
||||
|
||||
export class PanelResizerState {
|
||||
private application: WebApplication;
|
||||
alwaysVisible: boolean;
|
||||
collapsable: boolean;
|
||||
collapsed = false;
|
||||
currentMinWidth = 0;
|
||||
defaultWidth: number;
|
||||
hoverable: boolean;
|
||||
lastDownX = 0;
|
||||
lastLeft = 0;
|
||||
lastWidth = 0;
|
||||
panel: HTMLDivElement;
|
||||
pressed = false;
|
||||
prefKey: PrefKey;
|
||||
resizeFinishCallback?: ResizeFinishCallback;
|
||||
side: PanelSide;
|
||||
startLeft = 0;
|
||||
startWidth = 0;
|
||||
widthBeforeLastDblClick = 0;
|
||||
widthEventCallback?: () => void;
|
||||
|
||||
constructor({
|
||||
alwaysVisible,
|
||||
application,
|
||||
defaultWidth,
|
||||
hoverable,
|
||||
collapsable,
|
||||
minWidth,
|
||||
panel,
|
||||
prefKey,
|
||||
resizeFinishCallback,
|
||||
side,
|
||||
widthEventCallback,
|
||||
}: PanelResizerProps) {
|
||||
this.alwaysVisible = alwaysVisible ?? false;
|
||||
this.application = application;
|
||||
this.collapsable = collapsable ?? false;
|
||||
this.collapsed = false;
|
||||
this.currentMinWidth = minWidth ?? 0;
|
||||
this.defaultWidth = defaultWidth ?? 0;
|
||||
this.hoverable = hoverable ?? true;
|
||||
this.lastDownX = 0;
|
||||
this.lastLeft = this.startLeft;
|
||||
this.lastWidth = this.startWidth;
|
||||
this.panel = panel;
|
||||
this.prefKey = prefKey;
|
||||
this.pressed = false;
|
||||
this.side = side;
|
||||
this.startLeft = this.panel.offsetLeft;
|
||||
this.startWidth = this.panel.scrollWidth;
|
||||
this.widthBeforeLastDblClick = 0;
|
||||
this.widthEventCallback = widthEventCallback;
|
||||
this.resizeFinishCallback = resizeFinishCallback;
|
||||
|
||||
application.addEventObserver(async () => {
|
||||
const changedWidth = application.getPreference(prefKey) as number;
|
||||
if (changedWidth !== this.lastWidth) this.setWidth(changedWidth, true);
|
||||
}, ApplicationEvent.PreferencesChanged);
|
||||
|
||||
makeObservable(this, {
|
||||
pressed: observable,
|
||||
collapsed: observable,
|
||||
|
||||
onMouseUp: action,
|
||||
onMouseDown: action,
|
||||
onDblClick: action,
|
||||
handleWidthEvent: action,
|
||||
handleLeftEvent: action,
|
||||
setWidth: action,
|
||||
setMinWidth: action,
|
||||
reloadDefaultValues: action,
|
||||
|
||||
appFrame: computed,
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
document.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
if (this.side === PanelSide.Right) {
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
debounce(this.handleResize.bind(this), 250)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get appFrame() {
|
||||
return document.getElementById('app')?.getBoundingClientRect() as DOMRect;
|
||||
}
|
||||
|
||||
getParentRect() {
|
||||
return (this.panel.parentNode as HTMLElement).getBoundingClientRect();
|
||||
}
|
||||
|
||||
isAtMaxWidth = () => {
|
||||
return (
|
||||
Math.round(this.lastWidth + this.lastLeft) ===
|
||||
Math.round(this.getParentRect().width)
|
||||
);
|
||||
};
|
||||
|
||||
isCollapsed() {
|
||||
return this.lastWidth <= this.currentMinWidth;
|
||||
}
|
||||
|
||||
reloadDefaultValues = () => {
|
||||
this.startWidth = this.isAtMaxWidth()
|
||||
? this.getParentRect().width
|
||||
: this.panel.scrollWidth;
|
||||
this.lastWidth = this.startWidth;
|
||||
};
|
||||
|
||||
finishSettingWidth = () => {
|
||||
if (!this.collapsable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.collapsed = this.isCollapsed();
|
||||
};
|
||||
|
||||
setWidth = (width: number, finish = false) => {
|
||||
if (width < this.currentMinWidth) {
|
||||
width = this.currentMinWidth;
|
||||
}
|
||||
const parentRect = this.getParentRect();
|
||||
if (width > parentRect.width) {
|
||||
width = parentRect.width;
|
||||
}
|
||||
|
||||
const maxWidth = this.appFrame.width - this.panel.getBoundingClientRect().x;
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth;
|
||||
}
|
||||
|
||||
if (Math.round(width + this.lastLeft) === Math.round(parentRect.width)) {
|
||||
this.panel.style.width = `calc(100% - ${this.lastLeft}px)`;
|
||||
} else {
|
||||
this.panel.style.width = width + 'px';
|
||||
}
|
||||
|
||||
this.lastWidth = width;
|
||||
|
||||
if (finish) {
|
||||
this.finishSettingWidth();
|
||||
if (this.resizeFinishCallback) {
|
||||
this.resizeFinishCallback(
|
||||
this.lastWidth,
|
||||
this.lastLeft,
|
||||
this.isAtMaxWidth(),
|
||||
this.isCollapsed()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.application.setPreference(this.prefKey, this.lastWidth);
|
||||
};
|
||||
|
||||
setLeft = (left: number) => {
|
||||
this.panel.style.left = left + 'px';
|
||||
this.lastLeft = left;
|
||||
};
|
||||
|
||||
onDblClick = () => {
|
||||
const collapsed = this.isCollapsed();
|
||||
if (collapsed) {
|
||||
this.setWidth(this.widthBeforeLastDblClick || this.defaultWidth);
|
||||
} else {
|
||||
this.widthBeforeLastDblClick = this.lastWidth;
|
||||
this.setWidth(this.currentMinWidth);
|
||||
}
|
||||
this.application.setPreference(this.prefKey, this.lastWidth);
|
||||
this.finishSettingWidth();
|
||||
if (this.resizeFinishCallback) {
|
||||
this.resizeFinishCallback(
|
||||
this.lastWidth,
|
||||
this.lastLeft,
|
||||
this.isAtMaxWidth(),
|
||||
this.isCollapsed()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleWidthEvent(event?: MouseEvent) {
|
||||
if (this.widthEventCallback) {
|
||||
this.widthEventCallback();
|
||||
}
|
||||
let x;
|
||||
if (event) {
|
||||
x = event.clientX;
|
||||
} else {
|
||||
/** Coming from resize event */
|
||||
x = 0;
|
||||
this.lastDownX = 0;
|
||||
}
|
||||
const deltaX = x - this.lastDownX;
|
||||
const newWidth = this.startWidth + deltaX;
|
||||
this.setWidth(newWidth, false);
|
||||
}
|
||||
|
||||
handleLeftEvent(event: MouseEvent) {
|
||||
const panelRect = this.panel.getBoundingClientRect();
|
||||
const x = event.clientX || panelRect.x;
|
||||
let deltaX = x - this.lastDownX;
|
||||
let newLeft = this.startLeft + deltaX;
|
||||
if (newLeft < 0) {
|
||||
newLeft = 0;
|
||||
deltaX = -this.startLeft;
|
||||
}
|
||||
const parentRect = this.getParentRect();
|
||||
let newWidth = this.startWidth - deltaX;
|
||||
if (newWidth < this.currentMinWidth) {
|
||||
newWidth = this.currentMinWidth;
|
||||
}
|
||||
if (newWidth > parentRect.width) {
|
||||
newWidth = parentRect.width;
|
||||
}
|
||||
if (newLeft + newWidth > parentRect.width) {
|
||||
newLeft = parentRect.width - newWidth;
|
||||
}
|
||||
this.setLeft(newLeft);
|
||||
this.setWidth(newWidth, false);
|
||||
}
|
||||
|
||||
handleResize = () => {
|
||||
this.reloadDefaultValues();
|
||||
this.handleWidthEvent();
|
||||
this.finishSettingWidth();
|
||||
};
|
||||
|
||||
onMouseDown = (event: MouseEvent) => {
|
||||
this.pressed = true;
|
||||
this.lastDownX = event.clientX;
|
||||
this.startWidth = this.panel.scrollWidth;
|
||||
this.startLeft = this.panel.offsetLeft;
|
||||
};
|
||||
|
||||
onMouseUp = () => {
|
||||
if (!this.pressed) {
|
||||
return;
|
||||
}
|
||||
this.pressed = false;
|
||||
const isMaxWidth = this.isAtMaxWidth();
|
||||
if (this.resizeFinishCallback) {
|
||||
this.resizeFinishCallback(
|
||||
this.lastWidth,
|
||||
this.lastLeft,
|
||||
isMaxWidth,
|
||||
this.isCollapsed()
|
||||
);
|
||||
}
|
||||
this.finishSettingWidth();
|
||||
};
|
||||
|
||||
onMouseMove(event: MouseEvent) {
|
||||
if (!this.pressed) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (this.side === PanelSide.Left) {
|
||||
this.handleLeftEvent(event);
|
||||
} else {
|
||||
this.handleWidthEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
setMinWidth = (minWidth?: number) => {
|
||||
this.currentMinWidth = minWidth ?? this.currentMinWidth;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user