Prototype/mobile split (#7906)

This commit is contained in:
Elian Doran 2025-12-02 07:35:34 +00:00 committed by GitHub
commit 4b7d243406
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 158 additions and 87 deletions

View File

@ -29,6 +29,7 @@ import type AppContext from "../components/app_context.js";
import NoteDetail from "../widgets/NoteDetail.jsx"; import NoteDetail from "../widgets/NoteDetail.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx"; import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx"; import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
const MOBILE_CSS = ` const MOBILE_CSS = `
<style> <style>
@ -142,33 +143,35 @@ export default class MobileLayout {
.id("detail-container") .id("detail-container")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9") .class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
.child( .child(
new NoteWrapperWidget() new SplitNoteContainer(() =>
.child( new NoteWrapperWidget()
new FlexContainer("row") .child(
.contentSized() new FlexContainer("row")
.css("font-size", "larger") .contentSized()
.css("align-items", "center") .css("font-size", "larger")
.child(<ToggleSidebarButton />) .css("align-items", "center")
.child(<NoteTitleWidget />) .child(<ToggleSidebarButton />)
.child(<MobileDetailMenu />) .child(<NoteTitleWidget />)
) .child(<MobileDetailMenu />)
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />) )
.child(<PromotedAttributes />) .child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
.child( .child(<PromotedAttributes />)
new ScrollingContainer() .child(
.filling() new ScrollingContainer()
.contentSized() .filling()
.child(new ContentHeader() .contentSized()
.child(<ReadOnlyNoteInfoBar />) .child(new ContentHeader()
.child(<SharedInfoWidget />) .child(<ReadOnlyNoteInfoBar />)
) .child(<SharedInfoWidget />)
.child(<NoteDetail />) )
.child(<NoteList media="screen" />) .child(<NoteDetail />)
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />) .child(<NoteList media="screen" />)
.child(<SearchResult />) .child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
.child(<FilePropertiesWrapper />) .child(<SearchResult />)
) .child(<FilePropertiesWrapper />)
.child(<MobileEditorToolbar />) )
.child(<MobileEditorToolbar />)
)
) )
) )
) )

View File

