Compare commits

..

14 Commits

Author SHA1 Message Date
Elian Doran
f729e5b379 style(lexical): improve toolbar 2026-03-20 18:30:56 +02:00
Elian Doran
6e8c4813d9 fix(lexical): toolbar icon alignment 2026-03-20 18:28:51 +02:00
Elian Doran
029e8469b4 feat(lexical): add icons to toolbar and basic styling 2026-03-20 18:27:28 +02:00
Elian Doran
715b87fb3d chore(lexical): start working on a toolbar 2026-03-20 18:11:43 +02:00
Elian Doran
3d860ba2c4 feat(lexical): integrate scroll to end 2026-03-20 18:05:40 +02:00
Elian Doran
8b479ac4ba fix(lexical): unnecessary outline 2026-03-20 18:03:04 +02:00
Elian Doran
dcfcc7a334 fix(lexical): missing margin 2026-03-20 18:02:07 +02:00
Elian Doran
daa0109f7f fix(lexical): placeholder display issue 2026-03-20 18:00:47 +02:00
Elian Doran
f46e20aa25 feat(lexical): clear history when switching notes 2026-03-20 17:54:08 +02:00
Elian Doran
e8045424b2 feat(lexical): basic read 2026-03-20 17:51:23 +02:00
Elian Doran
7e6b628f86 feat(lexical): write without reading 2026-03-20 17:43:32 +02:00
Elian Doran
fc247fe790 feat(lexical): add very basic integration 2026-03-20 17:34:29 +02:00
Elian Doran
7a2fa2e829 feat(lexical): switch editable text based on MIME 2026-03-20 17:25:20 +02:00
Elian Doran
3e3f455b24 feat(lexical): integrate different MIME type for text 2026-03-20 17:17:44 +02:00
78 changed files with 2295 additions and 3918 deletions

2
.nvmrc
View File

@@ -1 +1 @@
24.14.1
24.14.0

View File

@@ -14,15 +14,15 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.33.0",
"packageManager": "pnpm@10.32.1",
"devDependencies": {
"@redocly/cli": "2.25.1",
"@redocly/cli": "2.24.0",
"archiver": "7.0.1",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"typedoc": "0.28.18",
"typedoc": "0.28.17",
"typedoc-plugin-missing-exports": "4.1.2"
}
}

View File

@@ -16,7 +16,6 @@
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
},
"dependencies": {
"@algolia/autocomplete-js": "1.19.6",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
@@ -25,6 +24,7 @@
"@fullcalendar/multimonth": "6.1.20",
"@fullcalendar/rrule": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@lexical/react": "0.42.0",
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.1",
"@mind-elixir/node-menu": "5.0.1",
@@ -45,6 +45,7 @@
"@univerjs/preset-sheets-sort": "0.18.0",
"@univerjs/presets": "0.18.0",
"@zumer/snapdom": "2.5.0",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
"clsx": "2.1.1",
@@ -53,22 +54,23 @@
"draggabilly": "3.0.0",
"force-graph": "1.51.2",
"globals": "17.4.0",
"i18next": "25.10.10",
"i18next": "25.8.18",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.43",
"katex": "0.16.39",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"lexical": "0.42.0",
"mark.js": "8.11.1",
"marked": "17.0.5",
"marked": "17.0.4",
"mermaid": "11.13.0",
"mind-elixir": "5.9.3",
"normalize.css": "8.0.1",
"panzoom": "9.4.4",
"panzoom": "9.4.3",
"preact": "10.29.0",
"react-i18next": "16.6.6",
"react-i18next": "16.5.8",
"react-window": "2.2.7",
"reveal.js": "6.0.0",
"rrule": "2.8.1",
@@ -86,9 +88,9 @@
"@types/mark.js": "8.11.12",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "14.0.0",
"happy-dom": "20.8.9",
"happy-dom": "20.8.4",
"lightningcss": "1.32.0",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.4.0"
"vite-plugin-static-copy": "3.3.0"
}
}

View File

@@ -1,6 +1,5 @@
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
import { closeAllHeadlessAutocompletes } from "../services/autocomplete_core.js";
import bundleService from "../services/bundle.js";
import dateNoteService from "../services/date_notes.js";
import froca from "../services/froca.js";
@@ -198,7 +197,7 @@ export default class Entrypoints extends Component {
hideAllPopups() {
if (utils.isDesktop()) {
closeAllHeadlessAutocompletes();
$(".aa-input").autocomplete("close");
}
}

View File

@@ -1,16 +1,15 @@
import type FNote from "../entities/fnote.js";
import { closeAllHeadlessAutocompletes } from "../services/autocomplete_core.js";
import froca from "../services/froca.js";
import linkService from "../services/link.js";
import options from "../services/options.js";
import server from "../services/server.js";
import SpacedUpdate from "../services/spaced_update.js";
import treeService from "../services/tree.js";
import Mutex from "../utils/mutex.js";
import type { EventData } from "./app_context.js";
import appContext from "./app_context.js";
import Component from "./component.js";
import SpacedUpdate from "../services/spaced_update.js";
import server from "../services/server.js";
import options from "../services/options.js";
import froca from "../services/froca.js";
import treeService from "../services/tree.js";
import NoteContext from "./note_context.js";
import appContext from "./app_context.js";
import Mutex from "../utils/mutex.js";
import linkService from "../services/link.js";
import type { EventData } from "./app_context.js";
import type FNote from "../entities/fnote.js";
interface TabState {
contexts: NoteContext[];
@@ -430,7 +429,10 @@ export default class TabManager extends Component {
}
// close dangling autocompletes after closing the tab
closeAllHeadlessAutocompletes();
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
// close dangling tooltips
$("body > div.tooltip").remove();

View File

@@ -1,3 +1,5 @@
import "autocomplete.js/index_jquery.js";
import type ElectronRemote from "@electron/remote";
import type Electron from "electron";

View File

@@ -16,6 +16,17 @@ async function initJQuery() {
const $ = (await import("jquery")).default;
window.$ = $;
window.jQuery = $;
// Polyfill removed jQuery methods for autocomplete.js compatibility
($ as any).isArray = Array.isArray;
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
($ as any).isPlainObject = function(obj: any) {
if (obj == null || typeof obj !== 'object') { return false; }
const proto = Object.getPrototypeOf(obj);
if (proto === null) { return true; }
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor === 'function' && Ctor === Object;
};
}
async function setupGlob() {

View File

@@ -39,6 +39,7 @@ export interface MenuCommandItem<T> {
title: string;
command?: T;
type?: string;
mime?: string;
/**
* The icon to display in the menu item.
*

View File

@@ -288,7 +288,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
return items.filter((row) => row !== null) as MenuItem<TreeCommandNames>[];
}
async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
async selectMenuItemHandler({ command, type, mime, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
const notePath = treeService.getNotePath(this.node);
if (utils.isMobile()) {
@@ -305,6 +305,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
target: "after",
targetBranchId: this.node.data.branchId,
type,
mime,
isProtected,
templateNoteId
});
@@ -313,6 +314,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
noteCreateService.createNote(parentNotePath, {
type,
mime,
isProtected: this.node.data.isProtected,
templateNoteId
});

View File

@@ -1,3 +1,5 @@
import "autocomplete.js/index_jquery.js";
import appContext from "./components/app_context.js";
import glob from "./services/glob.js";
import noteAutocompleteService from "./services/note_autocomplete.js";

View File

@@ -8,6 +8,17 @@ async function loadBootstrap() {
}
}
// Polyfill removed jQuery methods for autocomplete.js compatibility
($ as any).isArray = Array.isArray;
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
($ as any).isPlainObject = function(obj: any) {
if (obj == null || typeof obj !== 'object') { return false; }
const proto = Object.getPrototypeOf(obj);
if (proto === null) { return true; }
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor === 'function' && Ctor === Object;
};
(window as any).$ = $;
(window as any).jQuery = $;
await loadBootstrap();

View File

@@ -1,47 +0,0 @@
import { describe, expect, it } from "vitest";
import { shouldAutocompleteHandleEnterKey } from "./attribute_autocomplete.js";
describe("attribute autocomplete enter handling", () => {
it("delegates plain Enter when the panel is open and an item is active", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: false, metaKey: false },
{ isPanelOpen: true, hasActiveItem: true }
)).toBe(true);
});
it("does not delegate plain Enter when there is no active suggestion", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: false, metaKey: false },
{ isPanelOpen: true, hasActiveItem: false }
)).toBe(false);
});
it("does not delegate plain Enter when the panel is closed", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: false, metaKey: false },
{ isPanelOpen: false, hasActiveItem: false }
)).toBe(false);
});
it("does not delegate Ctrl+Enter even when an item is active", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: true, metaKey: false },
{ isPanelOpen: true, hasActiveItem: true }
)).toBe(false);
});
it("does not delegate Cmd+Enter even when an item is active", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: false, metaKey: true },
{ isPanelOpen: true, hasActiveItem: true }
)).toBe(false);
});
it("ignores non-Enter keys", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "ArrowDown", ctrlKey: false, metaKey: false },
{ isPanelOpen: false, hasActiveItem: false }
)).toBe(true);
});
});

View File

@@ -1,450 +1,114 @@
import type { AutocompleteApi as CoreAutocompleteApi, BaseItem } from "@algolia/autocomplete-core";
import { createAutocomplete } from "@algolia/autocomplete-core";
import type { AttributeType } from "../entities/fattribute.js";
import { bindAutocompleteInput, createHeadlessPanelController, registerHeadlessAutocompleteCloser, withHeadlessSourceDefaults } from "./autocomplete_core.js";
import server from "./server.js";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface NameItem extends BaseItem {
name: string;
}
export function shouldAutocompleteHandleEnterKey(
event: Pick<KeyboardEvent, "key" | "ctrlKey" | "metaKey">,
{ isPanelOpen, hasActiveItem }: { isPanelOpen: boolean; hasActiveItem: boolean }
) {
if (event.key !== "Enter") {
return true;
}
if (event.ctrlKey || event.metaKey) {
return false;
}
return isPanelOpen && hasActiveItem;
}
interface InitAttributeNameOptions {
/** The <input> element where the user types */
interface InitOptions {
$el: JQuery<HTMLElement>;
attributeType?: AttributeType | (() => AttributeType);
open: boolean;
/** Called when the user selects a value or the panel closes */
onValueChange?: (value: string) => void;
}
// ---------------------------------------------------------------------------
// Instance tracking
// ---------------------------------------------------------------------------
interface ManagedInstance {
autocomplete: CoreAutocompleteApi<NameItem>;
panelEl: HTMLElement;
cleanup: () => void;
}
const instanceMap = new WeakMap<HTMLElement, ManagedInstance>();
function renderItems(
panelEl: HTMLElement,
items: NameItem[],
activeItemId: number | null,
onSelect: (item: NameItem) => void,
onActivate: (index: number) => void,
onDeactivate: () => void
): void {
panelEl.innerHTML = "";
if (items.length === 0) {
panelEl.style.display = "none";
return;
}
const list = document.createElement("ul");
list.className = "aa-core-list";
items.forEach((item, index) => {
const li = document.createElement("li");
li.className = "aa-core-item";
if (index === activeItemId) {
li.classList.add("aa-core-item--active");
}
li.textContent = item.name;
li.addEventListener("mousemove", () => {
if (activeItemId === index) {
return;
}
onActivate(index);
});
li.addEventListener("mouseleave", (event) => {
const relatedTarget = event.relatedTarget;
if (relatedTarget instanceof HTMLElement && li.contains(relatedTarget)) {
return;
}
onDeactivate();
});
li.addEventListener("mousedown", (e) => {
e.preventDefault(); // prevent input blur
e.stopPropagation();
onSelect(item);
});
list.appendChild(li);
});
panelEl.appendChild(list);
}
// ---------------------------------------------------------------------------
// Attribute name autocomplete — new (autocomplete-core, headless)
// ---------------------------------------------------------------------------
function initAttributeNameAutocomplete({ $el, attributeType, open, onValueChange }: InitAttributeNameOptions) {
const inputEl = $el[0] as HTMLInputElement;
const syncQueryFromInputValue = (autocomplete: CoreAutocompleteApi<NameItem>) => {
autocomplete.setQuery(inputEl.value || "");
};
// Already initialized — just open if requested
if (instanceMap.has(inputEl)) {
if (open) {
const inst = instanceMap.get(inputEl)!;
syncQueryFromInputValue(inst.autocomplete);
inst.autocomplete.setIsOpen(true);
inst.autocomplete.refresh();
}
return;
}
const panelController = createHeadlessPanelController({ inputEl });
const { panelEl } = panelController;
let isPanelOpen = false;
let hasActiveItem = false;
const autocomplete = createAutocomplete<NameItem>({
openOnFocus: true,
defaultActiveItemId: 0,
shouldPanelOpen() {
return true;
},
getSources({ query }) {
return [
withHeadlessSourceDefaults({
sourceId: "attribute-names",
getItems() {
const type = typeof attributeType === "function" ? attributeType() : attributeType;
return server
.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(query)}`)
.then((names) => names.map((name) => ({ name })));
},
getItemInputValue({ item }) {
return item.name;
},
onSelect({ item }) {
inputEl.value = item.name;
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
},
}),
];
},
onStateChange({ state }) {
isPanelOpen = state.isOpen;
hasActiveItem = state.activeItemId !== null;
// Render items
const collections = state.collections;
const items = collections.length > 0 ? (collections[0].items as NameItem[]) : [];
const activeId = state.activeItemId ?? null;
if (state.isOpen && items.length > 0) {
renderItems(
panelEl,
items,
activeId,
(item) => {
inputEl.value = item.name;
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
},
(index) => {
autocomplete.setActiveItemId(index);
},
() => {
autocomplete.setActiveItemId(null);
}
);
panelController.startPositioning();
} else {
panelController.hide();
}
if (!state.isOpen) {
panelController.hide();
}
},
});
const unregisterGlobalCloser = registerHeadlessAutocompleteCloser(() => {
autocomplete.setIsOpen(false);
panelController.hide();
});
const cleanupInputBindings = bindAutocompleteInput<NameItem>({
inputEl,
autocomplete,
onInput(e, handlers) {
handlers.onChange(e as any);
},
onFocus(e, handlers) {
syncQueryFromInputValue(autocomplete);
handlers.onFocus(e as any);
},
onBlur() {
// Delay to allow mousedown on panel items
setTimeout(() => {
autocomplete.setIsOpen(false);
panelController.hide();
onValueChange?.(inputEl.value);
}, 50);
},
onKeyDown(e, handlers) {
if (!shouldAutocompleteHandleEnterKey(e, { isPanelOpen, hasActiveItem })) {
return;
}
if (e.key === "Enter") {
// Prevent the enter key from propagating to parent dialogs
// (which might interpret it as "submit" or "save and close")
e.stopPropagation();
}
handlers.onKeyDown(e as any);
}
});
const cleanup = () => {
unregisterGlobalCloser();
cleanupInputBindings();
panelController.destroy();
};
instanceMap.set(inputEl, { autocomplete, panelEl, cleanup });
if (open) {
syncQueryFromInputValue(autocomplete);
autocomplete.setIsOpen(true);
autocomplete.refresh();
panelController.startPositioning();
}
}
// ---------------------------------------------------------------------------
// Label value autocomplete (headless autocomplete-core)
// ---------------------------------------------------------------------------
interface LabelValueInitOptions {
$el: JQuery<HTMLElement>;
open: boolean;
nameCallback?: () => string;
onValueChange?: (value: string) => void;
}
function initLabelValueAutocomplete({ $el, open, nameCallback, onValueChange }: LabelValueInitOptions) {
const inputEl = $el[0] as HTMLInputElement;
const syncQueryFromInputValue = (autocomplete: CoreAutocompleteApi<NameItem>) => {
autocomplete.setQuery(inputEl.value || "");
};
/**
* @param $el - element on which to init autocomplete
* @param attributeType - "relation" or "label" or callback providing one of those values as a type of autocompleted attributes
* @param open - should the autocomplete be opened after init?
*/
function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions) {
if (!$el.hasClass("aa-input")) {
$el.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
},
[
{
displayKey: "name",
// disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type
cache: false,
source: async (term, cb) => {
const type = typeof attributeType === "function" ? attributeType() : attributeType;
if (instanceMap.has(inputEl)) {
if (open) {
const inst = instanceMap.get(inputEl)!;
syncQueryFromInputValue(inst.autocomplete);
inst.autocomplete.setIsOpen(true);
inst.autocomplete.refresh();
}
const names = await server.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`);
const result = names.map((name) => ({ name }));
cb(result);
}
}
]
);
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete("close");
}
});
}
if (open) {
$el.autocomplete("open");
}
}
async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptions) {
if ($el.hasClass("aa-input")) {
// we reinit every time because autocomplete seems to have a bug where it retains state from last
// open even though the value was reset
$el.autocomplete("destroy");
}
let attributeName = "";
if (nameCallback) {
attributeName = nameCallback();
}
if (attributeName.trim() === "") {
return;
}
const panelController = createHeadlessPanelController({ inputEl });
const { panelEl } = panelController;
const attributeValues = (await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`)).map((attribute) => ({ value: attribute }));
let isPanelOpen = false;
let hasActiveItem = false;
let isSelecting = false;
if (attributeValues.length === 0) {
return;
}
let cachedAttributeName = "";
let cachedAttributeValues: NameItem[] = [];
const handleSelect = (item: NameItem) => {
isSelecting = true;
inputEl.value = item.name;
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
isSelecting = false;
setTimeout(() => {
// Preserve the legacy contract: several consumers still commit the
// selected value from their existing Enter key handlers instead of
// listening to the autocomplete selection event directly.
inputEl.dispatchEvent(new KeyboardEvent("keydown", {
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
}));
}, 0);
};
const autocomplete = createAutocomplete<NameItem>({
openOnFocus: true,
defaultActiveItemId: null,
shouldPanelOpen() {
return true;
$el.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
openOnFocus: false, // handled manually
minLength: 0,
tabAutocomplete: false
},
[
{
displayKey: "value",
cache: false,
source: async function (term, cb) {
term = term.toLowerCase();
getSources({ query }) {
return [
withHeadlessSourceDefaults({
sourceId: "attribute-values",
async getItems() {
const attributeName = nameCallback ? nameCallback() : "";
if (!attributeName.trim()) {
return [];
}
const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term));
if (attributeName !== cachedAttributeName || cachedAttributeValues.length === 0) {
cachedAttributeName = attributeName;
const values = await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`);
cachedAttributeValues = values.map((name) => ({ name }));
}
const q = query.toLowerCase();
return cachedAttributeValues.filter((attr) => attr.name.toLowerCase().includes(q));
},
getItemInputValue({ item }) {
return item.name;
},
onSelect({ item }) {
handleSelect(item);
},
}),
];
},
onStateChange({ state }) {
isPanelOpen = state.isOpen;
hasActiveItem = state.activeItemId !== null;
const collections = state.collections;
const items = collections.length > 0 ? (collections[0].items as NameItem[]) : [];
const activeId = state.activeItemId ?? null;
if (state.isOpen && items.length > 0) {
renderItems(
panelEl,
items,
activeId,
handleSelect,
(index) => {
autocomplete.setActiveItemId(index);
},
() => {
autocomplete.setActiveItemId(null);
}
);
panelController.startPositioning();
} else {
panelController.hide();
cb(filtered);
}
}
]
);
if (!state.isOpen) {
panelController.hide();
}
},
});
const unregisterGlobalCloser = registerHeadlessAutocompleteCloser(() => {
autocomplete.setIsOpen(false);
panelController.hide();
});
const cleanupInputBindings = bindAutocompleteInput<NameItem>({
inputEl,
autocomplete,
onInput(e, handlers) {
if (!isSelecting) {
handlers.onChange(e as any);
}
},
onFocus(e, handlers) {
const attributeName = nameCallback ? nameCallback() : "";
if (attributeName !== cachedAttributeName) {
cachedAttributeName = "";
cachedAttributeValues = [];
}
syncQueryFromInputValue(autocomplete);
handlers.onFocus(e as any);
},
onBlur() {
setTimeout(() => {
autocomplete.setIsOpen(false);
panelController.hide();
onValueChange?.(inputEl.value);
}, 50);
},
onKeyDown(e, handlers) {
if (!shouldAutocompleteHandleEnterKey(e, { isPanelOpen, hasActiveItem })) {
return;
}
if (e.key === "Enter") {
e.stopPropagation();
}
handlers.onKeyDown(e as any);
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete("close");
}
});
const cleanup = () => {
unregisterGlobalCloser();
cleanupInputBindings();
panelController.destroy();
};
instanceMap.set(inputEl, { autocomplete, panelEl, cleanup });
if (open) {
syncQueryFromInputValue(autocomplete);
autocomplete.setIsOpen(true);
autocomplete.refresh();
panelController.startPositioning();
}
}
export function destroyAutocomplete($el: JQuery<HTMLElement> | HTMLElement) {
const inputEl = $el instanceof HTMLElement ? $el : $el[0] as HTMLInputElement;
const instance = instanceMap.get(inputEl);
if (instance) {
instance.cleanup();
instanceMap.delete(inputEl);
$el.autocomplete("open");
}
}
export default {
initAttributeNameAutocomplete,
destroyAutocomplete,
initLabelValueAutocomplete,
initLabelValueAutocomplete
};

