Merge pull request #821 from TriliumNext/feature/client_typescript_port2

Port frontend to TypeScript (36.7% -> 48.5%)
This commit is contained in:
Elian Doran 2024-12-22 15:23:01 +02:00 committed by GitHub
commit 2ec903893c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 1475 additions and 822 deletions

View File

@ -10,4 +10,4 @@ echo By file
cloc HEAD \ cloc HEAD \
--git --md \ --git --md \
--include-lang=javascript,typescript \ --include-lang=javascript,typescript \
--by-file --by-file | grep \.js\|

View File

@ -29,7 +29,7 @@ const copy = async () => {
fs.copySync(path.join("build", srcFile), destFile, { recursive: true }); 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) { for (const file of filesToCopy) {
log(`Copying ${file}`); log(`Copying ${file}`);
await fs.copy(file, path.join(DEST_DIR, file)); await fs.copy(file, path.join(DEST_DIR, file));

View File

@ -14,6 +14,10 @@ import MainTreeExecutors from "./main_tree_executors.js";
import toast from "../services/toast.js"; import toast from "../services/toast.js";
import ShortcutComponent from "./shortcut_component.js"; import ShortcutComponent from "./shortcut_component.js";
import { t, initLocale } from "../services/i18n.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 { interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget; getRootWidget: (appContext: AppContext) => RootWidget;
@ -27,13 +31,72 @@ interface BeforeUploadListener extends Component {
beforeUnloadEvent(): boolean; beforeUnloadEvent(): boolean;
} }
interface TriggerData { interface CommandData {
noteId?: string; ntxId?: string;
noteIds?: string[];
messages?: unknown[];
callback?: () => void;
} }
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 { class AppContext extends Component {
isMainWindow: boolean; isMainWindow: boolean;
@ -127,11 +190,15 @@ class AppContext extends Component {
this.triggerEvent('initialRenderComplete'); 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); 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) { for (const executor of this.components) {
const fun = (executor as any)[`${name}Command`]; const fun = (executor as any)[`${name}Command`];
@ -144,7 +211,7 @@ class AppContext extends Component {
// in the component tree to communicate with each other // in the component tree to communicate with each other
console.debug(`Unhandled command ${name}, converting to event.`); console.debug(`Unhandled command ${name}, converting to event.`);
return this.triggerEvent(name, data); return this.triggerEvent(name, data as CommandAndEventMappings[K]);
} }
getComponentByEl(el: HTMLElement) { getComponentByEl(el: HTMLElement) {

View File

@ -16,6 +16,7 @@ export default class Component {
children: Component[]; children: Component[];
initialized: Promise<void> | null; initialized: Promise<void> | null;
parent?: Component; parent?: Component;
position!: number;
constructor() { constructor() {
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`; this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;

View File

@ -21,10 +21,11 @@ class FAttachment {
attachmentId!: string; attachmentId!: string;
private ownerId!: string; private ownerId!: string;
role!: string; role!: string;
private mime!: string; mime!: string;
private title!: string; title!: string;
isProtected!: boolean; // TODO: Is this used?
private dateModified!: string; private dateModified!: string;
private utcDateModified!: string; utcDateModified!: string;
private utcDateScheduledForErasureSince!: string; private utcDateScheduledForErasureSince!: string;
/** /**
* optionally added to the entity * optionally added to the entity

View 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;
}

View File

@ -1,11 +1,19 @@
import { AttributeType } from "../entities/fattribute.js";
import server from "./server.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 $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 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? * @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")) { if (!$el.hasClass("aa-input")) {
$el.autocomplete({ $el.autocomplete({
appendTo: document.querySelector('body'), appendTo: document.querySelector('body'),
@ -20,7 +28,7 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
source: async (term, cb) => { source: async (term, cb) => {
const type = typeof attributeType === "function" ? attributeType() : attributeType; 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})); const result = names.map(name => ({name}));
cb(result); 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")) { if ($el.hasClass("aa-input")) {
// we reinit every time because autocomplete seems to have a bug where it retains state from last // we reinit every time because autocomplete seems to have a bug where it retains state from last
// open even though the value was reset // open even though the value was reset
@ -52,7 +60,7 @@ async function initLabelValueAutocomplete({ $el, open, nameCallback }) {
return; return;
} }
const attributeValues = (await server.get(`attribute-values/${encodeURIComponent(attributeName)}`)) const attributeValues = (await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`))
.map(attribute => ({ value: attribute })); .map(attribute => ({ value: attribute }));
if (attributeValues.length === 0) { if (attributeValues.length === 0) {

View File

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

View File

@ -1,14 +1,30 @@
import FAttribute, { AttributeType, FAttributeRow } from "../entities/fattribute.js";
import utils from "./utils.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(); str = str.trim();
const tokens = []; const tokens: Token[] = [];
let quotes = false; let quotes: boolean | string = false;
let currentWord = ''; let currentWord = '';
function isOperatorSymbol(chr) { function isOperatorSymbol(chr: string) {
return ['=', '*', '>', '<', '!'].includes(chr); return ['=', '*', '>', '<', '!'].includes(chr);
} }
@ -24,7 +40,7 @@ function lex(str) {
/** /**
* @param endIndex - index of the last character of the token * @param endIndex - index of the last character of the token
*/ */
function finishWord(endIndex) { function finishWord(endIndex: number) {
if (currentWord === '') { if (currentWord === '') {
return; return;
} }
@ -107,7 +123,7 @@ function lex(str) {
return tokens; return tokens;
} }
function checkAttributeName(attrName) { function checkAttributeName(attrName: string) {
if (attrName.length === 0) { if (attrName.length === 0) {
throw new Error("Attribute name is empty, please fill the name."); throw new Error("Attribute name is empty, please fill the name.");
} }
@ -117,10 +133,10 @@ function checkAttributeName(attrName) {
} }
} }
function parse(tokens, str, allowEmptyRelations = false) { function parse(tokens: Token[], str: string, allowEmptyRelations = false) {
const attrs = []; const attrs: Attribute[] = [];
function context(i) { function context(i: number) {
let { startIndex, endIndex } = tokens[i]; let { startIndex, endIndex } = tokens[i];
startIndex = Math.max(0, startIndex - 20); startIndex = Math.max(0, startIndex - 20);
endIndex = Math.min(str.length, endIndex + 20); endIndex = Math.min(str.length, endIndex + 20);
@ -151,7 +167,7 @@ function parse(tokens, str, allowEmptyRelations = false) {
checkAttributeName(labelName); checkAttributeName(labelName);
const attr = { const attr: Attribute = {
type: 'label', type: 'label',
name: labelName, name: labelName,
isInheritable: isInheritable(), isInheritable: isInheritable(),
@ -177,7 +193,7 @@ function parse(tokens, str, allowEmptyRelations = false) {
checkAttributeName(relationName); checkAttributeName(relationName);
const attr = { const attr: Attribute = {
type: 'relation', type: 'relation',
name: relationName, name: relationName,
isInheritable: isInheritable(), isInheritable: isInheritable(),
@ -216,7 +232,7 @@ function parse(tokens, str, allowEmptyRelations = false) {
return attrs; return attrs;
} }
function lexAndParse(str, allowEmptyRelations = false) { function lexAndParse(str: string, allowEmptyRelations = false) {
const tokens = lex(str); const tokens = lex(str);
return parse(tokens, str, allowEmptyRelations); return parse(tokens, str, allowEmptyRelations);

View File

@ -1,7 +1,9 @@
import ws from "./ws.js"; import ws from "./ws.js";
import froca from "./froca.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 isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : '';
const $attr = $("<span>"); 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 // when the relation has just been created, then it might not have a value
if (attribute.value) { if (attribute.value) {
$attr.append(document.createTextNode(`~${attribute.name}${isInheritable}=`)); $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 { } else {
ws.logError(`Unknown attr type: ${attribute.type}`); ws.logError(`Unknown attr type: ${attribute.type}`);
@ -29,7 +35,7 @@ async function renderAttribute(attribute, renderIsInheritable) {
return $attr; return $attr;
} }
function formatValue(val) { function formatValue(val: string) {
if (/^[\p{L}\p{N}\-_,.]+$/u.test(val)) { if (/^[\p{L}\p{N}\-_,.]+$/u.test(val)) {
return 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); const note = await froca.getNote(noteId);
if (!note) { if (!note) {
@ -61,7 +67,7 @@ async function createLink(noteId) {
.text(note.title); .text(note.title);
} }
async function renderAttributes(attributes, renderIsInheritable) { async function renderAttributes(attributes: FAttribute[], renderIsInheritable: boolean) {
const $container = $('<span class="rendered-note-attributes">'); const $container = $('<span class="rendered-note-attributes">');
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
@ -89,7 +95,7 @@ const HIDDEN_ATTRIBUTES = [
'viewType' 'viewType'
]; ];
async function renderNormalAttributes(note) { async function renderNormalAttributes(note: FNote) {
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
let attrs = note.getAttributes(); let attrs = note.getAttributes();

View File

@ -1,7 +1,8 @@
import server from './server.js'; import server from './server.js';
import froca from './froca.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`, { await server.put(`notes/${noteId}/attribute`, {
type: 'label', type: 'label',
name: name, 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`, { await server.put(`notes/${noteId}/set-attribute`, {
type: 'label', type: 'label',
name: name, 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}`); 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 * 2. attribute is owned by the template of the note
* 3. attribute is owned by some note's ancestor and is inheritable * 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) { if (!affectedNote || !attrRow) {
return false; return false;
} }
@ -48,6 +49,7 @@ function isAffecting(attrRow, affectedNote) {
} }
} }
// TODO: This doesn't seem right.
if (this.isInheritable) { if (this.isInheritable) {
for (const owningNote of owningNotes) { for (const owningNote of owningNotes) {
if (owningNote.hasAncestor(attrNote.noteId, true)) { if (owningNote.hasAncestor(attrNote.noteId, true)) {

View File

@ -1,17 +1,28 @@
import utils from './utils.js'; import utils from './utils.js';
import server from './server.js'; import server from './server.js';
import toastService from "./toast.js"; import toastService, { ToastOptions } from "./toast.js";
import froca from "./froca.js"; import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js"; import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js"; import ws from "./ws.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import { t } from './i18n.js'; import { t } from './i18n.js';
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 = filterRootNote(branchIdsToMove);
branchIdsToMove = filterSearchBranches(branchIdsToMove); branchIdsToMove = filterSearchBranches(branchIdsToMove);
const beforeBranch = froca.getBranch(beforeBranchId); const beforeBranch = froca.getBranch(beforeBranchId);
if (!beforeBranch) {
return;
}
if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) { if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) {
toastService.showError(t("branches.cannot-move-notes-here")); toastService.showError(t("branches.cannot-move-notes-here"));
@ -19,7 +30,7 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
} }
for (const branchIdToMove of branchIdsToMove) { 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) { if (!resp.success) {
toastService.showError(resp.message); 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 = filterRootNote(branchIdsToMove);
branchIdsToMove = filterSearchBranches(branchIdsToMove); branchIdsToMove = filterSearchBranches(branchIdsToMove);
const afterNote = froca.getBranch(afterBranchId).getNote(); const afterNote = await froca.getBranch(afterBranchId)?.getNote();
if (!afterNote) {
return;
}
const forbiddenNoteIds = [ const forbiddenNoteIds = [
'root', 'root',
@ -50,7 +64,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
branchIdsToMove.reverse(); // need to reverse to keep the note order branchIdsToMove.reverse(); // need to reverse to keep the note order
for (const branchIdToMove of branchIdsToMove) { 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) { if (!resp.success) {
toastService.showError(resp.message); 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); const newParentBranch = froca.getBranch(newParentBranchId);
if (!newParentBranch) {
return;
}
if (newParentBranch.noteId === '_lbRoot') { if (newParentBranch.noteId === '_lbRoot') {
toastService.showError(t("branches.cannot-move-notes-here")); toastService.showError(t("branches.cannot-move-notes-here"));
@ -72,12 +89,13 @@ async function moveToParentNote(branchIdsToMove, newParentBranchId) {
for (const branchIdToMove of branchIdsToMove) { for (const branchIdToMove of branchIdsToMove) {
const branchToMove = froca.getBranch(branchIdToMove); const branchToMove = froca.getBranch(branchIdToMove);
if (branchToMove.noteId === hoistedNoteService.getHoistedNoteId() if (!branchToMove
|| (await branchToMove.getParentNote()).type === 'search') { || branchToMove.noteId === hoistedNoteService.getHoistedNoteId()
|| (await branchToMove.getParentNote())?.type === 'search') {
continue; 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) { if (!resp.success) {
toastService.showError(resp.message); 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); branchIdsToDelete = filterRootNote(branchIdsToDelete);
if (branchIdsToDelete.length === 0) { if (branchIdsToDelete.length === 0) {
@ -100,7 +118,7 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
deleteAllClones = false; deleteAllClones = false;
} }
else { else {
({proceed, deleteAllClones, eraseNotes} = await new Promise(res => ({proceed, deleteAllClones, eraseNotes} = await new Promise<ResolveOptions>(res =>
appContext.triggerCommand('showDeleteNotesDialog', {branchIdsToDelete, callback: res, forceDeleteAllClones}))); appContext.triggerCommand('showDeleteNotesDialog', {branchIdsToDelete, callback: res, forceDeleteAllClones})));
} }
@ -127,10 +145,9 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
const branch = froca.getBranch(branchIdToDelete); const branch = froca.getBranch(branchIdToDelete);
if (deleteAllClones) { if (deleteAllClones && branch) {
await server.remove(`notes/${branch.noteId}${query}`); await server.remove(`notes/${branch.noteId}${query}`);
} } else {
else {
await server.remove(`branches/${branchIdToDelete}${query}`); 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) if (hoistedNoteService.isHoistedNode(node)
|| hoistedNoteService.isTopLevelNode(node) || hoistedNoteService.isTopLevelNode(node)
|| node.getParent().data.noteType === 'search') { || node.getParent().data.noteType === 'search') {
@ -162,7 +179,7 @@ async function moveNodeUpInHierarchy(node) {
const targetBranchId = node.getParent().data.branchId; const targetBranchId = node.getParent().data.branchId;
const branchIdToMove = node.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) { if (!resp.success) {
toastService.showError(resp.message); 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-')); return branchIds.filter(branchId => !branchId.startsWith('virt-'));
} }
function filterRootNote(branchIds) { function filterRootNote(branchIds: string[]) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
return branchIds.filter(branchId => { return branchIds.filter(branchId => {
const branch = froca.getBranch(branchId); const branch = froca.getBranch(branchId);
if (!branch) {
return false;
}
return branch.noteId !== 'root' return branch.noteId !== 'root'
&& branch.noteId !== hoistedNoteId; && branch.noteId !== hoistedNoteId;
}); });
} }
function makeToast(id, message) { function makeToast(id: string, message: string): ToastOptions {
return { return {
id: id, id: id,
title: t("branches.delete-status"), title: t("branches.delete-status"),
@ -235,8 +255,8 @@ ws.subscribeToMessages(async message => {
} }
}); });
async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) { async function cloneNoteToBranch(childNoteId: string, parentBranchId: string, prefix?: string) {
const resp = await server.put(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, { const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-branch/${parentBranchId}`, {
prefix: prefix prefix: prefix
}); });
@ -245,8 +265,8 @@ async function cloneNoteToBranch(childNoteId, parentBranchId, prefix) {
} }
} }
async function cloneNoteToParentNote(childNoteId, parentNoteId, prefix) { async function cloneNoteToParentNote(childNoteId: string, parentNoteId: string, prefix: string) {
const resp = await server.put(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, { const resp = await server.put<Response>(`notes/${childNoteId}/clone-to-note/${parentNoteId}`, {
prefix: prefix prefix: prefix
}); });
@ -256,8 +276,8 @@ async function cloneNoteToParentNote(childNoteId, parentNoteId, prefix) {
} }
// beware that the first arg is noteId and the second is branchId! // beware that the first arg is noteId and the second is branchId!
async function cloneNoteAfter(noteId, afterBranchId) { async function cloneNoteAfter(noteId: string, afterBranchId: string) {
const resp = await server.put(`notes/${noteId}/clone-after/${afterBranchId}`); const resp = await server.put<Response>(`notes/${noteId}/clone-after/${afterBranchId}`);
if (!resp.success) { if (!resp.success) {
toastService.showError(resp.message); toastService.showError(resp.message);

View File

@ -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 AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import FNote from "../entities/fnote.js";
const ACTION_GROUPS = [ const ACTION_GROUPS = [
{ {
@ -50,7 +51,7 @@ const ACTION_CLASSES = [
ExecuteScriptBulkAction ExecuteScriptBulkAction
]; ];
async function addAction(noteId, actionName) { async function addAction(noteId: string, actionName: string) {
await server.post(`notes/${noteId}/attributes`, { await server.post(`notes/${noteId}/attributes`, {
type: 'label', type: 'label',
name: 'action', name: 'action',
@ -62,7 +63,7 @@ async function addAction(noteId, actionName) {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
} }
function parseActions(note) { function parseActions(note: FNote) {
const actionLabels = note.getLabels('action'); const actionLabels = note.getLabels('action');
return actionLabels.map(actionAttr => { return actionLabels.map(actionAttr => {
@ -70,7 +71,7 @@ function parseActions(note) {
try { try {
actionDef = JSON.parse(actionAttr.value); actionDef = JSON.parse(actionAttr.value);
} catch (e) { } catch (e: any) {
logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`); logError(`Parsing of attribute: '${actionAttr.value}' failed with error: ${e.message}`);
return null; return null;
} }

View File

@ -4,9 +4,22 @@ import toastService from "./toast.js";
import froca from "./froca.js"; import froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import { Entity } from "./frontend_script_api.js";
async function getAndExecuteBundle(noteId, originEntity = null, script = null, params = null) { // TODO: Deduplicate with server.
const bundle = await server.post(`script/bundle/${noteId}`, { 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, script,
params params
}); });
@ -14,24 +27,23 @@ async function getAndExecuteBundle(noteId, originEntity = null, script = null, p
return await executeBundle(bundle, originEntity); 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); const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
try { try {
return await (function () { return await (function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
}.call(apiContext)); }.call(apiContext));
} } catch (e: any) {
catch (e) {
const note = await froca.getNote(bundle.noteId); 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() { async function executeStartupBundles() {
const isMobile = utils.isMobile(); 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) { for (const bundle of scriptBundles) {
await executeBundle(bundle); await executeBundle(bundle);
@ -39,11 +51,14 @@ async function executeStartupBundles() {
} }
class WidgetsByParent { class WidgetsByParent {
private byParent: Record<string, Widget[]>;
constructor() { constructor() {
this.byParent = {}; this.byParent = {};
} }
add(widget) { add(widget: Widget) {
if (!widget.parentWidget) { if (!widget.parentWidget) {
console.log(`Custom widget does not have mandatory 'parentWidget' property defined`); console.log(`Custom widget does not have mandatory 'parentWidget' property defined`);
return; return;
@ -53,7 +68,7 @@ class WidgetsByParent {
this.byParent[widget.parentWidget].push(widget); this.byParent[widget.parentWidget].push(widget);
} }
get(parentName) { get(parentName: string) {
if (!this.byParent[parentName]) { if (!this.byParent[parentName]) {
return []; return [];
} }
@ -62,12 +77,12 @@ class WidgetsByParent {
// previously, custom widgets were provided as a single instance, but that has the disadvantage // 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 // for splits where we actually need multiple instaces and thus having a class to instantiate is better
// https://github.com/zadam/trilium/issues/4274 // 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() { async function getWidgetBundlesByParent() {
const scriptBundles = await server.get("script/widgets"); const scriptBundles = await server.get<Bundle[]>("script/widgets");
const widgetsByParent = new WidgetsByParent(); const widgetsByParent = new WidgetsByParent();
@ -80,7 +95,7 @@ async function getWidgetBundlesByParent() {
widget._noteId = bundle.noteId; widget._noteId = bundle.noteId;
widgetsByParent.add(widget); widgetsByParent.add(widget);
} }
} catch (e) { } catch (e: any) {
const noteId = bundle.noteId; const noteId = bundle.noteId;
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
toastService.showPersistent({ toastService.showPersistent({
@ -88,7 +103,7 @@ async function getWidgetBundlesByParent() {
icon: "alert", icon: "alert",
message: t("toast.bundle-error.message", { message: t("toast.bundle-error.message", {
id: noteId, id: noteId,
title: note.title, title: note?.title,
message: e.message message: e.message
}) })
}); });

View File

@ -5,10 +5,10 @@ import linkService from "./link.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
let clipboardBranchIds = []; let clipboardBranchIds: string[] = [];
let clipboardMode = null; let clipboardMode: string | null = null;
async function pasteAfter(afterBranchId) { async function pasteAfter(afterBranchId: string) {
if (isClipboardEmpty()) { if (isClipboardEmpty()) {
return; return;
} }
@ -23,7 +23,14 @@ async function pasteAfter(afterBranchId) {
const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId)); const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId));
for (const clipboardBranch of clipboardBranches) { for (const clipboardBranch of clipboardBranches) {
if (!clipboardBranch) {
continue;
}
const clipboardNote = await clipboardBranch.getNote(); const clipboardNote = await clipboardBranch.getNote();
if (!clipboardNote) {
continue;
}
await branchService.cloneNoteAfter(clipboardNote.noteId, afterBranchId); 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()) { if (isClipboardEmpty()) {
return; return;
} }
@ -50,7 +57,14 @@ async function pasteInto(parentBranchId) {
const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId)); const clipboardBranches = clipboardBranchIds.map(branchId => froca.getBranch(branchId));
for (const clipboardBranch of clipboardBranches) { for (const clipboardBranch of clipboardBranches) {
if (!clipboardBranch) {
continue;
}
const clipboardNote = await clipboardBranch.getNote(); const clipboardNote = await clipboardBranch.getNote();
if (!clipboardNote) {
continue;
}
await branchService.cloneNoteToBranch(clipboardNote.noteId, parentBranchId); 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; clipboardBranchIds = branchIds;
clipboardMode = 'copy'; clipboardMode = 'copy';
@ -82,7 +96,7 @@ async function copy(branchIds) {
toastService.showMessage(t("clipboard.copied")); toastService.showMessage(t("clipboard.copied"));
} }
function cut(branchIds) { function cut(branchIds: string[]) {
clipboardBranchIds = branchIds; clipboardBranchIds = branchIds;
if (clipboardBranchIds.length > 0) { if (clipboardBranchIds.length > 0) {

View File

@ -16,12 +16,13 @@ import { loadElkIfNeeded } from "./mermaid.js";
let idCounter = 1; let idCounter = 1;
/** interface Options {
* @param {FNote|FAttachment} entity tooltip?: boolean;
* @param {object} options trim?: boolean;
* @return {Promise<{type: string, $renderedContent: jQuery}>} imageHasZoom?: boolean;
*/ }
async function getRenderedContent(entity, options = {}) {
async function getRenderedContent(this: {} | { ctx: string }, entity: FNote, options: Options = {}) {
options = Object.assign({ options = Object.assign({
tooltip: false tooltip: false
}, options); }, options);
@ -49,7 +50,7 @@ async function getRenderedContent(entity, options = {}) {
else if (type === 'render') { else if (type === 'render') {
const $content = $('<div>'); const $content = $('<div>');
await renderService.render(entity, $content, this.ctx); await renderService.render(entity, $content);
$renderedContent.append($content); $renderedContent.append($content);
} }
@ -86,12 +87,11 @@ async function getRenderedContent(entity, options = {}) {
}; };
} }
/** @param {FNote} note */ async function renderText(note: FNote, $renderedContent: JQuery<HTMLElement>) {
async function renderText(note, $renderedContent) {
// entity must be FNote // entity must be FNote
const blob = await note.getBlob(); 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)); $renderedContent.append($('<div class="ck-content">').html(blob.content));
if ($renderedContent.find('span.math-tex').length > 0) { if ($renderedContent.find('span.math-tex').length > 0) {
@ -100,9 +100,9 @@ async function renderText(note, $renderedContent) {
renderMathInElement($renderedContent[0], {trust: true}); 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 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); await froca.getNotes(noteIdsToPrefetch);
for (const el of referenceLinks) { 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. * 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 blob = await note.getBlob();
const $codeBlock = $("<code>"); const $codeBlock = $("<code>");
$codeBlock.text(blob.content); $codeBlock.text(blob?.content || "");
$renderedContent.append($("<pre>").append($codeBlock)); $renderedContent.append($("<pre>").append($codeBlock));
await applySingleBlockSyntaxHighlight($codeBlock, mime_types.normalizeMimeTypeForCKEditor(note.mime)); 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); const encodedTitle = encodeURIComponent(entity.title);
let url; let url;
@ -146,7 +144,7 @@ function renderImage(entity, $renderedContent, options = {}) {
.css('justify-content', 'center'); .css('justify-content', 'center');
const $img = $("<img>") const $img = $("<img>")
.attr("src", url) .attr("src", url || "")
.attr("id", "attachment-image-" + idCounter++) .attr("id", "attachment-image-" + idCounter++)
.css("max-width", "100%"); .css("max-width", "100%");
@ -165,7 +163,7 @@ function renderImage(entity, $renderedContent, options = {}) {
imageContextMenuService.setupContextMenu($img); imageContextMenuService.setupContextMenu($img);
} }
function renderFile(entity, type, $renderedContent) { function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>) {
let entityType, entityId; let entityType, entityId;
if (entity instanceof FNote) { if (entity instanceof FNote) {
@ -201,7 +199,7 @@ function renderFile(entity, type, $renderedContent) {
$content.append($videoPreview); $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 // TODO: we should make this available also for attachments, but there's a problem with "Open externally" support
// in attachment list // in attachment list
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>'); 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); $renderedContent.append($content);
} }
async function renderMermaid(note, $renderedContent) { async function renderMermaid(note: FNote, $renderedContent: JQuery<HTMLElement>) {
await libraryLoader.requireLibrary(libraryLoader.MERMAID); await libraryLoader.requireLibrary(libraryLoader.MERMAID);
const blob = await note.getBlob(); const blob = await note.getBlob();
const content = blob.content || ""; const content = blob?.content || "";
$renderedContent $renderedContent
.css("display", "flex") .css("display", "flex")
@ -254,7 +252,7 @@ async function renderMermaid(note, $renderedContent) {
* @param {FNote} note * @param {FNote} note
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function renderChildrenList($renderedContent, note) { async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
$renderedContent.css("padding", "10px"); $renderedContent.css("padding", "10px");
$renderedContent.addClass("text-with-ellipsis"); $renderedContent.addClass("text-with-ellipsis");
@ -277,15 +275,21 @@ async function renderChildrenList($renderedContent, note) {
} }
} }
function getRenderingType(entity) { function getRenderingType(entity: FNote | FAttachment) {
let type = entity.type || entity.role; let type: string = "";
const mime = entity.mime; 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') { if (type === 'file' && mime === 'application/pdf') {
type = 'pdf'; type = 'pdf';
} else if (type === 'file' && mime.startsWith('audio/')) { } else if (type === 'file' && mime && mime.startsWith('audio/')) {
type = 'audio'; type = 'audio';
} else if (type === 'file' && mime.startsWith('video/')) { } else if (type === 'file' && mime && mime.startsWith('video/')) {
type = 'video'; type = 'video';
} }

View File

@ -7,13 +7,17 @@
* *
* @source underscore.js * @source underscore.js
* @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/ * @see http://unscriptable.com/2009/03/20/debouncing-javascript-methods/
* @param {Function} func to wrap * @param func to wrap
* @param {Number} waitMs in ms (`100`) * @param waitMs in ms (`100`)
* @param {Boolean} [immediate=false] whether to execute at the beginning (`false`) * @param whether to execute at the beginning (`false`)
* @api public * @api public
*/ */
function debounce(func, waitMs, immediate = false) { function debounce<T>(func: (...args: unknown[]) => T, waitMs: number, immediate: boolean = false) {
let timeout, args, context, timestamp, result; 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; if (null == waitMs) waitMs = 100;
function later() { function later() {
@ -24,20 +28,20 @@ function debounce(func, waitMs, immediate = false) {
} else { } else {
timeout = null; timeout = null;
if (!immediate) { if (!immediate) {
result = func.apply(context, args); result = func.apply(context, args || []);
context = args = null; context = args = null;
} }
} }
} }
const debounced = function () { const debounced = function (this: any) {
context = this; context = this;
args = arguments; args = arguments as unknown as unknown[];
timestamp = Date.now(); timestamp = Date.now();
const callNow = immediate && !timeout; const callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, waitMs); if (!timeout) timeout = setTimeout(later, waitMs);
if (callNow) { if (callNow) {
result = func.apply(context, args); result = func.apply(context, args || []);
context = args = null; context = args = null;
} }
@ -53,7 +57,7 @@ function debounce(func, waitMs, immediate = false) {
debounced.flush = function() { debounced.flush = function() {
if (timeout) { if (timeout) {
result = func.apply(context, args); result = func.apply(context, args || []);
context = args = null; context = args = null;
clearTimeout(timeout); clearTimeout(timeout);

View File

@ -1,24 +1,26 @@
import appContext from "../components/app_context.js"; 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 => return new Promise(res =>
appContext.triggerCommand("showInfoDialog", {message, callback: res})); appContext.triggerCommand("showInfoDialog", {message, callback: res}));
} }
async function confirm(message) { async function confirm(message: string) {
return new Promise(res => return new Promise(res =>
appContext.triggerCommand("showConfirmDialog", { appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
message, 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 => return new Promise(res =>
appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", {title, callback: res})); appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", {title, callback: res}));
} }
async function prompt(props) { async function prompt(props: PromptDialogOptions) {
return new Promise(res => return new Promise(res =>
appContext.triggerCommand("showPromptDialog", {...props, callback: res})); appContext.triggerCommand("showPromptDialog", {...props, callback: res}));
} }

View File

@ -1,36 +1,45 @@
import ws from "./ws.js"; import ws from "./ws.js";
import appContext from "../components/app_context.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: {}, notes: {},
attachments: {} attachments: {}
}; };
function checkType(type) { function checkType(type: string) {
if (type !== 'notes' && type !== 'attachments') { if (type !== 'notes' && type !== 'attachments') {
throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`); throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
} }
} }
function getFileModificationStatus(entityType, entityId) { function getFileModificationStatus(entityType: string, entityId: string) {
checkType(entityType); checkType(entityType);
return fileModificationStatus[entityType][entityId]; return fileModificationStatus[entityType][entityId];
} }
function fileModificationUploaded(entityType, entityId) { function fileModificationUploaded(entityType: string, entityId: string) {
checkType(entityType); checkType(entityType);
delete fileModificationStatus[entityType][entityId]; delete fileModificationStatus[entityType][entityId];
} }
function ignoreModification(entityType, entityId) { function ignoreModification(entityType: string, entityId: string) {
checkType(entityType); checkType(entityType);
delete fileModificationStatus[entityType][entityId]; delete fileModificationStatus[entityType][entityId];
} }
ws.subscribeToMessages(async message => { ws.subscribeToMessages(async (message: Message) => {
if (message.type !== 'openedFileUpdated') { if (message.type !== 'openedFileUpdated') {
return; return;
} }

View File

@ -243,7 +243,7 @@ class FrocaImpl implements Froca {
}).filter(note => !!note) as FNote[]; }).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 noteIds = Array.from(new Set(noteIds)); // make unique
const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]); const missingNoteIds = noteIds.filter(noteId => !this.notes[noteId]);

View File

@ -7,7 +7,7 @@ import FBranch, { FBranchRow } from "../entities/fbranch.js";
import FAttribute, { FAttributeRow } from "../entities/fattribute.js"; import FAttribute, { FAttributeRow } from "../entities/fattribute.js";
import FAttachment, { FAttachmentRow } from "../entities/fattachment.js"; import FAttachment, { FAttachmentRow } from "../entities/fattachment.js";
import FNote, { FNoteRow } from "../entities/fnote.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[]) { async function processEntityChanges(entityChanges: EntityChange[]) {
const loadResults = new LoadResults(entityChanges); const loadResults = new LoadResults(entityChanges);

View File

@ -27,7 +27,7 @@ function setupGlobs() {
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline"); window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
window.onerror = function (msg, url, lineNo, columnNo, error) { window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = msg.toLowerCase(); const string = String(msg).toLowerCase();
let message = "Uncaught error: "; let message = "Uncaught error: ";

View File

@ -4,7 +4,7 @@ import options from "./options.js";
await library_loader.requireLibrary(library_loader.I18NEXT); await library_loader.requireLibrary(library_loader.I18NEXT);
export async function initLocale() { export async function initLocale() {
const locale = options.get("locale") || "en"; const locale = (options.get("locale") as string) || "en";
await i18next await i18next
.use(i18nextHttpBackend) .use(i18nextHttpBackend)

View File

@ -1,6 +1,7 @@
import { t } from "./i18n.js";
import toastService from "./toast.js"; import toastService from "./toast.js";
function copyImageReferenceToClipboard($imageWrapper) { function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
try { try {
$imageWrapper.attr('contenteditable', 'true'); $imageWrapper.attr('contenteditable', 'true');
selectImage($imageWrapper.get(0)); selectImage($imageWrapper.get(0));
@ -14,17 +15,21 @@ function copyImageReferenceToClipboard($imageWrapper) {
} }
} }
finally { finally {
window.getSelection().removeAllRanges(); window.getSelection()?.removeAllRanges();
$imageWrapper.removeAttr('contenteditable'); $imageWrapper.removeAttr('contenteditable');
} }
} }
function selectImage(element) { function selectImage(element: HTMLElement | undefined) {
if (!element) {
return;
}
const selection = window.getSelection(); const selection = window.getSelection();
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(element); range.selectNodeContents(element);
selection.removeAllRanges(); selection?.removeAllRanges();
selection.addRange(range); selection?.addRange(range);
} }
export default { export default {

View File

@ -1,11 +1,11 @@
import toastService from "./toast.js"; import toastService, { ToastOptions } from "./toast.js";
import server from "./server.js"; import server from "./server.js";
import ws from "./ws.js"; import ws from "./ws.js";
import utils from "./utils.js"; import utils from "./utils.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
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)) { if (!['notes', 'attachments'].includes(entityType)) {
throw new Error(`Unrecognized import entity type '${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 { return {
id: id, id: id,
title: t("import.import-status"), title: t("import.import-status"),

View File

@ -1,10 +1,18 @@
import server from "./server.js"; 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 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 actions = actions.filter(a => !!a.actionName); // filter out separators
for (const action of actions) { for (const action of actions) {
@ -20,13 +28,13 @@ async function getActions() {
return await keyboardActionsLoaded; return await keyboardActionsLoaded;
} }
async function getActionsForScope(scope) { async function getActionsForScope(scope: string) {
const actions = await keyboardActionsLoaded; const actions = await keyboardActionsLoaded;
return actions.filter(action => action.scope === scope); 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); const actions = await getActionsForScope(scope);
for (const action of actions) { 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; await keyboardActionsLoaded;
const action = keyboardActionRepo[actionName]; const action = keyboardActionRepo[actionName];
@ -61,9 +69,15 @@ async function getAction(actionName, silent = false) {
return action; 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) => { $container.find('kbd[data-command]').each(async (i, el) => {
const actionName = $(el).attr('data-command'); const actionName = $(el).attr('data-command');
if (!actionName) {
return;
}
const action = await getAction(actionName, true); const action = await getAction(actionName, true);
if (action) { 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) => { $container.find('[data-trigger-command]').each(async (i, el) => {
const actionName = $(el).attr('data-trigger-command'); const actionName = $(el).attr('data-trigger-command');
if (!actionName) {
return;
}
const action = await getAction(actionName, true); const action = await getAction(actionName, true);
if (action) { if (action) {

View File

@ -2,9 +2,16 @@ import mimeTypesService from "./mime_types.js";
import optionsService from "./options.js"; import optionsService from "./options.js";
import { getStylesheetUrl } from "./syntax_highlight.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: [ js: [
"node_modules/codemirror/lib/codemirror.js", "node_modules/codemirror/lib/codemirror.js",
"node_modules/codemirror/addon/display/placeholder.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: [ js: [
"node_modules/jsplumb/dist/js/jsplumb.min.js", "node_modules/jsplumb/dist/js/jsplumb.min.js",
"node_modules/panzoom/dist/panzoom.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", js: [ "node_modules/katex/dist/katex.min.js",
"node_modules/katex/dist/contrib/mhchem.min.js", "node_modules/katex/dist/contrib/mhchem.min.js",
"node_modules/katex/dist/contrib/auto-render.min.js" ], "node_modules/katex/dist/contrib/auto-render.min.js" ],
css: [ "node_modules/katex/dist/katex.min.css" ] 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"] 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"] js: [ "node_modules/force-graph/dist/force-graph.min.js"]
}; };
const MERMAID = { const MERMAID: Library = {
js: [ js: [
"node_modules/mermaid/dist/mermaid.min.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. * The ELK extension of Mermaid.js, which supports more advanced layouts.
* See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information. * See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information.
*/ */
const MERMAID_ELK = { const MERMAID_ELK: Library = {
js: [ js: [
"libraries/mermaid-elk/elk.min.js" "libraries/mermaid-elk/elk.min.js"
] ]
} }
const EXCALIDRAW = { const EXCALIDRAW: Library = {
js: [ js: [
"node_modules/react/umd/react.production.min.js", "node_modules/react/umd/react.production.min.js",
"node_modules/react-dom/umd/react-dom.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: [ js: [
"node_modules/mark.js/dist/jquery.mark.es6.min.js" "node_modules/mark.js/dist/jquery.mark.es6.min.js"
] ]
}; };
const I18NEXT = { const I18NEXT: Library = {
js: [ js: [
"node_modules/i18next/i18next.min.js", "node_modules/i18next/i18next.min.js",
"node_modules/i18next-http-backend/i18nextHttpBackend.min.js" "node_modules/i18next-http-backend/i18nextHttpBackend.min.js"
] ]
}; };
const MIND_ELIXIR = { const MIND_ELIXIR: Library = {
js: [ js: [
"node_modules/mind-elixir/dist/MindElixir.iife.js", "node_modules/mind-elixir/dist/MindElixir.iife.js",
"node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs" "node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs"
] ]
}; };
const HIGHLIGHT_JS = { const HIGHLIGHT_JS: Library = {
js: () => { js: () => {
const mimeTypes = mimeTypesService.getMimeTypes(); const mimeTypes = mimeTypesService.getMimeTypes();
const scriptsToLoad = new Set(); const scriptsToLoad = new Set<string>();
scriptsToLoad.add("node_modules/@highlightjs/cdn-assets/highlight.min.js"); scriptsToLoad.add("node_modules/@highlightjs/cdn-assets/highlight.min.js");
for (const mimeType of mimeTypes) { for (const mimeType of mimeTypes) {
const id = mimeType.highlightJs; const id = mimeType.highlightJs;
@ -120,14 +135,14 @@ const HIGHLIGHT_JS = {
} }
} }
const currentTheme = optionsService.get("codeBlockTheme"); const currentTheme = String(optionsService.get("codeBlockTheme"));
loadHighlightingTheme(currentTheme); loadHighlightingTheme(currentTheme);
return Array.from(scriptsToLoad); return Array.from(scriptsToLoad);
} }
}; };
async function requireLibrary(library) { async function requireLibrary(library: Library) {
if (library.css) { if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl)); 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") { if (typeof value === "function") {
return value(); return (value as () => T)();
} }
return value; return value;
} }
// we save the promises in case of the same script being required concurrently multiple times // 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}`; url = `${window.glob.assetPath}/${url}`;
if (!loadedScriptPromises[url]) { if (!loadedScriptPromises[url]) {
@ -164,7 +179,7 @@ async function requireScript(url) {
await loadedScriptPromises[url]; await loadedScriptPromises[url];
} }
async function requireCss(url, prependAssetPath = true) { async function requireCss(url: string, prependAssetPath = true) {
const cssLinks = Array const cssLinks = Array
.from(document.querySelectorAll('link')) .from(document.querySelectorAll('link'))
.map(el => el.href); .map(el => el.href);
@ -178,8 +193,8 @@ async function requireCss(url, prependAssetPath = true) {
} }
} }
let highlightingThemeEl = null; let highlightingThemeEl: JQuery<HTMLElement> | null = null;
function loadHighlightingTheme(theme) { function loadHighlightingTheme(theme: string) {
if (!theme) { if (!theme) {
return; return;
} }

View File

@ -4,19 +4,19 @@ import appContext from "../components/app_context.js";
import froca from "./froca.js"; import froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
function getNotePathFromUrl(url) { function getNotePathFromUrl(url: string) {
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url); const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
return notePathMatch === null ? null : notePathMatch[1]; return notePathMatch === null ? null : notePathMatch[1];
} }
async function getLinkIcon(noteId, viewMode) { async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
let icon; let icon;
if (viewMode === 'default') { if (!viewMode || viewMode === 'default') {
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
icon = note.getIcon(); icon = note?.getIcon();
} else if (viewMode === 'source') { } else if (viewMode === 'source') {
icon = 'bx bx-code-curly'; icon = 'bx bx-code-curly';
} else if (viewMode === 'attachments') { } else if (viewMode === 'attachments') {
@ -25,7 +25,24 @@ async function getLinkIcon(noteId, viewMode) {
return icon; 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()) { if (!notePath || !notePath.trim()) {
logError("Missing note path"); logError("Missing note path");
@ -45,6 +62,12 @@ async function createLink(notePath, options = {}) {
const autoConvertToImage = options.autoConvertToImage === undefined ? false : options.autoConvertToImage; const autoConvertToImage = options.autoConvertToImage === undefined ? false : options.autoConvertToImage;
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath); const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId) {
logError("Missing note ID");
return $("<span>").text("[missing note]");
}
const viewScope = options.viewScope || {}; const viewScope = options.viewScope || {};
const viewMode = viewScope.viewMode || 'default'; const viewMode = viewScope.viewMode || 'default';
let linkTitle = options.title; let linkTitle = options.title;
@ -54,19 +77,19 @@ async function createLink(notePath, options = {}) {
const attachment = await froca.getAttachment(viewScope.attachmentId); const attachment = await froca.getAttachment(viewScope.attachmentId);
linkTitle = attachment ? attachment.title : '[missing attachment]'; linkTitle = attachment ? attachment.title : '[missing attachment]';
} else { } else if (noteId) {
linkTitle = await treeService.getNoteTitle(noteId, parentNoteId); linkTitle = await treeService.getNoteTitle(noteId, parentNoteId);
} }
} }
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
if (autoConvertToImage && ['image', 'canvas', 'mermaid'].includes(note.type) && viewMode === 'default') { if (autoConvertToImage && (note?.type && ['image', 'canvas', 'mermaid'].includes(note.type)) && viewMode === 'default') {
const encodedTitle = encodeURIComponent(linkTitle); const encodedTitle = encodeURIComponent(linkTitle || "");
return $("<img>") return $("<img>")
.attr("src", `api/images/${noteId}/${encodedTitle}?${Math.random()}`) .attr("src", `api/images/${noteId}/${encodedTitle}?${Math.random()}`)
.attr("alt", linkTitle); .attr("alt", linkTitle || "");
} }
const $container = $("<span>"); const $container = $("<span>");
@ -102,7 +125,7 @@ async function createLink(notePath, options = {}) {
$container.append($noteLink); $container.append($noteLink);
if (showNotePath) { if (showNotePath) {
const resolvedPathSegments = await treeService.resolveNotePathToSegments(notePath); const resolvedPathSegments = await treeService.resolveNotePathToSegments(notePath) || [];
resolvedPathSegments.pop(); // Remove last element resolvedPathSegments.pop(); // Remove last element
const resolvedPath = resolvedPathSegments.join("/"); const resolvedPath = resolvedPathSegments.join("/");
@ -118,7 +141,14 @@ async function createLink(notePath, options = {}) {
return $container; 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 || ""; notePath = notePath || "";
const params = [ const params = [
ntxId ? { ntxId: ntxId } : null, ntxId ? { ntxId: ntxId } : null,
@ -129,9 +159,9 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
const paramStr = params.map(pair => { const paramStr = params.map(pair => {
const name = Object.keys(pair)[0]; 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("&"); }).join("&");
if (!notePath && !paramStr) { if (!notePath && !paramStr) {
@ -147,7 +177,7 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
return hash; return hash;
} }
function parseNavigationStateFromUrl(url) { function parseNavigationStateFromUrl(url: string | undefined) {
if (!url) { if (!url) {
return {}; return {};
} }
@ -164,7 +194,7 @@ function parseNavigationStateFromUrl(url) {
return {}; return {};
} }
const viewScope = { const viewScope: ViewScope = {
viewMode: 'default' viewMode: 'default'
}; };
let ntxId = null; let ntxId = null;
@ -184,7 +214,7 @@ function parseNavigationStateFromUrl(url) {
} else if (name === 'searchString') { } else if (name === 'searchString') {
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
} else if (['viewMode', 'attachmentId'].includes(name)) { } else if (['viewMode', 'attachmentId'].includes(name)) {
viewScope[name] = value; (viewScope as any)[name] = value;
} else { } else {
console.warn(`Unrecognized hash parameter '${name}'.`); console.warn(`Unrecognized hash parameter '${name}'.`);
} }
@ -201,14 +231,14 @@ function parseNavigationStateFromUrl(url) {
}; };
} }
function goToLink(evt) { function goToLink(evt: MouseEvent) {
const $link = $(evt.target).closest("a,.block-link"); const $link = $(evt.target as any).closest("a,.block-link");
const hrefLink = $link.attr('href') || $link.attr('data-href'); const hrefLink = $link.attr('href') || $link.attr('data-href');
return goToLinkExt(evt, hrefLink, $link); return goToLinkExt(evt, hrefLink, $link);
} }
function goToLinkExt(evt, hrefLink, $link) { function goToLinkExt(evt: MouseEvent, hrefLink: string | undefined, $link: JQuery<HTMLElement>) {
if (hrefLink?.startsWith("data:")) { if (hrefLink?.startsWith("data:")) {
return true; return true;
} }
@ -230,7 +260,7 @@ function goToLinkExt(evt, hrefLink, $link) {
if (openInNewTab) { if (openInNewTab) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope}); appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope});
} else if (isLeftClick) { } 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 const noteContext = ntxId
? appContext.tabManager.getNoteContextById(ntxId) ? appContext.tabManager.getNoteContextById(ntxId)
@ -275,8 +305,8 @@ function goToLinkExt(evt, hrefLink, $link) {
return true; return true;
} }
function linkContextMenu(e) { function linkContextMenu(e: Event) {
const $link = $(e.target).closest("a"); const $link = $(e.target as any).closest("a");
const url = $link.attr("href") || $link.attr("data-href"); const url = $link.attr("href") || $link.attr("data-href");
const { notePath, viewScope } = parseNavigationStateFromUrl(url); const { notePath, viewScope } = parseNavigationStateFromUrl(url);
@ -290,7 +320,7 @@ function linkContextMenu(e) {
linkContextMenuService.openContextMenu(notePath, e, viewScope, null); 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"); const $link = $el[0].tagName === 'A' ? $el : $el.find("a");
href = href || $link.attr("href"); href = href || $link.attr("href");
@ -300,6 +330,11 @@ async function loadReferenceLinkTitle($el, href = null) {
} }
const {noteId, viewScope} = parseNavigationStateFromUrl(href); const {noteId, viewScope} = parseNavigationStateFromUrl(href);
if (!noteId) {
console.warn("Missing note ID.");
return;
}
const note = await froca.getNote(noteId, true); const note = await froca.getNote(noteId, true);
if (note) { if (note) {
@ -312,11 +347,13 @@ async function loadReferenceLinkTitle($el, href = null) {
if (note) { if (note) {
const icon = await getLinkIcon(noteId, viewScope.viewMode); const icon = await getLinkIcon(noteId, viewScope.viewMode);
$el.prepend($("<span>").addClass(icon)); if (icon) {
$el.prepend($("<span>").addClass(icon));
}
} }
} }
async function getReferenceLinkTitle(href) { async function getReferenceLinkTitle(href: string) {
const {noteId, viewScope} = parseNavigationStateFromUrl(href); const {noteId, viewScope} = parseNavigationStateFromUrl(href);
if (!noteId) { if (!noteId) {
return "[missing note]"; return "[missing note]";
@ -336,7 +373,7 @@ async function getReferenceLinkTitle(href) {
} }
} }
function getReferenceLinkTitleSync(href) { function getReferenceLinkTitleSync(href: string) {
const {noteId, viewScope} = parseNavigationStateFromUrl(href); const {noteId, viewScope} = parseNavigationStateFromUrl(href);
if (!noteId) { if (!noteId) {
return "[missing note]"; 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); $(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('auxclick', "a", goToLink); // to handle the middle button
$(document).on('contextmenu', 'a', linkContextMenu); $(document).on('contextmenu', 'a', linkContextMenu);
$(document).on('dblclick', "a", e => { $(document).on('dblclick', "a", e => {

View File

@ -1,4 +1,4 @@
import { EntityChange } from "../../../services/entity_changes_interface.js"; import { EntityChange } from "../server_types.js";
interface BranchRow { interface BranchRow {
branchId: string; branchId: string;

View File

@ -15,7 +15,7 @@ function init() {
} }
} }
function exec(cmd) { function exec(cmd: string) {
document.execCommand(cmd); document.execCommand(cmd);
return false; return false;

View File

@ -11,7 +11,7 @@ let elkLoaded = false;
* *
* @param mermaidContent the plain text of the mermaid diagram, potentially including a frontmatter. * @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) { if (elkLoaded) {
// Exit immediately since the ELK library is already loaded. // Exit immediately since the ELK library is already loaded.
return; return;

View File

@ -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. * 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" }, { default: true, title: "Plain text", mime: "text/plain", highlightJs: "plaintext" },
{ title: "APL", mime: "text/apl" }, { title: "APL", mime: "text/apl" },
{ title: "ASN.1", mime: "text/x-ttcn-asn" }, { title: "ASN.1", mime: "text/x-ttcn-asn" },
@ -170,10 +184,10 @@ const MIME_TYPES_DICT = [
{ title: "Z80", mime: "text/x-z80" } { title: "Z80", mime: "text/x-z80" }
]; ];
let mimeTypes = null; let mimeTypes: MimeType[] | null = null;
function loadMimeTypes() { 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') const enabledMimeTypes = options.getJson('codeNotesMimeTypes')
|| MIME_TYPES_DICT.filter(mt => mt.default).map(mt => mt.mime); || MIME_TYPES_DICT.filter(mt => mt.default).map(mt => mt.mime);
@ -183,32 +197,34 @@ function loadMimeTypes() {
} }
} }
function getMimeTypes() { function getMimeTypes(): MimeType[] {
if (mimeTypes === null) { if (mimeTypes === null) {
loadMimeTypes(); 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. * 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. * 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`. * @returns the corresponding highlight.js tag, for example `c` for `text-c-src`.
*/ */
function getHighlightJsNameForMime(mimeType) { function getHighlightJsNameForMime(mimeType: string) {
if (!mimeToHighlightJsMapping) { if (!mimeToHighlightJsMapping) {
const mimeTypes = getMimeTypes(); const mimeTypes = getMimeTypes();
mimeToHighlightJsMapping = {}; mimeToHighlightJsMapping = {};
for (const mimeType of mimeTypes) { 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. // 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); const normalizedMime = normalizeMimeTypeForCKEditor(mimeType.mime);
mimeToHighlightJsMapping[normalizedMime] = mimeType.highlightJs; if (mimeType.highlightJs) {
mimeToHighlightJsMapping[normalizedMime] = mimeType.highlightJs;
}
} }
} }
@ -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 * 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. * 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`). * @returns the normalized MIME type (e.g. `text-c-src`).
*/ */
function normalizeMimeTypeForCKEditor(mimeType) { function normalizeMimeTypeForCKEditor(mimeType: string) {
return mimeType.toLowerCase() return mimeType.toLowerCase()
.replace(/[\W_]+/g,"-"); .replace(/[\W_]+/g,"-");
} }

View File

@ -10,7 +10,26 @@ const SELECTED_NOTE_PATH_KEY = "data-note-path";
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link"; 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) => { return await new Promise((res, rej) => {
autocompleteSource(queryText, rows => { autocompleteSource(queryText, rows => {
res(rows.map(row => { 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; const fastSearch = options.fastSearch === false ? false : true;
if (fastSearch === false) { if (fastSearch === false) {
if (term.trim().length === 0){ if (term.trim().length === 0){
@ -46,7 +65,7 @@ async function autocompleteSource(term, cb, options = {}) {
const activeNoteId = appContext.tabManager.getActiveContextNoteId(); 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) { if (term.trim().length >= 1 && options.allowCreatingNotes) {
results = [ results = [
{ {
@ -54,7 +73,7 @@ async function autocompleteSource(term, cb, options = {}) {
noteTitle: term, noteTitle: term,
parentNoteId: activeNoteId || 'root', parentNoteId: activeNoteId || 'root',
highlightedNotePathTitle: t("note_autocomplete.create-note", { term }) highlightedNotePathTitle: t("note_autocomplete.create-note", { term })
} } as Suggestion
].concat(results); ].concat(results);
} }
@ -74,14 +93,14 @@ async function autocompleteSource(term, cb, options = {}) {
action: 'external-link', action: 'external-link',
externalLink: term, externalLink: term,
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term }) highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term })
} } as Suggestion
].concat(results); ].concat(results);
} }
cb(results); cb(results);
} }
function clearText($el) { function clearText($el: JQuery<HTMLElement>) {
if (utils.isMobile()) { if (utils.isMobile()) {
return; return;
} }
@ -90,7 +109,7 @@ function clearText($el) {
$el.autocomplete("val", "").trigger('change'); $el.autocomplete("val", "").trigger('change');
} }
function setText($el, text) { function setText($el: JQuery<HTMLElement>, text: string) {
if (utils.isMobile()) { if (utils.isMobile()) {
return; return;
} }
@ -101,7 +120,7 @@ function setText($el, text) {
.autocomplete("open"); .autocomplete("open");
} }
function showRecentNotes($el) { function showRecentNotes($el:JQuery<HTMLElement>) {
if (utils.isMobile()) { if (utils.isMobile()) {
return; return;
} }
@ -112,21 +131,22 @@ function showRecentNotes($el) {
$el.trigger('focus'); $el.trigger('focus');
} }
function fullTextSearch($el, options){ function fullTextSearch($el: JQuery<HTMLElement>, options: Options){
const searchString = $el.autocomplete('val'); const searchString = $el.autocomplete('val') as unknown as string;
if (options.fastSearch === false || searchString.trim().length === 0) { if (options.fastSearch === false || searchString?.trim().length === 0) {
return; return;
} }
$el.trigger('focus'); $el.trigger('focus');
options.fastSearch = false; options.fastSearch = false;
$el.autocomplete('val', ''); $el.autocomplete('val', '');
$el.autocomplete()
$el.setSelectedNotePath(""); $el.setSelectedNotePath("");
$el.autocomplete('val', searchString); $el.autocomplete('val', searchString);
// Set a delay to avoid resetting to true before full text search (await server.get) is called. // Set a delay to avoid resetting to true before full text search (await server.get) is called.
setTimeout(() => { options.fastSearch = true; }, 100); setTimeout(() => { options.fastSearch = true; }, 100);
} }
function initNoteAutocomplete($el, options) { function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
if ($el.hasClass("note-autocomplete-input") || utils.isMobile()) { if ($el.hasClass("note-autocomplete-input") || utils.isMobile()) {
// clear any event listener added in previous invocation of this function // clear any event listener added in previous invocation of this function
$el.off('autocomplete:noteselected'); $el.off('autocomplete:noteselected');
@ -174,7 +194,7 @@ function initNoteAutocomplete($el, options) {
return false; return false;
}); });
let autocompleteOptions = {}; let autocompleteOptions: AutoCompleteConfig = {};
if (options.container) { if (options.container) {
autocompleteOptions.dropdownMenuContainer = options.container; autocompleteOptions.dropdownMenuContainer = options.container;
autocompleteOptions.debug = true; // don't close on blur 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') { if (suggestion.action === 'external-link') {
$el.setSelectedNotePath(null); $el.setSelectedNotePath(null);
$el.setSelectedExternalLink(suggestion.externalLink); $el.setSelectedExternalLink(suggestion.externalLink);
@ -250,7 +271,7 @@ function initNoteAutocomplete($el, options) {
}); });
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId; const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
suggestion.notePath = note.getBestNotePathString(hoistedNoteId); suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
} }
if (suggestion.action === 'search-notes') { if (suggestion.action === 'search-notes') {
@ -270,7 +291,7 @@ function initNoteAutocomplete($el, options) {
}); });
$el.on('autocomplete:closed', () => { $el.on('autocomplete:closed', () => {
if (!$el.val().trim()) { if (!String($el.val())?.trim()) {
clearText($el); clearText($el);
} }
}); });
@ -289,7 +310,7 @@ function initNoteAutocomplete($el, options) {
function init() { function init() {
$.fn.getSelectedNotePath = function () { $.fn.getSelectedNotePath = function () {
if (!$(this).val().trim()) { if (!String($(this).val())?.trim()) {
return ""; return "";
} else { } else {
return $(this).attr(SELECTED_NOTE_PATH_KEY); return $(this).attr(SELECTED_NOTE_PATH_KEY);
@ -297,7 +318,8 @@ function init() {
}; };
$.fn.getSelectedNoteId = function () { $.fn.getSelectedNoteId = function () {
const notePath = $(this).getSelectedNotePath(); const $el = $(this as unknown as HTMLElement);
const notePath = $el.getSelectedNotePath();
if (!notePath) { if (!notePath) {
return null; return null;
} }
@ -320,7 +342,7 @@ function init() {
}; };
$.fn.getSelectedExternalLink = function () { $.fn.getSelectedExternalLink = function () {
if (!$(this).val().trim()) { if (!String($(this).val())?.trim()) {
return ""; return "";
} else { } else {
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY); return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
@ -329,6 +351,7 @@ function init() {
$.fn.setSelectedExternalLink = function (externalLink) { $.fn.setSelectedExternalLink = function (externalLink) {
if (externalLink) { if (externalLink) {
// TODO: This doesn't seem to do anything with the external link, is it normal?
$(this) $(this)
.closest(".input-group") .closest(".input-group")
.find(".go-to-selected-note-button") .find(".go-to-selected-note-button")

View File

@ -6,8 +6,41 @@ import froca from "./froca.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import toastService from "./toast.js"; import toastService from "./toast.js";
import { t } from "./i18n.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({ options = Object.assign({
activate: true, activate: true,
focus: 'title', focus: 'title',
@ -24,7 +57,7 @@ async function createNote(parentNotePath, options = {}) {
options.saveSelection = false; options.saveSelection = false;
} }
if (options.saveSelection) { if (options.saveSelection && options.textEditor) {
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml()); [options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
} }
@ -38,7 +71,7 @@ async function createNote(parentNotePath, options = {}) {
C-->D;` 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, title: options.title,
content: options.content || "", content: options.content || "",
isProtected: options.isProtected, isProtected: options.isProtected,
@ -49,7 +82,7 @@ async function createNote(parentNotePath, options = {}) {
if (options.saveSelection) { if (options.saveSelection) {
// we remove the selection only after it was saved to server to make sure we don't lose anything // 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(); await ws.waitForMaxKnownEntityChangeId();
@ -76,12 +109,14 @@ async function createNote(parentNotePath, options = {}) {
} }
async function chooseNoteType() { 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}); appContext.triggerCommand("chooseNoteType", {callback: res});
}); });
} }
async function createNoteWithTypePrompt(parentNotePath, options = {}) { async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
const {success, noteType, templateNoteId} = await chooseNoteType(); const {success, noteType, templateNoteId} = await chooseNoteType();
if (!success) { 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. */ /* 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); 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)) { if (dom.length > 0 && dom[0].tagName && dom[0].tagName.match(/h[1-6]/i)) {
const title = $(dom[0]).text(); const title = $(dom[0]).text();
// remove the title from content (only first occurrence) // 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, ""); const content = selectedHtml.replace(dom[0].outerHTML, "");
return [title, content]; 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 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(); await ws.waitForMaxKnownEntityChangeId();
@ -120,7 +159,7 @@ async function duplicateSubtree(noteId, parentNotePath) {
activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`); activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
const origNote = await froca.getNote(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 { export default {

View File

@ -5,6 +5,7 @@ import utils from "./utils.js";
import attributeRenderer from "./attribute_renderer.js"; import attributeRenderer from "./attribute_renderer.js";
import contentRenderer from "./content_renderer.js"; import contentRenderer from "./content_renderer.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import FNote from "../entities/fnote.js";
function setupGlobalTooltip() { function setupGlobalTooltip() {
$(document).on("mouseenter", "a", mouseEnterHandler); $(document).on("mouseenter", "a", mouseEnterHandler);
@ -24,11 +25,11 @@ function cleanUpTooltips() {
$('.note-tooltip').remove(); $('.note-tooltip').remove();
} }
function setupElementTooltip($el) { function setupElementTooltip($el: JQuery<HTMLElement>) {
$el.on('mouseenter', mouseEnterHandler); $el.on('mouseenter', mouseEnterHandler);
} }
async function mouseEnterHandler() { async function mouseEnterHandler(this: HTMLElement) {
const $link = $(this); const $link = $(this);
if ($link.hasClass("no-tooltip-preview") || $link.hasClass("disabled")) { 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 url = $link.attr("href") || $link.attr("data-href");
const { notePath, noteId, viewScope } = linkService.parseNavigationStateFromUrl(url); const { notePath, noteId, viewScope } = linkService.parseNavigationStateFromUrl(url);
if (!notePath || viewScope.viewMode !== 'default') { if (!notePath || !noteId || viewScope?.viewMode !== 'default') {
return; return;
} }
@ -64,7 +65,7 @@ async function mouseEnterHandler() {
new Promise(res => setTimeout(res, 500)) new Promise(res => setTimeout(res, 500))
]); ]);
if (utils.isHtmlEmpty(content)) { if (!content || utils.isHtmlEmpty(content)) {
return; return;
} }
@ -81,7 +82,8 @@ async function mouseEnterHandler() {
// with bottom this flickering happens a bit less // with bottom this flickering happens a bit less
placement: 'bottom', placement: 'bottom',
trigger: 'manual', trigger: 'manual',
boundary: 'window', //TODO: boundary No longer applicable?
//boundary: 'window',
title: html, title: html,
html: true, html: true,
template: `<div class="tooltip note-tooltip ${tooltipClass}" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>`, 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) { if (!note) {
return '<div>Note has been deleted.</div>'; return '<div>Note has been deleted.</div>';
} }
@ -126,7 +128,11 @@ async function renderTooltip(note) {
return; 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); const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);

View File

@ -2,8 +2,22 @@ import server from "./server.js";
import froca from "./froca.js"; import froca from "./froca.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
async function getNoteTypeItems(command) { interface NoteTypeSeparator {
const items = [ 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.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.code"), command: command, type: "code", uiIcon: "bx bx-code" },
{ title: t("note_types.saved-search"), command: command, type: "search", uiIcon: "bx bx-file-find" }, { 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" } { 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); const templateNotes = await froca.getNotes(templateNoteIds);
if (templateNotes.length > 0) { if (templateNotes.length > 0) {

View File

@ -1,7 +1,7 @@
import server from "./server.js"; import server from "./server.js";
type OptionValue = string | number; type OptionValue = number | string;
class Options { class Options {
initializedPromise: Promise<void>; initializedPromise: Promise<void>;

View File

@ -1,6 +1,7 @@
import server from './server.js'; import server from './server.js';
import protectedSessionHolder from './protected_session_holder.js'; import protectedSessionHolder from './protected_session_holder.js';
import toastService from "./toast.js"; import toastService from "./toast.js";
import type { ToastOptions } from "./toast.js";
import ws from "./ws.js"; import ws from "./ws.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import froca from "./froca.js"; import froca from "./froca.js";
@ -8,7 +9,19 @@ import utils from "./utils.js";
import options from "./options.js"; import options from "./options.js";
import { t } from './i18n.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() { async function leaveProtectedSession() {
if (protectedSessionHolder.isProtectedSessionAvailable()) { if (protectedSessionHolder.isProtectedSessionAvailable()) {
@ -44,11 +57,11 @@ async function reloadData() {
await froca.loadInitialTree(); await froca.loadInitialTree();
// make sure that all notes used in the application are loaded, including the ones not shown in the tree // 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) { async function setupProtectedSession(password: string) {
const response = await server.post('login/protected', { password: password }); const response = await server.post<Response>('login/protected', { password: password });
if (!response.success) { if (!response.success) {
toastService.showError(t("protected_session.wrong_password"), 3000); 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 enterProtectedSession();
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`); 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 { return {
id: message.taskId, id: message.taskId,
title, title,

View File

@ -1,7 +1,8 @@
import server from "./server.js"; 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 relations = note.getRelations('renderNote');
const renderNoteIds = relations const renderNoteIds = relations
.map(rel => rel.value) .map(rel => rel.value)
@ -10,7 +11,7 @@ async function render(note, $el) {
$el.empty().toggle(renderNoteIds.length > 0); $el.empty().toggle(renderNoteIds.length > 0);
for (const renderNoteId of renderNoteIds) { 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>'); const $scriptContainer = $('<div>');
$el.append($scriptContainer); $el.append($scriptContainer);

View File

@ -1,9 +1,9 @@
import options from "./options.js"; import options from "./options.js";
let leftInstance; let leftInstance: ReturnType<typeof Split> | null;
let rightInstance; let rightInstance: ReturnType<typeof Split> | null;
function setupLeftPaneResizer(leftPaneVisible) { function setupLeftPaneResizer(leftPaneVisible: boolean) {
if (leftInstance) { if (leftInstance) {
leftInstance.destroy(); leftInstance.destroy();
leftInstance = null; leftInstance = null;

View File

@ -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 utils from './utils.js';
import froca from './froca.js'; import froca from './froca.js';
async function ScriptContext(startNoteId, allNoteIds, originEntity = null, $container = null) { async function ScriptContext(startNoteId: string, allNoteIds: string[], originEntity: Entity | null = null, $container: JQuery<HTMLElement> | null = null) {
const modules = {}; const modules: Record<string, { exports: unknown }> = {};
await froca.initializedPromise; await froca.initializedPromise;
const startNote = await froca.getNote(startNoteId); const startNote = await froca.getNote(startNoteId);
const allNotes = await froca.getNotes(allNoteIds); const allNotes = await froca.getNotes(allNoteIds);
if (!startNote) {
throw new Error(`Could not find start note ${startNoteId}.`);
}
return { return {
modules: modules, modules: modules,
notes: utils.toObject(allNotes, note => [note.noteId, note]), notes: utils.toObject(allNotes, note => [note.noteId, note]),
apis: utils.toObject(allNotes, note => [note.noteId, new FrontendScriptApi(startNote, note, originEntity, $container)]), apis: utils.toObject(allNotes, note => [note.noteId, new FrontendScriptApi(startNote, note, originEntity, $container)]),
require: moduleNoteIds => { require: (moduleNoteIds: string) => {
return moduleName => { return (moduleName: string) => {
const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId)); const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
const note = candidates.find(c => c.title === moduleName); const note = candidates.find(c => c.title === moduleName);

View File

@ -1,11 +1,11 @@
import server from "./server.js"; import server from "./server.js";
import froca from "./froca.js"; import froca from "./froca.js";
async function searchForNoteIds(searchString) { async function searchForNoteIds(searchString: string) {
return await server.get(`search/${encodeURIComponent(searchString)}`); return await server.get<string[]>(`search/${encodeURIComponent(searchString)}`);
} }
async function searchForNotes(searchString) { async function searchForNotes(searchString: string) {
const noteIds = await searchForNoteIds(searchString); const noteIds = await searchForNoteIds(searchString);
return await froca.getNotes(noteIds); return await froca.getNotes(noteIds);

View File

@ -1,14 +1,17 @@
import utils from "./utils.js"; 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); 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); 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()) { if (utils.isDesktop()) {
keyboardShortcut = normalizeShortcut(keyboardShortcut); 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) // method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted)
if (keyboardShortcut) { if (keyboardShortcut) {
$el.bind(eventName, keyboardShortcut, e => { $el.bind(eventName, keyboardShortcut, e => {
handler(e); if (handler) {
handler(e);
}
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -36,7 +41,7 @@ function bindElShortcut($el, keyboardShortcut, handler, namespace = null) {
/** /**
* Normalize to the form expected by the jquery.hotkeys.js * Normalize to the form expected by the jquery.hotkeys.js
*/ */
function normalizeShortcut(shortcut) { function normalizeShortcut(shortcut: string): string {
if (!shortcut) { if (!shortcut) {
return shortcut; return shortcut;
} }

View File

@ -1,4 +1,4 @@
type Callback = () => Promise<void>; type Callback = () => Promise<void> | void;
export default class SpacedUpdate { export default class SpacedUpdate {
private updater: Callback; private updater: Callback;

View File

@ -2,8 +2,15 @@ import { t } from './i18n.js';
import server from './server.js'; import server from './server.js';
import toastService from "./toast.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) { async function syncNow(ignoreNotConfigured = false) {
const result = await server.post('sync/now'); const result = await server.post<SyncResult>('sync/now');
if (result.success) { if (result.success) {
toastService.showMessage(t("sync.finished-successfully")); toastService.showMessage(t("sync.finished-successfully"));

View File

@ -2,7 +2,7 @@ import library_loader from "./library_loader.js";
import mime_types from "./mime_types.js"; import mime_types from "./mime_types.js";
import options from "./options.js"; import options from "./options.js";
export function getStylesheetUrl(theme) { export function getStylesheetUrl(theme: string) {
if (!theme) { if (!theme) {
return null; 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. * @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()) { if (!isSyntaxHighlightEnabled()) {
return; 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. * 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"); $codeBlock.parent().toggleClass("hljs");
const text = $codeBlock.text(); 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. * 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`). * @returns the normalized MIME type (e.g. `text-css` instead of `language-text-css`).
*/ */
function extractLanguageFromClassList(el) { function extractLanguageFromClassList(el: HTMLElement) {
const prefix = "language-"; const prefix = "language-";
for (const className of el.classList) { for (const className of el.classList) {
if (className.startsWith(prefix)) { if (className.startsWith(prefix)) {

View File

@ -1,7 +1,7 @@
import ws from "./ws.js"; import ws from "./ws.js";
import utils from "./utils.js"; import utils from "./utils.js";
interface ToastOptions { export interface ToastOptions {
id?: string; id?: string;
icon: string; icon: string;
title: string; title: string;

View File

@ -6,9 +6,14 @@ import appContext from "../components/app_context.js";
export interface Node { export interface Node {
getParent(): Node; getParent(): Node;
getChildren(): Node[];
folder: boolean;
renderTitle(): void,
data: { data: {
noteId?: string; noteId?: string;
isProtected?: boolean; isProtected?: boolean;
branchId: string;
noteType: string;
} }
} }
@ -144,7 +149,7 @@ function getParentProtectedStatus(node: Node) {
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected; return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
} }
function getNoteIdFromUrl(urlOrNotePath: string) { function getNoteIdFromUrl(urlOrNotePath: string | undefined) {
if (!urlOrNotePath) { if (!urlOrNotePath) {
return null; return null;
} }

View File

@ -1,5 +1,4 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Modal } from "bootstrap";
function reloadFrontendApp(reason?: string) { function reloadFrontendApp(reason?: string) {
if (reason) { if (reason) {
@ -99,7 +98,7 @@ function isMac() {
return navigator.platform.indexOf('Mac') > -1; return navigator.platform.indexOf('Mac') > -1;
} }
function isCtrlKey(evt: KeyboardEvent) { function isCtrlKey(evt: KeyboardEvent | MouseEvent) {
return (!isMac() && evt.ctrlKey) return (!isMac() && evt.ctrlKey)
|| (isMac() && evt.metaKey); || (isMac() && evt.metaKey);
} }
@ -138,8 +137,8 @@ function formatSize(size: number) {
} }
} }
function toObject<T>(array: T[], fn: (arg0: T) => [key: string, value: T]) { function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
const obj: Record<string, T> = {}; const obj: Record<string, R> = {};
for (const item of array) { for (const item of array) {
const [key, value] = fn(item); const [key, value] = fn(item);
@ -205,7 +204,9 @@ function getMimeTypeClass(mime: string) {
function closeActiveDialog() { function closeActiveDialog() {
if (glob.activeDialog) { 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; glob.activeDialog = null;
} }
} }
@ -249,7 +250,9 @@ async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
} }
saveFocusedElement(); 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', () => { $dialog.on('hidden.bs.modal', () => {
$(".aa-input").autocomplete("close"); $(".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) // for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button)
// so we do it manually // so we do it manually
$el.on("click", e => { $el.on("click", e => {

View File

@ -4,8 +4,8 @@ import server from "./server.js";
import options from "./options.js"; import options from "./options.js";
import frocaUpdater from "./froca_updater.js"; import frocaUpdater from "./froca_updater.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import { EntityChange } from '../../../services/entity_changes_interface.js';
import { t } from './i18n.js'; import { t } from './i18n.js';
import { EntityChange } from '../server_types.js';
type MessageHandler = (message: any) => void; type MessageHandler = (message: any) => void;
const messageHandlers: MessageHandler[] = []; const messageHandlers: MessageHandler[] = [];

View File

@ -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 { interface ElectronProcess {
type: string; type: string;
@ -6,16 +14,16 @@ interface ElectronProcess {
} }
interface CustomGlobals { interface CustomGlobals {
isDesktop: boolean; isDesktop: typeof utils.isDesktop;
isMobile: boolean; isMobile: typeof utils.isMobile;
device: "mobile" | "desktop"; device: "mobile" | "desktop";
getComponentsByEl: (el: unknown) => unknown; getComponentByEl: typeof appContext.getComponentByEl;
getHeaders: Promise<Record<string, string>>; getHeaders: typeof server.getHeaders;
getReferenceLinkTitle: (href: string) => Promise<string>; getReferenceLinkTitle: (href: string) => Promise<string>;
getReferenceLinkTitleSync: (href: string) => string; getReferenceLinkTitleSync: (href: string) => string;
getActiveContextNote: FNote; getActiveContextNote: FNote;
requireLibrary: (library: string) => Promise<void>; requireLibrary: typeof library_loader.requireLibrary;
ESLINT: { js: string[]; }; ESLINT: Library;
appContext: AppContext; appContext: AppContext;
froca: Froca; froca: Froca;
treeCache: Froca; treeCache: Froca;
@ -30,6 +38,9 @@ interface CustomGlobals {
isMainWindow: boolean; isMainWindow: boolean;
maxEntityChangeIdAtLoad: number; maxEntityChangeIdAtLoad: number;
maxEntityChangeSyncIdAtLoad: number; maxEntityChangeSyncIdAtLoad: number;
assetPath: string;
instanceName: string;
appCssNoteIds: string[];
} }
type RequireMethod = (moduleName: string) => any; type RequireMethod = (moduleName: string) => any;
@ -37,19 +48,100 @@ type RequireMethod = (moduleName: string) => any;
declare global { declare global {
interface Window { interface Window {
logError(message: string); logError(message: string);
logInfo(message: string); logInfo(message: string);
process?: ElectronProcess; process?: ElectronProcess;
glob?: CustomGlobals; glob?: CustomGlobals;
} }
interface JQuery { interface AutoCompleteConfig {
autocomplete: (action: "close") => void; appendTo?: HTMLElement | null;
hint?: boolean;
openOnFocus?: boolean;
minLength?: number;
tabAutocomplete?: boolean;
autoselect?: boolean;
dropdownMenuContainer?: HTMLElement;
debug?: boolean;
} }
declare var logError: (message: string) => void; type AutoCompleteCallback = (values: AutoCompleteCallbackArg[]) => void;
declare var logInfo: (message: string) => void;
declare var glob: CustomGlobals; interface AutoCompleteArg {
declare var require: RequireMethod; displayKey: "name" | "value" | "notePathTitle";
declare var __non_webpack_require__: RequireMethod | undefined; 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;
} }

View File

@ -9,6 +9,13 @@ import toastService from "../services/toast.js";
* For information on using widgets, see the tutorial {@tutorial widget_basics}. * For information on using widgets, see the tutorial {@tutorial widget_basics}.
*/ */
class BasicWidget extends Component { 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() { constructor() {
super(); super();
@ -21,7 +28,7 @@ class BasicWidget extends Component {
this.childPositionCounter = 10; this.childPositionCounter = 10;
} }
child(...components) { child(...components: Component[]) {
if (!components) { if (!components) {
return this; return this;
} }
@ -43,11 +50,11 @@ class BasicWidget extends Component {
/** /**
* Conditionally adds the given components as children to this component. * Conditionally adds the given components as children to this component.
* *
* @param {boolean} condition whether to add the components. * @param 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 components the components to be added as children to this component provided the condition is truthy.
* @returns self for chaining. * @returns self for chaining.
*/ */
optChild(condition, ...components) { optChild(condition: boolean, ...components: Component[]) {
if (condition) { if (condition) {
return this.child(...components); return this.child(...components);
} else { } else {
@ -55,12 +62,12 @@ class BasicWidget extends Component {
} }
} }
id(id) { id(id: string) {
this.attrs.id = id; this.attrs.id = id;
return this; return this;
} }
class(className) { class(className: string) {
this.classes.push(className); this.classes.push(className);
return this; return this;
} }
@ -68,11 +75,11 @@ class BasicWidget extends Component {
/** /**
* Sets the CSS attribute of the given name to the given value. * 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 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 value the value of the CSS attribute to set (e.g. `12px`).
* @returns self for chaining. * @returns self for chaining.
*/ */
css(name, value) { css(name: string, value: string) {
this.attrs.style += `${name}: ${value};`; this.attrs.style += `${name}: ${value};`;
return this; 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. * 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 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 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 value the value of the CSS attribute to set (e.g. `12px`).
* @returns self for chaining. * @returns self for chaining.
*/ */
optCss(condition, name, value) { optCss(condition: boolean, name: string, value: string) {
if (condition) { if (condition) {
return this.css(name, value); return this.css(name, value);
} }
@ -112,10 +119,9 @@ class BasicWidget extends Component {
/** /**
* Accepts a string of CSS to add with the widget. * Accepts a string of CSS to add with the widget.
* @param {string} block * @returns for chaining
* @returns {this} for chaining
*/ */
cssBlock(block) { cssBlock(block: string) {
this.cssEl = block; this.cssEl = block;
return this; return this;
} }
@ -123,7 +129,7 @@ class BasicWidget extends Component {
render() { render() {
try { try {
this.doRender(); this.doRender();
} catch (e) { } catch (e: any) {
this.logRenderingError(e); this.logRenderingError(e);
} }
@ -163,7 +169,7 @@ class BasicWidget extends Component {
return this.$widget; return this.$widget;
} }
logRenderingError(e) { logRenderingError(e: Error) {
console.log("Got issue in widget ", this); console.log("Got issue in widget ", this);
console.error(e); console.error(e);
@ -175,7 +181,7 @@ class BasicWidget extends Component {
icon: "alert", icon: "alert",
message: t("toast.widget-error.message-custom", { message: t("toast.widget-error.message-custom", {
id: noteId, id: noteId,
title: note.title, title: note?.title,
message: e.message message: e.message
}) })
}); });
@ -208,7 +214,7 @@ class BasicWidget extends Component {
*/ */
doRender() {} doRender() {}
toggleInt(show) { toggleInt(show: boolean) {
this.$widget.toggleClass('hidden-int', !show); this.$widget.toggleClass('hidden-int', !show);
} }
@ -216,7 +222,7 @@ class BasicWidget extends Component {
return this.$widget.hasClass('hidden-int'); return this.$widget.hasClass('hidden-int');
} }
toggleExt(show) { toggleExt(show: boolean) {
this.$widget.toggleClass('hidden-ext', !show); this.$widget.toggleClass('hidden-ext', !show);
} }

View File

@ -2,9 +2,27 @@ import { t } from "../../services/i18n.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import ws from "../../services/ws.js"; import ws from "../../services/ws.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import FAttribute from "../../entities/fattribute.js";
export default class AbstractBulkAction { interface ActionDefinition {
constructor(attribute, actionDef) { 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.attribute = attribute;
this.actionDef = actionDef; this.actionDef = actionDef;
} }
@ -20,18 +38,18 @@ export default class AbstractBulkAction {
utils.initHelpDropdown($rendered); utils.initHelpDropdown($rendered);
return $rendered; return $rendered;
} } catch (e: any) {
catch (e) {
logError(`Failed rendering search action: ${JSON.stringify(this.attribute.dto)} with error: ${e.message} ${e.stack}`); logError(`Failed rendering search action: ${JSON.stringify(this.attribute.dto)} with error: ${e.message} ${e.stack}`);
return null; return null;
} }
} }
// to be overridden // to be overridden
doRender() {} abstract doRender(): JQuery<HTMLElement>;
static get actionName() { return ""; }
async saveAction(data) { async saveAction(data: {}) {
const actionObject = Object.assign({ name: this.constructor.actionName }, data); const actionObject = Object.assign({ name: (this.constructor as typeof AbstractBulkAction).actionName }, data);
await server.put(`notes/${this.attribute.noteId}/attribute`, { await server.put(`notes/${this.attribute.noteId}/attribute`, {
attributeId: this.attribute.attributeId, attributeId: this.attribute.attributeId,

View File

@ -27,7 +27,36 @@ const TPL = `
</div> </div>
</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 { 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() { constructor() {
super(); super();
@ -37,6 +66,8 @@ export default class ConfirmDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
// TODO: Fix once we use proper ES imports.
//@ts-ignore
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget); this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$confirmContent = this.$widget.find(".confirm-dialog-content"); this.$confirmContent = this.$widget.find(".confirm-dialog-content");
this.$okButton = this.$widget.find(".confirm-dialog-ok-button"); 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)); this.$okButton.on('click', () => this.doResolve(true));
} }
showConfirmDialogEvent({ message, callback }) { showConfirmDialogEvent({ message, callback }: ConfirmWithMessageOptions) {
this.$originallyFocused = $(':focus'); this.$originallyFocused = $(':focus');
this.$custom.hide(); this.$custom.hide();
@ -77,8 +108,8 @@ export default class ConfirmDialog extends BasicWidget {
this.resolve = callback; this.resolve = callback;
} }
showConfirmDeleteNoteBoxWithNoteDialogEvent({ title, callback }) { showConfirmDeleteNoteBoxWithNoteDialogEvent({ title, callback }: ConfirmWithTitleOptions) {
glob.activeDialog = this.$widget; glob.activeDialog = this.$widget;
this.$confirmContent.text(`${t('confirm.are_you_sure_remove_note', { title: title })}`); 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; this.resolve = callback;
} }
doResolve(ret) { doResolve(ret: boolean) {
this.resolve({ if (this.resolve) {
confirmed: ret, this.resolve({
isDeleteNoteChecked: this.$widget.find(`.${DELETE_NOTE_BUTTON_CLASS}:checked`).length > 0 confirmed: ret,
}); isDeleteNoteChecked: this.$widget.find(`.${DELETE_NOTE_BUTTON_CLASS}:checked`).length > 0
});
}
this.resolve = null; this.resolve = null;

View File

@ -4,6 +4,25 @@ import linkService from "../../services/link.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import 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 = ` const TPL = `
<div class="delete-notes-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="delete-notes-dialog modal mx-auto" tabindex="-1" role="dialog">
@ -54,11 +73,29 @@ const TPL = `
</div>`; </div>`;
export default class DeleteNotesDialog extends BasicWidget { 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() { constructor() {
super(); super();
this.branchIds = null; this.branchIds = null;
this.resolve = null;
} }
doRender() { doRender() {
@ -98,7 +135,7 @@ export default class DeleteNotesDialog extends BasicWidget {
} }
async renderDeletePreview() { async renderDeletePreview() {
const response = await server.post('delete-notes-preview', { const response = await server.post<Response>('delete-notes-preview', {
branchIdsToDelete: this.branchIds, branchIdsToDelete: this.branchIds,
deleteAllClones: this.forceDeleteAllClones || this.isDeleteAllClonesChecked() 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.branchIds = branchIdsToDelete;
this.forceDeleteAllClones = forceDeleteAllClones; this.forceDeleteAllClones = forceDeleteAllClones;

View File

@ -1,5 +1,5 @@
import { t } from "../../services/i18n.js"; 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"; import BasicWidget from "../basic_widget.js";
const TPL = ` const TPL = `
@ -41,9 +41,25 @@ const TPL = `
</div> </div>
</div>`; </div>`;
export interface ChooseNoteTypeResponse {
success: boolean;
noteType?: string;
templateNoteId?: string;
}
type Callback = (data: ChooseNoteTypeResponse) => void;
export default class NoteTypeChooserDialog extends BasicWidget { 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.resolve = null;
this.$originalFocused = null; // element focused before the dialog was opened, so we can return to it afterward this.$originalFocused = null; // element focused before the dialog was opened, so we can return to it afterward
@ -51,10 +67,14 @@ export default class NoteTypeChooserDialog extends BasicWidget {
} }
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
// TODO: Remove once we import bootstrap the right way
//@ts-ignore
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget); this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown"); 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.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find(".note-type-dropdown-trigger"));
this.$widget.on("hidden.bs.modal", () => { this.$widget.on("hidden.bs.modal", () => {
@ -88,13 +108,15 @@ export default class NoteTypeChooserDialog extends BasicWidget {
this.$noteTypeDropdown.parent().on('hide.bs.dropdown', e => { this.$noteTypeDropdown.parent().on('hide.bs.dropdown', e => {
// prevent closing dropdown by clicking outside // prevent closing dropdown by clicking outside
// TODO: Check if this actually works.
//@ts-ignore
if (e.clickEvent) { if (e.clickEvent) {
e.preventDefault(); e.preventDefault();
} }
}); });
} }
async chooseNoteTypeEvent({ callback }) { async chooseNoteTypeEvent({ callback }: { callback: Callback }) {
this.$originalFocused = $(':focus'); this.$originalFocused = $(':focus');
const noteTypes = await noteTypesService.getNoteTypeItems(); const noteTypes = await noteTypesService.getNoteTypeItems();
@ -104,13 +126,12 @@ export default class NoteTypeChooserDialog extends BasicWidget {
for (const noteType of noteTypes) { for (const noteType of noteTypes) {
if (noteType.title === '----') { if (noteType.title === '----') {
this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates"))); this.$noteTypeDropdown.append($('<h6 class="dropdown-header">').append(t("note_type_chooser.templates")));
} } else {
else {
this.$noteTypeDropdown.append( this.$noteTypeDropdown.append(
$('<a class="dropdown-item" tabindex="0">') $('<a class="dropdown-item" tabindex="0">')
.attr("data-note-type", noteType.type) .attr("data-note-type", (noteType as NoteType).type)
.attr("data-template-note-id", noteType.templateNoteId) .attr("data-template-note-id", (noteType as NoteType).templateNoteId || "")
.append($("<span>").addClass(noteType.uiIcon)) .append($("<span>").addClass((noteType as NoteType).uiIcon))
.append(` ${noteType.title}`) .append(` ${noteType.title}`)
); );
} }
@ -127,16 +148,18 @@ export default class NoteTypeChooserDialog extends BasicWidget {
this.resolve = callback; this.resolve = callback;
} }
doResolve(e) { doResolve(e: JQuery.KeyDownEvent | JQuery.ClickEvent) {
const $item = $(e.target).closest(".dropdown-item"); const $item = $(e.target).closest(".dropdown-item");
const noteType = $item.attr("data-note-type"); const noteType = $item.attr("data-note-type");
const templateNoteId = $item.attr("data-template-note-id"); const templateNoteId = $item.attr("data-template-note-id");
this.resolve({ if (this.resolve) {
success: true, this.resolve({
noteType, success: true,
templateNoteId noteType,
}); templateNoteId
});
}
this.resolve = null; this.resolve = null;
this.modal.hide(); this.modal.hide();

View File

@ -20,7 +20,34 @@ const TPL = `
</div> </div>
</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 { 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() { constructor() {
super(); super();
@ -30,6 +57,8 @@ export default class PromptDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
// TODO: Fix once we use proper ES imports.
//@ts-ignore
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget); this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$dialogBody = this.$widget.find(".modal-body"); this.$dialogBody = this.$widget.find(".modal-body");
this.$form = this.$widget.find(".prompt-dialog-form"); 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", () => { this.$widget.on("hidden.bs.modal", () => {
@ -57,13 +86,15 @@ export default class PromptDialog extends BasicWidget {
this.$form.on('submit', e => { this.$form.on('submit', e => {
e.preventDefault(); e.preventDefault();
this.resolve(this.$answer.val()); if (this.resolve) {
this.resolve(this.$answer?.val() as string);
}
this.modal.hide(); this.modal.hide();
}); });
} }
showPromptDialogEvent({ title, message, defaultValue, shown, callback }) { showPromptDialogEvent({ title, message, defaultValue, shown, callback }: PromptDialogOptions) {
this.shownCb = shown; this.shownCb = shown;
this.resolve = callback; this.resolve = callback;
@ -71,7 +102,7 @@ export default class PromptDialog extends BasicWidget {
this.$question = $("<label>") this.$question = $("<label>")
.prop("for", "prompt-dialog-answer") .prop("for", "prompt-dialog-answer")
.text(message); .text(message || "");
this.$answer = $("<input>") this.$answer = $("<input>")
.prop("type", "text") .prop("type", "text")

View File

@ -7,6 +7,7 @@
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"allowJs": true,
"lib": [ "lib": [
"ES2022" "ES2022"
], ],
@ -18,14 +19,14 @@
"./src/**/*.js", "./src/**/*.js",
"./src/**/*.ts", "./src/**/*.ts",
"./*.ts", "./*.ts",
"./spec/**/*.ts", "./spec/**/*.ts"
"./spec-es6/**/*.ts"
], ],
"exclude": [ "exclude": [
"./src/public/**/*", "./node_modules/**/*",
"./node_modules/**/*" "./spec-es6/**/*.ts"
], ],
"files": [ "files": [
"src/types.d.ts" "src/types.d.ts",
"src/public/app/types.d.ts"
] ]
} }