@ -2,26 +2,32 @@ import { t } from "../services/i18n.js";
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js"; import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
import appContext, { type CommandNames } from "../components/app_context.js"; import appContext, { type CommandNames } from "../components/app_context.js";
import type { ViewScope } from "../services/link.js"; import type { ViewScope } from "../services/link.js";
import utils, { isMobile } from "../services/utils.js";
import { getClosestNtxId } from "../widgets/widget_utils.js";
import type { LeafletMouseEvent } from "leaflet";
function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) { function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewScope = {}, hoistedNoteId: string | null = null) {
contextMenu.show({ contextMenu.show({
x: e.pageX, x: e.pageX,
y: e.pageY, y: e.pageY,
items: getItems(), items: getItems(e),
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId) selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, e, notePath, viewScope, hoistedNoteId)
}); });
} }
function getItems(): MenuItem<CommandNames>[] { function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandNames>[] {
const ntxId = getNtxId(e);
const isMobileSplitOpen = isMobile() && appContext.tabManager.getNoteContextById(ntxId).getMainContext().getSubContexts().length > 1;
return [ return [
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" }, { title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" }, { title: !isMobileSplitOpen ? t("link_context_menu.open_note_in_new_split") : t("link_context_menu.open_note_in_other_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" }, { title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" } { title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
]; ];
} }
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) { function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | LeafletMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
if (!hoistedNoteId) { if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null; hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
} }
@ -29,15 +35,8 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
if (command === "openNoteInNewTab") { if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope }); appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} else if (command === "openNoteInNewSplit") { } else if (command === "openNoteInNewSplit") {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts(); const ntxId = getNtxId(e);
if (!ntxId) return;
if (!subContexts) {
logError("subContexts is null");
return;
}
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInNewWindow") { } else if (command === "openNoteInNewWindow") {
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
@ -46,6 +45,18 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
} }
} }
function getNtxId(e: ContextMenuEvent | LeafletMouseEvent) {
if (utils.isDesktop()) {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
if (!subContexts) return null;
return subContexts[subContexts.length - 1].ntxId;
} else if (e.target instanceof HTMLElement) {
return getClosestNtxId(e.target);
} else {
return null;
}
}
export default { export default {
getItems, getItems,
handleLinkContextMenuItem, handleLinkContextMenuItem,

View File

@ -2595,4 +2595,25 @@ iframe.print-iframe {
.calendar-view a.fc-daygrid-event { .calendar-view a.fc-daygrid-event {
/* Workaround: set font weight only if the theme-next is not active */ /* Workaround: set font weight only if the theme-next is not active */
font-weight: var(--root-background, 800); font-weight: var(--root-background, 800);
}
@media (max-width: 991px) {
body.mobile {
.split-note-container-widget {
flex-direction: column !important;
.note-split {
width: 100%;
}
.note-split.visible + .note-split.visible {
border-top: 1px solid var(--main-border-color);
}
}
#root-widget.virtual-keyboard-opened .note-split:not(:focus-within) {
max-height: 80px;
opacity: 0.4;
}
}
} }

View File

@ -1879,6 +1879,7 @@
"link_context_menu": { "link_context_menu": {
"open_note_in_new_tab": "Open note in a new tab", "open_note_in_new_tab": "Open note in a new tab",
"open_note_in_new_split": "Open note in a new split", "open_note_in_new_split": "Open note in a new split",
"open_note_in_other_split": "Open note in the other split",
"open_note_in_new_window": "Open note in a new window", "open_note_in_new_window": "Open note in a new window",
"open_note_in_popup": "Quick edit" "open_note_in_popup": "Quick edit"
}, },

View File

@ -41,7 +41,7 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
x: event.pageX, x: event.pageX,
y: event.pageY, y: event.pageY,
items: [ items: [
...link_context_menu.getItems(), ...link_context_menu.getItems(event),
{ kind: "separator" }, { kind: "separator" },
{ {
title: t("board_view.insert-above"), title: t("board_view.insert-above"),
@ -81,7 +81,7 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
componentFn: () => NoteColorPicker({note}) componentFn: () => NoteColorPicker({note})
} }
], ],
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, note.noteId), selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, event, note.noteId),
}); });
} }

View File

@ -14,7 +14,7 @@ export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parent
x: e.pageX, x: e.pageX,
y: e.pageY, y: e.pageY,
items: [ items: [
...link_context_menu.getItems(), ...link_context_menu.getItems(e),
{ kind: "separator" }, { kind: "separator" },
getArchiveMenuItem(note), getArchiveMenuItem(note),
{ {
@ -40,6 +40,6 @@ export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parent
componentFn: () => NoteColorPicker({note: note}) componentFn: () => NoteColorPicker({note: note})
} }
], ],
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, note.noteId), selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, e, note.noteId),
}) })
} }

View File

@ -12,7 +12,7 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is
let items: MenuItem<keyof CommandMappings>[] = [ let items: MenuItem<keyof CommandMappings>[] = [
...buildGeoLocationItem(e), ...buildGeoLocationItem(e),
{ kind: "separator" }, { kind: "separator" },
...linkContextMenu.getItems(), ...linkContextMenu.getItems(e),
]; ];
if (isEditable) { if (isEditable) {
@ -32,14 +32,14 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is
x: e.originalEvent.pageX, x: e.originalEvent.pageX,
y: e.originalEvent.pageY, y: e.originalEvent.pageY,
items, items,
selectMenuItemHandler: ({ command }, e) => { selectMenuItemHandler: ({ command }) => {
if (command === "deleteFromMap") { if (command === "deleteFromMap") {
appContext.triggerCommand(command, { noteId }); appContext.triggerCommand(command, { noteId });
return; return;
} }
// Pass the events to the link context menu // Pass the events to the link context menu
linkContextMenu.handleLinkContextMenuItem(command, noteId); linkContextMenu.handleLinkContextMenuItem(command, e, noteId);
} }
}); });
} }