View File

@@ -1,93 +0,0 @@
import $ from "jquery";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const {
showSpy,
hideSpy,
updateDisplayedShortcutsSpy,
saveFocusedElementSpy,
focusSavedElementSpy
} = vi.hoisted(() => ({
showSpy: vi.fn(),
hideSpy: vi.fn(),
updateDisplayedShortcutsSpy: vi.fn(),
saveFocusedElementSpy: vi.fn(),
focusSavedElementSpy: vi.fn()
}));
vi.mock("bootstrap", () => ({
Modal: {
getOrCreateInstance: vi.fn(() => ({
show: showSpy,
hide: hideSpy
}))
}
}));
vi.mock("./keyboard_actions.js", () => ({
default: {
updateDisplayedShortcuts: updateDisplayedShortcutsSpy
}
}));
vi.mock("./focus.js", () => ({
saveFocusedElement: saveFocusedElementSpy,
focusSavedElement: focusSavedElementSpy
}));
import { closeAllHeadlessAutocompletes, registerHeadlessAutocompleteCloser } from "./autocomplete_core.js";
import { openDialog } from "./dialog.js";
describe("headless autocomplete closing", () => {
const unregisterClosers: Array<() => void> = [];
beforeEach(() => {
vi.clearAllMocks();
(window as any).glob = {
...(window as any).glob,
activeDialog: null
};
});
afterEach(() => {
while (unregisterClosers.length > 0) {
unregisterClosers.pop()?.();
}
});
it("closes every registered closer and skips unregistered ones", () => {
const closer1 = vi.fn();
const closer2 = vi.fn();
const closer3 = vi.fn();
unregisterClosers.push(registerHeadlessAutocompleteCloser(closer1));
const unregister2 = registerHeadlessAutocompleteCloser(closer2);
unregisterClosers.push(unregister2);
unregisterClosers.push(registerHeadlessAutocompleteCloser(closer3));
unregister2();
closeAllHeadlessAutocompletes();
expect(closer1).toHaveBeenCalledTimes(1);
expect(closer2).not.toHaveBeenCalled();
expect(closer3).toHaveBeenCalledTimes(1);
});
it("closes registered autocompletes when a dialog finishes hiding", async () => {
const closer = vi.fn();
unregisterClosers.push(registerHeadlessAutocompleteCloser(closer));
const dialogEl = document.createElement("div");
const $dialog = $(dialogEl);
await openDialog($dialog, false);
$dialog.trigger("hidden.bs.modal");
expect(showSpy).toHaveBeenCalledTimes(1);
expect(updateDisplayedShortcutsSpy).toHaveBeenCalledWith($dialog);
expect(saveFocusedElementSpy).toHaveBeenCalledTimes(1);
expect(closer).toHaveBeenCalledTimes(1);
expect(focusSavedElementSpy).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,195 +0,0 @@
import type { AutocompleteApi, AutocompleteSource, BaseItem } from "@algolia/autocomplete-core";
export const HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR = ".aa-core-panel";
type HeadlessSourceDefaults = Required<Pick<AutocompleteSource<any>, "getItemUrl" | "onActive" | "onResolve">>;
const headlessAutocompleteClosers = new Set<() => void>();
export function withHeadlessSourceDefaults<TSource extends AutocompleteSource<any>>(
source: TSource
): TSource & HeadlessSourceDefaults {
return {
getItemUrl() {
return undefined;
},
onActive() {
// Headless consumers handle highlight side effects themselves.
},
onResolve() {
// Headless consumers resolve and render items manually.
},
...source
} as TSource & HeadlessSourceDefaults;
}
export function registerHeadlessAutocompleteCloser(close: () => void) {
headlessAutocompleteClosers.add(close);
return () => {
headlessAutocompleteClosers.delete(close);
};
}
export function closeAllHeadlessAutocompletes() {
for (const close of Array.from(headlessAutocompleteClosers)) {
close();
}
}
interface HeadlessPanelControllerOptions {
inputEl: HTMLElement;
container?: HTMLElement | null;
className?: string;
containedClassName?: string;
}
export function createHeadlessPanelController({
inputEl,
container,
className = "aa-core-panel",
containedClassName = "aa-core-panel--contained"
}: HeadlessPanelControllerOptions) {
const panelEl = document.createElement("div");
panelEl.className = className;
const isContained = Boolean(container);
if (isContained) {
panelEl.classList.add(containedClassName);
container!.appendChild(panelEl);
} else {
document.body.appendChild(panelEl);
}
panelEl.style.display = "none";
let rafId: number | null = null;
const positionPanel = () => {
if (isContained) {
panelEl.style.position = "static";
panelEl.style.top = "";
panelEl.style.left = "";
panelEl.style.width = "100%";
panelEl.style.display = "block";
return;
}
const rect = inputEl.getBoundingClientRect();
panelEl.style.position = "fixed";
panelEl.style.top = `${rect.bottom}px`;
panelEl.style.left = `${rect.left}px`;
panelEl.style.width = `${rect.width}px`;
panelEl.style.display = "block";
};
const stopPositioning = () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
};
const startPositioning = () => {
if (isContained) {
positionPanel();
return;
}
if (rafId !== null) {
return;
}
const update = () => {
positionPanel();
rafId = requestAnimationFrame(update);
};
update();
};
const hide = () => {
panelEl.style.display = "none";
stopPositioning();
};
const destroy = () => {
hide();
panelEl.remove();
};
return {
panelEl,
hide,
destroy,
startPositioning,
stopPositioning
};
}
type InputHandlers<TItem extends BaseItem> = ReturnType<AutocompleteApi<TItem>["getInputProps"]>;
interface InputBinding<TEvent extends Event = Event> {
type: string;
listener: (event: TEvent) => void;
}
interface BindAutocompleteInputOptions<TItem extends BaseItem> {
inputEl: HTMLInputElement;
autocomplete: AutocompleteApi<TItem>;
onInput?: (event: Event, handlers: InputHandlers<TItem>) => void;
onFocus?: (event: Event, handlers: InputHandlers<TItem>) => void;
onBlur?: (event: Event, handlers: InputHandlers<TItem>) => void;
onKeyDown?: (event: KeyboardEvent, handlers: InputHandlers<TItem>) => void;
extraBindings?: InputBinding[];
}
export function bindAutocompleteInput<TItem extends BaseItem>({
inputEl,
autocomplete,
onInput,
onFocus,
onBlur,
onKeyDown,
extraBindings = []
}: BindAutocompleteInputOptions<TItem>) {
const handlers = autocomplete.getInputProps({ inputElement: inputEl });
const bindings: InputBinding[] = [
{
type: "input",
listener: (event: Event) => {
onInput?.(event, handlers);
}
},
{
type: "focus",
listener: (event: Event) => {
onFocus?.(event, handlers);
}
},
{
type: "blur",
listener: (event: Event) => {
onBlur?.(event, handlers);
}
},
{
type: "keydown",
listener: (event: Event) => {
onKeyDown?.(event as KeyboardEvent, handlers);
}
},
...extraBindings
];
bindings.forEach(({ type, listener }) => {
inputEl.addEventListener(type, listener as EventListener);
});
return () => {
bindings.forEach(({ type, listener }) => {
inputEl.removeEventListener(type, listener as EventListener);
});
};
}

View File

@@ -1,11 +1,9 @@
import { Modal } from "bootstrap";
import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { closeAllHeadlessAutocompletes } from "./autocomplete_core.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
if (closeActDialog) {
@@ -17,7 +15,10 @@ export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog =
Modal.getOrCreateInstance($dialog[0], config).show();
$dialog.on("hidden.bs.modal", () => {
closeAllHeadlessAutocompletes();
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();

View File

@@ -1,9 +1,8 @@
import type { ActionKeyboardShortcut } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import type Component from "../components/component.js";
import server from "./server.js";
import appContext from "../components/app_context.js";
import shortcutService, { ShortcutBinding } from "./shortcuts.js";
import type Component from "../components/component.js";
import type { ActionKeyboardShortcut } from "@triliumnext/commons";
const keyboardActionRepo: Record<string, ActionKeyboardShortcut> = {};
@@ -52,10 +51,7 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
getActionsForScope("window").then((actions) => {
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts ?? []) {
shortcutService.bindGlobalShortcut(shortcut, () => {
const ntxId = appContext.tabManager?.activeNtxId ?? null;
appContext.triggerCommand(action.actionName, { ntxId });
});
shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
}
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
{ type: "text", mime: "application/json", title: "Text (Lexical)", icon: "bx-note" },
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true },
// Text notes group
@@ -97,6 +98,7 @@ function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandName
title: nt.title,
command,
type: nt.type,
mime: nt.mime,
uiIcon: `bx ${nt.icon}`,
badges: []
};

View File

@@ -892,6 +892,33 @@ table.promoted-attributes-in-tooltip th {
opacity: 1;
}
.algolia-autocomplete {
width: calc(100% - 30px);
z-index: 2000 !important;
}
.algolia-autocomplete-container .aa-dropdown-menu {
position: inherit !important;
overflow: auto;
}
.algolia-autocomplete .aa-input,
.algolia-autocomplete .aa-hint {
width: 100%;
}
.algolia-autocomplete .aa-dropdown-menu {
width: 100%;
background-color: var(--main-background-color);
border: 1px solid var(--main-border-color);
border-top: none;
z-index: 2000 !important;
max-height: 500px;
overflow: auto;
padding: 0;
margin: 0;
}
.aa-dropdown-menu .aa-suggestion {
cursor: pointer;
padding: 6px 16px;
@@ -933,153 +960,6 @@ table.promoted-attributes-in-tooltip th {
background-color: var(--active-item-background-color);
}
/* ===== @algolia/autocomplete-core (headless, custom panel) ===== */
.aa-core-panel {
z-index: 10000;
background-color: var(--main-background-color);
border: 1px solid var(--main-border-color);
border-top: none;
max-height: 500px;
overflow: auto;
padding: 0;
margin: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.aa-core-panel.aa-dropdown-menu {
width: 100%;
}
.aa-core-panel--contained {
position: static !important;
border: 0;
background: transparent;
box-shadow: none;
}
.aa-core-list {
list-style: none;
padding: 0;
margin: 0;
}
.aa-core-item {
cursor: pointer;
padding: 7px 16px;
margin: 0;
white-space: normal;
}
.aa-core-item--active {
color: var(--active-item-text-color);
background-color: var(--active-item-background-color);
}
.aa-core-item .note-suggestion {
display: flex;
align-items: flex-start;
gap: 8px;
width: 100%;
}
.aa-core-item .icon,
.aa-core-item .command-icon {
flex-shrink: 0;
line-height: 1.4;
margin-top: 1px;
}
.aa-core-item .text {
min-width: 0;
flex: 1;
}
.aa-core-item .aa-core-primary-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.aa-core-item .search-result-title {
display: block;
min-width: 0;
line-height: 1.35;
word-break: break-word;
font-size: 1.02em;
}
.aa-core-item .search-result-attributes {
display: block;
margin-top: 1px;
font-size: 0.8em;
color: var(--muted-text-color);
opacity: 0.65;
line-height: 1.2;
word-break: break-word;
}
.aa-core-item .search-result-attributes {
padding-inline-start: 14px;
}
.aa-core-item .aa-core-shortcut,
.aa-core-item kbd.command-shortcut {
flex-shrink: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--muted-text-color);
font-family: inherit !important;
font-size: 0.8em;
opacity: 0.85;
}
.aa-core-item .command-suggestion {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
font-size: 0.9em;
}
.aa-core-item .command-content {
flex-grow: 1;
min-width: 0;
}
.aa-core-item .command-name {
font-weight: bold;
line-height: 1.35;
}
.aa-core-item .command-description {
font-size: 0.8em;
line-height: 1.3;
opacity: 0.75;
}
.aa-core-item .search-result-title b,
.aa-core-item .search-result-path b,
.aa-core-item .search-result-attributes b,
.aa-core-item .command-name b,
.aa-core-item .command-description b {
color: var(--admonition-warning-accent-color);
text-decoration: underline;
}
.aa-core-item .aa-core-separator {
padding: 0 2px;
}
.jump-to-note-results .aa-core-panel--contained {
max-height: calc(80vh - 200px);
overflow-y: auto;
overflow-x: hidden;
text-overflow: ellipsis;
}
.help-button {
float: inline-end;
background: none;

View File

@@ -128,8 +128,8 @@
margin-inline: auto;
}
/* The headless autocomplete panel rendered into the empty-note results container */
.note-detail-empty .aa-core-panel--contained {
/* The search results list */
.note-detail-empty span.aa-dropdown-menu {
margin-top: 1em;
border: unset;
}

View File

@@ -93,10 +93,7 @@
"digits": "dígits",
"inheritable": "Heretable",
"delete": "Suprimeix",
"color_type": "Color",
"textarea": "Text multi linia",
"date_time": "Data i hora",
"precision_title": "Quants dígits han d'estar disponibles per a coma flotant a la interfície de configuració."
"color_type": "Color"
},
"rename_label": {
"to": "Per"

View File

@@ -446,8 +446,7 @@
"and_more": "... 以及另外 {{count}} 个。",
"print_landscape": "导出为 PDF 时,将页面方向更改为横向而不是纵向。",
"print_page_size": "导出为 PDF 时,更改页面大小。支持的值:<code>A0</code>、<code>A1</code>、<code>A2</code>、<code>A3</code>、<code>A4</code>、<code>A5</code>、<code>A6</code>、<code>Legal</code>、<code>Letter</code>、<code>Tabloid</code>、<code>Ledger</code>。",
"color_type": "颜色",
"textarea": "多行文本"
"color_type": "颜色"
},
"attribute_editor": {
"help_text_body1": "要添加标签,只需输入例如 <code>#rock</code> 或者如果您还想添加值,则例如 <code>#year = 2020</code>",
@@ -2168,52 +2167,5 @@
},
"setup_form": {
"more_info": "了解更多"
},
"media": {
"play": "播放(空格)",
"pause": "暂停(空格)",
"back-10s": "后退10秒左箭头键",
"forward-30s": "前进30秒",
"mute": "静音M",
"unmute": "取消静音M",
"playback-speed": "播放速度",
"loop": "循环播放",
"disable-loop": "禁用循环播放",
"rotate": "旋转",
"picture-in-picture": "画中画",
"exit-picture-in-picture": "退出画中画",
"fullscreen": "全屏F",
"exit-fullscreen": "退出全屏",
"unsupported-format": "此文件格式不支持媒体预览:\n{{mime}}",
"zoom-to-fit": "缩放以填充",
"zoom-reset": "重置缩放以填充"
},
"mermaid": {
"sample_diagrams": "示例图:",
"sample_flowchart": "流程图",
"sample_class": "类图",
"sample_sequence": "时序图",
"sample_entity_relationship": "实体关系图",
"sample_state": "状态图",
"sample_mindmap": "思维导图",
"sample_architecture": "架构图",
"sample_block": "模块图",
"sample_c4": "C4 图",
"sample_gantt": "甘特图",
"sample_git": "Git 流程图",
"sample_kanban": "看板图",
"sample_packet": "数据包图",
"sample_pie": "饼图",
"sample_quadrant": "象限图",
"sample_radar": "雷达图",
"sample_requirement": "需求图",
"sample_sankey": "桑基图",
"sample_timeline": "时间轴图",
"sample_treemap": "树形图",
"sample_user_journey": "用户旅程图",
"sample_xy": "散点图",
"sample_venn": "韦恩图",
"sample_ishikawa": "鱼骨图",
"placeholder": "输入你的美人鱼图的内容,或者使用下面的示例图之一。"
}
}

View File

@@ -446,8 +446,7 @@
"and_more": "... und {{count}} mehr.",
"print_landscape": "Beim Export als PDF, wird die Seitenausrichtung Querformat anstatt Hochformat verwendet.",
"print_page_size": "Beim Export als PDF, wird die Größe der Seite angepasst. Unterstützte Größen: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Farbe",
"textarea": "Mehrzeilen-Text"
"color_type": "Farbe"
},
"attribute_editor": {
"help_text_body1": "Um ein Label hinzuzufügen, gebe einfach z.B. ein. <code>#rock</code> oder wenn du auch einen Wert hinzufügen möchten, dann z.B. <code>#year = 2024</code>",

View File

@@ -1,6 +1,6 @@
{
"about": {
"title": "Σχετικά με το Trilium Notes",
"title": "Πληροφορίες για το Trilium Notes",
"homepage": "Αρχική Σελίδα:",
"app_version": "Έκδοση εφαρμογής:",
"db_version": "Έκδοση βάσης δεδομένων:",

View File

@@ -477,8 +477,7 @@
"and_more": "... agus {{count}} eile.",
"print_landscape": "Agus é á onnmhairiú go PDF, athraítear treoshuíomh an leathanaigh go tírdhreach seachas portráid.",
"print_page_size": "Agus é á easpórtáil go PDF, athraítear méid an leathanaigh. Luachanna tacaithe: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Dath",
"textarea": "Téacs Il-líne"
"color_type": "Dath"
},
"attribute_editor": {
"help_text_body1": "Chun lipéad a chur leis, clóscríobh m.sh. <code>#rock</code> nó más mian leat luach a chur leis freisin ansin m.sh. <code>#year = 2020</code>",

View File

@@ -520,7 +520,7 @@
"custom_name_label": "Nome del motore di ricerca personalizzato",
"custom_name_placeholder": "Personalizza il nome del motore di ricerca",
"custom_url_label": "L'URL del motore di ricerca personalizzato deve includere {keyword} come segnaposto per il termine di ricerca.",
"custom_url_placeholder": "Personalizza indirizzo url del motore di ricerca"
"custom_url_placeholder": "Personalizza l'URL del motore di ricerca"
},
"sql_table_schemas": {
"tables": "Tabelle"
@@ -917,8 +917,7 @@
"print_landscape": "Quando si esporta in PDF, cambia l'orientamento della pagina da verticale a orizzontale.",
"print_page_size": "Quando si esporta in PDF, modifica le dimensioni della pagina. Valori supportati: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Colore",
"share_root": "segna la nota che viene servita su /share root.",
"textarea": "Testo su più righe"
"share_root": "segna la nota che viene servita su /share root."
},
"attribute_editor": {
"help_text_body1": "Per aggiungere un'etichetta, basta digitare ad esempio <code>#rock</code> oppure, se si desidera aggiungere anche un valore, ad esempio <code>#year = 2020</code>",
@@ -2198,52 +2197,5 @@
},
"setup_form": {
"more_info": "Per saperne di più"
},
"media": {
"play": "Gioca (Barra spaziatrice)",
"pause": "Pausa (Barra spaziatrice)",
"back-10s": "Indietro di 10 (tasto freccia sinistra)",
"forward-30s": "Avanti 30s",
"mute": "Muto (M)",
"unmute": "Riattiva audio (M)",
"playback-speed": "Velocità di riproduzione",
"loop": "Ciclo",
"disable-loop": "Disattiva il ciclo",
"rotate": "Ruota",
"picture-in-picture": "Immagine nell'immagine",
"exit-picture-in-picture": "Esci dalla modalità picture-in-picture",
"fullscreen": "Schermo intero (F)",
"exit-fullscreen": "Esci dalla modalità a schermo intero",
"unsupported-format": "Per questo formato di file non è disponibile l'anteprima multimediale:\n{{mime}}",
"zoom-to-fit": "Ingrandisci per riempire",
"zoom-reset": "Ripristina lo zoom a schermo intero"
},
"mermaid": {
"placeholder": "Digita il contenuto del tuo diagramma Mermaid oppure utilizza uno dei diagrammi di esempio riportati di seguito.",
"sample_diagrams": "Esempi di diagrammi:",
"sample_flowchart": "Diagramma di flusso",
"sample_class": "Classe",
"sample_sequence": "Sequenza",
"sample_entity_relationship": "Relazioni tra entità",
"sample_state": "Stato",
"sample_mindmap": "Mappa mentale",
"sample_architecture": "Architettura",
"sample_block": "Blocco",
"sample_c4": "C4",
"sample_gantt": "Gantt",
"sample_git": "Git",
"sample_kanban": "Kanban",
"sample_packet": "Packet",
"sample_pie": "Torta",
"sample_quadrant": "Quadrante",
"sample_radar": "Radar",
"sample_requirement": "Requisito",
"sample_sankey": "Chiave",
"sample_timeline": "Cronologia",
"sample_treemap": "Treemap",
"sample_user_journey": "Percorso dell'utente",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa"
}
}

View File

@@ -1180,8 +1180,7 @@
"is_owned_by_note": "ノートによって所有されています",
"and_more": "...その他 {{count}} 件。",
"print_landscape": "PDF にエクスポートするときに、ページの向きを縦向きではなく横向きに変更します。",
"print_page_size": "PDF にエクスポートするときに、ページのサイズを変更します。サポートされる値: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>。",
"textarea": "複数行テキスト"
"print_page_size": "PDF にエクスポートするときに、ページのサイズを変更します。サポートされる値: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>。"
},
"link_context_menu": {
"open_note_in_popup": "クイック編集",

View File

@@ -117,7 +117,7 @@
"no_path_to_clone_to": "Brak ścieżki do sklonowania.",
"note_cloned": "Notatka \"{{clonedTitle}}\" została sklonowana do \"{{targetTitle}}\"",
"help_on_links": "Pomoc dotycząca linków",
"target_parent_note": "Docelowa notatka pierwotna"
"target_parent_note": "Docelowa notatka nadrzędna"
},
"help": {
"title": "Ściągawka",
@@ -126,7 +126,7 @@
"collapseExpand": "zwiń/rozwiń węzeł",
"notSet": "nie ustawiono",
"goBackForwards": "idź wstecz / do przodu w historii",
"showJumpToNoteDialog": "pokaż <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Przejdź do\"</a>",
"showJumpToNoteDialog": "pokaż okno <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Przejdź do\"</a>",
"scrollToActiveNote": "przewiń do aktywnej notatki",
"jumpToParentNote": "przejdź do notatki nadrzędnej",
"collapseWholeTree": "zwiń całe drzewo notatek",
@@ -402,8 +402,7 @@
"and_more": "... i {{count}} więcej.",
"print_landscape": "Podczas eksportowania do PDF zmienia orientację strony na poziomą zamiast pionowej.",
"print_page_size": "Podczas eksportowania do PDF zmienia rozmiar strony. Obsługiwane wartości: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Kolor",
"textarea": "Wiele linii tekstu"
"color_type": "Kolor"
},
"import": {
"importIntoNote": "Importuj do notatki",
@@ -1614,7 +1613,7 @@
"password_changed_success": "Hasło zostało zmienione. Trilium zostanie przeładowane po naciśnięciu OK."
},
"multi_factor_authentication": {
"title": "Uwierzytelnianie wieloskładnikowe",
"title": "Uwierzytelnianie wieloskładnikowe (MFA)",
"description": "Uwierzytelnianie wieloskładnikowe (MFA) dodaje dodatkową warstwę zabezpieczeń do Twojego konta. Zamiast tylko wpisywać hasło do logowania, MFA wymaga podania jednego lub więcej dodatkowych dowodów tożsamości. W ten sposób, nawet jeśli ktoś zdobędzie Twoje hasło, nadal nie będzie mógł uzyskać dostępu do Twojego konta bez drugiej informacji. To jak dodanie dodatkowego zamka do drzwi, utrudniającego włamanie.<br><br>Proszę postępować zgodnie z poniższymi instrukcjami, aby włączyć MFA. Jeśli nie skonfigurujesz poprawnie, logowanie powróci do samego hasła.",
"mfa_enabled": "Włącz uwierzytelnianie wieloskładnikowe",
"mfa_method": "Metoda MFA",
@@ -1629,7 +1628,7 @@
"totp_secret_generated": "Sekret TOTP wygenerowany",
"totp_secret_warning": "Proszę zapisać wygenerowany sekret w bezpiecznym miejscu. Nie zostanie pokazany ponownie.",
"totp_secret_regenerate_confirm": "Czy na pewno chcesz ponownie wygenerować sekret TOTP? To unieważni poprzedni sekret TOTP i wszystkie istniejące kody odzyskiwania.",
"recovery_keys_title": "Klucze odzyskiwania logowania jednokrotnego",
"recovery_keys_title": "Klucze odzyskiwania logowania jednokrotnego (SSO)",
"recovery_keys_description": "Klucze odzyskiwania logowania jednokrotnego służą do logowania w przypadku braku dostępu do kodów Authenticator.",
"recovery_keys_description_warning": "Klucze odzyskiwania nie zostaną pokazane ponownie po opuszczeniu strony, przechowuj je w bezpiecznym miejscu.<br>Po użyciu klucza odzyskiwania nie można go użyć ponownie.",
"recovery_keys_error": "Błąd generowania kodów odzyskiwania",
@@ -1767,7 +1766,7 @@
"book": "Kolekcja",
"mermaid-diagram": "Diagram Mermaid",
"canvas": "Płótno",
"web-view": "Widok strony web",
"web-view": "Widok WWW",
"mind-map": "Mapa myśli",
"file": "Plik",
"image": "Obraz",
@@ -1816,9 +1815,9 @@
"modal_title": "Konfiguracja listy wyróżnień",
"menu_configure": "Konfiguracja listy wyróżnień...",
"no_highlights": "Nie znaleziono wyróżnień.",
"title_with_count_one": "{{count}} wyróżnienie",
"title_with_count_few": "{{count}} wyróżnienia",
"title_with_count_many": "{{count}} wyróżnień"
"title_with_count_one": "{{count}} podświetlenie",
"title_with_count_few": "{{count}} podświetlenia",
"title_with_count_many": "{{count}} podświetleń"
},
"quick-search": {
"placeholder": "Szybkie wyszukiwanie",
@@ -2071,7 +2070,7 @@
"read_only_temporarily_disabled_description": "Ta notatka jest obecnie edytowalna, ale normalnie jest tylko do odczytu. Notatka powróci do trybu tylko do odczytu, gdy tylko przejdziesz do innej notatki.\n\nKliknij, aby ponownie włączyć tryb tylko do odczytu.",
"shared_publicly": "Udostępniona publicznie",
"shared_locally": "Udostępniona lokalnie",
"clipped_note": "Wycinek z sieci",
"clipped_note": "Wycinek WWW",
"clipped_note_description": "Ta notatka została pierwotnie pobrana z {{url}}.\n\nKliknij, aby przejść do źródłowej strony internetowej.",
"execute_script": "Uruchom skrypt",
"execute_script_description": "Ta notatka jest notatką skryptową. Kliknij, aby wykonać skrypt.",
@@ -2237,7 +2236,7 @@
"sample_c4": "C4",
"sample_gantt": "Wykres Gantta",
"sample_git": "Diagram Git",
"sample_kanban": "Tablica Kanban",
"sample_kanban": "Kanban",
"sample_packet": "Diagram pakietów",
"sample_pie": "Wykres kołowy",
"sample_quadrant": "Diagram kwadrantowy",

View File

@@ -446,8 +446,7 @@
"app_theme_base": "設定為 \"next\"、\"next-light \" 或 \"next-dark\",以使用相應的 TriliumNext 主題(自動、淺色或深色)作為自訂主題的基礎,而非傳統主題。",
"print_landscape": "匯出為 PDF 時,將頁面方向更改為橫向而非縱向。",
"print_page_size": "在匯出 PDF 時更改頁面大小。支援的值:<code>A0</code>、<code>A1</code>、<code>A2</code>、<code>A3</code>、<code>A4</code>、<code>A5</code>、<code>A6</code>、<code>Legal</code>、<code>Letter</code>、<code>Tabloid</code>、<code>Ledger</code>。",
"color_type": "顏色",
"textarea": "多行文字"
"color_type": "顏色"
},
"attribute_editor": {
"help_text_body1": "要新增標籤,只需輸入例如 <code>#rock</code> 或者如果您還想新增值,則例如 <code>#year = 2020</code>",
@@ -2183,52 +2182,5 @@
},
"setup_form": {
"more_info": "了解更多"
},
"media": {
"play": "播放 (空白鍵)",
"pause": "暫停 (空白鍵)",
"back-10s": "往前 10 秒 (左方向鍵)",
"forward-30s": "往後 30 秒",
"mute": "靜音 (M)",
"unmute": "解除靜音 (M)",
"playback-speed": "播放速度",
"loop": "循環",
"disable-loop": "解除循環",
"rotate": "旋轉",
"picture-in-picture": "畫中畫",
"exit-picture-in-picture": "退出畫中畫",
"fullscreen": "全螢幕 (F)",
"exit-fullscreen": "退出全螢幕",
"unsupported-format": "此檔案格式不支援媒體預覽:\n{{mime}}",
"zoom-to-fit": "放大至填滿畫面",
"zoom-reset": "重設放大至填滿畫面"
},
"mermaid": {
"placeholder": "請輸入您的美人魚圖表內容,或選用下方其中一個範例圖表。",
"sample_diagrams": "範例圖表:",
"sample_flowchart": "流程圖",
"sample_class": "階層圖",
"sample_sequence": "時序圖",
"sample_entity_relationship": "實體關係圖",
"sample_state": "狀態圖",
"sample_mindmap": "心智圖",
"sample_architecture": "架構圖",
"sample_block": "區塊圖",
"sample_c4": "C4 圖",
"sample_gantt": "甘特圖",
"sample_git": "Git 分支圖",
"sample_kanban": "看板圖",
"sample_packet": "數據包圖",
"sample_pie": "圓餅圖",
"sample_quadrant": "象限圖",
"sample_radar": "雷達圖",
"sample_requirement": "需求圖",
"sample_sankey": "桑基圖",
"sample_timeline": "時間軸",
"sample_treemap": "樹狀圖",
"sample_user_journey": "使用者旅程",
"sample_xy": "XY 圖表",
"sample_venn": "韋恩圖",
"sample_ishikawa": "魚骨圖"
}
}

View File

@@ -6,6 +6,7 @@ import type { PrintReport } from "./print";
import type { lint } from "./services/eslint";
import type { Froca } from "./services/froca-interface";
import { Library } from "./services/library_loader";
import { Suggestion } from "./services/note_autocomplete";
import server from "./services/server";
import utils from "./services/utils";
@@ -82,7 +83,34 @@ declare global {
"note-load-progress": CustomEvent<{ progress: number }>;
}
interface AutoCompleteConfig {
appendTo?: HTMLElement | null;
hint?: boolean;
openOnFocus?: boolean;
minLength?: number;
tabAutocomplete?: boolean;
autoselect?: boolean;
dropdownMenuContainer?: HTMLElement;
debug?: boolean;
}
type AutoCompleteCallback = (values: AutoCompleteArg[]) => void;
interface AutoCompleteArg {
name?: string;
value?: string;
notePathTitle?: string;
displayKey?: "name" | "value" | "notePathTitle";
cache?: boolean;
source?: (term: string, cb: AutoCompleteCallback) => void,
templates?: {
suggestion: (suggestion: Suggestion) => string | undefined
}
}
interface JQuery {
autocomplete: (action?: "close" | "open" | "destroy" | "val" | AutoCompleteConfig, args?: AutoCompleteArg[] | string) => JQuery<HTMLElement>;
getSelectedNotePath(): string | undefined;
getSelectedNoteId(): string | null;
setSelectedNotePath(notePath: string | null | undefined);

View File

@@ -8,7 +8,6 @@ import { Dispatch, StateUpdater, useCallback, useEffect, useRef, useState } from
import NoteContext from "../components/note_context";
import FAttribute from "../entities/fattribute";
import FNote from "../entities/fnote";
import attributeAutocompleteService from "../services/attribute_autocomplete";
import { Attribute } from "../services/attribute_parser";
import attributes from "../services/attributes";
import { t } from "../services/i18n";
@@ -37,7 +36,8 @@ interface CellProps {
setCellToFocus(cell: Cell): void;
}
type OnChangeEventData = TargetedEvent<HTMLInputElement | HTMLTextAreaElement, Event> | InputEvent;
type OnChangeEventData = TargetedEvent<HTMLInputElement | HTMLTextAreaElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
type OnChangeListener = (e: OnChangeEventData) => void | Promise<void>;
export default function PromotedAttributes() {
const { note, componentId, noteContext } = useNoteContext();
@@ -201,9 +201,10 @@ function LabelInput(props: CellProps & { inputId: string }) {
}, [ cell, componentId, note, setCells ]);
const extraInputProps: InputHTMLAttributes = {};
useTextLabelAutocomplete(inputId, valueAttr, definition, async (value) => {
setDraft(value);
await updateAttribute(note, cell, componentId, value, setCells);
useTextLabelAutocomplete(inputId, valueAttr, definition, (e) => {
if (e.currentTarget instanceof HTMLInputElement) {
setDraft(e.currentTarget.value);
}
});
// React to model changes.
@@ -259,7 +260,7 @@ function LabelInput(props: CellProps & { inputId: string }) {
className="open-external-link-button"
icon="bx bx-window-open"
title={t("promoted_attributes.open_external_link")}
onClick={() => {
onClick={(e) => {
const inputEl = document.getElementById(inputId) as HTMLInputElement | null;
const url = inputEl?.value;
if (url) {
@@ -414,31 +415,55 @@ function InputButton({ icon, className, title, onClick }: {
);
}
function useTextLabelAutocomplete(inputId: string, valueAttr: Attribute, definition: DefinitionObject, onValueChange: (value: string) => void) {
function useTextLabelAutocomplete(inputId: string, valueAttr: Attribute, definition: DefinitionObject, onChangeListener: OnChangeListener) {
const [ attributeValues, setAttributeValues ] = useState<{ value: string }[] | null>(null);
// Obtain data.
useEffect(() => {
if (definition.labelType !== "text") {
return;
}
server.get<string[]>(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributesValues) => {
setAttributeValues(_attributesValues.map((attribute) => ({ value: attribute })));
});
}, [ definition.labelType, valueAttr.name ]);
// Initialize autocomplete.
useEffect(() => {
if (attributeValues?.length === 0) return;
const el = document.getElementById(inputId) as HTMLInputElement | null;
if (!el) {
return;
}
if (!el) return;
const $input = $(el);
attributeAutocompleteService.initLabelValueAutocomplete({
$el: $input,
open: false,
nameCallback: () => valueAttr.name,
onValueChange: (value) => {
onValueChange(value);
}
});
$input.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
autoselect: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
},
[
{
displayKey: "value",
source (term, cb) {
term = term.toLowerCase();
return () => {
attributeAutocompleteService.destroyAutocomplete($input);
};
}, [ definition.labelType, inputId, onValueChange, valueAttr.name ]);
const filtered = (attributeValues ?? []).filter((attr) => attr.value.toLowerCase().includes(term));
cb(filtered);
}
}
]
);
$input.off("autocomplete:selected");
$input.on("autocomplete:selected", onChangeListener);
return () => $input.autocomplete("destroy");
}, [ inputId, attributeValues, onChangeListener ]);
}
async function updateAttribute(note: FNote, cell: Cell, componentId: string, value: string | undefined, setCells: Dispatch<StateUpdater<Cell[] | undefined>>) {

View File

@@ -1,19 +1,18 @@
import appContext from "../../components/app_context.js";
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
import type { Attribute } from "../../services/attribute_parser.js";
import { HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR } from "../../services/autocomplete_core.js";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
import froca from "../../services/froca.js";
import { t } from "../../services/i18n.js";
import server from "../../services/server.js";
import froca from "../../services/froca.js";
import linkService from "../../services/link.js";
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
import server from "../../services/server.js";
import shortcutService from "../../services/shortcuts.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import SpacedUpdate from "../../services/spaced_update.js";
import utils from "../../services/utils.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import shortcutService from "../../services/shortcuts.js";
import appContext from "../../components/app_context.js";
import type { Attribute } from "../../services/attribute_parser.js";
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
const TPL = /*html*/`
<div class="attr-detail tn-tool-dialog">
@@ -30,7 +29,6 @@ const TPL = /*html*/`
max-height: 600px;
overflow: auto;
box-shadow: 10px 10px 93px -25px black;
contain: none;
}
.attr-help td {
@@ -345,7 +343,6 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
private $relatedNotesList!: JQuery<HTMLElement>;
private $relatedNotesMoreNotes!: JQuery<HTMLElement>;
private $attrHelp!: JQuery<HTMLElement>;
private $statusBar?: JQuery<HTMLElement>;
private relatedNotesSpacedUpdate!: SpacedUpdate;
private attribute!: Attribute;
@@ -376,13 +373,13 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
}
});
this.$inputName.on("change", () => this.userEditedAttribute());
this.$inputName.on("autocomplete:closed", () => this.userEditedAttribute());
this.$inputName.on("focus", () => {
attributeAutocompleteService.initAttributeNameAutocomplete({
$el: this.$inputName,
attributeType: () => (["relation", "relation-definition"].includes(this.attrType || "") ? "relation" : "label"),
open: true,
onValueChange: () => this.userEditedAttribute(),
open: true
});
});
@@ -395,12 +392,12 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
}
});
this.$inputValue.on("change", () => this.userEditedAttribute());
this.$inputValue.on("autocomplete:closed", () => this.userEditedAttribute());
this.$inputValue.on("focus", () => {
attributeAutocompleteService.initLabelValueAutocomplete({
$el: this.$inputValue,
open: true,
nameCallback: () => String(this.$inputName.val()),
onValueChange: () => this.userEditedAttribute(),
nameCallback: () => String(this.$inputName.val())
});
});
@@ -481,9 +478,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.$relatedNotesMoreNotes = this.$relatedNotesContainer.find(".related-notes-more-notes");
$(window).on("mousedown", (e) => {
if (!$(e.target).closest(this.$widget[0]).length
&& !$(e.target).closest(HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR).length
&& !$(e.target).closest("#context-menu-container").length) {
if (!$(e.target).closest(this.$widget[0]).length && !$(e.target).closest(".algolia-autocomplete").length && !$(e.target).closest("#context-menu-container").length) {
this.hide();
}
});
@@ -582,24 +577,17 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
return;
}
if (isNewLayout) {
if (!this.$statusBar) {
this.$statusBar = $(document.body).find(".component.status-bar");
}
this.$widget
.css("left", detPosition.left)
.css("right", detPosition.right)
.css("top", y - offset.top + 70)
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
const statusBarHeight = this.$statusBar.outerHeight() ?? 0;
const maxHeight = document.body.clientHeight - statusBarHeight;
if (isNewLayout) {
this.$widget
.css("left", offset.left + (typeof detPosition.left === "number" ? detPosition.left : 0))
.css("top", "unset")
.css("bottom", statusBarHeight ?? 0)
.css("max-height", maxHeight);
} else {
this.$widget
.css("left", detPosition.left)
.css("right", detPosition.right)
.css("top", y - offset.top + 70)
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
.css("bottom", 70)
.css("max-height", "80vh");
}
if (focus === "name") {
@@ -707,14 +695,14 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
return "label-definition";
} else if (attribute.name.startsWith("relation:")) {
return "relation-definition";
} else {
return "label";
}
return "label";
} else if (attribute.type === "relation") {
return "relation";
} else {
this.$title.text("");
}
this.$title.text("");
}
updateAttributeInEditor() {

View File

@@ -364,19 +364,23 @@
mask-repeat: no-repeat;
mask-size: 100% 100%;
}
.ck-content p {
margin-bottom: 0.5em;
line-height: 1.3;
}
.ck-content figure.image {
width: 25%;
}
.ck-content .table {
display: flex;
flex-direction: column-reverse;
overflow-x: scroll;
--scrollbar-thickness: 0;
scrollbar-width: none;
table {
width: max-content;
table-layout: auto;
@@ -431,4 +435,4 @@
}
}
/* #endregion */
/* #endregion */

View File

@@ -1,70 +0,0 @@
import type { FunctionComponent } from "preact";
import { render } from "preact";
import { act } from "preact/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { type AddLinkDialogTestState,createAddLinkDialogTestState, setupAddLinkDialogMocks } from "./add_link.spec_utils";
describe("AddLinkDialog", () => {
let container: HTMLDivElement;
let AddLinkDialog: FunctionComponent;
let state: AddLinkDialogTestState;
beforeEach(async () => {
vi.resetModules();
state = createAddLinkDialogTestState();
vi.clearAllMocks();
setupAddLinkDialogMocks(state);
({ default: AddLinkDialog } = await import("./add_link"));
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
act(() => {
render(null, container);
});
container.remove();
});
it("submits the selected note when Enter picks an autocomplete suggestion", async () => {
act(() => {
render(<AddLinkDialog />, container);
});
const showDialog = state.triliumEventHandlers.get("showAddLinkDialog");
if (!showDialog) {
throw new Error("showAddLinkDialog handler was not registered");
}
await act(async () => {
showDialog({
text: "",
hasSelection: false,
addLink: state.addLinkSpy
});
});
act(() => {
state.latestNoteAutocompletePropsRef.current.onKeyDownCapture({
key: "Enter",
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
isComposing: false
});
state.latestNoteAutocompletePropsRef.current.onChange({
notePath: "root/target-note"
});
});
await act(async () => {
state.latestModalPropsRef.current.onHidden();
});
expect(state.addLinkSpy).toHaveBeenCalledWith("root/target-note", null);
});
});

View File

@@ -1,99 +0,0 @@
import $ from "jquery";
import type { ComponentChildren } from "preact";
import { vi } from "vitest";
export interface AddLinkDialogTestState {
triliumEventHandlers: Map<string, (payload: any) => void>;
latestModalPropsRef: { current: any };
latestNoteAutocompletePropsRef: { current: any };
addLinkSpy: ReturnType<typeof vi.fn>;
logErrorSpy: ReturnType<typeof vi.fn>;
showRecentNotesSpy: ReturnType<typeof vi.fn>;
setTextSpy: ReturnType<typeof vi.fn>;
}
export function createAddLinkDialogTestState(): AddLinkDialogTestState {
return {
triliumEventHandlers: new Map<string, (payload: any) => void>(),
latestModalPropsRef: { current: null as any },
latestNoteAutocompletePropsRef: { current: null as any },
addLinkSpy: vi.fn(() => Promise.resolve()),
logErrorSpy: vi.fn(),
showRecentNotesSpy: vi.fn(),
setTextSpy: vi.fn()
};
}
export function setupAddLinkDialogMocks(state: AddLinkDialogTestState) {
vi.doMock("../../services/i18n", () => ({
t: (key: string) => key
}));
vi.doMock("../../services/tree", () => ({
default: {
getNoteIdFromUrl: (notePath: string) => notePath.split("/").at(-1),
getNoteTitle: vi.fn(async () => "Target note")
}
}));
vi.doMock("../../services/ws", () => ({
logError: state.logErrorSpy
}));
vi.doMock("../../services/note_autocomplete", () => ({
__esModule: true,
default: {
showRecentNotes: state.showRecentNotesSpy,
setText: state.setTextSpy
}
}));
vi.doMock("../react/react_utils", () => ({
refToJQuerySelector: (ref: { current: HTMLInputElement | null }) => ref.current ? $(ref.current) : $()
}));
vi.doMock("../react/hooks", () => ({
useTriliumEvent: (name: string, handler: (payload: any) => void) => {
state.triliumEventHandlers.set(name, handler);
}
}));
vi.doMock("../react/Modal", () => ({
default: (props: any) => {
state.latestModalPropsRef.current = props;
if (!props.show) {
return null;
}
return (
<form onSubmit={(e) => {
e.preventDefault();
props.onSubmit?.();
}}>
{props.children}
{props.footer}
</form>
);
}
}));
vi.doMock("../react/FormGroup", () => ({
default: ({ children }: { children: ComponentChildren }) => <div>{children}</div>
}));
vi.doMock("../react/Button", () => ({
default: ({ text }: { text: string }) => <button type="submit">{text}</button>
}));
vi.doMock("../react/FormRadioGroup", () => ({
default: () => null
}));
vi.doMock("../react/NoteAutocomplete", () => ({
default: (props: any) => {
state.latestNoteAutocompletePropsRef.current = props;
return <input ref={props.inputRef} />;
}
}));
}

View File

@@ -1,17 +1,15 @@
import type { JSX } from "preact";
import { useEffect,useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import tree from "../../services/tree";
import { logError } from "../../services/ws";
import Button from "../react/Button";
import FormGroup from "../react/FormGroup.js";
import FormRadioGroup from "../react/FormRadioGroup";
import { useTriliumEvent } from "../react/hooks";
import Modal from "../react/Modal";
import Button from "../react/Button";
import FormRadioGroup from "../react/FormRadioGroup";
import NoteAutocomplete from "../react/NoteAutocomplete";
import { useRef, useState, useEffect } from "preact/hooks";
import tree from "../../services/tree";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import { logError } from "../../services/ws";
import FormGroup from "../react/FormGroup.js";
import { refToJQuerySelector } from "../react/react_utils";
import { useTriliumEvent } from "../react/hooks";
type LinkType = "reference-link" | "external-link" | "hyper-link";
@@ -28,8 +26,6 @@ export default function AddLinkDialog() {
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
const [ shown, setShown ] = useState(false);
const hasSubmittedRef = useRef(false);
const suggestionRef = useRef<Suggestion | null>(null);
const submitOnSelectionRef = useRef(false);
useTriliumEvent("showAddLinkDialog", opts => {
setOpts(opts);
@@ -89,44 +85,15 @@ export default function AddLinkDialog() {
.trigger("select");
}
function submitSelectedLink(selectedSuggestion: Suggestion | null) {
submitOnSelectionRef.current = false;
hasSubmittedRef.current = Boolean(selectedSuggestion);
if (!selectedSuggestion) {
logError("No link to add.");
return;
}
// Insertion logic in onHidden because it needs focus.
setShown(false);
}
function onSuggestionChange(nextSuggestion: Suggestion | null) {
suggestionRef.current = nextSuggestion;
setSuggestion(nextSuggestion);
if (submitOnSelectionRef.current && nextSuggestion) {
submitSelectedLink(nextSuggestion);
}
}
function onAutocompleteKeyDownCapture(e: JSX.TargetedKeyboardEvent<HTMLInputElement>) {
if (e.key !== "Enter" || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || e.isComposing) {
return;
}
submitOnSelectionRef.current = true;
}
function onAutocompleteKeyUpCapture(e: JSX.TargetedKeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
submitOnSelectionRef.current = false;
}
}
function onSubmit() {
submitSelectedLink(suggestionRef.current);
hasSubmittedRef.current = true;
if (suggestion) {
// Insertion logic in onHidden because it needs focus.
setShown(false);
} else {
logError("No link to add.");
}
}
const autocompleteRef = useRef<HTMLInputElement>(null);
@@ -142,22 +109,19 @@ export default function AddLinkDialog() {
onSubmit={onSubmit}
onShown={onShown}
onHidden={() => {
submitOnSelectionRef.current = false;
// Insert the link.
if (hasSubmittedRef.current && suggestionRef.current && opts) {
if (hasSubmittedRef.current && suggestion && opts) {
hasSubmittedRef.current = false;
if (suggestionRef.current.notePath) {
if (suggestion.notePath) {
// Handle note link
opts.addLink(suggestionRef.current.notePath, linkType === "reference-link" ? null : linkTitle);
} else if (suggestionRef.current.externalLink) {
opts.addLink(suggestion.notePath, linkType === "reference-link" ? null : linkTitle);
} else if (suggestion.externalLink) {
// Handle external link
opts.addLink(suggestionRef.current.externalLink, linkTitle, true);
opts.addLink(suggestion.externalLink, linkTitle, true);
}
}
suggestionRef.current = null;
setSuggestion(null);
setShown(false);
}}
@@ -166,9 +130,7 @@ export default function AddLinkDialog() {
<FormGroup label={t("add_link.note")} name="note">
<NoteAutocomplete
inputRef={autocompleteRef}
onChange={onSuggestionChange}
onKeyDownCapture={onAutocompleteKeyDownCapture}
onKeyUpCapture={onAutocompleteKeyUpCapture}
onChange={setSuggestion}
opts={{
allowExternalLinks: true,
allowCreatingNotes: true

View File

@@ -108,4 +108,4 @@ async function cloneNotesTo(notePath: string, clonedNoteIds: string[], prefix?:
toast.showMessage(t("clone_to.note_cloned", { clonedTitle: clonedNote.title, targetTitle: targetNote.title }));
}
}
}

View File

@@ -1,15 +1,14 @@
import Modal from "../react/Modal";
import Button from "../react/Button";
import NoteAutocomplete from "../react/NoteAutocomplete";
import { t } from "../../services/i18n";
import { useRef, useState } from "preact/hooks";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import appContext from "../../components/app_context";
import commandRegistry from "../../services/command_registry";
import { t } from "../../services/i18n";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import shortcutService from "../../services/shortcuts";
import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks";
import Modal from "../react/Modal";
import NoteAutocomplete from "../react/NoteAutocomplete";
import { refToJQuerySelector } from "../react/react_utils";
import { useTriliumEvent } from "../react/hooks";
import shortcutService from "../../services/shortcuts";
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
@@ -24,14 +23,14 @@ export default function JumpToNoteDialogComponent() {
const [ initialText, setInitialText ] = useState(isCommandMode ? "> " : "");
const actualText = useRef<string>(initialText);
const [ shown, setShown ] = useState(false);
async function openDialog(commandMode: boolean) {
async function openDialog(commandMode: boolean) {
let newMode: Mode;
let initialText = "";
if (commandMode) {
newMode = "commands";
initialText = ">";
initialText = ">";
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
// if you open the Jump To dialog soon after using it previously, it can often mean that you
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
@@ -59,7 +58,7 @@ export default function JumpToNoteDialogComponent() {
if (!suggestion) {
return;
}
setShown(false);
if (suggestion.notePath) {
appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath);
@@ -84,7 +83,7 @@ export default function JumpToNoteDialogComponent() {
$autoComplete
.trigger("focus")
.trigger("select");
// Add keyboard shortcut for full search
shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => {
if (!isCommandMode) {
@@ -92,7 +91,7 @@ export default function JumpToNoteDialogComponent() {
}
});
}
async function showInFullSearch() {
try {
setShown(false);
@@ -127,18 +126,18 @@ export default function JumpToNoteDialogComponent() {
setIsCommandMode(text.startsWith(">"));
}}
onChange={onItemSelected}
/>}
/>}
onShown={onShown}
onHidden={() => setShown(false)}
footer={!isCommandMode && <Button
className="show-in-full-text-button"
text={t("jump_to_note.search_button")}
footer={!isCommandMode && <Button
className="show-in-full-text-button"
text={t("jump_to_note.search_button")}
keyboardShortcut="Ctrl+Enter"
onClick={showInFullSearch}
/>}
show={shown}
>
<div className="jump-to-note-results" ref={containerRef} />
<div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div>
</Modal>
);
}

View File

@@ -75,4 +75,4 @@ async function moveNotesTo(movedBranchIds: string[] | undefined, parentBranchId:
const parentNote = await parentBranch?.getNote();
toast.showMessage(`${t("move_to.move_success_message")} ${parentNote?.title}`);
}
}

View File

@@ -1,12 +1,11 @@
import { useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import FormGroup from "../react/FormGroup";
import FormTextBox from "../react/FormTextBox";
import { useTriliumEvent } from "../react/hooks";
import Modal from "../react/Modal";
import FormTextBox from "../react/FormTextBox";
import FormGroup from "../react/FormGroup";
import { refToJQuerySelector } from "../react/react_utils";
import { useTriliumEvent } from "../react/hooks";
// JQuery here is maintained for compatibility with existing code.
interface ShownCallbackData {
@@ -41,7 +40,7 @@ export default function PromptDialog() {
opts.current = newOpts;
setValue(newOpts.defaultValue ?? "");
setShown(true);
});
})
return (
<Modal
@@ -61,7 +60,7 @@ export default function PromptDialog() {
answerRef.current?.select();
}}
onSubmit={() => {
submitValue.current = answerRef.current?.value || value;
submitValue.current = value;
setShown(false);
}}
onHidden={() => {

View File

@@ -1,152 +0,0 @@
import $ from "jquery";
import { render } from "preact";
import { act } from "preact/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const {
initNoteAutocompleteSpy,
setTextSpy,
clearTextSpy,
destroyAutocompleteSpy
} = vi.hoisted(() => ({
initNoteAutocompleteSpy: vi.fn(($el) => $el),
setTextSpy: vi.fn(),
clearTextSpy: vi.fn(),
destroyAutocompleteSpy: vi.fn()
}));
vi.mock("../../services/i18n", () => ({
t: (key: string) => key
}));
vi.mock("../../services/note_autocomplete", () => ({
__esModule: true,
default: {
initNoteAutocomplete: initNoteAutocompleteSpy,
setText: setTextSpy,
clearText: clearTextSpy,
destroyAutocomplete: destroyAutocompleteSpy
}
}));
import NoteAutocomplete from "./NoteAutocomplete";
describe("NoteAutocomplete", () => {
let container: HTMLDivElement;
let setNoteSpy: ReturnType<typeof vi.fn>;
let getSelectedNoteIdSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
container = document.createElement("div");
document.body.appendChild(container);
setNoteSpy = vi.fn(() => Promise.resolve());
getSelectedNoteIdSpy = vi.fn(() => "selected-note-id");
($.fn as any).setNote = setNoteSpy;
($.fn as any).getSelectedNoteId = getSelectedNoteIdSpy;
});
afterEach(() => {
act(() => {
render(null, container);
});
container.remove();
});
it("syncs text props through the headless helper functions", () => {
act(() => {
render(<NoteAutocomplete text="hello" />, container);
});
const input = container.querySelector("input") as HTMLInputElement;
expect(initNoteAutocompleteSpy).toHaveBeenCalledTimes(1);
expect(initNoteAutocompleteSpy.mock.calls[0][0][0]).toBe(input);
expect(setTextSpy).toHaveBeenCalledTimes(1);
expect(setTextSpy.mock.calls[0][0][0]).toBe(input);
expect(setTextSpy).toHaveBeenCalledWith(expect.anything(), "hello");
act(() => {
render(<NoteAutocomplete text="" />, container);
});
expect(clearTextSpy).toHaveBeenCalled();
});
it("syncs noteId props through the jQuery setNote extension", () => {
act(() => {
render(<NoteAutocomplete noteId="note-123" />, container);
});
expect(setNoteSpy).toHaveBeenCalledWith("note-123");
expect(clearTextSpy).not.toHaveBeenCalled();
});
it("forwards autocomplete selection and clear events to consumers", () => {
const onChange = vi.fn();
const noteIdChanged = vi.fn();
act(() => {
render(<NoteAutocomplete onChange={onChange} noteIdChanged={noteIdChanged} />, container);
});
const input = container.querySelector("input") as HTMLInputElement;
const $input = $(input);
const suggestion = { notePath: "root/child-note", noteTitle: "Child note" };
$input.trigger("autocomplete:noteselected", [suggestion]);
expect(onChange).toHaveBeenCalledWith(suggestion);
expect(noteIdChanged).toHaveBeenCalledWith("child-note");
input.value = "";
$input.trigger("change");
expect(onChange).toHaveBeenCalledWith(null);
});
it("forwards onTextChange, onKeyDown and onBlur events", () => {
const onTextChange = vi.fn();
const onKeyDown = vi.fn();
const onBlur = vi.fn();
act(() => {
render(
<NoteAutocomplete
onTextChange={onTextChange}
onKeyDown={onKeyDown}
onBlur={onBlur}
/>,
container
);
});
const input = container.querySelector("input") as HTMLInputElement;
const $input = $(input);
input.value = "typed text";
$input.trigger("input");
$input.trigger($.Event("keydown", { originalEvent: new KeyboardEvent("keydown", { key: "Enter" }) }));
$input.trigger("blur");
expect(onTextChange).toHaveBeenCalledWith("typed text");
expect(onKeyDown).toHaveBeenCalledWith(expect.any(KeyboardEvent));
expect(onBlur).toHaveBeenCalledWith("selected-note-id");
});
it("destroys the autocomplete instance on unmount", () => {
act(() => {
render(<NoteAutocomplete />, container);
});
const input = container.querySelector("input") as HTMLInputElement;
act(() => {
render(null, container);
});
expect(destroyAutocompleteSpy).toHaveBeenCalledWith(input);
});
});

View File

@@ -1,9 +1,8 @@
import type { JSX,RefObject } from "preact";
import type { CSSProperties } from "preact/compat";
import { useEffect } from "preact/hooks";
import { t } from "../../services/i18n";
import { useEffect } from "preact/hooks";
import note_autocomplete, { Options, type Suggestion } from "../../services/note_autocomplete";
import type { RefObject } from "preact";
import type { CSSProperties } from "preact/compat";
import { useSyncedRef } from "./hooks";
interface NoteAutocompleteProps {
@@ -17,118 +16,85 @@ interface NoteAutocompleteProps {
onChange?: (suggestion: Suggestion | null) => void;
onTextChange?: (text: string) => void;
onKeyDown?: (e: KeyboardEvent) => void;
onKeyDownCapture?: JSX.KeyboardEventHandler<HTMLInputElement>;
onKeyUpCapture?: JSX.KeyboardEventHandler<HTMLInputElement>;
onBlur?: (newValue: string) => void;
noteIdChanged?: (noteId: string) => void;
noteId?: string;
}
export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged, onKeyDown, onKeyDownCapture, onKeyUpCapture, onBlur }: NoteAutocompleteProps) {
export default function NoteAutocomplete({ id, inputRef: externalInputRef, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged, onKeyDown, onBlur }: NoteAutocompleteProps) {
const ref = useSyncedRef<HTMLInputElement>(externalInputRef);
useEffect(() => {
if (!ref.current) return;
const inputEl = ref.current;
const $autoComplete = $(inputEl);
const $autoComplete = $(ref.current);
// clear any event listener added in previous invocation of this function
$autoComplete
.off("autocomplete:noteselected")
.off("autocomplete:commandselected")
// The headless autocomplete keeps internal state while the user types, so
// initialize it once per mount and drive updates through the helper methods below.
note_autocomplete.initNoteAutocomplete($autoComplete, {
...opts,
container: container?.current
});
return () => {
note_autocomplete.destroyAutocomplete(inputEl);
};
}, []);
useEffect(() => {
if (!ref.current) return;
const $autoComplete = $(ref.current);
const inputListener = () => onTextChange?.($autoComplete[0].value);
const keyDownListener = (e) => e.originalEvent && onKeyDown?.(e.originalEvent);
const blurListener = () => onBlur?.($autoComplete.getSelectedNoteId() ?? "");
if (onTextChange) {
$autoComplete.on("input", inputListener);
$autoComplete.on("input", () => onTextChange($autoComplete[0].value));
}
if (onKeyDown) {
$autoComplete.on("keydown", keyDownListener);
$autoComplete.on("keydown", (e) => e.originalEvent && onKeyDown(e.originalEvent));
}
if (onBlur) {
$autoComplete.on("blur", blurListener);
$autoComplete.on("blur", () => onBlur($autoComplete.getSelectedNoteId() ?? ""));
}
}, [opts, container?.current]);
return () => {
if (onTextChange) {
$autoComplete.off("input", inputListener);
}
if (onKeyDown) {
$autoComplete.off("keydown", keyDownListener);
}
if (onBlur) {
$autoComplete.off("blur", blurListener);
}
};
}, [onBlur, onKeyDown, onTextChange]);
// On change event handlers.
useEffect(() => {
if (!ref.current) return;
const $autoComplete = $(ref.current);
if (!(onChange || noteIdChanged)) {
return;
}
const autoCompleteListener = (_e, suggestion) => {
onChange?.(suggestion);
if (onChange || noteIdChanged) {
const autoCompleteListener = (_e, suggestion) => {
onChange?.(suggestion);
if (noteIdChanged) {
const noteId = suggestion?.notePath?.split("/")?.at(-1);
noteIdChanged(noteId);
}
};
const changeListener = (e) => {
if (!ref.current?.value) {
autoCompleteListener(e, null);
}
};
$autoComplete
.on("autocomplete:noteselected", autoCompleteListener)
.on("autocomplete:externallinkselected", autoCompleteListener)
.on("autocomplete:commandselected", autoCompleteListener)
.on("change", changeListener);
return () => {
if (noteIdChanged) {
const noteId = suggestion?.notePath?.split("/")?.at(-1);
noteIdChanged(noteId);
}
};
const changeListener = (e) => {
if (!ref.current?.value) {
autoCompleteListener(e, null);
}
};
$autoComplete
.off("autocomplete:noteselected", autoCompleteListener)
.off("autocomplete:externallinkselected", autoCompleteListener)
.off("autocomplete:commandselected", autoCompleteListener)
.off("change", changeListener);
};
}, [onChange, noteIdChanged]);
.on("autocomplete:noteselected", autoCompleteListener)
.on("autocomplete:externallinkselected", autoCompleteListener)
.on("autocomplete:commandselected", autoCompleteListener)
.on("change", changeListener);
return () => {
$autoComplete
.off("autocomplete:noteselected", autoCompleteListener)
.off("autocomplete:externallinkselected", autoCompleteListener)
.off("autocomplete:commandselected", autoCompleteListener)
.off("change", changeListener);
};
}
}, [opts, container?.current, onChange, noteIdChanged])
useEffect(() => {
if (!ref.current) return;
const $autoComplete = $(ref.current);
if (noteId) {
void $autoComplete.setNote(noteId);
return;
$autoComplete.setNote(noteId);
} else if (text) {
note_autocomplete.setText($autoComplete, text);
} else {
$autoComplete.setSelectedNotePath("");
$autoComplete.autocomplete("val", "");
ref.current.value = "";
}
if (text !== undefined) {
if (text) {
note_autocomplete.setText($autoComplete, text);
} else {
note_autocomplete.clearText($autoComplete);
}
return;
}
note_autocomplete.clearText($autoComplete);
}, [text, noteId]);
return (
@@ -137,8 +103,6 @@ export default function NoteAutocomplete({ id, inputRef: externalInputRef, text,
id={id}
ref={ref}
className="note-autocomplete form-control"
onKeyDownCapture={onKeyDownCapture}
onKeyUpCapture={onKeyUpCapture}
placeholder={placeholder ?? t("add_link.search_note")} />
</div>
);

View File

@@ -1,4 +1,3 @@
import { createPortal } from "preact/compat";
import { useEffect, useState } from "preact/hooks";
import FAttribute from "../../entities/fattribute";
@@ -75,7 +74,7 @@ export default function InheritedAttributesTab({ note, componentId, emptyListStr
)}
</div>
{createPortal(attributeDetailWidgetEl, document.body)}
{attributeDetailWidgetEl}
</div>
);
}

View File

@@ -1,6 +1,5 @@
import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5";
import { AttributeType } from "@triliumnext/commons";
import { createPortal } from "preact/compat";
import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/hooks";
import type { CommandData, FilteredCommandNames } from "../../../components/app_context";
@@ -337,8 +336,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
let matchedAttr: Attribute | null = null;
for (const attr of parsedAttrs) {
if (attr.startIndex !== undefined && clickIndex > attr.startIndex &&
attr.endIndex !== undefined && clickIndex <= attr.endIndex) {
if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) {
matchedAttr = attr;
break;
}
@@ -409,7 +407,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
)}
</div>}
{createPortal(attributeDetailWidgetEl, document.body)}
{attributeDetailWidgetEl}
</>
);
}

View File

@@ -50,8 +50,9 @@ body.desktop {
border-radius: 8px;
}
.note-detail-empty-results .aa-core-panel--contained {
border: 0;
.note-detail-empty-results .aa-dropdown-menu {
border: var(--bs-border-width) solid var(--bs-border-color);
border-top: 0;
}
.empty-tab-search label {

View File

@@ -25,9 +25,8 @@ function NoteSearch({ ntxId }: { ntxId: string | null }) {
// Show recent notes.
useEffect(() => {
queueMicrotask(() => {
note_autocomplete.showRecentNotes(refToJQuerySelector(autocompleteRef));
});
const $autoComplete = refToJQuerySelector(autocompleteRef);
note_autocomplete.showRecentNotes($autoComplete);
}, []);
return (

View File

@@ -14,10 +14,11 @@ import note_create from "../../../services/note_create";
import options from "../../../services/options";
import toast from "../../../services/toast";
import utils, { hasTouchBar, isMobile } from "../../../services/utils";
import { useEditorSpacedUpdate, useLegacyImperativeHandlers, useNoteLabel, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import { useEditorSpacedUpdate, useLegacyImperativeHandlers, useNoteLabel, useNoteProperty, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar";
import { TypeWidgetProps } from "../type_widget";
import CKEditorWithWatchdog, { CKEditorApi } from "./CKEditorWithWatchdog";
import LexicalText from "./lexical";
import getTemplates, { updateTemplateCache } from "./snippets.js";
import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./utils";
@@ -27,7 +28,15 @@ import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./util
* - Ballon block mode, in which there is a floating toolbar for the selected text, but another floating button for the entire block (i.e. paragraph).
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
*/
export default function EditableText({ note, parentComponent, ntxId, noteContext }: TypeWidgetProps) {
export default function EditableText(props: TypeWidgetProps) {
const mime = useNoteProperty(props.note, "mime");
if (mime === "application/json") {
return <LexicalText {...props} />;
}
return <EditableTextCKEditor {...props} />;
}
function EditableTextCKEditor({ note, parentComponent, ntxId, noteContext }: TypeWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<string>("");
const watchdogRef = useRef<EditorWatchdog>(null);

View File

@@ -0,0 +1,41 @@
.note-detail-editable-text {
.toolbar {
display: flex;
margin-bottom: 1px;
background: var(--classic-toolbar-vert-layout-background-color);
padding: 3px 6px;
border-radius: 6px;
margin: 20px;
vertical-align: middle;
}
.toolbar .divider {
width: 1px;
background-color: var(--main-border-color);
margin: 0 6px;
}
.toolbar .toolbar-item .text {
display: flex;
line-height: 20px;
width: 200px;
vertical-align: middle;
font-size: 14px;
color: #777;
text-overflow: ellipsis;
width: 70px;
overflow: hidden;
height: 20px;
text-align: left;
}
.toolbar .toolbar-item .icon {
display: flex;
width: 20px;
height: 20px;
user-select: none;
margin-right: 8px;
line-height: 16px;
background-size: contain;
}
}

View File

@@ -0,0 +1,177 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import "./ToolbarPlugin.css";
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
CAN_REDO_COMMAND,
CAN_UNDO_COMMAND,
COMMAND_PRIORITY_LOW,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
REDO_COMMAND,
SELECTION_CHANGE_COMMAND,
UNDO_COMMAND,
} from 'lexical';
import {useCallback, useEffect, useRef, useState} from 'react';
import ActionButton, { ActionButtonProps } from "../../../react/ActionButton";
function Divider() {
return <div className="divider" />;
}
export default function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const toolbarRef = useRef(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const $updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
}
}, []);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({editorState}) => {
editorState.read(
() => {
$updateToolbar();
},
{editor},
);
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, _newEditor) => {
$updateToolbar();
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, $updateToolbar]);
return (
<div className="toolbar" ref={toolbarRef}>
<ToolbarButton
disabled={!canUndo}
onClick={() => {
editor.dispatchCommand(UNDO_COMMAND, undefined);
}}
text="Undo"
icon="bx bx-undo"
/>
<ToolbarButton
disabled={!canRedo}
onClick={() => {
editor.dispatchCommand(REDO_COMMAND, undefined);
}}
text="Redo"
icon="bx bx-redo"
/>
<Divider />
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
active={isBold}
text="Format Bold"
icon="bx bx-bold"
/>
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}}
active={isItalic}
text="Format Italics"
icon="bx bx-italic"
/>
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}}
active={isUnderline}
text="Format Underline"
icon="bx bx-underline"
/>
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
}}
active={isStrikethrough}
text="Format Strikethrough"
icon="bx bx-strikethrough"
/>
<Divider />
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left');
}}
text="Left Align"
icon="bx bx-align-left"
/>
<ToolbarButton
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center');
}}
text="Center Align"
icon="bx bx-align-middle"
/>
<ToolbarButton
onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')}
text="Right Align"
icon="bx bx-align-right"
/>
<ToolbarButton
onClick={() => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify')}
text="Justify Align"
icon="bx bx-align-justify"
/>{' '}
</div>
);
}
function ToolbarButton(props: Pick<ActionButtonProps, "icon" | "disabled" | "onClick" | "text">) {
return (
<ActionButton
className="toolbar-item"
{...props}
/>
);
}

