mirror of
https://github.com/docmost/docmost.git
synced 2026-06-10 01:52:43 +08:00
feat(base): inline embed extends grid + toolbar beyond page width
When a base is embedded inline in a doc page, measure the parent container's available area and extend toolbar + grid sections to fill it via CSS variables (--embed-width / --embed-shift / --embed-pad). Inner content is re-padded so toolbar buttons and the first column visually align with the page text, while the box itself reaches the viewport edges for horizontal scroll headroom on wide databases. Sticky inset-inline-start keeps the toolbar pinned to the page-content edge during horizontal scroll. Standalone full-page bases are unaffected (the embedded prop defaults to false).
This commit is contained in:
@@ -43,9 +43,10 @@ import classes from "@/features/base/styles/grid.module.css";
|
||||
|
||||
type BaseTableProps = {
|
||||
pageId: string;
|
||||
embedded?: boolean;
|
||||
};
|
||||
|
||||
export function BaseTable({ pageId }: BaseTableProps) {
|
||||
export function BaseTable({ pageId, embedded }: BaseTableProps) {
|
||||
const { t } = useTranslation();
|
||||
// Subscribe to the base's realtime room so other clients' edits,
|
||||
// schema changes, and async-job completions reconcile into our cache.
|
||||
@@ -313,39 +314,75 @@ export function BaseTable({ pageId }: BaseTableProps) {
|
||||
|
||||
if (!base) return null;
|
||||
|
||||
// When the table is embedded inline in a doc page, the parent
|
||||
// <NodeViewWrapper> measures the available area and exposes
|
||||
// --embed-width / --embed-shift / --embed-pad. We extend the
|
||||
// toolbar and grid sections out to the full available width via
|
||||
// those vars, then re-pad the inner content so it visually aligns
|
||||
// with page text. Sticky inset-inline-start keeps the toolbar
|
||||
// pinned to the page-content edge during horizontal scroll.
|
||||
const extendStyle = embedded
|
||||
? ({
|
||||
position: "relative",
|
||||
width: "var(--embed-width, 100%)",
|
||||
insetInlineStart: "var(--embed-shift, 0px)",
|
||||
paddingInline: "var(--embed-pad, 0px)",
|
||||
} as const)
|
||||
: undefined;
|
||||
|
||||
const stickyToolbarStyle = embedded
|
||||
? ({
|
||||
position: "sticky",
|
||||
insetInlineStart: 0,
|
||||
zIndex: 4,
|
||||
} as const)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
||||
<BaseViewDraftBanner
|
||||
isDirty={isDirty}
|
||||
canSave={canSave}
|
||||
onReset={resetDraft}
|
||||
onSave={handleSaveDraft}
|
||||
saving={updateViewMutation.isPending}
|
||||
/>
|
||||
<BaseToolbar
|
||||
base={base}
|
||||
activeView={effectiveView}
|
||||
views={views}
|
||||
table={table}
|
||||
onViewChange={handleViewChange}
|
||||
onAddView={handleAddView}
|
||||
onPersistViewConfig={persistViewConfig}
|
||||
onDraftSortsChange={handleDraftSortsChange}
|
||||
onDraftFiltersChange={handleDraftFiltersChange}
|
||||
/>
|
||||
<GridContainer
|
||||
table={table}
|
||||
properties={base.properties}
|
||||
onCellUpdate={handleCellUpdate}
|
||||
onAddRow={handleAddRow}
|
||||
pageId={pageId}
|
||||
onColumnReorder={handleColumnReorder}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
onRowReorder={handleRowReorder}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: embedded ? "auto" : "100%",
|
||||
}}
|
||||
>
|
||||
<div style={extendStyle}>
|
||||
<BaseViewDraftBanner
|
||||
isDirty={isDirty}
|
||||
canSave={canSave}
|
||||
onReset={resetDraft}
|
||||
onSave={handleSaveDraft}
|
||||
saving={updateViewMutation.isPending}
|
||||
/>
|
||||
<div style={stickyToolbarStyle}>
|
||||
<BaseToolbar
|
||||
base={base}
|
||||
activeView={effectiveView}
|
||||
views={views}
|
||||
table={table}
|
||||
onViewChange={handleViewChange}
|
||||
onAddView={handleAddView}
|
||||
onPersistViewConfig={persistViewConfig}
|
||||
onDraftSortsChange={handleDraftSortsChange}
|
||||
onDraftFiltersChange={handleDraftFiltersChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={extendStyle}>
|
||||
<GridContainer
|
||||
table={table}
|
||||
properties={base.properties}
|
||||
onCellUpdate={handleCellUpdate}
|
||||
onAddRow={handleAddRow}
|
||||
pageId={pageId}
|
||||
onColumnReorder={handleColumnReorder}
|
||||
onResizeEnd={handleResizeEnd}
|
||||
onRowReorder={handleRowReorder}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,69 @@
|
||||
import { NodeViewWrapper, NodeViewProps } from '@tiptap/react';
|
||||
import { Box, Text } from '@mantine/core';
|
||||
import { BaseTable } from '@/features/base/components/base-table';
|
||||
import { useBaseQuery } from '@/features/base/queries/base-query';
|
||||
import { NodeViewWrapper, NodeViewProps } from "@tiptap/react";
|
||||
import { Box, Text } from "@mantine/core";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { BaseTable } from "@/features/base/components/base-table";
|
||||
import { useBaseQuery } from "@/features/base/queries/base-query";
|
||||
|
||||
const SIDE_GUTTER = 8;
|
||||
|
||||
// Walk up from `el` to find the closest ancestor that's meaningfully
|
||||
// wider than `el` itself. That ancestor is the available drawing area
|
||||
// our embed should expand into (e.g. AppShell.Main when the page sits
|
||||
// inside a 900px Mantine Container). Returns null if no such ancestor
|
||||
// exists — the embed then renders without extension.
|
||||
function findWiderAncestor(el: HTMLElement): HTMLElement | null {
|
||||
const baseWidth = el.getBoundingClientRect().width;
|
||||
let cur: HTMLElement | null = el.parentElement;
|
||||
while (cur && cur !== document.body) {
|
||||
const w = cur.getBoundingClientRect().width;
|
||||
if (w > baseWidth + 32) return cur;
|
||||
cur = cur.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyExtension(wrapper: HTMLDivElement) {
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
const wider = findWiderAncestor(wrapper);
|
||||
if (!wider) {
|
||||
wrapper.style.setProperty("--embed-shift", "0px");
|
||||
wrapper.style.setProperty("--embed-width", "100%");
|
||||
wrapper.style.setProperty("--embed-pad", "0px");
|
||||
return;
|
||||
}
|
||||
const widerRect = wider.getBoundingClientRect();
|
||||
const targetLeft = widerRect.left + SIDE_GUTTER;
|
||||
const targetWidth = widerRect.width - SIDE_GUTTER * 2;
|
||||
const shift = targetLeft - wrapperRect.left;
|
||||
wrapper.style.setProperty("--embed-shift", `${shift}px`);
|
||||
wrapper.style.setProperty("--embed-width", `${targetWidth}px`);
|
||||
// Re-pad inner content back to the original wrapper bounds so
|
||||
// toolbar buttons / first column visually align with page text.
|
||||
wrapper.style.setProperty("--embed-pad", `${-shift}px`);
|
||||
}
|
||||
|
||||
export function BaseEmbedView({ node }: NodeViewProps) {
|
||||
const pageId = node.attrs.pageId as string | null;
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const wrapper = wrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
|
||||
const update = () => applyExtension(wrapper);
|
||||
update();
|
||||
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(wrapper);
|
||||
const wider = findWiderAncestor(wrapper);
|
||||
if (wider) ro.observe(wider);
|
||||
|
||||
window.addEventListener("resize", update);
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener("resize", update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!pageId) {
|
||||
return (
|
||||
@@ -40,9 +99,9 @@ export function BaseEmbedView({ node }: NodeViewProps) {
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<Box style={{ minHeight: 200 }}>
|
||||
<BaseTable pageId={pageId} />
|
||||
</Box>
|
||||
<div ref={wrapperRef} style={{ minHeight: 200 }}>
|
||||
<BaseTable pageId={pageId} embedded />
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user