mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-11-01 21:41:45 +08:00
chore(client/ts): port type_widgets/relation_map
This commit is contained in:
parent
e682f01c47
commit
3047957239
@ -1,9 +1,8 @@
|
||||
import type { CommandNames } from "../components/app_context.js";
|
||||
import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
interface ContextMenuOptions<T extends CommandNames> {
|
||||
interface ContextMenuOptions<T> {
|
||||
x: number;
|
||||
y: number;
|
||||
orientation?: "left";
|
||||
@ -17,7 +16,7 @@ interface MenuSeparatorItem {
|
||||
title: "----";
|
||||
}
|
||||
|
||||
export interface MenuCommandItem<T extends CommandNames> {
|
||||
export interface MenuCommandItem<T> {
|
||||
title: string;
|
||||
command?: T;
|
||||
type?: string;
|
||||
@ -30,8 +29,8 @@ export interface MenuCommandItem<T extends CommandNames> {
|
||||
spellingSuggestion?: string;
|
||||
}
|
||||
|
||||
export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||
|
||||
class ContextMenu {
|
||||
@ -55,7 +54,7 @@ class ContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
async show<T extends CommandNames>(options: ContextMenuOptions<T>) {
|
||||
async show<T>(options: ContextMenuOptions<T>) {
|
||||
this.options = options;
|
||||
|
||||
note_tooltip.dismissAllTooltips();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import appContext from "../components/app_context.js";
|
||||
import type { ConfirmDialogOptions, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
|
||||
async function info(message: string) {
|
||||
@ -16,7 +16,7 @@ async function confirm(message: string) {
|
||||
}
|
||||
|
||||
async function confirmDeleteNoteBoxWithNote(title: string) {
|
||||
return new Promise((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
|
||||
return new Promise<ConfirmDialogResult | undefined>((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
|
||||
}
|
||||
|
||||
async function prompt(props: PromptDialogOptions) {
|
||||
|
||||
@ -252,7 +252,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
};
|
||||
}
|
||||
|
||||
function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
|
||||
function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
|
||||
const $link = $(evt.target as any).closest("a,.block-link");
|
||||
const hrefLink = $link.attr("href") || $link.attr("data-href");
|
||||
|
||||
|
||||
42
src/public/app/types.d.ts
vendored
42
src/public/app/types.d.ts
vendored
@ -7,6 +7,7 @@ import server from "./services/server.ts";
|
||||
import library_loader, { Library } from "./services/library_loader.ts";
|
||||
import type { init } from "i18next";
|
||||
import type { lint } from "./services/eslint.ts";
|
||||
import type { RelationType } from "./widgets/type_widgets/relation_map.ts";
|
||||
|
||||
interface ElectronProcess {
|
||||
type: string;
|
||||
@ -363,4 +364,45 @@ declare global {
|
||||
minimumCharacters: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
/*
|
||||
* jsPlumb
|
||||
*/
|
||||
var jsPlumb: typeof import("jsplumb").jsPlumb;
|
||||
type jsPlumbInstance = import("jsplumb").jsPlumbInstance;
|
||||
type OverlaySpec = typeof import("jsplumb").OverlaySpec;
|
||||
type ConnectionMadeEventInfo = typeof import("jsplumb").ConnectionMadeEventInfo;
|
||||
|
||||
/*
|
||||
* Panzoom
|
||||
*/
|
||||
|
||||
function panzoom(el: HTMLElement, opts: {
|
||||
maxZoom: number,
|
||||
minZoom: number,
|
||||
smoothScroll: false,
|
||||
filterKey: (e: { altKey: boolean }, dx: number, dy: number, dz: number) => void;
|
||||
});
|
||||
|
||||
interface PanZoom {
|
||||
zoomTo(x: number, y: number, scale: number);
|
||||
moveTo(x: number, y: number);
|
||||
on(event: string, callback: () => void);
|
||||
getTransform(): unknown;
|
||||
dispose(): void;
|
||||
}
|
||||
}
|
||||
|
||||
module "jsplumb" {
|
||||
interface Connection {
|
||||
canvas: HTMLCanvasElement;
|
||||
}
|
||||
|
||||
interface Overlay {
|
||||
setLabel(label: string);
|
||||
}
|
||||
|
||||
interface ConnectParams {
|
||||
type: RelationType;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,8 @@ const TPL = `
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export type ConfirmDialogCallback = (val?: false | ConfirmDialogOptions) => void;
|
||||
export type ConfirmDialogResult = false | ConfirmDialogOptions;
|
||||
export type ConfirmDialogCallback = (val?: ConfirmDialogResult) => void;
|
||||
|
||||
export interface ConfirmDialogOptions {
|
||||
confirmed: boolean;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { t } from "../services/i18n.js";
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import contextMenu from "../menus/context_menu.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const TPL = `<div class="spacer"></div>`;
|
||||
@ -26,7 +26,7 @@ export default class SpacerWidget extends BasicWidget {
|
||||
this.$widget.on("contextmenu", (e) => {
|
||||
this.$widget.tooltip("hide");
|
||||
|
||||
contextMenu.show({
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (utils.isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
||||
|
||||
@ -4,7 +4,7 @@ import BasicWidget from "./basic_widget.js";
|
||||
import contextMenu from "../menus/context_menu.js";
|
||||
import utils from "../services/utils.js";
|
||||
import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
|
||||
import appContext, { type CommandNames, type CommandListenerData, type EventData } from "../components/app_context.js";
|
||||
import froca from "../services/froca.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import type NoteContext from "../components/note_context.js";
|
||||
@ -268,7 +268,7 @@ export default class TabRowWidget extends BasicWidget {
|
||||
|
||||
const ntxId = $(e.target).closest(".note-tab").attr("data-ntx-id");
|
||||
|
||||
contextMenu.show({
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
|
||||
@ -5,13 +5,14 @@ import contextMenu from "../../menus/context_menu.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import appContext, { type EventData } from "../../components/app_context.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const uniDirectionalOverlays = [
|
||||
const uniDirectionalOverlays: OverlaySpec[] = [
|
||||
[
|
||||
"Arrow",
|
||||
{
|
||||
@ -92,7 +93,62 @@ const TPL = `
|
||||
|
||||
let containerCounter = 1;
|
||||
|
||||
interface Clipboard {
|
||||
noteId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface MapData {
|
||||
notes: {
|
||||
noteId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}[];
|
||||
transform: {
|
||||
x: number,
|
||||
y: number,
|
||||
scale: number
|
||||
}
|
||||
}
|
||||
|
||||
export type RelationType = "uniDirectional" | "biDirectional" | "inverse";
|
||||
|
||||
interface Relation {
|
||||
name: string;
|
||||
attributeId: string;
|
||||
sourceNoteId: string;
|
||||
targetNoteId: string;
|
||||
type: RelationType;
|
||||
render: boolean;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate.
|
||||
interface PostNoteResponse {
|
||||
note: {
|
||||
noteId: string;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Deduplicate.
|
||||
interface RelationMapPostResponse {
|
||||
relations: Relation[];
|
||||
inverseRelations: Record<string, string>;
|
||||
noteTitles: Record<string, string>;
|
||||
}
|
||||
|
||||
type MenuCommands = "openInNewTab" | "remove" | "editTitle";
|
||||
|
||||
export default class RelationMapTypeWidget extends TypeWidget {
|
||||
|
||||
private clipboard?: Clipboard | null;
|
||||
private jsPlumbInstance?: import("jsplumb").jsPlumbInstance | null;
|
||||
private pzInstance?: PanZoom | null;
|
||||
private mapData?: MapData | null;
|
||||
private relations?: Relation[] | null;
|
||||
|
||||
private $relationMapContainer!: JQuery<HTMLElement>;
|
||||
private $relationMapWrapper!: JQuery<HTMLElement>;
|
||||
|
||||
static getType() {
|
||||
return "relationMap";
|
||||
}
|
||||
@ -109,7 +165,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
|
||||
this.$relationMapWrapper = this.$widget.find(".relation-map-wrapper");
|
||||
this.$relationMapWrapper.on("click", (event) => {
|
||||
if (this.clipboard) {
|
||||
if (this.clipboard && this.mapData) {
|
||||
let { x, y } = this.getMousePosition(event);
|
||||
|
||||
// modifying position so that the cursor is on the top-center of the box
|
||||
@ -130,7 +186,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
|
||||
this.$relationMapContainer.attr("id", "relation-map-container-" + containerCounter++);
|
||||
this.$relationMapContainer.on("contextmenu", ".note-box", (e) => {
|
||||
contextMenu.show({
|
||||
contextMenu.show<MenuCommands>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
@ -151,14 +207,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
|
||||
this.initialized = new Promise(async (res) => {
|
||||
await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP);
|
||||
|
||||
jsPlumb.ready(res);
|
||||
// TODO: Remove once we port to webpack.
|
||||
(jsPlumb as unknown as jsPlumbInstance).ready(res);
|
||||
});
|
||||
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
async contextMenuHandler(command, originalTarget) {
|
||||
async contextMenuHandler(command: MenuCommands | undefined, originalTarget: HTMLElement) {
|
||||
const $noteBox = $(originalTarget).closest(".note-box");
|
||||
const $title = $noteBox.find(".title a");
|
||||
const noteId = this.idToNoteId($noteBox.prop("id"));
|
||||
@ -168,11 +224,11 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
} else if (command === "remove") {
|
||||
const result = await dialogService.confirmDeleteNoteBoxWithNote($title.text());
|
||||
|
||||
if (!result.confirmed) {
|
||||
if (typeof result !== "object" || !result.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jsPlumbInstance.remove(this.noteIdToId(noteId));
|
||||
this.jsPlumbInstance?.remove(this.noteIdToId(noteId));
|
||||
|
||||
if (result.isDeleteNoteChecked) {
|
||||
const taskId = utils.randomString(10);
|
||||
@ -180,9 +236,13 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`);
|
||||
}
|
||||
|
||||
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId !== noteId);
|
||||
if (this.mapData) {
|
||||
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId !== noteId);
|
||||
}
|
||||
|
||||
this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId);
|
||||
if (this.relations) {
|
||||
this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId);
|
||||
}
|
||||
|
||||
this.saveData();
|
||||
} else if (command === "editTitle") {
|
||||
@ -216,9 +276,9 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
}
|
||||
};
|
||||
|
||||
const blob = await this.note.getBlob();
|
||||
const blob = await this.note?.getBlob();
|
||||
|
||||
if (blob.content) {
|
||||
if (blob?.content) {
|
||||
try {
|
||||
this.mapData = JSON.parse(blob.content);
|
||||
} catch (e) {
|
||||
@ -227,15 +287,15 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
noteIdToId(noteId) {
|
||||
noteIdToId(noteId: string) {
|
||||
return `rel-map-note-${noteId}`;
|
||||
}
|
||||
|
||||
idToNoteId(id) {
|
||||
idToNoteId(id: string) {
|
||||
return id.substr(13);
|
||||
}
|
||||
|
||||
async doRefresh(note) {
|
||||
async doRefresh(note: FNote) {
|
||||
await this.loadMapData();
|
||||
|
||||
this.initJsPlumbInstance();
|
||||
@ -248,15 +308,19 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
clearMap() {
|
||||
// delete all endpoints and connections
|
||||
// this is done at this point (after async operations) to reduce flicker to the minimum
|
||||
this.jsPlumbInstance.deleteEveryEndpoint();
|
||||
this.jsPlumbInstance?.deleteEveryEndpoint();
|
||||
|
||||
// without this, we still end up with note boxes remaining in the canvas
|
||||
this.$relationMapContainer.empty();
|
||||
}
|
||||
|
||||
async loadNotesAndRelations() {
|
||||
if (!this.mapData || !this.jsPlumbInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteIds = this.mapData.notes.map((note) => note.noteId);
|
||||
const data = await server.post("relation-map", { noteIds, relationMapNoteId: this.noteId });
|
||||
const data = await server.post<RelationMapPostResponse>("relation-map", { noteIds, relationMapNoteId: this.noteId });
|
||||
|
||||
this.relations = [];
|
||||
|
||||
@ -282,6 +346,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId in data.noteTitles);
|
||||
|
||||
this.jsPlumbInstance.batch(async () => {
|
||||
if (!this.jsPlumbInstance || !this.mapData || !this.relations) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearMap();
|
||||
|
||||
for (const note of this.mapData.notes) {
|
||||
@ -301,6 +369,8 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
type: relation.type
|
||||
});
|
||||
|
||||
// TODO: Does this actually do anything.
|
||||
//@ts-expect-error
|
||||
connection.id = relation.attributeId;
|
||||
|
||||
if (relation.type === "inverse") {
|
||||
@ -331,14 +401,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.pzInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pzInstance.on("transform", () => {
|
||||
// gets triggered on any transform change
|
||||
this.jsPlumbInstance.setZoom(this.getZoom());
|
||||
this.jsPlumbInstance?.setZoom(this.getZoom());
|
||||
|
||||
this.saveCurrentTransform();
|
||||
});
|
||||
|
||||
if (this.mapData.transform) {
|
||||
if (this.mapData?.transform) {
|
||||
this.pzInstance.zoomTo(0, 0, this.mapData.transform.scale);
|
||||
|
||||
this.pzInstance.moveTo(this.mapData.transform.x, this.mapData.transform.y);
|
||||
@ -349,9 +423,13 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
saveCurrentTransform() {
|
||||
if (!this.pzInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTransform = this.pzInstance.getTransform();
|
||||
|
||||
if (JSON.stringify(newTransform) !== JSON.stringify(this.mapData.transform)) {
|
||||
if (this.mapData && JSON.stringify(newTransform) !== JSON.stringify(this.mapData.transform)) {
|
||||
// clone transform object
|
||||
this.mapData.transform = JSON.parse(JSON.stringify(newTransform));
|
||||
|
||||
@ -385,6 +463,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
Container: this.$relationMapContainer.attr("id")
|
||||
});
|
||||
|
||||
if (!this.jsPlumbInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jsPlumbInstance.registerConnectionType("uniDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: uniDirectionalOverlays });
|
||||
|
||||
this.jsPlumbInstance.registerConnectionType("biDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: biDirectionalOverlays });
|
||||
@ -396,10 +478,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent));
|
||||
}
|
||||
|
||||
async connectionCreatedHandler(info, originalEvent) {
|
||||
async connectionCreatedHandler(info: ConnectionMadeEventInfo, originalEvent: Event) {
|
||||
const connection = info.connection;
|
||||
|
||||
connection.bind("contextmenu", (obj, event) => {
|
||||
connection.bind("contextmenu", (obj: unknown, event: MouseEvent) => {
|
||||
if (connection.getType().includes("link")) {
|
||||
// don't create context menu if it's a link since there's nothing to do with link from relation map
|
||||
// (don't open browser menu either)
|
||||
@ -414,15 +496,17 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }],
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "remove") {
|
||||
if (!(await dialogService.confirm(t("relation_map.confirm_remove_relation")))) {
|
||||
if (!(await dialogService.confirm(t("relation_map.confirm_remove_relation"))) || !this.relations) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
|
||||
|
||||
await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`);
|
||||
if (relation) {
|
||||
await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`);
|
||||
}
|
||||
|
||||
this.jsPlumbInstance.deleteConnection(connection);
|
||||
this.jsPlumbInstance?.deleteConnection(connection);
|
||||
|
||||
this.relations = this.relations.filter((relation) => relation.attributeId !== connection.id);
|
||||
}
|
||||
@ -432,16 +516,20 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
});
|
||||
|
||||
// if there's no event, then this has been triggered programmatically
|
||||
if (!originalEvent) {
|
||||
if (!originalEvent || !this.jsPlumbInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = await dialogService.prompt({
|
||||
message: t("relation_map.specify_new_relation_name"),
|
||||
shown: ({ $answer }) => {
|
||||
if (!$answer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$answer.on("keyup", () => {
|
||||
// invalid characters are simply ignored (from user perspective they are not even entered)
|
||||
const attrName = utils.filterAttributeName($answer.val());
|
||||
const attrName = utils.filterAttributeName($answer.val() as string);
|
||||
|
||||
$answer.val(attrName);
|
||||
});
|
||||
@ -465,7 +553,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
const targetNoteId = this.idToNoteId(connection.target.id);
|
||||
const sourceNoteId = this.idToNoteId(connection.source.id);
|
||||
|
||||
const relationExists = this.relations.some((rel) => rel.targetNoteId === targetNoteId && rel.sourceNoteId === sourceNoteId && rel.name === name);
|
||||
const relationExists = this.relations?.some((rel) => rel.targetNoteId === targetNoteId && rel.sourceNoteId === sourceNoteId && rel.name === name);
|
||||
|
||||
if (relationExists) {
|
||||
await dialogService.info(t("relation_map.connection_exists", { name }));
|
||||
@ -484,11 +572,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
async createNoteBox(noteId, title, x, y) {
|
||||
async createNoteBox(noteId: string, title: string, x: number, y: number) {
|
||||
if (!this.jsPlumbInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $link = await linkService.createLink(noteId, { title });
|
||||
$link.mousedown((e) => linkService.goToLink(e));
|
||||
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $noteBox = $("<div>")
|
||||
.addClass("note-box")
|
||||
@ -507,13 +602,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
stop: (params) => {
|
||||
const noteId = this.idToNoteId(params.el.id);
|
||||
|
||||
const note = this.mapData.notes.find((note) => note.noteId === noteId);
|
||||
const note = this.mapData?.notes.find((note) => note.noteId === noteId);
|
||||
|
||||
if (!note) {
|
||||
logError(t("relation_map.note_not_found", { noteId }));
|
||||
return;
|
||||
}
|
||||
|
||||
//@ts-expect-error TODO: Check if this is still valid.
|
||||
[note.x, note.y] = params.finalPos;
|
||||
|
||||
this.saveData();
|
||||
@ -552,25 +648,29 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
throw new Error(t("relation_map.cannot_match_transform", { transform }));
|
||||
}
|
||||
|
||||
return matches[1];
|
||||
return parseFloat(matches[1]);
|
||||
}
|
||||
|
||||
async dropNoteOntoRelationMapHandler(ev) {
|
||||
async dropNoteOntoRelationMapHandler(ev: JQuery.DropEvent) {
|
||||
ev.preventDefault();
|
||||
|
||||
const notes = JSON.parse(ev.originalEvent.dataTransfer.getData("text"));
|
||||
const dragData = ev.originalEvent?.dataTransfer?.getData("text");
|
||||
if (!dragData) {
|
||||
return;
|
||||
}
|
||||
const notes = JSON.parse(dragData);
|
||||
|
||||
let { x, y } = this.getMousePosition(ev);
|
||||
|
||||
for (const note of notes) {
|
||||
const exists = this.mapData.notes.some((n) => n.noteId === note.noteId);
|
||||
const exists = this.mapData?.notes.some((n) => n.noteId === note.noteId);
|
||||
|
||||
if (exists) {
|
||||
toastService.showError(t("relation_map.note_already_in_diagram", { title: note.title }));
|
||||
continue;
|
||||
}
|
||||
|
||||
this.mapData.notes.push({ noteId: note.noteId, x, y });
|
||||
this.mapData?.notes.push({ noteId: note.noteId, x, y });
|
||||
|
||||
if (x > 1000) {
|
||||
y += 100;
|
||||
@ -585,14 +685,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.loadNotesAndRelations();
|
||||
}
|
||||
|
||||
getMousePosition(evt) {
|
||||
getMousePosition(evt: JQuery.ClickEvent | JQuery.DropEvent) {
|
||||
const rect = this.$relationMapContainer[0].getBoundingClientRect();
|
||||
|
||||
const zoom = this.getZoom();
|
||||
|
||||
return {
|
||||
x: (evt.clientX - rect.left) / zoom,
|
||||
y: (evt.clientY - rect.top) / zoom
|
||||
x: ((evt.clientX ?? 0) - rect.left) / zoom,
|
||||
y: ((evt.clientY ?? 0) - rect.top) / zoom
|
||||
};
|
||||
}
|
||||
|
||||
@ -602,18 +702,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
};
|
||||
}
|
||||
|
||||
async relationMapCreateChildNoteEvent({ ntxId }) {
|
||||
async relationMapCreateChildNoteEvent({ ntxId }: EventData<"relationMapCreateChildNote">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
||||
|
||||
if (!title.trim()) {
|
||||
if (!title?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { note } = await server.post(`notes/${this.noteId}/children?target=into`, {
|
||||
const { note } = await server.post<PostNoteResponse>(`notes/${this.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text"
|
||||
@ -624,29 +724,29 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.clipboard = { noteId: note.noteId, title };
|
||||
}
|
||||
|
||||
relationMapResetPanZoomEvent({ ntxId }) {
|
||||
relationMapResetPanZoomEvent({ ntxId }: EventData<"relationMapResetPanZoom">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reset to initial pan & zoom state
|
||||
this.pzInstance.zoomTo(0, 0, 1 / this.getZoom());
|
||||
this.pzInstance.moveTo(0, 0);
|
||||
this.pzInstance?.zoomTo(0, 0, 1 / this.getZoom());
|
||||
this.pzInstance?.moveTo(0, 0);
|
||||
}
|
||||
|
||||
relationMapResetZoomInEvent({ ntxId }) {
|
||||
relationMapResetZoomInEvent({ ntxId }: EventData<"relationMapResetZoomIn">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pzInstance.zoomTo(0, 0, 1.2);
|
||||
this.pzInstance?.zoomTo(0, 0, 1.2);
|
||||
}
|
||||
|
||||
relationMapResetZoomOutEvent({ ntxId }) {
|
||||
relationMapResetZoomOutEvent({ ntxId }: EventData<"relationMapResetZoomOut">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pzInstance.zoomTo(0, 0, 0.8);
|
||||
this.pzInstance?.zoomTo(0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user