View File

@@ -0,0 +1,21 @@
.note-detail-editable-text .lexical-wrapper {
color: var(--main-text-color);
font-family: var(--main-font-family);
font-size: var(--main-font-size);
line-height: 1.5;
word-break: break-word;
position: relative;
margin-inline: var(--content-margin-inline);
>div[contenteditable="true"] {
outline: 0;
}
.lexical-placeholder {
opacity: 0.5;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
}

View File

@@ -0,0 +1,120 @@
import "./index.css";
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {$createRangeSelection, $getRoot, $setSelection, CLEAR_HISTORY_COMMAND} from 'lexical';
import { useEffect } from 'preact/hooks';
import { useEditorSpacedUpdate, useTriliumEvent } from '../../../react/hooks';
import { TypeWidgetProps } from "../../type_widget";
import ToolbarPlugin from "./ToolbarPlugin";
const theme = {
// Theme styling goes here
//...
};
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error) {
console.error(error);
}
export default function LexicalText(props: TypeWidgetProps) {
const initialConfig = {
namespace: 'MyEditor',
theme,
onError,
};
const placeholder = (
<div className="lexical-placeholder">
Enter some text...
</div>
);
return (
<LexicalComposer initialConfig={initialConfig}>
<ToolbarPlugin />
<div className="lexical-wrapper">
<RichTextPlugin
contentEditable={<ContentEditable /> as never}
placeholder={placeholder as never}
ErrorBoundary={LexicalErrorBoundary}
/>
</div>
<HistoryPlugin />
<AutoFocusPlugin />
<ScrollToEndPlugin />
<CustomEditorPersistencePlugin {...props} />
</LexicalComposer>
);
}
function CustomEditorPersistencePlugin({ note, noteContext }: TypeWidgetProps) {
const [editor] = useLexicalComposerContext();
const spacedUpdate = useEditorSpacedUpdate({
note,
noteContext,
noteType: "text",
getData() {
return {
content: JSON.stringify(editor.toJSON().editorState)
};
},
onContentChange(newContent) {
if (!newContent) {
editor.update(() => {
$getRoot().clear();
});
return;
}
try {
const editorState = editor.parseEditorState(newContent);
editor.setEditorState(editorState);
} catch (err) {
console.error("Error parsing Lexical content", err);
}
},
});
// Clear the history whenever note changes.
useEffect(() => {
editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined);
}, [ editor, note ]);
// Detect changes in content.
useEffect(() => {
return editor.registerUpdateListener(() => {
spacedUpdate.scheduleUpdate();
});
}, [ spacedUpdate, editor ]);
}
function ScrollToEndPlugin() {
const [editor] = useLexicalComposerContext();
useTriliumEvent("scrollToEnd", () => {
editor.update(() => {
const root = $getRoot();
const lastChild = root.getLastDescendant();
if (lastChild) {
const selection = $createRangeSelection();
selection.anchor.set(lastChild.getKey(), lastChild.getTextContentSize(), 'text');
selection.focus.set(lastChild.getKey(), lastChild.getTextContentSize(), 'text');
$setSelection(selection);
}
});
editor.focus();
});
return null;
}

