mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 21:36:33 +02:00
Compare commits
14 Commits
autocomple
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f729e5b379 | ||
|
|
6e8c4813d9 | ||
|
|
029e8469b4 | ||
|
|
715b87fb3d | ||
|
|
3d860ba2c4 | ||
|
|
8b479ac4ba | ||
|
|
dcfcc7a334 | ||
|
|
daa0109f7f | ||
|
|
f46e20aa25 | ||
|
|
e8045424b2 | ||
|
|
7e6b628f86 | ||
|
|
fc247fe790 | ||
|
|
7a2fa2e829 | ||
|
|
3e3f455b24 |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "autocomplete.js/index_jquery.js";
|
||||
|
||||
import type ElectronRemote from "@electron/remote";
|
||||
import type Electron from "electron";
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface MenuCommandItem<T> {
|
||||
title: string;
|
||||
command?: T;
|
||||
type?: string;
|
||||
mime?: string;
|
||||
/**
|
||||
* The icon to display in the menu item.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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: []
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "输入你的美人鱼图的内容,或者使用下面的示例图之一。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Σχετικά με το Trilium Notes",
|
||||
"title": "Πληροφορίες για το Trilium Notes",
|
||||
"homepage": "Αρχική Σελίδα:",
|
||||
"app_version": "Έκδοση εφαρμογής:",
|
||||
"db_version": "Έκδοση βάσης δεδομένων:",
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "クイック編集",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "魚骨圖"
|
||||
}
|
||||
}
|
||||
|
||||
28
apps/client/src/types.d.ts
vendored
28
apps/client/src/types.d.ts
vendored
@@ -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);
|
||||
|
||||
@@ -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>>) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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} />;
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
21
apps/client/src/widgets/type_widgets/text/lexical/index.css
Normal file
21
apps/client/src/widgets/type_widgets/text/lexical/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
120
apps/client/src/widgets/type_widgets/text/lexical/index.tsx
Normal file
120
apps/client/src/widgets/type_widgets/text/lexical/index.tsx
Normal 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;
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
2
docs/README-pl.md
vendored
@@ -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
17
flake.lock
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
20
package.json
20
package.json
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
2536
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user