chore(client/ts): port tab_row

This commit is contained in:
Elian Doran 2025-01-09 20:20:06 +02:00
parent 2080ce5123
commit 5111f1760d
No known key found for this signature in database
5 changed files with 110 additions and 48 deletions

View File

@ -179,6 +179,16 @@ export type CommandMappings = {
setActiveScreen: CommandData & { setActiveScreen: CommandData & {
screen: Screen; screen: Screen;
}; };
closeTab: CommandData;
closeOtherTabs: CommandData;
closeRightTabs: CommandData;
closeAllTabs: CommandData;
reopenLastTab: CommandData;
moveTabToNewWindow: CommandData;
copyTabToNewWindow: CommandData;
closeActiveTab: CommandData & {
$el: JQuery<HTMLElement>
}
}; };
type EventMappings = { type EventMappings = {
@ -233,6 +243,23 @@ type EventMappings = {
showHighlightsListWidget: { showHighlightsListWidget: {
noteId: string; noteId: string;
}; };
hoistedNoteChanged: {
ntxId: string;
};
contextsReopenedEvent: {
mainNtxId: string;
tabPosition: number;
};
noteContextReorderEvent: {
oldMainNtxId: string;
newMainNtxId: string;
};
newNoteContextCreated: {
noteContext: NoteContext;
};
noteContextRemovedEvent: {
ntxIds: string[];
}
}; };
export type EventListener<T extends EventNames> = { export type EventListener<T extends EventNames> = {

View File

@ -20,10 +20,10 @@ export type GetTextEditorCallback = () => void;
class NoteContext extends Component implements EventListener<"entitiesReloaded"> { class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
ntxId: string | null; ntxId: string | null;
hoistedNoteId: string; hoistedNoteId: string;
private mainNtxId: string | null; mainNtxId: string | null;
notePath?: string | null; notePath?: string | null;
private noteId?: string | null; noteId?: string | null;
private parentNoteId?: string | null; private parentNoteId?: string | null;
viewScope?: ViewScope; viewScope?: ViewScope;

22
src/public/app/types-lib.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
// TODO: Use real @types/ but that one generates a lot of errors.
declare module "draggabilly" {
type DraggabillyEventData = {};
interface MoveVector {
x: number;
y: number;
}
type DraggabillyCallback = (event: unknown, pointer: unknown, moveVector: MoveVector) => void;
export default class Draggabilly {
constructor(el: HTMLElement, opts: {
axis: "x" | "y";
handle: string;
containment: HTMLElement
});
element: HTMLElement;
on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback)
dragEnd();
isDragging: boolean;
positionDrag: () => void;
destroy();
}
}

View File

@ -1,12 +1,13 @@
import Draggabilly from "draggabilly"; import Draggabilly, { type DraggabillyCallback, type MoveVector } from "draggabilly";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import BasicWidget from "./basic_widget.js"; import BasicWidget from "./basic_widget.js";
import contextMenu from "../menus/context_menu.js"; import contextMenu from "../menus/context_menu.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import keyboardActionService from "../services/keyboard_actions.js"; import keyboardActionService from "../services/keyboard_actions.js";
import appContext from "../components/app_context.js"; import appContext, { type CommandData, type CommandListenerData, type EventData } from "../components/app_context.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import attributeService from "../services/attributes.js"; import attributeService from "../services/attributes.js";
import type NoteContext from "../components/note_context.js";
const TAB_CONTAINER_MIN_WIDTH = 24; const TAB_CONTAINER_MIN_WIDTH = 24;
const TAB_CONTAINER_MAX_WIDTH = 240; const TAB_CONTAINER_MAX_WIDTH = 240;
@ -229,6 +230,16 @@ const TAB_ROW_TPL = `
</div>`; </div>`;
export default class TabRowWidget extends BasicWidget { export default class TabRowWidget extends BasicWidget {
private isDragging?: boolean;
private showNoteIcons?: boolean;
private draggabillies!: Draggabilly[];
private draggabillyDragging?: Draggabilly | null;
private $style!: JQuery<HTMLElement>;
private $filler!: JQuery<HTMLElement>;
private $newTab!: JQuery<HTMLElement>;
doRender() { doRender() {
this.$widget = $(TAB_ROW_TPL); this.$widget = $(TAB_ROW_TPL);
@ -256,7 +267,7 @@ export default class TabRowWidget extends BasicWidget {
items: [ items: [
{ title: t("tab_row.close"), command: "closeTab", uiIcon: "bx bx-x" }, { title: t("tab_row.close"), command: "closeTab", uiIcon: "bx bx-x" },
{ title: t("tab_row.close_other_tabs"), command: "closeOtherTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.length !== 1 }, { title: t("tab_row.close_other_tabs"), command: "closeOtherTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.length !== 1 },
{ title: t("tab_row.close_right_tabs"), command: "closeRightTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.at(-1).ntxId !== ntxId }, { title: t("tab_row.close_right_tabs"), command: "closeRightTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts?.at(-1)?.ntxId !== ntxId },
{ title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-empty" }, { title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-empty" },
{ title: "----" }, { title: "----" },
@ -269,7 +280,9 @@ export default class TabRowWidget extends BasicWidget {
{ title: t("tab_row.copy_tab_to_new_window"), command: "copyTabToNewWindow", uiIcon: "bx bx-empty" } { title: t("tab_row.copy_tab_to_new_window"), command: "copyTabToNewWindow", uiIcon: "bx bx-empty" }
], ],
selectMenuItemHandler: ({ command }) => { selectMenuItemHandler: ({ command }) => {
this.triggerCommand(command, { ntxId }); if (command) {
this.triggerCommand(command, { ntxId });
}
} }
}); });
}); });
@ -281,12 +294,10 @@ export default class TabRowWidget extends BasicWidget {
} }
setupEvents() { setupEvents() {
const resizeListener = (_) => { new ResizeObserver((_) => {
this.cleanUpPreviouslyDraggedTabs(); this.cleanUpPreviouslyDraggedTabs();
this.layoutTabs(); this.layoutTabs();
}; }).observe(this.$widget[0]);
new ResizeObserver(resizeListener).observe(this.$widget[0]);
this.tabEls.forEach((tabEl) => this.setTabCloseEvent(tabEl)); this.tabEls.forEach((tabEl) => this.setTabCloseEvent(tabEl));
} }
@ -334,7 +345,7 @@ export default class TabRowWidget extends BasicWidget {
} }
getTabPositions() { getTabPositions() {
const tabPositions = []; const tabPositions: number[] = [];
let position = TAB_CONTAINER_LEFT_PADDING; let position = TAB_CONTAINER_LEFT_PADDING;
this.tabWidths.forEach((width) => { this.tabWidths.forEach((width) => {
@ -380,7 +391,7 @@ export default class TabRowWidget extends BasicWidget {
this.$style.html(styleHTML); this.$style.html(styleHTML);
} }
addTab(ntxId) { addTab(ntxId: string) {
const $tab = $(TAB_TPL).attr("data-ntx-id", ntxId); const $tab = $(TAB_TPL).attr("data-ntx-id", ntxId);
keyboardActionService.updateDisplayedShortcuts($tab); keyboardActionService.updateDisplayedShortcuts($tab);
@ -398,13 +409,13 @@ export default class TabRowWidget extends BasicWidget {
this.setupDraggabilly(); this.setupDraggabilly();
} }
closeActiveTabCommand({ $el }) { closeActiveTabCommand({ $el }: CommandListenerData<"closeActiveTab">) {
const ntxId = $el.closest(".note-tab").attr("data-ntx-id"); const ntxId = $el.closest(".note-tab").attr("data-ntx-id");
appContext.tabManager.removeNoteContext(ntxId); appContext.tabManager.removeNoteContext(ntxId);
} }
setTabCloseEvent($tab) { setTabCloseEvent($tab: JQuery<HTMLElement>) {
$tab.on("mousedown", (e) => { $tab.on("mousedown", (e) => {
if (e.which === 2) { if (e.which === 2) {
appContext.tabManager.removeNoteContext($tab.attr("data-ntx-id")); appContext.tabManager.removeNoteContext($tab.attr("data-ntx-id"));
@ -436,17 +447,17 @@ export default class TabRowWidget extends BasicWidget {
if (tabEl) tabEl.setAttribute("active", ""); if (tabEl) tabEl.setAttribute("active", "");
} }
newNoteContextCreatedEvent({ noteContext }) { newNoteContextCreatedEvent({ noteContext }: EventData<"newNoteContextCreated">) {
if (!noteContext.mainNtxId) { if (!noteContext.mainNtxId && noteContext.ntxId) {
this.addTab(noteContext.ntxId); this.addTab(noteContext.ntxId);
} }
} }
removeTab(ntxId) { removeTab(ntxId: string) {
const tabEl = this.getTabById(ntxId)[0]; const tabEl = this.getTabById(ntxId)[0];
if (tabEl) { if (tabEl) {
tabEl.parentNode.removeChild(tabEl); tabEl.parentNode?.removeChild(tabEl);
this.cleanUpPreviouslyDraggedTabs(); this.cleanUpPreviouslyDraggedTabs();
this.layoutTabs(); this.layoutTabs();
this.setupDraggabilly(); this.setupDraggabilly();
@ -458,20 +469,20 @@ export default class TabRowWidget extends BasicWidget {
return this.tabEls.map((el) => el.getAttribute("data-ntx-id")); return this.tabEls.map((el) => el.getAttribute("data-ntx-id"));
} }
updateTitle($tab, title) { updateTitle($tab: JQuery<HTMLElement>, title: string) {
$tab.attr("title", title); $tab.attr("title", title);
$tab.find(".note-tab-title").text(title); $tab.find(".note-tab-title").text(title);
} }
getTabById(ntxId) { getTabById(ntxId: string | null) {
return this.$widget.find(`[data-ntx-id='${ntxId}']`); return this.$widget.find(`[data-ntx-id='${ntxId}']`);
} }
getTabId($tab) { getTabId($tab: JQuery<HTMLElement>) {
return $tab.attr("data-ntx-id"); return $tab.attr("data-ntx-id");
} }
noteContextRemovedEvent({ ntxIds }) { noteContextRemovedEvent({ ntxIds }: EventData<"noteContextRemovedEvent">) {
for (const ntxId of ntxIds) { for (const ntxId of ntxIds) {
this.removeTab(ntxId); this.removeTab(ntxId);
} }
@ -485,14 +496,15 @@ export default class TabRowWidget extends BasicWidget {
const tabEls = this.tabEls; const tabEls = this.tabEls;
const { tabPositions } = this.getTabPositions(); const { tabPositions } = this.getTabPositions();
if (this.isDragging) { if (this.isDragging && this.draggabillyDragging) {
this.isDragging = false; this.isDragging = false;
this.$widget.removeClass("tab-row-widget-is-sorting"); this.$widget.removeClass("tab-row-widget-is-sorting");
// TODO: Some of these don't make sense, might need removal.
this.draggabillyDragging.element.classList.remove("note-tab-is-dragging"); this.draggabillyDragging.element.classList.remove("note-tab-is-dragging");
this.draggabillyDragging.element.style.transform = ""; this.draggabillyDragging.element.style.transform = "";
this.draggabillyDragging.dragEnd(); this.draggabillyDragging.dragEnd();
this.draggabillyDragging.isDragging = false; this.draggabillyDragging.isDragging = false;
this.draggabillyDragging.positionDrag = (_) => {}; // Prevent Draggabilly from updating tabEl.style.transform in later frames this.draggabillyDragging.positionDrag = () => {}; // Prevent Draggabilly from updating tabEl.style.transform in later frames
this.draggabillyDragging.destroy(); this.draggabillyDragging.destroy();
this.draggabillyDragging = null; this.draggabillyDragging = null;
} }
@ -509,18 +521,18 @@ export default class TabRowWidget extends BasicWidget {
this.draggabillies.push(draggabilly); this.draggabillies.push(draggabilly);
draggabilly.on("pointerDown", (_) => { draggabilly.on("pointerDown", () => {
appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id")); appContext.tabManager.activateNoteContext(tabEl.getAttribute("data-ntx-id"));
}); });
draggabilly.on("dragStart", (_) => { draggabilly.on("dragStart", () => {
this.isDragging = true; this.isDragging = true;
this.draggabillyDragging = draggabilly; this.draggabillyDragging = draggabilly;
tabEl.classList.add("note-tab-is-dragging"); tabEl.classList.add("note-tab-is-dragging");
this.$widget.addClass("tab-row-widget-is-sorting"); this.$widget.addClass("tab-row-widget-is-sorting");
}); });
draggabilly.on("dragEnd", (_) => { draggabilly.on("dragEnd", () => {
this.isDragging = false; this.isDragging = false;
const finalTranslateX = parseFloat(tabEl.style.left); const finalTranslateX = parseFloat(tabEl.style.left);
tabEl.style.transform = `translate3d(0, 0, 0)`; tabEl.style.transform = `translate3d(0, 0, 0)`;
@ -546,7 +558,7 @@ export default class TabRowWidget extends BasicWidget {
}); });
}); });
draggabilly.on("dragMove", (event, pointer, moveVector) => { draggabilly.on("dragMove", (event: unknown, pointer: unknown, moveVector: MoveVector) => {
// The current index be computed within the event since it can change during the dragMove // The current index be computed within the event since it can change during the dragMove
const tabEls = this.tabEls; const tabEls = this.tabEls;
const currentIndex = tabEls.indexOf(tabEl); const currentIndex = tabEls.indexOf(tabEl);
@ -566,13 +578,13 @@ export default class TabRowWidget extends BasicWidget {
}); });
} }
animateTabMove(tabEl, originIndex, destinationIndex) { animateTabMove(tabEl: HTMLElement, originIndex: number, destinationIndex: number) {
if (destinationIndex < originIndex) { if (destinationIndex < originIndex) {
tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex]); tabEl.parentNode?.insertBefore(tabEl, this.tabEls[destinationIndex]);
} else { } else {
const beforeEl = this.tabEls[destinationIndex + 1] || this.$newTab[0]; const beforeEl = this.tabEls[destinationIndex + 1] || this.$newTab[0];
tabEl.parentNode.insertBefore(tabEl, beforeEl); tabEl.parentNode?.insertBefore(tabEl, beforeEl);
} }
this.triggerEvent("tabReorder", { ntxIdsInOrder: this.getNtxIdsInOrder() }); this.triggerEvent("tabReorder", { ntxIdsInOrder: this.getNtxIdsInOrder() });
this.layoutTabs(); this.layoutTabs();
@ -590,7 +602,7 @@ export default class TabRowWidget extends BasicWidget {
this.$tabContainer.append(this.$filler); this.$tabContainer.append(this.$filler);
} }
closest(value, array) { closest(value: number, array: number[]) {
let closest = Infinity; let closest = Infinity;
let closestIndex = -1; let closestIndex = -1;
@ -604,17 +616,17 @@ export default class TabRowWidget extends BasicWidget {
return closestIndex; return closestIndex;
} }
noteSwitchedAndActivatedEvent({ noteContext }) { noteSwitchedAndActivatedEvent({ noteContext }: EventData<"noteSwitchedAndActivatedEvent">) {
this.activeContextChangedEvent(); this.activeContextChangedEvent();
this.updateTabById(noteContext.mainNtxId || noteContext.ntxId); this.updateTabById(noteContext.mainNtxId || noteContext.ntxId);
} }
noteSwitchedEvent({ noteContext }) { noteSwitchedEvent({ noteContext }: EventData<"noteSwitched">) {
this.updateTabById(noteContext.mainNtxId || noteContext.ntxId); this.updateTabById(noteContext.mainNtxId || noteContext.ntxId);
} }
noteContextReorderEvent({ oldMainNtxId, newMainNtxId }) { noteContextReorderEvent({ oldMainNtxId, newMainNtxId }: EventData<"noteContextReorderEvent">) {
if (!oldMainNtxId || !newMainNtxId) { if (!oldMainNtxId || !newMainNtxId) {
// no need to update tab row // no need to update tab row
return; return;
@ -625,16 +637,16 @@ export default class TabRowWidget extends BasicWidget {
this.updateTabById(newMainNtxId); this.updateTabById(newMainNtxId);
} }
contextsReopenedEvent({ mainNtxId, tabPosition }) { contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopenedEvent">) {
if (mainNtxId === undefined || tabPosition === undefined) { if (mainNtxId === undefined || tabPosition === undefined) {
// no tab reopened // no tab reopened
return; return;
} }
const tabEl = this.getTabById(mainNtxId)[0]; const tabEl = this.getTabById(mainNtxId)[0];
tabEl.parentNode.insertBefore(tabEl, this.tabEls[tabPosition]); tabEl.parentNode?.insertBefore(tabEl, this.tabEls[tabPosition]);
} }
updateTabById(ntxId) { updateTabById(ntxId: string | null) {
const $tab = this.getTabById(ntxId); const $tab = this.getTabById(ntxId);
const noteContext = appContext.tabManager.getNoteContextById(ntxId); const noteContext = appContext.tabManager.getNoteContextById(ntxId);
@ -642,11 +654,7 @@ export default class TabRowWidget extends BasicWidget {
this.updateTab($tab, noteContext); this.updateTab($tab, noteContext);
} }
/** async updateTab($tab: JQuery<HTMLElement>, noteContext: NoteContext) {
* @param {jQuery} $tab
* @param {NoteContext} noteContext
*/
async updateTab($tab, noteContext) {
if (!$tab.length) { if (!$tab.length) {
return; return;
} }
@ -681,7 +689,9 @@ export default class TabRowWidget extends BasicWidget {
} }
const title = await noteContext.getNavigationTitle(); const title = await noteContext.getNavigationTitle();
this.updateTitle($tab, title); if (title) {
this.updateTitle($tab, title);
}
$tab.addClass(note.getCssClass()); $tab.addClass(note.getCssClass());
$tab.addClass(utils.getNoteTypeClass(note.type)); $tab.addClass(utils.getNoteTypeClass(note.type));
@ -696,7 +706,7 @@ export default class TabRowWidget extends BasicWidget {
} }
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
for (const noteContext of appContext.tabManager.noteContexts) { for (const noteContext of appContext.tabManager.noteContexts) {
if (!noteContext.noteId) { if (!noteContext.noteId) {
continue; continue;
@ -706,7 +716,7 @@ export default class TabRowWidget extends BasicWidget {
loadResults.isNoteReloaded(noteContext.noteId) || loadResults.isNoteReloaded(noteContext.noteId) ||
loadResults loadResults
.getAttributeRows() .getAttributeRows()
.find((attr) => ["workspace", "workspaceIconClass", "workspaceTabBackgroundColor"].includes(attr.name) && attributeService.isAffecting(attr, noteContext.note)) .find((attr) => ["workspace", "workspaceIconClass", "workspaceTabBackgroundColor"].includes(attr.name || "") && attributeService.isAffecting(attr, noteContext.note))
) { ) {
const $tab = this.getTabById(noteContext.ntxId); const $tab = this.getTabById(noteContext.ntxId);
@ -723,7 +733,7 @@ export default class TabRowWidget extends BasicWidget {
} }
} }
hoistedNoteChangedEvent({ ntxId }) { hoistedNoteChangedEvent({ ntxId }: EventData<"hoistedNoteChanged">) {
const $tab = this.getTabById(ntxId); const $tab = this.getTabById(ntxId);
if ($tab) { if ($tab) {

View File

@ -14,5 +14,8 @@
"allowJs": true "allowJs": true
}, },
"include": ["./src/public/app/**/*"], "include": ["./src/public/app/**/*"],
"files": ["./src/public/app/types.d.ts"] "files": [
"./src/public/app/types.d.ts",
"./src/public/app/types-lib.d.ts"
]
} }