mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-11-02 22:21:41 +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 keyboardActionService from "../services/keyboard_actions.js";
|
||||||
import note_tooltip from "../services/note_tooltip.js";
|
import note_tooltip from "../services/note_tooltip.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
interface ContextMenuOptions<T extends CommandNames> {
|
interface ContextMenuOptions<T> {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
orientation?: "left";
|
orientation?: "left";
|
||||||
@ -17,7 +16,7 @@ interface MenuSeparatorItem {
|
|||||||
title: "----";
|
title: "----";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuCommandItem<T extends CommandNames> {
|
export interface MenuCommandItem<T> {
|
||||||
title: string;
|
title: string;
|
||||||
command?: T;
|
command?: T;
|
||||||
type?: string;
|
type?: string;
|
||||||
@ -30,8 +29,8 @@ export interface MenuCommandItem<T extends CommandNames> {
|
|||||||
spellingSuggestion?: string;
|
spellingSuggestion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem;
|
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||||
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||||
|
|
||||||
class ContextMenu {
|
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;
|
this.options = options;
|
||||||
|
|
||||||
note_tooltip.dismissAllTooltips();
|
note_tooltip.dismissAllTooltips();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import appContext from "../components/app_context.js";
|
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";
|
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||||
|
|
||||||
async function info(message: string) {
|
async function info(message: string) {
|
||||||
@ -16,7 +16,7 @@ async function confirm(message: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDeleteNoteBoxWithNote(title: 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) {
|
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 $link = $(evt.target as any).closest("a,.block-link");
|
||||||
const hrefLink = $link.attr("href") || $link.attr("data-href");
|
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 library_loader, { Library } from "./services/library_loader.ts";
|
||||||
import type { init } from "i18next";
|
import type { init } from "i18next";
|
||||||
import type { lint } from "./services/eslint.ts";
|
import type { lint } from "./services/eslint.ts";
|
||||||
|
import type { RelationType } from "./widgets/type_widgets/relation_map.ts";
|
||||||
|
|
||||||
interface ElectronProcess {
|
interface ElectronProcess {
|
||||||
type: string;
|
type: string;
|
||||||
@ -363,4 +364,45 @@ declare global {
|
|||||||
minimumCharacters: number;
|
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>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export type ConfirmDialogCallback = (val?: false | ConfirmDialogOptions) => void;
|
export type ConfirmDialogResult = false | ConfirmDialogOptions;
|
||||||
|
export type ConfirmDialogCallback = (val?: ConfirmDialogResult) => void;
|
||||||
|
|
||||||
export interface ConfirmDialogOptions {
|
export interface ConfirmDialogOptions {
|
||||||
confirmed: boolean;
|
confirmed: boolean;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
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 appContext from "../components/app_context.js";
|
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
const TPL = `<div class="spacer"></div>`;
|
const TPL = `<div class="spacer"></div>`;
|
||||||
@ -26,7 +26,7 @@ export default class SpacerWidget extends BasicWidget {
|
|||||||
this.$widget.on("contextmenu", (e) => {
|
this.$widget.on("contextmenu", (e) => {
|
||||||
this.$widget.tooltip("hide");
|
this.$widget.tooltip("hide");
|
||||||
|
|
||||||
contextMenu.show({
|
contextMenu.show<CommandNames>({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (utils.isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
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 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, { 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 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";
|
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");
|
const ntxId = $(e.target).closest(".note-tab").attr("data-ntx-id");
|
||||||
|
|
||||||
contextMenu.show({
|
contextMenu.show<CommandNames>({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@ -5,13 +5,14 @@ import contextMenu from "../../menus/context_menu.js";
|
|||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
||||||
import TypeWidget from "./type_widget.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 utils from "../../services/utils.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import dialogService from "../../services/dialog.js";
|
import dialogService from "../../services/dialog.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
|
||||||
const uniDirectionalOverlays = [
|
const uniDirectionalOverlays: OverlaySpec[] = [
|
||||||
[
|
[
|
||||||
"Arrow",
|
"Arrow",
|
||||||
{
|
{
|
||||||
@ -92,7 +93,62 @@ const TPL = `
|
|||||||
|
|
||||||
let containerCounter = 1;
|
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 {
|
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() {
|
static getType() {
|
||||||
return "relationMap";
|
return "relationMap";
|
||||||
}
|
}
|
||||||
@ -109,7 +165,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
|
|
||||||
this.$relationMapWrapper = this.$widget.find(".relation-map-wrapper");
|
this.$relationMapWrapper = this.$widget.find(".relation-map-wrapper");
|
||||||
this.$relationMapWrapper.on("click", (event) => {
|
this.$relationMapWrapper.on("click", (event) => {
|
||||||
if (this.clipboard) {
|
if (this.clipboard && this.mapData) {
|
||||||
let { x, y } = this.getMousePosition(event);
|
let { x, y } = this.getMousePosition(event);
|
||||||
|
|
||||||
// modifying position so that the cursor is on the top-center of the box
|
// 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.attr("id", "relation-map-container-" + containerCounter++);
|
||||||
this.$relationMapContainer.on("contextmenu", ".note-box", (e) => {
|
this.$relationMapContainer.on("contextmenu", ".note-box", (e) => {
|
||||||
contextMenu.show({
|
contextMenu.show<MenuCommands>({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: [
|
items: [
|
||||||
@ -151,14 +207,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
|
|
||||||
this.initialized = new Promise(async (res) => {
|
this.initialized = new Promise(async (res) => {
|
||||||
await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP);
|
await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP);
|
||||||
|
// TODO: Remove once we port to webpack.
|
||||||
jsPlumb.ready(res);
|
(jsPlumb as unknown as jsPlumbInstance).ready(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
super.doRender();
|
super.doRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
async contextMenuHandler(command, originalTarget) {
|
async contextMenuHandler(command: MenuCommands | undefined, originalTarget: HTMLElement) {
|
||||||
const $noteBox = $(originalTarget).closest(".note-box");
|
const $noteBox = $(originalTarget).closest(".note-box");
|
||||||
const $title = $noteBox.find(".title a");
|
const $title = $noteBox.find(".title a");
|
||||||
const noteId = this.idToNoteId($noteBox.prop("id"));
|
const noteId = this.idToNoteId($noteBox.prop("id"));
|
||||||
@ -168,11 +224,11 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
} else if (command === "remove") {
|
} else if (command === "remove") {
|
||||||
const result = await dialogService.confirmDeleteNoteBoxWithNote($title.text());
|
const result = await dialogService.confirmDeleteNoteBoxWithNote($title.text());
|
||||||
|
|
||||||
if (!result.confirmed) {
|
if (typeof result !== "object" || !result.confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.jsPlumbInstance.remove(this.noteIdToId(noteId));
|
this.jsPlumbInstance?.remove(this.noteIdToId(noteId));
|
||||||
|
|
||||||
if (result.isDeleteNoteChecked) {
|
if (result.isDeleteNoteChecked) {
|
||||||
const taskId = utils.randomString(10);
|
const taskId = utils.randomString(10);
|
||||||
@ -180,9 +236,13 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`);
|
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();
|
this.saveData();
|
||||||
} else if (command === "editTitle") {
|
} 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 {
|
try {
|
||||||
this.mapData = JSON.parse(blob.content);
|
this.mapData = JSON.parse(blob.content);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -227,15 +287,15 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
noteIdToId(noteId) {
|
noteIdToId(noteId: string) {
|
||||||
return `rel-map-note-${noteId}`;
|
return `rel-map-note-${noteId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
idToNoteId(id) {
|
idToNoteId(id: string) {
|
||||||
return id.substr(13);
|
return id.substr(13);
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh(note) {
|
async doRefresh(note: FNote) {
|
||||||
await this.loadMapData();
|
await this.loadMapData();
|
||||||
|
|
||||||
this.initJsPlumbInstance();
|
this.initJsPlumbInstance();
|
||||||
@ -248,15 +308,19 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
clearMap() {
|
clearMap() {
|
||||||
// delete all endpoints and connections
|
// delete all endpoints and connections
|
||||||
// this is done at this point (after async operations) to reduce flicker to the minimum
|
// 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
|
// without this, we still end up with note boxes remaining in the canvas
|
||||||
this.$relationMapContainer.empty();
|
this.$relationMapContainer.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadNotesAndRelations() {
|
async loadNotesAndRelations() {
|
||||||
|
if (!this.mapData || !this.jsPlumbInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const noteIds = this.mapData.notes.map((note) => note.noteId);
|
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 = [];
|
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.mapData.notes = this.mapData.notes.filter((note) => note.noteId in data.noteTitles);
|
||||||
|
|
||||||
this.jsPlumbInstance.batch(async () => {
|
this.jsPlumbInstance.batch(async () => {
|
||||||
|
if (!this.jsPlumbInstance || !this.mapData || !this.relations) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.clearMap();
|
this.clearMap();
|
||||||
|
|
||||||
for (const note of this.mapData.notes) {
|
for (const note of this.mapData.notes) {
|
||||||
@ -301,6 +369,8 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
type: relation.type
|
type: relation.type
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Does this actually do anything.
|
||||||
|
//@ts-expect-error
|
||||||
connection.id = relation.attributeId;
|
connection.id = relation.attributeId;
|
||||||
|
|
||||||
if (relation.type === "inverse") {
|
if (relation.type === "inverse") {
|
||||||
@ -331,14 +401,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!this.pzInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.pzInstance.on("transform", () => {
|
this.pzInstance.on("transform", () => {
|
||||||
// gets triggered on any transform change
|
// gets triggered on any transform change
|
||||||
this.jsPlumbInstance.setZoom(this.getZoom());
|
this.jsPlumbInstance?.setZoom(this.getZoom());
|
||||||
|
|
||||||
this.saveCurrentTransform();
|
this.saveCurrentTransform();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.mapData.transform) {
|
if (this.mapData?.transform) {
|
||||||
this.pzInstance.zoomTo(0, 0, this.mapData.transform.scale);
|
this.pzInstance.zoomTo(0, 0, this.mapData.transform.scale);
|
||||||
|
|
||||||
this.pzInstance.moveTo(this.mapData.transform.x, this.mapData.transform.y);
|
this.pzInstance.moveTo(this.mapData.transform.x, this.mapData.transform.y);
|
||||||
@ -349,9 +423,13 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveCurrentTransform() {
|
saveCurrentTransform() {
|
||||||
|
if (!this.pzInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newTransform = this.pzInstance.getTransform();
|
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
|
// clone transform object
|
||||||
this.mapData.transform = JSON.parse(JSON.stringify(newTransform));
|
this.mapData.transform = JSON.parse(JSON.stringify(newTransform));
|
||||||
|
|
||||||
@ -385,6 +463,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
Container: this.$relationMapContainer.attr("id")
|
Container: this.$relationMapContainer.attr("id")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!this.jsPlumbInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.jsPlumbInstance.registerConnectionType("uniDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: uniDirectionalOverlays });
|
this.jsPlumbInstance.registerConnectionType("uniDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: uniDirectionalOverlays });
|
||||||
|
|
||||||
this.jsPlumbInstance.registerConnectionType("biDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: biDirectionalOverlays });
|
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));
|
this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent));
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectionCreatedHandler(info, originalEvent) {
|
async connectionCreatedHandler(info: ConnectionMadeEventInfo, originalEvent: Event) {
|
||||||
const connection = info.connection;
|
const connection = info.connection;
|
||||||
|
|
||||||
connection.bind("contextmenu", (obj, event) => {
|
connection.bind("contextmenu", (obj: unknown, event: MouseEvent) => {
|
||||||
if (connection.getType().includes("link")) {
|
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 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)
|
// (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" }],
|
items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }],
|
||||||
selectMenuItemHandler: async ({ command }) => {
|
selectMenuItemHandler: async ({ command }) => {
|
||||||
if (command === "remove") {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
|
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);
|
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 there's no event, then this has been triggered programmatically
|
||||||
if (!originalEvent) {
|
if (!originalEvent || !this.jsPlumbInstance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = await dialogService.prompt({
|
let name = await dialogService.prompt({
|
||||||
message: t("relation_map.specify_new_relation_name"),
|
message: t("relation_map.specify_new_relation_name"),
|
||||||
shown: ({ $answer }) => {
|
shown: ({ $answer }) => {
|
||||||
|
if (!$answer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$answer.on("keyup", () => {
|
$answer.on("keyup", () => {
|
||||||
// invalid characters are simply ignored (from user perspective they are not even entered)
|
// 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);
|
$answer.val(attrName);
|
||||||
});
|
});
|
||||||
@ -465,7 +553,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
const targetNoteId = this.idToNoteId(connection.target.id);
|
const targetNoteId = this.idToNoteId(connection.target.id);
|
||||||
const sourceNoteId = this.idToNoteId(connection.source.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) {
|
if (relationExists) {
|
||||||
await dialogService.info(t("relation_map.connection_exists", { name }));
|
await dialogService.info(t("relation_map.connection_exists", { name }));
|
||||||
@ -484,11 +572,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.spacedUpdate.scheduleUpdate();
|
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 });
|
const $link = await linkService.createLink(noteId, { title });
|
||||||
$link.mousedown((e) => linkService.goToLink(e));
|
$link.mousedown((e) => linkService.goToLink(e));
|
||||||
|
|
||||||
const note = await froca.getNote(noteId);
|
const note = await froca.getNote(noteId);
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const $noteBox = $("<div>")
|
const $noteBox = $("<div>")
|
||||||
.addClass("note-box")
|
.addClass("note-box")
|
||||||
@ -507,13 +602,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
stop: (params) => {
|
stop: (params) => {
|
||||||
const noteId = this.idToNoteId(params.el.id);
|
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) {
|
if (!note) {
|
||||||
logError(t("relation_map.note_not_found", { noteId }));
|
logError(t("relation_map.note_not_found", { noteId }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@ts-expect-error TODO: Check if this is still valid.
|
||||||
[note.x, note.y] = params.finalPos;
|
[note.x, note.y] = params.finalPos;
|
||||||
|
|
||||||
this.saveData();
|
this.saveData();
|
||||||
@ -552,25 +648,29 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
throw new Error(t("relation_map.cannot_match_transform", { transform }));
|
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();
|
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);
|
let { x, y } = this.getMousePosition(ev);
|
||||||
|
|
||||||
for (const note of notes) {
|
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) {
|
if (exists) {
|
||||||
toastService.showError(t("relation_map.note_already_in_diagram", { title: note.title }));
|
toastService.showError(t("relation_map.note_already_in_diagram", { title: note.title }));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mapData.notes.push({ noteId: note.noteId, x, y });
|
this.mapData?.notes.push({ noteId: note.noteId, x, y });
|
||||||
|
|
||||||
if (x > 1000) {
|
if (x > 1000) {
|
||||||
y += 100;
|
y += 100;
|
||||||
@ -585,14 +685,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.loadNotesAndRelations();
|
this.loadNotesAndRelations();
|
||||||
}
|
}
|
||||||
|
|
||||||
getMousePosition(evt) {
|
getMousePosition(evt: JQuery.ClickEvent | JQuery.DropEvent) {
|
||||||
const rect = this.$relationMapContainer[0].getBoundingClientRect();
|
const rect = this.$relationMapContainer[0].getBoundingClientRect();
|
||||||
|
|
||||||
const zoom = this.getZoom();
|
const zoom = this.getZoom();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: (evt.clientX - rect.left) / zoom,
|
x: ((evt.clientX ?? 0) - rect.left) / zoom,
|
||||||
y: (evt.clientY - rect.top) / 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)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
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;
|
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,
|
title,
|
||||||
content: "",
|
content: "",
|
||||||
type: "text"
|
type: "text"
|
||||||
@ -624,29 +724,29 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.clipboard = { noteId: note.noteId, title };
|
this.clipboard = { noteId: note.noteId, title };
|
||||||
}
|
}
|
||||||
|
|
||||||
relationMapResetPanZoomEvent({ ntxId }) {
|
relationMapResetPanZoomEvent({ ntxId }: EventData<"relationMapResetPanZoom">) {
|
||||||
if (!this.isNoteContext(ntxId)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset to initial pan & zoom state
|
// reset to initial pan & zoom state
|
||||||
this.pzInstance.zoomTo(0, 0, 1 / this.getZoom());
|
this.pzInstance?.zoomTo(0, 0, 1 / this.getZoom());
|
||||||
this.pzInstance.moveTo(0, 0);
|
this.pzInstance?.moveTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
relationMapResetZoomInEvent({ ntxId }) {
|
relationMapResetZoomInEvent({ ntxId }: EventData<"relationMapResetZoomIn">) {
|
||||||
if (!this.isNoteContext(ntxId)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
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)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pzInstance.zoomTo(0, 0, 0.8);
|
this.pzInstance?.zoomTo(0, 0, 0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user