View File

@@ -5,7 +5,7 @@
"description": "Tool to compare content of Trilium databases. Useful for debugging sync problems.",
"dependencies": {
"colors": "1.4.0",
"diff": "8.0.4",
"diff": "8.0.3",
"sqlite": "5.1.1",
"sqlite3": "6.0.1"
},

View File

@@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "14.0.0",
"electron": "41.0.4",
"electron": "41.0.3",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",

View File

@@ -6,7 +6,7 @@
"dependencies": {
"better-sqlite3": "12.8.0",
"mime-types": "3.0.2",
"sanitize-filename": "1.6.4",
"sanitize-filename": "1.6.3",
"tsx": "4.21.0",
"yargs": "18.0.0"
},

View File

@@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "14.0.0",
"electron": "41.0.4",
"electron": "41.0.3",
"fs-extra": "11.3.4"
},
"scripts": {

View File

@@ -1,5 +1,4 @@
import { expect,test } from "@playwright/test";
import { test, expect } from "@playwright/test";
import App from "../support/app";
const TEXT_NOTE_TITLE = "Text notes";
@@ -33,7 +32,8 @@ test("Open the note in the correct split pane", async ({ page, context }) => {
await noteContent.focus();
// Click the search result in the second split.
await app.getNoteAutocompleteSuggestion(resultsSelector, CODE_NOTE_TITLE).click();
await resultsSelector.locator(".aa-suggestion", { hasText: CODE_NOTE_TITLE })
.nth(1).click();
await expect(split2).toContainText(CODE_NOTE_TITLE);
});
@@ -69,4 +69,4 @@ test("Can directly focus the autocomplete input within the split", async ({ page
await page.waitForTimeout(100);
await expect(autocomplete).toBeFocused();
});
});

View File

@@ -27,7 +27,6 @@ export default class App {
readonly currentNoteSplitContent: Locator;
readonly sidebar: Locator;
private isMobile: boolean = false;
private readonly noteAutocompleteSuggestionSelector = ".aa-suggestion:not(.create-note-action):not(.search-notes-action):not(.command-action):not(.external-link-action)";
constructor(page: Page, context: BrowserContext) {
this.page = page;
@@ -77,19 +76,12 @@ export default class App {
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
await expect(resultsSelector).toContainText(noteTitle);
const suggestionSelector = resultsSelector
.locator(this.noteAutocompleteSuggestionSelector, { hasText: noteTitle })
.first();
const suggestionSelector = resultsSelector.locator(".aa-suggestion")
.nth(1); // Select the second one (best candidate), as the first one is "Create a new note"
await expect(suggestionSelector).toContainText(noteTitle);
await suggestionSelector.click();
}
getNoteAutocompleteSuggestion(resultsContainer: Locator, noteTitle: string) {
return resultsContainer
.locator(this.noteAutocompleteSuggestionSelector, { hasText: noteTitle })
.first();
}
async goToSettings() {
await this.page.locator(".launcher-button.bx-cog").click();
}

View File

@@ -83,13 +83,13 @@
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "5.0.1",
"electron": "41.0.4",
"electron": "41.0.3",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "5.2.1",
"express-http-proxy": "2.1.2",
"express-openid-connect": "2.20.0",
"express-openid-connect": "2.19.4",
"express-rate-limit": "8.3.1",
"express-session": "1.19.0",
"file-uri-to-path": "2.0.0",
@@ -99,21 +99,21 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "8.0.0",
"https-proxy-agent": "8.0.0",
"i18next": "25.10.10",
"i18next": "25.8.18",
"i18next-fs-backend": "2.6.1",
"image-type": "6.1.0",
"image-type": "6.0.0",
"ini": "6.0.0",
"is-animated": "2.0.2",
"is-svg": "6.1.0",
"jimp": "1.6.0",
"lorem-ipsum": "2.0.8",
"marked": "17.0.5",
"marked": "17.0.4",
"mime-types": "3.0.2",
"multer": "2.1.1",
"normalize-strings": "1.1.1",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.17.2",
"sax": "1.6.0",
"serve-favicon": "2.5.1",
@@ -126,8 +126,8 @@
"tmp": "0.2.5",
"turnish": "1.8.0",
"unescape": "1.0.1",
"vite": "8.0.2",
"ws": "8.20.0",
"vite": "8.0.1",
"ws": "8.19.0",
"xml2js": "0.6.2",
"yauzl": "3.2.1"
}

View File

@@ -14,7 +14,7 @@
"creating-and-moving-notes": "Tworzenie i przenoszenie notatek",
"create-note-after": "Utwórz notatkę po aktywnej notatce",
"create-note-into": "Utwórz notatkę jako podrzędną aktywnej notatki",
"create-note-into-inbox": "Utwórz notatkę w skrzynce odbiorczej (jeśli zdefiniowano) lub w notatce dziennej",
"create-note-into-inbox": "Utwórz notatkę w skrzynce odbiorczej (jeśli zdefiniowana) lub notatkę dnia",
"delete-note": "Usuń notatkę",
"move-note-up": "Przenieś notatkę w górę",
"move-note-down": "Przenieś notatkę w dół",
@@ -59,7 +59,7 @@
"show-backend-log": "Otwórz stronę \"Logi backendu\"",
"show-help": "Otwórz wbudowany Poradnik Użytkownika",
"show-cheatsheet": "Pokaż listę skrótów klawiszowych",
"text-note-operations": "Operacje na notatkach",
"text-note-operations": "Operacje na notatkach tekstowych",
"add-link-to-text": "Otwórz okno dodawania linku do tekstu",
"follow-link-under-cursor": "Podążaj za linkiem pod kursorem",
"insert-date-and-time-to-text": "Wstaw aktualną datę i czas",

View File

@@ -71,27 +71,6 @@ function getAttributeNames(type: string, nameLike: string) {
[type, `%${nameLike}%`]
);
// Also include attribute definitions (e.g. 'relation:*' or 'label:*') which are saved as type='label'
if (type === "relation" || type === "label") {
const prefix = `${type}:`;
const defNames = sql.getColumn<string>(
/*sql*/`SELECT DISTINCT name
FROM attributes
WHERE isDeleted = 0
AND type = 'label'
AND name LIKE ?`,
[`${prefix}%${nameLike}%`]
);
for (const dn of defNames) {
if (dn.startsWith(prefix)) {
const stripped = dn.substring(prefix.length);
if (!names.includes(stripped)) {
names.push(stripped);
}
}
}
}
for (const attr of BUILTIN_ATTRIBUTES) {
if (attr.type === type && attr.name.toLowerCase().includes(nameLike) && !names.includes(attr.name)) {
names.push(attr.name);

View File

@@ -13,7 +13,7 @@
"postinstall": "wxt prepare"
},
"keywords": [],
"packageManager": "pnpm@10.33.0",
"packageManager": "pnpm@10.32.1",
"devDependencies": {
"@wxt-dev/auto-icons": "1.1.1",
"wxt": "0.20.20"

View File

@@ -9,21 +9,21 @@
"preview": "pnpm build && vite preview"
},
"dependencies": {
"i18next": "25.10.10",
"i18next": "25.8.18",
"i18next-http-backend": "3.0.2",
"preact": "10.29.0",
"preact-iso": "2.11.1",
"preact-render-to-string": "6.6.6",
"react-i18next": "16.6.6"
"react-i18next": "16.5.8"
},
"devDependencies": {
"@preact/preset-vite": "2.10.5",
"eslint": "10.1.0",
"@preact/preset-vite": "2.10.4",
"eslint": "10.0.3",
"eslint-config-preact": "2.0.0",
"typescript": "5.9.3",
"user-agent-data-types": "0.4.3",
"vite": "8.0.2",
"vitest": "4.1.2"
"user-agent-data-types": "0.4.2",
"vite": "8.0.1",
"vitest": "4.1.0"
},
"eslintConfig": {
"extends": "preact"

View File

@@ -201,7 +201,7 @@
"resources": {
"title": "Risorse",
"icon_packs": "Pacchetti di icone",
"icon_packs_intro": "Ampliate la selezione di icone disponibili per le vostre note utilizzando un pacchetto di icone. Per ulteriori informazioni sui pacchetti di icone, consultate la <DocumentationLink>documentazione ufficiale</DocumentationLink>.",
"icon_packs_intro": "Ampliate la selezione di icone disponibili per le vostre note utilizzando un pacchetto di icone. Per ulteriori informazioni sui pacchetti di icone, consultate la<DocumentationLink>documentazione ufficiale</DocumentationLink>.",
"download": "Scarica",
"website": "Sito web"
}

View File

@@ -201,7 +201,7 @@
"resources": {
"title": "Zasoby",
"icon_packs": "Paczki ikon",
"icon_packs_intro": "Rozszerz wybór dostępnych ikon dla swoich notatek, korzystając z pakietu ikon. Więcej informacji na temat pakietów ikon znajdziesz w <DocumentationLink> oficjalnej dokumentacji </DocumentationLink>.",
"icon_packs_intro": "Rozszerz wybór dostępnych ikon dla swoich notatek, korzystając z pakietu ikon. Więcej informacji na temat pakietów ikon znajdziesz w <DocumentationLink> dokumentacji </DocumentationLink>.",
"download": "Pobieranie",
"website": "Strona internetowa"
}

2
docs/README-pl.md vendored
View File

@@ -48,7 +48,7 @@ wiedzy.
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
Nasza dokumentacja jest dostępna w wielu formatach:
- **Dokumentacja online**: Przeglądaj pełną dokumentację pod linkiem
- **Dokumentacja Online**: Pełna dokumentacja dostępna na
[docs.triliumnotes.org](https://docs.triliumnotes.org/)
- **Pomoc w aplikacji**: Naciśnij `F1` w Trilium, aby uzyskać dostęp do tej
samej dokumentacji bezpośrednio w aplikacji

17
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1774171785,
"narHash": "sha256-upDSNdH1WEL2Z0ISvRXTWk7rEndTxUcaTOLY9imJYa8=",
"lastModified": 1769184885,
"narHash": "sha256-wVX5Cqpz66SINNsmt3Bv/Ijzzfl8EPUISq5rKK129K0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f8a13215c766347f3da9beef4cfc952eb23fa46e",
"rev": "12689597ba7a6d776c3c979f393896be095269d4",
"type": "github"
},
"original": {
@@ -43,16 +43,15 @@
]
},
"locked": {
"lastModified": 1774171918,
"narHash": "sha256-0OBrtBnowvYP/YMKh7GB1GX22ORK+2X771EVgT+1tsk=",
"owner": "TriliumNext",
"lastModified": 1749022118,
"narHash": "sha256-7Qzmy1snKbxFBKoqUrfyxxmEB8rPxDdV7PQwRiAR01o=",
"owner": "FliegendeWurst",
"repo": "pnpm2nix-nzbr",
"rev": "536d67261ffe7c91cb286c8581cc799a1b61e969",
"rev": "35f88a41d29839b3989f31871263451c8e092cb1",
"type": "github"
},
"original": {
"owner": "TriliumNext",
"ref": "fix/optional_dependencies_filtering",
"owner": "FliegendeWurst",
"repo": "pnpm2nix-nzbr",
"type": "github"
}

View File

@@ -5,7 +5,7 @@
nixpkgs.url = "github:NixOS/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
pnpm2nix = {
url = "github:TriliumNext/pnpm2nix-nzbr/fix/optional_dependencies_filtering";
url = "github:FliegendeWurst/pnpm2nix-nzbr";
inputs = {
flake-utils.follows = "flake-utils";
nixpkgs.follows = "nixpkgs";
@@ -325,8 +325,6 @@
buildInputs = [
nodejs
pnpm
electron
nodejs.python
];
};
}

View File

@@ -52,19 +52,19 @@
"@types/express": "5.0.6",
"@types/js-yaml": "4.0.9",
"@types/node": "24.12.0",
"@vitest/browser-webdriverio": "4.1.2",
"@vitest/coverage-v8": "4.1.2",
"@vitest/ui": "4.1.2",
"@vitest/browser-webdriverio": "4.1.0",
"@vitest/coverage-v8": "4.1.0",
"@vitest/ui": "4.1.0",
"chalk": "5.6.2",
"cross-env": "10.1.0",
"dpdm": "4.0.1",
"esbuild": "0.27.4",
"eslint": "10.1.0",
"eslint": "10.0.3",
"eslint-config-preact": "2.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-playwright": "2.10.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"happy-dom": "20.8.9",
"happy-dom": "20.8.4",
"http-server": "14.1.1",
"jiti": "2.6.1",
"js-yaml": "4.1.1",
@@ -74,11 +74,11 @@
"tslib": "2.8.1",
"tsx": "4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "8.57.2",
"typescript-eslint": "8.57.1",
"upath": "2.0.1",
"vite": "8.0.2",
"vite": "8.0.1",
"vite-plugin-dts": "4.5.4",
"vitest": "4.1.2"
"vitest": "4.1.0"
},
"license": "AGPL-3.0-only",
"author": {
@@ -94,14 +94,14 @@
"url": "https://github.com/TriliumNext/Trilium/issues"
},
"homepage": "https://triliumnotes.org",
"packageManager": "pnpm@10.33.0",
"packageManager": "pnpm@10.32.1",
"pnpm": {
"patchedDependencies": {
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",
"@ckeditor/ckeditor5-code-block": "patches/@ckeditor__ckeditor5-code-block.patch"
},
"overrides": {
"@codemirror/language": "6.12.3",
"@codemirror/language": "6.12.2",
"@lezer/highlight": "1.2.3",
"@lezer/common": "1.5.1",
"mermaid": "11.13.0",

View File

@@ -24,22 +24,22 @@
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitest/browser": "4.1.2",
"@vitest/coverage-istanbul": "4.1.2",
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"@vitest/browser": "4.1.0",
"@vitest/coverage-istanbul": "4.1.0",
"ckeditor5": "47.6.1",
"eslint": "10.1.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.4.0",
"stylelint": "17.6.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.2",
"webdriverio": "9.27.0"
"vitest": "4.1.0",
"webdriverio": "9.26.1"
},
"peerDependencies": {
"ckeditor5": "47.6.1"

View File

@@ -25,22 +25,22 @@
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitest/browser": "4.1.2",
"@vitest/coverage-istanbul": "4.1.2",
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"@vitest/browser": "4.1.0",
"@vitest/coverage-istanbul": "4.1.0",
"ckeditor5": "47.6.1",
"eslint": "10.1.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.4.0",
"stylelint": "17.6.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.2",
"webdriverio": "9.27.0"
"vitest": "4.1.0",
"webdriverio": "9.26.1"
},
"peerDependencies": {
"ckeditor5": "47.6.1"

View File

@@ -27,22 +27,22 @@
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitest/browser": "4.1.2",
"@vitest/coverage-istanbul": "4.1.2",
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"@vitest/browser": "4.1.0",
"@vitest/coverage-istanbul": "4.1.0",
"ckeditor5": "47.6.1",
"eslint": "10.1.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.4.0",
"stylelint": "17.6.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.2",
"webdriverio": "9.27.0"
"vitest": "4.1.0",
"webdriverio": "9.26.1"
},
"peerDependencies": {
"ckeditor5": "47.6.1"

View File

@@ -27,22 +27,22 @@
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitest/browser": "4.1.2",
"@vitest/coverage-istanbul": "4.1.2",
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"@vitest/browser": "4.1.0",
"@vitest/coverage-istanbul": "4.1.0",
"ckeditor5": "47.6.1",
"eslint": "10.1.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.4.0",
"stylelint": "17.6.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.2",
"webdriverio": "9.27.0"
"vitest": "4.1.0",
"webdriverio": "9.26.1"
},
"peerDependencies": {
"ckeditor5": "47.6.1"

View File

@@ -22,9 +22,8 @@
flex-direction: column;
padding: var(--ck-spacing-standard);
box-sizing: border-box;
min-width: 400px;
max-width: 60vw;
max-height: 350px;
max-width: 80vw;
max-height: 80vh;
overflow: visible;
user-select: text;
}
@@ -64,8 +63,8 @@
border-radius: var(--ck-border-radius);
background: var(--ck-color-input-background) !important;
transition: border-color 120ms ease;
overflow: auto;
clip-path: none;
overflow: visible !important;
clip-path: none !important;
}
.ck.ck-math-input .ck-mathlive-container:focus-within {
border-color: var(--ck-color-focus-border);
@@ -160,12 +159,16 @@
.ck.ck-math-preview {
width: 100%;
min-height: 40px;
max-height: none !important;
height: auto !important;
padding: var(--ck-spacing-small);
background: transparent !important;
border: none !important;
display: block;
text-align: left;
overflow: auto;
overflow-x: auto !important;
overflow-y: visible !important;
flex-shrink: 0;
}
/* Center equation when in display mode */
@@ -210,7 +213,8 @@
.ck.ck-balloon-panel .ck-balloon-panel__content,
.ck.ck-math-form,
.ck-math-view,
.ck.ck-math-input {
.ck.ck-math-input,
.ck.ck-math-input .ck-mathlive-container {
overflow: visible !important;
clip-path: none !important;
}

View File

@@ -27,22 +27,22 @@
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.1.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitest/browser": "4.1.2",
"@vitest/coverage-istanbul": "4.1.2",
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"@vitest/browser": "4.1.0",
"@vitest/coverage-istanbul": "4.1.0",
"ckeditor5": "47.6.1",
"eslint": "10.1.0",
"eslint": "10.0.3",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.4.0",
"stylelint": "17.6.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "2.0.0",
"vitest": "4.1.2",
"webdriverio": "9.27.0"
"vitest": "4.1.0",
"webdriverio": "9.26.1"
},
"peerDependencies": {
"ckeditor5": "47.6.1"

View File

@@ -52,6 +52,6 @@
"codemirror-lang-elixir": "4.0.1",
"codemirror-lang-hcl": "0.1.0",
"codemirror-lang-mermaid": "0.5.0",
"eslint-linter-browserify": "10.1.0"
"eslint-linter-browserify": "10.0.3"
}
}

View File

@@ -25,17 +25,17 @@
"license": "Apache-2.0",
"dependencies": {
"fuse.js": "7.1.0",
"katex": "0.16.43",
"katex": "0.16.39",
"mermaid": "11.13.0"
},
"devDependencies": {
"@digitak/esrun": "3.2.26",
"@triliumnext/ckeditor5": "workspace:*",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@typescript-eslint/eslint-plugin": "8.57.1",
"@typescript-eslint/parser": "8.57.1",
"dotenv": "17.3.1",
"esbuild": "0.27.4",
"eslint": "10.1.0",
"eslint": "10.0.3",
"highlight.js": "11.11.1",
"typescript": "5.9.3"
}

2536
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff