diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index dbe88ade8..7fb925533 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -7,6 +7,7 @@ import FlexContainer from "../widgets/containers/flex_container.js"; import RootContainer from "../widgets/containers/root_container.js"; import ScrollingContainer from "../widgets/containers/scrolling_container.js"; import SplitNoteContainer from "../widgets/containers/split_note_container.js"; +import FindWidget from "../widgets/find.js"; import FloatingButtons from "../widgets/FloatingButtons.jsx"; import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx"; @@ -27,7 +28,6 @@ import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx"; import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx"; import SearchResult from "../widgets/search_result.jsx"; import SharedInfoWidget from "../widgets/shared_info.js"; -import TabRowWidget from "../widgets/tab_row.js"; import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx"; import { applyModals } from "./layout_commons.js"; @@ -148,7 +148,6 @@ export default class MobileLayout { .child( new FlexContainer("row") .contentSized() - .css("font-size", "larger") .css("align-items", "center") .child() .child() @@ -171,6 +170,7 @@ export default class MobileLayout { .child() ) .child() + .child(new FindWidget()) ) ) ) diff --git a/apps/client/src/services/experimental_features.ts b/apps/client/src/services/experimental_features.ts index 62d4ebb05..8cfbe126e 100644 --- a/apps/client/src/services/experimental_features.ts +++ b/apps/client/src/services/experimental_features.ts @@ -1,5 +1,6 @@ import { t } from "./i18n"; import options from "./options"; +import { isMobile } from "./utils"; export interface ExperimentalFeature { id: string; @@ -21,7 +22,7 @@ let enabledFeatures: Set | null = null; export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean { if (featureId === "new-layout") { - return options.is("newLayout"); + return (isMobile() || options.is("newLayout")); } return getEnabledFeatures().has(featureId); @@ -29,7 +30,7 @@ export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): export function getEnabledExperimentalFeatureIds() { const values = [ ...getEnabledFeatures().values() ]; - if (options.is("newLayout")) { + if (isMobile() || options.is("newLayout")) { values.push("new-layout"); } return values; diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 69632dd29..93163c06d 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -454,7 +454,7 @@ body.desktop .tabulator-popup-container, visibility: hidden; } -body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item, +.dropdown-menu:not(#context-menu-container) .dropdown-item, body.desktop .dropdown-menu .dropdown-toggle, body #context-menu-container .dropdown-item > span, body.mobile .dropdown .dropdown-submenu > span { @@ -462,6 +462,15 @@ body.mobile .dropdown .dropdown-submenu > span { align-items: center; } + +body.mobile .dropdown .dropdown-submenu { + flex-wrap: wrap; + + & > span { + flex-grow: 1; + } +} + .dropdown-item span.keyboard-shortcut, .dropdown-item *:not(.keyboard-shortcut) > kbd { flex-grow: 1; @@ -1530,7 +1539,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { @media (max-width: 991px) { body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show, - body.mobile #launcher-container .dropdown > .dropdown-menu.show { + body.mobile #launcher-container .dropdown > .dropdown-menu.show, + body.mobile .dropdown.note-actions > .dropdown-menu.show { --dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size)); position: fixed !important; bottom: var(--dropdown-bottom) !important; diff --git a/apps/client/src/widgets/PromotedAttributes.tsx b/apps/client/src/widgets/PromotedAttributes.tsx index a9618d3a6..cbe573725 100644 --- a/apps/client/src/widgets/PromotedAttributes.tsx +++ b/apps/client/src/widgets/PromotedAttributes.tsx @@ -5,6 +5,7 @@ import clsx from "clsx"; import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact"; import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks"; +import NoteContext from "../components/note_context"; import FAttribute from "../entities/fattribute"; import FNote from "../entities/fnote"; import { Attribute } from "../services/attribute_parser"; @@ -40,8 +41,8 @@ type OnChangeEventData = TargetedEvent | InputEvent | J type OnChangeListener = (e: OnChangeEventData) => Promise; export default function PromotedAttributes() { - const { note, componentId } = useNoteContext(); - const [ cells, setCells ] = usePromotedAttributeData(note, componentId); + const { note, componentId, noteContext } = useNoteContext(); + const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext); return ; } @@ -74,12 +75,12 @@ export function PromotedAttributesContent({ note, componentId, cells, setCells } * * The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell. */ -export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch> ] { +export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string, noteContext: NoteContext | undefined): [ Cell[] | undefined, Dispatch> ] { const [ viewType ] = useNoteLabel(note, "viewType"); const [ cells, setCells ] = useState(); function refresh() { - if (!note || viewType === "table") { + if (!note || viewType === "table" || noteContext?.viewScope?.viewMode !== "default") { setCells([]); return; } @@ -124,7 +125,7 @@ export function usePromotedAttributeData(note: FNote | null | undefined, compone setCells(cells); } - useEffect(refresh, [ note, viewType ]); + useEffect(refresh, [ note, viewType, noteContext ]); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) { refresh(); diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 255cf89c9..95a3b8b94 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -29,7 +29,6 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: const isVerticalLayout = !isHorizontalLayout; const parentComponent = useContext(ParentComponent); const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus(); - const isMobileLocal = isMobile(); const logoRef = useRef(null); useStaticTooltip(logoRef); @@ -44,8 +43,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: } } noDropdownListStyle - onShown={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover") : undefined} - onHidden={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover") : undefined} + mobileBackdrop > diff --git a/apps/client/src/widgets/layout/NoteTitleActions.tsx b/apps/client/src/widgets/layout/NoteTitleActions.tsx index 6886acc7e..96a2e9296 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.tsx +++ b/apps/client/src/widgets/layout/NoteTitleActions.tsx @@ -48,7 +48,7 @@ function PromotedAttributes({ note, componentId, noteContext }: { componentId: string, noteContext: NoteContext | undefined }) { - const [ cells, setCells ] = usePromotedAttributeData(note, componentId); + const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext); const [ expanded, setExpanded ] = useState(false); useEffect(() => { diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx index 1566d0889..7db00d0bc 100644 --- a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx @@ -1,84 +1,57 @@ -import { useContext } from "preact/hooks"; - -import appContext, { CommandMappings } from "../../components/app_context"; -import contextMenu, { MenuItem } from "../../menus/context_menu"; -import branches from "../../services/branches"; import { t } from "../../services/i18n"; import { getHelpUrlForNote } from "../../services/in_app_help"; import note_create from "../../services/note_create"; -import tree from "../../services/tree"; import { openInAppHelpFromUrl } from "../../services/utils"; -import BasicWidget from "../basic_widget"; -import ActionButton from "../react/ActionButton"; -import { ParentComponent } from "../react/react_utils"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { useNoteContext } from "../react/hooks"; +import { NoteContextMenu } from "../ribbon/NoteActions"; export default function MobileDetailMenu() { - const parentComponent = useContext(ParentComponent); + const { note, noteContext, parentComponent, ntxId } = useNoteContext(); + const helpUrl = getHelpUrlForNote(note); + const subContexts = noteContext?.getMainContext().getSubContexts() ?? []; + const isMainContext = noteContext?.isMainContext(); return ( - { - const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId(); - if (!ntxId) return; - - const noteContext = appContext.tabManager.getNoteContextById(ntxId); - const subContexts = noteContext.getMainContext().getSubContexts(); - const isMainContext = noteContext?.isMainContext(); - const note = noteContext.note; - const helpUrl = getHelpUrlForNote(note); - - const items: (MenuItem)[] = [ - { title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" }, - { title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" }, - { kind: "separator" }, - { title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" }, - { kind: "separator" }, - helpUrl && { - title: t("help-button.title"), - uiIcon: "bx bx-help-circle", - handler: () => openInAppHelpFromUrl(helpUrl) - }, - { kind: "separator" }, - subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" }, - !isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" } - ].filter(i => !!i) as MenuItem[]; - - const lastItem = items.at(-1); - if (lastItem && "kind" in lastItem && lastItem.kind === "separator") { - items.pop(); - } - - contextMenu.show({ - x: e.pageX, - y: e.pageY, - items, - selectMenuItemHandler: async ({ command }) => { - if (command === "insertChildNote") { - note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined); - } else if (command === "delete") { - const notePath = appContext.tabManager.getActiveContextNotePath(); - if (!notePath) { - throw new Error("Cannot get note path to delete."); - } - - const branchId = await tree.getBranchIdFromUrl(notePath); - - if (!branchId) { - throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath })); - } - - if (await branches.deleteNotes([branchId]) && parentComponent) { - parentComponent.triggerCommand("setActiveScreen", { screen: "tree" }); - } - } else if (command && parentComponent) { - parentComponent.triggerCommand(command, { ntxId }); - } - }, - forcePositionOnMobile: true - }); - }} - /> +
+ {note && ( + + noteContext?.notePath && note_create.createNote(noteContext.notePath)} + icon="bx bx-plus" + >{t("mobile_detail_menu.insert_child_note")} + {helpUrl && <> + + openInAppHelpFromUrl(helpUrl)} + >{t("help-button.title")} + } + {subContexts.length < 2 && <> + + parentComponent.triggerCommand("openNewNoteSplit", { ntxId })} + icon="bx bx-dock-right" + >{t("create_pane_button.create_new_split")} + } + {!isMainContext && <> + + { + // Wait first for the context menu to be dismissed, otherwise the backdrop stays on. + requestAnimationFrame(() => { + parentComponent.triggerCommand("closeThisNoteSplit", { ntxId }); + }); + }} + >{t("close_pane_button.close_this_pane")} + } + + } + /> + )} +
); } diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index 5af2f6228..407b14a63 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -3,6 +3,7 @@ import { ComponentChildren, HTMLAttributes } from "preact"; import { CSSProperties, HTMLProps } from "preact/compat"; import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks"; +import { isMobile } from "../../services/utils"; import { useTooltip, useUniqueName } from "./hooks"; type DataAttributes = { @@ -32,9 +33,10 @@ export interface DropdownProps extends Pick, "id" | "c dropdownRef?: MutableRef; titlePosition?: "top" | "right" | "bottom" | "left"; titleOptions?: Partial; + mobileBackdrop?: boolean; } -export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions }: DropdownProps) { +export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions, mobileBackdrop }: DropdownProps) { const containerRef = useRef(null); const triggerRef = useRef(null); const dropdownContainerRef = useRef(null); @@ -74,12 +76,18 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi setShown(true); externalOnShown?.(); hideTooltip(); - }, [ hideTooltip ]); + if (mobileBackdrop && isMobile()) { + document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover"); + } + }, [ hideTooltip, mobileBackdrop ]); const onHidden = useCallback(() => { setShown(false); externalOnHidden?.(); - }, []); + if (mobileBackdrop && isMobile()) { + document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover"); + } + }, [ mobileBackdrop ]); useEffect(() => { if (!containerRef.current) return; diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index 6db1d384e..51788085d 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -1,6 +1,6 @@ import { ConvertToAttachmentResponse } from "@triliumnext/commons"; import { Dropdown as BootstrapDropdown } from "bootstrap"; -import { RefObject } from "preact"; +import { ComponentChildren, RefObject } from "preact"; import { useContext, useEffect, useRef } from "preact/hooks"; import appContext, { CommandNames } from "../../components/app_context"; @@ -63,7 +63,7 @@ function RevisionsButton({ note }: { note: FNote }) { type ItemToFocus = "basic-properties"; -function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { +export function NoteContextMenu({ note, noteContext, extraItems }: { note: FNote, noteContext?: NoteContext, extraItems?: ComponentChildren; }) { const dropdownRef = useRef(null); const parentComponent = useContext(ParentComponent); const noteType = useNoteProperty(note, "type") ?? ""; @@ -99,12 +99,15 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not dropdownRef={dropdownRef} buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" } className="note-actions" + dropdownContainerClassName="mobile-bottom-menu" hideToggleArrow noSelectButtonStyle noDropdownListStyle iconAction onHidden={() => itemToFocusRef.current = null } + mobileBackdrop > + {extraItems} {isReadOnly && <> void), disabled?: boolean, destructive?: boolean }) { +export function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) { return