mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 18:12:29 +08:00
Merge pull request #821 from TriliumNext/feature/client_typescript_port2
Port frontend to TypeScript (36.7% -> 48.5%)
This commit is contained in:
commit
2ec903893c
@ -10,4 +10,4 @@ echo By file
|
||||
cloc HEAD \
|
||||
--git --md \
|
||||
--include-lang=javascript,typescript \
|
||||
--by-file
|
||||
--by-file | grep \.js\|
|
@ -29,7 +29,7 @@ const copy = async () => {
|
||||
fs.copySync(path.join("build", srcFile), destFile, { recursive: true });
|
||||
}
|
||||
|
||||
const filesToCopy = ["config-sample.ini"];
|
||||
const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json"];
|
||||
for (const file of filesToCopy) {
|
||||
log(`Copying ${file}`);
|
||||
await fs.copy(file, path.join(DEST_DIR, file));
|
||||
|
@ -14,6 +14,10 @@ import MainTreeExecutors from "./main_tree_executors.js";
|
||||
import toast from "../services/toast.js";
|
||||
import ShortcutComponent from "./shortcut_component.js";
|
||||
import { t, initLocale } from "../services/i18n.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootWidget;
|
||||
@ -27,13 +31,72 @@ interface BeforeUploadListener extends Component {
|
||||
beforeUnloadEvent(): boolean;
|
||||
}
|
||||
|
||||
interface TriggerData {
|
||||
noteId?: string;
|
||||
noteIds?: string[];
|
||||
messages?: unknown[];
|
||||
callback?: () => void;
|
||||
interface CommandData {
|
||||
ntxId?: string;
|
||||
}
|
||||
|
||||
type CommandMappings = {
|
||||
"api-log-messages": CommandData;
|
||||
focusOnDetail: Required<CommandData>;
|
||||
searchNotes: CommandData & {
|
||||
searchString: string | undefined;
|
||||
};
|
||||
showDeleteNotesDialog: CommandData & {
|
||||
branchIdsToDelete: string[];
|
||||
callback: (value: ResolveOptions) => void;
|
||||
forceDeleteAllClones: boolean;
|
||||
};
|
||||
showConfirmDeleteNoteBoxWithNoteDialog: ConfirmWithTitleOptions;
|
||||
openedFileUpdated: CommandData & {
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
lastModifiedMs: number;
|
||||
filePath: string;
|
||||
};
|
||||
focusAndSelectTitle: CommandData & {
|
||||
isNewNote: boolean;
|
||||
};
|
||||
showPromptDialog: PromptDialogOptions;
|
||||
showInfoDialog: ConfirmWithMessageOptions;
|
||||
showConfirmDialog: ConfirmWithMessageOptions;
|
||||
openNewNoteSplit: CommandData & {
|
||||
ntxId: string;
|
||||
notePath: string;
|
||||
};
|
||||
executeInActiveNoteDetailWidget: CommandData & {
|
||||
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void
|
||||
};
|
||||
addTextToActiveEditor: CommandData & {
|
||||
text: string;
|
||||
};
|
||||
|
||||
importMarkdownInline: CommandData;
|
||||
showPasswordNotSet: CommandData;
|
||||
showProtectedSessionPasswordDialog: CommandData;
|
||||
closeProtectedSessionPasswordDialog: CommandData;
|
||||
}
|
||||
|
||||
type EventMappings = {
|
||||
initialRenderComplete: {};
|
||||
frocaReloaded: {};
|
||||
protectedSessionStarted: {};
|
||||
notesReloaded: {
|
||||
noteIds: string[];
|
||||
};
|
||||
refreshIncludedNote: {
|
||||
noteId: string;
|
||||
};
|
||||
apiLogMessages: {
|
||||
noteId: string;
|
||||
messages: string[];
|
||||
};
|
||||
}
|
||||
|
||||
type CommandAndEventMappings = (CommandMappings & EventMappings);
|
||||
|
||||
export type CommandNames = keyof CommandMappings;
|
||||
type EventNames = keyof EventMappings;
|
||||
|
||||
class AppContext extends Component {
|
||||
|
||||
isMainWindow: boolean;
|
||||
@ -127,11 +190,15 @@ class AppContext extends Component {
|
||||
this.triggerEvent('initialRenderComplete');
|
||||
}
|
||||
|
||||
triggerEvent(name: string, data: TriggerData = {}) {
|
||||
// TODO: Remove ignore once all commands are mapped out.
|
||||
//@ts-ignore
|
||||
triggerEvent<K extends EventNames | CommandNames>(name: K, data: CommandAndEventMappings[K] = {}) {
|
||||
return this.handleEvent(name, data);
|
||||
}
|
||||
|
||||
triggerCommand(name: string, data: TriggerData = {}) {
|
||||
// TODO: Remove ignore once all commands are mapped out.
|
||||
//@ts-ignore
|
||||
triggerCommand<K extends CommandNames>(name: K, data: CommandMappings[K] = {}) {
|
||||
for (const executor of this.components) {
|
||||
const fun = (executor as any)[`${name}Command`];
|
||||
|
||||
@ -144,7 +211,7 @@ class AppContext extends Component {
|
||||
// in the component tree to communicate with each other
|
||||
console.debug(`Unhandled command ${name}, converting to event.`);
|
||||
|
||||
return this.triggerEvent(name, data);
|
||||
return this.triggerEvent(name, data as CommandAndEventMappings[K]);
|
||||
}
|
||||
|
||||
getComponentByEl(el: HTMLElement) {
|
||||
|
@ -16,6 +16,7 @@ export default class Component {
|
||||
children: Component[];
|
||||
initialized: Promise<void> | null;
|
||||
parent?: Component;
|
||||
position!: number;
|
||||
|
||||
constructor() {
|
||||
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
|
||||
|
@ -21,10 +21,11 @@ class FAttachment {
|
||||
attachmentId!: string;
|
||||
private ownerId!: string;
|
||||
role!: string;
|
||||
private mime!: string;
|
||||
private title!: string;
|
||||
mime!: string;
|
||||
title!: string;
|
||||
isProtected!: boolean; // TODO: Is this used?
|
||||
private dateModified!: string;
|
||||
private utcDateModified!: string;
|
||||
utcDateModified!: string;
|
||||
private utcDateScheduledForErasureSince!: string;
|
||||
/**
|
||||
* optionally added to the entity
|
||||
|
18
src/public/app/server_types.ts
Normal file
18
src/public/app/server_types.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// TODO: Deduplicate with src/services/entity_changes_interface.ts
|
||||
export interface EntityChange {
|
||||
id?: number | null;
|
||||
noteId?: string;
|
||||
entityName: string;
|
||||
entityId: string;
|
||||
entity?: any;
|
||||
positions?: Record<string, number>;
|
||||
hash: string;
|
||||
utcDateChanged?: string;
|
||||
utcDateModified?: string;
|
||||
utcDateCreated?: string;
|
||||
isSynced: boolean | 1 | 0;
|
||||
isErased: boolean | 1 | 0;
|
||||
componentId?: string | null;
|
||||
changeId?: string | null;
|
||||
instanceId?: string | null;
|
||||
}
|
@ -1,11 +1,19 @@
|
||||
import { AttributeType } from "../entities/fattribute.js";
|
||||
import server from "./server.js";
|
||||
|
||||
interface InitOptions {
|
||||
$el: JQuery<HTMLElement>;
|
||||
attributeType: AttributeType | (() => AttributeType);
|
||||
open: boolean;
|
||||
nameCallback: () => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $el - element on which to init autocomplete
|
||||
* @param attributeType - "relation" or "label" or callback providing one of those values as a type of autocompleted attributes
|
||||
* @param open - should the autocomplete be opened after init?
|
||||
*/
|
||||
function initAttributeNameAutocomplete({ $el, attributeType, open }) {
|
||||
function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions) {
|
||||
if (!$el.hasClass("aa-input")) {
|
||||
$el.autocomplete({
|
||||
appendTo: document.querySelector('body'),
|
||||
@ -20,7 +28,7 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
|
||||
source: async (term, cb) => {
|
||||
const type = typeof attributeType === "function" ? attributeType() : attributeType;
|
||||
|
||||
const names = await server.get(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`);
|
||||
const names = await server.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`);
|
||||
const result = names.map(name => ({name}));
|
||||
|
||||
cb(result);
|
||||
@ -39,7 +47,7 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function initLabelValueAutocomplete({ $el, open, nameCallback }) {
|
||||
async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptions) {
|
||||
if ($el.hasClass("aa-input")) {
|
||||
// we reinit every time because autocomplete seems to have a bug where it retains state from last
|
||||
// open even though the value was reset
|
||||
@ -52,7 +60,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attributeValues = (await server.get(`attribute-values/${encodeURIComponent(attributeName)}`))
|
||||
const attributeValues = (await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`))
|
||||
.map(attribute => ({ value: attribute }));
|
||||
|
||||
if (attributeValues.length === 0) {
|
@ -1,7 +0,0 @@
|
||||
declare module 'attribute_parser';
|
||||
|
||||
|
||||
export function lex(str: string): any[]
|
||||
export function parse(tokens: any[], str?: string, allowEmptyRelations?: boolean): any[]
|
||||
export function lexAndParse(str: string, allowEmptyRelations?: boolean): any[]
|
||||
|
@ -1,14 +1,30 @@
|
||||
import FAttribute, { AttributeType, FAttributeRow } from "../entities/fattribute.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
function lex(str) {
|
||||
interface Token {
|
||||
text: string;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
interface Attribute {
|
||||
type: AttributeType;
|
||||
name: string;
|
||||
isInheritable: boolean;
|
||||
value?: string;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
function lex(str: string) {
|
||||
str = str.trim();
|
||||
|
||||
const tokens = [];
|
||||
const tokens: Token[] = [];
|
||||
|
||||
let quotes = false;
|
||||
let quotes: boolean | string = false;
|
||||
let currentWord = '';
|
||||
|
||||
function isOperatorSymbol(chr) {
|
||||
function isOperatorSymbol(chr: string) {
|
||||
return ['=', '*', '>', '<', '!'].includes(chr);
|
||||
}
|
||||
|
||||
@ -24,7 +40,7 @@ function lex(str) {
|
||||
/**
|
||||
* @param endIndex - index of the last character of the token
|
||||
*/
|
||||
function finishWord(endIndex) {
|
||||
function finishWord(endIndex: number) {
|
||||
if (currentWord === '') {
|
||||
return;
|
||||
}
|
||||
@ -107,7 +123,7 @@ function lex(str) {
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function checkAttributeName(attrName) {
|
||||
function checkAttributeName(attrName: string) {
|
||||
if (attrName.length === 0) {
|
||||
throw new Error("Attribute name is empty, please fill the name.");
|
||||
}
|
||||
@ -117,10 +133,10 @@ function checkAttributeName(attrName) {
|
||||
}
|
||||
}
|
||||
|
||||
function parse(tokens, str, allowEmptyRelations = false) {
|
||||
const attrs = [];
|
||||
function parse(tokens: Token[], str: string, allowEmptyRelations = false) {
|
||||
const attrs: Attribute[] = [];
|
||||
|
||||
function context(i) {
|
||||
function context(i: number) {
|
||||
let { startIndex, endIndex } = tokens[i];
|
||||
startIndex = Math.max(0, startIndex - 20);
|
||||
endIndex = Math.min(str.length, endIndex + 20);
|
||||
@ -151,7 +167,7 @@ function parse(tokens, str, allowEmptyRelations = false) {
|
||||
|
||||
checkAttributeName(labelName);
|
||||
|
||||
const attr = {
|
||||
const attr: Attribute = {
|
||||
type: 'label',
|
||||
name: labelName,
|
||||
isInheritable: isInheritable(),
|
||||
@ -177,7 +193,7 @@ function parse(tokens, str, allowEmptyRelations = false) {
|
||||
|
||||
checkAttributeName(relationName);
|
||||
|
||||
const attr = {
|
||||
const attr: Attribute = {
|
||||
type: 'relation',
|
||||
name: relationName,
|
||||
isInheritable: isInheritable(),
|
||||
@ -216,7 +232,7 @@ function parse(tokens, str, allowEmptyRelations = false) {
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function lexAndParse(str, allowEmptyRelations = false) {
|
||||
function lexAndParse(str: string, allowEmptyRelations = false) {
|
||||
const tokens = lex(str);
|
||||
|
||||
return parse(tokens, str, allowEmptyRelations);
|
@ -1,7 +1,9 @@
|
||||
import ws from "./ws.js";
|
||||
import froca from "./froca.js";
|
||||
import FAttribute from "../entities/fattribute.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
|
||||
async function renderAttribute(attribute, renderIsInheritable) {
|
||||
async function renderAttribute(attribute: FAttribute, renderIsInheritable: boolean) {
|
||||
const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : '';
|
||||
const $attr = $("<span>");
|
||||
|
||||
@ -20,7 +22,11 @@ async function renderAttribute(attribute, renderIsInheritable) {
|
||||
// when the relation has just been created, then it might not have a value
|
||||
if (attribute.value) {
|
||||
$attr.append(document.createTextNode(`~${attribute.name}${isInheritable}=`));
|
||||
$attr.append(await createLink(attribute.value));
|
||||
|
||||
const link = await createLink(attribute.value);
|
||||
if (link) {
|
||||
$attr.append(link);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ws.logError(`Unknown attr type: ${attribute.type}`);
|
||||
@ -29,7 +35,7 @@ async function renderAttribute(attribute, renderIsInheritable) {
|
||||
return $attr;
|
||||
}
|
||||
|
||||
function formatValue(val) {
|
||||
function formatValue(val: string) {
|
||||
if (/^[\p{L}\p{N}\-_,.]+$/u.test(val)) {
|
||||
return val;
|
||||
}
|
||||
@ -47,7 +53,7 @@ function formatValue(val) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createLink(noteId) {
|
||||
async function createLink(noteId: string) {
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
@ -61,7 +67,7 @@ async function createLink(noteId) {
|
||||
.text(note.title);
|
||||
}
|
||||
|
||||
async function renderAttributes(attributes, renderIsInheritable) {
|
||||
async function renderAttributes(attributes: FAttribute[], renderIsInheritable: boolean) {
|
||||
const $container = $('<span class="rendered-note-attributes">');
|
||||
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
@ -89,7 +95,7 @@ const HIDDEN_ATTRIBUTES = [
|
||||
'viewType'
|
||||
];
|
||||
|
||||
async function renderNormalAttributes(note) {
|
||||
async function renderNormalAttributes(note: FNote) {
|
||||
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
|
||||
let attrs = note.getAttributes();
|
||||
|
@ -1,7 +1,8 @@
|
||||
import server from './server.js';
|
||||
import froca from './froca.js';
|
||||
import FNote from '../entities/fnote.js';
|
||||
|
||||
async function addLabel(noteId, name, value = "") {
|
||||
async function addLabel(noteId: string, name: string, value: string = "") {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
type: 'label',
|
||||
name: name,
|
||||
@ -9,7 +10,7 @@ async function addLabel(noteId, name, value = "") {
|
||||
});
|
||||
}
|
||||
|
||||
async function setLabel(noteId, name, value = "") {
|
||||
async function setLabel(noteId: string, name: string, value: string = "") {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: 'label',
|
||||
name: name,
|
||||
@ -17,7 +18,7 @@ async function setLabel(noteId, name, value = "") {
|
||||
});
|
||||
}
|
||||
|
||||
async function removeAttributeById(noteId, attributeId) {
|
||||
async function removeAttributeById(noteId: string, attributeId: string) {
|
||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||
}
|
||||
|
||||
@ -28,7 +29,7 @@ async function removeAttributeById(noteId, attributeId) {
|
||||
* 2. attribute is owned by the template of the note
|
||||
* 3. attribute is owned by some note's ancestor and is inheritable
|
||||
*/
|
||||
function isAffecting(attrRow, affectedNote) {
|
||||
function isAffecting(this: { isInheritable: boolean }, attrRow: { noteId: string }, affectedNote: FNote) {
|
||||
if (!affectedNote || !attrRow) {
|
||||
return false;
|
||||
}
|
||||
@ -48,6 +49,7 @@ function isAffecting(attrRow, affectedNote) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This doesn't seem right.
|
||||
if (this.isInheritable) {
|
||||
for (const owningNote of owningNotes) {
|
||||
if (owningNote.hasAncestor(attrNote.noteId, true)) {
|
@ -1,17 +1,28 @@
|
||||
import utils from './utils.js';
|
||||
import server from './server.js';
|
||||
import toastService from "./toast.js";
|
||||
import toastService, { ToastOptions } from "./toast.js";
|
||||
import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from './i18n.js';
|
||||
import { Node } from './tree.js';
|
||||
import { ResolveOptions } from '../widgets/dialogs/delete_notes.js';
|
||||
|
||||
async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
|
||||
// TODO: Deduplicate type with server
|
||||
interface Response {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
async function moveBeforeBranch(branchIdsToMove: string[], beforeBranchId: string) {
|
||||
branchIdsToMove = filterRootNote(branchIdsToMove);
|
||||
branchIdsToMove = filterSearchBranches(branchIdsToMove);
|
||||
|
||||
const beforeBranch = froca.getBranch(beforeBranchId);
|
||||
if (!beforeBranch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) {
|
||||
toastService.showError(t("branches.cannot-move-notes-here"));
|
||||
@ -19,7 +30,7 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
|
||||
}
|
||||
|
||||
for (const branchIdToMove of branchIdsToMove) {
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-before/${beforeBranchId}`);
|
||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-before/${beforeBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(resp.message);
|
||||
@ -28,11 +39,14 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string) {
|
||||
branchIdsToMove = filterRootNote(branchIdsToMove);
|
||||
branchIdsToMove = filterSearchBranches(branchIdsToMove);
|
||||
|
||||
const afterNote = froca.getBranch(afterBranchId).getNote();
|
||||
const afterNote = await froca.getBranch(afterBranchId)?.getNote();
|
||||
if (!afterNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
const forbiddenNoteIds = [
|
||||
'root',
|
||||
@ -50,7 +64,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
branchIdsToMove.reverse(); // need to reverse to keep the note order
|
||||
|
||||
for (const branchIdToMove of branchIdsToMove) {
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-after/${afterBranchId}`);
|
||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-after/${afterBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(resp.message);
|
||||
@ -59,8 +73,11 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToParentNote(branchIdsToMove, newParentBranchId) {
|
||||
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string) {
|
||||
const newParentBranch = froca.getBranch(newParentBranchId);
|
||||
if (!newParentBranch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newParentBranch.noteId === '_lbRoot') {
|
||||
toastService.showError(t("branches.cannot-move-notes-here"));
|
||||
@ -72,12 +89,13 @@ async function moveToParentNote(branchIdsToMove, newParentBranchId) {
|
||||
for (const branchIdToMove of branchIdsToMove) {
|
||||
const branchToMove = froca.getBranch(branchIdToMove);
|
||||
|
||||
if (branchToMove.noteId === hoistedNoteService.getHoistedNoteId()
|
||||
|| (await branchToMove.getParentNote()).type === 'search') {
|
||||
if (!branchToMove
|
||||
|| branchToMove.noteId === hoistedNoteService.getHoistedNoteId()
|
||||
|| (await branchToMove.getParentNote())?.type === 'search') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
|
||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(resp.message);
|
||||
@ -86,7 +104,7 @@ async function moveToParentNote(branchIdsToMove, newParentBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
|
||||
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false) {
|
||||
branchIdsToDelete = filterRootNote(branchIdsToDelete);
|
||||
|
||||
if (branchIdsToDelete.length === 0) {
|
||||
@ -100,7 +118,7 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
|
||||
deleteAllClones = false;
|
||||
}
|
||||
else {
|
||||
({proceed, deleteAllClones, eraseNotes} = await new Promise(res =>
|
||||
({proceed, deleteAllClones, eraseNotes} = await new Promise<ResolveOptions>(res =>
|
||||
appContext.triggerCommand('showDeleteNotesDialog', {branchIdsToDelete, callback: res, forceDeleteAllClones})));
|
||||
}
|
||||
|
||||
@ -127,10 +145,9 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
|
||||
|
||||
const branch = froca.getBranch(branchIdToDelete);
|
||||
|
||||
if (deleteAllClones) {
|
||||
if (deleteAllClones && branch) {
|
||||
await server.remove(`notes/${branch.noteId}${query}`);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
await server.remove(`branches/${branchIdToDelete}${query}`);
|
||||
}
|
||||
}
|
||||
@ -152,7 +169,7 @@ async function activateParentNotePath() {
|
||||
}
|
||||
}
|
||||
|
||||
async function moveNodeUpInHierarchy(node) {
|
||||
async function moveNodeUpInHierarchy(node: Node) {
|
||||
if (hoistedNoteService.isHoistedNode(node)
|
||||
|| hoistedNoteService.isTopLevelNode(node)
|
||||
|| node.getParent().data.noteType === 'search') {
|
||||
@ -162,7 +179,7 @@ async function moveNodeUpInHierarchy(node) {
|
||||
const targetBranchId = node.getParent().data.branchId;
|
||||
const branchIdToMove = node.data.branchId;
|
||||
|
||||
const resp = await server.put(`branches/${branchIdToMove}/move-after/${targetBranchId}`);
|
||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-after/${targetBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(resp.message);
|
||||
@ -175,22 +192,25 @@ async function moveNodeUpInHierarchy(node) {
|
||||
}
|
||||
}
|
||||
|
||||
function filterSearchBranches(branchIds) {
|
||||
function filterSearchBranches(branchIds: string[]) {
|
||||
return branchIds.filter(branchId => !branchId.startsWith('virt-'));
|
||||
}
|
||||
|
||||
function filterRootNote(branchIds) {
|
||||
function filterRootNote(branchIds: string[]) {
|
||||
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
|
||||
|
||||
return branchIds.filter(branchId => {
|
||||
const branch = froca.getBranch(branchId);
|
||||
if (!branch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return branch.noteId !== 'root'
|
||||
&& branch.noteId !== hoistedNoteId;
|
||||
});
|
||||
}
|
||||
|
||||
function makeToast(id, message) {
|
||||
function makeToast(id: string, message: string): ToastOptions {
|
||||
return {
|
||||
id: id,
|
||||
title: t("branches.delete-status"),
|
||||
@ -235,8 +255,8 @@ ws.subscribeToMessages(async message => {
|
||||
}
|
||||
});
|
||||
|
||||
async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) {
|
||||
const resp = await server.put(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
|
||||
async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, prefix?: string) {
|
||||
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
@ -245,8 +265,8 @@ async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cloneNoteToParentNote(childNoteId, parentNoteId, prefix) {
|
||||
const resp = await server.put(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
|
||||
async function cloneNoteToParentNote(childNoteId: string, parentNoteId: string, prefix: string) {
|
||||
const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
|
||||
prefix: prefix
|
||||
});
|
||||
|
||||
@ -256,8 +276,8 @@ async function cloneNoteToParentNote(childNoteId, parentNoteId, prefix) {
|
||||
}
|
||||
|
||||
// beware that the first arg is noteId and the second is branchId!
|
||||
async function cloneNoteAfter(noteId, afterBranchId) {
|
||||
const resp = await server.put(`notes/${noteId}/clone-after/${afterBranchId}`);
|
||||
async function cloneNoteAfter(noteId: string, afterBranchId: string) {
|
||||
const resp = await server.put<Response>(`notes/${noteId}/clone-after/${afterBranchId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(resp.message);
|
@ -14,6 +14,7 @@ import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
|
||||
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
|
||||
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
|
||||
import { t } from "./i18n.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
|
||||
const ACTION_GROUPS = [
|
||||
{
|
||||
@ -50,7 +51,7 @@ const ACTION_CLASSES = [
|
||||
ExecuteScriptBulkAction
|
||||
];
|
||||
|
||||
async function addAction(noteId, actionName) {
|
||||
async function addAction(noteId: string, actionName: string) {
|
||||
await server.post(`notes/${noteId}/attributes`, {
|
||||
type: 'label',
|
||||
name: 'action',
|
||||
@ -62,7 +63,7 @@ async function addAction(noteId, actionName) {
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
}
|
||||
|
||||
function parseActions(note) {
|
||||
function parseActions(note: FNote) {
|
||||
const actionLabels = note.getLabels('action');
|
||||
|
||||
return actionLabels.map(actionAttr => {
|
||||
@ -70,7 +71,7 @@ function parseActions(note) {
|
||||
|
||||
try {
|
||||
actionDef = JSON.parse(actionAttr.value);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
|
||||
return null;
|
||||
}
|
@ -4,9 +4,22 @@ import toastService from "./toast.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { Entity } from "./frontend_script_api.js";
|
||||
|
||||
async function getAndExecuteBundle(noteId, originEntity = null, script = null, params = null) {
|
||||
const bundle = await server.post(`script/bundle/${noteId}`, {
|
||||
// TODO: Deduplicate with server.
|
||||
export interface Bundle {
|
||||
script: string;
|
||||
html: string;
|
||||
noteId: string;
|
||||
allNoteIds: string[];
|
||||
}
|
||||
|
||||
interface Widget {
|
||||
parentWidget?: string;
|
||||
}
|
||||
|
||||
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
|
||||
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
|
||||
script,
|
||||
params
|
||||
});
|
||||
@ -14,24 +27,23 @@ async function getAndExecuteBundle(noteId, originEntity = null, script = null, p
|
||||
return await executeBundle(bundle, originEntity);
|
||||
}
|
||||
|
||||
async function executeBundle(bundle, originEntity, $container) {
|
||||
async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
|
||||
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
|
||||
|
||||
try {
|
||||
return await (function () {
|
||||
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
|
||||
}.call(apiContext));
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e: any) {
|
||||
const note = await froca.getNote(bundle.noteId);
|
||||
|
||||
toastService.showAndLogError(`Execution of JS note "${note.title}" with ID ${bundle.noteId} failed with error: ${e.message}`);
|
||||
toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function executeStartupBundles() {
|
||||
const isMobile = utils.isMobile();
|
||||
const scriptBundles = await server.get("script/startup" + (isMobile ? "?mobile=true" : ""));
|
||||
const scriptBundles = await server.get<Bundle[]>("script/startup" + (isMobile ? "?mobile=true" : ""));
|
||||
|
||||
for (const bundle of scriptBundles) {
|
||||
await executeBundle(bundle);
|
||||
@ -39,11 +51,14 @@ async function executeStartupBundles() {
|
||||
}
|
||||
|
||||
class WidgetsByParent {
|
||||
|
||||
private byParent: Record<string, Widget[]>;
|
||||
|
||||
constructor() {
|
||||
this.byParent = {};
|
||||
}
|
||||
|
||||
add(widget) {
|
||||
add(widget: Widget) {
|
||||
if (!widget.parentWidget) {
|
||||
console.log(`Custom widget does not have mandatory 'parentWidget' property defined`);
|
||||
return;
|
||||
@ -53,7 +68,7 @@ class WidgetsByParent {
|
||||
this.byParent[widget.parentWidget].push(widget);
|
||||
}
|
||||
|
||||
get(parentName) {
|
||||
get(parentName: string) {
|
||||
if (!this.byParent[parentName]) {
|
||||
return [];
|
||||
}
|
||||
@ -62,12 +77,12 @@ class WidgetsByParent {
|
||||
// previously, custom widgets were provided as a single instance, but that has the disadvantage
|
||||
// for splits where we actually need multiple instaces and thus having a class to instantiate is better
|
||||
// https://github.com/zadam/trilium/issues/4274
|
||||
.map(w => w.prototype ? new w() : w);
|
||||
.map((w: any) => w.prototype ? new w() : w);
|
||||
}
|
||||
}
|
||||
|
||||
async function getWidgetBundlesByParent() {
|
||||
const scriptBundles = await server.get("script/widgets");
|
||||
const scriptBundles = await server.get<Bundle[]>("script/widgets");
|
||||
|
||||
const widgetsByParent = new WidgetsByParent();
|
||||
|
||||
@ -80,7 +95,7 @@ async function getWidgetBundlesByParent() {
|
||||
widget._noteId = bundle.noteId;
|
||||
widgetsByParent.add(widget);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
const noteId = bundle.noteId;
|
||||
const note = await froca.getNote(noteId);
|
||||
toastService.showPersistent({
|
||||
@ -88,7 +103,7 @@ async function getWidgetBundlesByParent() {
|
||||
icon: "alert",
|
||||
message: t("toast.bundle-error.message", {
|
||||
id: noteId,
|
||||
title: note.title,
|
||||
title: note?.title,
|
||||
message: e.message
|
||||
})
|
||||
});
|
@ -5,10 +5,10 @@ import linkService from "./link.js";
|
||||
import utils from "./utils.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
let clipboardBranchIds = [];
|
||||
let clipboardMode = null;
|
||||
let clipboardBranchIds: string[] = [];
|
||||
let clipboardMode: string | null = null;
|
||||
|
||||
async function pasteAfter(afterBranchId) {
|
||||
async function pasteAfter(afterBranchId: string) {
|
||||
if (isClipboardEmpty()) {
|
||||
return;
|
||||
}
|
||||
@ -23,7 +23,14 @@ async function pasteAfter(afterBranchId) {
|
||||
const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId));
|
||||
|
||||
for (const clipboardBranch of clipboardBranches) {
|
||||
if (!clipboardBranch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clipboardNote = await clipboardBranch.getNote();
|
||||
if (!clipboardNote) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await branchService.cloneNoteAfter(clipboardNote.noteId, afterBranchId);
|
||||
}
|
||||
@ -35,7 +42,7 @@ async function pasteAfter(afterBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function pasteInto(parentBranchId) {
|
||||
async function pasteInto(parentBranchId: string) {
|
||||
if (isClipboardEmpty()) {
|
||||
return;
|
||||
}
|
||||
@ -50,7 +57,14 @@ async function pasteInto(parentBranchId) {
|
||||
const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId));
|
||||
|
||||
for (const clipboardBranch of clipboardBranches) {
|
||||
if (!clipboardBranch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clipboardNote = await clipboardBranch.getNote();
|
||||
if (!clipboardNote) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await branchService.cloneNoteToBranch(clipboardNote.noteId, parentBranchId);
|
||||
}
|
||||
@ -62,7 +76,7 @@ async function pasteInto(parentBranchId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function copy(branchIds) {
|
||||
async function copy(branchIds: string[]) {
|
||||
clipboardBranchIds = branchIds;
|
||||
clipboardMode = 'copy';
|
||||
|
||||
@ -82,7 +96,7 @@ async function copy(branchIds) {
|
||||
toastService.showMessage(t("clipboard.copied"));
|
||||
}
|
||||
|
||||
function cut(branchIds) {
|
||||
function cut(branchIds: string[]) {
|
||||
clipboardBranchIds = branchIds;
|
||||
|
||||
if (clipboardBranchIds.length > 0) {
|
@ -16,12 +16,13 @@ import { loadElkIfNeeded } from "./mermaid.js";
|
||||
|
||||
let idCounter = 1;
|
||||
|
||||
/**
|
||||
* @param {FNote|FAttachment} entity
|
||||
* @param {object} options
|
||||
* @return {Promise<{type: string, $renderedContent: jQuery}>}
|
||||
*/
|
||||
async function getRenderedContent(entity, options = {}) {
|
||||
interface Options {
|
||||
tooltip?: boolean;
|
||||
trim?: boolean;
|
||||
imageHasZoom?: boolean;
|
||||
}
|
||||
|
||||
async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, options: Options = {}) {
|
||||
options = Object.assign({
|
||||
tooltip: false
|
||||
}, options);
|
||||
@ -49,7 +50,7 @@ async function getRenderedContent(entity, options = {}) {
|
||||
else if (type === 'render') {
|
||||
const $content = $('<div>');
|
||||
|
||||
await renderService.render(entity, $content, this.ctx);
|
||||
await renderService.render(entity, $content);
|
||||
|
||||
$renderedContent.append($content);
|
||||
}
|
||||
@ -86,12 +87,11 @@ async function getRenderedContent(entity, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {FNote} note */
|
||||
async function renderText(note, $renderedContent) {
|
||||
async function renderText(note: FNote, $renderedContent: JQuery<HTMLElement>) {
|
||||
// entity must be FNote
|
||||
const blob = await note.getBlob();
|
||||
|
||||
if (!utils.isHtmlEmpty(blob.content)) {
|
||||
if (blob && !utils.isHtmlEmpty(blob.content)) {
|
||||
$renderedContent.append($('<div class="ck-content">').html(blob.content));
|
||||
|
||||
if ($renderedContent.find('span.math-tex').length > 0) {
|
||||
@ -100,9 +100,9 @@ async function renderText(note, $renderedContent) {
|
||||
renderMathInElement($renderedContent[0], {trust: true});
|
||||
}
|
||||
|
||||
const getNoteIdFromLink = el => treeService.getNoteIdFromUrl($(el).attr('href'));
|
||||
const getNoteIdFromLink = (el: HTMLElement) => treeService.getNoteIdFromUrl($(el).attr('href') || "");
|
||||
const referenceLinks = $renderedContent.find("a.reference-link");
|
||||
const noteIdsToPrefetch = referenceLinks.map(el => getNoteIdFromLink(el));
|
||||
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
|
||||
await froca.getNotes(noteIdsToPrefetch);
|
||||
|
||||
for (const el of referenceLinks) {
|
||||
@ -117,19 +117,17 @@ async function renderText(note, $renderedContent) {
|
||||
|
||||
/**
|
||||
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
|
||||
*
|
||||
* @param {FNote} note
|
||||
*/
|
||||
async function renderCode(note, $renderedContent) {
|
||||
async function renderCode(note: FNote, $renderedContent: JQuery<HTMLElement>) {
|
||||
const blob = await note.getBlob();
|
||||
|
||||
const $codeBlock = $("<code>");
|
||||
$codeBlock.text(blob.content);
|
||||
$codeBlock.text(blob?.content || "");
|
||||
$renderedContent.append($("<pre>").append($codeBlock));
|
||||
await applySingleBlockSyntaxHighlight($codeBlock, mime_types.normalizeMimeTypeForCKEditor(note.mime));
|
||||
}
|
||||
|
||||
function renderImage(entity, $renderedContent, options = {}) {
|
||||
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) {
|
||||
const encodedTitle = encodeURIComponent(entity.title);
|
||||
|
||||
let url;
|
||||
@ -146,7 +144,7 @@ function renderImage(entity, $renderedContent, options = {}) {
|
||||
.css('justify-content', 'center');
|
||||
|
||||
const $img = $("<img>")
|
||||
.attr("src", url)
|
||||
.attr("src", url || "")
|
||||
.attr("id", "attachment-image-" + idCounter++)
|
||||
.css("max-width", "100%");
|
||||
|
||||
@ -165,7 +163,7 @@ function renderImage(entity, $renderedContent, options = {}) {
|
||||
imageContextMenuService.setupContextMenu($img);
|
||||
}
|
||||
|
||||
function renderFile(entity, type, $renderedContent) {
|
||||
function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>) {
|
||||
let entityType, entityId;
|
||||
|
||||
if (entity instanceof FNote) {
|
||||
@ -201,7 +199,7 @@ function renderFile(entity, type, $renderedContent) {
|
||||
$content.append($videoPreview);
|
||||
}
|
||||
|
||||
if (entityType === 'notes') {
|
||||
if (entityType === 'notes' && "noteId" in entity) {
|
||||
// TODO: we should make this available also for attachments, but there's a problem with "Open externally" support
|
||||
// in attachment list
|
||||
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
|
||||
@ -222,11 +220,11 @@ function renderFile(entity, type, $renderedContent) {
|
||||
$renderedContent.append($content);
|
||||
}
|
||||
|
||||
async function renderMermaid(note, $renderedContent) {
|
||||
async function renderMermaid(note: FNote, $renderedContent: JQuery<HTMLElement>) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
|
||||
|
||||
const blob = await note.getBlob();
|
||||
const content = blob.content || "";
|
||||
const content = blob?.content || "";
|
||||
|
||||
$renderedContent
|
||||
.css("display", "flex")
|
||||
@ -254,7 +252,7 @@ async function renderMermaid(note, $renderedContent) {
|
||||
* @param {FNote} note
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function renderChildrenList($renderedContent, note) {
|
||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
||||
$renderedContent.css("padding", "10px");
|
||||
$renderedContent.addClass("text-with-ellipsis");
|
||||
|
||||
@ -277,15 +275,21 @@ async function renderChildrenList($renderedContent, note) {
|
||||
}
|
||||
}
|
||||
|
||||
function getRenderingType(entity) {
|
||||
let type = entity.type || entity.role;
|
||||
const mime = entity.mime;
|
||||
function getRenderingType(entity: FNote | FAttachment) {
|
||||
let type: string = "";
|
||||
if ("type" in entity) {
|
||||
type = entity.type;
|
||||
} else if ("role" in entity) {
|
||||
type = entity.role;
|
||||
}
|
||||
|
||||
const mime = ("mime" in entity && entity.mime);
|
||||
|
||||
if (type === 'file' && mime === 'application/pdf') {
|
||||
type = 'pdf';
|
||||
} else if (type === 'file' && mime.startsWith('audio/')) {
|
||||
} else if (type === 'file' && mime && mime.startsWith('audio/')) {
|
||||
type = 'audio';
|
||||
} else if (type === 'file' && mime.startsWith('video/')) {
|
||||
} else if (type === 'file' && mime && mime.startsWith('video/')) {
|
||||
type = 'video';
|
||||
}
|
||||
|
@ -7,13 +7,17 @@
|
||||
*
|
||||
* @source underscore.js
|
||||
* @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/
|
||||
* @param {Function} func to wrap
|
||||
* @param {Number} waitMs in ms (`100`)
|
||||
* @param {Boolean} [immediate=false] whether to execute at the beginning (`false`)
|
||||
* @param func to wrap
|
||||
* @param waitMs in ms (`100`)
|
||||
* @param whether to execute at the beginning (`false`)
|
||||
* @api public
|
||||
*/
|
||||
function debounce(func, waitMs, immediate = false) {
|
||||
let timeout, args, context, timestamp, result;
|
||||
function debounce<T>(func: (...args: unknown[]) => T, waitMs: number, immediate: boolean = false) {
|
||||
let timeout: any; // TODO: fix once we split client and server.
|
||||
let args: unknown[] | null;
|
||||
let context: unknown;
|
||||
let timestamp: number;
|
||||
let result: T;
|
||||
if (null == waitMs) waitMs = 100;
|
||||
|
||||
function later() {
|
||||
@ -24,20 +28,20 @@ function debounce(func, waitMs, immediate = false) {
|
||||
} else {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
result = func.apply(context, args);
|
||||
result = func.apply(context, args || []);
|
||||
context = args = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const debounced = function () {
|
||||
const debounced = function (this: any) {
|
||||
context = this;
|
||||
args = arguments;
|
||||
args = arguments as unknown as unknown[];
|
||||
timestamp = Date.now();
|
||||
const callNow = immediate && !timeout;
|
||||
if (!timeout) timeout = setTimeout(later, waitMs);
|
||||
if (callNow) {
|
||||
result = func.apply(context, args);
|
||||
result = func.apply(context, args || []);
|
||||
context = args = null;
|
||||
}
|
||||
|
||||
@ -53,7 +57,7 @@ function debounce(func, waitMs, immediate = false) {
|
||||
|
||||
debounced.flush = function() {
|
||||
if (timeout) {
|
||||
result = func.apply(context, args);
|
||||
result = func.apply(context, args || []);
|
||||
context = args = null;
|
||||
|
||||
clearTimeout(timeout);
|
@ -1,24 +1,26 @@
|
||||
import appContext from "../components/app_context.js";
|
||||
import { ConfirmDialogOptions, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||
import { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
|
||||
async function info(message) {
|
||||
async function info(message: string) {
|
||||
return new Promise(res =>
|
||||
appContext.triggerCommand("showInfoDialog", {message, callback: res}));
|
||||
}
|
||||
|
||||
async function confirm(message) {
|
||||
async function confirm(message: string) {
|
||||
return new Promise(res =>
|
||||
appContext.triggerCommand("showConfirmDialog", {
|
||||
appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
|
||||
message,
|
||||
callback: x => res(x.confirmed)
|
||||
callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed)
|
||||
}));
|
||||
}
|
||||
|
||||
async function confirmDeleteNoteBoxWithNote(title) {
|
||||
async function confirmDeleteNoteBoxWithNote(title: string) {
|
||||
return new Promise(res =>
|
||||
appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", {title, callback: res}));
|
||||
}
|
||||
|
||||
async function prompt(props) {
|
||||
async function prompt(props: PromptDialogOptions) {
|
||||
return new Promise(res =>
|
||||
appContext.triggerCommand("showPromptDialog", {...props, callback: res}));
|
||||
}
|
@ -1,36 +1,45 @@
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
|
||||
const fileModificationStatus = {
|
||||
// TODO: Deduplicate
|
||||
interface Message {
|
||||
type: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
lastModifiedMs: number;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const fileModificationStatus: Record<string, Record<string, Message>> = {
|
||||
notes: {},
|
||||
attachments: {}
|
||||
};
|
||||
|
||||
function checkType(type) {
|
||||
function checkType(type: string) {
|
||||
if (type !== 'notes' && type !== 'attachments') {
|
||||
throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileModificationStatus(entityType, entityId) {
|
||||
function getFileModificationStatus(entityType: string, entityId: string) {
|
||||
checkType(entityType);
|
||||
|
||||
return fileModificationStatus[entityType][entityId];
|
||||
}
|
||||
|
||||
function fileModificationUploaded(entityType, entityId) {
|
||||
function fileModificationUploaded(entityType: string, entityId: string) {
|
||||
checkType(entityType);
|
||||
|
||||
delete fileModificationStatus[entityType][entityId];
|
||||
}
|
||||
|
||||
function ignoreModification(entityType, entityId) {
|
||||
function ignoreModification(entityType: string, entityId: string) {
|
||||
checkType(entityType);
|
||||
|
||||
delete fileModificationStatus[entityType][entityId];
|
||||
}
|
||||
|
||||
ws.subscribeToMessages(async message => {
|
||||
ws.subscribeToMessages(async (message: Message) => {
|
||||
if (message.type !== 'openedFileUpdated') {
|
||||
return;
|
||||
}
|
@ -243,7 +243,7 @@ class FrocaImpl implements Froca {
|
||||
}).filter(note => !!note) as FNote[];
|
||||
}
|
||||
|
||||
async getNotes(noteIds: string[], silentNotFoundError = false): Promise<FNote[]> {
|
||||
async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> {
|
||||
noteIds = Array.from(new Set(noteIds)); // make unique
|
||||
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);
|
||||
|
||||
|
@ -7,7 +7,7 @@ import FBranch, { FBranchRow } from "../entities/fbranch.js";
|
||||
import FAttribute, { FAttributeRow } from "../entities/fattribute.js";
|
||||
import FAttachment, { FAttachmentRow } from "../entities/fattachment.js";
|
||||
import FNote, { FNoteRow } from "../entities/fnote.js";
|
||||
import { EntityChange } from "../../../services/entity_changes_interface.js";
|
||||
import type { EntityChange } from "../server_types.js"
|
||||
|
||||
async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
const loadResults = new LoadResults(entityChanges);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -27,7 +27,7 @@ function setupGlobs() {
|
||||
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
|
||||
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
const string = msg.toLowerCase();
|
||||
const string = String(msg).toLowerCase();
|
||||
|
||||
let message = "Uncaught error: ";
|
||||
|
@ -4,7 +4,7 @@ import options from "./options.js";
|
||||
await library_loader.requireLibrary(library_loader.I18NEXT);
|
||||
|
||||
export async function initLocale() {
|
||||
const locale = options.get("locale") || "en";
|
||||
const locale = (options.get("locale") as string) || "en";
|
||||
|
||||
await i18next
|
||||
.use(i18nextHttpBackend)
|
@ -1,6 +1,7 @@
|
||||
import { t } from "./i18n.js";
|
||||
import toastService from "./toast.js";
|
||||
|
||||
function copyImageReferenceToClipboard($imageWrapper) {
|
||||
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
|
||||
try {
|
||||
$imageWrapper.attr('contenteditable', 'true');
|
||||
selectImage($imageWrapper.get(0));
|
||||
@ -14,17 +15,21 @@ function copyImageReferenceToClipboard($imageWrapper) {
|
||||
}
|
||||
}
|
||||
finally {
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
$imageWrapper.removeAttr('contenteditable');
|
||||
}
|
||||
}
|
||||
|
||||
function selectImage(element) {
|
||||
function selectImage(element: HTMLElement | undefined) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}
|
||||
|
||||
export default {
|
@ -1,11 +1,11 @@
|
||||
import toastService from "./toast.js";
|
||||
import toastService, { ToastOptions } from "./toast.js";
|
||||
import server from "./server.js";
|
||||
import ws from "./ws.js";
|
||||
import utils from "./utils.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
export async function uploadFiles(entityType, parentNoteId, files, options) {
|
||||
export async function uploadFiles(entityType: string, parentNoteId: string, files: string[], options: Record<string, string | Blob>) {
|
||||
if (!['notes', 'attachments'].includes(entityType)) {
|
||||
throw new Error(`Unrecognized import entity type '${entityType}'.`);
|
||||
}
|
||||
@ -45,7 +45,7 @@ export async function uploadFiles(entityType, parentNoteId, files, options) {
|
||||
}
|
||||
}
|
||||
|
||||
function makeToast(id, message) {
|
||||
function makeToast(id: string, message: string): ToastOptions {
|
||||
return {
|
||||
id: id,
|
||||
title: t("import.import-status"),
|
@ -1,10 +1,18 @@
|
||||
import server from "./server.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import appContext, { CommandNames } from "../components/app_context.js";
|
||||
import shortcutService from "./shortcuts.js";
|
||||
import Component from "../components/component.js";
|
||||
|
||||
const keyboardActionRepo = {};
|
||||
const keyboardActionRepo: Record<string, Action> = {};
|
||||
|
||||
const keyboardActionsLoaded = server.get('keyboard-actions').then(actions => {
|
||||
// TODO: Deduplicate with server.
|
||||
interface Action {
|
||||
actionName: CommandNames;
|
||||
effectiveShortcuts: string[];
|
||||
scope: string;
|
||||
}
|
||||
|
||||
const keyboardActionsLoaded = server.get<Action[]>('keyboard-actions').then(actions => {
|
||||
actions = actions.filter(a => !!a.actionName); // filter out separators
|
||||
|
||||
for (const action of actions) {
|
||||
@ -20,13 +28,13 @@ async function getActions() {
|
||||
return await keyboardActionsLoaded;
|
||||
}
|
||||
|
||||
async function getActionsForScope(scope) {
|
||||
async function getActionsForScope(scope: string) {
|
||||
const actions = await keyboardActionsLoaded;
|
||||
|
||||
return actions.filter(action => action.scope === scope);
|
||||
}
|
||||
|
||||
async function setupActionsForElement(scope, $el, component) {
|
||||
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
|
||||
const actions = await getActionsForScope(scope);
|
||||
|
||||
for (const action of actions) {
|
||||
@ -44,7 +52,7 @@ getActionsForScope("window").then(actions => {
|
||||
}
|
||||
});
|
||||
|
||||
async function getAction(actionName, silent = false) {
|
||||
async function getAction(actionName: string, silent = false) {
|
||||
await keyboardActionsLoaded;
|
||||
|
||||
const action = keyboardActionRepo[actionName];
|
||||
@ -61,9 +69,15 @@ async function getAction(actionName, silent = false) {
|
||||
return action;
|
||||
}
|
||||
|
||||
function updateDisplayedShortcuts($container) {
|
||||
function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
|
||||
//@ts-ignore
|
||||
//TODO: each() does not support async callbacks.
|
||||
$container.find('kbd[data-command]').each(async (i, el) => {
|
||||
const actionName = $(el).attr('data-command');
|
||||
if (!actionName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = await getAction(actionName, true);
|
||||
|
||||
if (action) {
|
||||
@ -75,8 +89,13 @@ function updateDisplayedShortcuts($container) {
|
||||
}
|
||||
});
|
||||
|
||||
//@ts-ignore
|
||||
//TODO: each() does not support async callbacks.
|
||||
$container.find('[data-trigger-command]').each(async (i, el) => {
|
||||
const actionName = $(el).attr('data-trigger-command');
|
||||
if (!actionName) {
|
||||
return;
|
||||
}
|
||||
const action = await getAction(actionName, true);
|
||||
|
||||
if (action) {
|
@ -2,9 +2,16 @@ import mimeTypesService from "./mime_types.js";
|
||||
import optionsService from "./options.js";
|
||||
import { getStylesheetUrl } from "./syntax_highlight.js";
|
||||
|
||||
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
|
||||
export interface Library {
|
||||
js?: string[] | (() => string[]);
|
||||
css?: string[];
|
||||
}
|
||||
|
||||
const CODE_MIRROR = {
|
||||
const CKEDITOR: Library = {
|
||||
js: ["libraries/ckeditor/ckeditor.js"]
|
||||
};
|
||||
|
||||
const CODE_MIRROR: Library = {
|
||||
js: [
|
||||
"node_modules/codemirror/lib/codemirror.js",
|
||||
"node_modules/codemirror/addon/display/placeholder.js",
|
||||
@ -26,9 +33,13 @@ const CODE_MIRROR = {
|
||||
]
|
||||
};
|
||||
|
||||
const ESLINT = {js: ["node_modules/eslint/bin/eslint.js"]};
|
||||
const ESLINT: Library = {
|
||||
js: [
|
||||
"node_modules/eslint/bin/eslint.js"
|
||||
]
|
||||
};
|
||||
|
||||
const RELATION_MAP = {
|
||||
const RELATION_MAP: Library = {
|
||||
js: [
|
||||
"node_modules/jsplumb/dist/js/jsplumb.min.js",
|
||||
"node_modules/panzoom/dist/panzoom.min.js"
|
||||
@ -38,26 +49,30 @@ const RELATION_MAP = {
|
||||
]
|
||||
};
|
||||
|
||||
const PRINT_THIS = {js: ["node_modules/print-this/printThis.js"]};
|
||||
const PRINT_THIS: Library = {
|
||||
js: ["node_modules/print-this/printThis.js"]
|
||||
};
|
||||
|
||||
const CALENDAR_WIDGET = {css: ["stylesheets/calendar.css"]};
|
||||
const CALENDAR_WIDGET: Library = {
|
||||
css: ["stylesheets/calendar.css"]
|
||||
};
|
||||
|
||||
const KATEX = {
|
||||
const KATEX: Library = {
|
||||
js: [ "node_modules/katex/dist/katex.min.js",
|
||||
"node_modules/katex/dist/contrib/mhchem.min.js",
|
||||
"node_modules/katex/dist/contrib/auto-render.min.js" ],
|
||||
css: [ "node_modules/katex/dist/katex.min.css" ]
|
||||
};
|
||||
|
||||
const WHEEL_ZOOM = {
|
||||
const WHEEL_ZOOM: Library = {
|
||||
js: [ "node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
|
||||
};
|
||||
|
||||
const FORCE_GRAPH = {
|
||||
const FORCE_GRAPH: Library = {
|
||||
js: [ "node_modules/force-graph/dist/force-graph.min.js"]
|
||||
};
|
||||
|
||||
const MERMAID = {
|
||||
const MERMAID: Library = {
|
||||
js: [
|
||||
"node_modules/mermaid/dist/mermaid.min.js"
|
||||
]
|
||||
@ -67,13 +82,13 @@ const MERMAID = {
|
||||
* The ELK extension of Mermaid.js, which supports more advanced layouts.
|
||||
* See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information.
|
||||
*/
|
||||
const MERMAID_ELK = {
|
||||
const MERMAID_ELK: Library = {
|
||||
js: [
|
||||
"libraries/mermaid-elk/elk.min.js"
|
||||
]
|
||||
}
|
||||
|
||||
const EXCALIDRAW = {
|
||||
const EXCALIDRAW: Library = {
|
||||
js: [
|
||||
"node_modules/react/umd/react.production.min.js",
|
||||
"node_modules/react-dom/umd/react-dom.production.min.js",
|
||||
@ -81,30 +96,30 @@ const EXCALIDRAW = {
|
||||
]
|
||||
};
|
||||
|
||||
const MARKJS = {
|
||||
const MARKJS: Library = {
|
||||
js: [
|
||||
"node_modules/mark.js/dist/jquery.mark.es6.min.js"
|
||||
]
|
||||
};
|
||||
|
||||
const I18NEXT = {
|
||||
const I18NEXT: Library = {
|
||||
js: [
|
||||
"node_modules/i18next/i18next.min.js",
|
||||
"node_modules/i18next-http-backend/i18nextHttpBackend.min.js"
|
||||
]
|
||||
};
|
||||
|
||||
const MIND_ELIXIR = {
|
||||
const MIND_ELIXIR: Library = {
|
||||
js: [
|
||||
"node_modules/mind-elixir/dist/MindElixir.iife.js",
|
||||
"node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs"
|
||||
]
|
||||
};
|
||||
|
||||
const HIGHLIGHT_JS = {
|
||||
const HIGHLIGHT_JS: Library = {
|
||||
js: () => {
|
||||
const mimeTypes = mimeTypesService.getMimeTypes();
|
||||
const scriptsToLoad = new Set();
|
||||
const scriptsToLoad = new Set<string>();
|
||||
scriptsToLoad.add("node_modules/@highlightjs/cdn-assets/highlight.min.js");
|
||||
for (const mimeType of mimeTypes) {
|
||||
const id = mimeType.highlightJs;
|
||||
@ -120,14 +135,14 @@ const HIGHLIGHT_JS = {
|
||||
}
|
||||
}
|
||||
|
||||
const currentTheme = optionsService.get("codeBlockTheme");
|
||||
const currentTheme = String(optionsService.get("codeBlockTheme"));
|
||||
loadHighlightingTheme(currentTheme);
|
||||
|
||||
return Array.from(scriptsToLoad);
|
||||
}
|
||||
};
|
||||
|
||||
async function requireLibrary(library) {
|
||||
async function requireLibrary(library: Library) {
|
||||
if (library.css) {
|
||||
library.css.map(cssUrl => requireCss(cssUrl));
|
||||
}
|
||||
@ -139,18 +154,18 @@ async function requireLibrary(library) {
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapValue(value) {
|
||||
function unwrapValue<T>(value: T | (() => T)) {
|
||||
if (typeof value === "function") {
|
||||
return value();
|
||||
return (value as () => T)();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// we save the promises in case of the same script being required concurrently multiple times
|
||||
const loadedScriptPromises = {};
|
||||
const loadedScriptPromises: Record<string, JQuery.jqXHR> = {};
|
||||
|
||||
async function requireScript(url) {
|
||||
async function requireScript(url: string) {
|
||||
url = `${window.glob.assetPath}/${url}`;
|
||||
|
||||
if (!loadedScriptPromises[url]) {
|
||||
@ -164,7 +179,7 @@ async function requireScript(url) {
|
||||
await loadedScriptPromises[url];
|
||||
}
|
||||
|
||||
async function requireCss(url, prependAssetPath = true) {
|
||||
async function requireCss(url: string, prependAssetPath = true) {
|
||||
const cssLinks = Array
|
||||
.from(document.querySelectorAll('link'))
|
||||
.map(el => el.href);
|
||||
@ -178,8 +193,8 @@ async function requireCss(url, prependAssetPath = true) {
|
||||
}
|
||||
}
|
||||
|
||||
let highlightingThemeEl = null;
|
||||
function loadHighlightingTheme(theme) {
|
||||
let highlightingThemeEl: JQuery<HTMLElement> | null = null;
|
||||
function loadHighlightingTheme(theme: string) {
|
||||
if (!theme) {
|
||||
return;
|
||||
}
|
@ -4,19 +4,19 @@ import appContext from "../components/app_context.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
function getNotePathFromUrl(url) {
|
||||
function getNotePathFromUrl(url: string) {
|
||||
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
|
||||
|
||||
return notePathMatch === null ? null : notePathMatch[1];
|
||||
}
|
||||
|
||||
async function getLinkIcon(noteId, viewMode) {
|
||||
async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
|
||||
let icon;
|
||||
|
||||
if (viewMode === 'default') {
|
||||
if (!viewMode || viewMode === 'default') {
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
icon = note.getIcon();
|
||||
icon = note?.getIcon();
|
||||
} else if (viewMode === 'source') {
|
||||
icon = 'bx bx-code-curly';
|
||||
} else if (viewMode === 'attachments') {
|
||||
@ -25,7 +25,24 @@ async function getLinkIcon(noteId, viewMode) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
async function createLink(notePath, options = {}) {
|
||||
type ViewMode = "default" | "source" | "attachments" | string;
|
||||
|
||||
interface ViewScope {
|
||||
viewMode?: ViewMode;
|
||||
attachmentId?: string;
|
||||
}
|
||||
|
||||
interface CreateLinkOptions {
|
||||
title?: string;
|
||||
showTooltip?: boolean;
|
||||
showNotePath?: boolean;
|
||||
showNoteIcon?: boolean;
|
||||
referenceLink?: boolean;
|
||||
autoConvertToImage?: boolean;
|
||||
viewScope?: ViewScope;
|
||||
}
|
||||
|
||||
async function createLink(notePath: string, options: CreateLinkOptions = {}) {
|
||||
if (!notePath || !notePath.trim()) {
|
||||
logError("Missing note path");
|
||||
|
||||
@ -45,6 +62,12 @@ async function createLink(notePath, options = {}) {
|
||||
const autoConvertToImage = options.autoConvertToImage === undefined ? false : options.autoConvertToImage;
|
||||
|
||||
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
||||
if (!noteId) {
|
||||
logError("Missing note ID");
|
||||
|
||||
return $("<span>").text("[missing note]");
|
||||
}
|
||||
|
||||
const viewScope = options.viewScope || {};
|
||||
const viewMode = viewScope.viewMode || 'default';
|
||||
let linkTitle = options.title;
|
||||
@ -54,19 +77,19 @@ async function createLink(notePath, options = {}) {
|
||||
const attachment = await froca.getAttachment(viewScope.attachmentId);
|
||||
|
||||
linkTitle = attachment ? attachment.title : '[missing attachment]';
|
||||
} else {
|
||||
} else if (noteId) {
|
||||
linkTitle = await treeService.getNoteTitle(noteId, parentNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
if (autoConvertToImage && ['image', 'canvas', 'mermaid'].includes(note.type) && viewMode === 'default') {
|
||||
const encodedTitle = encodeURIComponent(linkTitle);
|
||||
if (autoConvertToImage && (note?.type && ['image', 'canvas', 'mermaid'].includes(note.type)) && viewMode === 'default') {
|
||||
const encodedTitle = encodeURIComponent(linkTitle || "");
|
||||
|
||||
return $("<img>")
|
||||
.attr("src", `api/images/${noteId}/${encodedTitle}?${Math.random()}`)
|
||||
.attr("alt", linkTitle);
|
||||
.attr("alt", linkTitle || "");
|
||||
}
|
||||
|
||||
const $container = $("<span>");
|
||||
@ -102,7 +125,7 @@ async function createLink(notePath, options = {}) {
|
||||
$container.append($noteLink);
|
||||
|
||||
if (showNotePath) {
|
||||
const resolvedPathSegments = await treeService.resolveNotePathToSegments(notePath);
|
||||
const resolvedPathSegments = await treeService.resolveNotePathToSegments(notePath) || [];
|
||||
resolvedPathSegments.pop(); // Remove last element
|
||||
|
||||
const resolvedPath = resolvedPathSegments.join("/");
|
||||
@ -118,7 +141,14 @@ async function createLink(notePath, options = {}) {
|
||||
return $container;
|
||||
}
|
||||
|
||||
function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
|
||||
interface CalculateHashOpts {
|
||||
notePath: string;
|
||||
ntxId?: string;
|
||||
hoistedNoteId?: string;
|
||||
viewScope: ViewScope;
|
||||
}
|
||||
|
||||
function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}: CalculateHashOpts) {
|
||||
notePath = notePath || "";
|
||||
const params = [
|
||||
ntxId ? { ntxId: ntxId } : null,
|
||||
@ -129,9 +159,9 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
|
||||
|
||||
const paramStr = params.map(pair => {
|
||||
const name = Object.keys(pair)[0];
|
||||
const value = pair[name];
|
||||
const value = (pair as Record<string, string | undefined>)[name];
|
||||
|
||||
return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
||||
return `${encodeURIComponent(name)}=${encodeURIComponent(value || "")}`;
|
||||
}).join("&");
|
||||
|
||||
if (!notePath && !paramStr) {
|
||||
@ -147,7 +177,7 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
function parseNavigationStateFromUrl(url) {
|
||||
function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
if (!url) {
|
||||
return {};
|
||||
}
|
||||
@ -164,7 +194,7 @@ function parseNavigationStateFromUrl(url) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const viewScope = {
|
||||
const viewScope: ViewScope = {
|
||||
viewMode: 'default'
|
||||
};
|
||||
let ntxId = null;
|
||||
@ -184,7 +214,7 @@ function parseNavigationStateFromUrl(url) {
|
||||
} else if (name === 'searchString') {
|
||||
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
|
||||
} else if (['viewMode', 'attachmentId'].includes(name)) {
|
||||
viewScope[name] = value;
|
||||
(viewScope as any)[name] = value;
|
||||
} else {
|
||||
console.warn(`Unrecognized hash parameter '${name}'.`);
|
||||
}
|
||||
@ -201,14 +231,14 @@ function parseNavigationStateFromUrl(url) {
|
||||
};
|
||||
}
|
||||
|
||||
function goToLink(evt) {
|
||||
const $link = $(evt.target).closest("a,.block-link");
|
||||
function goToLink(evt: MouseEvent) {
|
||||
const $link = $(evt.target as any).closest("a,.block-link");
|
||||
const hrefLink = $link.attr('href') || $link.attr('data-href');
|
||||
|
||||
return goToLinkExt(evt, hrefLink, $link);
|
||||
}
|
||||
|
||||
function goToLinkExt(evt, hrefLink, $link) {
|
||||
function goToLinkExt(evt: MouseEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement>) {
|
||||
if (hrefLink?.startsWith("data:")) {
|
||||
return true;
|
||||
}
|
||||
@ -230,7 +260,7 @@ function goToLinkExt(evt, hrefLink, $link) {
|
||||
if (openInNewTab) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope});
|
||||
} else if (isLeftClick) {
|
||||
const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id");
|
||||
const ntxId = $(evt.target as any).closest("[data-ntx-id]").attr("data-ntx-id");
|
||||
|
||||
const noteContext = ntxId
|
||||
? appContext.tabManager.getNoteContextById(ntxId)
|
||||
@ -275,8 +305,8 @@ function goToLinkExt(evt, hrefLink, $link) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function linkContextMenu(e) {
|
||||
const $link = $(e.target).closest("a");
|
||||
function linkContextMenu(e: Event) {
|
||||
const $link = $(e.target as any).closest("a");
|
||||
const url = $link.attr("href") || $link.attr("data-href");
|
||||
|
||||
const { notePath, viewScope } = parseNavigationStateFromUrl(url);
|
||||
@ -290,7 +320,7 @@ function linkContextMenu(e) {
|
||||
linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
|
||||
}
|
||||
|
||||
async function loadReferenceLinkTitle($el, href = null) {
|
||||
async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) {
|
||||
const $link = $el[0].tagName === 'A' ? $el : $el.find("a");
|
||||
|
||||
href = href || $link.attr("href");
|
||||
@ -300,6 +330,11 @@ async function loadReferenceLinkTitle($el, href = null) {
|
||||
}
|
||||
|
||||
const {noteId, viewScope} = parseNavigationStateFromUrl(href);
|
||||
if (!noteId) {
|
||||
console.warn("Missing note ID.");
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await froca.getNote(noteId, true);
|
||||
|
||||
if (note) {
|
||||
@ -312,11 +347,13 @@ async function loadReferenceLinkTitle($el, href = null) {
|
||||
if (note) {
|
||||
const icon = await getLinkIcon(noteId, viewScope.viewMode);
|
||||
|
||||
if (icon) {
|
||||
$el.prepend($("<span>").addClass(icon));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getReferenceLinkTitle(href) {
|
||||
async function getReferenceLinkTitle(href: string) {
|
||||
const {noteId, viewScope} = parseNavigationStateFromUrl(href);
|
||||
if (!noteId) {
|
||||
return "[missing note]";
|
||||
@ -336,7 +373,7 @@ async function getReferenceLinkTitle(href) {
|
||||
}
|
||||
}
|
||||
|
||||
function getReferenceLinkTitleSync(href) {
|
||||
function getReferenceLinkTitleSync(href: string) {
|
||||
const {noteId, viewScope} = parseNavigationStateFromUrl(href);
|
||||
if (!noteId) {
|
||||
return "[missing note]";
|
||||
@ -360,7 +397,11 @@ function getReferenceLinkTitleSync(href) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on('click', "a", goToLink);
|
||||
// TODO: Check why the event is not supported.
|
||||
//@ts-ignore
|
||||
$(document).on('auxclick', "a", goToLink); // to handle the middle button
|
||||
$(document).on('contextmenu', 'a', linkContextMenu);
|
||||
$(document).on('dblclick', "a", e => {
|
@ -1,4 +1,4 @@
|
||||
import { EntityChange } from "../../../services/entity_changes_interface.js";
|
||||
import { EntityChange } from "../server_types.js";
|
||||
|
||||
interface BranchRow {
|
||||
branchId: string;
|
||||
|
@ -15,7 +15,7 @@ function init() {
|
||||
}
|
||||
}
|
||||
|
||||
function exec(cmd) {
|
||||
function exec(cmd: string) {
|
||||
document.execCommand(cmd);
|
||||
|
||||
return false;
|
@ -11,7 +11,7 @@ let elkLoaded = false;
|
||||
*
|
||||
* @param mermaidContent the plain text of the mermaid diagram, potentially including a frontmatter.
|
||||
*/
|
||||
export async function loadElkIfNeeded(mermaidContent) {
|
||||
export async function loadElkIfNeeded(mermaidContent: string) {
|
||||
if (elkLoaded) {
|
||||
// Exit immediately since the ELK library is already loaded.
|
||||
return;
|
@ -9,7 +9,21 @@ const MIME_TYPE_AUTO = "text-x-trilium-auto";
|
||||
* For highlight.js-supported languages, see https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md.
|
||||
*/
|
||||
|
||||
const MIME_TYPES_DICT = [
|
||||
interface MimeTypeDefinition {
|
||||
default?: boolean;
|
||||
title: string;
|
||||
mime: string;
|
||||
/** The name of the language/mime type as defined by highlight.js (or one of the aliases), in order to be used for syntax highlighting such as inside code blocks. */
|
||||
highlightJs?: string;
|
||||
/** If specified, will load the corresponding highlight.js file from the `libraries/highlightjs/${id}.js` instead of `node_modules/@highlightjs/cdn-assets/languages/${id}.min.js`. */
|
||||
highlightJsSource?: "libraries";
|
||||
}
|
||||
|
||||
interface MimeType extends MimeTypeDefinition {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const MIME_TYPES_DICT: MimeTypeDefinition[] = [
|
||||
{ default: true, title: "Plain text", mime: "text/plain", highlightJs: "plaintext" },
|
||||
{ title: "APL", mime: "text/apl" },
|
||||
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
|
||||
@ -170,10 +184,10 @@ const MIME_TYPES_DICT = [
|
||||
{ title: "Z80", mime: "text/x-z80" }
|
||||
];
|
||||
|
||||
let mimeTypes = null;
|
||||
let mimeTypes: MimeType[] | null = null;
|
||||
|
||||
function loadMimeTypes() {
|
||||
mimeTypes = JSON.parse(JSON.stringify(MIME_TYPES_DICT)); // clone
|
||||
mimeTypes = JSON.parse(JSON.stringify(MIME_TYPES_DICT)) as MimeType[]; // clone
|
||||
|
||||
const enabledMimeTypes = options.getJson('codeNotesMimeTypes')
|
||||
|| MIME_TYPES_DICT.filter(mt => mt.default).map(mt => mt.mime);
|
||||
@ -183,34 +197,36 @@ function loadMimeTypes() {
|
||||
}
|
||||
}
|
||||
|
||||
function getMimeTypes() {
|
||||
function getMimeTypes(): MimeType[] {
|
||||
if (mimeTypes === null) {
|
||||
loadMimeTypes();
|
||||
}
|
||||
|
||||
return mimeTypes;
|
||||
return mimeTypes as MimeType[];
|
||||
}
|
||||
|
||||
let mimeToHighlightJsMapping = null;
|
||||
let mimeToHighlightJsMapping: Record<string, string> | null = null;
|
||||
|
||||
/**
|
||||
* Obtains the corresponding language tag for highlight.js for a given MIME type.
|
||||
*
|
||||
* The mapping is built the first time this method is built and then the results are cached for better performance.
|
||||
*
|
||||
* @param {string} mimeType The MIME type of the code block, in the CKEditor-normalized format (e.g. `text-c-src` instead of `text/c-src`).
|
||||
* @param mimeType The MIME type of the code block, in the CKEditor-normalized format (e.g. `text-c-src` instead of `text/c-src`).
|
||||
* @returns the corresponding highlight.js tag, for example `c` for `text-c-src`.
|
||||
*/
|
||||
function getHighlightJsNameForMime(mimeType) {
|
||||
function getHighlightJsNameForMime(mimeType: string) {
|
||||
if (!mimeToHighlightJsMapping) {
|
||||
const mimeTypes = getMimeTypes();
|
||||
mimeToHighlightJsMapping = {};
|
||||
for (const mimeType of mimeTypes) {
|
||||
// The mime stored by CKEditor is text-x-csrc instead of text/x-csrc so we keep this format for faster lookup.
|
||||
const normalizedMime = normalizeMimeTypeForCKEditor(mimeType.mime);
|
||||
if (mimeType.highlightJs) {
|
||||
mimeToHighlightJsMapping[normalizedMime] = mimeType.highlightJs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mimeToHighlightJsMapping[mimeType];
|
||||
}
|
||||
@ -219,10 +235,10 @@ function getHighlightJsNameForMime(mimeType) {
|
||||
* Given a MIME type in the usual format (e.g. `text/csrc`), it returns a MIME type that can be passed down to the CKEditor
|
||||
* code plugin.
|
||||
*
|
||||
* @param {string} mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`).
|
||||
* @param mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`).
|
||||
* @returns the normalized MIME type (e.g. `text-c-src`).
|
||||
*/
|
||||
function normalizeMimeTypeForCKEditor(mimeType) {
|
||||
function normalizeMimeTypeForCKEditor(mimeType: string) {
|
||||
return mimeType.toLowerCase()
|
||||
.replace(/[\W_]+/g,"-");
|
||||
}
|
@ -10,7 +10,26 @@ const SELECTED_NOTE_PATH_KEY = "data-note-path";
|
||||
|
||||
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
|
||||
|
||||
async function autocompleteSourceForCKEditor(queryText) {
|
||||
export interface Suggestion {
|
||||
noteTitle?: string;
|
||||
externalLink?: string;
|
||||
notePathTitle?: string;
|
||||
notePath?: string;
|
||||
highlightedNotePathTitle?: string;
|
||||
action?: string | "create-note" | "search-notes" | "external-link";
|
||||
parentNoteId?: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
container?: HTMLElement;
|
||||
fastSearch?: boolean;
|
||||
allowCreatingNotes?: boolean;
|
||||
allowJumpToSearchNotes?: boolean;
|
||||
allowExternalLinks?: boolean;
|
||||
hideGoToSelectedNoteButton?: boolean;
|
||||
}
|
||||
|
||||
async function autocompleteSourceForCKEditor(queryText: string) {
|
||||
return await new Promise((res, rej) => {
|
||||
autocompleteSource(queryText, rows => {
|
||||
res(rows.map(row => {
|
||||
@ -30,7 +49,7 @@ async function autocompleteSourceForCKEditor(queryText) {
|
||||
});
|
||||
}
|
||||
|
||||
async function autocompleteSource(term, cb, options = {}) {
|
||||
async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) {
|
||||
const fastSearch = options.fastSearch === false ? false : true;
|
||||
if (fastSearch === false) {
|
||||
if (term.trim().length === 0){
|
||||
@ -46,7 +65,7 @@ async function autocompleteSource(term, cb, options = {}) {
|
||||
|
||||
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
|
||||
|
||||
let results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`);
|
||||
let results: Suggestion[] = await server.get<Suggestion[]>(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`);
|
||||
if (term.trim().length >= 1 && options.allowCreatingNotes) {
|
||||
results = [
|
||||
{
|
||||
@ -54,7 +73,7 @@ async function autocompleteSource(term, cb, options = {}) {
|
||||
noteTitle: term,
|
||||
parentNoteId: activeNoteId || 'root',
|
||||
highlightedNotePathTitle: t("note_autocomplete.create-note", { term })
|
||||
}
|
||||
} as Suggestion
|
||||
].concat(results);
|
||||
}
|
||||
|
||||
@ -74,14 +93,14 @@ async function autocompleteSource(term, cb, options = {}) {
|
||||
action: 'external-link',
|
||||
externalLink: term,
|
||||
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term })
|
||||
}
|
||||
} as Suggestion
|
||||
].concat(results);
|
||||
}
|
||||
|
||||
cb(results);
|
||||
}
|
||||
|
||||
function clearText($el) {
|
||||
function clearText($el: JQuery<HTMLElement>) {
|
||||
if (utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
@ -90,7 +109,7 @@ function clearText($el) {
|
||||
$el.autocomplete("val", "").trigger('change');
|
||||
}
|
||||
|
||||
function setText($el, text) {
|
||||
function setText($el: JQuery<HTMLElement>, text: string) {
|
||||
if (utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
@ -101,7 +120,7 @@ function setText($el, text) {
|
||||
.autocomplete("open");
|
||||
}
|
||||
|
||||
function showRecentNotes($el) {
|
||||
function showRecentNotes($el:JQuery<HTMLElement>) {
|
||||
if (utils.isMobile()) {
|
||||
return;
|
||||
}
|
||||
@ -112,21 +131,22 @@ function showRecentNotes($el) {
|
||||
$el.trigger('focus');
|
||||
}
|
||||
|
||||
function fullTextSearch($el, options){
|
||||
const searchString = $el.autocomplete('val');
|
||||
if (options.fastSearch === false || searchString.trim().length === 0) {
|
||||
function fullTextSearch($el: JQuery<HTMLElement>, options: Options){
|
||||
const searchString = $el.autocomplete('val') as unknown as string;
|
||||
if (options.fastSearch === false || searchString?.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
$el.trigger('focus');
|
||||
options.fastSearch = false;
|
||||
$el.autocomplete('val', '');
|
||||
$el.autocomplete()
|
||||
$el.setSelectedNotePath("");
|
||||
$el.autocomplete('val', searchString);
|
||||
// Set a delay to avoid resetting to true before full text search (await server.get) is called.
|
||||
setTimeout(() => { options.fastSearch = true; }, 100);
|
||||
}
|
||||
|
||||
function initNoteAutocomplete($el, options) {
|
||||
function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
||||
if ($el.hasClass("note-autocomplete-input") || utils.isMobile()) {
|
||||
// clear any event listener added in previous invocation of this function
|
||||
$el.off('autocomplete:noteselected');
|
||||
@ -174,7 +194,7 @@ function initNoteAutocomplete($el, options) {
|
||||
return false;
|
||||
});
|
||||
|
||||
let autocompleteOptions = {};
|
||||
let autocompleteOptions: AutoCompleteConfig = {};
|
||||
if (options.container) {
|
||||
autocompleteOptions.dropdownMenuContainer = options.container;
|
||||
autocompleteOptions.debug = true; // don't close on blur
|
||||
@ -221,7 +241,8 @@ function initNoteAutocomplete($el, options) {
|
||||
}
|
||||
]);
|
||||
|
||||
$el.on('autocomplete:selected', async (event, suggestion) => {
|
||||
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
|
||||
($el as any).on('autocomplete:selected', async (event: Event, suggestion: Suggestion) => {
|
||||
if (suggestion.action === 'external-link') {
|
||||
$el.setSelectedNotePath(null);
|
||||
$el.setSelectedExternalLink(suggestion.externalLink);
|
||||
@ -250,7 +271,7 @@ function initNoteAutocomplete($el, options) {
|
||||
});
|
||||
|
||||
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
suggestion.notePath = note.getBestNotePathString(hoistedNoteId);
|
||||
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
|
||||
}
|
||||
|
||||
if (suggestion.action === 'search-notes') {
|
||||
@ -270,7 +291,7 @@ function initNoteAutocomplete($el, options) {
|
||||
});
|
||||
|
||||
$el.on('autocomplete:closed', () => {
|
||||
if (!$el.val().trim()) {
|
||||
if (!String($el.val())?.trim()) {
|
||||
clearText($el);
|
||||
}
|
||||
});
|
||||
@ -289,7 +310,7 @@ function initNoteAutocomplete($el, options) {
|
||||
|
||||
function init() {
|
||||
$.fn.getSelectedNotePath = function () {
|
||||
if (!$(this).val().trim()) {
|
||||
if (!String($(this).val())?.trim()) {
|
||||
return "";
|
||||
} else {
|
||||
return $(this).attr(SELECTED_NOTE_PATH_KEY);
|
||||
@ -297,7 +318,8 @@ function init() {
|
||||
};
|
||||
|
||||
$.fn.getSelectedNoteId = function () {
|
||||
const notePath = $(this).getSelectedNotePath();
|
||||
const $el = $(this as unknown as HTMLElement);
|
||||
const notePath = $el.getSelectedNotePath();
|
||||
if (!notePath) {
|
||||
return null;
|
||||
}
|
||||
@ -320,7 +342,7 @@ function init() {
|
||||
};
|
||||
|
||||
$.fn.getSelectedExternalLink = function () {
|
||||
if (!$(this).val().trim()) {
|
||||
if (!String($(this).val())?.trim()) {
|
||||
return "";
|
||||
} else {
|
||||
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
|
||||
@ -329,6 +351,7 @@ function init() {
|
||||
|
||||
$.fn.setSelectedExternalLink = function (externalLink) {
|
||||
if (externalLink) {
|
||||
// TODO: This doesn't seem to do anything with the external link, is it normal?
|
||||
$(this)
|
||||
.closest(".input-group")
|
||||
.find(".go-to-selected-note-button")
|
@ -6,8 +6,41 @@ import froca from "./froca.js";
|
||||
import treeService from "./tree.js";
|
||||
import toastService from "./toast.js";
|
||||
import { t } from "./i18n.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import FBranch from "../entities/fbranch.js";
|
||||
import { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
|
||||
|
||||
async function createNote(parentNotePath, options = {}) {
|
||||
interface CreateNoteOpts {
|
||||
isProtected?: boolean;
|
||||
saveSelection?: boolean;
|
||||
title?: string | null;
|
||||
content?: string | null;
|
||||
type?: string;
|
||||
mime?: string;
|
||||
templateNoteId?: string;
|
||||
activate?: boolean;
|
||||
focus?: "title" | "content";
|
||||
target?: string;
|
||||
targetBranchId?: string;
|
||||
textEditor?: {
|
||||
// TODO: Replace with interface once note_context.js is converted.
|
||||
getSelectedHtml(): string;
|
||||
removeSelection(): void;
|
||||
}
|
||||
}
|
||||
|
||||
interface Response {
|
||||
// TODO: Deduplicate with server once we have client/server architecture.
|
||||
note: FNote;
|
||||
branch: FBranch;
|
||||
}
|
||||
|
||||
interface DuplicateResponse {
|
||||
// TODO: Deduplicate with server once we have client/server architecture.
|
||||
note: FNote;
|
||||
}
|
||||
|
||||
async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}) {
|
||||
options = Object.assign({
|
||||
activate: true,
|
||||
focus: 'title',
|
||||
@ -24,7 +57,7 @@ async function createNote(parentNotePath, options = {}) {
|
||||
options.saveSelection = false;
|
||||
}
|
||||
|
||||
if (options.saveSelection) {
|
||||
if (options.saveSelection && options.textEditor) {
|
||||
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
|
||||
}
|
||||
|
||||
@ -38,7 +71,7 @@ async function createNote(parentNotePath, options = {}) {
|
||||
C-->D;`
|
||||
}
|
||||
|
||||
const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
|
||||
const {note, branch} = await server.post<Response>(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
|
||||
title: options.title,
|
||||
content: options.content || "",
|
||||
isProtected: options.isProtected,
|
||||
@ -49,7 +82,7 @@ async function createNote(parentNotePath, options = {}) {
|
||||
|
||||
if (options.saveSelection) {
|
||||
// we remove the selection only after it was saved to server to make sure we don't lose anything
|
||||
options.textEditor.removeSelection();
|
||||
options.textEditor?.removeSelection();
|
||||
}
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
@ -76,12 +109,14 @@ async function createNote(parentNotePath, options = {}) {
|
||||
}
|
||||
|
||||
async function chooseNoteType() {
|
||||
return new Promise(res => {
|
||||
return new Promise<ChooseNoteTypeResponse>(res => {
|
||||
// TODO: Remove ignore after callback for chooseNoteType is defined in app_context.ts
|
||||
//@ts-ignore
|
||||
appContext.triggerCommand("chooseNoteType", {callback: res});
|
||||
});
|
||||
}
|
||||
|
||||
async function createNoteWithTypePrompt(parentNotePath, options = {}) {
|
||||
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
|
||||
const {success, noteType, templateNoteId} = await chooseNoteType();
|
||||
|
||||
if (!success) {
|
||||
@ -95,12 +130,16 @@ async function createNoteWithTypePrompt(parentNotePath, options = {}) {
|
||||
}
|
||||
|
||||
/* If the first element is heading, parse it out and use it as a new heading. */
|
||||
function parseSelectedHtml(selectedHtml) {
|
||||
function parseSelectedHtml(selectedHtml: string) {
|
||||
const dom = $.parseHTML(selectedHtml);
|
||||
|
||||
// TODO: tagName and outerHTML appear to be missing.
|
||||
//@ts-ignore
|
||||
if (dom.length > 0 && dom[0].tagName && dom[0].tagName.match(/h[1-6]/i)) {
|
||||
const title = $(dom[0]).text();
|
||||
// remove the title from content (only first occurrence)
|
||||
// TODO: tagName and outerHTML appear to be missing.
|
||||
//@ts-ignore
|
||||
const content = selectedHtml.replace(dom[0].outerHTML, "");
|
||||
|
||||
return [title, content];
|
||||
@ -110,9 +149,9 @@ function parseSelectedHtml(selectedHtml) {
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateSubtree(noteId, parentNotePath) {
|
||||
async function duplicateSubtree(noteId: string, parentNotePath: string) {
|
||||
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
|
||||
const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`);
|
||||
const {note} = await server.post<DuplicateResponse>(`notes/${noteId}/duplicate/${parentNoteId}`);
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
@ -120,7 +159,7 @@ async function duplicateSubtree(noteId, parentNotePath) {
|
||||
activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
|
||||
|
||||
const origNote = await froca.getNote(noteId);
|
||||
toastService.showMessage(t("note_create.duplicated", { title: origNote.title }));
|
||||
toastService.showMessage(t("note_create.duplicated", { title: origNote?.title }));
|
||||
}
|
||||
|
||||
export default {
|
@ -5,6 +5,7 @@ import utils from "./utils.js";
|
||||
import attributeRenderer from "./attribute_renderer.js";
|
||||
import contentRenderer from "./content_renderer.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
|
||||
function setupGlobalTooltip() {
|
||||
$(document).on("mouseenter", "a", mouseEnterHandler);
|
||||
@ -24,11 +25,11 @@ function cleanUpTooltips() {
|
||||
$('.note-tooltip').remove();
|
||||
}
|
||||
|
||||
function setupElementTooltip($el) {
|
||||
function setupElementTooltip($el: JQuery<HTMLElement>) {
|
||||
$el.on('mouseenter', mouseEnterHandler);
|
||||
}
|
||||
|
||||
async function mouseEnterHandler() {
|
||||
async function mouseEnterHandler(this: HTMLElement) {
|
||||
const $link = $(this);
|
||||
|
||||
if ($link.hasClass("no-tooltip-preview") || $link.hasClass("disabled")) {
|
||||
@ -44,7 +45,7 @@ async function mouseEnterHandler() {
|
||||
const url = $link.attr("href") || $link.attr("data-href");
|
||||
const { notePath, noteId, viewScope } = linkService.parseNavigationStateFromUrl(url);
|
||||
|
||||
if (!notePath || viewScope.viewMode !== 'default') {
|
||||
if (!notePath || !noteId || viewScope?.viewMode !== 'default') {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -64,7 +65,7 @@ async function mouseEnterHandler() {
|
||||
new Promise(res => setTimeout(res, 500))
|
||||
]);
|
||||
|
||||
if (utils.isHtmlEmpty(content)) {
|
||||
if (!content || utils.isHtmlEmpty(content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -81,7 +82,8 @@ async function mouseEnterHandler() {
|
||||
// with bottom this flickering happens a bit less
|
||||
placement: 'bottom',
|
||||
trigger: 'manual',
|
||||
boundary: 'window',
|
||||
//TODO: boundary No longer applicable?
|
||||
//boundary: 'window',
|
||||
title: html,
|
||||
html: true,
|
||||
template: `<div class="tooltip note-tooltip ${tooltipClass}" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>`,
|
||||
@ -114,7 +116,7 @@ async function mouseEnterHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
async function renderTooltip(note) {
|
||||
async function renderTooltip(note: FNote | null) {
|
||||
if (!note) {
|
||||
return '<div>Note has been deleted.</div>';
|
||||
}
|
||||
@ -126,7 +128,11 @@ async function renderTooltip(note) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = `<h5 class="note-tooltip-title">${(await treeService.getNoteTitleWithPathAsSuffix(bestNotePath)).prop('outerHTML')}</h5>`;
|
||||
const noteTitleWithPathAsSuffix = await treeService.getNoteTitleWithPathAsSuffix(bestNotePath);
|
||||
let content = "";
|
||||
if (noteTitleWithPathAsSuffix) {
|
||||
content = `<h5 class="note-tooltip-title">${noteTitleWithPathAsSuffix.prop('outerHTML')}</h5>`;
|
||||
}
|
||||
|
||||
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
|
||||
|
@ -2,8 +2,22 @@ import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
async function getNoteTypeItems(command) {
|
||||
const items = [
|
||||
interface NoteTypeSeparator {
|
||||
title: "----"
|
||||
}
|
||||
|
||||
export interface NoteType {
|
||||
title: string;
|
||||
command?: string;
|
||||
type: string;
|
||||
uiIcon: string;
|
||||
templateNoteId?: string;
|
||||
}
|
||||
|
||||
type NoteTypeItem = NoteType | NoteTypeSeparator;
|
||||
|
||||
async function getNoteTypeItems(command?: string) {
|
||||
const items: NoteTypeItem[] = [
|
||||
{ title: t("note_types.text"), command: command, type: "text", uiIcon: "bx bx-note" },
|
||||
{ title: t("note_types.code"), command: command, type: "code", uiIcon: "bx bx-code" },
|
||||
{ title: t("note_types.saved-search"), command: command, type: "search", uiIcon: "bx bx-file-find" },
|
||||
@ -17,7 +31,7 @@ async function getNoteTypeItems(command) {
|
||||
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }
|
||||
];
|
||||
|
||||
const templateNoteIds = await server.get("search-templates");
|
||||
const templateNoteIds = await server.get<string[]>("search-templates");
|
||||
const templateNotes = await froca.getNotes(templateNoteIds);
|
||||
|
||||
if (templateNotes.length > 0) {
|
@ -1,7 +1,7 @@
|
||||
|
||||
import server from "./server.js";
|
||||
|
||||
type OptionValue = string | number;
|
||||
type OptionValue = number | string;
|
||||
|
||||
class Options {
|
||||
initializedPromise: Promise<void>;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import server from './server.js';
|
||||
import protectedSessionHolder from './protected_session_holder.js';
|
||||
import toastService from "./toast.js";
|
||||
import type { ToastOptions } from "./toast.js";
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import froca from "./froca.js";
|
||||
@ -8,7 +9,19 @@ import utils from "./utils.js";
|
||||
import options from "./options.js";
|
||||
import { t } from './i18n.js';
|
||||
|
||||
let protectedSessionDeferred = null;
|
||||
let protectedSessionDeferred: JQuery.Deferred<any, any, any> | null = null;
|
||||
|
||||
// TODO: Deduplicate with server when possible.
|
||||
interface Response {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
taskId: string;
|
||||
data: {
|
||||
protect: boolean
|
||||
}
|
||||
}
|
||||
|
||||
async function leaveProtectedSession() {
|
||||
if (protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||
@ -44,11 +57,11 @@ async function reloadData() {
|
||||
await froca.loadInitialTree();
|
||||
|
||||
// make sure that all notes used in the application are loaded, including the ones not shown in the tree
|
||||
await froca.reloadNotes(allNoteIds, true);
|
||||
await froca.reloadNotes(allNoteIds);
|
||||
}
|
||||
|
||||
async function setupProtectedSession(password) {
|
||||
const response = await server.post('login/protected', { password: password });
|
||||
async function setupProtectedSession(password: string) {
|
||||
const response = await server.post<Response>('login/protected', { password: password });
|
||||
|
||||
if (!response.success) {
|
||||
toastService.showError(t("protected_session.wrong_password"), 3000);
|
||||
@ -80,13 +93,13 @@ ws.subscribeToMessages(async message => {
|
||||
}
|
||||
});
|
||||
|
||||
async function protectNote(noteId, protect, includingSubtree) {
|
||||
async function protectNote(noteId: string, protect: boolean, includingSubtree: boolean) {
|
||||
await enterProtectedSession();
|
||||
|
||||
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`);
|
||||
}
|
||||
|
||||
function makeToast(message, title, text) {
|
||||
function makeToast(message: Message, title: string, text: string): ToastOptions {
|
||||
return {
|
||||
id: message.taskId,
|
||||
title,
|
@ -1,7 +1,8 @@
|
||||
import server from "./server.js";
|
||||
import bundleService from "./bundle.js";
|
||||
import bundleService, { Bundle } from "./bundle.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
|
||||
async function render(note, $el) {
|
||||
async function render(note: FNote, $el: JQuery<HTMLElement>) {
|
||||
const relations = note.getRelations('renderNote');
|
||||
const renderNoteIds = relations
|
||||
.map(rel => rel.value)
|
||||
@ -10,7 +11,7 @@ async function render(note, $el) {
|
||||
$el.empty().toggle(renderNoteIds.length > 0);
|
||||
|
||||
for (const renderNoteId of renderNoteIds) {
|
||||
const bundle = await server.post(`script/bundle/${renderNoteId}`);
|
||||
const bundle = await server.post<Bundle>(`script/bundle/${renderNoteId}`);
|
||||
|
||||
const $scriptContainer = $('<div>');
|
||||
$el.append($scriptContainer);
|
@ -1,9 +1,9 @@
|
||||
import options from "./options.js";
|
||||
|
||||
let leftInstance;
|
||||
let rightInstance;
|
||||
let leftInstance: ReturnType<typeof Split> | null;
|
||||
let rightInstance: ReturnType<typeof Split> | null;
|
||||
|
||||
function setupLeftPaneResizer(leftPaneVisible) {
|
||||
function setupLeftPaneResizer(leftPaneVisible: boolean) {
|
||||
if (leftInstance) {
|
||||
leftInstance.destroy();
|
||||
leftInstance = null;
|
@ -1,21 +1,25 @@
|
||||
import FrontendScriptApi from './frontend_script_api.js';
|
||||
import FrontendScriptApi, { Entity } from './frontend_script_api.js';
|
||||
import utils from './utils.js';
|
||||
import froca from './froca.js';
|
||||
|
||||
async function ScriptContext(startNoteId, allNoteIds, originEntity = null, $container = null) {
|
||||
const modules = {};
|
||||
async function ScriptContext(startNoteId: string, allNoteIds: string[], originEntity: Entity | null = null, $container: JQuery<HTMLElement> | null = null) {
|
||||
const modules: Record<string, { exports: unknown }> = {};
|
||||
|
||||
await froca.initializedPromise;
|
||||
|
||||
const startNote = await froca.getNote(startNoteId);
|
||||
const allNotes = await froca.getNotes(allNoteIds);
|
||||
|
||||
if (!startNote) {
|
||||
throw new Error(`Could not find start note ${startNoteId}.`);
|
||||
}
|
||||
|
||||
return {
|
||||
modules: modules,
|
||||
notes: utils.toObject(allNotes, note => [note.noteId, note]),
|
||||
apis: utils.toObject(allNotes, note => [note.noteId, new FrontendScriptApi(startNote, note, originEntity, $container)]),
|
||||
require: moduleNoteIds => {
|
||||
return moduleName => {
|
||||
require: (moduleNoteIds: string) => {
|
||||
return (moduleName: string) => {
|
||||
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
|
||||
const note = candidates.find(c => c.title === moduleName);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
|
||||
async function searchForNoteIds(searchString) {
|
||||
return await server.get(`search/${encodeURIComponent(searchString)}`);
|
||||
async function searchForNoteIds(searchString: string) {
|
||||
return await server.get<string[]>(`search/${encodeURIComponent(searchString)}`);
|
||||
}
|
||||
|
||||
async function searchForNotes(searchString) {
|
||||
async function searchForNotes(searchString: string) {
|
||||
const noteIds = await searchForNoteIds(searchString);
|
||||
|
||||
return await froca.getNotes(noteIds);
|
@ -1,14 +1,17 @@
|
||||
import utils from "./utils.js";
|
||||
|
||||
function removeGlobalShortcut(namespace) {
|
||||
type ElementType = HTMLElement | Document;
|
||||
type Handler = (e: JQuery.TriggeredEvent<ElementType, string, ElementType, ElementType>) => void;
|
||||
|
||||
function removeGlobalShortcut(namespace: string) {
|
||||
bindGlobalShortcut('', null, namespace);
|
||||
}
|
||||
|
||||
function bindGlobalShortcut(keyboardShortcut, handler, namespace = null) {
|
||||
function bindGlobalShortcut(keyboardShortcut: string, handler: Handler | null, namespace: string | null = null) {
|
||||
bindElShortcut($(document), keyboardShortcut, handler, namespace);
|
||||
}
|
||||
|
||||
function bindElShortcut($el, keyboardShortcut, handler, namespace = null) {
|
||||
function bindElShortcut($el: JQuery<ElementType>, keyboardShortcut: string, handler: Handler | null, namespace: string | null = null) {
|
||||
if (utils.isDesktop()) {
|
||||
keyboardShortcut = normalizeShortcut(keyboardShortcut);
|
||||
|
||||
@ -24,7 +27,9 @@ function bindElShortcut($el, keyboardShortcut, handler, namespace = null) {
|
||||
// method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted)
|
||||
if (keyboardShortcut) {
|
||||
$el.bind(eventName, keyboardShortcut, e => {
|
||||
if (handler) {
|
||||
handler(e);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -36,7 +41,7 @@ function bindElShortcut($el, keyboardShortcut, handler, namespace = null) {
|
||||
/**
|
||||
* Normalize to the form expected by the jquery.hotkeys.js
|
||||
*/
|
||||
function normalizeShortcut(shortcut) {
|
||||
function normalizeShortcut(shortcut: string): string {
|
||||
if (!shortcut) {
|
||||
return shortcut;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
type Callback = () => Promise<void>;
|
||||
type Callback = () => Promise<void> | void;
|
||||
|
||||
export default class SpacedUpdate {
|
||||
private updater: Callback;
|
||||
|
@ -2,8 +2,15 @@ import { t } from './i18n.js';
|
||||
import server from './server.js';
|
||||
import toastService from "./toast.js";
|
||||
|
||||
// TODO: De-duplicate with server once we have a commons.
|
||||
interface SyncResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
errorCode?: string;
|
||||
}
|
||||
|
||||
async function syncNow(ignoreNotConfigured = false) {
|
||||
const result = await server.post('sync/now');
|
||||
const result = await server.post<SyncResult>('sync/now');
|
||||
|
||||
if (result.success) {
|
||||
toastService.showMessage(t("sync.finished-successfully"));
|
@ -2,7 +2,7 @@ import library_loader from "./library_loader.js";
|
||||
import mime_types from "./mime_types.js";
|
||||
import options from "./options.js";
|
||||
|
||||
export function getStylesheetUrl(theme) {
|
||||
export function getStylesheetUrl(theme: string) {
|
||||
if (!theme) {
|
||||
return null;
|
||||
}
|
||||
@ -20,7 +20,7 @@ export function getStylesheetUrl(theme) {
|
||||
*
|
||||
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
|
||||
*/
|
||||
export async function applySyntaxHighlight($container) {
|
||||
export async function applySyntaxHighlight($container: JQuery<HTMLElement>) {
|
||||
if (!isSyntaxHighlightEnabled()) {
|
||||
return;
|
||||
}
|
||||
@ -38,11 +38,8 @@ export async function applySyntaxHighlight($container) {
|
||||
|
||||
/**
|
||||
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
|
||||
*
|
||||
* @param {*} $codeBlock
|
||||
* @param {*} normalizedMimeType
|
||||
*/
|
||||
export async function applySingleBlockSyntaxHighlight($codeBlock, normalizedMimeType) {
|
||||
export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLElement>, normalizedMimeType: string) {
|
||||
$codeBlock.parent().toggleClass("hljs");
|
||||
const text = $codeBlock.text();
|
||||
|
||||
@ -79,10 +76,10 @@ export function isSyntaxHighlightEnabled() {
|
||||
/**
|
||||
* Given a HTML element, tries to extract the `language-` class name out of it.
|
||||
*
|
||||
* @param {string} el the HTML element from which to extract the language tag.
|
||||
* @param el the HTML element from which to extract the language tag.
|
||||
* @returns the normalized MIME type (e.g. `text-css` instead of `language-text-css`).
|
||||
*/
|
||||
function extractLanguageFromClassList(el) {
|
||||
function extractLanguageFromClassList(el: HTMLElement) {
|
||||
const prefix = "language-";
|
||||
for (const className of el.classList) {
|
||||
if (className.startsWith(prefix)) {
|
@ -1,7 +1,7 @@
|
||||
import ws from "./ws.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
interface ToastOptions {
|
||||
export interface ToastOptions {
|
||||
id?: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
|
@ -6,9 +6,14 @@ import appContext from "../components/app_context.js";
|
||||
|
||||
export interface Node {
|
||||
getParent(): Node;
|
||||
getChildren(): Node[];
|
||||
folder: boolean;
|
||||
renderTitle(): void,
|
||||
data: {
|
||||
noteId?: string;
|
||||
isProtected?: boolean;
|
||||
branchId: string;
|
||||
noteType: string;
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,7 +149,7 @@ function getParentProtectedStatus(node: Node) {
|
||||
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
|
||||
}
|
||||
|
||||
function getNoteIdFromUrl(urlOrNotePath: string) {
|
||||
function getNoteIdFromUrl(urlOrNotePath: string | undefined) {
|
||||
if (!urlOrNotePath) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
function reloadFrontendApp(reason?: string) {
|
||||
if (reason) {
|
||||
@ -99,7 +98,7 @@ function isMac() {
|
||||
return navigator.platform.indexOf('Mac') > -1;
|
||||
}
|
||||
|
||||
function isCtrlKey(evt: KeyboardEvent) {
|
||||
function isCtrlKey(evt: KeyboardEvent | MouseEvent) {
|
||||
return (!isMac() && evt.ctrlKey)
|
||||
|| (isMac() && evt.metaKey);
|
||||
}
|
||||
@ -138,8 +137,8 @@ function formatSize(size: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function toObject<T>(array: T[], fn: (arg0: T) => [key: string, value: T]) {
|
||||
const obj: Record<string, T> = {};
|
||||
function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
|
||||
const obj: Record<string, R> = {};
|
||||
|
||||
for (const item of array) {
|
||||
const [key, value] = fn(item);
|
||||
@ -205,7 +204,9 @@ function getMimeTypeClass(mime: string) {
|
||||
|
||||
function closeActiveDialog() {
|
||||
if (glob.activeDialog) {
|
||||
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
|
||||
// TODO: Fix once we use proper ES imports.
|
||||
//@ts-ignore
|
||||
bootstrap.Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
|
||||
glob.activeDialog = null;
|
||||
}
|
||||
}
|
||||
@ -249,7 +250,9 @@ async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
|
||||
}
|
||||
|
||||
saveFocusedElement();
|
||||
Modal.getOrCreateInstance($dialog[0]).show();
|
||||
// TODO: Fix once we use proper ES imports.
|
||||
//@ts-ignore
|
||||
bootstrap.Modal.getOrCreateInstance($dialog[0]).show();
|
||||
|
||||
$dialog.on('hidden.bs.modal', () => {
|
||||
$(".aa-input").autocomplete("close");
|
||||
@ -350,7 +353,7 @@ function openHelp($button: JQuery<HTMLElement>) {
|
||||
}
|
||||
}
|
||||
|
||||
function initHelpButtons($el: JQuery<HTMLElement>) {
|
||||
function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) {
|
||||
// for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button)
|
||||
// so we do it manually
|
||||
$el.on("click", e => {
|
||||
|
@ -4,8 +4,8 @@ import server from "./server.js";
|
||||
import options from "./options.js";
|
||||
import frocaUpdater from "./froca_updater.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { EntityChange } from '../../../services/entity_changes_interface.js';
|
||||
import { t } from './i18n.js';
|
||||
import { EntityChange } from '../server_types.js';
|
||||
|
||||
type MessageHandler = (message: any) => void;
|
||||
const messageHandlers: MessageHandler[] = [];
|
||||
|
120
src/public/app/types.d.ts
vendored
120
src/public/app/types.d.ts
vendored
@ -1,4 +1,12 @@
|
||||
import FNote from "./entities/fnote";
|
||||
import type FNote from "./entities/fnote";
|
||||
import type { BackendModule, i18n } from "i18next";
|
||||
import type { Froca } from "./services/froca-interface";
|
||||
import type { HttpBackendOptions } from "i18next-http-backend";
|
||||
import { Suggestion } from "./services/note_autocomplete.ts";
|
||||
import utils from "./services/utils.ts";
|
||||
import appContext from "./components/app_context.ts";
|
||||
import server from "./services/server.ts";
|
||||
import library_loader, { Library } from "./services/library_loader.ts";
|
||||
|
||||
interface ElectronProcess {
|
||||
type: string;
|
||||
@ -6,16 +14,16 @@ interface ElectronProcess {
|
||||
}
|
||||
|
||||
interface CustomGlobals {
|
||||
isDesktop: boolean;
|
||||
isMobile: boolean;
|
||||
isDesktop: typeof utils.isDesktop;
|
||||
isMobile: typeof utils.isMobile;
|
||||
device: "mobile" | "desktop";
|
||||
getComponentsByEl: (el: unknown) => unknown;
|
||||
getHeaders: Promise<Record<string, string>>;
|
||||
getComponentByEl: typeof appContext.getComponentByEl;
|
||||
getHeaders: typeof server.getHeaders;
|
||||
getReferenceLinkTitle: (href: string) => Promise<string>;
|
||||
getReferenceLinkTitleSync: (href: string) => string;
|
||||
getActiveContextNote: FNote;
|
||||
requireLibrary: (library: string) => Promise<void>;
|
||||
ESLINT: { js: string[]; };
|
||||
requireLibrary: typeof library_loader.requireLibrary;
|
||||
ESLINT: Library;
|
||||
appContext: AppContext;
|
||||
froca: Froca;
|
||||
treeCache: Froca;
|
||||
@ -30,6 +38,9 @@ interface CustomGlobals {
|
||||
isMainWindow: boolean;
|
||||
maxEntityChangeIdAtLoad: number;
|
||||
maxEntityChangeSyncIdAtLoad: number;
|
||||
assetPath: string;
|
||||
instanceName: string;
|
||||
appCssNoteIds: string[];
|
||||
}
|
||||
|
||||
type RequireMethod = (moduleName: string) => any;
|
||||
@ -43,13 +54,94 @@ declare global {
|
||||
glob?: CustomGlobals;
|
||||
}
|
||||
|
||||
interface JQuery {
|
||||
autocomplete: (action: "close") => void;
|
||||
interface AutoCompleteConfig {
|
||||
appendTo?: HTMLElement | null;
|
||||
hint?: boolean;
|
||||
openOnFocus?: boolean;
|
||||
minLength?: number;
|
||||
tabAutocomplete?: boolean;
|
||||
autoselect?: boolean;
|
||||
dropdownMenuContainer?: HTMLElement;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
declare var logError: (message: string) => void;
|
||||
declare var logInfo: (message: string) => void;
|
||||
declare var glob: CustomGlobals;
|
||||
declare var require: RequireMethod;
|
||||
declare var __non_webpack_require__: RequireMethod | undefined;
|
||||
type AutoCompleteCallback = (values: AutoCompleteCallbackArg[]) => void;
|
||||
|
||||
interface AutoCompleteArg {
|
||||
displayKey: "name" | "value" | "notePathTitle";
|
||||
cache: boolean;
|
||||
source: (term: string, cb: AutoCompleteCallback) => void,
|
||||
templates?: {
|
||||
suggestion: (suggestion: Suggestion) => string | undefined
|
||||
}
|
||||
};
|
||||
|
||||
interface JQuery {
|
||||
autocomplete: (action?: "close" | "open" | "destroy" | "val" | AutoCompleteConfig, args?: AutoCompleteArg[] | string) => JQuery<?>;
|
||||
|
||||
getSelectedNotePath(): string | undefined;
|
||||
getSelectedNoteId(): string | null;
|
||||
setSelectedNotePath(notePath: string | null | undefined);
|
||||
getSelectedExternalLink(this: HTMLElement): string | undefined;
|
||||
setSelectedExternalLink(externalLink: string | null | undefined);
|
||||
setNote(noteId: string);
|
||||
}
|
||||
|
||||
var logError: (message: string, e?: Error) => void;
|
||||
var logInfo: (message: string) => void;
|
||||
var glob: CustomGlobals;
|
||||
var require: RequireMethod;
|
||||
var __non_webpack_require__: RequireMethod | undefined;
|
||||
|
||||
// Libraries
|
||||
// TODO: Replace once library loader is replaced with webpack.
|
||||
var i18next: i18n;
|
||||
var i18nextHttpBackend: BackendModule<HttpBackendOptions>;
|
||||
var hljs: {
|
||||
highlightAuto(text: string);
|
||||
highlight(text: string, {
|
||||
language: string
|
||||
});
|
||||
};
|
||||
var dayjs: {};
|
||||
var Split: (selectors: string[], config: {
|
||||
sizes: [ number, number ];
|
||||
gutterSize: number;
|
||||
onDragEnd: (sizes: [ number, number ]) => void;
|
||||
}) => {
|
||||
destroy();
|
||||
};
|
||||
var renderMathInElement: (element: HTMLElement, options: {
|
||||
trust: boolean;
|
||||
}) => void;
|
||||
var WZoom = {
|
||||
create(selector: string, opts: {
|
||||
maxScale: number;
|
||||
speed: number;
|
||||
zoomOnClick: boolean
|
||||
})
|
||||
};
|
||||
interface MermaidApi {
|
||||
initialize(opts: {
|
||||
startOnLoad: boolean,
|
||||
theme: string,
|
||||
securityLevel: "antiscript"
|
||||
}): void;
|
||||
render(selector: string, data: string);
|
||||
}
|
||||
interface MermaidLoader {
|
||||
|
||||
}
|
||||
var mermaid: {
|
||||
mermaidAPI: MermaidApi;
|
||||
registerLayoutLoaders(loader: MermaidLoader);
|
||||
parse(content: string, opts: {
|
||||
suppressErrors: true
|
||||
}): {
|
||||
config: {
|
||||
layout: string;
|
||||
}
|
||||
}
|
||||
};
|
||||
var MERMAID_ELK: MermaidLoader;
|
||||
}
|
||||
|
@ -9,6 +9,13 @@ import toastService from "../services/toast.js";
|
||||
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
|
||||
*/
|
||||
class BasicWidget extends Component {
|
||||
private attrs: Record<string, string>;
|
||||
private classes: string[];
|
||||
private childPositionCounter: number;
|
||||
private cssEl?: string;
|
||||
protected $widget!: JQuery<HTMLElement>;
|
||||
_noteId!: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -21,7 +28,7 @@ class BasicWidget extends Component {
|
||||
this.childPositionCounter = 10;
|
||||
}
|
||||
|
||||
child(...components) {
|
||||
child(...components: Component[]) {
|
||||
if (!components) {
|
||||
return this;
|
||||
}
|
||||
@ -43,11 +50,11 @@ class BasicWidget extends Component {
|
||||
/**
|
||||
* Conditionally adds the given components as children to this component.
|
||||
*
|
||||
* @param {boolean} condition whether to add the components.
|
||||
* @param {...any} components the components to be added as children to this component provided the condition is truthy.
|
||||
* @param condition whether to add the components.
|
||||
* @param components the components to be added as children to this component provided the condition is truthy.
|
||||
* @returns self for chaining.
|
||||
*/
|
||||
optChild(condition, ...components) {
|
||||
optChild(condition: boolean, ...components: Component[]) {
|
||||
if (condition) {
|
||||
return this.child(...components);
|
||||
} else {
|
||||
@ -55,12 +62,12 @@ class BasicWidget extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
id(id) {
|
||||
id(id: string) {
|
||||
this.attrs.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
class(className) {
|
||||
class(className: string) {
|
||||
this.classes.push(className);
|
||||
return this;
|
||||
}
|
||||
@ -68,11 +75,11 @@ class BasicWidget extends Component {
|
||||
/**
|
||||
* Sets the CSS attribute of the given name to the given value.
|
||||
*
|
||||
* @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
|
||||
* @param {string} value the value of the CSS attribute to set (e.g. `12px`).
|
||||
* @param name the name of the CSS attribute to set (e.g. `padding-left`).
|
||||
* @param value the value of the CSS attribute to set (e.g. `12px`).
|
||||
* @returns self for chaining.
|
||||
*/
|
||||
css(name, value) {
|
||||
css(name: string, value: string) {
|
||||
this.attrs.style += `${name}: ${value};`;
|
||||
return this;
|
||||
}
|
||||
@ -80,12 +87,12 @@ class BasicWidget extends Component {
|
||||
/**
|
||||
* Sets the CSS attribute of the given name to the given value, but only if the condition provided is truthy.
|
||||
*
|
||||
* @param {boolean} condition `true` in order to apply the CSS, `false` to ignore it.
|
||||
* @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
|
||||
* @param {string} value the value of the CSS attribute to set (e.g. `12px`).
|
||||
* @param condition `true` in order to apply the CSS, `false` to ignore it.
|
||||
* @param name the name of the CSS attribute to set (e.g. `padding-left`).
|
||||
* @param value the value of the CSS attribute to set (e.g. `12px`).
|
||||
* @returns self for chaining.
|
||||
*/
|
||||
optCss(condition, name, value) {
|
||||
optCss(condition: boolean, name: string, value: string) {
|
||||
if (condition) {
|
||||
return this.css(name, value);
|
||||
}
|
||||
@ -112,10 +119,9 @@ class BasicWidget extends Component {
|
||||
|
||||
/**
|
||||
* Accepts a string of CSS to add with the widget.
|
||||
* @param {string} block
|
||||
* @returns {this} for chaining
|
||||
* @returns for chaining
|
||||
*/
|
||||
cssBlock(block) {
|
||||
cssBlock(block: string) {
|
||||
this.cssEl = block;
|
||||
return this;
|
||||
}
|
||||
@ -123,7 +129,7 @@ class BasicWidget extends Component {
|
||||
render() {
|
||||
try {
|
||||
this.doRender();
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
this.logRenderingError(e);
|
||||
}
|
||||
|
||||
@ -163,7 +169,7 @@ class BasicWidget extends Component {
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
logRenderingError(e) {
|
||||
logRenderingError(e: Error) {
|
||||
console.log("Got issue in widget ", this);
|
||||
console.error(e);
|
||||
|
||||
@ -175,7 +181,7 @@ class BasicWidget extends Component {
|
||||
icon: "alert",
|
||||
message: t("toast.widget-error.message-custom", {
|
||||
id: noteId,
|
||||
title: note.title,
|
||||
title: note?.title,
|
||||
message: e.message
|
||||
})
|
||||
});
|
||||
@ -208,7 +214,7 @@ class BasicWidget extends Component {
|
||||
*/
|
||||
doRender() {}
|
||||
|
||||
toggleInt(show) {
|
||||
toggleInt(show: boolean) {
|
||||
this.$widget.toggleClass('hidden-int', !show);
|
||||
}
|
||||
|
||||
@ -216,7 +222,7 @@ class BasicWidget extends Component {
|
||||
return this.$widget.hasClass('hidden-int');
|
||||
}
|
||||
|
||||
toggleExt(show) {
|
||||
toggleExt(show: boolean) {
|
||||
this.$widget.toggleClass('hidden-ext', !show);
|
||||
}
|
||||
|
@ -2,9 +2,27 @@ import { t } from "../../services/i18n.js";
|
||||
import server from "../../services/server.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import FAttribute from "../../entities/fattribute.js";
|
||||
|
||||
export default class AbstractBulkAction {
|
||||
constructor(attribute, actionDef) {
|
||||
interface ActionDefinition {
|
||||
script: string;
|
||||
relationName: string;
|
||||
targetNoteId: string;
|
||||
targetParentNoteId: string;
|
||||
oldRelationName?: string;
|
||||
newRelationName?: string;
|
||||
newTitle?: string;
|
||||
labelName?: string;
|
||||
labelValue?: string;
|
||||
oldLabelName?: string;
|
||||
newLabelName?: string;
|
||||
}
|
||||
|
||||
export default abstract class AbstractBulkAction {
|
||||
attribute: FAttribute;
|
||||
actionDef: ActionDefinition;
|
||||
|
||||
constructor(attribute: FAttribute, actionDef: ActionDefinition) {
|
||||
this.attribute = attribute;
|
||||
this.actionDef = actionDef;
|
||||
}
|
||||
@ -20,18 +38,18 @@ export default class AbstractBulkAction {
|
||||
utils.initHelpDropdown($rendered);
|
||||
|
||||
return $rendered;
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e: any) {
|
||||
logError(`Failed rendering search action: ${JSON.stringify(this.attribute.dto)} with error: ${e.message} ${e.stack}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// to be overridden
|
||||
doRender() {}
|
||||
abstract doRender(): JQuery<HTMLElement>;
|
||||
static get actionName() { return ""; }
|
||||
|
||||
async saveAction(data) {
|
||||
const actionObject = Object.assign({ name: this.constructor.actionName }, data);
|
||||
async saveAction(data: {}) {
|
||||
const actionObject = Object.assign({ name: (this.constructor as typeof AbstractBulkAction).actionName }, data);
|
||||
|
||||
await server.put(`notes/${this.attribute.noteId}/attribute`, {
|
||||
attributeId: this.attribute.attributeId,
|
@ -27,7 +27,36 @@ const TPL = `
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
type ConfirmDialogCallback = (val: false | ConfirmDialogOptions) => void;
|
||||
|
||||
export interface ConfirmDialogOptions {
|
||||
confirmed: boolean;
|
||||
isDeleteNoteChecked: boolean
|
||||
}
|
||||
|
||||
// For "showConfirmDialog"
|
||||
|
||||
export interface ConfirmWithMessageOptions {
|
||||
message: string | HTMLElement | JQuery<HTMLElement>;
|
||||
callback: ConfirmDialogCallback;
|
||||
}
|
||||
|
||||
export interface ConfirmWithTitleOptions {
|
||||
title: string;
|
||||
callback: ConfirmDialogCallback;
|
||||
}
|
||||
|
||||
export default class ConfirmDialog extends BasicWidget {
|
||||
|
||||
private resolve: ConfirmDialogCallback | null;
|
||||
|
||||
private modal!: bootstrap.Modal;
|
||||
private $originallyFocused!: JQuery<HTMLElement> | null;
|
||||
private $confirmContent!: JQuery<HTMLElement>;
|
||||
private $okButton!: JQuery<HTMLElement>;
|
||||
private $cancelButton!: JQuery<HTMLElement>;
|
||||
private $custom!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -37,6 +66,8 @@ export default class ConfirmDialog extends BasicWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
// TODO: Fix once we use proper ES imports.
|
||||
//@ts-ignore
|
||||
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
|
||||
this.$confirmContent = this.$widget.find(".confirm-dialog-content");
|
||||
this.$okButton = this.$widget.find(".confirm-dialog-ok-button");
|
||||
@ -60,7 +91,7 @@ export default class ConfirmDialog extends BasicWidget {
|
||||
this.$okButton.on('click', () => this.doResolve(true));
|
||||
}
|
||||
|
||||
showConfirmDialogEvent({ message, callback }) {
|
||||
showConfirmDialogEvent({ message, callback }: ConfirmWithMessageOptions) {
|
||||
this.$originallyFocused = $(':focus');
|
||||
|
||||
this.$custom.hide();
|
||||
@ -78,7 +109,7 @@ export default class ConfirmDialog extends BasicWidget {
|
||||
this.resolve = callback;
|
||||
}
|
||||
|
||||
showConfirmDeleteNoteBoxWithNoteDialogEvent({ title, callback }) {
|
||||
showConfirmDeleteNoteBoxWithNoteDialogEvent({ title, callback }: ConfirmWithTitleOptions) {
|
||||
glob.activeDialog = this.$widget;
|
||||
|
||||
this.$confirmContent.text(`${t('confirm.are_you_sure_remove_note', { title: title })}`);
|
||||
@ -107,11 +138,13 @@ export default class ConfirmDialog extends BasicWidget {
|
||||
this.resolve = callback;
|
||||
}
|
||||
|
||||
doResolve(ret) {
|
||||
doResolve(ret: boolean) {
|
||||
if (this.resolve) {
|
||||
this.resolve({
|
||||
confirmed: ret,
|
||||
isDeleteNoteChecked: this.$widget.find(`.${DELETE_NOTE_BUTTON_CLASS}:checked`).length > 0
|
||||
});
|
||||
}
|
||||
|
||||
this.resolve = null;
|
||||
|
@ -4,6 +4,25 @@ import linkService from "../../services/link.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import FAttribute, { FAttributeRow } from "../../entities/fattribute.js";
|
||||
|
||||
// TODO: Use common with server.
|
||||
interface Response {
|
||||
noteIdsToBeDeleted: string[];
|
||||
brokenRelations: FAttributeRow[];
|
||||
}
|
||||
|
||||
export interface ResolveOptions {
|
||||
proceed: boolean;
|
||||
deleteAllClones?: boolean;
|
||||
eraseNotes?: boolean;
|
||||
}
|
||||
|
||||
interface ShowDeleteNotesDialogOpts {
|
||||
branchIdsToDelete: string[];
|
||||
callback: (opts: ResolveOptions) => void;
|
||||
forceDeleteAllClones: boolean;
|
||||
}
|
||||
|
||||
const TPL = `
|
||||
<div class="delete-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||
@ -54,11 +73,29 @@ const TPL = `
|
||||
</div>`;
|
||||
|
||||
export default class DeleteNotesDialog extends BasicWidget {
|
||||
|
||||
private branchIds: string[] | null;
|
||||
private resolve!: (options: ResolveOptions) => void;
|
||||
|
||||
private $content!: JQuery<HTMLElement>;
|
||||
private $okButton!: JQuery<HTMLElement>;
|
||||
private $cancelButton!: JQuery<HTMLElement>;
|
||||
private $deleteNotesList!: JQuery<HTMLElement>;
|
||||
private $brokenRelationsList!: JQuery<HTMLElement>;
|
||||
private $deletedNotesCount!: JQuery<HTMLElement>;
|
||||
private $noNoteToDeleteWrapper!: JQuery<HTMLElement>;
|
||||
private $deleteNotesListWrapper!: JQuery<HTMLElement>;
|
||||
private $brokenRelationsListWrapper!: JQuery<HTMLElement>;
|
||||
private $brokenRelationsCount!: JQuery<HTMLElement>;
|
||||
private $deleteAllClones!: JQuery<HTMLElement>;
|
||||
private $eraseNotes!: JQuery<HTMLElement>;
|
||||
|
||||
private forceDeleteAllClones?: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.branchIds = null;
|
||||
this.resolve = null;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
@ -98,7 +135,7 @@ export default class DeleteNotesDialog extends BasicWidget {
|
||||
}
|
||||
|
||||
async renderDeletePreview() {
|
||||
const response = await server.post('delete-notes-preview', {
|
||||
const response = await server.post<Response>('delete-notes-preview', {
|
||||
branchIdsToDelete: this.branchIds,
|
||||
deleteAllClones: this.forceDeleteAllClones || this.isDeleteAllClonesChecked()
|
||||
});
|
||||
@ -135,7 +172,7 @@ export default class DeleteNotesDialog extends BasicWidget {
|
||||
}
|
||||
}
|
||||
|
||||
async showDeleteNotesDialogEvent({branchIdsToDelete, callback, forceDeleteAllClones}) {
|
||||
async showDeleteNotesDialogEvent({branchIdsToDelete, callback, forceDeleteAllClones}: ShowDeleteNotesDialogOpts) {
|
||||
this.branchIds = branchIdsToDelete;
|
||||
this.forceDeleteAllClones = forceDeleteAllClones;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import noteTypesService from "../../services/note_types.js";
|
||||
import noteTypesService, { NoteType } from "../../services/note_types.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
|
||||
const TPL = `
|
||||
@ -41,9 +41,25 @@ const TPL = `
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export interface ChooseNoteTypeResponse {
|
||||
success: boolean;
|
||||
noteType?: string;
|
||||
templateNoteId?: string;
|
||||
}
|
||||
|
||||
type Callback = (data: ChooseNoteTypeResponse) => void;
|
||||
|
||||
export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
private resolve: Callback | null;
|
||||
private dropdown!: bootstrap.Dropdown;
|
||||
private modal!: JQuery<HTMLElement>;
|
||||
private $noteTypeDropdown!: JQuery<HTMLElement>;
|
||||
private $originalFocused: JQuery<HTMLElement> | null;
|
||||
private $originalDialog: JQuery<HTMLElement> | null;
|
||||
|
||||
constructor(props: {}) {
|
||||
super();
|
||||
|
||||
this.resolve = null;
|
||||
this.$originalFocused = null; // element focused before the dialog was opened, so we can return to it afterward
|
||||
@ -52,9 +68,13 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
// TODO: Remove once we import bootstrap the right way
|
||||
//@ts-ignore
|
||||
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
|
||||
|
||||
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
|
||||
// TODO: Remove once we import bootstrap the right way
|
||||
//@ts-ignore
|
||||
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger"));
|
||||
|
||||
this.$widget.on("hidden.bs.modal", () => {
|
||||
@ -88,13 +108,15 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
|
||||
this.$noteTypeDropdown.parent().on('hide.bs.dropdown', e => {
|
||||
// prevent closing dropdown by clicking outside
|
||||
// TODO: Check if this actually works.
|
||||
//@ts-ignore
|
||||
if (e.clickEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async chooseNoteTypeEvent({ callback }) {
|
||||
async chooseNoteTypeEvent({ callback }: { callback: Callback }) {
|
||||
this.$originalFocused = $(':focus');
|
||||
|
||||
const noteTypes = await noteTypesService.getNoteTypeItems();
|
||||
@ -104,13 +126,12 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
for (const noteType of noteTypes) {
|
||||
if (noteType.title === '----') {
|
||||
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates")));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.$noteTypeDropdown.append(
|
||||
$('<a class="dropdown-item" tabindex="0">')
|
||||
.attr("data-note-type", noteType.type)
|
||||
.attr("data-template-note-id", noteType.templateNoteId)
|
||||
.append($("<span>").addClass(noteType.uiIcon))
|
||||
.attr("data-note-type", (noteType as NoteType).type)
|
||||
.attr("data-template-note-id", (noteType as NoteType).templateNoteId || "")
|
||||
.append($("<span>").addClass((noteType as NoteType).uiIcon))
|
||||
.append(` ${noteType.title}`)
|
||||
);
|
||||
}
|
||||
@ -127,16 +148,18 @@ export default class NoteTypeChooserDialog extends BasicWidget {
|
||||
this.resolve = callback;
|
||||
}
|
||||
|
||||
doResolve(e) {
|
||||
doResolve(e: JQuery.KeyDownEvent | JQuery.ClickEvent) {
|
||||
const $item = $(e.target).closest(".dropdown-item");
|
||||
const noteType = $item.attr("data-note-type");
|
||||
const templateNoteId = $item.attr("data-template-note-id");
|
||||
|
||||
if (this.resolve) {
|
||||
this.resolve({
|
||||
success: true,
|
||||
noteType,
|
||||
templateNoteId
|
||||
});
|
||||
}
|
||||
this.resolve = null;
|
||||
|
||||
this.modal.hide();
|
@ -20,7 +20,34 @@ const TPL = `
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
interface ShownCallbackData {
|
||||
$dialog: JQuery<HTMLElement>;
|
||||
$question: JQuery<HTMLElement> | null;
|
||||
$answer: JQuery<HTMLElement> | null;
|
||||
$form: JQuery<HTMLElement>;
|
||||
}
|
||||
|
||||
export interface PromptDialogOptions {
|
||||
title?: string;
|
||||
message?: string;
|
||||
defaultValue?: string;
|
||||
shown: PromptShownDialogCallback;
|
||||
callback: (value: unknown) => void;
|
||||
}
|
||||
|
||||
export type PromptShownDialogCallback = ((callback: ShownCallbackData) => void) | null;
|
||||
|
||||
export default class PromptDialog extends BasicWidget {
|
||||
|
||||
private resolve: ((val: string | null) => void) | null;
|
||||
private shownCb: PromptShownDialogCallback;
|
||||
|
||||
private modal!: bootstrap.Modal;
|
||||
private $dialogBody!: JQuery<HTMLElement>;
|
||||
private $question!: JQuery<HTMLElement> | null;
|
||||
private $answer!: JQuery<HTMLElement> | null;
|
||||
private $form!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -30,6 +57,8 @@ export default class PromptDialog extends BasicWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
// TODO: Fix once we use proper ES imports.
|
||||
//@ts-ignore
|
||||
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
|
||||
this.$dialogBody = this.$widget.find(".modal-body");
|
||||
this.$form = this.$widget.find(".prompt-dialog-form");
|
||||
@ -46,7 +75,7 @@ export default class PromptDialog extends BasicWidget {
|
||||
});
|
||||
}
|
||||
|
||||
this.$answer.trigger('focus').select();
|
||||
this.$answer?.trigger('focus').select();
|
||||
});
|
||||
|
||||
this.$widget.on("hidden.bs.modal", () => {
|
||||
@ -57,13 +86,15 @@ export default class PromptDialog extends BasicWidget {
|
||||
|
||||
this.$form.on('submit', e => {
|
||||
e.preventDefault();
|
||||
this.resolve(this.$answer.val());
|
||||
if (this.resolve) {
|
||||
this.resolve(this.$answer?.val() as string);
|
||||
}
|
||||
|
||||
this.modal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
showPromptDialogEvent({ title, message, defaultValue, shown, callback }) {
|
||||
showPromptDialogEvent({ title, message, defaultValue, shown, callback }: PromptDialogOptions) {
|
||||
this.shownCb = shown;
|
||||
this.resolve = callback;
|
||||
|
||||
@ -71,7 +102,7 @@ export default class PromptDialog extends BasicWidget {
|
||||
|
||||
this.$question = $("<label>")
|
||||
.prop("for", "prompt-dialog-answer")
|
||||
.text(message);
|
||||
.text(message || "");
|
||||
|
||||
this.$answer = $("<input>")
|
||||
.prop("type", "text")
|
@ -7,6 +7,7 @@
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"lib": [
|
||||
"ES2022"
|
||||
],
|
||||
@ -18,14 +19,14 @@
|
||||
"./src/**/*.js",
|
||||
"./src/**/*.ts",
|
||||
"./*.ts",
|
||||
"./spec/**/*.ts",
|
||||
"./spec-es6/**/*.ts"
|
||||
"./spec/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"./src/public/**/*",
|
||||
"./node_modules/**/*"
|
||||
"./node_modules/**/*",
|
||||
"./spec-es6/**/*.ts"
|
||||
],
|
||||
"files": [
|
||||
"src/types.d.ts"
|
||||
"src/types.d.ts",
|
||||
"src/public/app/types.d.ts"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user