feat: add tag context menu (#890)
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
import { STRING_DELETE_TAG } from '@/strings';
|
||||
import {
|
||||
MAX_MENU_SIZE_MULTIPLIER,
|
||||
MENU_MARGIN_FROM_APP_BORDER,
|
||||
} from '@/constants';
|
||||
import {
|
||||
ComponentAction,
|
||||
ContentType,
|
||||
@@ -72,6 +76,15 @@ export class TagsState {
|
||||
selected_: AnyTag | undefined;
|
||||
previouslySelected_: AnyTag | undefined;
|
||||
editing_: SNTag | undefined;
|
||||
addingSubtagTo: SNTag | undefined;
|
||||
|
||||
contextMenuOpen = false;
|
||||
contextMenuPosition: { top?: number; left: number; bottom?: number } = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
};
|
||||
contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 };
|
||||
contextMenuMaxHeight: number | 'auto' = 'auto';
|
||||
|
||||
private readonly tagsCountsState: TagsCountsState;
|
||||
|
||||
@@ -85,6 +98,7 @@ export class TagsState {
|
||||
this.selected_ = undefined;
|
||||
this.previouslySelected_ = undefined;
|
||||
this.editing_ = undefined;
|
||||
this.addingSubtagTo = undefined;
|
||||
|
||||
this.smartTags = this.application.getSmartTags();
|
||||
this.selected_ = this.smartTags[0];
|
||||
@@ -105,6 +119,9 @@ export class TagsState {
|
||||
selectedUuid: computed,
|
||||
editingTag: computed,
|
||||
|
||||
addingSubtagTo: observable,
|
||||
setAddingSubtagTo: action,
|
||||
|
||||
assignParent: action,
|
||||
|
||||
rootTags: computed,
|
||||
@@ -114,6 +131,15 @@ export class TagsState {
|
||||
undoCreateNewTag: action,
|
||||
save: action,
|
||||
remove: action,
|
||||
|
||||
contextMenuOpen: observable,
|
||||
contextMenuPosition: observable,
|
||||
contextMenuMaxHeight: observable,
|
||||
contextMenuClickLocation: observable,
|
||||
setContextMenuOpen: action,
|
||||
setContextMenuClickLocation: action,
|
||||
setContextMenuPosition: action,
|
||||
setContextMenuMaxHeight: action,
|
||||
});
|
||||
|
||||
appEventListeners.push(
|
||||
@@ -159,6 +185,114 @@ export class TagsState {
|
||||
);
|
||||
}
|
||||
|
||||
async createSubtagAndAssignParent(parent: SNTag, title: string) {
|
||||
const hasEmptyTitle = title.length === 0;
|
||||
|
||||
if (hasEmptyTitle) {
|
||||
this.setAddingSubtagTo(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const createdTag = await this.application.createTagOrSmartTag(title);
|
||||
|
||||
const futureSiblings = this.application.getTagChildren(parent);
|
||||
|
||||
if (!isValidFutureSiblings(this.application, futureSiblings, createdTag)) {
|
||||
this.setAddingSubtagTo(undefined);
|
||||
this.remove(createdTag, false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.assignParent(createdTag.uuid, parent.uuid);
|
||||
|
||||
this.application.sync();
|
||||
|
||||
runInAction(() => {
|
||||
this.selected = createdTag as SNTag;
|
||||
});
|
||||
|
||||
this.setAddingSubtagTo(undefined);
|
||||
}
|
||||
|
||||
setAddingSubtagTo(tag: SNTag | undefined): void {
|
||||
this.addingSubtagTo = tag;
|
||||
}
|
||||
|
||||
setContextMenuOpen(open: boolean): void {
|
||||
this.contextMenuOpen = open;
|
||||
}
|
||||
|
||||
setContextMenuClickLocation(location: { x: number; y: number }): void {
|
||||
this.contextMenuClickLocation = location;
|
||||
}
|
||||
|
||||
setContextMenuPosition(position: {
|
||||
top?: number;
|
||||
left: number;
|
||||
bottom?: number;
|
||||
}): void {
|
||||
this.contextMenuPosition = position;
|
||||
}
|
||||
|
||||
setContextMenuMaxHeight(maxHeight: number | 'auto'): void {
|
||||
this.contextMenuMaxHeight = maxHeight;
|
||||
}
|
||||
|
||||
reloadContextMenuLayout(): void {
|
||||
const { clientHeight } = document.documentElement;
|
||||
const defaultFontSize = window.getComputedStyle(
|
||||
document.documentElement
|
||||
).fontSize;
|
||||
const maxContextMenuHeight =
|
||||
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER;
|
||||
const footerElementRect = document
|
||||
.getElementById('footer-bar')
|
||||
?.getBoundingClientRect();
|
||||
const footerHeightInPx = footerElementRect?.height;
|
||||
|
||||
// Open up-bottom is default behavior
|
||||
let openUpBottom = true;
|
||||
|
||||
if (footerHeightInPx) {
|
||||
const bottomSpace =
|
||||
clientHeight - footerHeightInPx - this.contextMenuClickLocation.y;
|
||||
const upSpace = this.contextMenuClickLocation.y;
|
||||
|
||||
// If not enough space to open up-bottom
|
||||
if (maxContextMenuHeight > bottomSpace) {
|
||||
// If there's enough space, open bottom-up
|
||||
if (upSpace > maxContextMenuHeight) {
|
||||
openUpBottom = false;
|
||||
this.setContextMenuMaxHeight('auto');
|
||||
// Else, reduce max height (menu will be scrollable) and open in whichever direction there's more space
|
||||
} else {
|
||||
if (upSpace > bottomSpace) {
|
||||
this.setContextMenuMaxHeight(upSpace - MENU_MARGIN_FROM_APP_BORDER);
|
||||
openUpBottom = false;
|
||||
} else {
|
||||
this.setContextMenuMaxHeight(
|
||||
bottomSpace - MENU_MARGIN_FROM_APP_BORDER
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.setContextMenuMaxHeight('auto');
|
||||
}
|
||||
}
|
||||
|
||||
if (openUpBottom) {
|
||||
this.setContextMenuPosition({
|
||||
top: this.contextMenuClickLocation.y,
|
||||
left: this.contextMenuClickLocation.x,
|
||||
});
|
||||
} else {
|
||||
this.setContextMenuPosition({
|
||||
bottom: clientHeight - this.contextMenuClickLocation.y,
|
||||
left: this.contextMenuClickLocation.x,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get allLocalRootTags(): SNTag[] {
|
||||
if (this.editing_ && this.application.isTemplateItem(this.editing_)) {
|
||||
return [this.editing_, ...this.rootTags];
|
||||
@@ -270,9 +404,9 @@ export class TagsState {
|
||||
this.selected_ = tag;
|
||||
}
|
||||
|
||||
public setExpanded(tag: SNTag, exapnded: boolean) {
|
||||
public setExpanded(tag: SNTag, expanded: boolean) {
|
||||
this.application.changeAndSaveItem<TagMutator>(tag.uuid, (mutator) => {
|
||||
mutator.expanded = exapnded;
|
||||
mutator.expanded = expanded;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,13 +446,15 @@ export class TagsState {
|
||||
this.selected = previousTag;
|
||||
}
|
||||
|
||||
public async remove(tag: SNTag) {
|
||||
if (
|
||||
await confirmDialog({
|
||||
public async remove(tag: SNTag, userTriggered: boolean) {
|
||||
let shouldDelete = !userTriggered;
|
||||
if (userTriggered) {
|
||||
shouldDelete = await confirmDialog({
|
||||
text: STRING_DELETE_TAG,
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
});
|
||||
}
|
||||
if (shouldDelete) {
|
||||
this.application.deleteItem(tag);
|
||||
this.selected = this.smartTags[0];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user