mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 22:44:25 +01:00
Merge pull request #821 from TriliumNext/feature/client_typescript_port2
Port frontend to TypeScript (36.7% -> 48.5%)
This commit is contained in:
commit
2ec903893c
@ -10,4 +10,4 @@ echo By file
|
|||||||
cloc HEAD \
|
cloc HEAD \
|
||||||
--git --md \
|
--git --md \
|
||||||
--include-lang=javascript,typescript \
|
--include-lang=javascript,typescript \
|
||||||
--by-file
|
--by-file | grep \.js\|
|
||||||
@ -29,7 +29,7 @@ const copy = async () => {
|
|||||||
fs.copySync(path.join("build", srcFile), destFile, { recursive: true });
|
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));
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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)}`;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
18
src/public/app/server_types.ts
Normal file
18
src/public/app/server_types.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// TODO: Deduplicate with src/services/entity_changes_interface.ts
|
||||||
|
export interface EntityChange {
|
||||||
|
id?: number | null;
|
||||||
|
noteId?: string;
|
||||||
|
entityName: string;
|
||||||
|
entityId: string;
|
||||||
|
entity?: any;
|
||||||
|
positions?: Record<string, number>;
|
||||||
|
hash: string;
|
||||||
|
utcDateChanged?: string;
|
||||||
|
utcDateModified?: string;
|
||||||
|
utcDateCreated?: string;
|
||||||
|
isSynced: boolean | 1 | 0;
|
||||||
|
isErased: boolean | 1 | 0;
|
||||||
|
componentId?: string | null;
|
||||||
|
changeId?: string | null;
|
||||||
|
instanceId?: string | null;
|
||||||
|
}
|
||||||
@ -1,11 +1,19 @@
|
|||||||
|
import { AttributeType } from "../entities/fattribute.js";
|
||||||
import server from "./server.js";
|
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) {
|
||||||
@ -1,7 +0,0 @@
|
|||||||
declare module 'attribute_parser';
|
|
||||||
|
|
||||||
|
|
||||||
export function lex(str: string): any[]
|
|
||||||
export function parse(tokens: any[], str?: string, allowEmptyRelations?: boolean): any[]
|
|
||||||
export function lexAndParse(str: string, allowEmptyRelations?: boolean): any[]
|
|
||||||
|
|
||||||
@ -1,14 +1,30 @@
|
|||||||
|
import FAttribute, { AttributeType, FAttributeRow } from "../entities/fattribute.js";
|
||||||
import utils from "./utils.js";
|
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);
|
||||||
@ -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();
|
||||||
|
|
||||||
@ -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)) {
|
||||||
@ -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);
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@ -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) {
|
||||||
@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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);
|
||||||
@ -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}));
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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]);
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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: ";
|
||||||
|
|
||||||
@ -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)
|
||||||
@ -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 {
|
||||||
@ -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"),
|
||||||
@ -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) {
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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 => {
|
||||||
@ -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;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function exec(cmd) {
|
function exec(cmd: string) {
|
||||||
document.execCommand(cmd);
|
document.execCommand(cmd);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -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;
|
||||||
@ -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,"-");
|
||||||
}
|
}
|
||||||
@ -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")
|
||||||
@ -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 {
|
||||||
@ -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);
|
||||||
|
|
||||||
@ -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) {
|
||||||
@ -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>;
|
||||||
|
|||||||
@ -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,
|
||||||
@ -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);
|
||||||
@ -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;
|
||||||
@ -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);
|
||||||
|
|
||||||
@ -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);
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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"));
|
||||||
@ -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)) {
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 => {
|
||||||
|
|||||||
@ -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[] = [];
|
||||||
|
|||||||
122
src/public/app/types.d.ts
vendored
122
src/public/app/types.d.ts
vendored
@ -1,4 +1,12 @@
|
|||||||
import FNote from "./entities/fnote";
|
import type FNote from "./entities/fnote";
|
||||||
|
import type { BackendModule, i18n } from "i18next";
|
||||||
|
import type { Froca } from "./services/froca-interface";
|
||||||
|
import type { HttpBackendOptions } from "i18next-http-backend";
|
||||||
|
import { Suggestion } from "./services/note_autocomplete.ts";
|
||||||
|
import utils from "./services/utils.ts";
|
||||||
|
import appContext from "./components/app_context.ts";
|
||||||
|
import server from "./services/server.ts";
|
||||||
|
import library_loader, { Library } from "./services/library_loader.ts";
|
||||||
|
|
||||||
interface ElectronProcess {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,
|
||||||
@ -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;
|
||||||
|
|
||||||
@ -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;
|
||||||
|
|
||||||
@ -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();
|
||||||
@ -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")
|
||||||
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user