Compare commits

..

42 Commits

Author SHA1 Message Date
Elian Doran
b9e257a39d
refactor(client): redundant interface
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run
Deploy website / Build & deploy website (push) Waiting to run
2025-11-29 20:18:43 +02:00
Elian Doran
e7eaa5fd58
fix(mobile): global menu backdrop on tablet view 2025-11-29 19:49:38 +02:00
Elian Doran
c9aa992e73
fix(read-only-bar): displayed when viewing attachments 2025-11-29 19:40:00 +02:00
Elian Doran
f325930f68
chore(read-only-bar): use in-app help 2025-11-29 19:37:38 +02:00
Adorian Doran
1346ffb77e Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-29 18:50:24 +02:00
Adorian Doran
3378746530 style: disable text selection in UI 2025-11-29 18:50:16 +02:00
Elian Doran
ce2d94f04e
Resolve focus issues within split pane (#7877) 2025-11-29 18:34:26 +02:00
Elian Doran
b3c2a1e6c5
fix(insertDateTime): unable to insert date/time via quick editor or s… (#7889) 2025-11-29 18:30:28 +02:00
Elian Doran
dbf63787da
Merge branches 'main' and 'main' of ssh://github.com/TriliumNext/trilium 2025-11-29 18:08:19 +02:00
Elian Doran
88a7ebef69
fix(quick-edit): background broke for colors with no hue 2025-11-29 18:07:24 +02:00
Adorian Doran
a716151dd9 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-29 18:04:53 +02:00
Adorian Doran
7462f1b7a5 style/empty tab: improve layout 2025-11-29 18:04:45 +02:00
Elian Doran
ec76b9dc5c
chore(quick-edit): increase max-width on mobile 2025-11-29 18:01:07 +02:00
Elian Doran
79cd96ade9
style(context_menu): improve submenu separator style 2025-11-29 17:47:26 +02:00
Elian Doran
a5b84406be
style(context_menu): improve submenu bg on mobile 2025-11-29 17:35:37 +02:00
Elian Doran
8c1a04c4b2
fix(mobile): shortcut keyboard + visible 2025-11-29 17:32:32 +02:00
Elian Doran
ee81037173
feat(quick_edit): smooth transition between colors 2025-11-29 17:26:17 +02:00
Elian Doran
453349be26
feat(quick_edit): seamless transition between color changes 2025-11-29 17:19:43 +02:00
Elian Doran
81a9e06b23
feat(quick_edit): basic reactivity to color changes 2025-11-29 17:19:43 +02:00
Elian Doran
7d8af0f252
refactor(client): use var for modal max height 2025-11-29 17:19:43 +02:00
Elian Doran
a68cd7526b
style(mobile): improve quick edit max height 2025-11-29 17:19:43 +02:00
Elian Doran
470ca3b6dc
style(mobile): improve quick edit max width 2025-11-29 17:19:43 +02:00
Elian Doran
e8bae61afc
style(mobile): center modals on tablet view 2025-11-29 17:19:43 +02:00
Elian Doran
c1f663a200
style(mobile): no bottom border radius on modals 2025-11-29 17:19:43 +02:00
Elian Doran
22b2e21df0
Translations update from Hosted Weblate (#7887) 2025-11-29 17:11:15 +02:00
SiriusXT
5f19710791 fix(insertDateTime): unable to insert date/time via quick editor or shortcut 2025-11-29 22:40:49 +08:00
pythaac
d3f3ff4eab
Translated using Weblate (Korean)
Currently translated at 43.4% (66 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2025-11-29 14:02:26 +00:00
noobhjy
5af7425cae
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1637 of 1637 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-11-29 14:02:25 +00:00
Elian Doran
fe10c9f8c8
fix(text): strikethrough icon appears disabled 2025-11-29 15:34:43 +02:00
Elian Doran
cd2a085d00
fix(type_widgets/notemap): bottom part not visible 2025-11-29 15:30:17 +02:00
Elian Doran
3c61626370
fix(launch_bar/calendar): tooltip showing over the calendar dropdown 2025-11-29 15:16:43 +02:00
Elian Doran
351fe5848f
fix(launch_bar/calendar): clicking on the edges would dismiss modal 2025-11-29 13:26:48 +02:00
Elian Doran
ca7bbefbdc
fix(launch_bar/calendar): dropdown remains open when switching years 2025-11-29 13:19:49 +02:00
Elian Doran
7094f71e32
refactor(server): remove now unnecessary attachment without size 2025-11-29 13:08:05 +02:00
Elian Doran
88b5e9db87
fix(server): uploading new attachments doesn't report size 2025-11-29 13:03:08 +02:00
SiriusXT
53a8f6b4c0 Merge branch 'main' into fix/split_pane
Some checks failed
Checks / main (push) Has been cancelled
2025-11-29 11:39:05 +08:00
SiriusXT
9ae1a55896 chore(react/empty): obtain ntxId via React props instead of DOM query 2025-11-29 11:38:45 +08:00
SiriusXT
4d1a91baa6 Merge branch 'main' into fix/split_pane 2025-11-28 19:49:08 +08:00
SiriusXT
1898efa282 chore(e2e): add Playwright tests for split pane 2025-11-28 19:48:37 +08:00
SiriusXT
648ab4d736 fix(left-pane): only focus the note when toggling left pane visibility if necessary 2025-11-28 19:45:19 +08:00
SiriusXT
407cac588a fix(split): only trigger focusOnDetail when necessary 2025-11-28 19:42:04 +08:00
SiriusXT
210dcfb989 fix(empty): open note in the correct split pane 2025-11-28 19:38:52 +08:00
45 changed files with 336 additions and 202 deletions

View File

@ -487,7 +487,7 @@ type EventMappings = {
relationMapResetPanZoom: { ntxId: string | null | undefined };
relationMapResetZoomIn: { ntxId: string | null | undefined };
relationMapResetZoomOut: { ntxId: string | null | undefined };
activeNoteChanged: {};
activeNoteChanged: {ntxId: string | null | undefined};
showAddLinkDialog: AddLinkOpts;
showIncludeDialog: IncludeNoteOpts;
openBulkActionsDialog: {

View File

@ -165,7 +165,7 @@ export default class TabManager extends Component {
const activeNoteContext = this.getActiveContext();
this.updateDocumentTitle(activeNoteContext);
this.triggerEvent("activeNoteChanged", {}); // trigger this even in on popstate event
this.triggerEvent("activeNoteChanged", {ntxId:activeNoteContext?.ntxId}); // trigger this even in on popstate event
}
calculateHash(): string {

View File

@ -32,6 +32,7 @@ import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
const MOBILE_CSS = `
<style>
span.keyboard-shortcut,
kbd {
display: none;
}

View File

@ -1,7 +1,9 @@
import clsx from "clsx";
import {readCssVar} from "../utils/css-var";
import Color, { ColorInstance } from "color";
const registeredClasses = new Set<string>();
const colorsWithHue = new Set<string>();
// Read the color lightness limits defined in the theme as CSS variables
@ -26,19 +28,23 @@ function createClassForColor(colorString: string | null) {
if (!registeredClasses.has(className)) {
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
darkThemeColorMinLightness!);
const hue = getHue(color);
$("head").append(`<style>
.${className}, span.fancytree-active.${className} {
--light-theme-custom-color: ${adjustedColor.lightThemeColor};
--dark-theme-custom-color: ${adjustedColor.darkThemeColor};
--custom-color-hue: ${getHue(color) ?? 'unset'};
--custom-color-hue: ${hue ?? 'unset'};
}
</style>`);
registeredClasses.add(className);
if (hue) {
colorsWithHue.add(className);
}
}
return className;
return clsx(className, colorsWithHue.has(className) && "with-hue");
}
function parseColor(color: string) {

View File

@ -4,6 +4,10 @@
box-sizing: border-box;
}
.dropdown-menu:not(.static).calendar-dropdown-menu {
padding: 0 !important;
}
.calendar-dropdown-widget {
margin: 0 auto;
overflow: hidden;

View File

@ -26,6 +26,7 @@
--bs-body-color: var(--main-text-color) !important;
--bs-body-bg: var(--main-background-color) !important;
--ck-mention-list-max-height: 500px;
--tn-modal-max-height: 90vh;
}
body#trilium-app.motion-disabled *,
@ -212,6 +213,16 @@ input::placeholder,
background-color: var(--modal-backdrop-color) !important;
}
body.mobile .modal .modal-dialog {
left: 50%;
transform: translateX(-50%);
width: 100%;
}
body.mobile .modal .modal-content {
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
}
.component {
contain: size;
}
@ -706,11 +717,6 @@ table.promoted-attributes-in-tooltip th {
z-index: 32767 !important;
}
.tooltip-trigger {
background: transparent;
pointer-events: none;
}
.bs-tooltip-bottom .tooltip-arrow::before {
border-bottom-color: var(--main-border-color) !important;
}
@ -1006,10 +1012,18 @@ div[data-notify="container"] {
font-family: var(--monospace-font-family);
}
svg.ck-icon .note-icon {
svg.ck-icon {
&.ck-icon_inherit-color {
* {
fill: currentColor;
}
}
&.note-icon {
color: var(--main-text-color);
font-size: 20px;
}
}
.ck-content {
--ck-content-font-family: var(--detail-font-family);
@ -1117,10 +1131,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
display: inline-block;
}
.note-detail-empty {
margin: 50px;
}
.modal-header {
padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */
}
@ -1316,7 +1326,7 @@ body.mobile #context-menu-container.mobile-bottom-menu {
inset-inline-end: 0 !important;
bottom: 0 !important;
top: unset !important;
max-height: 90vh;
max-height: var(--tn-modal-max-height);
overflow: auto !important;
user-select: none;
-webkit-user-select: none;
@ -1379,6 +1389,20 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
flex-shrink: 0;
}
.right-dropdown-widget .right-dropdown-button {
position: relative;
}
.tooltip-trigger {
background: transparent;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#launcher-pane.horizontal .right-dropdown-widget {
width: 53px;
}
@ -1562,7 +1586,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
inset-inline-end: 0 !important;
transform: unset !important;
overflow-y: auto;
max-height: calc(90vh - var(--dropdown-bottom));
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
}
#mobile-sidebar-container {

View File

@ -98,6 +98,7 @@
--menu-item-delimiter-color: #ffffff1c;
--menu-item-group-header-color: #ffffff91;
--menu-section-background-color: #fefefe08;
--menu-submenu-mobile-background-color: rgba(0, 0, 0, 0.15);
--modal-backdrop-color: #000;
--modal-shadow-color: rgba(0, 0, 0, .5);
@ -300,7 +301,7 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
border-color: var(--muted-text-color) !important;
}
.tinted-quick-edit-dialog {
.quick-edit-dialog-wrapper.with-hue {
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);

View File

@ -276,7 +276,7 @@
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
}
.tinted-quick-edit-dialog {
.quick-edit-dialog-wrapper.with-hue {
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);

View File

@ -62,6 +62,7 @@
--menu-padding-size: 8px;
--menu-item-icon-vert-offset: -2px;
--menu-submenu-mobile-background-color: rgba(255, 255, 255, 0.15);
--more-accented-background-color: var(--card-background-hover-color);
@ -99,6 +100,14 @@
--tree-item-dark-theme-min-color-lightness: 65;
}
body {
user-select: none;
}
.selectable-text {
user-select: text;
}
body.backdrop-effects-disabled {
/* Backdrop effects are disabled, replace the menu background color with the
* no-backdrop fallback color */
@ -311,6 +320,10 @@ body.mobile #context-menu-cover {
&.global-menu-cover {
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
@media (min-width: 992px) {
bottom: 0;
}
}
}
@ -376,7 +389,8 @@ body.mobile .dropdown-menu {
}
.dropdown-menu {
--menu-background-color: rgba(0, 0, 0, 0.15);
--menu-background-color: --menu-submenu-mobile-background-color;
--bs-dropdown-divider-margin-y: 0.25rem;
border-radius: 0;
max-height: 0;
transition: max-height 100ms ease-in;
@ -384,22 +398,7 @@ body.mobile .dropdown-menu {
&.show {
max-height: 1000px;
}
.dropdown-item {
background: transparent;
}
}
.dropdown-divider {
visibility: visible;
margin: 0;
height: 3px;
border-top: unset;
background-color: rgba(0, 0, 0, 0.2);
&:after {
content: unset;
padding: 0.5rem 0.75rem !important;
}
}

View File

@ -124,12 +124,8 @@
/* The container */
.note-split.empty-note {
--max-content-width: 70%;
}
.note-split.empty-note div.note-detail {
margin: 50px auto;
margin-inline: auto;
}
/* The search results list */

View File

@ -345,7 +345,7 @@ body[dir=ltr] #launcher-container {
*/
.calendar-dropdown-widget {
padding: 12px;
padding: 18px;
color: var(--calendar-color);
user-select: none;
}

View File

@ -1662,7 +1662,7 @@
},
"editable-text": {
"auto-detect-language": "自动检测",
"keeps-crashing": "编辑组件时崩溃。请尝试重启 Trilium。如果问题仍然存在请考虑提交错误报告。"
"keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在请考虑提交错误报告。"
},
"highlighting": {
"title": "代码块",

View File

@ -1647,7 +1647,6 @@
"read-only-info": {
"read-only-note": "Currently viewing a read-only note.",
"auto-read-only-note": "This note is shown in a read-only mode for faster loading.",
"auto-read-only-learn-more": "Learn more",
"edit-note": "Edit note"
},
"note_types": {

View File

@ -105,9 +105,11 @@ export default function NoteDetail() {
});
// Automatically focus the editor.
useTriliumEvent("activeNoteChanged", () => {
// Restore focus to the editor when switching tabs, but only if the note tree is not already focused.
if (!document.activeElement?.classList.contains("fancytree-title")) {
useTriliumEvent("activeNoteChanged", ({ ntxId: eventNtxId }) => {
if (eventNtxId != ntxId) return;
// Restore focus to the editor when switching tabs,
// but only if the note tree and the note panel (e.g., note title or note detail) are not focused.
if (!document.activeElement?.classList.contains("fancytree-title") && !parentComponent.$widget[0].closest(".note-split")?.contains(document.activeElement)) {
parentComponent.triggerCommand("focusOnDetail", { ntxId });
}
});

View File

@ -3,28 +3,27 @@ import { t } from "../services/i18n";
import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks"
import Button from "./react/Button";
import InfoBar from "./react/InfoBar";
import HelpButton from "./react/HelpButton";
export default function ReadOnlyNoteInfoBar(props: {}) {
const { note, noteContext } = useNoteContext();
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
return <InfoBar className="read-only-note-info-bar-widget"
return (
<InfoBar
className="read-only-note-info-bar-widget"
type={(isExplicitReadOnly ? "subtle" : "prominent")}
style={{display: (!isReadOnly) ? "none" : undefined}}>
style={{display: (!isReadOnly) ? "none" : undefined}}
>
<div class="read-only-note-info-bar-widget-content">
{(isExplicitReadOnly) ? (
<div>{t("read-only-info.read-only-note")}</div>
) : (
<div>
{t("read-only-info.auto-read-only-note")}
&nbsp;
<a class="tn-link"
href="https://docs.triliumnotes.org/user-guide/concepts/notes/read-only-notes#automatic-read-only-mode">
{t("read-only-info.auto-read-only-learn-more")}
</a>
{" "}
<HelpButton helpPage="CoFPLs3dRlXc" />
</div>
)}
@ -32,5 +31,5 @@ export default function ReadOnlyNoteInfoBar(props: {}) {
icon="bx-pencil" onClick={() => enableEditing()} />
</div>
</InfoBar>
);
}

View File

@ -110,7 +110,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
private weekNotes: string[] = [];
constructor(title: string = "", icon: string = "") {
super(title, icon, DROPDOWN_TPL);
super(title, icon, DROPDOWN_TPL, "calendar-dropdown-menu");
}
doRender() {
@ -211,8 +211,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
const $target = $(e.target);
// Keep dropdown open when clicking on month select button or year selector area
if ($target.closest('.btn.dropdown-toggle.select-button').length ||
$target.closest('.calendar-year-selector').length) {
if ($target.closest('.btn.dropdown-toggle.select-button').length) {
e.stopPropagation();
return;
}

View File

@ -7,9 +7,9 @@ const TPL = /*html*/`
<div class="dropdown right-dropdown-widget">
<button type="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"
class="bx right-dropdown-button launcher-button"></button>
class="bx right-dropdown-button launcher-button">
<div class="tooltip-trigger"></div>
</button>
<div class="dropdown-menu"></div>
</div>
@ -24,14 +24,16 @@ export default class RightDropdownButtonWidget extends BasicWidget {
protected dropdown!: Dropdown;
protected $tooltip!: JQuery<HTMLElement>;
protected tooltip!: Tooltip;
private dropdownClass?: string;
public $dropdownContent!: JQuery<HTMLElement>;
constructor(title: string, iconClass: string, dropdownTpl: string) {
constructor(title: string, iconClass: string, dropdownTpl: string, dropdownClass?: string) {
super();
this.iconClass = iconClass;
this.title = title;
this.dropdownTpl = dropdownTpl;
this.dropdownClass = dropdownClass;
this.settings = {
titlePlacement: "right"
@ -41,15 +43,17 @@ export default class RightDropdownButtonWidget extends BasicWidget {
doRender() {
this.$widget = $(TPL);
this.$dropdownMenu = this.$widget.find(".dropdown-menu");
if (this.dropdownClass) {
this.$dropdownMenu.addClass(this.dropdownClass);
}
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], {
popperConfig: {
placement: this.settings.titlePlacement,
}
});
this.$widget.attr("title", this.title);
this.tooltip = Tooltip.getOrCreateInstance(this.$widget[0], {
trigger: "hover",
this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
this.tooltip = new Tooltip(this.$tooltip[0], {
placement: handleRightToLeftPlacement(this.settings.titlePlacement),
fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ]
});
@ -57,7 +61,9 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.$widget
.find(".right-dropdown-button")
.addClass(this.iconClass)
.on("click", () => this.tooltip.hide());
.on("click", () => this.tooltip.hide())
.on("mouseenter", () => this.tooltip.show())
.on("mouseleave", () => this.tooltip.hide());
this.$widget.on("show.bs.dropdown", async () => {
await this.dropdownShown();

View File

@ -29,7 +29,11 @@ export default class LeftPaneContainer extends FlexContainer<Component> {
if (visible) {
this.triggerEvent("focusTree", {});
} else {
this.triggerEvent("focusOnDetail", { ntxId: appContext.tabManager.getActiveContext()?.ntxId });
const ntxId = appContext.tabManager.getActiveContext()?.ntxId;
const noteContainer = document.querySelector(`.note-split[data-ntx-id="${ntxId}"]`);
if (!noteContainer?.contains(document.activeElement)) {
this.triggerEvent("focusOnDetail", { ntxId });
}
}
options.save("leftPaneVisible", this.currentLeftPaneVisible.toString());

View File

@ -1,12 +1,8 @@
import FlexContainer from "./flex_container.js";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import type NoteContext from "../../components/note_context.js";
import Component from "../../components/component.js";
import splitService from "../../services/resizer.js";
interface NoteContextEvent {
noteContext: NoteContext;
}
interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean;

View File

@ -5,14 +5,24 @@ body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
body.desktop .modal.popup-editor-dialog .modal-dialog {
max-width: 75vw;
}
.modal.popup-editor-dialog .modal-dialog {
border-bottom-left-radius: var(--bs-modal-border-radius);
border-bottom-right-radius: var(--bs-modal-border-radius);
}
body.desktop .modal.popup-editor-dialog .modal-dialog {
overflow: hidden;
}
body.mobile .modal.popup-editor-dialog .modal-dialog {
max-width: min(var(--preferred-max-content-width), 95vw);
max-height: var(--tn-modal-max-height);
height: 100%;
}
.modal.popup-editor-dialog .modal-content {
transition: background-color 250ms ease-in;
}
.modal.popup-editor-dialog .modal-header .modal-title {
font-size: 1.1em;
}
@ -52,12 +62,16 @@ body.desktop .modal.popup-editor-dialog .modal-dialog {
font-size: 1em;
}
.modal.popup-editor-dialog .classic-toolbar-outer-container.visible {
background-color: transparent;
}
.modal.popup-editor-dialog .classic-toolbar-widget {
position: sticky;
top: 0;
inset-inline-start: 0;
inset-inline-end: 0;
background: var(--modal-background-color);
background: transparent;
z-index: 998;
align-items: flex-start;
}

View File

@ -1,7 +1,7 @@
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import Modal from "../react/Modal";
import "./PopupEditor.css";
import { useNoteContext, useTriliumEvent } from "../react/hooks";
import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
import NoteTitleWidget from "../note_title";
import NoteIcon from "../note_icon";
import NoteContext from "../../components/note_context";
@ -89,17 +89,10 @@ export default function PopupEditor() {
export function DialogWrapper({ children }: { children: ComponentChildren }) {
const { note } = useNoteContext();
const wrapperRef = useRef<HTMLDivElement>(null);
const [ hasTint, setHasTint ] = useState(false);
// Apply the tinted-dialog class only if the custom color CSS class specifies a hue
useEffect(() => {
if (!wrapperRef.current) return;
const customHue = getComputedStyle(wrapperRef.current).getPropertyValue("--custom-color-hue");
setHasTint(!!customHue);
}, [ note ]);
useNoteLabel(note, "color"); // to update color class
return (
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""} ${hasTint ? "tinted-quick-edit-dialog" : ""}`}>
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""}`}>
{children}
</div>
)

View File

@ -31,29 +31,29 @@ export default function AboutDialog() {
<tbody>
<tr>
<th>{t("about.homepage")}</th>
<td><a className="tn-link external" href="https://github.com/TriliumNext/Trilium" style={forceWordBreak}>https://github.com/TriliumNext/Trilium</a></td>
<td className="selectable-text"><a className="tn-link external" href="https://github.com/TriliumNext/Trilium" style={forceWordBreak}>https://github.com/TriliumNext/Trilium</a></td>
</tr>
<tr>
<th>{t("about.app_version")}</th>
<td className="app-version">{appInfo?.appVersion}</td>
<td className="app-version selectable-text">{appInfo?.appVersion}</td>
</tr>
<tr>
<th>{t("about.db_version")}</th>
<td className="db-version">{appInfo?.dbVersion}</td>
<td className="db-version selectable-text">{appInfo?.dbVersion}</td>
</tr>
<tr>
<th>{t("about.sync_version")}</th>
<td className="sync-version">{appInfo?.syncVersion}</td>
<td className="sync-version selectable-text">{appInfo?.syncVersion}</td>
</tr>
<tr>
<th>{t("about.build_date")}</th>
<td className="build-date">
<td className="build-date selectable-text">
{appInfo?.buildDate ? formatDateTime(appInfo.buildDate) : ""}
</td>
</tr>
<tr>
<th>{t("about.build_revision")}</th>
<td>
<td className="selectable-text">
{appInfo?.buildRevision && <a className="tn-link build-revision external" href={`https://github.com/TriliumNext/Trilium/commit/${appInfo.buildRevision}`} target="_blank" style={forceWordBreak}>{appInfo.buildRevision}</a>}
</td>
</tr>
@ -76,8 +76,8 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
openService.openDirectory(directory);
};
return <a className="tn-link" href="#" onClick={onClick} style={style}>{directory}</a>
return <a className="tn-link selectable-text" href="#" onClick={onClick} style={style}>{directory}</a>
} else {
return <span style={style}>{directory}</span>;
return <span className="selectable-text" style={style}>{directory}</span>;
}
}

View File

@ -208,7 +208,7 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
}
</div>)}
</div>
<div className="revision-content use-tn-links" style={{ overflow: "auto", wordBreak: "break-word" }}>
<div className="revision-content use-tn-links selectable-text" style={{ overflow: "auto", wordBreak: "break-word" }}>
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
</div>
</>

View File

@ -810,7 +810,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
setIsReadOnly(readOnly);
});
}
}, [note, noteContext]);
}, [ note, noteContext, noteContext?.viewScope ]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) {

View File

@ -17,24 +17,24 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
return (
<div className="file-properties-widget">
{note && (
<table class="file-table">
<table className="file-table">
<tbody>
<tr>
<th class="text-nowrap">{t("file_properties.note_id")}:</th>
<td class="file-note-id">{note.noteId}</td>
<th class="text-nowrap">{t("file_properties.original_file_name")}:</th>
<td class="file-filename">{originalFileName ?? "?"}</td>
<th className="text-nowrap">{t("file_properties.note_id")}:</th>
<td className="file-note-id selectable-text">{note.noteId}</td>
<th className="text-nowrap">{t("file_properties.original_file_name")}:</th>
<td className="file-filename selectable-text">{originalFileName ?? "?"}</td>
</tr>
<tr>
<th class="text-nowrap">{t("file_properties.file_type")}:</th>
<td class="file-filetype">{note.mime}</td>
<th class="text-nowrap">{t("file_properties.file_size")}:</th>
<td class="file-filesize">{formatSize(blob?.contentLength ?? 0)}</td>
<th className="text-nowrap">{t("file_properties.file_type")}:</th>
<td className="file-filetype selectable-text">{note.mime}</td>
<th className="text-nowrap">{t("file_properties.file_size")}:</th>
<td className="file-filesize selectable-text">{formatSize(blob?.contentLength ?? 0)}</td>
</tr>
<tr>
<td colSpan={4}>
<div class="file-buttons">
<div className="file-buttons">
<Button
icon="bx bx-download"
text={t("file_properties.download")}

View File

@ -23,17 +23,17 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
<div style={{ display: "flex", justifyContent: "space-evenly", margin: "10px" }}>
<span>
<strong>{t("image_properties.original_file_name")}:</strong>{" "}
<span>{originalFileName ?? "?"}</span>
<span className="selectable-text">{originalFileName ?? "?"}</span>
</span>
<span>
<strong>{t("image_properties.file_type")}:</strong>{" "}
<span>{note.mime}</span>
<span className="selectable-text">{note.mime}</span>
</span>
<span>
<strong>{t("image_properties.file_size")}:</strong>{" "}
<span>{formatSize(blob?.contentLength)}</span>
<span className="selectable-text">{formatSize(blob?.contentLength)}</span>
</span>
</div>

View File

@ -37,7 +37,7 @@ export default function InheritedAttributesTab({ note, componentId }: TabContext
return (
<div className="inherited-attributes-widget">
<div className="inherited-attributes-container">
<div className="inherited-attributes-container selectable-text">
{inheritedAttributes?.length ? (
joinElements(inheritedAttributes.map(attribute => (
<InheritedAttribute

View File

@ -39,21 +39,21 @@ export default function NoteInfoTab({ note }: TabContext) {
<>
<div className="note-info-item">
<span>{t("note_info_widget.note_id")}:</span>
<span className="note-info-id">{note.noteId}</span>
<span className="note-info-id selectable-text">{note.noteId}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.created")}:</span>
<span>{formatDateTime(metadata?.dateCreated)}</span>
<span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.modified")}:</span>
<span>{formatDateTime(metadata?.dateModified)}</span>
<span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
</div>
<div className="note-info-item">
<span>{t("note_info_widget.type")}:</span>
<span>
<span className="note-info-type">{note.type}</span>{' '}
{note.mime && <span className="note-info-mime">({note.mime})</span>}
{note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
</span>
</div>
<div className="note-info-item">
@ -77,7 +77,7 @@ export default function NoteInfoTab({ note }: TabContext) {
/>
)}
<span className="note-sizes-wrapper">
<span className="note-sizes-wrapper selectable-text">
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&

View File

@ -1,5 +1,6 @@
.note-detail-doc-content {
padding: 15px;
user-select: text;
}
.note-detail-doc-content pre {

View File

@ -1,3 +1,21 @@
.note-detail-empty {
container-type: size;
padding-top: 50px;
min-width: 350px;
}
.note-detail-empty > * {
margin-inline: auto;
max-width: 850px;
padding-inline: 50px;
}
@container (max-width: 600px) {
.note-detail-empty > * {
padding-inline: 20px;
}
}
.workspace-notes {
display: flex;
flex-direction: row;
@ -14,7 +32,8 @@
.workspace-notes .workspace-note:hover {
cursor: pointer;
border: 1px solid var(--main-border-color);
background-color: var(--icon-button-hover-background);
border-radius: 8px;
}
.note-detail-empty-results .aa-dropdown-menu {
@ -24,6 +43,11 @@
border-top: 0;
}
.empty-tab-search label {
margin-bottom: 8px;
color: var(--muted-text-color);
}
.empty-tab-search .note-autocomplete-input {
border-bottom-left-radius: 0;
}

View File

@ -10,16 +10,16 @@ import FNote from "../../entities/fnote";
import search from "../../services/search";
import { TypeWidgetProps } from "./type_widget";
export default function Empty({ }: TypeWidgetProps) {
export default function Empty({ ntxId }: TypeWidgetProps) {
return (
<>
<WorkspaceSwitcher />
<NoteSearch />
<NoteSearch ntxId={ntxId ?? null} />
</>
)
}
function NoteSearch() {
function NoteSearch({ ntxId }: { ntxId: string | null }) {
const resultsContainerRef = useRef<HTMLDivElement>(null);
const autocompleteRef = useRef<HTMLInputElement>(null);
@ -45,10 +45,9 @@ function NoteSearch() {
if (!suggestion?.notePath) {
return false;
}
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(suggestion.notePath);
const activeNoteContext = appContext.tabManager.getNoteContextById(ntxId) ?? appContext.tabManager.getActiveContext();
if (activeNoteContext) {
activeNoteContext.setNote(suggestion.notePath);
}
}}
/>

View File

@ -0,0 +1,10 @@
.note-detail-note-map {
&>div {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
}

View File

@ -1,6 +1,7 @@
import { TypeWidgetProps } from "./type_widget";
import NoteMapEl from "../note_map/NoteMap";
import { useRef } from "preact/hooks";
import "./NoteMap.css";
export default function NoteMap({ note }: TypeWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);

View File

@ -104,7 +104,7 @@ export function BackupList({ backups }: { backups: DatabaseBackup[] }) {
backups.map(({ mtime, filePath }) => (
<tr>
<td>{mtime ? formatDateTime(mtime) : "-"}</td>
<td>{filePath}</td>
<td className="selectable-text">{filePath}</td>
</tr>
))
) : (

View File

@ -226,7 +226,7 @@ function CodeBlockPreview({ theme, wordWrap }: { theme: string, wordWrap: boolea
return (
<div className="note-detail-readonly-text-content ck-content code-sample-wrapper">
<pre className="hljs" style={{ marginBottom: 0 }}>
<pre className="hljs selectable-text" style={{ marginBottom: 0 }}>
<code className="code-sample" style={codeStyle} dangerouslySetInnerHTML={getHtml(code)} />
</pre>
</div>

View File

@ -98,6 +98,14 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
editorApi: editorApiRef.current,
});
},
insertDateTimeToTextCommand() {
if (!editorApiRef.current) return;
const date = new Date();
const customDateTimeFormat = options.get("customDateTimeFormat");
const dateString = utils.formatDateTime(date, customDateTimeFormat);
addTextToEditor(dateString);
},
// Include note functionality note
addIncludeNoteToTextCommand() {
if (!editorApiRef.current) return;
@ -197,14 +205,6 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
});
}
useTriliumEvent("insertDateTimeToText", ({ ntxId: eventNtxId }) => {
if (eventNtxId !== ntxId) return;
const date = new Date();
const customDateTimeFormat = options.get("customDateTimeFormat");
const dateString = utils.formatDateTime(date, customDateTimeFormat);
addTextToEditor(dateString);
});
useTriliumEvent("addTextToActiveEditor", ({ text }) => {
if (!noteContext?.isActive()) return;
addTextToEditor(text);

View File

@ -55,7 +55,7 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
<>
<RawHtmlBlock
containerRef={contentRef}
className={clsx("note-detail-readonly-text-content ck-content use-tn-links", codeBlockWordWrap && "word-wrap")}
className={clsx("note-detail-readonly-text-content ck-content use-tn-links selectable-text", codeBlockWordWrap && "word-wrap")}
tabindex={100}
dir={isRtl ? "rtl" : "ltr"}
html={blob?.content}

View File

@ -0,0 +1,72 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
const TEXT_NOTE_TITLE = "Text notes";
const CODE_NOTE_TITLE = "Code notes";
test("Open the note in the correct split pane", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
// Open the first split.
await app.goToNoteInNewTab(TEXT_NOTE_TITLE);
const split1 = app.currentNoteSplit;
// Create a new split.
const splitButton = split1.locator("button.bx-dock-right");
await expect(splitButton).toBeVisible();
await splitButton.click();
// Search for "Code notes" in the empty area of the second split.
const split2 = app.currentNoteSplit.nth(1);;
await expect(split2).toBeVisible();
const autocomplete = split2.locator(".note-autocomplete");
await autocomplete.fill(CODE_NOTE_TITLE);
const resultsSelector = split2.locator(".note-detail-empty-results");
await expect(resultsSelector).toContainText(CODE_NOTE_TITLE);
//Focus on the first split.
const noteContent = split1.locator(".note-detail-editable-text-editor");
await expect(noteContent.locator("p")).toBeVisible();
await noteContent.focus();
// Click the search result in the second split.
await resultsSelector.locator(".aa-suggestion", { hasText: CODE_NOTE_TITLE })
.nth(1).click();
await expect(split2).toContainText(CODE_NOTE_TITLE);
});
test("Can directly focus the autocomplete input within the split", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
// Open the first split.
await app.goToNoteInNewTab(TEXT_NOTE_TITLE);
const split1 = app.currentNoteSplit;
// Create a new split.
const splitButton = split1.locator("button.bx-dock-right");
await expect(splitButton).toBeVisible();
await splitButton.click();
// Search for "Code notes" in the empty area of the second split.
const split2 = app.currentNoteSplit.nth(1);;
await expect(split2).toBeVisible();
// Focus the first split.
const noteContent = split1.locator(".note-detail-editable-text-editor");
await expect(noteContent.locator("p")).toBeVisible();
await noteContent.focus();
await noteContent.click();
// click the autocomplete input box of the second split
const autocomplete = split2.locator(".note-autocomplete");
await autocomplete.focus();
await autocomplete.click();
await page.waitForTimeout(100);
await expect(autocomplete).toBeFocused();
});

View File

@ -13,10 +13,6 @@ import BBlob from "./entities/bblob.js";
import BRecentNote from "./entities/brecent_note.js";
import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
interface AttachmentOpts {
includeContentLength?: boolean;
}
/**
* Becca is a backend cache of all notes, branches, and attributes.
* There's a similar frontend cache Froca, and share cache Shaca.
@ -167,21 +163,18 @@ export default class Becca {
return revision;
}
getAttachment(attachmentId: string, opts: AttachmentOpts = {}): BAttachment | null {
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
getAttachment(attachmentId: string): BAttachment | null {
const query = /*sql*/`\
SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE attachmentId = ? AND isDeleted = 0`
: /*sql*/`SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`;
WHERE attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [attachmentId]).map((row) => new BAttachment(row))[0];
}
getAttachmentOrThrow(attachmentId: string, opts: AttachmentOpts = {}): BAttachment {
const attachment = this.getAttachment(attachmentId, opts);
getAttachmentOrThrow(attachmentId: string): BAttachment {
const attachment = this.getAttachment(attachmentId);
if (!attachment) {
throw new NotFoundError(`Attachment '${attachmentId}' has not been found.`);
}

View File

@ -61,10 +61,6 @@ interface ContentOpts {
forceFrontendReload?: boolean;
}
interface AttachmentOpts {
includeContentLength?: boolean;
}
interface Relationship {
parentNoteId: string;
childNoteId: string;
@ -1102,31 +1098,23 @@ class BNote extends AbstractBeccaEntity<BNote> {
return sql.getRows<RevisionRow>("SELECT * FROM revisions WHERE noteId = ? ORDER BY revisions.utcDateCreated ASC", [this.noteId]).map((row) => new BRevision(row));
}
getAttachments(opts: AttachmentOpts = {}) {
opts.includeContentLength = !!opts.includeContentLength;
// from testing, it looks like calculating length does not make a difference in performance even on large-ish DB
// given that we're always fetching attachments only for a specific note, we might just do it always
const query = opts.includeContentLength
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
getAttachments() {
const query = /*sql*/`\
SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND isDeleted = 0
ORDER BY position`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND isDeleted = 0 ORDER BY position`;
ORDER BY position`;
return sql.getRows<AttachmentRow>(query, [this.noteId]).map((row) => new BAttachment(row));
}
getAttachmentById(attachmentId: string, opts: AttachmentOpts = {}) {
opts.includeContentLength = !!opts.includeContentLength;
const query = opts.includeContentLength
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength
getAttachmentById(attachmentId: string) {
const query = /*sql*/`\
SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [this.noteId, attachmentId]).map((row) => new BAttachment(row))[0];
}

View File

@ -92,7 +92,7 @@ function getAndCheckNote(noteId: string) {
}
function getAndCheckAttachment(attachmentId: string) {
const attachment = becca.getAttachment(attachmentId, { includeContentLength: true });
const attachment = becca.getAttachment(attachmentId);
if (attachment) {
return attachment;

View File

@ -185,7 +185,7 @@ function register(router: Router) {
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments({ includeContentLength: true });
const attachments = note.getAttachments();
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
});

View File

@ -14,13 +14,13 @@ function getAttachmentBlob(req: Request) {
function getAttachments(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId);
return note.getAttachments({ includeContentLength: true });
return note.getAttachments();
}
function getAttachment(req: Request) {
const { attachmentId } = req.params;
return becca.getAttachmentOrThrow(attachmentId, { includeContentLength: true });
return becca.getAttachmentOrThrow(attachmentId);
}
function getAllAttachments(req: Request) {
@ -28,7 +28,7 @@ function getAllAttachments(req: Request) {
// one particular attachment is requested, but return all note's attachments
const attachment = becca.getAttachmentOrThrow(attachmentId);
return attachment.getNote()?.getAttachments({ includeContentLength: true }) || [];
return attachment.getNote()?.getAttachments() || [];
}
function saveAttachment(req: Request) {

View File

@ -764,7 +764,7 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment
note.setContent(newContent, { forceFrontendReload });
if (attachments?.length > 0) {
const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
const existingAttachmentsByTitle = toMap(note.getAttachments(), "title");
for (const { attachmentId, role, mime, title, position, content } of attachments) {
const existingAttachment = existingAttachmentsByTitle.get(title);

View File

@ -85,5 +85,8 @@
"title_arm64": "ARM 기반 리눅스",
"description_x64": "대부분의 리눅스 배포판에서 x86_64 아키텍처와 호환됩니다.",
"description_arm64": "ARM 기반 리눅스 배포판에서 aarch64 아키텍처와 호환됩니다."
},
"note_types": {
"text_title": "텍스트 노트"
}
}