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 MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
const MOBILE_CSS = `
<style>
@ -142,6 +143,7 @@ export default class MobileLayout {
.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")
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(
new FlexContainer("row")
@ -172,6 +174,7 @@ export default class MobileLayout {
)
)
)
)
.child(
new FlexContainer("column")
.contentSized()

View File

@ -2,26 +2,32 @@ import { t } from "../services/i18n.js";
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
import appContext, { type CommandNames } from "../components/app_context.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) {
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: getItems(),
selectMenuItemHandler: ({ command }) => handleLinkContextMenuItem(command, notePath, viewScope, hoistedNoteId)
items: getItems(e),
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 [
{ 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_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) {
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
}
@ -29,15 +35,8 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} else if (command === "openNoteInNewSplit") {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
if (!subContexts) {
logError("subContexts is null");
return;
}
const { ntxId } = subContexts[subContexts.length - 1];
const ntxId = getNtxId(e);
if (!ntxId) return;
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInNewWindow") {
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 {
getItems,
handleLinkContextMenuItem,

View File

@ -2596,3 +2596,24 @@ iframe.print-iframe {
/* Workaround: set font weight only if the theme-next is not active */
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": {
"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_other_split": "Open note in the other split",
"open_note_in_new_window": "Open note in a new window",
"open_note_in_popup": "Quick edit"
},

View File

@ -41,7 +41,7 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
x: event.pageX,
y: event.pageY,
items: [
...link_context_menu.getItems(),
...link_context_menu.getItems(event),
{ kind: "separator" },
{
title: t("board_view.insert-above"),
@ -81,7 +81,7 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
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,
y: e.pageY,
items: [
...link_context_menu.getItems(),
...link_context_menu.getItems(e),
{ kind: "separator" },
getArchiveMenuItem(note),
{
@ -40,6 +40,6 @@ export function openCalendarContextMenu(e: ContextMenuEvent, note: FNote, parent
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>[] = [
...buildGeoLocationItem(e),
{ kind: "separator" },
...linkContextMenu.getItems(),
...linkContextMenu.getItems(e),
];
if (isEditable) {
@ -32,14 +32,14 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is
x: e.originalEvent.pageX,
y: e.originalEvent.pageY,
items,
selectMenuItemHandler: ({ command }, e) => {
selectMenuItemHandler: ({ command }) => {
if (command === "deleteFromMap") {
appContext.triggerCommand(command, { noteId });
return;
}
// 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({
items: [
...link_context_menu.getItems(),
...link_context_menu.getItems(e),
{ kind: "separator" },
{
title: t("table_view.row-insert-above"),
@ -227,7 +227,7 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
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,
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 Component from "../../components/component.js";
import splitService from "../../services/resizer.js";
import { isMobile } from "../../services/utils.js";
import NoteContext from "../../components/note_context.js";
interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean;
@ -49,13 +51,14 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
this.child(widget);
if (noteContext.mainNtxId && noteContext.ntxId) {
if (noteContext.mainNtxId && noteContext.ntxId && !isMobile()) {
splitService.setupNoteSplitResizer([noteContext.mainNtxId,noteContext.ntxId]);
}
}
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) {
console.warn("Missing main note context ID");
return;
@ -69,22 +72,30 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const noteContext = await appContext.tabManager.openEmptyTab(null, hoistedNoteId, mainNtxId);
const subContexts = activeContext.getSubContexts();
let noteContext: NoteContext | undefined = undefined;
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;
}
// 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);

View File

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

View File

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

View File

@ -19,3 +19,9 @@ export function onWheelHorizontalScroll(event: WheelEvent) {
event.stopImmediatePropagation();
(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;
}