diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 3128581a9..479a163f2 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -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() .child() - .child(new SpacerWidget(0, 1)) + .child() .child() .child() .child() diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 2045cd4d7..e6da60ae5 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -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 | JQueryEventObject) { +export function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent | JQueryEventObject) { return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); } diff --git a/apps/client/src/widgets/bookmark_buttons.ts b/apps/client/src/widgets/bookmark_buttons.ts index b32393c9a..ef6cb97a1 100644 --- a/apps/client/src/widgets/bookmark_buttons.ts +++ b/apps/client/src/widgets/bookmark_buttons.ts @@ -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 { } async refresh(): Promise { - 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 { 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); diff --git a/apps/client/src/widgets/buttons/bookmark_folder.ts b/apps/client/src/widgets/buttons/bookmark_folder.ts deleted file mode 100644 index b749a1ab6..000000000 --- a/apps/client/src/widgets/buttons/bookmark_folder.ts +++ /dev/null @@ -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 = ` -
- - -
- -
    -
    `; - -interface LinkOptions { - showTooltip: boolean; - showNoteIcon: boolean; -} - -export default class BookmarkFolderWidget extends RightDropdownButtonWidget { - private note: FNote; - private $parentNote!: JQuery; - private $childrenNotes!: JQuery; - declare $dropdownContent: JQuery; - - 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 { - 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($("
  • ").append((await linkService.createLink(childNote.noteId, linkOptions)).addClass("note-link"))); - } - } - - refreshIcon(): void {} -} diff --git a/apps/client/src/widgets/buttons/open_note_button_widget.ts b/apps/client/src/widgets/buttons/open_note_button_widget.ts deleted file mode 100644 index c0a4c6334..000000000 --- a/apps/client/src/widgets/buttons/open_note_button_widget.ts +++ /dev/null @@ -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 - } -} diff --git a/apps/client/src/widgets/containers/launcher.ts b/apps/client/src/widgets/containers/launcher.tsx similarity index 91% rename from apps/client/src/widgets/containers/launcher.ts rename to apps/client/src/widgets/containers/launcher.tsx index e1bfc5a8b..4ae82bbcd 100644 --- a/apps/client/src/widgets/containers/launcher.ts +++ b/apps/client/src/widgets/containers/launcher.tsx @@ -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([ 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 ; case "bookmarks": - return new BookmarkButtons(this.isHorizontalLayout); + return case "protectedSession": return new ProtectedSessionStatusWidget(); case "syncStatus": diff --git a/apps/client/src/widgets/launch_bar/BookmarkButtons.css b/apps/client/src/widgets/launch_bar/BookmarkButtons.css new file mode 100644 index 000000000..b38ba59c0 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/BookmarkButtons.css @@ -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; +} \ No newline at end of file diff --git a/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx b/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx new file mode 100644 index 000000000..4c0df90e6 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/BookmarkButtons.tsx @@ -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(() => ({ + display: "flex", + flexDirection: isHorizontalLayout ? "row" : "column", + contain: "none" + }), [ isHorizontalLayout ]); + const childNotes = useChildNotes(PARENT_NOTE_ID); + + return ( +
    + {childNotes?.map(childNote => )} +
    + ) +} + +function SingleBookmark({ note }: { note: FNote }) { + const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder"); + return bookmarkFolder + ? + : +} + +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 && ( + { + 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 && ( + +
    +
    + +
    + +
      + {childNotes.map(childNote => ( +
    • + +
    • + ))} +
    +
    +
    + ) +} + +function getHoistedNoteId(noteToOpen: FNote) { + return noteToOpen.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId; +} diff --git a/apps/client/src/widgets/launch_bar/RightDropdownButton.tsx b/apps/client/src/widgets/launch_bar/RightDropdownButton.tsx new file mode 100644 index 000000000..e47b5624c --- /dev/null +++ b/apps/client/src/widgets/launch_bar/RightDropdownButton.tsx @@ -0,0 +1,3 @@ +export default function RightDropdownButton() { + return

    Button goes here.

    ; +} diff --git a/apps/client/src/widgets/launch_bar/SpacerWidget.tsx b/apps/client/src/widgets/launch_bar/SpacerWidget.tsx new file mode 100644 index 000000000..5f89369c2 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/SpacerWidget.tsx @@ -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 ( +
    { + e.preventDefault(); + contextMenu.show({ + 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); + } + } + }); + }} + /> + ) +} diff --git a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx new file mode 100644 index 000000000..1ea64a852 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx @@ -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) { + return ( + + ) +} + +export function LaunchBarDropdownButton({ children, icon, ...props }: Pick & { icon: string }) { + return ( + } + {...props} + >{children} + ) +} diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx index 28489005d..a37f34514 100644 --- a/apps/client/src/widgets/react/ActionButton.tsx +++ b/apps/client/src/widgets/react/ActionButton.tsx @@ -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, "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(null); const [ keyboardShortcut, setKeyboardShortcut ] = useState(); @@ -35,8 +35,8 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo return