mirror of
https://github.com/zadam/trilium.git
synced 2026-01-23 07:42:17 +01:00
Compare commits
31 Commits
week-note
...
renovate/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a57a1dfc47 | ||
|
|
17f3ffd00c | ||
|
|
8b86e17ac8 | ||
|
|
d6b6832a1d | ||
|
|
9dfc1cdc4c | ||
|
|
673c39d798 | ||
|
|
227be184ac | ||
|
|
d677f65eeb | ||
|
|
92f86bcca2 | ||
|
|
2015068d9e | ||
|
|
8a280c2f9d | ||
|
|
34fd6f9502 | ||
|
|
02acb36e47 | ||
|
|
280c0e0348 | ||
|
|
8528f0d848 | ||
|
|
96b1efcfdc | ||
|
|
794e03b2cb | ||
|
|
a285c46b97 | ||
|
|
4da6294ef2 | ||
|
|
16ed9a7e8e | ||
|
|
798efbc22f | ||
|
|
60c789b6c7 | ||
|
|
f83d95136d | ||
|
|
f96ed0af26 | ||
|
|
1539664026 | ||
|
|
8aff775d0e | ||
|
|
94248eafe9 | ||
|
|
02335bba3f | ||
|
|
dad9578b83 | ||
|
|
c043788b09 | ||
|
|
e5bc416b46 |
@@ -9,7 +9,7 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.5",
|
||||
"archiver": "7.0.1",
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mind-elixir/node-menu": "5.0.1",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@preact/signals": "2.5.1",
|
||||
"@preact/signals": "2.6.0",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
"@triliumnext/codemirror": "workspace:*",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@zumer/snapdom": "2.0.1",
|
||||
"@zumer/snapdom": "2.0.2",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
"boxicons": "2.1.4",
|
||||
@@ -44,7 +44,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "17.0.0",
|
||||
"i18next": "25.7.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -78,9 +78,9 @@
|
||||
"@types/reveal.js": "5.2.2",
|
||||
"@types/tabulator-tables": "6.3.1",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.3.3",
|
||||
"lightningcss": "1.30.2",
|
||||
"happy-dom": "20.3.4",
|
||||
"lightningcss": "1.31.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
"vite-plugin-static-copy": "3.1.5"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import shortcuts, { isIMEComposing,keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
import shortcuts, { isIMEComposing, keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
|
||||
// Mock utils module
|
||||
vi.mock("./utils.js", () => ({
|
||||
@@ -62,9 +62,10 @@ describe("shortcuts", () => {
|
||||
});
|
||||
|
||||
describe("keyMatches", () => {
|
||||
const createKeyboardEvent = (key: string, code?: string) => ({
|
||||
const createKeyboardEvent = (key: string, code?: string, extraProps: Partial<KeyboardEvent> = {}) => ({
|
||||
key,
|
||||
code: code || `Key${key.toUpperCase()}`
|
||||
code: code || `Key${key.toUpperCase()}`,
|
||||
...extraProps
|
||||
} as KeyboardEvent);
|
||||
|
||||
it("should match regular letter keys using key code", () => {
|
||||
@@ -102,17 +103,23 @@ describe("shortcuts", () => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should match azerty keys", () => {
|
||||
const event = createKeyboardEvent("A", "KeyQ");
|
||||
expect(keyMatches(event, "a")).toBe(true);
|
||||
expect(keyMatches(event, "q")).toBe(false);
|
||||
});
|
||||
|
||||
it("should match letter keys using code when key is a special character (macOS Alt behavior)", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' as the key, but code is still 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent("å", "KeyA");
|
||||
const macOSAltAEvent = createKeyboardEvent("å", "KeyA", { altKey: true });
|
||||
expect(keyMatches(macOSAltAEvent, "a")).toBe(true);
|
||||
|
||||
// Option + H produces '˙'
|
||||
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH");
|
||||
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH", { altKey: true });
|
||||
expect(keyMatches(macOSAltHEvent, "h")).toBe(true);
|
||||
|
||||
// Option + S produces 'ß'
|
||||
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS");
|
||||
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS", { altKey: true });
|
||||
expect(keyMatches(macOSAltSEvent, "s")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -216,6 +223,15 @@ describe("shortcuts", () => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("matches azerty", () => {
|
||||
const event = createKeyboardEvent({
|
||||
key: "a",
|
||||
code: "KeyQ",
|
||||
ctrlKey: true
|
||||
});
|
||||
expect(matchesShortcut(event, "Ctrl+A")).toBe(true);
|
||||
});
|
||||
|
||||
it("should match Alt+letter shortcuts on macOS where key is a special character", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' but code remains 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent({
|
||||
@@ -249,7 +265,7 @@ describe("shortcuts", () => {
|
||||
const handler = vi.fn();
|
||||
shortcuts.bindGlobalShortcut("ctrl+a", handler, "test-namespace");
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||
});
|
||||
|
||||
it("should not bind shortcuts when handler is null", () => {
|
||||
@@ -280,7 +296,7 @@ describe("shortcuts", () => {
|
||||
const handler = vi.fn();
|
||||
shortcuts.bindElShortcut(mockJQueryEl, "ctrl+a", handler, "test-namespace");
|
||||
|
||||
expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||
expect(mockEl.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||
});
|
||||
|
||||
it("should fall back to document when element is empty", () => {
|
||||
@@ -290,7 +306,7 @@ describe("shortcuts", () => {
|
||||
const handler = vi.fn();
|
||||
shortcuts.bindElShortcut(emptyJQuery, "ctrl+a", handler, "test-namespace");
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,7 +317,7 @@ describe("shortcuts", () => {
|
||||
|
||||
shortcuts.removeGlobalShortcut("test-namespace");
|
||||
|
||||
expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||
expect(mockElement.removeEventListener).toHaveBeenCalledWith("keydown", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -110,9 +110,8 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
|
||||
}
|
||||
};
|
||||
|
||||
// Add the event listener in capture phase to intercept events before they reach
|
||||
// child elements like CodeMirror
|
||||
element.addEventListener('keydown', listener, true);
|
||||
// Add the event listener
|
||||
element.addEventListener('keydown', listener);
|
||||
|
||||
// Store the binding for later cleanup
|
||||
const binding: ShortcutBinding = {
|
||||
@@ -139,16 +138,15 @@ export function removeIndividualBinding(binding: ShortcutBinding) {
|
||||
if (activeBindingsInNamespace) {
|
||||
activeBindings.set(key, activeBindingsInNamespace.filter(aBinding => aBinding.handler === binding.handler));
|
||||
}
|
||||
// Remove listener with capture phase to match how it was added
|
||||
binding.element.removeEventListener("keydown", binding.listener, true);
|
||||
binding.element.removeEventListener("keydown", binding.listener);
|
||||
}
|
||||
|
||||
function removeNamespaceBindings(namespace: string) {
|
||||
const bindings = activeBindings.get(namespace);
|
||||
if (bindings) {
|
||||
// Remove all event listeners for this namespace
|
||||
bindings.forEach(binding => {
|
||||
// Remove listener with capture phase to match how it was added
|
||||
binding.element.removeEventListener('keydown', binding.listener, true);
|
||||
binding.element.removeEventListener('keydown', binding.listener);
|
||||
});
|
||||
activeBindings.delete(namespace);
|
||||
}
|
||||
@@ -217,9 +215,12 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
// For letter keys, use the physical key code for consistency
|
||||
// On macOS, Option/Alt key produces special characters, so we must use e.code
|
||||
if (key.length === 1 && key >= 'a' && key <= 'z') {
|
||||
// e.code is like "KeyA", "KeyB", etc.
|
||||
const expectedCode = `Key${key.toUpperCase()}`;
|
||||
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
|
||||
if (e.altKey) {
|
||||
// e.code is like "KeyA", "KeyB", etc.
|
||||
const expectedCode = `Key${key.toUpperCase()}`;
|
||||
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
return e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
|
||||
// For regular keys, check both key and code as fallback
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs";
|
||||
import { MimeType } from "@triliumnext/commons";
|
||||
import { type AutoHighlightResult, ensureMimeTypes, highlight, highlightAuto, type HighlightResult, loadTheme, type Theme,Themes } from "@triliumnext/highlightjs";
|
||||
|
||||
import { copyText, copyTextWithToast } from "./clipboard_ext.js";
|
||||
import { t } from "./i18n.js";
|
||||
import mime_types from "./mime_types.js";
|
||||
import options from "./options.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { copyText, copyTextWithToast } from "./clipboard_ext.js";
|
||||
import { isShare } from "./utils.js";
|
||||
import { MimeType } from "@triliumnext/commons";
|
||||
|
||||
let highlightingLoaded = false;
|
||||
|
||||
@@ -76,13 +77,15 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
|
||||
}
|
||||
|
||||
export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
if (highlightingLoaded) {
|
||||
if (!mimeTypeHint && highlightingLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load theme.
|
||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||
await loadHighlightingTheme(currentThemeName);
|
||||
if (!highlightingLoaded) {
|
||||
const currentThemeName = String(options.get("codeBlockTheme"));
|
||||
await loadHighlightingTheme(currentThemeName);
|
||||
}
|
||||
|
||||
// Load mime types.
|
||||
let mimeTypes: MimeType[];
|
||||
@@ -94,7 +97,7 @@ export async function ensureMimeTypesForHighlighting(mimeTypeHint?: string) {
|
||||
enabled: true,
|
||||
mime: mimeTypeHint.replace("-", "/")
|
||||
}
|
||||
]
|
||||
];
|
||||
} else {
|
||||
mimeTypes = mime_types.getMimeTypes();
|
||||
}
|
||||
@@ -124,9 +127,9 @@ export function isSyntaxHighlightEnabled() {
|
||||
if (!isShare) {
|
||||
const theme = options.get("codeBlockTheme");
|
||||
return !!theme && theme !== "none";
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -217,6 +217,7 @@ function LabelInput({ inputId, ...props }: CellProps & { inputId: string }) {
|
||||
id={inputId}
|
||||
type={LABEL_MAPPINGS[definition.labelType ?? "text"]}
|
||||
value={valueAttr.value}
|
||||
checked={definition.labelType === "boolean" ? valueAttr.value === "true" : undefined}
|
||||
placeholder={t("promoted_attributes.unset-field-placeholder")}
|
||||
data-attribute-id={valueAttr.attributeId}
|
||||
data-attribute-type={valueAttr.type}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
import render from "../../services/render";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import Alert from "../react/Alert";
|
||||
import "./Render.css";
|
||||
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import attributes from "../../services/attributes";
|
||||
import { t } from "../../services/i18n";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import render from "../../services/render";
|
||||
import Alert from "../react/Alert";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -31,6 +34,13 @@ export default function Render({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
refresh();
|
||||
});
|
||||
|
||||
// Refresh on attribute change.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows().some(a => a.type === "relation" && a.name === "renderNote" && attributes.isAffecting(a, note))) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Integration with search.
|
||||
useTriliumEvent("executeWithContentElement", ({ resolve, ntxId: eventNtxId }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DEFAULT_GUTTER_SIZE } from "../../../services/resizer";
|
||||
import utils, { isMobile } from "../../../services/utils";
|
||||
import ActionButton, { ActionButtonProps } from "../../react/ActionButton";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||
import { useNoteBlob, useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
|
||||
import { EditableCode, EditableCodeProps } from "../code/Code";
|
||||
|
||||
export interface SplitEditorProps extends EditableCodeProps {
|
||||
@@ -30,12 +30,22 @@ export interface SplitEditorProps extends EditableCodeProps {
|
||||
* - Can display errors to the user via {@link setError}.
|
||||
* - Horizontal or vertical orientation for the editor/preview split, adjustable via the switch split orientation button floating button.
|
||||
*/
|
||||
export default function SplitEditor({ note, error, splitOptions, previewContent, previewButtons, className, editorBefore, forceOrientation, ...editorProps }: SplitEditorProps) {
|
||||
const splitEditorOrientation = useSplitOrientation(forceOrientation);
|
||||
const [ readOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
export default function SplitEditor(props: SplitEditorProps) {
|
||||
const [ readOnly ] = useNoteLabelBoolean(props.note, "readOnly");
|
||||
|
||||
const editor = (!readOnly &&
|
||||
if (readOnly) {
|
||||
return <ReadOnlyView {...props} />;
|
||||
}
|
||||
|
||||
return <EditorWithSplit {...props} />;
|
||||
|
||||
}
|
||||
|
||||
function EditorWithSplit({ note, error, splitOptions, previewContent, previewButtons, className, editorBefore, forceOrientation, ...editorProps }: SplitEditorProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const splitEditorOrientation = useSplitOrientation(forceOrientation);
|
||||
|
||||
const editor = (
|
||||
<div className="note-detail-split-editor-col">
|
||||
{editorBefore}
|
||||
<div className="note-detail-split-editor">
|
||||
@@ -53,19 +63,14 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
</div>
|
||||
);
|
||||
|
||||
const preview = (
|
||||
<div className="note-detail-split-preview-col">
|
||||
<div className={`note-detail-split-preview ${error ? "on-error" : ""}`}>
|
||||
{previewContent}
|
||||
</div>
|
||||
<div className="btn-group btn-group-sm map-type-switcher content-floating-buttons preview-buttons bottom-right" role="group">
|
||||
{previewButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const preview = <PreviewContainer
|
||||
error={error}
|
||||
previewContent={previewContent}
|
||||
previewButtons={previewButtons}
|
||||
/>;
|
||||
|
||||
useEffect(() => {
|
||||
if (!utils.isDesktop() || !containerRef.current || readOnly) return;
|
||||
if (!utils.isDesktop() || !containerRef.current) return;
|
||||
const elements = Array.from(containerRef.current?.children) as HTMLElement[];
|
||||
const splitInstance = Split(elements, {
|
||||
rtl: glob.isRtl,
|
||||
@@ -76,10 +81,10 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
});
|
||||
|
||||
return () => splitInstance.destroy();
|
||||
}, [ readOnly, splitEditorOrientation ]);
|
||||
}, [ splitEditorOrientation ]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${splitEditorOrientation}`} ${readOnly ? "split-read-only" : ""} ${className ?? ""}`}>
|
||||
<div ref={containerRef} className={`note-detail-split note-detail-printable ${`split-${splitEditorOrientation}`} ${className ?? ""}`}>
|
||||
{splitEditorOrientation === "horizontal"
|
||||
? <>{editor}{preview}</>
|
||||
: <>{preview}{editor}</>}
|
||||
@@ -87,6 +92,43 @@ export default function SplitEditor({ note, error, splitOptions, previewContent,
|
||||
);
|
||||
}
|
||||
|
||||
function ReadOnlyView({ ...props }: SplitEditorProps) {
|
||||
const { note, onContentChanged } = props;
|
||||
const content = useNoteBlob(note);
|
||||
const onContentChangedRef = useRef(onContentChanged);
|
||||
|
||||
useEffect(() => {
|
||||
onContentChangedRef.current = onContentChanged;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onContentChangedRef.current?.(content?.content ?? "");
|
||||
}, [ content ]);
|
||||
|
||||
return (
|
||||
<div className={`note-detail-split note-detail-printable ${props.className} split-read-only`}>
|
||||
<PreviewContainer {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewContainer({ error, previewContent, previewButtons }: {
|
||||
error?: string | null;
|
||||
previewContent: ComponentChildren;
|
||||
previewButtons?: ComponentChildren;
|
||||
}) {
|
||||
return (
|
||||
<div className="note-detail-split-preview-col">
|
||||
<div className={`note-detail-split-preview ${error ? "on-error" : ""}`}>
|
||||
{previewContent}
|
||||
</div>
|
||||
<div className="btn-group btn-group-sm map-type-switcher content-floating-buttons preview-buttons bottom-right" role="group">
|
||||
{previewButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviewButton(props: Omit<ActionButtonProps, "titlePosition">) {
|
||||
return <ActionButton
|
||||
{...props}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import server from "../../../services/server";
|
||||
import svgPanZoom from "svg-pan-zoom";
|
||||
import { RefObject } from "preact";
|
||||
import { useElementSize, useTriliumEvent } from "../../react/hooks";
|
||||
import utils from "../../../services/utils";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import svgPanZoom from "svg-pan-zoom";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import utils from "../../../services/utils";
|
||||
import { useElementSize, useTriliumEvent } from "../../react/hooks";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
|
||||
interface SvgSplitEditorProps extends Omit<SplitEditorProps, "previewContent"> {
|
||||
/**
|
||||
@@ -144,7 +145,7 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg: string | undefined) {
|
||||
@@ -181,7 +182,7 @@ function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg
|
||||
lastPanZoom.current = {
|
||||
pan: zoomInstance.getPan(),
|
||||
zoom: zoomInstance.getZoom()
|
||||
}
|
||||
};
|
||||
zoomRef.current = undefined;
|
||||
zoomInstance.destroy();
|
||||
};
|
||||
|
||||
@@ -191,7 +191,6 @@ function ExperimentalOptions() {
|
||||
values={filteredExperimentalFeatures}
|
||||
keyProperty="id"
|
||||
titleProperty="name"
|
||||
descriptionProperty="description"
|
||||
currentValue={enabledExperimentalFeatures} onChange={setEnabledExperimentalFeatures}
|
||||
/>
|
||||
</OptionsSection>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import FormCheckbox from "../../../react/FormCheckbox";
|
||||
|
||||
interface CheckboxListProps<T> {
|
||||
values: T[];
|
||||
keyProperty: keyof T;
|
||||
titleProperty?: keyof T;
|
||||
disabledProperty?: keyof T;
|
||||
descriptionProperty?: keyof T;
|
||||
currentValue: string[];
|
||||
onChange: (newValues: string[]) => void;
|
||||
columnWidth?: string;
|
||||
}
|
||||
|
||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, descriptionProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||
export default function CheckboxList<T>({ values, keyProperty, titleProperty, disabledProperty, currentValue, onChange, columnWidth }: CheckboxListProps<T>) {
|
||||
function toggleValue(value: string) {
|
||||
if (currentValue.includes(value)) {
|
||||
// Already there, needs removing.
|
||||
@@ -25,17 +22,20 @@ export default function CheckboxList<T>({ values, keyProperty, titleProperty, di
|
||||
return (
|
||||
<ul style={{ listStyleType: "none", marginBottom: 0, columnWidth: columnWidth ?? "400px" }}>
|
||||
{values.map(value => (
|
||||
<li key={String(value[keyProperty])}>
|
||||
<FormCheckbox
|
||||
label={String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||
name={String(value[keyProperty])}
|
||||
currentValue={currentValue.includes(String(value[keyProperty]))}
|
||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||
hint={value && (descriptionProperty ? String(value[descriptionProperty]) : undefined)}
|
||||
onChange={() => toggleValue(String(value[keyProperty]))}
|
||||
/>
|
||||
<li>
|
||||
<label className="tn-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
value={String(value[keyProperty])}
|
||||
checked={currentValue.includes(String(value[keyProperty]))}
|
||||
disabled={!!(disabledProperty && value[disabledProperty])}
|
||||
onChange={e => toggleValue((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{String(value[titleProperty ?? keyProperty] ?? value[keyProperty])}
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,15 @@ export default defineConfig(() => ({
|
||||
print: join(__dirname, "src", "print.tsx")
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "src/[name].js",
|
||||
entryFileNames: (chunk) => {
|
||||
// We enforce a hash in the main index file to avoid caching issues, this only works because we have the HTML entry point.
|
||||
if (chunk.name === "index") {
|
||||
return "src/[name]-[hash].js";
|
||||
}
|
||||
|
||||
// For EJS-rendered pages (e.g. login) we need to have a stable name.
|
||||
return "src/[name].js";
|
||||
},
|
||||
chunkFileNames: "src/[name]-[hash].js",
|
||||
assetFileNames: "src/[name]-[hash].[ext]",
|
||||
manualChunks: {
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"express-http-proxy": "2.1.2",
|
||||
"express-openid-connect": "2.19.4",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"express-session": "1.18.2",
|
||||
"express-session": "1.19.0",
|
||||
"file-uri-to-path": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"helmet": "8.1.0",
|
||||
@@ -99,7 +99,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.7.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-fs-backend": "2.6.1",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "6.0.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"time2fa": "1.4.2",
|
||||
"tmp": "0.2.5",
|
||||
"turnish": "1.7.1",
|
||||
"turnish": "1.8.0",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "7.3.1",
|
||||
"ws": "8.19.0",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"preview": "pnpm build && vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "25.7.4",
|
||||
"i18next": "25.8.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"preact": "10.28.2",
|
||||
"preact-iso": "2.11.1",
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-playwright": "2.5.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"happy-dom": "20.3.3",
|
||||
"happy-dom": "20.3.4",
|
||||
"http-server": "14.1.1",
|
||||
"jiti": "2.6.1",
|
||||
"js-yaml": "4.1.1",
|
||||
@@ -93,7 +93,7 @@
|
||||
"url": "https://github.com/TriliumNext/Trilium/issues"
|
||||
},
|
||||
"homepage": "https://triliumnotes.org",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",
|
||||
@@ -115,7 +115,7 @@
|
||||
"on-headers@<1.1.0": ">=1.1.0",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||
"form-data@>=3.0.0 <3.0.4": ">=3.0.4",
|
||||
"node-abi": "4.25.0"
|
||||
"node-abi": "4.26.0"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"sqlite3"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"ckeditor5-premium-features": "47.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@smithy/middleware-retry": "4.4.24",
|
||||
"@smithy/middleware-retry": "4.4.26",
|
||||
"@types/jquery": "3.5.33"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewPlugin, ViewUpdate, type EditorViewConfig } from "@codemirror/view";
|
||||
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewPlugin, ViewUpdate, type EditorViewConfig, KeyBinding } from "@codemirror/view";
|
||||
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter, codeFolding } from "@codemirror/language";
|
||||
import { Compartment, EditorSelection, EditorState, type Extension } from "@codemirror/state";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
@@ -12,6 +12,17 @@ import { createSearchHighlighter, SearchHighlighter, searchMatchHighlightTheme }
|
||||
|
||||
export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";
|
||||
|
||||
// Custom keymap to prevent Ctrl+Enter from inserting a newline
|
||||
// This allows the parent application to handle the shortcut (e.g., for "Run Active Note")
|
||||
const preventCtrlEnterKeymap: readonly KeyBinding[] = [
|
||||
{
|
||||
key: "Ctrl-Enter",
|
||||
mac: "Cmd-Enter",
|
||||
run: () => true, // Return true to mark event as handled, preventing default newline insertion
|
||||
preventDefault: true
|
||||
}
|
||||
];
|
||||
|
||||
type ContentChangedListener = () => void;
|
||||
|
||||
export interface EditorConfig {
|
||||
@@ -59,6 +70,7 @@ export default class CodeMirror extends EditorView {
|
||||
lineNumbers(),
|
||||
indentUnit.of(" ".repeat(4)),
|
||||
keymap.of([
|
||||
...preventCtrlEnterKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
...smartIndentWithTab
|
||||
|
||||
@@ -22,7 +22,6 @@ export async function ensureMimeTypes(mimeTypes: MimeType[]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
registeredMimeTypes.add(mime);
|
||||
const loader = syntaxDefinitions[mime];
|
||||
if (!loader) {
|
||||
unsupportedMimeTypes.add(mime);
|
||||
@@ -31,6 +30,7 @@ export async function ensureMimeTypes(mimeTypes: MimeType[]) {
|
||||
|
||||
const language = (await loader()).default;
|
||||
hljs.registerLanguage(mime, language);
|
||||
registeredMimeTypes.add(mime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
428
pnpm-lock.yaml
generated
428
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user