mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-09-24 21:41:30 +08:00
Merge pull request #1466 from TriliumNext/feature/port_ts
client: port ts
This commit is contained in:
commit
772d07a60f
@ -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\|
|
@ -80,10 +80,8 @@ try {
|
||||
"node_modules/jquery/dist/",
|
||||
"node_modules/jquery-hotkeys/",
|
||||
"node_modules/split.js/dist/",
|
||||
"node_modules/panzoom/dist/",
|
||||
"node_modules/i18next/",
|
||||
"node_modules/i18next-http-backend/",
|
||||
"node_modules/jsplumb/dist/",
|
||||
"node_modules/vanilla-js-wheel-zoom/dist/",
|
||||
"node_modules/mark.js/dist/",
|
||||
"node_modules/normalize.css/",
|
||||
|
@ -193,6 +193,8 @@ export type CommandMappings = {
|
||||
showPasswordNotSet: CommandData;
|
||||
showProtectedSessionPasswordDialog: CommandData;
|
||||
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
||||
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
|
||||
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
|
||||
closeProtectedSessionPasswordDialog: CommandData;
|
||||
copyImageReferenceToClipboard: CommandData;
|
||||
copyImageToClipboard: CommandData;
|
||||
@ -364,6 +366,9 @@ type EventMappings = {
|
||||
textTypeWidget: EditableTextTypeWidget;
|
||||
text: string;
|
||||
};
|
||||
showIncludeDialog: {
|
||||
textTypeWidget: EditableTextTypeWidget;
|
||||
};
|
||||
openBulkActionsDialog: {
|
||||
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>>;
|
||||
|
||||
class AppContext extends Component {
|
||||
export class AppContext extends Component {
|
||||
isMainWindow: boolean;
|
||||
components: Component[];
|
||||
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
||||
|
@ -16,7 +16,7 @@ export interface SetNoteOpts {
|
||||
viewScope?: ViewScope;
|
||||
}
|
||||
|
||||
export type GetTextEditorCallback = () => void;
|
||||
export type GetTextEditorCallback = (editor: TextEditor) => void;
|
||||
|
||||
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
|
||||
ntxId: string | null;
|
||||
|
@ -88,13 +88,18 @@ import utils from "../services/utils.js";
|
||||
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
||||
import ContextualHelpButton from "../widgets/floating_buttons/help_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 {
|
||||
constructor(customWidgets) {
|
||||
|
||||
private customWidgets: WidgetsByParent;
|
||||
|
||||
constructor(customWidgets: WidgetsByParent) {
|
||||
this.customWidgets = customWidgets;
|
||||
}
|
||||
|
||||
getRootWidget(appContext) {
|
||||
getRootWidget(appContext: AppContext) {
|
||||
appContext.noteTreeWidget = new NoteTreeWidget();
|
||||
|
||||
const launcherPaneIsHorizontal = options.get("layoutOrientation") === "horizontal";
|
||||
@ -267,7 +272,7 @@ export default class DesktopLayout {
|
||||
.child(new CloseZenButton());
|
||||
}
|
||||
|
||||
#buildLauncherPane(isHorizontal) {
|
||||
#buildLauncherPane(isHorizontal: boolean) {
|
||||
let launcherPane;
|
||||
|
||||
if (isHorizontal) {
|
@ -1,9 +1,8 @@
|
||||
import type { CommandNames } from "../components/app_context.js";
|
||||
import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import note_tooltip from "../services/note_tooltip.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
interface ContextMenuOptions<T extends CommandNames> {
|
||||
interface ContextMenuOptions<T> {
|
||||
x: number;
|
||||
y: number;
|
||||
orientation?: "left";
|
||||
@ -17,7 +16,7 @@ interface MenuSeparatorItem {
|
||||
title: "----";
|
||||
}
|
||||
|
||||
export interface MenuCommandItem<T extends CommandNames> {
|
||||
export interface MenuCommandItem<T> {
|
||||
title: string;
|
||||
command?: T;
|
||||
type?: string;
|
||||
@ -30,8 +29,8 @@ export interface MenuCommandItem<T extends CommandNames> {
|
||||
spellingSuggestion?: string;
|
||||
}
|
||||
|
||||
export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||
|
||||
class ContextMenu {
|
||||
@ -55,7 +54,7 @@ class ContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
async show<T extends CommandNames>(options: ContextMenuOptions<T>) {
|
||||
async show<T>(options: ContextMenuOptions<T>) {
|
||||
this.options = options;
|
||||
|
||||
note_tooltip.dismissAllTooltips();
|
||||
|
@ -8,9 +8,10 @@ interface Token {
|
||||
}
|
||||
|
||||
export interface Attribute {
|
||||
attributeId?: string;
|
||||
type: AttributeType;
|
||||
name: string;
|
||||
isInheritable: boolean;
|
||||
isInheritable?: boolean;
|
||||
value?: string;
|
||||
startIndex?: number;
|
||||
endIndex?: number;
|
||||
|
@ -50,7 +50,7 @@ async function executeStartupBundles() {
|
||||
}
|
||||
}
|
||||
|
||||
class WidgetsByParent {
|
||||
export class WidgetsByParent {
|
||||
private byParent: Record<string, Widget[]>;
|
||||
|
||||
constructor() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import appContext from "../components/app_context.js";
|
||||
import type { ConfirmDialogOptions, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
|
||||
async function info(message: string) {
|
||||
@ -16,7 +16,7 @@ async function confirm(message: string) {
|
||||
}
|
||||
|
||||
async function confirmDeleteNoteBoxWithNote(title: string) {
|
||||
return new Promise((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
|
||||
return new Promise<ConfirmDialogResult | undefined>((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
|
||||
}
|
||||
|
||||
async function prompt(props: PromptDialogOptions) {
|
||||
|
@ -5,13 +5,15 @@ import utils from "./utils.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
interface UploadFilesOptions {
|
||||
safeImport?: boolean;
|
||||
shrinkImages: boolean | "true" | "false";
|
||||
textImportedAsText?: boolean;
|
||||
codeImportedAsCode?: boolean;
|
||||
explodeArchives?: boolean;
|
||||
replaceUnderscoresWithSpaces?: boolean;
|
||||
type BooleanLike = boolean | "true" | "false";
|
||||
|
||||
export interface UploadFilesOptions {
|
||||
safeImport?: BooleanLike;
|
||||
shrinkImages: BooleanLike;
|
||||
textImportedAsText?: BooleanLike;
|
||||
codeImportedAsCode?: BooleanLike;
|
||||
explodeArchives?: BooleanLike;
|
||||
replaceUnderscoresWithSpaces?: BooleanLike;
|
||||
}
|
||||
|
||||
export async function uploadFiles(entityType: string, parentNoteId: string, files: string[] | File[], options: UploadFilesOptions) {
|
||||
|
@ -42,11 +42,6 @@ const CODE_MIRROR: Library = {
|
||||
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 = {
|
||||
css: ["stylesheets/calendar.css"]
|
||||
};
|
||||
@ -183,7 +178,6 @@ export default {
|
||||
loadHighlightingTheme,
|
||||
CKEDITOR,
|
||||
CODE_MIRROR,
|
||||
RELATION_MAP,
|
||||
CALENDAR_WIDGET,
|
||||
KATEX,
|
||||
WHEEL_ZOOM,
|
||||
|
@ -252,7 +252,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
};
|
||||
}
|
||||
|
||||
function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
|
||||
function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
|
||||
const $link = $(evt.target as any).closest("a,.block-link");
|
||||
const hrefLink = $link.attr("href") || $link.attr("data-href");
|
||||
|
||||
|
@ -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";
|
||||
|
||||
interface DefinitionObject {
|
||||
|
237
src/public/app/types.d.ts
vendored
237
src/public/app/types.d.ts
vendored
@ -7,6 +7,7 @@ import server from "./services/server.ts";
|
||||
import library_loader, { Library } from "./services/library_loader.ts";
|
||||
import type { init } from "i18next";
|
||||
import type { lint } from "./services/eslint.ts";
|
||||
import type { RelationType } from "./widgets/type_widgets/relation_map.ts";
|
||||
|
||||
interface ElectronProcess {
|
||||
type: string;
|
||||
@ -46,6 +47,7 @@ interface CustomGlobals {
|
||||
TRILIUM_SAFE_MODE: boolean;
|
||||
platform?: typeof process.platform;
|
||||
linter: typeof lint;
|
||||
hasNativeTitleBar: boolean;
|
||||
}
|
||||
|
||||
type RequireMethod = (moduleName: string) => any;
|
||||
@ -74,7 +76,7 @@ declare global {
|
||||
|
||||
interface AutoCompleteArg {
|
||||
displayKey: "name" | "value" | "notePathTitle";
|
||||
cache: boolean;
|
||||
cache?: boolean;
|
||||
source: (term: string, cb: AutoCompleteCallback) => void,
|
||||
templates?: {
|
||||
suggestion: (suggestion: Suggestion) => string | undefined
|
||||
@ -95,7 +97,11 @@ declare global {
|
||||
className: string;
|
||||
separateWordSearch: boolean;
|
||||
caseSensitive: boolean;
|
||||
})
|
||||
done?: () => void;
|
||||
});
|
||||
unmark(opts?: {
|
||||
done: () => void;
|
||||
});
|
||||
}
|
||||
|
||||
interface JQueryStatic {
|
||||
@ -171,17 +177,51 @@ declare global {
|
||||
}>
|
||||
};
|
||||
|
||||
var CKEditor: {
|
||||
BalloonEditor: {
|
||||
create(el: HTMLElement, config: {
|
||||
removePlugins?: string[];
|
||||
toolbar: {
|
||||
items: any[];
|
||||
},
|
||||
placeholder: string;
|
||||
mention: MentionConfig
|
||||
})
|
||||
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: {
|
||||
BalloonEditor: CKEditorInstance;
|
||||
DecoupledEditor: CKEditorInstance;
|
||||
EditorWatchdog: typeof CKWatchdog;
|
||||
};
|
||||
|
||||
var CKEditorInspector: {
|
||||
attach(editor: TextEditor);
|
||||
};
|
||||
|
||||
var CodeMirror: {
|
||||
@ -221,9 +261,24 @@ declare global {
|
||||
setOption(name: string, value: string);
|
||||
refresh();
|
||||
focus();
|
||||
getCursor(): { line: number, col: number, ch: number };
|
||||
setCursor(line: number, col: number);
|
||||
getSelection(): string;
|
||||
lineCount(): number;
|
||||
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: {
|
||||
@ -232,11 +287,22 @@ declare global {
|
||||
});
|
||||
}
|
||||
|
||||
type TextEditorElement = {};
|
||||
interface Range {
|
||||
toJSON(): object;
|
||||
getItems(): TextNode[];
|
||||
}
|
||||
interface Writer {
|
||||
setAttribute(name: string, value: string, el: TextEditorElement);
|
||||
createPositionAt(el: TextEditorElement, opt?: "end");
|
||||
setSelection(pos: number);
|
||||
setAttribute(name: string, value: string, el: CKNode);
|
||||
createPositionAt(el: CKNode, opt?: "end" | 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 {
|
||||
previousSibling?: TextNode;
|
||||
@ -252,29 +318,98 @@ declare global {
|
||||
interface TextPosition {
|
||||
textNode: TextNode;
|
||||
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 {
|
||||
create(el: HTMLElement, config: {
|
||||
removePlugins?: string[];
|
||||
toolbar: {
|
||||
items: any[];
|
||||
},
|
||||
placeholder: string;
|
||||
mention: MentionConfig
|
||||
});
|
||||
enableReadOnlyMode(reason: string);
|
||||
model: {
|
||||
document: {
|
||||
on(event: string, cb: () => void);
|
||||
getRoot(): TextEditorElement;
|
||||
getRoot(): CKNode;
|
||||
registerPostFixer(callback: (writer: Writer) => boolean);
|
||||
selection: {
|
||||
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)
|
||||
},
|
||||
editing: {
|
||||
view: {
|
||||
document: {
|
||||
on(event: string, cb: (event: {
|
||||
stop();
|
||||
}, data: {
|
||||
on(event: string, cb: (event: CKEvent, data: {
|
||||
preventDefault();
|
||||
}) => void, opts?: {
|
||||
priority: "high"
|
||||
});
|
||||
getRoot(): TextEditorElement
|
||||
getRoot(): CKNode
|
||||
},
|
||||
domRoots: {
|
||||
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;
|
||||
setData(data: string): void;
|
||||
getSelectedHtml(): string;
|
||||
removeSelection(): void;
|
||||
execute<T>(action: string, ...args: unknown[]): T;
|
||||
focus(): void;
|
||||
sourceElement: HTMLElement;
|
||||
}
|
||||
|
||||
interface EditingState {
|
||||
highlightedResult: string;
|
||||
results: unknown[];
|
||||
}
|
||||
|
||||
interface CKFindResult {
|
||||
results: {
|
||||
get(number): {
|
||||
marker: {
|
||||
getStart(): TextPosition;
|
||||
getRange(): number;
|
||||
};
|
||||
}
|
||||
} & [];
|
||||
}
|
||||
|
||||
interface MentionItem {
|
||||
action?: string;
|
||||
noteTitle?: string;
|
||||
@ -313,4 +487,23 @@ declare global {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -288,7 +288,7 @@ const ATTR_HELP: Record<string, Record<string, string>> = {
|
||||
};
|
||||
|
||||
interface AttributeDetailOpts {
|
||||
allAttributes: Attribute[];
|
||||
allAttributes?: Attribute[];
|
||||
attribute: Attribute;
|
||||
isOwned: boolean;
|
||||
x: number;
|
||||
@ -338,7 +338,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
private relatedNotesSpacedUpdate!: SpacedUpdate;
|
||||
private attribute!: Attribute;
|
||||
private allAttributes!: Attribute[];
|
||||
private allAttributes?: Attribute[];
|
||||
private attrType!: ReturnType<AttributeDetailWidget["getAttrType"]>;
|
||||
|
||||
async refresh() {
|
||||
@ -434,7 +434,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId
|
||||
|
||||
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes });
|
||||
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes ?? [] });
|
||||
this.updateRelatedNotes();
|
||||
});
|
||||
|
||||
@ -454,7 +454,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$deleteButton = this.$widget.find(".attr-delete-button");
|
||||
this.$deleteButton.on("click", async () => {
|
||||
await this.triggerCommand("updateAttributeList", {
|
||||
attributes: this.allAttributes.filter((attr) => attr !== this.attribute)
|
||||
attributes: (this.allAttributes || []).filter((attr) => attr !== this.attribute)
|
||||
});
|
||||
|
||||
await this.triggerCommand("saveAttributes");
|
||||
@ -714,7 +714,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.attribute.value = String(this.$inputValue.val());
|
||||
}
|
||||
|
||||
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes });
|
||||
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes ?? [] });
|
||||
}
|
||||
|
||||
buildDefinitionValue() {
|
||||
|
@ -5,6 +5,7 @@ import type CommandButtonWidget from "../buttons/command_button.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { NoteType } from "../../entities/fnote.js";
|
||||
import type { EventData, EventNames } from "../../components/app_context.js";
|
||||
import type NoteActionsWidget from "../buttons/note_actions.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="ribbon-container">
|
||||
@ -116,13 +117,15 @@ const TPL = `
|
||||
<div class="ribbon-body-container"></div>
|
||||
</div>`;
|
||||
|
||||
type ButtonWidget = (CommandButtonWidget | NoteActionsWidget);
|
||||
|
||||
export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
|
||||
private lastActiveComponentId?: string | null;
|
||||
private lastNoteType?: NoteType;
|
||||
|
||||
private ribbonWidgets: NoteContextAwareWidget[];
|
||||
private buttonWidgets: CommandButtonWidget[];
|
||||
private buttonWidgets: ButtonWidget[];
|
||||
private $tabContainer!: JQuery<HTMLElement>;
|
||||
private $buttonContainer!: JQuery<HTMLElement>;
|
||||
private $bodyContainer!: JQuery<HTMLElement>;
|
||||
@ -148,7 +151,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
return this;
|
||||
}
|
||||
|
||||
button(widget: CommandButtonWidget) {
|
||||
button(widget: ButtonWidget) {
|
||||
super.child(widget);
|
||||
|
||||
this.buttonWidgets.push(widget);
|
||||
|
@ -80,13 +80,13 @@ export default class AddLinkDialog extends BasicWidget {
|
||||
if (this.$autoComplete.getSelectedNotePath()) {
|
||||
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);
|
||||
} else if (this.$autoComplete.getSelectedExternalLink()) {
|
||||
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 {
|
||||
logError("No link to add.");
|
||||
}
|
||||
|
@ -28,7 +28,8 @@ const TPL = `
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export type ConfirmDialogCallback = (val?: false | ConfirmDialogOptions) => void;
|
||||
export type ConfirmDialogResult = false | ConfirmDialogOptions;
|
||||
export type ConfirmDialogCallback = (val?: ConfirmDialogResult) => void;
|
||||
|
||||
export interface ConfirmDialogOptions {
|
||||
confirmed: boolean;
|
||||
|
@ -1,10 +1,11 @@
|
||||
import utils, { escapeQuotes } from "../../services/utils.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 BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import { Modal, Tooltip } from "bootstrap";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||
@ -79,6 +80,20 @@ const TPL = `
|
||||
</div>`;
|
||||
|
||||
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() {
|
||||
super();
|
||||
|
||||
@ -87,7 +102,7 @@ export default class ImportDialog extends BasicWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
Modal.getOrCreateInstance(this.$widget);
|
||||
Modal.getOrCreateInstance(this.$widget[0]);
|
||||
|
||||
this.$form = this.$widget.find(".import-form");
|
||||
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.
|
||||
this.$importButton.attr("disabled", "disabled");
|
||||
|
||||
if (this.parentNoteId) {
|
||||
this.importIntoNote(this.parentNoteId);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
@ -124,7 +141,7 @@ export default class ImportDialog extends BasicWidget {
|
||||
});
|
||||
}
|
||||
|
||||
async showImportDialogEvent({ noteId }) {
|
||||
async showImportDialogEvent({ noteId }: EventData<"showImportDialog">) {
|
||||
this.parentNoteId = noteId;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async importIntoNote(parentNoteId) {
|
||||
const files = Array.from(this.$fileUploadInput[0].files); // shallow copy since we're resetting the upload button below
|
||||
async importIntoNote(parentNoteId: string) {
|
||||
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),
|
||||
shrinkImages: boolToString(this.$shrinkImagesCheckbox),
|
||||
textImportedAsText: boolToString(this.$textImportedAsTextCheckbox),
|
@ -5,6 +5,8 @@ import utils from "../../services/utils.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { Modal } from "bootstrap";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import type EditableTextTypeWidget from "../type_widgets/editable_text.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
@ -53,9 +55,15 @@ const TPL = `
|
||||
</div>`;
|
||||
|
||||
export default class IncludeNoteDialog extends BasicWidget {
|
||||
|
||||
private modal!: bootstrap.Modal;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $autoComplete!: JQuery<HTMLElement>;
|
||||
private textTypeWidget?: EditableTextTypeWidget;
|
||||
|
||||
doRender() {
|
||||
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.$autoComplete = this.$widget.find(".include-note-autocomplete");
|
||||
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;
|
||||
await this.refresh();
|
||||
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
|
||||
}
|
||||
|
||||
async refresh(widget) {
|
||||
async refresh() {
|
||||
this.$autoComplete.val("");
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
|
||||
hideGoToSelectedNoteButton: true,
|
||||
@ -89,17 +97,20 @@ export default class IncludeNoteDialog extends BasicWidget {
|
||||
noteAutocompleteService.showRecentNotes(this.$autoComplete);
|
||||
}
|
||||
|
||||
async includeNote(notePath) {
|
||||
async includeNote(notePath: string) {
|
||||
const noteId = treeService.getNoteIdFromUrl(notePath);
|
||||
if (!noteId) {
|
||||
return;
|
||||
}
|
||||
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,
|
||||
// so we'll just add an IMG tag
|
||||
this.textTypeWidget.addImage(noteId);
|
||||
this.textTypeWidget?.addImage(noteId);
|
||||
} else {
|
||||
this.textTypeWidget.addIncludeNote(noteId, boxSize);
|
||||
this.textTypeWidget?.addIncludeNote(noteId, boxSize);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
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() {
|
||||
super();
|
||||
|
||||
@ -36,7 +43,7 @@ export default class JumpToNoteDialog extends BasicWidget {
|
||||
|
||||
doRender() {
|
||||
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.$results = this.$widget.find(".jump-to-note-results");
|
||||
@ -54,17 +61,17 @@ export default class JumpToNoteDialog extends BasicWidget {
|
||||
|
||||
function reposition() {
|
||||
const offset = 100;
|
||||
const modalHeight = window.visualViewport.height - offset;
|
||||
const safeAreaInsetBottom = window.visualViewport.height - window.innerHeight;
|
||||
const modalHeight = (window.visualViewport?.height ?? 0) - offset;
|
||||
const safeAreaInsetBottom = (window.visualViewport?.height ?? 0) - window.innerHeight;
|
||||
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", () => {
|
||||
reposition();
|
||||
});
|
||||
|
||||
window.visualViewport.addEventListener("resize", () => {
|
||||
window.visualViewport?.addEventListener("resize", () => {
|
||||
reposition();
|
||||
});
|
||||
|
||||
@ -84,7 +91,7 @@ export default class JumpToNoteDialog extends BasicWidget {
|
||||
allowCreatingNotes: true,
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowJumpToSearchNotes: true,
|
||||
container: this.$results
|
||||
container: this.$results[0]
|
||||
})
|
||||
// clear any event listener added in previous invocation of this function
|
||||
.off("autocomplete:noteselected")
|
||||
@ -93,7 +100,7 @@ export default class JumpToNoteDialog extends BasicWidget {
|
||||
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
|
||||
@ -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)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const searchString = this.$autoComplete.val();
|
||||
const searchString = String(this.$autoComplete.val());
|
||||
|
||||
this.triggerCommand("searchNotes", { searchString });
|
||||
|
||||
this.modal.hide();
|
||||
}
|
||||
}
|
@ -27,7 +27,17 @@ const TPL = `
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
interface RenderMarkdownResponse {
|
||||
htmlContent: string;
|
||||
}
|
||||
|
||||
export default class MarkdownImportDialog extends BasicWidget {
|
||||
|
||||
private lastOpenedTs: number;
|
||||
private modal!: bootstrap.Modal;
|
||||
private $importTextarea!: JQuery<HTMLElement>;
|
||||
private $importButton!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -36,7 +46,7 @@ export default class MarkdownImportDialog extends BasicWidget {
|
||||
|
||||
doRender() {
|
||||
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.$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());
|
||||
}
|
||||
|
||||
async convertMarkdownToHtml(markdownContent) {
|
||||
const { htmlContent } = await server.post("other/render-markdown", { markdownContent });
|
||||
async convertMarkdownToHtml(markdownContent: string) {
|
||||
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 modelFragment = textEditor.data.toModel(viewFragment);
|
||||
@ -80,7 +93,7 @@ export default class MarkdownImportDialog extends BasicWidget {
|
||||
}
|
||||
|
||||
async sendForm() {
|
||||
const text = this.$importTextarea.val();
|
||||
const text = String(this.$importTextarea.val());
|
||||
|
||||
this.modal.hide();
|
||||
|
@ -6,6 +6,7 @@ import branchService from "../../services/branches.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
@ -39,6 +40,12 @@ const TPL = `
|
||||
</div>`;
|
||||
|
||||
export default class MoveToDialog extends BasicWidget {
|
||||
|
||||
private movedBranchIds: string[] | null;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
private $noteAutoComplete!: JQuery<HTMLElement>;
|
||||
private $noteList!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -58,7 +65,13 @@ export default class MoveToDialog extends BasicWidget {
|
||||
this.$widget.modal("hide");
|
||||
|
||||
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 {
|
||||
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;
|
||||
|
||||
utils.openDialog(this.$widget);
|
||||
@ -78,7 +91,14 @@ export default class MoveToDialog extends BasicWidget {
|
||||
|
||||
for (const branchId of this.movedBranchIds) {
|
||||
const branch = froca.getBranch(branchId);
|
||||
if (!branch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const note = await froca.getNote(branch.noteId);
|
||||
if (!note) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.$noteList.append($("<li>").text(note.title));
|
||||
}
|
||||
@ -87,12 +107,14 @@ export default class MoveToDialog extends BasicWidget {
|
||||
noteAutocompleteService.showRecentNotes(this.$noteAutoComplete);
|
||||
}
|
||||
|
||||
async moveNotesTo(parentBranchId) {
|
||||
async moveNotesTo(parentBranchId: string) {
|
||||
if (this.movedBranchIds) {
|
||||
await branchService.moveToParentNote(this.movedBranchIds, 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}`);
|
||||
}
|
||||
}
|
@ -9,10 +9,16 @@ import attributeService from "../services/attributes.js";
|
||||
import FindInText from "./find_in_text.js";
|
||||
import FindInCode from "./find_in_code.js";
|
||||
import FindInHtml from "./find_in_html.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
|
||||
const findWidgetDelayMillis = 200;
|
||||
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,
|
||||
// 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
|
||||
@ -92,6 +98,28 @@ const TPL = `
|
||||
</div>`;
|
||||
|
||||
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() {
|
||||
super();
|
||||
|
||||
@ -160,24 +188,24 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!["text", "code", "render"].includes(this.note.type)) {
|
||||
if (!["text", "code", "render"].includes(this.note?.type ?? "")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handler = await this.getHandler();
|
||||
|
||||
const isReadOnly = await this.noteContext.isReadOnly();
|
||||
const isReadOnly = await this.noteContext?.isReadOnly();
|
||||
|
||||
let selectedText = "";
|
||||
if (this.note.type === "code" && !isReadOnly) {
|
||||
if (this.note?.type === "code" && !isReadOnly && this.noteContext) {
|
||||
const codeEditor = await this.noteContext.getCodeEditor();
|
||||
selectedText = codeEditor.getSelection();
|
||||
} else {
|
||||
selectedText = window.getSelection().toString() || "";
|
||||
selectedText = window.getSelection()?.toString() || "";
|
||||
}
|
||||
this.$widget.show();
|
||||
this.$input.focus();
|
||||
if (["text", "code"].includes(this.note.type) && !isReadOnly) {
|
||||
if (["text", "code"].includes(this.note?.type ?? "") && !isReadOnly) {
|
||||
this.$replaceWidgetBox.show();
|
||||
} else {
|
||||
this.$replaceWidgetBox.hide();
|
||||
@ -208,16 +236,16 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async getHandler() {
|
||||
if (this.note.type === "render") {
|
||||
if (this.note?.type === "render") {
|
||||
return this.htmlHandler;
|
||||
}
|
||||
|
||||
const readOnly = await this.noteContext.isReadOnly();
|
||||
const readOnly = await this.noteContext?.isReadOnly();
|
||||
|
||||
if (readOnly) {
|
||||
return this.htmlHandler;
|
||||
} 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) {
|
||||
// Clear the previous timeout if any, it's ok if timeoutId is
|
||||
// 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
|
||||
// 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 = null;
|
||||
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
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async findNext(direction) {
|
||||
async findNext(direction: 1 | -1) {
|
||||
if (this.$totalFound.text() == "?") {
|
||||
await this.performFind();
|
||||
return;
|
||||
@ -268,16 +295,19 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
|
||||
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. */
|
||||
async performFind() {
|
||||
const searchTerm = this.$input.val();
|
||||
const searchTerm = String(this.$input.val());
|
||||
const matchCase = this.$caseSensitiveCheckbox.prop("checked");
|
||||
const wholeWord = this.$matchWordsCheckbox.prop("checked");
|
||||
|
||||
if (!this.handler) {
|
||||
return;
|
||||
}
|
||||
const { totalFound, currentFound } = await this.handler.performFind(searchTerm, matchCase, wholeWord);
|
||||
|
||||
this.$totalFound.text(totalFound);
|
||||
@ -297,28 +327,34 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.searchTerm = null;
|
||||
|
||||
await this.handler.findBoxClosed(totalFound, currentFound);
|
||||
await this.handler?.findBoxClosed(totalFound, currentFound);
|
||||
}
|
||||
}
|
||||
|
||||
async replace() {
|
||||
const replaceText = this.$replaceTextInput.val();
|
||||
const replaceText = String(this.$replaceTextInput.val());
|
||||
if (this.handler && "replace" in this.handler) {
|
||||
await this.handler.replace(replaceText);
|
||||
}
|
||||
}
|
||||
|
||||
async replaceAll() {
|
||||
const replaceText = this.$replaceTextInput.val();
|
||||
const replaceText = String(this.$replaceTextInput.val());
|
||||
if (this.handler && "replace" in this.handler) {
|
||||
await this.handler.replaceAll(replaceText);
|
||||
}
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && ["text", "code", "render"].includes(this.note.type);
|
||||
return super.isEnabled() && ["text", "code", "render"].includes(this.note?.type ?? "");
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }) {
|
||||
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
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();
|
||||
}
|
||||
}
|
@ -2,35 +2,54 @@
|
||||
// uses for highlighting matches, use the same one on CodeMirror
|
||||
// for consistency
|
||||
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_CSS_CLASSNAME = "ck-find-result";
|
||||
|
||||
// TODO: Deduplicate.
|
||||
interface Match {
|
||||
className: string;
|
||||
clear(): void;
|
||||
find(): {
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default class FindInCode {
|
||||
constructor(parent) {
|
||||
/** @property {FindWidget} */
|
||||
|
||||
private parent: FindWidget;
|
||||
private findResult?: Match[] | null;
|
||||
|
||||
constructor(parent: FindWidget) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
async getCodeEditor() {
|
||||
return this.parent.noteContext.getCodeEditor();
|
||||
return this.parent.noteContext?.getCodeEditor();
|
||||
}
|
||||
|
||||
async performFind(searchTerm, matchCase, wholeWord) {
|
||||
let findResult = null;
|
||||
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
|
||||
let findResult: Match[] | null = null;
|
||||
let totalFound = 0;
|
||||
let currentFound = -1;
|
||||
|
||||
// See https://codemirror.net/addon/search/searchcursor.js for tips
|
||||
const codeEditor = await this.getCodeEditor();
|
||||
if (!codeEditor) {
|
||||
return { totalFound: 0, currentFound: 0 };
|
||||
}
|
||||
|
||||
const doc = codeEditor.doc;
|
||||
const text = doc.getValue();
|
||||
|
||||
// Clear all markers
|
||||
if (this.findResult != null) {
|
||||
if (this.findResult) {
|
||||
codeEditor.operation(() => {
|
||||
for (let i = 0; i < this.findResult.length; ++i) {
|
||||
const marker = this.findResult[i];
|
||||
const findResult = this.findResult as Match[];
|
||||
for (let i = 0; i < findResult.length; ++i) {
|
||||
const marker = findResult[i];
|
||||
marker.clear();
|
||||
}
|
||||
});
|
||||
@ -49,7 +68,7 @@ export default class FindInCode {
|
||||
const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i"));
|
||||
let curLine = 0;
|
||||
let curChar = 0;
|
||||
let curMatch = null;
|
||||
let curMatch: RegExpExecArray | null = null;
|
||||
findResult = [];
|
||||
// All those markText take several seconds on e.g., this ~500-line
|
||||
// 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 };
|
||||
// or css = "color: #f3"
|
||||
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
|
||||
if (currentFound === -1) {
|
||||
@ -99,7 +118,7 @@ export default class FindInCode {
|
||||
this.findResult = findResult;
|
||||
|
||||
// Calculate curfound if not already, highlight it as selected
|
||||
if (totalFound > 0) {
|
||||
if (findResult && totalFound > 0) {
|
||||
currentFound = Math.max(0, currentFound);
|
||||
let marker = findResult[currentFound];
|
||||
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();
|
||||
if (!codeEditor || !this.findResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = codeEditor.doc;
|
||||
|
||||
//
|
||||
@ -137,18 +160,23 @@ export default class FindInCode {
|
||||
codeEditor.scrollIntoView(pos.from);
|
||||
}
|
||||
|
||||
async findBoxClosed(totalFound, currentFound) {
|
||||
async findBoxClosed(totalFound: number, currentFound: number) {
|
||||
const codeEditor = await this.getCodeEditor();
|
||||
|
||||
if (totalFound > 0) {
|
||||
if (codeEditor && totalFound > 0) {
|
||||
const doc = codeEditor.doc;
|
||||
const pos = this.findResult[currentFound].find();
|
||||
const pos = this.findResult?.[currentFound].find();
|
||||
// Note setting the selection sets the cursor to
|
||||
// the end of the selection and scrolls it into
|
||||
// view
|
||||
if (pos) {
|
||||
doc.setSelection(pos.from, pos.to);
|
||||
}
|
||||
// Clear all markers
|
||||
codeEditor.operation(() => {
|
||||
if (!this.findResult) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.findResult.length; ++i) {
|
||||
let marker = this.findResult[i];
|
||||
marker.clear();
|
||||
@ -157,9 +185,9 @@ export default class FindInCode {
|
||||
}
|
||||
this.findResult = null;
|
||||
|
||||
codeEditor.focus();
|
||||
codeEditor?.focus();
|
||||
}
|
||||
async replace(replaceText) {
|
||||
async replace(replaceText: string) {
|
||||
// this.findResult may be undefined and null
|
||||
if (!this.findResult || this.findResult.length === 0) {
|
||||
return;
|
||||
@ -178,8 +206,10 @@ export default class FindInCode {
|
||||
let marker = this.findResult[currentFound];
|
||||
let pos = marker.find();
|
||||
const codeEditor = await this.getCodeEditor();
|
||||
const doc = codeEditor.doc;
|
||||
const doc = codeEditor?.doc;
|
||||
if (doc) {
|
||||
doc.replaceRange(replaceText, pos.from, pos.to);
|
||||
}
|
||||
marker.clear();
|
||||
|
||||
let nextFound;
|
||||
@ -194,17 +224,21 @@ export default class FindInCode {
|
||||
}
|
||||
}
|
||||
}
|
||||
async replaceAll(replaceText) {
|
||||
async replaceAll(replaceText: string) {
|
||||
if (!this.findResult || this.findResult.length === 0) {
|
||||
return;
|
||||
}
|
||||
const codeEditor = await this.getCodeEditor();
|
||||
const doc = codeEditor.doc;
|
||||
codeEditor.operation(() => {
|
||||
const doc = codeEditor?.doc;
|
||||
codeEditor?.operation(() => {
|
||||
if (!this.findResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
|
||||
let marker = this.findResult[currentFound];
|
||||
let pos = marker.find();
|
||||
doc.replaceRange(replaceText, pos.from, pos.to);
|
||||
doc?.replaceRange(replaceText, pos.from, pos.to);
|
||||
marker.clear();
|
||||
}
|
||||
});
|
@ -4,28 +4,34 @@
|
||||
import libraryLoader from "../services/library_loader.js";
|
||||
import utils from "../services/utils.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_CSS_CLASSNAME = "ck-find-result";
|
||||
|
||||
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.currentIndex = 0;
|
||||
this.$results = null;
|
||||
}
|
||||
|
||||
async performFind(searchTerm, matchCase, wholeWord) {
|
||||
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
|
||||
|
||||
const $content = await this.parent.noteContext.getContentElement();
|
||||
const $content = await this.parent?.noteContext?.getContentElement();
|
||||
|
||||
const wholeWordChar = wholeWord ? "\\b" : "";
|
||||
const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi");
|
||||
|
||||
return new Promise((res) => {
|
||||
$content.unmark({
|
||||
return new Promise<FindResult>((res) => {
|
||||
$content?.unmark({
|
||||
done: () => {
|
||||
$content.markRegExp(regExp, {
|
||||
element: "span",
|
||||
@ -48,8 +54,8 @@ export default class FindInHtml {
|
||||
});
|
||||
}
|
||||
|
||||
async findNext(direction, currentFound, nextFound) {
|
||||
if (this.$results.length) {
|
||||
async findNext(direction: -1 | 1, currentFound: number, nextFound: number) {
|
||||
if (this.$results?.length) {
|
||||
this.currentIndex += direction;
|
||||
|
||||
if (this.currentIndex < 0) {
|
||||
@ -64,13 +70,15 @@ export default class FindInHtml {
|
||||
}
|
||||
}
|
||||
|
||||
async findBoxClosed(totalFound, currentFound) {
|
||||
const $content = await this.parent.noteContext.getContentElement();
|
||||
async findBoxClosed(totalFound: number, currentFound: number) {
|
||||
const $content = await this.parent?.noteContext?.getContentElement();
|
||||
if ($content) {
|
||||
$content.unmark();
|
||||
}
|
||||
}
|
||||
|
||||
async jumpTo() {
|
||||
if (this.$results.length) {
|
||||
if (this.$results?.length) {
|
||||
const offsetTop = 100;
|
||||
const $current = this.$results.eq(this.currentIndex);
|
||||
this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
|
||||
@ -79,10 +87,11 @@ export default class FindInHtml {
|
||||
$current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
|
||||
const position = $current.position().top - offsetTop;
|
||||
|
||||
const $content = await this.parent.noteContext.getContentElement();
|
||||
const $contentWiget = appContext.getComponentByEl($content);
|
||||
|
||||
$contentWiget.triggerCommand("scrollContainerTo", { position });
|
||||
const $content = await this.parent.noteContext?.getContentElement();
|
||||
if ($content) {
|
||||
const $contentWidget = appContext.getComponentByEl($content[0]);
|
||||
$contentWidget.triggerCommand("scrollContainerTo", { position });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
constructor(parent) {
|
||||
/** @property {FindWidget} */
|
||||
|
||||
private parent: FindWidget;
|
||||
private findResult?: CKFindResult | null;
|
||||
private editingState?: EditingState;
|
||||
|
||||
constructor(parent: FindWidget) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
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
|
||||
// the counters updated
|
||||
const textEditor = await this.getTextEditor();
|
||||
if (!textEditor) {
|
||||
return { currentFound: 0, totalFound: 0 };
|
||||
}
|
||||
|
||||
const model = textEditor.model;
|
||||
let findResult = null;
|
||||
let totalFound = 0;
|
||||
@ -31,14 +52,14 @@ export default class FindInText {
|
||||
// let m = text.match(re);
|
||||
// totalFound = m ? m.length : 0;
|
||||
const options = { matchCase: matchCase, wholeWords: wholeWord };
|
||||
findResult = textEditor.execute("find", searchTerm, options);
|
||||
findResult = textEditor.execute<CKFindResult>("find", searchTerm, options);
|
||||
totalFound = findResult.results.length;
|
||||
// Find the result beyond the cursor
|
||||
const cursorPos = model.document.selection.getLastPosition();
|
||||
for (let i = 0; i < findResult.results.length; ++i) {
|
||||
const marker = findResult.results.get(i).marker;
|
||||
const fromPos = marker.getStart();
|
||||
if (fromPos.compareWith(cursorPos) !== "before") {
|
||||
if (cursorPos && fromPos.compareWith(cursorPos) !== "before") {
|
||||
currentFound = i;
|
||||
break;
|
||||
}
|
||||
@ -54,7 +75,7 @@ export default class FindInText {
|
||||
// XXX Do this accessing the private data?
|
||||
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
|
||||
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();
|
||||
|
||||
// There are no parameters for findNext/findPrev
|
||||
@ -72,20 +93,23 @@ export default class FindInText {
|
||||
// curFound wrap around above assumes findNext and
|
||||
// findPrevious wraparound, which is what they do
|
||||
if (direction > 0) {
|
||||
textEditor.execute("findNext");
|
||||
textEditor?.execute("findNext");
|
||||
} else {
|
||||
textEditor.execute("findPrevious");
|
||||
textEditor?.execute("findPrevious");
|
||||
}
|
||||
}
|
||||
|
||||
async findBoxClosed(totalFound, currentFound) {
|
||||
async findBoxClosed(totalFound: number, currentFound: number) {
|
||||
const textEditor = await this.getTextEditor();
|
||||
if (!textEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalFound > 0) {
|
||||
// Clear the markers and set the caret to the
|
||||
// current occurrence
|
||||
const model = textEditor.model;
|
||||
const range = this.findResult.results.get(currentFound).marker.getRange();
|
||||
const range = this.findResult?.results?.get(currentFound).marker.getRange();
|
||||
// From
|
||||
// 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
|
||||
@ -93,9 +117,11 @@ export default class FindInText {
|
||||
let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
|
||||
findAndReplaceEditing.state.clear(model);
|
||||
findAndReplaceEditing.stop();
|
||||
if (range) {
|
||||
model.change((writer) => {
|
||||
writer.setSelection(range, 0);
|
||||
});
|
||||
}
|
||||
textEditor.editing.view.scrollToTheSelection();
|
||||
}
|
||||
|
||||
@ -104,17 +130,17 @@ export default class FindInText {
|
||||
textEditor.focus();
|
||||
}
|
||||
|
||||
async replace(replaceText) {
|
||||
async replace(replaceText: string) {
|
||||
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
|
||||
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) {
|
||||
const textEditor = await this.getTextEditor();
|
||||
textEditor.execute("replaceAll", replaceText, this.editingState.results);
|
||||
textEditor?.execute("replaceAll", replaceText, this.editingState.results);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,10 +5,9 @@ import type NoteContext from "../components/note_context.js";
|
||||
|
||||
/**
|
||||
* This widget allows for changing and updating depending on the active note.
|
||||
* @extends {BasicWidget}
|
||||
*/
|
||||
class NoteContextAwareWidget extends BasicWidget {
|
||||
protected noteContext?: NoteContext;
|
||||
noteContext?: NoteContext;
|
||||
|
||||
isNoteContext(ntxId: string | string[] | null | undefined) {
|
||||
if (Array.isArray(ntxId)) {
|
||||
|
@ -5,6 +5,7 @@ import openService from "../../services/open.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="file-properties-widget">
|
||||
@ -66,6 +67,16 @@ const TPL = `
|
||||
</div>`;
|
||||
|
||||
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() {
|
||||
return "fileProperties";
|
||||
}
|
||||
@ -99,8 +110,8 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget {
|
||||
this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision");
|
||||
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
|
||||
|
||||
this.$downloadButton.on("click", () => openService.downloadFileNote(this.noteId));
|
||||
this.$openButton.on("click", () => openService.openNoteExternally(this.noteId, this.note.mime));
|
||||
this.$downloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId));
|
||||
this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime));
|
||||
|
||||
this.$uploadNewRevisionButton.on("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();
|
||||
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$fileNoteId.text(note.noteId);
|
||||
this.$fileName.text(note.getLabelValue("originalFileName") || "?");
|
||||
this.$fileType.text(note.mime);
|
||||
|
||||
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
|
||||
this.$openButton.toggle(!note.isProtected);
|
@ -4,6 +4,7 @@ import toastService from "../../services/toast.js";
|
||||
import openService from "../../services/open.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="image-properties">
|
||||
@ -50,6 +51,16 @@ const TPL = `
|
||||
</div>`;
|
||||
|
||||
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() {
|
||||
return "imageProperties";
|
||||
}
|
||||
@ -76,7 +87,7 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
|
||||
this.contentSized();
|
||||
|
||||
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.$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.$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.on("click", () => openService.downloadFileNote(this.noteId));
|
||||
this.$imageDownloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId));
|
||||
|
||||
this.$uploadNewRevisionButton.on("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();
|
||||
|
||||
const blob = await this.note.getBlob();
|
||||
const blob = await this.note?.getBlob();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ import AttributeDetailWidget from "../attribute_widgets/attribute_detail.js";
|
||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="inherited-attributes-widget">
|
||||
@ -23,6 +25,11 @@ const TPL = `
|
||||
</div>`;
|
||||
|
||||
export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
|
||||
private attributeDetailWidget: AttributeDetailWidget;
|
||||
|
||||
private $container!: JQuery<HTMLElement>;
|
||||
|
||||
get name() {
|
||||
return "inheritedAttributes";
|
||||
}
|
||||
@ -34,7 +41,6 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/** @type {AttributeDetailWidget} */
|
||||
this.attributeDetailWidget = new AttributeDetailWidget().contentSized().setParent(this);
|
||||
|
||||
this.child(this.attributeDetailWidget);
|
||||
@ -42,7 +48,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
|
||||
getTitle() {
|
||||
return {
|
||||
show: !this.note.isLaunchBarConfig(),
|
||||
show: !this.note?.isLaunchBarConfig(),
|
||||
title: t("inherited_attribute_list.title"),
|
||||
icon: "bx bx-list-plus"
|
||||
};
|
||||
@ -56,7 +62,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
this.$widget.append(this.attributeDetailWidget.render());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$container.empty();
|
||||
|
||||
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);
|
||||
|
||||
attrs.sort((a, b) => {
|
||||
@ -105,7 +111,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
return attrs;
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }) {
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
||||
this.refresh();
|
||||
}
|
@ -7,6 +7,10 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import options from "../../services/options.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 = `
|
||||
<div class="promoted-attributes-widget">
|
||||
@ -60,12 +64,20 @@ const TPL = `
|
||||
<div class="promoted-attributes-container"></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 works without many issues (apart from autocomplete), but it should be kept in mind when changing things
|
||||
* and testing.
|
||||
*/
|
||||
export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $container!: JQuery<HTMLElement>;
|
||||
|
||||
get name() {
|
||||
return "promotedAttributes";
|
||||
}
|
||||
@ -80,7 +92,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
this.$container = this.$widget.find(".promoted-attributes-container");
|
||||
}
|
||||
|
||||
getTitle(note) {
|
||||
getTitle(note: FNote) {
|
||||
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
||||
|
||||
if (promotedDefAttrs.length === 0) {
|
||||
@ -95,7 +107,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
};
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$container.empty();
|
||||
|
||||
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
||||
@ -116,7 +128,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation";
|
||||
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) {
|
||||
valueAttrs.push({
|
||||
@ -134,9 +146,11 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
for (const valueAttr of valueAttrs) {
|
||||
const $cell = await this.createPromotedAttributeCell(definitionAttr, valueAttr, valueName);
|
||||
|
||||
if ($cell) {
|
||||
$cells.push($cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we replace the whole content in one step, so there can't be any race conditions
|
||||
// (previously we saw promoted attributes doubling)
|
||||
@ -144,14 +158,14 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
this.toggleInt(true);
|
||||
}
|
||||
|
||||
async createPromotedAttributeCell(definitionAttr, valueAttr, valueName) {
|
||||
async createPromotedAttributeCell(definitionAttr: FAttribute, valueAttr: Attribute, valueName: string) {
|
||||
const definition = definitionAttr.getDefinition();
|
||||
const id = `value-${valueAttr.attributeId}`;
|
||||
|
||||
const $input = $("<input>")
|
||||
.prop("tabindex", 200 + definitionAttr.position)
|
||||
.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-name", valueAttr.name)
|
||||
.prop("value", valueAttr.value)
|
||||
@ -161,7 +175,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
.on("change", (event) => this.promotedAttributeChanged(event));
|
||||
|
||||
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">')
|
||||
.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
|
||||
if (utils.isDesktop()) {
|
||||
// no need to await for this, can be done asynchronously
|
||||
server.get(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((attributeValues) => {
|
||||
if (attributeValues.length === 0) {
|
||||
server.get<string[]>(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributeValues) => {
|
||||
if (_attributeValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
attributeValues = attributeValues.map((attribute) => ({ value: attribute }));
|
||||
const attributeValues = _attributeValues.map((attribute) => ({ value: attribute }));
|
||||
|
||||
$input.autocomplete(
|
||||
{
|
||||
@ -245,11 +259,11 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
const $openButton = $("<span>")
|
||||
.addClass("input-group-text open-external-link-button bx bx-window-open")
|
||||
.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);
|
||||
} 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") {
|
||||
if (valueAttr.value) {
|
||||
@ -290,9 +304,11 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
valueName
|
||||
);
|
||||
|
||||
if ($new) {
|
||||
$wrapper.after($new);
|
||||
|
||||
$new.find("input").trigger("focus");
|
||||
}
|
||||
});
|
||||
|
||||
const $removeButton = $("<span>")
|
||||
@ -320,8 +336,10 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
valueName
|
||||
);
|
||||
|
||||
if ($new) {
|
||||
$wrapper.after($new);
|
||||
}
|
||||
}
|
||||
|
||||
$wrapper.remove();
|
||||
});
|
||||
@ -332,7 +350,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
async promotedAttributeChanged(event) {
|
||||
async promotedAttributeChanged(event: JQuery.TriggeredEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) {
|
||||
const $attr = $(event.target);
|
||||
|
||||
let value;
|
||||
@ -347,7 +365,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
value = $attr.val();
|
||||
}
|
||||
|
||||
const result = await server.put(
|
||||
const result = await server.put<AttributeResult>(
|
||||
`notes/${this.noteId}/attribute`,
|
||||
{
|
||||
attributeId: $attr.attr("data-attribute-id"),
|
||||
@ -365,7 +383,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
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))) {
|
||||
this.refresh();
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { t } from "../services/i18n.js";
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import contextMenu from "../menus/context_menu.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const TPL = `<div class="spacer"></div>`;
|
||||
@ -26,7 +26,7 @@ export default class SpacerWidget extends BasicWidget {
|
||||
this.$widget.on("contextmenu", (e) => {
|
||||
this.$widget.tooltip("hide");
|
||||
|
||||
contextMenu.show({
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (utils.isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
||||
|
@ -4,7 +4,7 @@ import BasicWidget from "./basic_widget.js";
|
||||
import contextMenu from "../menus/context_menu.js";
|
||||
import utils from "../services/utils.js";
|
||||
import keyboardActionService from "../services/keyboard_actions.js";
|
||||
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
|
||||
import appContext, { type CommandNames, type CommandListenerData, type EventData } from "../components/app_context.js";
|
||||
import froca from "../services/froca.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import type NoteContext from "../components/note_context.js";
|
||||
@ -268,7 +268,7 @@ export default class TabRowWidget extends BasicWidget {
|
||||
|
||||
const ntxId = $(e.target).closest(".note-tab").attr("data-ntx-id");
|
||||
|
||||
contextMenu.show({
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 linkService from "../../services/link.js";
|
||||
import contentRenderer from "../../services/content_renderer.js";
|
||||
@ -13,7 +13,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
||||
this.refreshCodeBlockOptions();
|
||||
}
|
||||
|
||||
setupImageOpening(singleClickOpens) {
|
||||
setupImageOpening(singleClickOpens: boolean) {
|
||||
this.$widget.on("dblclick", "img", (e) => this.openImageInCurrentTab($(e.target)));
|
||||
|
||||
this.$widget.on("click", "img", (e) => {
|
||||
@ -29,27 +29,27 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
||||
});
|
||||
}
|
||||
|
||||
async openImageInCurrentTab($img) {
|
||||
const { noteId, viewScope } = await this.parseFromImage($img);
|
||||
async openImageInCurrentTab($img: JQuery<HTMLElement>) {
|
||||
const parsedImage = await this.parseFromImage($img);
|
||||
|
||||
if (noteId) {
|
||||
appContext.tabManager.getActiveContext().setNote(noteId, { viewScope });
|
||||
if (parsedImage) {
|
||||
appContext.tabManager.getActiveContext()?.setNote(parsedImage.noteId, { viewScope: parsedImage.viewScope });
|
||||
} else {
|
||||
window.open($img.prop("src"), "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
async openImageInNewTab($img) {
|
||||
const { noteId, viewScope } = await this.parseFromImage($img);
|
||||
async openImageInNewTab($img: JQuery<HTMLElement>) {
|
||||
const parsedImage = await this.parseFromImage($img);
|
||||
|
||||
if (noteId) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(noteId, { viewScope });
|
||||
if (parsedImage) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(parsedImage.noteId, { viewScope: parsedImage.viewScope });
|
||||
} else {
|
||||
window.open($img.prop("src"), "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
async parseFromImage($img) {
|
||||
async parseFromImage($img: JQuery<HTMLElement>) {
|
||||
const imgSrc = $img.prop("src");
|
||||
|
||||
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);
|
||||
|
||||
return {
|
||||
noteId: attachment.ownerId,
|
||||
noteId: attachment?.ownerId,
|
||||
viewScope: {
|
||||
viewMode: "attachments",
|
||||
attachmentId: attachmentId
|
||||
@ -77,7 +77,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
||||
return null;
|
||||
}
|
||||
|
||||
async loadIncludedNote(noteId, $el) {
|
||||
async loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>) {
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
refreshIncludedNote($container, noteId) {
|
||||
refreshIncludedNote($container: JQuery<HTMLElement>, noteId: string) {
|
||||
if ($container) {
|
||||
$container.find(`section[data-note-id="${noteId}"]`).each((_, el) => {
|
||||
this.loadIncludedNote(noteId, $(el));
|
||||
@ -114,7 +114,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
||||
this.$widget.toggleClass("word-wrap", wordWrap);
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }) {
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.isOptionReloaded("codeBlockWordWrap")) {
|
||||
this.refreshCodeBlockOptions();
|
||||
}
|
@ -12,7 +12,7 @@ import library_loader from "../../../services/library_loader.js";
|
||||
import mime_types from "../../../services/mime_types.js";
|
||||
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||
|
||||
export async function initSyntaxHighlighting(editor) {
|
||||
export async function initSyntaxHighlighting(editor: TextEditor) {
|
||||
if (!isSyntaxHighlightEnabled) {
|
||||
return;
|
||||
}
|
||||
@ -25,39 +25,38 @@ const HIGHLIGHT_MAX_BLOCK_COUNT = 500;
|
||||
|
||||
const tag = "SyntaxHighlightWidget";
|
||||
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")) {
|
||||
warn = console.warn.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let info = function () {};
|
||||
let info = function (...args: unknown[]) {};
|
||||
if (debugLevel >= debugLevels.indexOf("info")) {
|
||||
info = console.info.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let log = function () {};
|
||||
let log = function (...args: unknown[]) {};
|
||||
if (debugLevel >= debugLevels.indexOf("log")) {
|
||||
log = console.log.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let dbg = function () {};
|
||||
let dbg = function (...args: unknown[]) {};
|
||||
if (debugLevel >= debugLevels.indexOf("debug")) {
|
||||
dbg = console.debug.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
function assert(e, msg) {
|
||||
function assert(e: boolean, msg?: string) {
|
||||
console.assert(e, tag + ": " + msg);
|
||||
}
|
||||
|
||||
// TODO: Should this be scoped to note?
|
||||
let markerCounter = 0;
|
||||
|
||||
function initTextEditor(textEditor) {
|
||||
function initTextEditor(textEditor: TextEditor) {
|
||||
log("initTextEditor");
|
||||
|
||||
let widget = this;
|
||||
const document = textEditor.model.document;
|
||||
|
||||
// Create a conversion from model to view that converts
|
||||
@ -100,7 +99,7 @@ function initTextEditor(textEditor) {
|
||||
// See
|
||||
// https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54
|
||||
const changes = document.differ.getChanges();
|
||||
let dirtyCodeBlocks = new Set();
|
||||
let dirtyCodeBlocks = new Set<CKNode>();
|
||||
|
||||
for (const change of changes) {
|
||||
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
|
||||
* 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()));
|
||||
const model = codeBlock.root.document.model;
|
||||
|
||||
@ -291,16 +290,16 @@ function highlightCodeBlock(codeBlock, writer) {
|
||||
iHtml = html.indexOf(">", iHtml) + 1;
|
||||
|
||||
// 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 });
|
||||
} else if (html[iHtml] == "<" && html[iHtml + 1] == "/") {
|
||||
// Done with this span, pop the span and mark the range
|
||||
iHtml = html.indexOf(">", iHtml + 1) + 1;
|
||||
|
||||
let stackTop = spanStack.pop();
|
||||
let posStart = stackTop.posStart;
|
||||
let className = stackTop.className;
|
||||
let posEnd = writer.createPositionAt(codeBlock, child.startOffset + iChildText);
|
||||
let posStart = stackTop?.posStart;
|
||||
let className = stackTop?.className;
|
||||
let posEnd = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
|
||||
let range = writer.createRange(posStart, posEnd);
|
||||
let markerName = "hljs:" + className + ":" + markerCounter;
|
||||
// Use an incrementing number for the uniqueId, random of
|
@ -8,7 +8,7 @@ import froca from "../../services/froca.js";
|
||||
import noteCreateService from "../../services/note_create.js";
|
||||
import AbstractTextTypeWidget from "./abstract_text_type_widget.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 { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
|
||||
import options from "../../services/options.js";
|
||||
@ -16,14 +16,15 @@ import toast from "../../services/toast.js";
|
||||
import { getMermaidConfig } from "../mermaid.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js";
|
||||
import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const ENABLE_INSPECTOR = false;
|
||||
|
||||
const mentionSetup = {
|
||||
const mentionSetup: MentionConfig = {
|
||||
feeds: [
|
||||
{
|
||||
marker: "@",
|
||||
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||
itemRenderer: (item) => {
|
||||
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.
|
||||
*/
|
||||
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
|
||||
private contentLanguage?: string | null;
|
||||
private watchdog!: CKWatchdog;
|
||||
|
||||
private $editor!: JQuery<HTMLElement>;
|
||||
|
||||
static getType() {
|
||||
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) {
|
||||
finalConfig.language = {
|
||||
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 notificationsPlugin = editor.plugins.get("Notification");
|
||||
notificationsPlugin.on("show:warning", (evt, data) => {
|
||||
notificationsPlugin.on("show:warning", (evt: CKEvent, data: PluginEventData) => {
|
||||
const title = data.title;
|
||||
const message = data.message.message;
|
||||
|
||||
@ -246,6 +253,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
editor.model.document.on("change:data", () => this.spacedUpdate.scheduleUpdate());
|
||||
|
||||
if (glob.isDev && ENABLE_INSPECTOR) {
|
||||
//@ts-expect-error TODO: Check if this still works.
|
||||
await import(/* webpackIgnore: true */ "../../../libraries/ckeditor/inspector.js");
|
||||
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();
|
||||
|
||||
await this.spacedUpdate.allowUpdateWithoutChange(async () => {
|
||||
const data = blob.content || "";
|
||||
const newContentLanguage = this.note.getLabelValue("language");
|
||||
const data = blob?.content || "";
|
||||
const newContentLanguage = this.note?.getLabelValue("language");
|
||||
if (this.contentLanguage !== newContentLanguage) {
|
||||
await this.reinitialize(data);
|
||||
} else {
|
||||
@ -334,7 +342,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
this.addTextToEditor(dateString);
|
||||
}
|
||||
|
||||
async addLinkToEditor(linkHref, linkTitle) {
|
||||
async addLinkToEditor(linkHref: string, linkTitle: string) {
|
||||
await this.initialized;
|
||||
|
||||
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;
|
||||
|
||||
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()) {
|
||||
return;
|
||||
}
|
||||
@ -360,7 +368,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
this.addTextToEditor(text);
|
||||
}
|
||||
|
||||
async addLink(notePath, linkTitle, externalLink = false) {
|
||||
async addLink(notePath: string, linkTitle: string | null, externalLink: boolean = false) {
|
||||
await this.initialized;
|
||||
|
||||
if (linkTitle) {
|
||||
@ -384,7 +392,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
return !selection.isCollapsed;
|
||||
}
|
||||
|
||||
async executeWithTextEditorEvent({ callback, resolve, ntxId }) {
|
||||
async executeWithTextEditorEvent({ callback, resolve, ntxId }: EventData<"executeWithTextEditor">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
@ -428,7 +436,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
const notePath = selectedElement.getAttribute("notePath");
|
||||
|
||||
if (notePath) {
|
||||
await appContext.tabManager.getActiveContext().setNote(notePath);
|
||||
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -441,7 +449,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
const notePath = link.getNotePathFromUrl(selectedLinkUrl);
|
||||
|
||||
if (notePath) {
|
||||
await appContext.tabManager.getActiveContext().setNote(notePath);
|
||||
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||
} else {
|
||||
window.open(selectedLinkUrl, "_blank");
|
||||
}
|
||||
@ -451,7 +459,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
this.triggerCommand("showIncludeNoteDialog", { textTypeWidget: this });
|
||||
}
|
||||
|
||||
addIncludeNote(noteId, boxSize) {
|
||||
addIncludeNote(noteId: string, boxSize?: string) {
|
||||
this.watchdog.editor.model.change((writer) => {
|
||||
// Insert <includeNote>*</includeNote> at the current selection position
|
||||
// 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);
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.watchdog.editor.model.change((writer) => {
|
||||
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, {
|
||||
activate: false,
|
||||
title: title
|
||||
});
|
||||
|
||||
if (!resp) {
|
||||
if (!resp || !resp.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
return resp.note.getBestNotePathString();
|
||||
}
|
||||
|
||||
async refreshIncludedNoteEvent({ noteId }) {
|
||||
async refreshIncludedNoteEvent({ noteId }: EventData<"refreshIncludedNote">) {
|
||||
this.refreshIncludedNote(this.$editor, noteId);
|
||||
}
|
||||
|
||||
async reinitialize(data) {
|
||||
async reinitialize(data: string) {
|
||||
if (!this.watchdog) {
|
||||
return;
|
||||
}
|
@ -114,7 +114,9 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
||||
this.$content.find("section").each((_, el) => {
|
||||
const noteId = $(el).attr("data-note-id");
|
||||
|
||||
if (noteId) {
|
||||
this.loadIncludedNote(noteId, $(el));
|
||||
}
|
||||
});
|
||||
|
||||
if (this.$content.find("span.math-tex").length > 0) {
|
||||
|
@ -1,17 +1,36 @@
|
||||
import server from "../../services/server.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import contextMenu from "../../menus/context_menu.js";
|
||||
import toastService from "../../services/toast.js";
|
||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import appContext, { type EventData } from "../../components/app_context.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
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",
|
||||
{
|
||||
@ -92,7 +111,62 @@ const TPL = `
|
||||
|
||||
let containerCounter = 1;
|
||||
|
||||
interface Clipboard {
|
||||
noteId: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface MapData {
|
||||
notes: {
|
||||
noteId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}[];
|
||||
transform: {
|
||||
x: number,
|
||||
y: number,
|
||||
scale: number
|
||||
}
|
||||
}
|
||||
|
||||
export type RelationType = "uniDirectional" | "biDirectional" | "inverse";
|
||||
|
||||
interface Relation {
|
||||
name: string;
|
||||
attributeId: string;
|
||||
sourceNoteId: string;
|
||||
targetNoteId: string;
|
||||
type: RelationType;
|
||||
render: boolean;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate.
|
||||
interface PostNoteResponse {
|
||||
note: {
|
||||
noteId: string;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Deduplicate.
|
||||
interface RelationMapPostResponse {
|
||||
relations: Relation[];
|
||||
inverseRelations: Record<string, string>;
|
||||
noteTitles: Record<string, string>;
|
||||
}
|
||||
|
||||
type MenuCommands = "openInNewTab" | "remove" | "editTitle";
|
||||
|
||||
export default class RelationMapTypeWidget extends TypeWidget {
|
||||
|
||||
private clipboard?: Clipboard | null;
|
||||
private jsPlumbInstance?: import("jsplumb").jsPlumbInstance | null;
|
||||
private pzInstance?: PanZoom | null;
|
||||
private mapData?: MapData | null;
|
||||
private relations?: Relation[] | null;
|
||||
|
||||
private $relationMapContainer!: JQuery<HTMLElement>;
|
||||
private $relationMapWrapper!: JQuery<HTMLElement>;
|
||||
|
||||
static getType() {
|
||||
return "relationMap";
|
||||
}
|
||||
@ -109,7 +183,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
|
||||
this.$relationMapWrapper = this.$widget.find(".relation-map-wrapper");
|
||||
this.$relationMapWrapper.on("click", (event) => {
|
||||
if (this.clipboard) {
|
||||
if (this.clipboard && this.mapData) {
|
||||
let { x, y } = this.getMousePosition(event);
|
||||
|
||||
// modifying position so that the cursor is on the top-center of the box
|
||||
@ -130,7 +204,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
|
||||
this.$relationMapContainer.attr("id", "relation-map-container-" + containerCounter++);
|
||||
this.$relationMapContainer.on("contextmenu", ".note-box", (e) => {
|
||||
contextMenu.show({
|
||||
contextMenu.show<MenuCommands>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
@ -150,15 +224,15 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.$widget.on("dragover", (ev) => ev.preventDefault());
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
async contextMenuHandler(command, originalTarget) {
|
||||
async contextMenuHandler(command: MenuCommands | undefined, originalTarget: HTMLElement) {
|
||||
const $noteBox = $(originalTarget).closest(".note-box");
|
||||
const $title = $noteBox.find(".title a");
|
||||
const noteId = this.idToNoteId($noteBox.prop("id"));
|
||||
@ -168,11 +242,11 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
} else if (command === "remove") {
|
||||
const result = await dialogService.confirmDeleteNoteBoxWithNote($title.text());
|
||||
|
||||
if (!result.confirmed) {
|
||||
if (typeof result !== "object" || !result.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jsPlumbInstance.remove(this.noteIdToId(noteId));
|
||||
this.jsPlumbInstance?.remove(this.noteIdToId(noteId));
|
||||
|
||||
if (result.isDeleteNoteChecked) {
|
||||
const taskId = utils.randomString(10);
|
||||
@ -180,9 +254,13 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`);
|
||||
}
|
||||
|
||||
if (this.mapData) {
|
||||
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId !== noteId);
|
||||
}
|
||||
|
||||
if (this.relations) {
|
||||
this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId);
|
||||
}
|
||||
|
||||
this.saveData();
|
||||
} 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 {
|
||||
this.mapData = JSON.parse(blob.content);
|
||||
} catch (e) {
|
||||
@ -227,20 +305,20 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
noteIdToId(noteId) {
|
||||
noteIdToId(noteId: string) {
|
||||
return `rel-map-note-${noteId}`;
|
||||
}
|
||||
|
||||
idToNoteId(id) {
|
||||
idToNoteId(id: string) {
|
||||
return id.substr(13);
|
||||
}
|
||||
|
||||
async doRefresh(note) {
|
||||
async doRefresh(note: FNote) {
|
||||
await this.loadMapData();
|
||||
|
||||
this.initJsPlumbInstance();
|
||||
await this.initJsPlumbInstance();
|
||||
|
||||
this.initPanZoom();
|
||||
await this.initPanZoom();
|
||||
|
||||
this.loadNotesAndRelations();
|
||||
}
|
||||
@ -248,15 +326,19 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
clearMap() {
|
||||
// delete all endpoints and connections
|
||||
// this is done at this point (after async operations) to reduce flicker to the minimum
|
||||
this.jsPlumbInstance.deleteEveryEndpoint();
|
||||
this.jsPlumbInstance?.deleteEveryEndpoint();
|
||||
|
||||
// without this, we still end up with note boxes remaining in the canvas
|
||||
this.$relationMapContainer.empty();
|
||||
}
|
||||
|
||||
async loadNotesAndRelations() {
|
||||
if (!this.mapData || !this.jsPlumbInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteIds = this.mapData.notes.map((note) => note.noteId);
|
||||
const data = await server.post("relation-map", { noteIds, relationMapNoteId: this.noteId });
|
||||
const data = await server.post<RelationMapPostResponse>("relation-map", { noteIds, relationMapNoteId: this.noteId });
|
||||
|
||||
this.relations = [];
|
||||
|
||||
@ -282,6 +364,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId in data.noteTitles);
|
||||
|
||||
this.jsPlumbInstance.batch(async () => {
|
||||
if (!this.jsPlumbInstance || !this.mapData || !this.relations) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearMap();
|
||||
|
||||
for (const note of this.mapData.notes) {
|
||||
@ -301,6 +387,8 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
type: relation.type
|
||||
});
|
||||
|
||||
// TODO: Does this actually do anything.
|
||||
//@ts-expect-error
|
||||
connection.id = relation.attributeId;
|
||||
|
||||
if (relation.type === "inverse") {
|
||||
@ -315,30 +403,37 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
});
|
||||
}
|
||||
|
||||
initPanZoom() {
|
||||
async initPanZoom() {
|
||||
if (this.pzInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panzoom = (await import("panzoom")).default;
|
||||
this.pzInstance = panzoom(this.$relationMapContainer[0], {
|
||||
maxZoom: 2,
|
||||
minZoom: 0.3,
|
||||
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
|
||||
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working
|
||||
return e.altKey;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.pzInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pzInstance.on("transform", () => {
|
||||
// gets triggered on any transform change
|
||||
this.jsPlumbInstance.setZoom(this.getZoom());
|
||||
this.jsPlumbInstance?.setZoom(this.getZoom());
|
||||
|
||||
this.saveCurrentTransform();
|
||||
});
|
||||
|
||||
if (this.mapData.transform) {
|
||||
if (this.mapData?.transform) {
|
||||
this.pzInstance.zoomTo(0, 0, this.mapData.transform.scale);
|
||||
|
||||
this.pzInstance.moveTo(this.mapData.transform.x, this.mapData.transform.y);
|
||||
@ -349,9 +444,13 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
saveCurrentTransform() {
|
||||
if (!this.pzInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTransform = this.pzInstance.getTransform();
|
||||
|
||||
if (JSON.stringify(newTransform) !== JSON.stringify(this.mapData.transform)) {
|
||||
if (this.mapData && JSON.stringify(newTransform) !== JSON.stringify(this.mapData.transform)) {
|
||||
// clone transform object
|
||||
this.mapData.transform = JSON.parse(JSON.stringify(newTransform));
|
||||
|
||||
@ -370,13 +469,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
initJsPlumbInstance() {
|
||||
async initJsPlumbInstance() {
|
||||
if (this.jsPlumbInstance) {
|
||||
this.cleanup();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const jsPlumb = (await import("jsplumb")).default.jsPlumb;
|
||||
this.jsPlumbInstance = jsPlumb.getInstance({
|
||||
Endpoint: ["Dot", { radius: 2 }],
|
||||
Connector: "StateMachine",
|
||||
@ -385,6 +485,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
Container: this.$relationMapContainer.attr("id")
|
||||
});
|
||||
|
||||
if (!this.jsPlumbInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jsPlumbInstance.registerConnectionType("uniDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: uniDirectionalOverlays });
|
||||
|
||||
this.jsPlumbInstance.registerConnectionType("biDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: biDirectionalOverlays });
|
||||
@ -396,10 +500,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent));
|
||||
}
|
||||
|
||||
async connectionCreatedHandler(info, originalEvent) {
|
||||
async connectionCreatedHandler(info: ConnectionMadeEventInfo, originalEvent: Event) {
|
||||
const connection = info.connection;
|
||||
|
||||
connection.bind("contextmenu", (obj, event) => {
|
||||
connection.bind("contextmenu", (obj: unknown, event: MouseEvent) => {
|
||||
if (connection.getType().includes("link")) {
|
||||
// don't create context menu if it's a link since there's nothing to do with link from relation map
|
||||
// (don't open browser menu either)
|
||||
@ -414,15 +518,17 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }],
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "remove") {
|
||||
if (!(await dialogService.confirm(t("relation_map.confirm_remove_relation")))) {
|
||||
if (!(await dialogService.confirm(t("relation_map.confirm_remove_relation"))) || !this.relations) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
|
||||
|
||||
if (relation) {
|
||||
await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`);
|
||||
}
|
||||
|
||||
this.jsPlumbInstance.deleteConnection(connection);
|
||||
this.jsPlumbInstance?.deleteConnection(connection);
|
||||
|
||||
this.relations = this.relations.filter((relation) => relation.attributeId !== connection.id);
|
||||
}
|
||||
@ -432,16 +538,20 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
});
|
||||
|
||||
// if there's no event, then this has been triggered programmatically
|
||||
if (!originalEvent) {
|
||||
if (!originalEvent || !this.jsPlumbInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name = await dialogService.prompt({
|
||||
message: t("relation_map.specify_new_relation_name"),
|
||||
shown: ({ $answer }) => {
|
||||
if (!$answer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$answer.on("keyup", () => {
|
||||
// invalid characters are simply ignored (from user perspective they are not even entered)
|
||||
const attrName = utils.filterAttributeName($answer.val());
|
||||
const attrName = utils.filterAttributeName($answer.val() as string);
|
||||
|
||||
$answer.val(attrName);
|
||||
});
|
||||
@ -465,7 +575,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
const targetNoteId = this.idToNoteId(connection.target.id);
|
||||
const sourceNoteId = this.idToNoteId(connection.source.id);
|
||||
|
||||
const relationExists = this.relations.some((rel) => rel.targetNoteId === targetNoteId && rel.sourceNoteId === sourceNoteId && rel.name === name);
|
||||
const relationExists = this.relations?.some((rel) => rel.targetNoteId === targetNoteId && rel.sourceNoteId === sourceNoteId && rel.name === name);
|
||||
|
||||
if (relationExists) {
|
||||
await dialogService.info(t("relation_map.connection_exists", { name }));
|
||||
@ -484,11 +594,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
async createNoteBox(noteId, title, x, y) {
|
||||
async createNoteBox(noteId: string, title: string, x: number, y: number) {
|
||||
if (!this.jsPlumbInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $link = await linkService.createLink(noteId, { title });
|
||||
$link.mousedown((e) => linkService.goToLink(e));
|
||||
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $noteBox = $("<div>")
|
||||
.addClass("note-box")
|
||||
@ -507,13 +624,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
stop: (params) => {
|
||||
const noteId = this.idToNoteId(params.el.id);
|
||||
|
||||
const note = this.mapData.notes.find((note) => note.noteId === noteId);
|
||||
const note = this.mapData?.notes.find((note) => note.noteId === noteId);
|
||||
|
||||
if (!note) {
|
||||
logError(t("relation_map.note_not_found", { noteId }));
|
||||
return;
|
||||
}
|
||||
|
||||
//@ts-expect-error TODO: Check if this is still valid.
|
||||
[note.x, note.y] = params.finalPos;
|
||||
|
||||
this.saveData();
|
||||
@ -552,25 +670,29 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
throw new Error(t("relation_map.cannot_match_transform", { transform }));
|
||||
}
|
||||
|
||||
return matches[1];
|
||||
return parseFloat(matches[1]);
|
||||
}
|
||||
|
||||
async dropNoteOntoRelationMapHandler(ev) {
|
||||
async dropNoteOntoRelationMapHandler(ev: JQuery.DropEvent) {
|
||||
ev.preventDefault();
|
||||
|
||||
const notes = JSON.parse(ev.originalEvent.dataTransfer.getData("text"));
|
||||
const dragData = ev.originalEvent?.dataTransfer?.getData("text");
|
||||
if (!dragData) {
|
||||
return;
|
||||
}
|
||||
const notes = JSON.parse(dragData);
|
||||
|
||||
let { x, y } = this.getMousePosition(ev);
|
||||
|
||||
for (const note of notes) {
|
||||
const exists = this.mapData.notes.some((n) => n.noteId === note.noteId);
|
||||
const exists = this.mapData?.notes.some((n) => n.noteId === note.noteId);
|
||||
|
||||
if (exists) {
|
||||
toastService.showError(t("relation_map.note_already_in_diagram", { title: note.title }));
|
||||
continue;
|
||||
}
|
||||
|
||||
this.mapData.notes.push({ noteId: note.noteId, x, y });
|
||||
this.mapData?.notes.push({ noteId: note.noteId, x, y });
|
||||
|
||||
if (x > 1000) {
|
||||
y += 100;
|
||||
@ -585,14 +707,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.loadNotesAndRelations();
|
||||
}
|
||||
|
||||
getMousePosition(evt) {
|
||||
getMousePosition(evt: JQuery.ClickEvent | JQuery.DropEvent) {
|
||||
const rect = this.$relationMapContainer[0].getBoundingClientRect();
|
||||
|
||||
const zoom = this.getZoom();
|
||||
|
||||
return {
|
||||
x: (evt.clientX - rect.left) / zoom,
|
||||
y: (evt.clientY - rect.top) / zoom
|
||||
x: ((evt.clientX ?? 0) - rect.left) / zoom,
|
||||
y: ((evt.clientY ?? 0) - rect.top) / zoom
|
||||
};
|
||||
}
|
||||
|
||||
@ -602,18 +724,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
};
|
||||
}
|
||||
|
||||
async relationMapCreateChildNoteEvent({ ntxId }) {
|
||||
async relationMapCreateChildNoteEvent({ ntxId }: EventData<"relationMapCreateChildNote">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
||||
|
||||
if (!title.trim()) {
|
||||
if (!title?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { note } = await server.post(`notes/${this.noteId}/children?target=into`, {
|
||||
const { note } = await server.post<PostNoteResponse>(`notes/${this.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text"
|
||||
@ -624,29 +746,29 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
||||
this.clipboard = { noteId: note.noteId, title };
|
||||
}
|
||||
|
||||
relationMapResetPanZoomEvent({ ntxId }) {
|
||||
relationMapResetPanZoomEvent({ ntxId }: EventData<"relationMapResetPanZoom">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reset to initial pan & zoom state
|
||||
this.pzInstance.zoomTo(0, 0, 1 / this.getZoom());
|
||||
this.pzInstance.moveTo(0, 0);
|
||||
this.pzInstance?.zoomTo(0, 0, 1 / this.getZoom());
|
||||
this.pzInstance?.moveTo(0, 0);
|
||||
}
|
||||
|
||||
relationMapResetZoomInEvent({ ntxId }) {
|
||||
relationMapResetZoomInEvent({ ntxId }: EventData<"relationMapResetZoomIn">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pzInstance.zoomTo(0, 0, 1.2);
|
||||
this.pzInstance?.zoomTo(0, 0, 1.2);
|
||||
}
|
||||
|
||||
relationMapResetZoomOutEvent({ ntxId }) {
|
||||
relationMapResetZoomOutEvent({ ntxId }: EventData<"relationMapResetZoomOut">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pzInstance.zoomTo(0, 0, 0.8);
|
||||
this.pzInstance?.zoomTo(0, 0, 0.8);
|
||||
}
|
||||
}
|
@ -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/panzoom/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/panzoom/dist/")));
|
||||
|
||||
// i18n
|
||||
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/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/mark.js/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/mark.js/dist/")));
|
||||
|
Loading…
x
Reference in New Issue
Block a user