Merge branch 'develop' of https://github.com/TriliumNext/Notes into develop

This commit is contained in:
Adorian Doran
2025-03-21 20:21:46 +02:00
42 changed files with 923 additions and 378 deletions

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
cd src/public
echo Summary
cloc HEAD \
--git --md \
--include-lang=javascript,typescript
echo By file
cloc HEAD \
--git --md \
--include-lang=javascript,typescript \
--by-file | grep \.js\|

View File

@@ -80,10 +80,8 @@ try {
"node_modules/jquery/dist/", "node_modules/jquery/dist/",
"node_modules/jquery-hotkeys/", "node_modules/jquery-hotkeys/",
"node_modules/split.js/dist/", "node_modules/split.js/dist/",
"node_modules/panzoom/dist/",
"node_modules/i18next/", "node_modules/i18next/",
"node_modules/i18next-http-backend/", "node_modules/i18next-http-backend/",
"node_modules/jsplumb/dist/",
"node_modules/vanilla-js-wheel-zoom/dist/", "node_modules/vanilla-js-wheel-zoom/dist/",
"node_modules/mark.js/dist/", "node_modules/mark.js/dist/",
"node_modules/normalize.css/", "node_modules/normalize.css/",

48
package-lock.json generated
View File

@@ -25,7 +25,7 @@
"cheerio": "1.0.0", "cheerio": "1.0.0",
"chokidar": "4.0.3", "chokidar": "4.0.3",
"cls-hooked": "4.2.2", "cls-hooked": "4.2.2",
"codemirror": "5.65.18", "codemirror": "5.65.19",
"compression": "1.8.0", "compression": "1.8.0",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"csrf-csrf": "3.1.0", "csrf-csrf": "3.1.0",
@@ -43,7 +43,7 @@
"express": "4.21.2", "express": "4.21.2",
"express-rate-limit": "7.5.0", "express-rate-limit": "7.5.0",
"express-session": "1.18.1", "express-session": "1.18.1",
"force-graph": "1.49.4", "force-graph": "1.49.5",
"fs-extra": "11.3.0", "fs-extra": "11.3.0",
"helmet": "8.1.0", "helmet": "8.1.0",
"html": "1.0.0", "html": "1.0.0",
@@ -70,7 +70,7 @@
"marked": "15.0.7", "marked": "15.0.7",
"mermaid": "11.5.0", "mermaid": "11.5.0",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.2",
"normalize-strings": "1.1.1", "normalize-strings": "1.1.1",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
@@ -164,7 +164,7 @@
"bootstrap": "5.3.3", "bootstrap": "5.3.3",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"electron": "35.0.1", "electron": "35.0.3",
"eslint": "9.22.0", "eslint": "9.22.0",
"esm": "3.2.25", "esm": "3.2.25",
"globals": "16.0.0", "globals": "16.0.0",
@@ -184,13 +184,13 @@
"sass": "1.86.0", "sass": "1.86.0",
"sass-loader": "16.0.5", "sass-loader": "16.0.5",
"split.js": "1.6.5", "split.js": "1.6.5",
"supertest": "7.0.0", "supertest": "7.1.0",
"svg-pan-zoom": "3.6.2", "svg-pan-zoom": "3.6.2",
"swagger-jsdoc": "6.2.8", "swagger-jsdoc": "6.2.8",
"ts-loader": "9.5.2", "ts-loader": "9.5.2",
"tslib": "2.8.1", "tslib": "2.8.1",
"tsx": "4.19.3", "tsx": "4.19.3",
"typedoc": "0.28.0", "typedoc": "0.28.1",
"typescript": "5.8.2", "typescript": "5.8.2",
"typescript-eslint": "8.27.0", "typescript-eslint": "8.27.0",
"vitest": "3.0.9", "vitest": "3.0.9",
@@ -8561,9 +8561,9 @@
} }
}, },
"node_modules/codemirror": { "node_modules/codemirror": {
"version": "5.65.18", "version": "5.65.19",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.19.tgz",
"integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==", "integrity": "sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-convert": { "node_modules/color-convert": {
@@ -10151,9 +10151,9 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "35.0.1", "version": "35.0.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-35.0.1.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-35.0.3.tgz",
"integrity": "sha512-iQonj6lnPhqfqha2KXx6LzV1dnu6UPTCWK+b7f9Zvg828umGemi22DKbcJ3/q+Opn7iUVTWyqp9z1JQqkIi6OA==", "integrity": "sha512-kjQAYEWXSr2TyK19IZoF85dzFIBaYuX7Yp/C+34b5Y/jmI2z270CGie+RjmEGMMitsy0G8YJKftukhYMuWlK6g==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -12231,9 +12231,9 @@
} }
}, },
"node_modules/force-graph": { "node_modules/force-graph": {
"version": "1.49.4", "version": "1.49.5",
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.49.4.tgz", "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.49.5.tgz",
"integrity": "sha512-TMbbXg3n0pjI8cmgNlv1IKEGewnd9LdwKVJ4cj4XzZXqP/Q5aSjsyuxzIITtkfDJ+KDsiLql1FHu19Lqrq41uQ==", "integrity": "sha512-mCTLxsaOPfp4Jq4FND8sHTpa8aZDLNXgkwAN98IDZ8Ve3nralz0gNsmE4Nx6NFm48olJ0gzCQYYLJrrYDqifew==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tweenjs/tween.js": "18 - 25", "@tweenjs/tween.js": "18 - 25",
@@ -15868,9 +15868,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/multer": { "node_modules/multer": {
"version": "1.4.5-lts.1", "version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
@@ -19742,9 +19742,9 @@
} }
}, },
"node_modules/supertest": { "node_modules/supertest": {
"version": "7.0.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz",
"integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", "integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -20617,9 +20617,9 @@
} }
}, },
"node_modules/typedoc": { "node_modules/typedoc": {
"version": "0.28.0", "version": "0.28.1",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.0.tgz", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.1.tgz",
"integrity": "sha512-UU+xxZXrpnUhEulBYRwY2afoYFC24J2fTFovOs3llj2foGShCoKVQL6cQCfQ+sBAOdiFn2dETpZ9xhah+CL3RQ==", "integrity": "sha512-Mn2VPNMaxoe/hlBiLriG4U55oyAa3Xo+8HbtEwV7F5WEOPXqtxzGuMZhJYHaqFJpajeQ6ZDUC2c990NAtTbdgw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {

View File

@@ -85,7 +85,7 @@
"cheerio": "1.0.0", "cheerio": "1.0.0",
"chokidar": "4.0.3", "chokidar": "4.0.3",
"cls-hooked": "4.2.2", "cls-hooked": "4.2.2",
"codemirror": "5.65.18", "codemirror": "5.65.19",
"compression": "1.8.0", "compression": "1.8.0",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"csrf-csrf": "3.1.0", "csrf-csrf": "3.1.0",
@@ -103,7 +103,7 @@
"express": "4.21.2", "express": "4.21.2",
"express-rate-limit": "7.5.0", "express-rate-limit": "7.5.0",
"express-session": "1.18.1", "express-session": "1.18.1",
"force-graph": "1.49.4", "force-graph": "1.49.5",
"fs-extra": "11.3.0", "fs-extra": "11.3.0",
"helmet": "8.1.0", "helmet": "8.1.0",
"html": "1.0.0", "html": "1.0.0",
@@ -130,7 +130,7 @@
"marked": "15.0.7", "marked": "15.0.7",
"mermaid": "11.5.0", "mermaid": "11.5.0",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.2",
"normalize-strings": "1.1.1", "normalize-strings": "1.1.1",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
@@ -221,7 +221,7 @@
"bootstrap": "5.3.3", "bootstrap": "5.3.3",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"electron": "35.0.1", "electron": "35.0.3",
"eslint": "9.22.0", "eslint": "9.22.0",
"esm": "3.2.25", "esm": "3.2.25",
"globals": "16.0.0", "globals": "16.0.0",
@@ -241,13 +241,13 @@
"sass": "1.86.0", "sass": "1.86.0",
"sass-loader": "16.0.5", "sass-loader": "16.0.5",
"split.js": "1.6.5", "split.js": "1.6.5",
"supertest": "7.0.0", "supertest": "7.1.0",
"svg-pan-zoom": "3.6.2", "svg-pan-zoom": "3.6.2",
"swagger-jsdoc": "6.2.8", "swagger-jsdoc": "6.2.8",
"ts-loader": "9.5.2", "ts-loader": "9.5.2",
"tslib": "2.8.1", "tslib": "2.8.1",
"tsx": "4.19.3", "tsx": "4.19.3",
"typedoc": "0.28.0", "typedoc": "0.28.1",
"typescript": "5.8.2", "typescript": "5.8.2",
"typescript-eslint": "8.27.0", "typescript-eslint": "8.27.0",
"vitest": "3.0.9", "vitest": "3.0.9",

View File

@@ -193,6 +193,8 @@ export type CommandMappings = {
showPasswordNotSet: CommandData; showPasswordNotSet: CommandData;
showProtectedSessionPasswordDialog: CommandData; showProtectedSessionPasswordDialog: CommandData;
showUploadAttachmentsDialog: CommandData & { noteId: string }; showUploadAttachmentsDialog: CommandData & { noteId: string };
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
closeProtectedSessionPasswordDialog: CommandData; closeProtectedSessionPasswordDialog: CommandData;
copyImageReferenceToClipboard: CommandData; copyImageReferenceToClipboard: CommandData;
copyImageToClipboard: CommandData; copyImageToClipboard: CommandData;
@@ -364,6 +366,9 @@ type EventMappings = {
textTypeWidget: EditableTextTypeWidget; textTypeWidget: EditableTextTypeWidget;
text: string; text: string;
}; };
showIncludeDialog: {
textTypeWidget: EditableTextTypeWidget;
};
openBulkActionsDialog: { openBulkActionsDialog: {
selectedOrActiveNoteIds: string[]; selectedOrActiveNoteIds: string[];
}; };
@@ -399,7 +404,7 @@ type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType
*/ */
export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMappings, FilterByValueType<CommandMappings, T>>; export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMappings, FilterByValueType<CommandMappings, T>>;
class AppContext extends Component { export class AppContext extends Component {
isMainWindow: boolean; isMainWindow: boolean;
components: Component[]; components: Component[];
beforeUnloadListeners: WeakRef<BeforeUploadListener>[]; beforeUnloadListeners: WeakRef<BeforeUploadListener>[];

View File

@@ -16,7 +16,7 @@ export interface SetNoteOpts {
viewScope?: ViewScope; viewScope?: ViewScope;
} }
export type GetTextEditorCallback = () => void; export type GetTextEditorCallback = (editor: TextEditor) => void;
class NoteContext extends Component implements EventListener<"entitiesReloaded"> { class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
ntxId: string | null; ntxId: string | null;

View File

@@ -88,13 +88,18 @@ import utils from "../services/utils.js";
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
import CloseZenButton from "../widgets/close_zen_button.js"; import CloseZenButton from "../widgets/close_zen_button.js";
import type { AppContext } from "./../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js";
export default class DesktopLayout { export default class DesktopLayout {
constructor(customWidgets) {
private customWidgets: WidgetsByParent;
constructor(customWidgets: WidgetsByParent) {
this.customWidgets = customWidgets; this.customWidgets = customWidgets;
} }
getRootWidget(appContext) { getRootWidget(appContext: AppContext) {
appContext.noteTreeWidget = new NoteTreeWidget(); appContext.noteTreeWidget = new NoteTreeWidget();
const launcherPaneIsHorizontal = options.get("layoutOrientation") === "horizontal"; const launcherPaneIsHorizontal = options.get("layoutOrientation") === "horizontal";
@@ -267,7 +272,7 @@ export default class DesktopLayout {
.child(new CloseZenButton()); .child(new CloseZenButton());
} }
#buildLauncherPane(isHorizontal) { #buildLauncherPane(isHorizontal: boolean) {
let launcherPane; let launcherPane;
if (isHorizontal) { if (isHorizontal) {

View File

@@ -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();

View File

@@ -8,9 +8,10 @@ interface Token {
} }
export interface Attribute { export interface Attribute {
attributeId?: string;
type: AttributeType; type: AttributeType;
name: string; name: string;
isInheritable: boolean; isInheritable?: boolean;
value?: string; value?: string;
startIndex?: number; startIndex?: number;
endIndex?: number; endIndex?: number;

View File

@@ -50,7 +50,7 @@ async function executeStartupBundles() {
} }
} }
class WidgetsByParent { export class WidgetsByParent {
private byParent: Record<string, Widget[]>; private byParent: Record<string, Widget[]>;
constructor() { constructor() {

View File

@@ -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) {

View File

@@ -5,13 +5,15 @@ import utils from "./utils.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
interface UploadFilesOptions { type BooleanLike = boolean | "true" | "false";
safeImport?: boolean;
shrinkImages: boolean | "true" | "false"; export interface UploadFilesOptions {
textImportedAsText?: boolean; safeImport?: BooleanLike;
codeImportedAsCode?: boolean; shrinkImages: BooleanLike;
explodeArchives?: boolean; textImportedAsText?: BooleanLike;
replaceUnderscoresWithSpaces?: boolean; codeImportedAsCode?: BooleanLike;
explodeArchives?: BooleanLike;
replaceUnderscoresWithSpaces?: BooleanLike;
} }
export async function uploadFiles(entityType: string, parentNoteId: string, files: string[] | File[], options: UploadFilesOptions) { export async function uploadFiles(entityType: string, parentNoteId: string, files: string[] | File[], options: UploadFilesOptions) {

View File

@@ -42,11 +42,6 @@ const CODE_MIRROR: Library = {
css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"] css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"]
}; };
const RELATION_MAP: Library = {
js: ["node_modules/jsplumb/dist/js/jsplumb.min.js", "node_modules/panzoom/dist/panzoom.min.js"],
css: ["stylesheets/relation_map.css"]
};
const CALENDAR_WIDGET: Library = { const CALENDAR_WIDGET: Library = {
css: ["stylesheets/calendar.css"] css: ["stylesheets/calendar.css"]
}; };
@@ -183,7 +178,6 @@ export default {
loadHighlightingTheme, loadHighlightingTheme,
CKEDITOR, CKEDITOR,
CODE_MIRROR, CODE_MIRROR,
RELATION_MAP,
CALENDAR_WIDGET, CALENDAR_WIDGET,
KATEX, KATEX,
WHEEL_ZOOM, WHEEL_ZOOM,

View File

@@ -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");

View File

@@ -1,4 +1,4 @@
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "url"; type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
type Multiplicity = "single" | "multi"; type Multiplicity = "single" | "multi";
interface DefinitionObject { interface DefinitionObject {

View File

@@ -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;
@@ -46,6 +47,7 @@ interface CustomGlobals {
TRILIUM_SAFE_MODE: boolean; TRILIUM_SAFE_MODE: boolean;
platform?: typeof process.platform; platform?: typeof process.platform;
linter: typeof lint; linter: typeof lint;
hasNativeTitleBar: boolean;
} }
type RequireMethod = (moduleName: string) => any; type RequireMethod = (moduleName: string) => any;
@@ -74,7 +76,7 @@ declare global {
interface AutoCompleteArg { interface AutoCompleteArg {
displayKey: "name" | "value" | "notePathTitle"; displayKey: "name" | "value" | "notePathTitle";
cache: boolean; cache?: boolean;
source: (term: string, cb: AutoCompleteCallback) => void, source: (term: string, cb: AutoCompleteCallback) => void,
templates?: { templates?: {
suggestion: (suggestion: Suggestion) => string | undefined suggestion: (suggestion: Suggestion) => string | undefined
@@ -95,7 +97,11 @@ declare global {
className: string; className: string;
separateWordSearch: boolean; separateWordSearch: boolean;
caseSensitive: boolean; caseSensitive: boolean;
}) done?: () => void;
});
unmark(opts?: {
done: () => void;
});
} }
interface JQueryStatic { interface JQueryStatic {
@@ -171,17 +177,51 @@ declare global {
}> }>
}; };
interface CKCodeBlockLanguage {
language: string;
label: string;
}
interface CKWatchdog {
constructor(editorClass: CKEditorInstance, opts: {
minimumNonErrorTimePeriod: number;
crashNumberLimit: number,
saveInterval: number
});
on(event: string, callback: () => void);
state: string;
crashes: unknown[];
editor: TextEditor;
setCreator(callback: (elementOrData, editorConfig) => void);
create(el: HTMLElement, opts: {
placeholder: string,
mention: MentionConfig,
codeBlock: {
languages: CKCodeBlockLanguage[]
},
math: {
engine: string,
outputType: string,
lazyLoad: () => Promise<void>,
forceOutputType: boolean,
enablePreview: boolean
},
mermaid: {
lazyLoad: () => Promise<void>,
config: MermaidConfig
}
});
destroy();
}
var CKEditor: { var CKEditor: {
BalloonEditor: { BalloonEditor: CKEditorInstance;
create(el: HTMLElement, config: { DecoupledEditor: CKEditorInstance;
removePlugins?: string[]; EditorWatchdog: typeof CKWatchdog;
toolbar: { };
items: any[];
}, var CKEditorInspector: {
placeholder: string; attach(editor: TextEditor);
mention: MentionConfig
})
}
}; };
var CodeMirror: { var CodeMirror: {
@@ -221,9 +261,24 @@ declare global {
setOption(name: string, value: string); setOption(name: string, value: string);
refresh(); refresh();
focus(); focus();
getCursor(): { line: number, col: number, ch: number };
setCursor(line: number, col: number); setCursor(line: number, col: number);
getSelection(): string;
lineCount(): number; lineCount(): number;
on(event: string, callback: () => void); on(event: string, callback: () => void);
operation(callback: () => void);
scrollIntoView(pos: number);
doc: {
getValue(): string;
markText(
from: { line: number, ch: number } | number,
to: { line: number, ch: number } | number,
opts: {
className: string
});
setSelection(from: number, to: number);
replaceRange(text: string, from: number, to: number);
}
} }
var katex: { var katex: {
@@ -232,11 +287,22 @@ declare global {
}); });
} }
type TextEditorElement = {}; interface Range {
toJSON(): object;
getItems(): TextNode[];
}
interface Writer { interface Writer {
setAttribute(name: string, value: string, el: TextEditorElement); setAttribute(name: string, value: string, el: CKNode);
createPositionAt(el: TextEditorElement, opt?: "end"); createPositionAt(el: CKNode, opt?: "end" | number);
setSelection(pos: number); setSelection(pos: number, pos?: number);
insertText(text: string, opts: Record<string, unknown> | undefined | TextPosition, position?: TextPosition);
addMarker(name: string, opts: {
range: Range;
usingOperation: boolean;
});
removeMarker(name: string);
createRange(start: number, end: number): Range;
createElement(type: string, opts: Record<string, string | null | undefined>);
} }
interface TextNode { interface TextNode {
previousSibling?: TextNode; previousSibling?: TextNode;
@@ -252,29 +318,98 @@ declare global {
interface TextPosition { interface TextPosition {
textNode: TextNode; textNode: TextNode;
offset: number; offset: number;
compareWith(pos: TextPosition): string;
} }
interface TextRange {
}
interface Marker {
name: string;
}
interface CKNode {
name: string;
childCount: number;
isEmpty: boolean;
toJSON(): object;
is(type: string, name?: string);
getAttribute(name: string): string;
getChild(index: number): CKNode;
data: string;
startOffset: number;
root: {
document: {
model: {
createRangeIn(el: CKNode): TextRange;
markers: {
getMarkersIntersectingRange(range: TextRange): Marker[];
}
}
}
};
}
interface CKEvent {
stop(): void;
}
interface PluginEventData {
title: string;
message: {
message: string;
};
}
interface TextEditor { interface TextEditor {
create(el: HTMLElement, config: {
removePlugins?: string[];
toolbar: {
items: any[];
},
placeholder: string;
mention: MentionConfig
});
enableReadOnlyMode(reason: string);
model: { model: {
document: { document: {
on(event: string, cb: () => void); on(event: string, cb: () => void);
getRoot(): TextEditorElement; getRoot(): CKNode;
registerPostFixer(callback: (writer: Writer) => boolean);
selection: { selection: {
getFirstPosition(): undefined | TextPosition; getFirstPosition(): undefined | TextPosition;
getLastPosition(): undefined | TextPosition;
getSelectedElement(): CKNode;
hasAttribute(attribute: string): boolean;
getAttribute(attribute: string): string;
getFirstRange(): Range;
isCollapsed: boolean;
};
differ: {
getChanges(): {
type: string;
name: string;
position: {
nodeAfter: CKNode;
parent: CKNode;
toJSON(): Object;
}
}[];
} }
}, },
insertContent(modelFragment: any, selection?: any);
change(cb: (writer: Writer) => void) change(cb: (writer: Writer) => void)
}, },
editing: { editing: {
view: { view: {
document: { document: {
on(event: string, cb: (event: { on(event: string, cb: (event: CKEvent, data: {
stop();
}, data: {
preventDefault(); preventDefault();
}) => void, opts?: { }) => void, opts?: {
priority: "high" priority: "high"
}); });
getRoot(): TextEditorElement getRoot(): CKNode
}, },
domRoots: { domRoots: {
values: () => { values: () => {
@@ -283,16 +418,55 @@ declare global {
} }
}; };
} }
change(cb: (writer: Writer) => void) change(cb: (writer: Writer) => void);
scrollToTheSelection(): void;
focus(): void;
} }
}, },
plugins: {
get(command: string)
},
data: {
processor: {
toView(html: string);
};
toModel(viewFeragment: any);
},
conversion: {
for(filter: string): {
markerToHighlight(data: {
model: string;
view: (data: {
markerName: string;
}) => void;
})
}
}
getData(): string; getData(): string;
setData(data: string): void; setData(data: string): void;
getSelectedHtml(): string; getSelectedHtml(): string;
removeSelection(): void; removeSelection(): void;
execute<T>(action: string, ...args: unknown[]): T;
focus(): void;
sourceElement: HTMLElement; sourceElement: HTMLElement;
} }
interface EditingState {
highlightedResult: string;
results: unknown[];
}
interface CKFindResult {
results: {
get(number): {
marker: {
getStart(): TextPosition;
getRange(): number;
};
}
} & [];
}
interface MentionItem { interface MentionItem {
action?: string; action?: string;
noteTitle?: string; noteTitle?: string;
@@ -313,4 +487,23 @@ declare global {
minimumCharacters: number; minimumCharacters: number;
}[]; }[];
} }
/*
* 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;
}
} }

View File

@@ -288,7 +288,7 @@ const ATTR_HELP: Record<string, Record<string, string>> = {
}; };
interface AttributeDetailOpts { interface AttributeDetailOpts {
allAttributes: Attribute[]; allAttributes?: Attribute[];
attribute: Attribute; attribute: Attribute;
isOwned: boolean; isOwned: boolean;
x: number; x: number;
@@ -338,7 +338,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
private relatedNotesSpacedUpdate!: SpacedUpdate; private relatedNotesSpacedUpdate!: SpacedUpdate;
private attribute!: Attribute; private attribute!: Attribute;
private allAttributes!: Attribute[]; private allAttributes?: Attribute[];
private attrType!: ReturnType<AttributeDetailWidget["getAttrType"]>; private attrType!: ReturnType<AttributeDetailWidget["getAttrType"]>;
async refresh() { async refresh() {
@@ -434,7 +434,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes }); this.triggerCommand("updateAttributeList", { attributes: this.allAttributes ?? [] });
this.updateRelatedNotes(); this.updateRelatedNotes();
}); });
@@ -454,7 +454,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.$deleteButton = this.$widget.find(".attr-delete-button"); this.$deleteButton = this.$widget.find(".attr-delete-button");
this.$deleteButton.on("click", async () => { this.$deleteButton.on("click", async () => {
await this.triggerCommand("updateAttributeList", { await this.triggerCommand("updateAttributeList", {
attributes: this.allAttributes.filter((attr) => attr !== this.attribute) attributes: (this.allAttributes || []).filter((attr) => attr !== this.attribute)
}); });
await this.triggerCommand("saveAttributes"); await this.triggerCommand("saveAttributes");
@@ -714,7 +714,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.attribute.value = String(this.$inputValue.val()); this.attribute.value = String(this.$inputValue.val());
} }
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes }); this.triggerCommand("updateAttributeList", { attributes: this.allAttributes ?? [] });
} }
buildDefinitionValue() { buildDefinitionValue() {

View File

@@ -5,6 +5,7 @@ import type CommandButtonWidget from "../buttons/command_button.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js"; import type { NoteType } from "../../entities/fnote.js";
import type { EventData, EventNames } from "../../components/app_context.js"; import type { EventData, EventNames } from "../../components/app_context.js";
import type NoteActionsWidget from "../buttons/note_actions.js";
const TPL = ` const TPL = `
<div class="ribbon-container"> <div class="ribbon-container">
@@ -116,13 +117,15 @@ const TPL = `
<div class="ribbon-body-container"></div> <div class="ribbon-body-container"></div>
</div>`; </div>`;
type ButtonWidget = (CommandButtonWidget | NoteActionsWidget);
export default class RibbonContainer extends NoteContextAwareWidget { export default class RibbonContainer extends NoteContextAwareWidget {
private lastActiveComponentId?: string | null; private lastActiveComponentId?: string | null;
private lastNoteType?: NoteType; private lastNoteType?: NoteType;
private ribbonWidgets: NoteContextAwareWidget[]; private ribbonWidgets: NoteContextAwareWidget[];
private buttonWidgets: CommandButtonWidget[]; private buttonWidgets: ButtonWidget[];
private $tabContainer!: JQuery<HTMLElement>; private $tabContainer!: JQuery<HTMLElement>;
private $buttonContainer!: JQuery<HTMLElement>; private $buttonContainer!: JQuery<HTMLElement>;
private $bodyContainer!: JQuery<HTMLElement>; private $bodyContainer!: JQuery<HTMLElement>;
@@ -148,7 +151,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
return this; return this;
} }
button(widget: CommandButtonWidget) { button(widget: ButtonWidget) {
super.child(widget); super.child(widget);
this.buttonWidgets.push(widget); this.buttonWidgets.push(widget);

View File

@@ -80,13 +80,13 @@ export default class AddLinkDialog extends BasicWidget {
if (this.$autoComplete.getSelectedNotePath()) { if (this.$autoComplete.getSelectedNotePath()) {
this.$widget.modal("hide"); this.$widget.modal("hide");
const linkTitle = this.getLinkType() === "reference-link" ? null : this.$linkTitle.val(); const linkTitle = this.getLinkType() === "reference-link" ? null : this.$linkTitle.val() as string;
this.textTypeWidget?.addLink(this.$autoComplete.getSelectedNotePath()!, linkTitle); this.textTypeWidget?.addLink(this.$autoComplete.getSelectedNotePath()!, linkTitle);
} else if (this.$autoComplete.getSelectedExternalLink()) { } else if (this.$autoComplete.getSelectedExternalLink()) {
this.$widget.modal("hide"); this.$widget.modal("hide");
this.textTypeWidget?.addLink(this.$autoComplete.getSelectedExternalLink()!, this.$linkTitle.val(), true); this.textTypeWidget?.addLink(this.$autoComplete.getSelectedExternalLink()!, this.$linkTitle.val() as string, true);
} else { } else {
logError("No link to add."); logError("No link to add.");
} }

View File

@@ -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;

View File

@@ -1,10 +1,11 @@
import utils, { escapeQuotes } from "../../services/utils.js"; import utils, { escapeQuotes } from "../../services/utils.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import importService from "../../services/import.js"; import importService, { type UploadFilesOptions } from "../../services/import.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import { Modal, Tooltip } from "bootstrap"; import { Modal, Tooltip } from "bootstrap";
import type { EventData } from "../../components/app_context.js";
const TPL = ` const TPL = `
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@@ -79,6 +80,20 @@ const TPL = `
</div>`; </div>`;
export default class ImportDialog extends BasicWidget { export default class ImportDialog extends BasicWidget {
private parentNoteId: string | null;
private $form!: JQuery<HTMLElement>;
private $noteTitle!: JQuery<HTMLElement>;
private $fileUploadInput!: JQuery<HTMLInputElement>;
private $importButton!: JQuery<HTMLElement>;
private $safeImportCheckbox!: JQuery<HTMLElement>;
private $shrinkImagesCheckbox!: JQuery<HTMLElement>;
private $textImportedAsTextCheckbox!: JQuery<HTMLElement>;
private $codeImportedAsCodeCheckbox!: JQuery<HTMLElement>;
private $explodeArchivesCheckbox!: JQuery<HTMLElement>;
private $replaceUnderscoresWithSpacesCheckbox!: JQuery<HTMLElement>;
constructor() { constructor() {
super(); super();
@@ -87,7 +102,7 @@ export default class ImportDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
Modal.getOrCreateInstance(this.$widget); Modal.getOrCreateInstance(this.$widget[0]);
this.$form = this.$widget.find(".import-form"); this.$form = this.$widget.find(".import-form");
this.$noteTitle = this.$widget.find(".import-note-title"); this.$noteTitle = this.$widget.find(".import-note-title");
@@ -104,7 +119,9 @@ export default class ImportDialog extends BasicWidget {
// disabling so that import is not triggered again. // disabling so that import is not triggered again.
this.$importButton.attr("disabled", "disabled"); this.$importButton.attr("disabled", "disabled");
this.importIntoNote(this.parentNoteId); if (this.parentNoteId) {
this.importIntoNote(this.parentNoteId);
}
return false; return false;
}); });
@@ -124,7 +141,7 @@ export default class ImportDialog extends BasicWidget {
}); });
} }
async showImportDialogEvent({ noteId }) { async showImportDialogEvent({ noteId }: EventData<"showImportDialog">) {
this.parentNoteId = noteId; this.parentNoteId = noteId;
this.$fileUploadInput.val("").trigger("change"); // to trigger Import button disabling listener below this.$fileUploadInput.val("").trigger("change"); // to trigger Import button disabling listener below
@@ -141,12 +158,12 @@ export default class ImportDialog extends BasicWidget {
utils.openDialog(this.$widget); utils.openDialog(this.$widget);
} }
async importIntoNote(parentNoteId) { async importIntoNote(parentNoteId: string) {
const files = Array.from(this.$fileUploadInput[0].files); // shallow copy since we're resetting the upload button below const files = Array.from(this.$fileUploadInput[0].files ?? []); // shallow copy since we're resetting the upload button below
const boolToString = ($el) => ($el.is(":checked") ? "true" : "false"); const boolToString = ($el: JQuery<HTMLElement>) => ($el.is(":checked") ? "true" : "false");
const options = { const options: UploadFilesOptions = {
safeImport: boolToString(this.$safeImportCheckbox), safeImport: boolToString(this.$safeImportCheckbox),
shrinkImages: boolToString(this.$shrinkImagesCheckbox), shrinkImages: boolToString(this.$shrinkImagesCheckbox),
textImportedAsText: boolToString(this.$textImportedAsTextCheckbox), textImportedAsText: boolToString(this.$textImportedAsTextCheckbox),

View File

@@ -5,6 +5,8 @@ import utils from "../../services/utils.js";
import froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import type { EventData } from "../../components/app_context.js";
import type EditableTextTypeWidget from "../type_widgets/editable_text.js";
const TPL = ` const TPL = `
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
@@ -53,9 +55,15 @@ const TPL = `
</div>`; </div>`;
export default class IncludeNoteDialog extends BasicWidget { export default class IncludeNoteDialog extends BasicWidget {
private modal!: bootstrap.Modal;
private $form!: JQuery<HTMLElement>;
private $autoComplete!: JQuery<HTMLElement>;
private textTypeWidget?: EditableTextTypeWidget;
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget); this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$form = this.$widget.find(".include-note-form"); this.$form = this.$widget.find(".include-note-form");
this.$autoComplete = this.$widget.find(".include-note-autocomplete"); this.$autoComplete = this.$widget.find(".include-note-autocomplete");
this.$form.on("submit", () => { this.$form.on("submit", () => {
@@ -72,7 +80,7 @@ export default class IncludeNoteDialog extends BasicWidget {
}); });
} }
async showIncludeNoteDialogEvent({ textTypeWidget }) { async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) {
this.textTypeWidget = textTypeWidget; this.textTypeWidget = textTypeWidget;
await this.refresh(); await this.refresh();
utils.openDialog(this.$widget); utils.openDialog(this.$widget);
@@ -80,7 +88,7 @@ export default class IncludeNoteDialog extends BasicWidget {
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
} }
async refresh(widget) { async refresh() {
this.$autoComplete.val(""); this.$autoComplete.val("");
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, { noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
hideGoToSelectedNoteButton: true, hideGoToSelectedNoteButton: true,
@@ -89,17 +97,20 @@ export default class IncludeNoteDialog extends BasicWidget {
noteAutocompleteService.showRecentNotes(this.$autoComplete); noteAutocompleteService.showRecentNotes(this.$autoComplete);
} }
async includeNote(notePath) { async includeNote(notePath: string) {
const noteId = treeService.getNoteIdFromUrl(notePath); const noteId = treeService.getNoteIdFromUrl(notePath);
if (!noteId) {
return;
}
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
const boxSize = $("input[name='include-note-box-size']:checked").val(); const boxSize = $("input[name='include-note-box-size']:checked").val() as string;
if (["image", "canvas", "mermaid"].includes(note.type)) { if (["image", "canvas", "mermaid"].includes(note?.type ?? "")) {
// there's no benefit to use insert note functionlity for images, // there's no benefit to use insert note functionlity for images,
// so we'll just add an IMG tag // so we'll just add an IMG tag
this.textTypeWidget.addImage(noteId); this.textTypeWidget?.addImage(noteId);
} else { } else {
this.textTypeWidget.addIncludeNote(noteId, boxSize); this.textTypeWidget?.addIncludeNote(noteId, boxSize);
} }
} }
} }

View File

@@ -28,6 +28,13 @@ const TPL = `<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120; const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
export default class JumpToNoteDialog extends BasicWidget { export default class JumpToNoteDialog extends BasicWidget {
private lastOpenedTs: number;
private modal!: bootstrap.Modal;
private $autoComplete!: JQuery<HTMLElement>;
private $results!: JQuery<HTMLElement>;
private $showInFullTextButton!: JQuery<HTMLElement>;
constructor() { constructor() {
super(); super();
@@ -36,7 +43,7 @@ export default class JumpToNoteDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget); this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$autoComplete = this.$widget.find(".jump-to-note-autocomplete"); this.$autoComplete = this.$widget.find(".jump-to-note-autocomplete");
this.$results = this.$widget.find(".jump-to-note-results"); this.$results = this.$widget.find(".jump-to-note-results");
@@ -54,17 +61,17 @@ export default class JumpToNoteDialog extends BasicWidget {
function reposition() { function reposition() {
const offset = 100; const offset = 100;
const modalHeight = window.visualViewport.height - offset; const modalHeight = (window.visualViewport?.height ?? 0) - offset;
const safeAreaInsetBottom = window.visualViewport.height - window.innerHeight; const safeAreaInsetBottom = (window.visualViewport?.height ?? 0) - window.innerHeight;
el.style.height = `${modalHeight}px`; el.style.height = `${modalHeight}px`;
el.style.bottom = `${window.visualViewport.height - modalHeight - safeAreaInsetBottom - offset}px`; el.style.bottom = `${(window.visualViewport?.height ?? 0) - modalHeight - safeAreaInsetBottom - offset}px`;
} }
this.$autoComplete.on("focus", () => { this.$autoComplete.on("focus", () => {
reposition(); reposition();
}); });
window.visualViewport.addEventListener("resize", () => { window.visualViewport?.addEventListener("resize", () => {
reposition(); reposition();
}); });
@@ -84,7 +91,7 @@ export default class JumpToNoteDialog extends BasicWidget {
allowCreatingNotes: true, allowCreatingNotes: true,
hideGoToSelectedNoteButton: true, hideGoToSelectedNoteButton: true,
allowJumpToSearchNotes: true, allowJumpToSearchNotes: true,
container: this.$results container: this.$results[0]
}) })
// clear any event listener added in previous invocation of this function // clear any event listener added in previous invocation of this function
.off("autocomplete:noteselected") .off("autocomplete:noteselected")
@@ -93,7 +100,7 @@ export default class JumpToNoteDialog extends BasicWidget {
return false; return false;
} }
appContext.tabManager.getActiveContext().setNote(suggestion.notePath); appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath);
}); });
// if you open the Jump To dialog soon after using it previously, it can often mean that you // if you open the Jump To dialog soon after using it previously, it can often mean that you
@@ -112,15 +119,14 @@ export default class JumpToNoteDialog extends BasicWidget {
} }
} }
showInFullText(e) { showInFullText(e: JQuery.TriggeredEvent) {
// stop from propagating upwards (dangerous, especially with ctrl+enter executable javascript notes) // stop from propagating upwards (dangerous, especially with ctrl+enter executable javascript notes)
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const searchString = this.$autoComplete.val(); const searchString = String(this.$autoComplete.val());
this.triggerCommand("searchNotes", { searchString }); this.triggerCommand("searchNotes", { searchString });
this.modal.hide(); this.modal.hide();
} }
} }

View File

@@ -27,7 +27,17 @@ const TPL = `
</div> </div>
</div>`; </div>`;
interface RenderMarkdownResponse {
htmlContent: string;
}
export default class MarkdownImportDialog extends BasicWidget { export default class MarkdownImportDialog extends BasicWidget {
private lastOpenedTs: number;
private modal!: bootstrap.Modal;
private $importTextarea!: JQuery<HTMLElement>;
private $importButton!: JQuery<HTMLElement>;
constructor() { constructor() {
super(); super();
@@ -36,7 +46,7 @@ export default class MarkdownImportDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget); this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$importTextarea = this.$widget.find(".markdown-import-textarea"); this.$importTextarea = this.$widget.find(".markdown-import-textarea");
this.$importButton = this.$widget.find(".markdown-import-button"); this.$importButton = this.$widget.find(".markdown-import-button");
@@ -47,10 +57,13 @@ export default class MarkdownImportDialog extends BasicWidget {
shortcutService.bindElShortcut(this.$widget, "ctrl+return", () => this.sendForm()); shortcutService.bindElShortcut(this.$widget, "ctrl+return", () => this.sendForm());
} }
async convertMarkdownToHtml(markdownContent) { async convertMarkdownToHtml(markdownContent: string) {
const { htmlContent } = await server.post("other/render-markdown", { markdownContent }); const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
const textEditor = await appContext.tabManager.getActiveContext().getTextEditor(); const textEditor = await appContext.tabManager.getActiveContext()?.getTextEditor();
if (!textEditor) {
return;
}
const viewFragment = textEditor.data.processor.toView(htmlContent); const viewFragment = textEditor.data.processor.toView(htmlContent);
const modelFragment = textEditor.data.toModel(viewFragment); const modelFragment = textEditor.data.toModel(viewFragment);
@@ -80,7 +93,7 @@ export default class MarkdownImportDialog extends BasicWidget {
} }
async sendForm() { async sendForm() {
const text = this.$importTextarea.val(); const text = String(this.$importTextarea.val());
this.modal.hide(); this.modal.hide();

View File

@@ -6,6 +6,7 @@ import branchService from "../../services/branches.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
const TPL = ` const TPL = `
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
@@ -39,6 +40,12 @@ const TPL = `
</div>`; </div>`;
export default class MoveToDialog extends BasicWidget { export default class MoveToDialog extends BasicWidget {
private movedBranchIds: string[] | null;
private $form!: JQuery<HTMLElement>;
private $noteAutoComplete!: JQuery<HTMLElement>;
private $noteList!: JQuery<HTMLElement>;
constructor() { constructor() {
super(); super();
@@ -58,7 +65,13 @@ export default class MoveToDialog extends BasicWidget {
this.$widget.modal("hide"); this.$widget.modal("hide");
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath); const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
froca.getBranchId(parentNoteId, noteId).then((branchId) => this.moveNotesTo(branchId)); if (parentNoteId) {
froca.getBranchId(parentNoteId, noteId).then((branchId) => {
if (branchId) {
this.moveNotesTo(branchId);
}
});
}
} else { } else {
logError(t("move_to.error_no_path")); logError(t("move_to.error_no_path"));
} }
@@ -67,7 +80,7 @@ export default class MoveToDialog extends BasicWidget {
}); });
} }
async moveBranchIdsToEvent({ branchIds }) { async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) {
this.movedBranchIds = branchIds; this.movedBranchIds = branchIds;
utils.openDialog(this.$widget); utils.openDialog(this.$widget);
@@ -78,7 +91,14 @@ export default class MoveToDialog extends BasicWidget {
for (const branchId of this.movedBranchIds) { for (const branchId of this.movedBranchIds) {
const branch = froca.getBranch(branchId); const branch = froca.getBranch(branchId);
if (!branch) {
continue;
}
const note = await froca.getNote(branch.noteId); const note = await froca.getNote(branch.noteId);
if (!note) {
continue;
}
this.$noteList.append($("<li>").text(note.title)); this.$noteList.append($("<li>").text(note.title));
} }
@@ -87,12 +107,14 @@ export default class MoveToDialog extends BasicWidget {
noteAutocompleteService.showRecentNotes(this.$noteAutoComplete); noteAutocompleteService.showRecentNotes(this.$noteAutoComplete);
} }
async moveNotesTo(parentBranchId) { async moveNotesTo(parentBranchId: string) {
await branchService.moveToParentNote(this.movedBranchIds, parentBranchId); if (this.movedBranchIds) {
await branchService.moveToParentNote(this.movedBranchIds, parentBranchId);
}
const parentBranch = froca.getBranch(parentBranchId); const parentBranch = froca.getBranch(parentBranchId);
const parentNote = await parentBranch.getNote(); const parentNote = await parentBranch?.getNote();
toastService.showMessage(`${t("move_to.move_success_message")} ${parentNote.title}`); toastService.showMessage(`${t("move_to.move_success_message")} ${parentNote?.title}`);
} }
} }

View File

@@ -9,10 +9,16 @@ import attributeService from "../services/attributes.js";
import FindInText from "./find_in_text.js"; import FindInText from "./find_in_text.js";
import FindInCode from "./find_in_code.js"; import FindInCode from "./find_in_code.js";
import FindInHtml from "./find_in_html.js"; import FindInHtml from "./find_in_html.js";
import type { EventData } from "../components/app_context.js";
const findWidgetDelayMillis = 200; const findWidgetDelayMillis = 200;
const waitForEnter = findWidgetDelayMillis < 0; const waitForEnter = findWidgetDelayMillis < 0;
export interface FindResult {
totalFound: number;
currentFound: number;
}
// tabIndex=-1 on the checkbox labels is necessary, so when clicking on the label, // tabIndex=-1 on the checkbox labels is necessary, so when clicking on the label,
// the focusout handler is called with relatedTarget equal to the label instead // the focusout handler is called with relatedTarget equal to the label instead
// of undefined. It's -1 instead of > 0, so they don't tabstop // of undefined. It's -1 instead of > 0, so they don't tabstop
@@ -92,6 +98,28 @@ const TPL = `
</div>`; </div>`;
export default class FindWidget extends NoteContextAwareWidget { export default class FindWidget extends NoteContextAwareWidget {
private searchTerm: string | null;
private textHandler: FindInText;
private codeHandler: FindInCode;
private htmlHandler: FindInHtml;
private handler?: FindInText | FindInCode | FindInHtml;
private timeoutId?: number | null;
private $input!: JQuery<HTMLElement>;
private $currentFound!: JQuery<HTMLElement>;
private $totalFound!: JQuery<HTMLElement>;
private $caseSensitiveCheckbox!: JQuery<HTMLElement>;
private $matchWordsCheckbox!: JQuery<HTMLElement>;
private $previousButton!: JQuery<HTMLElement>;
private $nextButton!: JQuery<HTMLElement>;
private $closeButton!: JQuery<HTMLElement>;
private $replaceWidgetBox!: JQuery<HTMLElement>;
private $replaceTextInput!: JQuery<HTMLElement>;
private $replaceAllButton!: JQuery<HTMLElement>;
private $replaceButton!: JQuery<HTMLElement>;
constructor() { constructor() {
super(); super();
@@ -160,24 +188,24 @@ export default class FindWidget extends NoteContextAwareWidget {
return; return;
} }
if (!["text", "code", "render"].includes(this.note.type)) { if (!["text", "code", "render"].includes(this.note?.type ?? "")) {
return; return;
} }
this.handler = await this.getHandler(); this.handler = await this.getHandler();
const isReadOnly = await this.noteContext.isReadOnly(); const isReadOnly = await this.noteContext?.isReadOnly();
let selectedText = ""; let selectedText = "";
if (this.note.type === "code" && !isReadOnly) { if (this.note?.type === "code" && !isReadOnly && this.noteContext) {
const codeEditor = await this.noteContext.getCodeEditor(); const codeEditor = await this.noteContext.getCodeEditor();
selectedText = codeEditor.getSelection(); selectedText = codeEditor.getSelection();
} else { } else {
selectedText = window.getSelection().toString() || ""; selectedText = window.getSelection()?.toString() || "";
} }
this.$widget.show(); this.$widget.show();
this.$input.focus(); this.$input.focus();
if (["text", "code"].includes(this.note.type) && !isReadOnly) { if (["text", "code"].includes(this.note?.type ?? "") && !isReadOnly) {
this.$replaceWidgetBox.show(); this.$replaceWidgetBox.show();
} else { } else {
this.$replaceWidgetBox.hide(); this.$replaceWidgetBox.hide();
@@ -208,16 +236,16 @@ export default class FindWidget extends NoteContextAwareWidget {
} }
async getHandler() { async getHandler() {
if (this.note.type === "render") { if (this.note?.type === "render") {
return this.htmlHandler; return this.htmlHandler;
} }
const readOnly = await this.noteContext.isReadOnly(); const readOnly = await this.noteContext?.isReadOnly();
if (readOnly) { if (readOnly) {
return this.htmlHandler; return this.htmlHandler;
} else { } else {
return this.note.type === "code" ? this.codeHandler : this.textHandler; return this.note?.type === "code" ? this.codeHandler : this.textHandler;
} }
} }
@@ -228,7 +256,7 @@ export default class FindWidget extends NoteContextAwareWidget {
if (!waitForEnter) { if (!waitForEnter) {
// Clear the previous timeout if any, it's ok if timeoutId is // Clear the previous timeout if any, it's ok if timeoutId is
// null or undefined // null or undefined
clearTimeout(this.timeoutId); clearTimeout(this.timeoutId as unknown as NodeJS.Timeout); // TODO: Fix once client is separated from Node.js types.
// Defer the search a few millis so the search doesn't start // Defer the search a few millis so the search doesn't start
// immediately, as this can cause search word typing lag with // immediately, as this can cause search word typing lag with
@@ -237,15 +265,14 @@ export default class FindWidget extends NoteContextAwareWidget {
this.timeoutId = setTimeout(async () => { this.timeoutId = setTimeout(async () => {
this.timeoutId = null; this.timeoutId = null;
await this.performFind(); await this.performFind();
}, findWidgetDelayMillis); }, findWidgetDelayMillis) as unknown as number; // TODO: Fix once client is separated from Node.js types.
} }
} }
/** /**
* @param direction +1 for next, -1 for previous * @param direction +1 for next, -1 for previous
* @returns {Promise<void>}
*/ */
async findNext(direction) { async findNext(direction: 1 | -1) {
if (this.$totalFound.text() == "?") { if (this.$totalFound.text() == "?") {
await this.performFind(); await this.performFind();
return; return;
@@ -268,16 +295,19 @@ export default class FindWidget extends NoteContextAwareWidget {
this.$currentFound.text(nextFound + 1); this.$currentFound.text(nextFound + 1);
await this.handler.findNext(direction, currentFound, nextFound); await this.handler?.findNext(direction, currentFound, nextFound);
} }
} }
/** Perform the find and highlight the find results. */ /** Perform the find and highlight the find results. */
async performFind() { async performFind() {
const searchTerm = this.$input.val(); const searchTerm = String(this.$input.val());
const matchCase = this.$caseSensitiveCheckbox.prop("checked"); const matchCase = this.$caseSensitiveCheckbox.prop("checked");
const wholeWord = this.$matchWordsCheckbox.prop("checked"); const wholeWord = this.$matchWordsCheckbox.prop("checked");
if (!this.handler) {
return;
}
const { totalFound, currentFound } = await this.handler.performFind(searchTerm, matchCase, wholeWord); const { totalFound, currentFound } = await this.handler.performFind(searchTerm, matchCase, wholeWord);
this.$totalFound.text(totalFound); this.$totalFound.text(totalFound);
@@ -297,28 +327,34 @@ export default class FindWidget extends NoteContextAwareWidget {
this.searchTerm = null; this.searchTerm = null;
await this.handler.findBoxClosed(totalFound, currentFound); await this.handler?.findBoxClosed(totalFound, currentFound);
} }
} }
async replace() { async replace() {
const replaceText = this.$replaceTextInput.val(); const replaceText = String(this.$replaceTextInput.val());
await this.handler.replace(replaceText); if (this.handler && "replace" in this.handler) {
await this.handler.replace(replaceText);
}
} }
async replaceAll() { async replaceAll() {
const replaceText = this.$replaceTextInput.val(); const replaceText = String(this.$replaceTextInput.val());
await this.handler.replaceAll(replaceText); if (this.handler && "replace" in this.handler) {
await this.handler.replaceAll(replaceText);
}
} }
isEnabled() { isEnabled() {
return super.isEnabled() && ["text", "code", "render"].includes(this.note.type); return super.isEnabled() && ["text", "code", "render"].includes(this.note?.type ?? "");
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteContentReloaded(this.noteId)) { if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
this.$totalFound.text("?"); this.$totalFound.text("?");
} else if (loadResults.getAttributeRows().find((attr) => attr.type === "label" && attr.name.toLowerCase().includes("readonly") && attributeService.isAffecting(attr, this.note))) { } else if (loadResults.getAttributeRows().find((attr) => attr.type === "label"
&& (attr.name?.toLowerCase() ?? "").includes("readonly")
&& attributeService.isAffecting(attr, this.note))) {
this.closeSearch(); this.closeSearch();
} }
} }

View File

@@ -2,35 +2,54 @@
// uses for highlighting matches, use the same one on CodeMirror // uses for highlighting matches, use the same one on CodeMirror
// for consistency // for consistency
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import type FindWidget from "./find.js";
const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; const FIND_RESULT_CSS_CLASSNAME = "ck-find-result";
// TODO: Deduplicate.
interface Match {
className: string;
clear(): void;
find(): {
from: number;
to: number;
};
}
export default class FindInCode { export default class FindInCode {
constructor(parent) {
/** @property {FindWidget} */ private parent: FindWidget;
private findResult?: Match[] | null;
constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
} }
async getCodeEditor() { async getCodeEditor() {
return this.parent.noteContext.getCodeEditor(); return this.parent.noteContext?.getCodeEditor();
} }
async performFind(searchTerm, matchCase, wholeWord) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
let findResult = null; let findResult: Match[] | null = null;
let totalFound = 0; let totalFound = 0;
let currentFound = -1; let currentFound = -1;
// See https://codemirror.net/addon/search/searchcursor.js for tips // See https://codemirror.net/addon/search/searchcursor.js for tips
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
if (!codeEditor) {
return { totalFound: 0, currentFound: 0 };
}
const doc = codeEditor.doc; const doc = codeEditor.doc;
const text = doc.getValue(); const text = doc.getValue();
// Clear all markers // Clear all markers
if (this.findResult != null) { if (this.findResult) {
codeEditor.operation(() => { codeEditor.operation(() => {
for (let i = 0; i < this.findResult.length; ++i) { const findResult = this.findResult as Match[];
const marker = this.findResult[i]; for (let i = 0; i < findResult.length; ++i) {
const marker = findResult[i];
marker.clear(); marker.clear();
} }
}); });
@@ -49,7 +68,7 @@ export default class FindInCode {
const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i")); const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i"));
let curLine = 0; let curLine = 0;
let curChar = 0; let curChar = 0;
let curMatch = null; let curMatch: RegExpExecArray | null = null;
findResult = []; findResult = [];
// All those markText take several seconds on e.g., this ~500-line // All those markText take several seconds on e.g., this ~500-line
// script, batch them inside an operation, so they become // script, batch them inside an operation, so they become
@@ -73,7 +92,7 @@ export default class FindInCode {
let toPos = { line: curLine, ch: curChar + curMatch[0].length }; let toPos = { line: curLine, ch: curChar + curMatch[0].length };
// or css = "color: #f3" // or css = "color: #f3"
let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME }); let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME });
findResult.push(marker); findResult?.push(marker);
// Set the first match beyond the cursor as the current match // Set the first match beyond the cursor as the current match
if (currentFound === -1) { if (currentFound === -1) {
@@ -99,7 +118,7 @@ export default class FindInCode {
this.findResult = findResult; this.findResult = findResult;
// Calculate curfound if not already, highlight it as selected // Calculate curfound if not already, highlight it as selected
if (totalFound > 0) { if (findResult && totalFound > 0) {
currentFound = Math.max(0, currentFound); currentFound = Math.max(0, currentFound);
let marker = findResult[currentFound]; let marker = findResult[currentFound];
let pos = marker.find(); let pos = marker.find();
@@ -114,8 +133,12 @@ export default class FindInCode {
}; };
} }
async findNext(direction, currentFound, nextFound) { async findNext(direction: number, currentFound: number, nextFound: number) {
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
if (!codeEditor || !this.findResult) {
return;
}
const doc = codeEditor.doc; const doc = codeEditor.doc;
// //
@@ -137,18 +160,23 @@ export default class FindInCode {
codeEditor.scrollIntoView(pos.from); codeEditor.scrollIntoView(pos.from);
} }
async findBoxClosed(totalFound, currentFound) { async findBoxClosed(totalFound: number, currentFound: number) {
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
if (totalFound > 0) { if (codeEditor && totalFound > 0) {
const doc = codeEditor.doc; const doc = codeEditor.doc;
const pos = this.findResult[currentFound].find(); const pos = this.findResult?.[currentFound].find();
// Note setting the selection sets the cursor to // Note setting the selection sets the cursor to
// the end of the selection and scrolls it into // the end of the selection and scrolls it into
// view // view
doc.setSelection(pos.from, pos.to); if (pos) {
doc.setSelection(pos.from, pos.to);
}
// Clear all markers // Clear all markers
codeEditor.operation(() => { codeEditor.operation(() => {
if (!this.findResult) {
return;
}
for (let i = 0; i < this.findResult.length; ++i) { for (let i = 0; i < this.findResult.length; ++i) {
let marker = this.findResult[i]; let marker = this.findResult[i];
marker.clear(); marker.clear();
@@ -157,9 +185,9 @@ export default class FindInCode {
} }
this.findResult = null; this.findResult = null;
codeEditor.focus(); codeEditor?.focus();
} }
async replace(replaceText) { async replace(replaceText: string) {
// this.findResult may be undefined and null // this.findResult may be undefined and null
if (!this.findResult || this.findResult.length === 0) { if (!this.findResult || this.findResult.length === 0) {
return; return;
@@ -178,8 +206,10 @@ export default class FindInCode {
let marker = this.findResult[currentFound]; let marker = this.findResult[currentFound];
let pos = marker.find(); let pos = marker.find();
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc; const doc = codeEditor?.doc;
doc.replaceRange(replaceText, pos.from, pos.to); if (doc) {
doc.replaceRange(replaceText, pos.from, pos.to);
}
marker.clear(); marker.clear();
let nextFound; let nextFound;
@@ -194,17 +224,21 @@ export default class FindInCode {
} }
} }
} }
async replaceAll(replaceText) { async replaceAll(replaceText: string) {
if (!this.findResult || this.findResult.length === 0) { if (!this.findResult || this.findResult.length === 0) {
return; return;
} }
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc; const doc = codeEditor?.doc;
codeEditor.operation(() => { codeEditor?.operation(() => {
if (!this.findResult) {
return;
}
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) { for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
let marker = this.findResult[currentFound]; let marker = this.findResult[currentFound];
let pos = marker.find(); let pos = marker.find();
doc.replaceRange(replaceText, pos.from, pos.to); doc?.replaceRange(replaceText, pos.from, pos.to);
marker.clear(); marker.clear();
} }
}); });

View File

@@ -4,28 +4,34 @@
import libraryLoader from "../services/library_loader.js"; import libraryLoader from "../services/library_loader.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import type FindWidget from "./find.js";
import type { FindResult } from "./find.js";
const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; const FIND_RESULT_CSS_CLASSNAME = "ck-find-result";
export default class FindInHtml { export default class FindInHtml {
constructor(parent) {
/** @property {FindWidget} */ private parent: FindWidget;
private currentIndex: number;
private $results: JQuery<HTMLElement> | null;
constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
this.currentIndex = 0; this.currentIndex = 0;
this.$results = null; this.$results = null;
} }
async performFind(searchTerm, matchCase, wholeWord) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
await libraryLoader.requireLibrary(libraryLoader.MARKJS); await libraryLoader.requireLibrary(libraryLoader.MARKJS);
const $content = await this.parent.noteContext.getContentElement(); const $content = await this.parent?.noteContext?.getContentElement();
const wholeWordChar = wholeWord ? "\\b" : ""; const wholeWordChar = wholeWord ? "\\b" : "";
const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi"); const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi");
return new Promise((res) => { return new Promise<FindResult>((res) => {
$content.unmark({ $content?.unmark({
done: () => { done: () => {
$content.markRegExp(regExp, { $content.markRegExp(regExp, {
element: "span", element: "span",
@@ -48,8 +54,8 @@ export default class FindInHtml {
}); });
} }
async findNext(direction, currentFound, nextFound) { async findNext(direction: -1 | 1, currentFound: number, nextFound: number) {
if (this.$results.length) { if (this.$results?.length) {
this.currentIndex += direction; this.currentIndex += direction;
if (this.currentIndex < 0) { if (this.currentIndex < 0) {
@@ -64,13 +70,15 @@ export default class FindInHtml {
} }
} }
async findBoxClosed(totalFound, currentFound) { async findBoxClosed(totalFound: number, currentFound: number) {
const $content = await this.parent.noteContext.getContentElement(); const $content = await this.parent?.noteContext?.getContentElement();
$content.unmark(); if ($content) {
$content.unmark();
}
} }
async jumpTo() { async jumpTo() {
if (this.$results.length) { if (this.$results?.length) {
const offsetTop = 100; const offsetTop = 100;
const $current = this.$results.eq(this.currentIndex); const $current = this.$results.eq(this.currentIndex);
this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME); this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
@@ -79,10 +87,11 @@ export default class FindInHtml {
$current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME); $current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
const position = $current.position().top - offsetTop; const position = $current.position().top - offsetTop;
const $content = await this.parent.noteContext.getContentElement(); const $content = await this.parent.noteContext?.getContentElement();
const $contentWiget = appContext.getComponentByEl($content); if ($content) {
const $contentWidget = appContext.getComponentByEl($content[0]);
$contentWiget.triggerCommand("scrollContainerTo", { position }); $contentWidget.triggerCommand("scrollContainerTo", { position });
}
} }
} }
} }

View File

@@ -1,17 +1,38 @@
import type { FindResult } from "./find.js";
import type FindWidget from "./find.js";
// TODO: Deduplicate.
interface Match {
className: string;
clear(): void;
find(): {
from: number;
to: number;
};
}
export default class FindInText { export default class FindInText {
constructor(parent) {
/** @property {FindWidget} */ private parent: FindWidget;
private findResult?: CKFindResult | null;
private editingState?: EditingState;
constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
} }
async getTextEditor() { async getTextEditor() {
return this.parent.noteContext.getTextEditor(); return this.parent?.noteContext?.getTextEditor();
} }
async performFind(searchTerm, matchCase, wholeWord) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean): Promise<FindResult> {
// Do this even if the searchTerm is empty so the markers are cleared and // Do this even if the searchTerm is empty so the markers are cleared and
// the counters updated // the counters updated
const textEditor = await this.getTextEditor(); const textEditor = await this.getTextEditor();
if (!textEditor) {
return { currentFound: 0, totalFound: 0 };
}
const model = textEditor.model; const model = textEditor.model;
let findResult = null; let findResult = null;
let totalFound = 0; let totalFound = 0;
@@ -31,14 +52,14 @@ export default class FindInText {
// let m = text.match(re); // let m = text.match(re);
// totalFound = m ? m.length : 0; // totalFound = m ? m.length : 0;
const options = { matchCase: matchCase, wholeWords: wholeWord }; const options = { matchCase: matchCase, wholeWords: wholeWord };
findResult = textEditor.execute("find", searchTerm, options); findResult = textEditor.execute<CKFindResult>("find", searchTerm, options);
totalFound = findResult.results.length; totalFound = findResult.results.length;
// Find the result beyond the cursor // Find the result beyond the cursor
const cursorPos = model.document.selection.getLastPosition(); const cursorPos = model.document.selection.getLastPosition();
for (let i = 0; i < findResult.results.length; ++i) { for (let i = 0; i < findResult.results.length; ++i) {
const marker = findResult.results.get(i).marker; const marker = findResult.results.get(i).marker;
const fromPos = marker.getStart(); const fromPos = marker.getStart();
if (fromPos.compareWith(cursorPos) !== "before") { if (cursorPos && fromPos.compareWith(cursorPos) !== "before") {
currentFound = i; currentFound = i;
break; break;
} }
@@ -54,7 +75,7 @@ export default class FindInText {
// XXX Do this accessing the private data? // XXX Do this accessing the private data?
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
for (let i = 0; i < currentFound; ++i) { for (let i = 0; i < currentFound; ++i) {
textEditor.execute("findNext", searchTerm); textEditor?.execute("findNext", searchTerm);
} }
} }
@@ -64,7 +85,7 @@ export default class FindInText {
}; };
} }
async findNext(direction, currentFound, nextFound) { async findNext(direction: number, currentFound: number, nextFound: number) {
const textEditor = await this.getTextEditor(); const textEditor = await this.getTextEditor();
// There are no parameters for findNext/findPrev // There are no parameters for findNext/findPrev
@@ -72,20 +93,23 @@ export default class FindInText {
// curFound wrap around above assumes findNext and // curFound wrap around above assumes findNext and
// findPrevious wraparound, which is what they do // findPrevious wraparound, which is what they do
if (direction > 0) { if (direction > 0) {
textEditor.execute("findNext"); textEditor?.execute("findNext");
} else { } else {
textEditor.execute("findPrevious"); textEditor?.execute("findPrevious");
} }
} }
async findBoxClosed(totalFound, currentFound) { async findBoxClosed(totalFound: number, currentFound: number) {
const textEditor = await this.getTextEditor(); const textEditor = await this.getTextEditor();
if (!textEditor) {
return;
}
if (totalFound > 0) { if (totalFound > 0) {
// Clear the markers and set the caret to the // Clear the markers and set the caret to the
// current occurrence // current occurrence
const model = textEditor.model; const model = textEditor.model;
const range = this.findResult.results.get(currentFound).marker.getRange(); const range = this.findResult?.results?.get(currentFound).marker.getRange();
// From // From
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92
// XXX Roll our own since already done for codeEditor and // XXX Roll our own since already done for codeEditor and
@@ -93,9 +117,11 @@ export default class FindInText {
let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing"); let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
findAndReplaceEditing.state.clear(model); findAndReplaceEditing.state.clear(model);
findAndReplaceEditing.stop(); findAndReplaceEditing.stop();
model.change((writer) => { if (range) {
writer.setSelection(range, 0); model.change((writer) => {
}); writer.setSelection(range, 0);
});
}
textEditor.editing.view.scrollToTheSelection(); textEditor.editing.view.scrollToTheSelection();
} }
@@ -104,17 +130,17 @@ export default class FindInText {
textEditor.focus(); textEditor.focus();
} }
async replace(replaceText) { async replace(replaceText: string) {
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) { if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
const textEditor = await this.getTextEditor(); const textEditor = await this.getTextEditor();
textEditor.execute("replace", replaceText, this.editingState.highlightedResult); textEditor?.execute("replace", replaceText, this.editingState.highlightedResult);
} }
} }
async replaceAll(replaceText) { async replaceAll(replaceText: string) {
if (this.editingState !== undefined && this.editingState.results.length > 0) { if (this.editingState !== undefined && this.editingState.results.length > 0) {
const textEditor = await this.getTextEditor(); const textEditor = await this.getTextEditor();
textEditor.execute("replaceAll", replaceText, this.editingState.results); textEditor?.execute("replaceAll", replaceText, this.editingState.results);
} }
} }
} }

View File

@@ -5,10 +5,9 @@ import type NoteContext from "../components/note_context.js";
/** /**
* This widget allows for changing and updating depending on the active note. * This widget allows for changing and updating depending on the active note.
* @extends {BasicWidget}
*/ */
class NoteContextAwareWidget extends BasicWidget { class NoteContextAwareWidget extends BasicWidget {
protected noteContext?: NoteContext; noteContext?: NoteContext;
isNoteContext(ntxId: string | string[] | null | undefined) { isNoteContext(ntxId: string | string[] | null | undefined) {
if (Array.isArray(ntxId)) { if (Array.isArray(ntxId)) {

View File

@@ -5,6 +5,7 @@ import openService from "../../services/open.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import protectedSessionHolder from "../../services/protected_session_holder.js"; import protectedSessionHolder from "../../services/protected_session_holder.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
const TPL = ` const TPL = `
<div class="file-properties-widget"> <div class="file-properties-widget">
@@ -66,6 +67,16 @@ const TPL = `
</div>`; </div>`;
export default class FilePropertiesWidget extends NoteContextAwareWidget { export default class FilePropertiesWidget extends NoteContextAwareWidget {
private $fileNoteId!: JQuery<HTMLElement>;
private $fileName!: JQuery<HTMLElement>;
private $fileType!: JQuery<HTMLElement>;
private $fileSize!: JQuery<HTMLElement>;
private $downloadButton!: JQuery<HTMLElement>;
private $openButton!: JQuery<HTMLElement>;
private $uploadNewRevisionButton!: JQuery<HTMLElement>;
private $uploadNewRevisionInput!: JQuery<HTMLFormElement>;
get name() { get name() {
return "fileProperties"; return "fileProperties";
} }
@@ -99,8 +110,8 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget {
this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision"); this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision");
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input"); this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
this.$downloadButton.on("click", () => openService.downloadFileNote(this.noteId)); this.$downloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId));
this.$openButton.on("click", () => openService.openNoteExternally(this.noteId, this.note.mime)); this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime));
this.$uploadNewRevisionButton.on("click", () => { this.$uploadNewRevisionButton.on("click", () => {
this.$uploadNewRevisionInput.trigger("click"); this.$uploadNewRevisionInput.trigger("click");
@@ -122,16 +133,20 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget {
}); });
} }
async refreshWithNote(note) { async refreshWithNote(note: FNote) {
this.$widget.show(); this.$widget.show();
if (!this.note) {
return;
}
this.$fileNoteId.text(note.noteId); this.$fileNoteId.text(note.noteId);
this.$fileName.text(note.getLabelValue("originalFileName") || "?"); this.$fileName.text(note.getLabelValue("originalFileName") || "?");
this.$fileType.text(note.mime); this.$fileType.text(note.mime);
const blob = await this.note.getBlob(); const blob = await this.note.getBlob();
this.$fileSize.text(utils.formatSize(blob.contentLength)); this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0));
// open doesn't work for protected notes since it works through a browser which isn't in protected session // open doesn't work for protected notes since it works through a browser which isn't in protected session
this.$openButton.toggle(!note.isProtected); this.$openButton.toggle(!note.isProtected);

View File

@@ -4,6 +4,7 @@ import toastService from "../../services/toast.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
const TPL = ` const TPL = `
<div class="image-properties"> <div class="image-properties">
@@ -50,6 +51,16 @@ const TPL = `
</div>`; </div>`;
export default class ImagePropertiesWidget extends NoteContextAwareWidget { export default class ImagePropertiesWidget extends NoteContextAwareWidget {
private $copyReferenceToClipboardButton!: JQuery<HTMLElement>;
private $uploadNewRevisionButton!: JQuery<HTMLElement>;
private $uploadNewRevisionInput!: JQuery<HTMLFormElement>;
private $fileName!: JQuery<HTMLElement>;
private $fileType!: JQuery<HTMLElement>;
private $fileSize!: JQuery<HTMLElement>;
private $openButton!: JQuery<HTMLElement>;
private $imageDownloadButton!: JQuery<HTMLElement>;
get name() { get name() {
return "imageProperties"; return "imageProperties";
} }
@@ -76,7 +87,7 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
this.contentSized(); this.contentSized();
this.$copyReferenceToClipboardButton = this.$widget.find(".image-copy-reference-to-clipboard"); this.$copyReferenceToClipboardButton = this.$widget.find(".image-copy-reference-to-clipboard");
this.$copyReferenceToClipboardButton.on("click", () => this.triggerEvent(`copyImageReferenceToClipboard`, { ntxId: this.noteContext.ntxId })); this.$copyReferenceToClipboardButton.on("click", () => this.triggerEvent(`copyImageReferenceToClipboard`, { ntxId: this.noteContext?.ntxId }));
this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision"); this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision");
this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input"); this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input");
@@ -86,10 +97,10 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
this.$fileSize = this.$widget.find(".image-filesize"); this.$fileSize = this.$widget.find(".image-filesize");
this.$openButton = this.$widget.find(".image-open"); this.$openButton = this.$widget.find(".image-open");
this.$openButton.on("click", () => openService.openNoteExternally(this.noteId, this.note.mime)); this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime));
this.$imageDownloadButton = this.$widget.find(".image-download"); this.$imageDownloadButton = this.$widget.find(".image-download");
this.$imageDownloadButton.on("click", () => openService.downloadFileNote(this.noteId)); this.$imageDownloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId));
this.$uploadNewRevisionButton.on("click", () => { this.$uploadNewRevisionButton.on("click", () => {
this.$uploadNewRevisionInput.trigger("click"); this.$uploadNewRevisionInput.trigger("click");
@@ -113,13 +124,13 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
}); });
} }
async refreshWithNote(note) { async refreshWithNote(note: FNote) {
this.$widget.show(); this.$widget.show();
const blob = await this.note.getBlob(); const blob = await this.note?.getBlob();
this.$fileName.text(note.getLabelValue("originalFileName") || "?"); this.$fileName.text(note.getLabelValue("originalFileName") || "?");
this.$fileSize.text(utils.formatSize(blob.contentLength)); this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0));
this.$fileType.text(note.mime); this.$fileType.text(note.mime);
} }
} }

View File

@@ -3,6 +3,8 @@ import AttributeDetailWidget from "../attribute_widgets/attribute_detail.js";
import attributeRenderer from "../../services/attribute_renderer.js"; import attributeRenderer from "../../services/attribute_renderer.js";
import attributeService from "../../services/attributes.js"; import attributeService from "../../services/attributes.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
import type { EventData } from "../../components/app_context.js";
const TPL = ` const TPL = `
<div class="inherited-attributes-widget"> <div class="inherited-attributes-widget">
@@ -10,7 +12,7 @@ const TPL = `
.inherited-attributes-widget { .inherited-attributes-widget {
position: relative; position: relative;
} }
.inherited-attributes-container { .inherited-attributes-container {
color: var(--muted-text-color); color: var(--muted-text-color);
max-height: 200px; max-height: 200px;
@@ -23,6 +25,11 @@ const TPL = `
</div>`; </div>`;
export default class InheritedAttributesWidget extends NoteContextAwareWidget { export default class InheritedAttributesWidget extends NoteContextAwareWidget {
private attributeDetailWidget: AttributeDetailWidget;
private $container!: JQuery<HTMLElement>;
get name() { get name() {
return "inheritedAttributes"; return "inheritedAttributes";
} }
@@ -34,7 +41,6 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
constructor() { constructor() {
super(); super();
/** @type {AttributeDetailWidget} */
this.attributeDetailWidget = new AttributeDetailWidget().contentSized().setParent(this); this.attributeDetailWidget = new AttributeDetailWidget().contentSized().setParent(this);
this.child(this.attributeDetailWidget); this.child(this.attributeDetailWidget);
@@ -42,7 +48,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
getTitle() { getTitle() {
return { return {
show: !this.note.isLaunchBarConfig(), show: !this.note?.isLaunchBarConfig(),
title: t("inherited_attribute_list.title"), title: t("inherited_attribute_list.title"),
icon: "bx bx-list-plus" icon: "bx bx-list-plus"
}; };
@@ -56,7 +62,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
this.$widget.append(this.attributeDetailWidget.render()); this.$widget.append(this.attributeDetailWidget.render());
} }
async refreshWithNote(note) { async refreshWithNote(note: FNote) {
this.$container.empty(); this.$container.empty();
const inheritedAttributes = this.getInheritedAttributes(note); const inheritedAttributes = this.getInheritedAttributes(note);
@@ -90,7 +96,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
} }
} }
getInheritedAttributes(note) { getInheritedAttributes(note: FNote) {
const attrs = note.getAttributes().filter((attr) => attr.noteId !== this.noteId); const attrs = note.getAttributes().filter((attr) => attr.noteId !== this.noteId);
attrs.sort((a, b) => { attrs.sort((a, b) => {
@@ -105,7 +111,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
return attrs; return attrs;
} }
entitiesReloadedEvent({ loadResults }) { entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) { if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
this.refresh(); this.refresh();
} }

View File

@@ -7,6 +7,10 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
import attributeService from "../../services/attributes.js"; import attributeService from "../../services/attributes.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import type FNote from "../../entities/fnote.js";
import type { Attribute } from "../../services/attribute_parser.js";
import type FAttribute from "../../entities/fattribute.js";
import type { EventData } from "../../components/app_context.js";
const TPL = ` const TPL = `
<div class="promoted-attributes-widget"> <div class="promoted-attributes-widget">
@@ -60,12 +64,20 @@ const TPL = `
<div class="promoted-attributes-container"></div> <div class="promoted-attributes-container"></div>
</div>`; </div>`;
// TODO: Deduplicate
interface AttributeResult {
attributeId: string;
}
/** /**
* This widget is quite special because it's used in the desktop ribbon, but in mobile outside of ribbon. * This widget is quite special because it's used in the desktop ribbon, but in mobile outside of ribbon.
* This works without many issues (apart from autocomplete), but it should be kept in mind when changing things * This works without many issues (apart from autocomplete), but it should be kept in mind when changing things
* and testing. * and testing.
*/ */
export default class PromotedAttributesWidget extends NoteContextAwareWidget { export default class PromotedAttributesWidget extends NoteContextAwareWidget {
private $container!: JQuery<HTMLElement>;
get name() { get name() {
return "promotedAttributes"; return "promotedAttributes";
} }
@@ -80,7 +92,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
this.$container = this.$widget.find(".promoted-attributes-container"); this.$container = this.$widget.find(".promoted-attributes-container");
} }
getTitle(note) { getTitle(note: FNote) {
const promotedDefAttrs = note.getPromotedDefinitionAttributes(); const promotedDefAttrs = note.getPromotedDefinitionAttributes();
if (promotedDefAttrs.length === 0) { if (promotedDefAttrs.length === 0) {
@@ -95,7 +107,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
}; };
} }
async refreshWithNote(note) { async refreshWithNote(note: FNote) {
this.$container.empty(); this.$container.empty();
const promotedDefAttrs = note.getPromotedDefinitionAttributes(); const promotedDefAttrs = note.getPromotedDefinitionAttributes();
@@ -116,7 +128,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation"; const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation";
const valueName = definitionAttr.name.substr(valueType.length + 1); const valueName = definitionAttr.name.substr(valueType.length + 1);
let valueAttrs = ownedAttributes.filter((el) => el.name === valueName && el.type === valueType); let valueAttrs = ownedAttributes.filter((el) => el.name === valueName && el.type === valueType) as Attribute[];
if (valueAttrs.length === 0) { if (valueAttrs.length === 0) {
valueAttrs.push({ valueAttrs.push({
@@ -134,7 +146,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
for (const valueAttr of valueAttrs) { for (const valueAttr of valueAttrs) {
const $cell = await this.createPromotedAttributeCell(definitionAttr, valueAttr, valueName); const $cell = await this.createPromotedAttributeCell(definitionAttr, valueAttr, valueName);
$cells.push($cell); if ($cell) {
$cells.push($cell);
}
} }
} }
@@ -144,14 +158,14 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
this.toggleInt(true); this.toggleInt(true);
} }
async createPromotedAttributeCell(definitionAttr, valueAttr, valueName) { async createPromotedAttributeCell(definitionAttr: FAttribute, valueAttr: Attribute, valueName: string) {
const definition = definitionAttr.getDefinition(); const definition = definitionAttr.getDefinition();
const id = `value-${valueAttr.attributeId}`; const id = `value-${valueAttr.attributeId}`;
const $input = $("<input>") const $input = $("<input>")
.prop("tabindex", 200 + definitionAttr.position) .prop("tabindex", 200 + definitionAttr.position)
.prop("id", id) .prop("id", id)
.attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : "") // if not owned, we'll force creation of a new attribute instead of updating the inherited one .attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId ?? "" : "") // if not owned, we'll force creation of a new attribute instead of updating the inherited one
.attr("data-attribute-type", valueAttr.type) .attr("data-attribute-type", valueAttr.type)
.attr("data-attribute-name", valueAttr.name) .attr("data-attribute-name", valueAttr.name)
.prop("value", valueAttr.value) .prop("value", valueAttr.value)
@@ -161,7 +175,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
.on("change", (event) => this.promotedAttributeChanged(event)); .on("change", (event) => this.promotedAttributeChanged(event));
const $actionCell = $("<div>"); const $actionCell = $("<div>");
const $multiplicityCell = $("<td>").addClass("multiplicity").attr("nowrap", true); const $multiplicityCell = $("<td>").addClass("multiplicity").attr("nowrap", "true");
const $wrapper = $('<div class="promoted-attribute-cell">') const $wrapper = $('<div class="promoted-attribute-cell">')
.append( .append(
@@ -180,12 +194,12 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
// autocomplete for label values is just nice to have, mobile can keep labels editable without autocomplete // autocomplete for label values is just nice to have, mobile can keep labels editable without autocomplete
if (utils.isDesktop()) { if (utils.isDesktop()) {
// no need to await for this, can be done asynchronously // no need to await for this, can be done asynchronously
server.get(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((attributeValues) => { server.get<string[]>(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributeValues) => {
if (attributeValues.length === 0) { if (_attributeValues.length === 0) {
return; return;
} }
attributeValues = attributeValues.map((attribute) => ({ value: attribute })); const attributeValues = _attributeValues.map((attribute) => ({ value: attribute }));
$input.autocomplete( $input.autocomplete(
{ {
@@ -245,11 +259,11 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
const $openButton = $("<span>") const $openButton = $("<span>")
.addClass("input-group-text open-external-link-button bx bx-window-open") .addClass("input-group-text open-external-link-button bx bx-window-open")
.prop("title", t("promoted_attributes.open_external_link")) .prop("title", t("promoted_attributes.open_external_link"))
.on("click", () => window.open($input.val(), "_blank")); .on("click", () => window.open($input.val() as string, "_blank"));
$input.after($openButton); $input.after($openButton);
} else { } else {
ws.logError(t("promoted_attributes.unknown_label_type", { type: definitionAttr.labelType })); ws.logError(t("promoted_attributes.unknown_label_type", { type: definition.labelType }));
} }
} else if (valueAttr.type === "relation") { } else if (valueAttr.type === "relation") {
if (valueAttr.value) { if (valueAttr.value) {
@@ -290,9 +304,11 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
valueName valueName
); );
$wrapper.after($new); if ($new) {
$wrapper.after($new);
$new.find("input").trigger("focus"); $new.find("input").trigger("focus");
}
}); });
const $removeButton = $("<span>") const $removeButton = $("<span>")
@@ -320,7 +336,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
valueName valueName
); );
$wrapper.after($new); if ($new) {
$wrapper.after($new);
}
} }
$wrapper.remove(); $wrapper.remove();
@@ -332,7 +350,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
return $wrapper; return $wrapper;
} }
async promotedAttributeChanged(event) { async promotedAttributeChanged(event: JQuery.TriggeredEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) {
const $attr = $(event.target); const $attr = $(event.target);
let value; let value;
@@ -347,7 +365,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
value = $attr.val(); value = $attr.val();
} }
const result = await server.put( const result = await server.put<AttributeResult>(
`notes/${this.noteId}/attribute`, `notes/${this.noteId}/attribute`,
{ {
attributeId: $attr.attr("data-attribute-id"), attributeId: $attr.attr("data-attribute-id"),
@@ -365,7 +383,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
this.$widget.find(".promoted-attribute-input:first").focus(); this.$widget.find(".promoted-attribute-input:first").focus();
} }
entitiesReloadedEvent({ loadResults }) { entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) { if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
this.refresh(); this.refresh();
} }

View File

@@ -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") }],

View File

@@ -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: [

View File

@@ -1,5 +1,5 @@
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 froca from "../../services/froca.js"; import froca from "../../services/froca.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import contentRenderer from "../../services/content_renderer.js"; import contentRenderer from "../../services/content_renderer.js";
@@ -13,7 +13,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
this.refreshCodeBlockOptions(); this.refreshCodeBlockOptions();
} }
setupImageOpening(singleClickOpens) { setupImageOpening(singleClickOpens: boolean) {
this.$widget.on("dblclick", "img", (e) => this.openImageInCurrentTab($(e.target))); this.$widget.on("dblclick", "img", (e) => this.openImageInCurrentTab($(e.target)));
this.$widget.on("click", "img", (e) => { this.$widget.on("click", "img", (e) => {
@@ -29,27 +29,27 @@ export default class AbstractTextTypeWidget extends TypeWidget {
}); });
} }
async openImageInCurrentTab($img) { async openImageInCurrentTab($img: JQuery<HTMLElement>) {
const { noteId, viewScope } = await this.parseFromImage($img); const parsedImage = await this.parseFromImage($img);
if (noteId) { if (parsedImage) {
appContext.tabManager.getActiveContext().setNote(noteId, { viewScope }); appContext.tabManager.getActiveContext()?.setNote(parsedImage.noteId, { viewScope: parsedImage.viewScope });
} else { } else {
window.open($img.prop("src"), "_blank"); window.open($img.prop("src"), "_blank");
} }
} }
async openImageInNewTab($img) { async openImageInNewTab($img: JQuery<HTMLElement>) {
const { noteId, viewScope } = await this.parseFromImage($img); const parsedImage = await this.parseFromImage($img);
if (noteId) { if (parsedImage) {
appContext.tabManager.openTabWithNoteWithHoisting(noteId, { viewScope }); appContext.tabManager.openTabWithNoteWithHoisting(parsedImage.noteId, { viewScope: parsedImage.viewScope });
} else { } else {
window.open($img.prop("src"), "_blank"); window.open($img.prop("src"), "_blank");
} }
} }
async parseFromImage($img) { async parseFromImage($img: JQuery<HTMLElement>) {
const imgSrc = $img.prop("src"); const imgSrc = $img.prop("src");
const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//); const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//);
@@ -66,7 +66,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
const attachment = await froca.getAttachment(attachmentId); const attachment = await froca.getAttachment(attachmentId);
return { return {
noteId: attachment.ownerId, noteId: attachment?.ownerId,
viewScope: { viewScope: {
viewMode: "attachments", viewMode: "attachments",
attachmentId: attachmentId attachmentId: attachmentId
@@ -77,7 +77,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
return null; return null;
} }
async loadIncludedNote(noteId, $el) { async loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>) {
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
if (note) { if (note) {
@@ -97,11 +97,11 @@ export default class AbstractTextTypeWidget extends TypeWidget {
} }
} }
async loadReferenceLinkTitle($el, href = null) { async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null = null) {
await linkService.loadReferenceLinkTitle($el, href); await linkService.loadReferenceLinkTitle($el, href);
} }
refreshIncludedNote($container, noteId) { refreshIncludedNote($container: JQuery<HTMLElement>, noteId: string) {
if ($container) { if ($container) {
$container.find(`section[data-note-id="${noteId}"]`).each((_, el) => { $container.find(`section[data-note-id="${noteId}"]`).each((_, el) => {
this.loadIncludedNote(noteId, $(el)); this.loadIncludedNote(noteId, $(el));
@@ -114,7 +114,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
this.$widget.toggleClass("word-wrap", wordWrap); this.$widget.toggleClass("word-wrap", wordWrap);
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("codeBlockWordWrap")) { if (loadResults.isOptionReloaded("codeBlockWordWrap")) {
this.refreshCodeBlockOptions(); this.refreshCodeBlockOptions();
} }

View File

@@ -12,7 +12,7 @@ import library_loader from "../../../services/library_loader.js";
import mime_types from "../../../services/mime_types.js"; import mime_types from "../../../services/mime_types.js";
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js"; import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
export async function initSyntaxHighlighting(editor) { export async function initSyntaxHighlighting(editor: TextEditor) {
if (!isSyntaxHighlightEnabled) { if (!isSyntaxHighlightEnabled) {
return; return;
} }
@@ -25,39 +25,38 @@ const HIGHLIGHT_MAX_BLOCK_COUNT = 500;
const tag = "SyntaxHighlightWidget"; const tag = "SyntaxHighlightWidget";
const debugLevels = ["error", "warn", "info", "log", "debug"]; const debugLevels = ["error", "warn", "info", "log", "debug"];
const debugLevel = "debug"; const debugLevel = debugLevels.indexOf("warn");
let warn = function () {}; let warn = function (...args: unknown[]) {};
if (debugLevel >= debugLevels.indexOf("warn")) { if (debugLevel >= debugLevels.indexOf("warn")) {
warn = console.warn.bind(console, tag + ": "); warn = console.warn.bind(console, tag + ": ");
} }
let info = function () {}; let info = function (...args: unknown[]) {};
if (debugLevel >= debugLevels.indexOf("info")) { if (debugLevel >= debugLevels.indexOf("info")) {
info = console.info.bind(console, tag + ": "); info = console.info.bind(console, tag + ": ");
} }
let log = function () {}; let log = function (...args: unknown[]) {};
if (debugLevel >= debugLevels.indexOf("log")) { if (debugLevel >= debugLevels.indexOf("log")) {
log = console.log.bind(console, tag + ": "); log = console.log.bind(console, tag + ": ");
} }
let dbg = function () {}; let dbg = function (...args: unknown[]) {};
if (debugLevel >= debugLevels.indexOf("debug")) { if (debugLevel >= debugLevels.indexOf("debug")) {
dbg = console.debug.bind(console, tag + ": "); dbg = console.debug.bind(console, tag + ": ");
} }
function assert(e, msg) { function assert(e: boolean, msg?: string) {
console.assert(e, tag + ": " + msg); console.assert(e, tag + ": " + msg);
} }
// TODO: Should this be scoped to note? // TODO: Should this be scoped to note?
let markerCounter = 0; let markerCounter = 0;
function initTextEditor(textEditor) { function initTextEditor(textEditor: TextEditor) {
log("initTextEditor"); log("initTextEditor");
let widget = this;
const document = textEditor.model.document; const document = textEditor.model.document;
// Create a conversion from model to view that converts // Create a conversion from model to view that converts
@@ -100,7 +99,7 @@ function initTextEditor(textEditor) {
// See // See
// https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54 // https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54
const changes = document.differ.getChanges(); const changes = document.differ.getChanges();
let dirtyCodeBlocks = new Set(); let dirtyCodeBlocks = new Set<CKNode>();
for (const change of changes) { for (const change of changes) {
dbg("change " + JSON.stringify(change)); dbg("change " + JSON.stringify(change));
@@ -151,7 +150,7 @@ function initTextEditor(textEditor) {
* the formatting would be stored with the note and it would need a * the formatting would be stored with the note and it would need a
* way to remove that formatting when editing back the note. * way to remove that formatting when editing back the note.
*/ */
function highlightCodeBlock(codeBlock, writer) { function highlightCodeBlock(codeBlock: CKNode, writer: Writer) {
log("highlighting codeblock " + JSON.stringify(codeBlock.toJSON())); log("highlighting codeblock " + JSON.stringify(codeBlock.toJSON()));
const model = codeBlock.root.document.model; const model = codeBlock.root.document.model;
@@ -291,16 +290,16 @@ function highlightCodeBlock(codeBlock, writer) {
iHtml = html.indexOf(">", iHtml) + 1; iHtml = html.indexOf(">", iHtml) + 1;
// push the span // push the span
let posStart = writer.createPositionAt(codeBlock, child.startOffset + iChildText); let posStart = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
spanStack.push({ className: className, posStart: posStart }); spanStack.push({ className: className, posStart: posStart });
} else if (html[iHtml] == "<" && html[iHtml + 1] == "/") { } else if (html[iHtml] == "<" && html[iHtml + 1] == "/") {
// Done with this span, pop the span and mark the range // Done with this span, pop the span and mark the range
iHtml = html.indexOf(">", iHtml + 1) + 1; iHtml = html.indexOf(">", iHtml + 1) + 1;
let stackTop = spanStack.pop(); let stackTop = spanStack.pop();
let posStart = stackTop.posStart; let posStart = stackTop?.posStart;
let className = stackTop.className; let className = stackTop?.className;
let posEnd = writer.createPositionAt(codeBlock, child.startOffset + iChildText); let posEnd = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
let range = writer.createRange(posStart, posEnd); let range = writer.createRange(posStart, posEnd);
let markerName = "hljs:" + className + ":" + markerCounter; let markerName = "hljs:" + className + ":" + markerCounter;
// Use an incrementing number for the uniqueId, random of // Use an incrementing number for the uniqueId, random of

View File

@@ -8,7 +8,7 @@ import froca from "../../services/froca.js";
import noteCreateService from "../../services/note_create.js"; import noteCreateService from "../../services/note_create.js";
import AbstractTextTypeWidget from "./abstract_text_type_widget.js"; import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
import link from "../../services/link.js"; import link from "../../services/link.js";
import appContext from "../../components/app_context.js"; import appContext, { type EventData } from "../../components/app_context.js";
import dialogService from "../../services/dialog.js"; import dialogService from "../../services/dialog.js";
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js"; import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
@@ -16,14 +16,15 @@ import toast from "../../services/toast.js";
import { getMermaidConfig } from "../mermaid.js"; import { getMermaidConfig } from "../mermaid.js";
import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js"; import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js";
import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js"; import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js";
import type FNote from "../../entities/fnote.js";
const ENABLE_INSPECTOR = false; const ENABLE_INSPECTOR = false;
const mentionSetup = { const mentionSetup: MentionConfig = {
feeds: [ feeds: [
{ {
marker: "@", marker: "@",
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (item) => { itemRenderer: (item) => {
const itemElement = document.createElement("button"); const itemElement = document.createElement("button");
@@ -118,6 +119,12 @@ function buildListOfLanguages() {
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works. * - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
*/ */
export default class EditableTextTypeWidget extends AbstractTextTypeWidget { export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
private contentLanguage?: string | null;
private watchdog!: CKWatchdog;
private $editor!: JQuery<HTMLElement>;
static getType() { static getType() {
return "editableText"; return "editableText";
} }
@@ -195,7 +202,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
} }
}; };
const contentLanguage = this.note.getLabelValue("language"); const contentLanguage = this.note?.getLabelValue("language");
if (contentLanguage) { if (contentLanguage) {
finalConfig.language = { finalConfig.language = {
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"), ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
@@ -209,7 +216,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const editor = await editorClass.create(elementOrData, finalConfig); const editor = await editorClass.create(elementOrData, finalConfig);
const notificationsPlugin = editor.plugins.get("Notification"); const notificationsPlugin = editor.plugins.get("Notification");
notificationsPlugin.on("show:warning", (evt, data) => { notificationsPlugin.on("show:warning", (evt: CKEvent, data: PluginEventData) => {
const title = data.title; const title = data.title;
const message = data.message.message; const message = data.message.message;
@@ -246,6 +253,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
editor.model.document.on("change:data", () => this.spacedUpdate.scheduleUpdate()); editor.model.document.on("change:data", () => this.spacedUpdate.scheduleUpdate());
if (glob.isDev && ENABLE_INSPECTOR) { if (glob.isDev && ENABLE_INSPECTOR) {
//@ts-expect-error TODO: Check if this still works.
await import(/* webpackIgnore: true */ "../../../libraries/ckeditor/inspector.js"); await import(/* webpackIgnore: true */ "../../../libraries/ckeditor/inspector.js");
CKEditorInspector.attach(editor); CKEditorInspector.attach(editor);
} }
@@ -277,12 +285,12 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}); });
} }
async doRefresh(note) { async doRefresh(note: FNote) {
const blob = await note.getBlob(); const blob = await note.getBlob();
await this.spacedUpdate.allowUpdateWithoutChange(async () => { await this.spacedUpdate.allowUpdateWithoutChange(async () => {
const data = blob.content || ""; const data = blob?.content || "";
const newContentLanguage = this.note.getLabelValue("language"); const newContentLanguage = this.note?.getLabelValue("language");
if (this.contentLanguage !== newContentLanguage) { if (this.contentLanguage !== newContentLanguage) {
await this.reinitialize(data); await this.reinitialize(data);
} else { } else {
@@ -334,7 +342,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.addTextToEditor(dateString); this.addTextToEditor(dateString);
} }
async addLinkToEditor(linkHref, linkTitle) { async addLinkToEditor(linkHref: string, linkTitle: string) {
await this.initialized; await this.initialized;
this.watchdog.editor.model.change((writer) => { this.watchdog.editor.model.change((writer) => {
@@ -343,7 +351,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}); });
} }
async addTextToEditor(text) { async addTextToEditor(text: string) {
await this.initialized; await this.initialized;
this.watchdog.editor.model.change((writer) => { this.watchdog.editor.model.change((writer) => {
@@ -352,7 +360,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}); });
} }
addTextToActiveEditorEvent({ text }) { addTextToActiveEditorEvent({ text }: EventData<"addTextToActiveEditor">) {
if (!this.isActive()) { if (!this.isActive()) {
return; return;
} }
@@ -360,7 +368,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.addTextToEditor(text); this.addTextToEditor(text);
} }
async addLink(notePath, linkTitle, externalLink = false) { async addLink(notePath: string, linkTitle: string | null, externalLink: boolean = false) {
await this.initialized; await this.initialized;
if (linkTitle) { if (linkTitle) {
@@ -384,7 +392,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
return !selection.isCollapsed; return !selection.isCollapsed;
} }
async executeWithTextEditorEvent({ callback, resolve, ntxId }) { async executeWithTextEditorEvent({ callback, resolve, ntxId }: EventData<"executeWithTextEditor">) {
if (!this.isNoteContext(ntxId)) { if (!this.isNoteContext(ntxId)) {
return; return;
} }
@@ -428,7 +436,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const notePath = selectedElement.getAttribute("notePath"); const notePath = selectedElement.getAttribute("notePath");
if (notePath) { if (notePath) {
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
return; return;
} }
} }
@@ -441,7 +449,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const notePath = link.getNotePathFromUrl(selectedLinkUrl); const notePath = link.getNotePathFromUrl(selectedLinkUrl);
if (notePath) { if (notePath) {
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
} else { } else {
window.open(selectedLinkUrl, "_blank"); window.open(selectedLinkUrl, "_blank");
} }
@@ -451,7 +459,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.triggerCommand("showIncludeNoteDialog", { textTypeWidget: this }); this.triggerCommand("showIncludeNoteDialog", { textTypeWidget: this });
} }
addIncludeNote(noteId, boxSize) { addIncludeNote(noteId: string, boxSize?: string) {
this.watchdog.editor.model.change((writer) => { this.watchdog.editor.model.change((writer) => {
// Insert <includeNote>*</includeNote> at the current selection position // Insert <includeNote>*</includeNote> at the current selection position
// in a way that will result in creating a valid model structure // in a way that will result in creating a valid model structure
@@ -464,8 +472,11 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}); });
} }
async addImage(noteId) { async addImage(noteId: string) {
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
if (!note) {
return;
}
this.watchdog.editor.model.change((writer) => { this.watchdog.editor.model.change((writer) => {
const encodedTitle = encodeURIComponent(note.title); const encodedTitle = encodeURIComponent(note.title);
@@ -475,24 +486,28 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}); });
} }
async createNoteForReferenceLink(title) { async createNoteForReferenceLink(title: string) {
if (!this.notePath) {
return;
}
const resp = await noteCreateService.createNoteWithTypePrompt(this.notePath, { const resp = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
activate: false, activate: false,
title: title title: title
}); });
if (!resp) { if (!resp || !resp.note) {
return; return;
} }
return resp.note.getBestNotePathString(); return resp.note.getBestNotePathString();
} }
async refreshIncludedNoteEvent({ noteId }) { async refreshIncludedNoteEvent({ noteId }: EventData<"refreshIncludedNote">) {
this.refreshIncludedNote(this.$editor, noteId); this.refreshIncludedNote(this.$editor, noteId);
} }
async reinitialize(data) { async reinitialize(data: string) {
if (!this.watchdog) { if (!this.watchdog) {
return; return;
} }

View File

@@ -114,7 +114,9 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
this.$content.find("section").each((_, el) => { this.$content.find("section").each((_, el) => {
const noteId = $(el).attr("data-note-id"); const noteId = $(el).attr("data-note-id");
this.loadIncludedNote(noteId, $(el)); if (noteId) {
this.loadIncludedNote(noteId, $(el));
}
}); });
if (this.$content.find("span.math-tex").length > 0) { if (this.$content.find("span.math-tex").length > 0) {

View File

@@ -1,17 +1,36 @@
import server from "../../services/server.js"; import server from "../../services/server.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import libraryLoader from "../../services/library_loader.js";
import contextMenu from "../../menus/context_menu.js"; 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";
import type { ConnectionMadeEventInfo, jsPlumbInstance, OverlaySpec } from "jsplumb";
import "../../../stylesheets/relation_map.css";
const uniDirectionalOverlays = [ declare module "jsplumb" {
interface Connection {
canvas: HTMLCanvasElement;
getType(): string;
bind(event: string, callback: (obj: unknown, event: MouseEvent) => void): void;
}
interface Overlay {
setLabel(label: string): void;
}
interface ConnectParams {
type: RelationType;
}
}
const uniDirectionalOverlays: OverlaySpec[] = [
[ [
"Arrow", "Arrow",
{ {
@@ -92,7 +111,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 +183,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 +204,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: [
@@ -150,15 +224,15 @@ export default class RelationMapTypeWidget extends TypeWidget {
this.$widget.on("dragover", (ev) => ev.preventDefault()); this.$widget.on("dragover", (ev) => ev.preventDefault());
this.initialized = new Promise(async (res) => { this.initialized = new Promise(async (res) => {
await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP); // Weird typecast is needed probably due to bad typings in the module itself.
const jsPlumb = (await import("jsplumb")).default.jsPlumb as unknown as jsPlumbInstance;
jsPlumb.ready(res); jsPlumb.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 +242,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 +254,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 +294,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,20 +305,20 @@ 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(); await this.initJsPlumbInstance();
this.initPanZoom(); await this.initPanZoom();
this.loadNotesAndRelations(); this.loadNotesAndRelations();
} }
@@ -248,15 +326,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 +364,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 +387,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") {
@@ -315,30 +403,37 @@ export default class RelationMapTypeWidget extends TypeWidget {
}); });
} }
initPanZoom() { async initPanZoom() {
if (this.pzInstance) { if (this.pzInstance) {
return; return;
} }
const panzoom = (await import("panzoom")).default;
this.pzInstance = panzoom(this.$relationMapContainer[0], { this.pzInstance = panzoom(this.$relationMapContainer[0], {
maxZoom: 2, maxZoom: 2,
minZoom: 0.3, minZoom: 0.3,
smoothScroll: false, smoothScroll: false,
filterKey: function (e, dx, dy, dz) {
//@ts-expect-error Upstream incorrectly mentions no arguments.
filterKey: function (e: KeyboardEvent) {
// if ALT is pressed, then panzoom should bubble the event up // if ALT is pressed, then panzoom should bubble the event up
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working // this is to preserve ALT-LEFT, ALT-RIGHT navigation working
return e.altKey; return e.altKey;
} }
}); });
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 +444,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));
@@ -370,13 +469,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
} }
} }
initJsPlumbInstance() { async initJsPlumbInstance() {
if (this.jsPlumbInstance) { if (this.jsPlumbInstance) {
this.cleanup(); this.cleanup();
return; return;
} }
const jsPlumb = (await import("jsplumb")).default.jsPlumb;
this.jsPlumbInstance = jsPlumb.getInstance({ this.jsPlumbInstance = jsPlumb.getInstance({
Endpoint: ["Dot", { radius: 2 }], Endpoint: ["Dot", { radius: 2 }],
Connector: "StateMachine", Connector: "StateMachine",
@@ -385,6 +485,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 +500,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 +518,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 +538,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 +575,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 +594,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 +624,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 +670,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 +707,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 +724,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 +746,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);
} }
} }

View File

@@ -70,15 +70,11 @@ async function register(app: express.Application) {
app.use(`/${assetPath}/node_modules/jquery-hotkeys/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/jquery-hotkeys/"))); app.use(`/${assetPath}/node_modules/jquery-hotkeys/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/jquery-hotkeys/")));
app.use(`/${assetPath}/node_modules/panzoom/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/panzoom/dist/")));
// i18n // i18n
app.use(`/${assetPath}/translations/`, persistentCacheStatic(path.join(srcRoot, "public", "translations/"))); app.use(`/${assetPath}/translations/`, persistentCacheStatic(path.join(srcRoot, "public", "translations/")));
app.use(`/${assetPath}/node_modules/eslint/bin/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/eslint/bin/"))); app.use(`/${assetPath}/node_modules/eslint/bin/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/eslint/bin/")));
app.use(`/${assetPath}/node_modules/jsplumb/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/jsplumb/dist/")));
app.use(`/${assetPath}/node_modules/vanilla-js-wheel-zoom/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/vanilla-js-wheel-zoom/dist/"))); app.use(`/${assetPath}/node_modules/vanilla-js-wheel-zoom/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/vanilla-js-wheel-zoom/dist/")));
app.use(`/${assetPath}/node_modules/mark.js/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/mark.js/dist/"))); app.use(`/${assetPath}/node_modules/mark.js/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/mark.js/dist/")));