Merge 5b310f3e46e18071bef89ae7cf5cba17c7cd6ae8 into b8585594cd138783588a7ac4d6a3260de779427d

This commit is contained in:
Elian Doran 2025-12-04 15:08:14 +02:00 committed by GitHub
commit 341b85a8cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 230 additions and 211 deletions

View File

@ -30,7 +30,6 @@ import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import SpacerWidget from "../widgets/spacer.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
@ -43,8 +42,8 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import utils from "../services/utils.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
export default class DesktopLayout {
@ -125,7 +124,7 @@ export default class DesktopLayout {
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(new SpacerWidget(0, 1))
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
.child(<MovePaneButton direction="left" />)
.child(<MovePaneButton direction="right" />)
.child(<ClosePaneButton />)

View File

@ -150,7 +150,7 @@ export function isMac() {
export const hasTouchBar = (isMac() && isElectron());
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
export function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
}

View File

@ -1,5 +1,4 @@
import FlexContainer from "./containers/flex_container.js";
import OpenNoteButtonWidget from "./buttons/open_note_button_widget.js";
import BookmarkFolderWidget from "./buttons/bookmark_folder.js";
import froca from "../services/froca.js";
import utils from "../services/utils.js";
@ -23,10 +22,6 @@ export default class BookmarkButtons extends FlexContainer<Component> {
}
async refresh(): Promise<void> {
this.$widget.empty();
this.children = [];
this.noteIds = [];
const bookmarkParentNote = await froca.getNote("_lbBookmarks");
if (!bookmarkParentNote) {
@ -37,16 +32,7 @@ export default class BookmarkButtons extends FlexContainer<Component> {
this.noteIds.push(note.noteId);
let buttonWidget: OpenNoteButtonWidget | BookmarkFolderWidget = note.isLabelTruthy("bookmarkFolder")
? new BookmarkFolderWidget(note)
: new OpenNoteButtonWidget(note).class("launcher-button");
if (this.settings.titlePlacement) {
if (!("settings" in buttonWidget)) {
(buttonWidget as any).settings = {};
}
(buttonWidget as any).settings.titlePlacement = this.settings.titlePlacement;
}
? new BookmarkFolderWidget(note);
this.child(buttonWidget);

View File

@ -1,88 +0,0 @@
import RightDropdownButtonWidget from "./right_dropdown_button.js";
import linkService from "../../services/link.js";
import utils from "../../services/utils.js";
import type FNote from "../../entities/fnote.js";
const DROPDOWN_TPL = `
<div class="bookmark-folder-widget">
<style>
.bookmark-folder-widget {
min-width: 400px;
max-height: 500px;
padding: 7px 15px 0 15px;
font-size: 1.2rem;
overflow: auto;
}
.bookmark-folder-widget ul {
padding: 0;
list-style-type: none;
}
.bookmark-folder-widget .note-link {
display: block;
padding: 5px 10px 5px 5px;
}
.bookmark-folder-widget .note-link:hover {
background-color: var(--accented-background-color);
text-decoration: none;
}
.dropdown-menu .bookmark-folder-widget a:hover {
text-decoration: none;
background: transparent !important;
}
.bookmark-folder-widget li .note-link {
padding-inline-start: 35px;
}
</style>
<div class="parent-note"></div>
<ul class="children-notes"></ul>
</div>`;
interface LinkOptions {
showTooltip: boolean;
showNoteIcon: boolean;
}
export default class BookmarkFolderWidget extends RightDropdownButtonWidget {
private note: FNote;
private $parentNote!: JQuery<HTMLElement>;
private $childrenNotes!: JQuery<HTMLElement>;
declare $dropdownContent: JQuery<HTMLElement>;
constructor(note: FNote) {
super(utils.escapeHtml(note.title), note.getIcon(), DROPDOWN_TPL);
this.note = note;
}
doRender(): void {
super.doRender();
this.$parentNote = this.$dropdownContent.find(".parent-note");
this.$childrenNotes = this.$dropdownContent.find(".children-notes");
}
async dropdownShown(): Promise<void> {
this.$parentNote.empty();
this.$childrenNotes.empty();
const linkOptions: LinkOptions = {
showTooltip: false,
showNoteIcon: true
};
this.$parentNote.append((await linkService.createLink(this.note.noteId, linkOptions)).addClass("note-link"));
for (const childNote of await this.note.getChildNotes()) {
this.$childrenNotes.append($("<li>").append((await linkService.createLink(childNote.noteId, linkOptions)).addClass("note-link")));
}
}
refreshIcon(): void {}
}

View File

@ -1,49 +0,0 @@
import OnClickButtonWidget from "./onclick_button.js";
import linkContextMenuService from "../../menus/link_context_menu.js";
import utils from "../../services/utils.js";
import appContext from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
export default class OpenNoteButtonWidget extends OnClickButtonWidget {
private noteToOpen: FNote;
constructor(noteToOpen: FNote) {
super();
this.noteToOpen = noteToOpen;
this.title(() => utils.escapeHtml(this.noteToOpen.title))
.icon(() => this.noteToOpen.getIcon())
.onClick((widget, evt) => this.launch(evt))
.onAuxClick((widget, evt) => this.launch(evt))
.onContextMenu((evt) => {
if (evt) {
linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt);
}
});
}
async launch(evt: JQuery.ClickEvent | JQuery.TriggeredEvent | JQuery.ContextMenuEvent) {
if (evt.which === 3) {
return;
}
const hoistedNoteId = this.getHoistedNoteId();
const ctrlKey = utils.isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = evt.shiftKey ? true : false;
await appContext.tabManager.openInNewTab(this.noteToOpen.noteId, hoistedNoteId, activate);
} else {
await appContext.tabManager.openInSameTab(this.noteToOpen.noteId);
}
}
getHoistedNoteId() {
return this.noteToOpen.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
}
initialRenderCompleteEvent() {
// we trigger refresh above
}
}

View File

@ -1,9 +1,7 @@
import CalendarWidget from "../buttons/calendar.js";
import SpacerWidget from "../spacer.js";
import BookmarkButtons from "../bookmark_buttons.js";
import ProtectedSessionStatusWidget from "../buttons/protected_session_status.js";
import SyncStatusWidget from "../sync_status.js";
import BasicWidget from "../basic_widget.js";
import BasicWidget, { wrapReactWidgets } from "../basic_widget.js";
import NoteLauncher from "../buttons/launcher/note_launcher.js";
import ScriptLauncher from "../buttons/launcher/script_launcher.js";
import CommandButtonWidget from "../buttons/command_button.js";
@ -14,6 +12,8 @@ import QuickSearchLauncherWidget from "../quick_search_launcher.js";
import type FNote from "../../entities/fnote.js";
import type { CommandNames } from "../../components/app_context.js";
import AiChatButton from "../buttons/ai_chat_button.js";
import BookmarkButtons from "../launch_bar/BookmarkButtons.jsx";
import SpacerWidget from "../launch_bar/SpacerWidget.jsx";
interface InnerWidget extends BasicWidget {
settings?: {
@ -64,7 +64,7 @@ export default class LauncherWidget extends BasicWidget {
} else if (launcherType === "customWidget") {
widget = await this.initCustomWidget(note);
} else if (launcherType === "builtinWidget") {
widget = this.initBuiltinWidget(note);
widget = wrapReactWidgets<BasicWidget>([ this.initBuiltinWidget(note) ])[0];
} else {
throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title '${note.title}'`);
}
@ -109,9 +109,9 @@ export default class LauncherWidget extends BasicWidget {
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return new SpacerWidget(baseSize, growthFactor);
return <SpacerWidget baseSize={baseSize} growthFactor={growthFactor} />;
case "bookmarks":
return new BookmarkButtons(this.isHorizontalLayout);
return <BookmarkButtons isHorizontalLayout={this.isHorizontalLayout} />
case "protectedSession":
return new ProtectedSessionStatusWidget();
case "syncStatus":

View File

@ -0,0 +1,31 @@
.bookmark-folder-widget {
min-width: 400px;
max-height: 500px;
padding: 7px 15px 0 15px;
font-size: 1.2rem;
overflow: auto;
}
.bookmark-folder-widget ul {
padding: 0;
list-style-type: none;
}
.bookmark-folder-widget .note-link {
display: block;
padding: 5px 10px 5px 5px;
}
.bookmark-folder-widget .note-link:hover {
background-color: var(--accented-background-color);
text-decoration: none;
}
.dropdown-menu .bookmark-folder-widget a:hover:not(.disabled) {
text-decoration: none;
background-color: transparent !important;
}
.bookmark-folder-widget li .note-link {
padding-inline-start: 35px;
}

View File

@ -0,0 +1,98 @@
import { useMemo } from "preact/hooks";
import { LaunchBarActionButton, LaunchBarDropdownButton, type LaunchBarWidgetProps } from "./launch_bar_widgets";
import { CSSProperties } from "preact";
import type FNote from "../../entities/fnote";
import { useChildNotes, useNoteLabel, useNoteLabelBoolean, useNoteProperty } from "../react/hooks";
import appContext from "../../components/app_context";
import { escapeHtml, isCtrlKey } from "../../services/utils";
import link_context_menu from "../../menus/link_context_menu";
import "./BookmarkButtons.css";
import NoteLink from "../react/NoteLink";
const PARENT_NOTE_ID = "_lbBookmarks";
export default function BookmarkButtons({ isHorizontalLayout }: LaunchBarWidgetProps) {
const style = useMemo<CSSProperties>(() => ({
display: "flex",
flexDirection: isHorizontalLayout ? "row" : "column",
contain: "none"
}), [ isHorizontalLayout ]);
const childNotes = useChildNotes(PARENT_NOTE_ID);
return (
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark note={childNote} />)}
</div>
)
}
function SingleBookmark({ note }: { note: FNote }) {
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
return bookmarkFolder
? <BookmarkFolder note={note} />
: <OpenNoteButtonWidget note={note} />
}
function OpenNoteButtonWidget({ note }: { note: FNote }) {
const [ iconClass ] = useNoteLabel(note, "iconClass");
const title = useNoteProperty(note, "title");
async function launch(evt: MouseEvent) {
if (evt.which === 3) {
return;
}
const hoistedNoteId = getHoistedNoteId(note);
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = evt.shiftKey ? true : false;
await appContext.tabManager.openInNewTab(note.noteId, hoistedNoteId, activate);
} else {
await appContext.tabManager.openInSameTab(note.noteId);
}
}
return title && iconClass && (
<LaunchBarActionButton
icon={iconClass}
text={escapeHtml(title)}
onClick={launch}
onAuxClick={launch}
onContextMenu={evt => {
evt.preventDefault();
link_context_menu.openContextMenu(note.noteId, evt);
}}
/>
)
}
function BookmarkFolder({ note }: { note: FNote }) {
const [ iconClass ] = useNoteLabel(note, "iconClass");
const title = useNoteProperty(note, "title");
const childNotes = useChildNotes(note.noteId);
return title && iconClass && (
<LaunchBarDropdownButton
icon={iconClass}
title={escapeHtml(title)}
>
<div className="bookmark-folder-widget">
<div className="parent-note">
<NoteLink notePath={note.noteId} noPreview showNoteIcon containerClassName="note-link" noTnLink />
</div>
<ul className="children-notes">
{childNotes.map(childNote => (
<li>
<NoteLink notePath={childNote.noteId} noPreview showNoteIcon containerClassName="note-link" noTnLink />
</li>
))}
</ul>
</div>
</LaunchBarDropdownButton>
)
}
function getHoistedNoteId(noteToOpen: FNote) {
return noteToOpen.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
}

View File

@ -0,0 +1,3 @@
export default function RightDropdownButton() {
return <p>Button goes here.</p>;
}

View File

@ -0,0 +1,35 @@
import appContext, { CommandNames } from "../../components/app_context";
import contextMenu from "../../menus/context_menu";
import { t } from "../../services/i18n";
import { isMobile } from "../../services/utils";
interface SpacerWidgetProps {
baseSize?: number;
growthFactor?: number;
}
export default function SpacerWidget({ baseSize, growthFactor }: SpacerWidgetProps) {
return (
<div
className="spacer"
style={{
flexBasis: baseSize ?? 0,
flexGrow: growthFactor ?? 1000,
flexShrink: 1000
}}
onContextMenu={(e) => {
e.preventDefault();
contextMenu.show<CommandNames>({
x: e.pageX,
y: e.pageY,
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (isMobile() ? "bx-mobile" : "bx-sidebar") }],
selectMenuItemHandler: ({ command }) => {
if (command) {
appContext.triggerCommand(command);
}
}
});
}}
/>
)
}

View File

@ -0,0 +1,30 @@
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
import Dropdown, { DropdownProps } from "../react/Dropdown";
import Icon from "../react/Icon";
export interface LaunchBarWidgetProps {
isHorizontalLayout: boolean;
}
export function LaunchBarActionButton(props: Omit<ActionButtonProps, "className" | "noIconActionClass" | "titlePosition">) {
return (
<ActionButton
className="button-widget launcher-button"
noIconActionClass
titlePosition="right"
{...props}
/>
)
}
export function LaunchBarDropdownButton({ children, icon, ...props }: Pick<DropdownProps, "title" | "children"> & { icon: string }) {
return (
<Dropdown
className="right-dropdown-widget"
buttonClassName="right-dropdown-button launcher-button"
hideToggleArrow
text={<Icon icon={icon} />}
{...props}
>{children}</Dropdown>
)
}

View File

@ -2,13 +2,13 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import { useStaticTooltip } from "./hooks";
import keyboard_actions from "../../services/keyboard_actions";
import { HTMLAttributes } from "preact";
export interface ActionButtonProps {
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu"> {
text: string;
titlePosition?: "top" | "right" | "bottom" | "left";
icon: string;
className?: string;
onClick?: (e: MouseEvent) => void;
triggerCommand?: CommandNames;
noIconActionClass?: boolean;
frame?: boolean;
@ -16,7 +16,7 @@ export interface ActionButtonProps {
disabled?: boolean;
}
export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled }: ActionButtonProps) {
export default function ActionButton({ text, icon, className, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled, ...restProps }: ActionButtonProps) {
const buttonRef = useRef<HTMLButtonElement>(null);
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
@ -35,8 +35,8 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
return <button
ref={buttonRef}
class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon} ${frame ? "btn btn-primary" : ""} ${disabled ? "disabled" : ""} ${active ? "active" : ""}`}
onClick={onClick}
data-trigger-command={triggerCommand}
disabled={disabled}
{...restProps}
/>;
}

View File

@ -4,6 +4,7 @@ import { useImperativeSearchHighlighlighting, useTriliumEvent } from "./hooks";
interface NoteLinkOpts {
className?: string;
containerClassName?: string;
notePath: string | string[];
showNotePath?: boolean;
showNoteIcon?: boolean;
@ -17,7 +18,7 @@ interface NoteLinkOpts {
noContextMenu?: boolean;
}
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope, noContextMenu }: NoteLinkOpts) {
export default function NoteLink({ className, containerClassName, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope, noContextMenu }: NoteLinkOpts) {
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
const noteId = stringifiedNotePath.split("/").at(-1);
const ref = useRef<HTMLSpanElement>(null);
@ -71,6 +72,6 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
$linkEl?.addClass(className);
}
return <span ref={ref} />
return <span className={containerClassName} ref={ref} />
}

View File

@ -23,6 +23,7 @@ import toast, { ToastOptions } from "../../services/toast";
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
import server from "../../services/server";
import { removeIndividualBinding } from "../../services/shortcuts";
import froca from "../../services/froca";
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
const parentComponent = useContext(ParentComponent);
@ -836,3 +837,15 @@ async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
return true;
}
export function useChildNotes(parentNoteId: string) {
const [ childNotes, setChildNotes ] = useState<FNote[]>([]);
async function refreshChildNotes() {
const parentNote = await froca.getNote(parentNoteId);
const childNotes = await parentNote?.getChildNotes();
setChildNotes(childNotes ?? []);
}
useEffect(() => { refreshChildNotes() }, [ parentNoteId ]);
return childNotes;
}

View File

@ -1,43 +0,0 @@
import { t } from "../services/i18n.js";
import BasicWidget from "./basic_widget.js";
import contextMenu from "../menus/context_menu.js";
import appContext, { type CommandNames } from "../components/app_context.js";
import utils from "../services/utils.js";
const TPL = /*html*/`<div class="spacer"></div>`;
export default class SpacerWidget extends BasicWidget {
private baseSize: number;
private growthFactor: number;
constructor(baseSize = 0, growthFactor = 1000) {
super();
this.baseSize = baseSize;
this.growthFactor = growthFactor;
}
doRender() {
this.$widget = $(TPL);
this.$widget.css("flex-basis", this.baseSize);
this.$widget.css("flex-grow", this.growthFactor);
this.$widget.css("flex-shrink", 1000);
this.$widget.on("contextmenu", (e) => {
this.$widget.tooltip("hide");
contextMenu.show<CommandNames>({
x: e.pageX,
y: e.pageY,
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (utils.isMobile() ? "bx-mobile" : "bx-sidebar") }],
selectMenuItemHandler: ({ command }) => {
if (command) {
appContext.triggerCommand(command);
}
}
});
return false; // blocks default browser right click menu
});
}
}

View File

@ -24,6 +24,9 @@ type Labels = {
orderBy: string;
orderDirection: string;
// Launch bar
bookmarkFolder: boolean;
// Collection-specific
viewType: string;
status: string;