View File

@ -174,7 +174,7 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
contextMenu.show({ contextMenu.show({
items: [ items: [
...link_context_menu.getItems(), ...link_context_menu.getItems(e),
{ kind: "separator" }, { kind: "separator" },
{ {
title: t("table_view.row-insert-above"), title: t("table_view.row-insert-above"),
@ -227,7 +227,7 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
componentFn: () => NoteColorPicker({note: rowData.noteId}) componentFn: () => NoteColorPicker({note: rowData.noteId})
} }
], ],
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, rowData.noteId), selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, e, rowData.noteId),
x: e.pageX, x: e.pageX,
y: e.pageY y: e.pageY
}); });

View File

@ -3,6 +3,8 @@ import appContext, { type CommandData, type CommandListenerData, type EventData,
import type BasicWidget from "../basic_widget.js"; import type BasicWidget from "../basic_widget.js";
import Component from "../../components/component.js"; import Component from "../../components/component.js";
import splitService from "../../services/resizer.js"; import splitService from "../../services/resizer.js";
import { isMobile } from "../../services/utils.js";
import NoteContext from "../../components/note_context.js";
interface SplitNoteWidget extends BasicWidget { interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean; hasBeenAlreadyShown?: boolean;
@ -49,13 +51,14 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
this.child(widget); this.child(widget);
if (noteContext.mainNtxId && noteContext.ntxId) { if (noteContext.mainNtxId && noteContext.ntxId && !isMobile()) {
splitService.setupNoteSplitResizer([noteContext.mainNtxId,noteContext.ntxId]); splitService.setupNoteSplitResizer([noteContext.mainNtxId,noteContext.ntxId]);
} }
} }
async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }: EventData<"openNewNoteSplit">) { async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }: EventData<"openNewNoteSplit">) {
const mainNtxId = appContext.tabManager.getActiveMainContext()?.ntxId; const activeContext = appContext.tabManager.getActiveMainContext();
const mainNtxId = activeContext?.ntxId;
if (!mainNtxId) { if (!mainNtxId) {
console.warn("Missing main note context ID"); console.warn("Missing main note context ID");
return; return;
@ -69,22 +72,30 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext()?.hoistedNoteId; hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const noteContext = await appContext.tabManager.openEmptyTab(null, hoistedNoteId, mainNtxId);
if (!noteContext.ntxId) { const subContexts = activeContext.getSubContexts();
logError("Failed to create new note context!"); let noteContext: NoteContext | undefined = undefined;
return; if (isMobile() && subContexts.length > 1) {
noteContext = subContexts.find(s => s.ntxId !== ntxId);
}
if (!noteContext) {
noteContext = await appContext.tabManager.openEmptyTab(null, hoistedNoteId, mainNtxId);
// remove the original position of newly created note context
const ntxIds = appContext.tabManager.children.map((c) => c.ntxId).filter((id) => id !== noteContext?.ntxId) as string[];
// insert the note context after the originating note context
if (!noteContext.ntxId) {
logError("Failed to create new note context!");
return;
}
ntxIds.splice(ntxIds.indexOf(ntxId) + 1, 0, noteContext.ntxId);
this.triggerCommand("noteContextReorder", { ntxIdsInOrder: ntxIds });
// move the note context rendered widget after the originating widget
this.$widget.find(`[data-ntx-id="${noteContext.ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${ntxId}"]`));
} }
// remove the original position of newly created note context
const ntxIds = appContext.tabManager.children.map((c) => c.ntxId).filter((id) => id !== noteContext.ntxId) as string[];
// insert the note context after the originating note context
ntxIds.splice(ntxIds.indexOf(ntxId) + 1, 0, noteContext.ntxId);
this.triggerCommand("noteContextReorder", { ntxIdsInOrder: ntxIds });
// move the note context rendered widget after the originating widget
this.$widget.find(`[data-ntx-id="${noteContext.ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${ntxId}"]`));
await appContext.tabManager.activateNoteContext(noteContext.ntxId); await appContext.tabManager.activateNoteContext(noteContext.ntxId);

View File

@ -1,12 +1,13 @@
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import appContext from "../../components/app_context"; import appContext, { CommandMappings } from "../../components/app_context";
import contextMenu from "../../menus/context_menu"; import contextMenu, { MenuItem } from "../../menus/context_menu";
import branches from "../../services/branches"; import branches from "../../services/branches";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import note_create from "../../services/note_create"; import note_create from "../../services/note_create";
import tree from "../../services/tree"; import tree from "../../services/tree";
import ActionButton from "../react/ActionButton"; import ActionButton from "../react/ActionButton";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import BasicWidget from "../basic_widget";
export default function MobileDetailMenu() { export default function MobileDetailMenu() {
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
@ -16,17 +17,33 @@ export default function MobileDetailMenu() {
icon="bx bx-dots-vertical-rounded" icon="bx bx-dots-vertical-rounded"
text="" text=""
onClick={(e) => { onClick={(e) => {
const note = appContext.tabManager.getActiveContextNote(); const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId();
if (!ntxId) return;
contextMenu.show<"insertChildNote" | "delete" | "showRevisions">({ const noteContext = appContext.tabManager.getNoteContextById(ntxId);
const subContexts = noteContext.getMainContext().getSubContexts();
const isMainContext = noteContext?.isMainContext();
const note = noteContext.note;
const items: (MenuItem<keyof CommandMappings>)[] = [
{ 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" },
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<keyof CommandMappings>[];
const lastItem = items.at(-1);
if (lastItem && "kind" in lastItem && lastItem.kind === "separator") {
items.pop();
}
contextMenu.show<keyof CommandMappings>({
x: e.pageX, x: e.pageX,
y: e.pageY, y: e.pageY,
items: [ items,
{ 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" }
],
selectMenuItemHandler: async ({ command }) => { selectMenuItemHandler: async ({ command }) => {
if (command === "insertChildNote") { if (command === "insertChildNote") {
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined); note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
@ -46,7 +63,7 @@ export default function MobileDetailMenu() {
parentComponent.triggerCommand("setActiveScreen", { screen: "tree" }); parentComponent.triggerCommand("setActiveScreen", { screen: "tree" });
} }
} else if (command && parentComponent) { } else if (command && parentComponent) {
parentComponent.triggerCommand(command); parentComponent.triggerCommand(command, { ntxId });
} }
}, },
forcePositionOnMobile: true forcePositionOnMobile: true

View File

@ -1,18 +1,19 @@
import { useContext } from "preact/hooks";
import ActionButton from "../react/ActionButton"; import ActionButton from "../react/ActionButton";
import { ParentComponent } from "../react/react_utils";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import { useNoteContext } from "../react/hooks";
export default function ToggleSidebarButton() { export default function ToggleSidebarButton() {
const parentComponent = useContext(ParentComponent); const { noteContext, parentComponent } = useNoteContext();
return ( return (
<ActionButton <div style={{ contain: "none", minWidth: 8 }}>
icon="bx bx-sidebar" { noteContext?.isMainContext() && <ActionButton
text={t("note_tree.toggle-sidebar")} icon="bx bx-sidebar"
onClick={() => parentComponent?.triggerCommand("setActiveScreen", { text={t("note_tree.toggle-sidebar")}
screen: "tree" onClick={() => parentComponent?.triggerCommand("setActiveScreen", {
})} screen: "tree"
/> })}
/>}
</div>
) )
} }

View File

@ -19,3 +19,9 @@ export function onWheelHorizontalScroll(event: WheelEvent) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
(event.currentTarget as HTMLElement).scrollLeft += event.deltaY + event.deltaX; (event.currentTarget as HTMLElement).scrollLeft += event.deltaY + event.deltaX;
} }
export function getClosestNtxId(element: HTMLElement) {
const closestNtxEl = element.closest<HTMLElement>("[data-ntx-id]");
if (!closestNtxEl) return null;
return closestNtxEl.dataset.ntxId ?? null;
}