@drawnagency/primitives 0.1.56 → 0.1.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-KGYWQDBB.js → chunk-ICLXLWQ5.js} +9 -72
- package/dist/chunk-NSCT3AMV.js +32 -0
- package/dist/{chunk-EU6NZ4GS.js → chunk-PRKUXM7E.js} +23 -9
- package/dist/{chunk-7IAWF7LE.js → chunk-PYWS3MOJ.js} +12 -2
- package/dist/chunk-TG43X7JO.js +123 -0
- package/dist/chunk-VKAGMEKE.js +90 -0
- package/dist/components/editor/ChildBlockWrapper.d.ts +19 -0
- package/dist/components/editor/ChildBlockWrapper.d.ts.map +1 -0
- package/dist/components/editor/ColSpanControl.d.ts +9 -0
- package/dist/components/editor/ColSpanControl.d.ts.map +1 -0
- package/dist/components/editor/SectionWrapper.d.ts +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/SettingsForm.d.ts +5 -1
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/primitives/EditableGrid.d.ts.map +1 -1
- package/dist/components/primitives/IconPicker.d.ts +7 -1
- package/dist/components/primitives/IconPicker.d.ts.map +1 -1
- package/dist/components/sections/Container/Container.d.ts +20 -0
- package/dist/components/sections/Container/Container.d.ts.map +1 -0
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts +17 -0
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts.map +1 -0
- package/dist/components/sections/Container/index.d.ts +11 -0
- package/dist/components/sections/Container/index.d.ts.map +1 -0
- package/dist/components/sections/IconList/IconList.d.ts +1 -0
- package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconListSettings.d.ts +3 -4
- package/dist/components/sections/IconList/IconListSettings.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts +1 -0
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/Media/MediaBlock.d.ts +19 -0
- package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -0
- package/dist/components/sections/{MediaGrid → Media}/index.d.ts +15 -25
- package/dist/components/sections/Media/index.d.ts.map +1 -0
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/Spacer/Spacer.d.ts +2 -0
- package/dist/components/sections/Spacer/Spacer.d.ts.map +1 -0
- package/dist/components/sections/Spacer/index.d.ts +6 -0
- package/dist/components/sections/Spacer/index.d.ts.map +1 -0
- package/dist/components/sections/all-sections.d.ts +29 -103
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.d.ts.map +1 -1
- package/dist/components/shared/Tabs.d.ts +24 -0
- package/dist/components/shared/Tabs.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/components/shell/blockMoveDispatch.d.ts +21 -0
- package/dist/components/shell/blockMoveDispatch.d.ts.map +1 -0
- package/dist/hooks/useBlockDnd.d.ts +48 -0
- package/dist/hooks/useBlockDnd.d.ts.map +1 -0
- package/dist/index.js +56 -48
- package/dist/lib/block-dnd.d.ts +42 -0
- package/dist/lib/block-dnd.d.ts.map +1 -0
- package/dist/lib/block-move.d.ts +31 -0
- package/dist/lib/block-move.d.ts.map +1 -0
- package/dist/lib/container-grid.d.ts +29 -0
- package/dist/lib/container-grid.d.ts.map +1 -0
- package/dist/lib/container-ops.d.ts +44 -0
- package/dist/lib/container-ops.d.ts.map +1 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +13 -0
- package/dist/lib/index.js +10 -7
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/migrate-sections-transform.d.ts +12 -0
- package/dist/lib/migrate-sections-transform.d.ts.map +1 -0
- package/dist/lib/migrate-sections-transform.js +6 -0
- package/dist/lib/registry.d.ts +39 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/registry.js +26 -0
- package/dist/schemas/block.d.ts +20 -0
- package/dist/schemas/block.d.ts.map +1 -0
- package/dist/schemas/block.js +14 -0
- package/dist/schemas/index.js +8 -2
- package/dist/schemas/link.d.ts +7 -0
- package/dist/schemas/link.d.ts.map +1 -1
- package/dist/schemas/rich-text.d.ts +9 -0
- package/dist/schemas/rich-text.d.ts.map +1 -0
- package/dist/schemas/sections.d.ts +2 -0
- package/dist/schemas/sections.d.ts.map +1 -1
- package/dist/schemas/shared.d.ts +30 -0
- package/dist/schemas/shared.d.ts.map +1 -1
- package/package.json +13 -1
- package/src/components/brandguide/Colors.tsx +35 -33
- package/src/components/editor/ChildBlockWrapper.tsx +108 -0
- package/src/components/editor/ColSpanControl.tsx +56 -0
- package/src/components/editor/SectionWrapper.tsx +44 -20
- package/src/components/editor/SettingsForm.tsx +100 -73
- package/src/components/primitives/EditableGrid.tsx +40 -36
- package/src/components/primitives/IconPicker.tsx +116 -26
- package/src/components/sections/Container/Container.tsx +354 -0
- package/src/components/sections/Container/ContainerSettingsForm.tsx +113 -0
- package/src/components/sections/Container/index.tsx +51 -0
- package/src/components/sections/IconList/IconList.tsx +113 -43
- package/src/components/sections/IconList/IconListSettings.tsx +2 -2
- package/src/components/sections/IconList/index.tsx +1 -1
- package/src/components/sections/Media/MediaBlock.tsx +103 -0
- package/src/components/sections/Media/index.tsx +85 -0
- package/src/components/sections/Prose/index.tsx +1 -0
- package/src/components/sections/Spacer/Spacer.tsx +6 -0
- package/src/components/sections/Spacer/index.tsx +18 -0
- package/src/components/sections/all-sections.ts +10 -8
- package/src/components/sections/register-schemas.ts +5 -2
- package/src/components/shared/Tabs.tsx +63 -0
- package/src/components/shell/EditorShell.tsx +105 -13
- package/src/components/shell/SiteSettingsModal.tsx +41 -51
- package/src/components/shell/blockMoveDispatch.ts +40 -0
- package/src/hooks/useBlockDnd.ts +144 -0
- package/src/lib/block-dnd.ts +58 -0
- package/src/lib/block-move.ts +236 -0
- package/src/lib/container-grid.ts +58 -0
- package/src/lib/container-ops.ts +159 -0
- package/src/lib/dexie.ts +22 -0
- package/src/lib/loader.ts +16 -4
- package/src/lib/migrate-sections-transform.ts +147 -0
- package/src/lib/registry.ts +48 -0
- package/src/schemas/block.ts +40 -0
- package/src/schemas/link.ts +19 -1
- package/src/schemas/rich-text.ts +11 -0
- package/src/schemas/sections.ts +5 -1
- package/src/schemas/shared.ts +6 -0
- package/dist/components/brandguide/DoDontList.d.ts +0 -16
- package/dist/components/brandguide/DoDontList.d.ts.map +0 -1
- package/dist/components/brandguide/DoDontMediaGrid.d.ts +0 -16
- package/dist/components/brandguide/DoDontMediaGrid.d.ts.map +0 -1
- package/dist/components/primitives/MediaSettingsForms.d.ts +0 -23
- package/dist/components/primitives/MediaSettingsForms.d.ts.map +0 -1
- package/dist/components/sections/DoDontList/index.d.ts +0 -21
- package/dist/components/sections/DoDontList/index.d.ts.map +0 -1
- package/dist/components/sections/DoDontMediaGrid/index.d.ts +0 -55
- package/dist/components/sections/DoDontMediaGrid/index.d.ts.map +0 -1
- package/dist/components/sections/MediaGrid/MediaGrid.d.ts +0 -17
- package/dist/components/sections/MediaGrid/MediaGrid.d.ts.map +0 -1
- package/dist/components/sections/MediaGrid/index.d.ts.map +0 -1
- package/dist/components/sections/SplitContent/SplitContent.d.ts +0 -14
- package/dist/components/sections/SplitContent/SplitContent.d.ts.map +0 -1
- package/dist/components/sections/SplitContent/index.d.ts +0 -13
- package/dist/components/sections/SplitContent/index.d.ts.map +0 -1
- package/src/components/brandguide/DoDontList.d.ts.map +0 -1
- package/src/components/brandguide/DoDontList.tsx +0 -67
- package/src/components/brandguide/DoDontMediaGrid.d.ts.map +0 -1
- package/src/components/brandguide/DoDontMediaGrid.tsx +0 -19
- package/src/components/primitives/MediaSettingsForms.tsx +0 -128
- package/src/components/sections/DoDontList/index.d.ts.map +0 -1
- package/src/components/sections/DoDontList/index.tsx +0 -45
- package/src/components/sections/DoDontMediaGrid/index.d.ts.map +0 -1
- package/src/components/sections/DoDontMediaGrid/index.tsx +0 -63
- package/src/components/sections/MediaGrid/MediaGrid.d.ts.map +0 -1
- package/src/components/sections/MediaGrid/MediaGrid.tsx +0 -239
- package/src/components/sections/MediaGrid/index.d.ts.map +0 -1
- package/src/components/sections/MediaGrid/index.tsx +0 -57
- package/src/components/sections/SplitContent/SplitContent.d.ts.map +0 -1
- package/src/components/sections/SplitContent/SplitContent.tsx +0 -84
- package/src/components/sections/SplitContent/index.d.ts.map +0 -1
- package/src/components/sections/SplitContent/index.tsx +0 -55
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineSection } from "../../../lib/registry";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { SquareDashed } from "lucide-react";
|
|
4
|
+
import Spacer from "./Spacer";
|
|
5
|
+
|
|
6
|
+
const schema = z.object({
|
|
7
|
+
type: z.literal("spacer"),
|
|
8
|
+
content: z.object({}).default({}),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export default defineSection({
|
|
12
|
+
type: "spacer",
|
|
13
|
+
label: "Spacer",
|
|
14
|
+
icon: <SquareDashed size={18} />,
|
|
15
|
+
schema,
|
|
16
|
+
component: () => <Spacer />,
|
|
17
|
+
defaults: () => ({ type: "spacer" as const, content: {} }),
|
|
18
|
+
});
|
|
@@ -2,13 +2,12 @@ import linkHeading from "./LinkHeading";
|
|
|
2
2
|
import subHeading from "./SubHeading";
|
|
3
3
|
import subSubHeading from "./SubSubHeading";
|
|
4
4
|
import prose from "./Prose";
|
|
5
|
-
import
|
|
6
|
-
import splitContent from "./SplitContent";
|
|
5
|
+
import media from "./Media";
|
|
7
6
|
import button from "./Button";
|
|
8
7
|
import colors from "./Colors";
|
|
9
|
-
import doDontList from "./DoDontList";
|
|
10
|
-
import doDontImageGrid from "./DoDontMediaGrid";
|
|
11
8
|
import iconList from "./IconList";
|
|
9
|
+
import container from "./Container";
|
|
10
|
+
import spacer from "./Spacer";
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* The single ordered list of built-in section definitions.
|
|
@@ -18,6 +17,10 @@ import iconList from "./IconList";
|
|
|
18
17
|
* array, so the two can no longer drift — previously they hand-maintained
|
|
19
18
|
* parallel 11-entry lists, and a section added to one but not the other would
|
|
20
19
|
* make the editor render a type that save rejects with a 400 (or vice versa).
|
|
20
|
+
*
|
|
21
|
+
* Retired: split_content, media_grid, do_dont_grid, do_dont — fully decomposable
|
|
22
|
+
* into container sections. The loader fallback in mergeSiteContent transforms any
|
|
23
|
+
* legacy-typed JSON to container before validation.
|
|
21
24
|
*/
|
|
22
25
|
// Intentionally un-annotated: each section is SectionDefinition<its-own-shape>,
|
|
23
26
|
// and a wider `SectionDefinition<unknown>[]` annotation isn't assignable from
|
|
@@ -28,11 +31,10 @@ export const allSectionDefs = [
|
|
|
28
31
|
subHeading,
|
|
29
32
|
subSubHeading,
|
|
30
33
|
prose,
|
|
31
|
-
|
|
32
|
-
splitContent,
|
|
34
|
+
media,
|
|
33
35
|
button,
|
|
34
36
|
colors,
|
|
35
|
-
doDontList,
|
|
36
|
-
doDontImageGrid,
|
|
37
37
|
iconList,
|
|
38
|
+
container,
|
|
39
|
+
spacer,
|
|
38
40
|
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { registerSchema } from "../../lib/registry";
|
|
1
|
+
import { registerSchema, registerRichText } from "../../lib/registry";
|
|
2
2
|
import { allSectionDefs } from "./all-sections";
|
|
3
3
|
|
|
4
4
|
// NOTE: this still imports the full (component-bearing) section modules via the
|
|
@@ -10,7 +10,10 @@ import { allSectionDefs } from "./all-sections";
|
|
|
10
10
|
let _ensured = false;
|
|
11
11
|
export function ensureSchemasRegistered(): number {
|
|
12
12
|
if (!_ensured) {
|
|
13
|
-
allSectionDefs.forEach((def) =>
|
|
13
|
+
allSectionDefs.forEach((def) => {
|
|
14
|
+
registerSchema(def.type, def.schema);
|
|
15
|
+
if (def.richTextFields) registerRichText(def.type, def.richTextFields);
|
|
16
|
+
});
|
|
14
17
|
_ensured = true;
|
|
15
18
|
}
|
|
16
19
|
return allSectionDefs.length;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface TabItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
content: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TabsProps {
|
|
11
|
+
tabs: TabItem[];
|
|
12
|
+
/** Initial active tab when uncontrolled. Defaults to the first tab. */
|
|
13
|
+
defaultTabId?: string;
|
|
14
|
+
/** When provided, the component is controlled by the parent. */
|
|
15
|
+
activeTabId?: string;
|
|
16
|
+
onTabChange?: (id: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* When true, the tab-bar row uses -mx-6 to break out of the parent's px-6 padding
|
|
19
|
+
* (e.g. EditorModal's content wrapper) and re-pads with px-6 so buttons stay aligned.
|
|
20
|
+
* The tab panel inherits the parent's padding unchanged.
|
|
21
|
+
* Use only when the parent container has px-6; leave false for noPadding contexts.
|
|
22
|
+
*/
|
|
23
|
+
fullBleedTabBar?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Tabs({ tabs, defaultTabId, activeTabId, onTabChange, fullBleedTabBar }: TabsProps) {
|
|
27
|
+
const [uncontrolledId, setUncontrolledId] = useState(defaultTabId ?? tabs[0]?.id);
|
|
28
|
+
const isControlled = activeTabId !== undefined;
|
|
29
|
+
const activeId = isControlled ? activeTabId : uncontrolledId;
|
|
30
|
+
const activeTab = tabs.find((t) => t.id === activeId) ?? tabs[0];
|
|
31
|
+
|
|
32
|
+
function handleSelect(id: string) {
|
|
33
|
+
if (!isControlled) setUncontrolledId(id);
|
|
34
|
+
onTabChange?.(id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
39
|
+
<div className={cn("border-b border-base-200", fullBleedTabBar && "-mx-6 px-6")}>
|
|
40
|
+
<div className="flex" role="tablist">
|
|
41
|
+
{tabs.map((tab) => (
|
|
42
|
+
<button
|
|
43
|
+
key={tab.id}
|
|
44
|
+
type="button"
|
|
45
|
+
role="tab"
|
|
46
|
+
aria-selected={activeTab?.id === tab.id}
|
|
47
|
+
onClick={() => handleSelect(tab.id)}
|
|
48
|
+
className={cn(
|
|
49
|
+
"cursor-pointer px-4 py-2 text-sm font-medium border-b-2 -mb-px",
|
|
50
|
+
activeTab?.id === tab.id
|
|
51
|
+
? "border-brand text-brand"
|
|
52
|
+
: "border-transparent text-base-contrast-light hover:text-base-contrast",
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{tab.label}
|
|
56
|
+
</button>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div role="tabpanel">{activeTab?.content}</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -3,8 +3,9 @@ import { Fragment, useState, useCallback, useEffect, useRef, useMemo, type React
|
|
|
3
3
|
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
4
4
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
5
5
|
import { autoScrollWindowForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
|
6
|
+
import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
6
7
|
import type { LoadedSection } from "../../lib/loader";
|
|
7
|
-
import type { SectionContent } from "../../schemas/sections";
|
|
8
|
+
import type { Section, SectionContent } from "../../schemas/sections";
|
|
8
9
|
import type { SiteIndex, SiteConfig, Page } from "../../schemas/site-config";
|
|
9
10
|
import type { Audience } from "../../auth/types";
|
|
10
11
|
import type { MediaManifest } from "../../media/types";
|
|
@@ -12,6 +13,9 @@ import type { QueueItem } from "../../media/queue";
|
|
|
12
13
|
import { SiteConfigSchema } from "../../schemas/site-config";
|
|
13
14
|
import { ensureSanitizer } from "../../lib/sanitize";
|
|
14
15
|
import { EditorProvider, useEditorContext } from "./EditorContext";
|
|
16
|
+
import { makeBlockMoveDispatch } from "./blockMoveDispatch";
|
|
17
|
+
import { ROOT_CONTAINER_ID, resolveDropIndex } from "../../lib/block-dnd";
|
|
18
|
+
import type { BlockMove } from "../../lib/block-move";
|
|
15
19
|
import { EditorModalProvider, useEditorModal } from "./EditorModalContext";
|
|
16
20
|
import { EditorModal } from "./EditorModal";
|
|
17
21
|
import { SiteSettingsModal } from "./SiteSettingsModal";
|
|
@@ -19,8 +23,10 @@ import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
|
19
23
|
import { MediaLibraryContext } from "./MediaLibraryContext";
|
|
20
24
|
import { ProcessingIndicator } from "./ProcessingIndicator";
|
|
21
25
|
import { SectionSkeleton } from "./SectionSkeleton";
|
|
26
|
+
import { SectionTypePicker } from "./SectionTypePicker";
|
|
22
27
|
import { ensureSectionsRegistered } from "../sections/register";
|
|
23
28
|
import { getSection, getAllSections } from "../../lib/registry";
|
|
29
|
+
import { childInsertableTypes, insertChildAt } from "../../lib/container-ops";
|
|
24
30
|
|
|
25
31
|
ensureSectionsRegistered();
|
|
26
32
|
import { BugReportFAB } from "./BugReportFAB";
|
|
@@ -145,6 +151,8 @@ export default function EditorShell({
|
|
|
145
151
|
// doesn't enqueue a GitHub file deletion for a path that was never committed.
|
|
146
152
|
const remoteSectionIdsRef = useRef<Set<string>>(new Set());
|
|
147
153
|
const fontLinkRef = useRef<HTMLLinkElement | null>(null);
|
|
154
|
+
const sectionsRef = useRef<LoadedSection[]>([]);
|
|
155
|
+
useEffect(() => { sectionsRef.current = sections; }, [sections]);
|
|
148
156
|
useEffect(() => { siteIndexRef.current = siteIndex; }, [siteIndex]);
|
|
149
157
|
useEffect(() => { void ensureSanitizer(); }, []);
|
|
150
158
|
|
|
@@ -635,6 +643,24 @@ export default function EditorShell({
|
|
|
635
643
|
[persistence, resolvedActivePage],
|
|
636
644
|
);
|
|
637
645
|
|
|
646
|
+
const onBlockMove = useMemo(
|
|
647
|
+
() => makeBlockMoveDispatch({
|
|
648
|
+
getState: () => ({ sections: sectionsRef.current, index: siteIndexRef.current, rootPageId: resolvedActivePage?.id ?? homePage(siteIndexRef.current).id }),
|
|
649
|
+
setSections,
|
|
650
|
+
setSiteIndex,
|
|
651
|
+
persistence: { markSectionDirty: persistence.markSectionDirty, markIndexDirty: persistence.markIndexDirty, removeSection: persistence.removeSection },
|
|
652
|
+
isRemote: (id) => remoteSectionIdsRef.current.has(id),
|
|
653
|
+
scheduleDelete: (id) => setDeletedSections((prev) => new Set(prev).add(id)),
|
|
654
|
+
markLocalChanges: () => setLocalChangesExist(true),
|
|
655
|
+
newSectionContent: (id, secs) => {
|
|
656
|
+
const s = secs.find((x) => x.section.id === id)?.section;
|
|
657
|
+
const { id: _omit, ...content } = (s ?? { id }) as { id: string } & Record<string, unknown>;
|
|
658
|
+
return content as unknown as SectionContent;
|
|
659
|
+
},
|
|
660
|
+
}),
|
|
661
|
+
[persistence, resolvedActivePage],
|
|
662
|
+
);
|
|
663
|
+
|
|
638
664
|
const handleMoveSection = useCallback((sectionId: string, destPageId: string, position: "top" | "bottom") => {
|
|
639
665
|
const next = moveSectionReducer(siteIndexRef.current, sectionId, destPageId, position);
|
|
640
666
|
setSiteIndex(next);
|
|
@@ -882,6 +908,7 @@ export default function EditorShell({
|
|
|
882
908
|
onAddSection={onAddSection}
|
|
883
909
|
onDeleteSection={setPendingDeleteSectionId}
|
|
884
910
|
onReorderSections={onReorderSections}
|
|
911
|
+
onBlockMove={onBlockMove}
|
|
885
912
|
onMoveSection={siteIndex.pages.length > 1 ? setMovingSectionId : undefined}
|
|
886
913
|
onAccessChange={onAccessChange}
|
|
887
914
|
onStatusChange={onStatusChange}
|
|
@@ -1140,6 +1167,7 @@ function EditorContent({
|
|
|
1140
1167
|
onAddSection,
|
|
1141
1168
|
onDeleteSection,
|
|
1142
1169
|
onReorderSections,
|
|
1170
|
+
onBlockMove,
|
|
1143
1171
|
onMoveSection,
|
|
1144
1172
|
onAccessChange,
|
|
1145
1173
|
onStatusChange,
|
|
@@ -1156,6 +1184,7 @@ function EditorContent({
|
|
|
1156
1184
|
onAddSection: (insertIndex: number, type: string) => void;
|
|
1157
1185
|
onDeleteSection: (sectionId: string) => void;
|
|
1158
1186
|
onReorderSections: (fromIndex: number, toIndex: number) => void;
|
|
1187
|
+
onBlockMove: (move: BlockMove) => void;
|
|
1159
1188
|
onMoveSection?: (sectionId: string) => void;
|
|
1160
1189
|
onAccessChange: (sectionId: string, access: string[]) => void;
|
|
1161
1190
|
onStatusChange: (sectionId: string, status: "draft" | "live" | "archived") => void;
|
|
@@ -1164,17 +1193,19 @@ function EditorContent({
|
|
|
1164
1193
|
viewSections: LoadedSection[] | null;
|
|
1165
1194
|
}) {
|
|
1166
1195
|
const { isEditMode, viewBranch } = useEditorContext();
|
|
1167
|
-
const { openModal } = useEditorModal();
|
|
1196
|
+
const { openModal, closeModal } = useEditorModal();
|
|
1168
1197
|
const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null);
|
|
1169
1198
|
const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
|
|
1170
1199
|
|
|
1171
1200
|
const typeOptions = useMemo(
|
|
1172
1201
|
() =>
|
|
1173
|
-
getAllSections()
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1202
|
+
getAllSections()
|
|
1203
|
+
.filter((def) => def.type !== "spacer")
|
|
1204
|
+
.map((def) => ({
|
|
1205
|
+
type: def.type,
|
|
1206
|
+
label: def.label,
|
|
1207
|
+
icon: def.icon,
|
|
1208
|
+
})),
|
|
1178
1209
|
[],
|
|
1179
1210
|
);
|
|
1180
1211
|
|
|
@@ -1192,19 +1223,42 @@ function EditorContent({
|
|
|
1192
1223
|
useEffect(() => {
|
|
1193
1224
|
return combine(
|
|
1194
1225
|
monitorForElements({
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1226
|
+
canMonitor: ({ source }) => source.data.dragType === "block",
|
|
1227
|
+
onDragStart: () => setPendingInsertIndex(null),
|
|
1228
|
+
onDrop: ({ source, location }) => {
|
|
1229
|
+
const target = location.current.dropTargets[0];
|
|
1230
|
+
if (!target) return;
|
|
1231
|
+
const s = source.data as { blockId: string; containerId: string; index: number };
|
|
1232
|
+
const d = target.data as { dropContainerId: string; index: number; toColumn?: number };
|
|
1233
|
+
if (d.toColumn != null) {
|
|
1234
|
+
onBlockMove({
|
|
1235
|
+
blockId: s.blockId,
|
|
1236
|
+
fromContainerId: s.containerId,
|
|
1237
|
+
fromIndex: s.index,
|
|
1238
|
+
toContainerId: d.dropContainerId,
|
|
1239
|
+
toIndex: 0,
|
|
1240
|
+
toColumn: d.toColumn,
|
|
1241
|
+
});
|
|
1242
|
+
return;
|
|
1198
1243
|
}
|
|
1244
|
+
const edge = extractClosestEdge(target.data);
|
|
1245
|
+
const toIndex = resolveDropIndex(d.index, edge);
|
|
1246
|
+
onBlockMove({
|
|
1247
|
+
blockId: s.blockId,
|
|
1248
|
+
fromContainerId: s.containerId,
|
|
1249
|
+
fromIndex: s.index,
|
|
1250
|
+
toContainerId: d.dropContainerId,
|
|
1251
|
+
toIndex,
|
|
1252
|
+
});
|
|
1199
1253
|
},
|
|
1200
1254
|
}),
|
|
1201
|
-
// Gradually scroll the window when a
|
|
1255
|
+
// Gradually scroll the window when a block drag nears the viewport
|
|
1202
1256
|
// edge, so long reorders don't require repeated drag-and-release
|
|
1203
1257
|
autoScrollWindowForElements({
|
|
1204
|
-
canScroll: ({ source }) => source.data.dragType === "
|
|
1258
|
+
canScroll: ({ source }) => source.data.dragType === "block",
|
|
1205
1259
|
}),
|
|
1206
1260
|
);
|
|
1207
|
-
}, []);
|
|
1261
|
+
}, [onBlockMove]);
|
|
1208
1262
|
|
|
1209
1263
|
return (
|
|
1210
1264
|
<div>
|
|
@@ -1245,6 +1299,8 @@ function EditorContent({
|
|
|
1245
1299
|
dirty={dirtySectionIds.has(section.id)}
|
|
1246
1300
|
index={index}
|
|
1247
1301
|
isLast={index === displaySections.length - 1}
|
|
1302
|
+
containerId={ROOT_CONTAINER_ID}
|
|
1303
|
+
isContainerBlock={section.type === "container"}
|
|
1248
1304
|
definition={definition}
|
|
1249
1305
|
mainStatus={mainIndex?.sections[section.id]?.status ?? null}
|
|
1250
1306
|
contentDiffersFromMain={changedSectionIds.has(section.id) || dirtySectionIds.has(section.id)}
|
|
@@ -1278,6 +1334,42 @@ function EditorContent({
|
|
|
1278
1334
|
onMoveSection={editingEnabled && onMoveSection ? () => onMoveSection(section.id) : undefined}
|
|
1279
1335
|
onRequestInsert={editingEnabled ? (i) => setPendingInsertIndex(i) : undefined}
|
|
1280
1336
|
onDelete={editingEnabled ? () => onDeleteSection(section.id) : undefined}
|
|
1337
|
+
onAddChild={
|
|
1338
|
+
editingEnabled && section.type === "container"
|
|
1339
|
+
? () => {
|
|
1340
|
+
openModal(
|
|
1341
|
+
"Add block",
|
|
1342
|
+
<SectionTypePicker
|
|
1343
|
+
types={childInsertableTypes()}
|
|
1344
|
+
onClose={closeModal}
|
|
1345
|
+
onSelect={(type) => {
|
|
1346
|
+
const def = getSection(type);
|
|
1347
|
+
if (def) {
|
|
1348
|
+
const id =
|
|
1349
|
+
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
1350
|
+
? crypto.randomUUID()
|
|
1351
|
+
: `child-${Date.now()}`;
|
|
1352
|
+
const child = { id, ...(def.defaults() as object) } as Section;
|
|
1353
|
+
const containerSection: Section = {
|
|
1354
|
+
id: section.id,
|
|
1355
|
+
type: "container",
|
|
1356
|
+
content: section.content as Record<string, unknown>,
|
|
1357
|
+
};
|
|
1358
|
+
const childCount =
|
|
1359
|
+
(section.content as { children?: unknown[] }).children?.length ?? 0;
|
|
1360
|
+
const next = insertChildAt(containerSection, child, childCount);
|
|
1361
|
+
onSectionChange(section.id, {
|
|
1362
|
+
...section,
|
|
1363
|
+
content: next.content,
|
|
1364
|
+
} as SectionContent);
|
|
1365
|
+
}
|
|
1366
|
+
closeModal();
|
|
1367
|
+
}}
|
|
1368
|
+
/>,
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
: undefined
|
|
1372
|
+
}
|
|
1281
1373
|
>
|
|
1282
1374
|
<Component
|
|
1283
1375
|
content={section}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { EditorModal } from "./EditorModal";
|
|
3
3
|
import { Button } from "../shared/Button";
|
|
4
|
+
import { Tabs } from "../shared/Tabs";
|
|
4
5
|
import { SiteSettingsViewerAccess } from "./SiteSettingsViewerAccess";
|
|
5
6
|
import { SiteSettingsDisplay } from "./SiteSettingsDisplay";
|
|
6
7
|
import { SiteSettingsUsers } from "./SiteSettingsUsers";
|
|
7
|
-
import { cn } from "../../lib/cn";
|
|
8
8
|
import type { SiteConfig } from "../../schemas/site-config";
|
|
9
9
|
import type { Audience } from "../../auth/types";
|
|
10
10
|
|
|
@@ -25,17 +25,7 @@ interface Props {
|
|
|
25
25
|
currentUser: { email: string; role: "owner" | "editor" } | null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
type TabId = "users" | "viewer-access" | "display";
|
|
29
|
-
|
|
30
28
|
export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigChange, onAudiencesChange, capabilities, currentUser }: Props) {
|
|
31
|
-
const tabs: { id: TabId; label: string; show: boolean }[] = [
|
|
32
|
-
{ id: "users", label: "Users", show: capabilities.userManagement && currentUser?.role === "owner" },
|
|
33
|
-
{ id: "viewer-access", label: "Viewer Access", show: true },
|
|
34
|
-
{ id: "display", label: "Display", show: true },
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
const visibleTabs = tabs.filter((t) => t.show);
|
|
38
|
-
const [activeTab, setActiveTab] = useState<TabId>(visibleTabs[0]?.id ?? "display");
|
|
39
29
|
const [signOutError, setSignOutError] = useState<string | null>(null);
|
|
40
30
|
|
|
41
31
|
async function handleSignOut() {
|
|
@@ -58,49 +48,49 @@ export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigCha
|
|
|
58
48
|
? (currentUser.role === "owner" ? "Owner" : "Editor")
|
|
59
49
|
: null;
|
|
60
50
|
|
|
51
|
+
const allTabs = [
|
|
52
|
+
{
|
|
53
|
+
id: "users",
|
|
54
|
+
label: "Users",
|
|
55
|
+
show: capabilities.userManagement && currentUser?.role === "owner",
|
|
56
|
+
content: (
|
|
57
|
+
<div data-testid="site-settings-tab-panel" className="flex flex-1 flex-col overflow-y-auto px-6 py-4">
|
|
58
|
+
<SiteSettingsUsers currentUser={currentUser} />
|
|
59
|
+
</div>
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "viewer-access",
|
|
64
|
+
label: "Viewer Access",
|
|
65
|
+
show: true,
|
|
66
|
+
content: (
|
|
67
|
+
<div data-testid="site-settings-tab-panel" className="flex flex-1 flex-col overflow-y-auto px-6 py-4">
|
|
68
|
+
<SiteSettingsViewerAccess
|
|
69
|
+
audienceManagement={capabilities.audienceManagement}
|
|
70
|
+
passwordToggle={capabilities.passwordToggle}
|
|
71
|
+
onAudiencesChange={onAudiencesChange}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "display",
|
|
78
|
+
label: "Display",
|
|
79
|
+
show: true,
|
|
80
|
+
content: (
|
|
81
|
+
<div data-testid="site-settings-tab-panel" className="flex flex-1 flex-col overflow-y-auto px-6 py-4">
|
|
82
|
+
<SiteSettingsDisplay siteConfig={siteConfig} onChange={onSiteConfigChange} />
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const tabItems = allTabs.filter((t) => t.show).map(({ id, label, content }) => ({ id, label, content }));
|
|
89
|
+
|
|
61
90
|
return (
|
|
62
91
|
<EditorModal isOpen={isOpen} onClose={onClose} title="Site Settings" size="settings" noPadding>
|
|
63
92
|
<div className="flex flex-1 flex-col overflow-hidden">
|
|
64
|
-
<
|
|
65
|
-
<div className="flex px-6" role="tablist">
|
|
66
|
-
{visibleTabs.map((tab) => (
|
|
67
|
-
<button
|
|
68
|
-
key={tab.id}
|
|
69
|
-
role="tab"
|
|
70
|
-
aria-selected={activeTab === tab.id}
|
|
71
|
-
onClick={() => setActiveTab(tab.id)}
|
|
72
|
-
className={cn(
|
|
73
|
-
"cursor-pointer px-4 py-2 text-sm font-medium border-b-2 -mb-px",
|
|
74
|
-
activeTab === tab.id
|
|
75
|
-
? "border-brand text-brand"
|
|
76
|
-
: "border-transparent text-base-contrast-light hover:text-base-contrast",
|
|
77
|
-
)}
|
|
78
|
-
>
|
|
79
|
-
{tab.label}
|
|
80
|
-
</button>
|
|
81
|
-
))}
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<div
|
|
86
|
-
data-testid="site-settings-tab-panel"
|
|
87
|
-
className="flex flex-1 flex-col overflow-y-auto px-6 py-4"
|
|
88
|
-
>
|
|
89
|
-
{activeTab === "users" && <SiteSettingsUsers currentUser={currentUser} />}
|
|
90
|
-
{activeTab === "viewer-access" && (
|
|
91
|
-
<SiteSettingsViewerAccess
|
|
92
|
-
audienceManagement={capabilities.audienceManagement}
|
|
93
|
-
passwordToggle={capabilities.passwordToggle}
|
|
94
|
-
onAudiencesChange={onAudiencesChange}
|
|
95
|
-
/>
|
|
96
|
-
)}
|
|
97
|
-
{activeTab === "display" && (
|
|
98
|
-
<SiteSettingsDisplay
|
|
99
|
-
siteConfig={siteConfig}
|
|
100
|
-
onChange={onSiteConfigChange}
|
|
101
|
-
/>
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
93
|
+
<Tabs tabs={tabItems} />
|
|
104
94
|
|
|
105
95
|
{currentUser && (
|
|
106
96
|
<div className="flex items-center justify-between gap-3 border-t border-base-200 px-6 py-3">
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { applyBlockMove, type BlockMove, type BlockMoveState } from "../../lib/block-move";
|
|
2
|
+
import type { LoadedSection } from "../../lib/loader";
|
|
3
|
+
import type { SiteIndex } from "../../schemas/site-config";
|
|
4
|
+
import type { SectionContent } from "../../schemas/sections";
|
|
5
|
+
|
|
6
|
+
export interface BlockMoveDispatchDeps {
|
|
7
|
+
getState: () => BlockMoveState;
|
|
8
|
+
setSections: (updater: (prev: LoadedSection[]) => LoadedSection[]) => void;
|
|
9
|
+
setSiteIndex: (updater: (prev: SiteIndex) => SiteIndex) => void;
|
|
10
|
+
persistence: {
|
|
11
|
+
markSectionDirty: (id: string, content: SectionContent) => void;
|
|
12
|
+
markIndexDirty: () => void;
|
|
13
|
+
removeSection: (id: string) => void;
|
|
14
|
+
};
|
|
15
|
+
isRemote: (id: string) => boolean;
|
|
16
|
+
scheduleDelete: (id: string) => void;
|
|
17
|
+
markLocalChanges: () => void;
|
|
18
|
+
/** Latest content for a section id, for markSectionDirty after the move. */
|
|
19
|
+
newSectionContent: (id: string, sections: LoadedSection[]) => SectionContent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function makeBlockMoveDispatch(deps: BlockMoveDispatchDeps) {
|
|
23
|
+
return function onBlockMove(move: BlockMove) {
|
|
24
|
+
const result = applyBlockMove(deps.getState(), move);
|
|
25
|
+
if (!result.changed) return;
|
|
26
|
+
|
|
27
|
+
deps.setSections(() => result.sections);
|
|
28
|
+
deps.setSiteIndex(() => result.index);
|
|
29
|
+
|
|
30
|
+
for (const id of result.effects.deleteSectionIds) {
|
|
31
|
+
if (deps.isRemote(id)) deps.scheduleDelete(id);
|
|
32
|
+
deps.persistence.removeSection(id);
|
|
33
|
+
}
|
|
34
|
+
for (const id of result.effects.markDirtySectionIds) {
|
|
35
|
+
deps.persistence.markSectionDirty(id, deps.newSectionContent(id, result.sections));
|
|
36
|
+
}
|
|
37
|
+
if (result.effects.indexDirty) deps.persistence.markIndexDirty();
|
|
38
|
+
deps.markLocalChanges();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
3
|
+
import { buildBlockDragData, buildBlockDropData, isBlockDragData, canDropBlock } from "../lib/block-dnd";
|
|
4
|
+
|
|
5
|
+
type AllowedEdges = Edge[];
|
|
6
|
+
|
|
7
|
+
export interface UseBlockDndArgs {
|
|
8
|
+
blockId: string;
|
|
9
|
+
containerId: string;
|
|
10
|
+
index: number;
|
|
11
|
+
isContainer: boolean;
|
|
12
|
+
/** ["left","right"] inside a row container, ["top","bottom"] at root */
|
|
13
|
+
allowedEdges: AllowedEdges;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wires a block as a unified drag source + drop target. Manages only this block's
|
|
19
|
+
* local hover edge + dragging flag — the actual move dispatch is centralized in the
|
|
20
|
+
* EditorContent monitor (a later task), so nested targets never double-fire. The edge
|
|
21
|
+
* indicator is shown only when this element is the innermost drop target.
|
|
22
|
+
*
|
|
23
|
+
* Callers must pass a stable `allowedEdges` array (module-level constant or memoized)
|
|
24
|
+
* to avoid re-subscribing on every render.
|
|
25
|
+
*/
|
|
26
|
+
export function useBlockDnd({ blockId, containerId, index, isContainer, allowedEdges, enabled }: UseBlockDndArgs) {
|
|
27
|
+
const dragRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
const handleRef = useRef<HTMLButtonElement>(null);
|
|
29
|
+
const [closestEdge, setClosestEdge] = useState<Edge | null>(null);
|
|
30
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const element = dragRef.current;
|
|
34
|
+
const handle = handleRef.current;
|
|
35
|
+
if (!element || !enabled) return;
|
|
36
|
+
|
|
37
|
+
let cleanup: (() => void) | undefined;
|
|
38
|
+
let cancelled = false;
|
|
39
|
+
|
|
40
|
+
Promise.all([
|
|
41
|
+
import("@atlaskit/pragmatic-drag-and-drop/element/adapter"),
|
|
42
|
+
import("@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"),
|
|
43
|
+
]).then(([{ draggable, dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
|
|
44
|
+
if (cancelled) return;
|
|
45
|
+
|
|
46
|
+
const cleanupDraggable = draggable({
|
|
47
|
+
element,
|
|
48
|
+
dragHandle: handle ?? undefined,
|
|
49
|
+
getInitialData: () => buildBlockDragData({ blockId, containerId, index, isContainer }),
|
|
50
|
+
onGenerateDragPreview: () => {
|
|
51
|
+
element.style.opacity = "0.4";
|
|
52
|
+
requestAnimationFrame(() => { element.style.opacity = ""; });
|
|
53
|
+
},
|
|
54
|
+
onDragStart: () => setIsDragging(true),
|
|
55
|
+
onDrop: () => setIsDragging(false),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const cleanupDrop = dropTargetForElements({
|
|
59
|
+
element,
|
|
60
|
+
canDrop: ({ source }) => isBlockDragData(source.data) && canDropBlock(source.data, containerId),
|
|
61
|
+
getData: ({ input, element: el }) =>
|
|
62
|
+
attachClosestEdge(buildBlockDropData({ dropContainerId: containerId, index }), {
|
|
63
|
+
input, element: el, allowedEdges,
|
|
64
|
+
}),
|
|
65
|
+
onDrag: ({ self, source, location }) => {
|
|
66
|
+
const innermost = location.current.dropTargets[0]?.element === self.element;
|
|
67
|
+
if (!innermost || !isBlockDragData(source.data) || source.data.blockId === blockId) {
|
|
68
|
+
setClosestEdge(null);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
setClosestEdge(extractClosestEdge(self.data));
|
|
72
|
+
},
|
|
73
|
+
onDragLeave: () => setClosestEdge(null),
|
|
74
|
+
onDrop: () => setClosestEdge(null),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
cleanup = () => { cleanupDraggable(); cleanupDrop(); };
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return () => { cancelled = true; cleanup?.(); };
|
|
81
|
+
}, [blockId, containerId, index, isContainer, enabled, allowedEdges]);
|
|
82
|
+
|
|
83
|
+
return { dragRef, handleRef, closestEdge, isDragging };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface UseBlockDropZoneArgs {
|
|
87
|
+
containerId: string;
|
|
88
|
+
index: number;
|
|
89
|
+
/** When set, the zone is a column-targeted drop (ghost/spacer): place at this 1-based column. */
|
|
90
|
+
toColumn?: number;
|
|
91
|
+
allowedEdges: AllowedEdges;
|
|
92
|
+
enabled: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A drop-target-ONLY zone (not draggable) — used for an empty container's first-drop
|
|
97
|
+
* area, where there are no child cells to act as targets. It advertises
|
|
98
|
+
* `{ dropContainerId, index }` and shows an edge indicator only when it is the innermost
|
|
99
|
+
* drop target. The actual move dispatch is centralized in the EditorContent monitor.
|
|
100
|
+
*
|
|
101
|
+
* Callers must pass a stable `allowedEdges` array (module-level constant or memoized).
|
|
102
|
+
*/
|
|
103
|
+
export function useBlockDropZone({ containerId, index, toColumn, allowedEdges, enabled }: UseBlockDropZoneArgs) {
|
|
104
|
+
const dropRef = useRef<HTMLDivElement>(null);
|
|
105
|
+
const [closestEdge, setClosestEdge] = useState<Edge | null>(null);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const element = dropRef.current;
|
|
109
|
+
if (!element || !enabled) return;
|
|
110
|
+
|
|
111
|
+
let cleanup: (() => void) | undefined;
|
|
112
|
+
let cancelled = false;
|
|
113
|
+
|
|
114
|
+
Promise.all([
|
|
115
|
+
import("@atlaskit/pragmatic-drag-and-drop/element/adapter"),
|
|
116
|
+
import("@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"),
|
|
117
|
+
]).then(([{ dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
|
|
118
|
+
if (cancelled) return;
|
|
119
|
+
|
|
120
|
+
cleanup = dropTargetForElements({
|
|
121
|
+
element,
|
|
122
|
+
canDrop: ({ source }) => isBlockDragData(source.data) && canDropBlock(source.data, containerId),
|
|
123
|
+
getData: ({ input, element: el }) =>
|
|
124
|
+
attachClosestEdge(buildBlockDropData({ dropContainerId: containerId, index, toColumn }), {
|
|
125
|
+
input, element: el, allowedEdges,
|
|
126
|
+
}),
|
|
127
|
+
onDrag: ({ self, source, location }) => {
|
|
128
|
+
const innermost = location.current.dropTargets[0]?.element === self.element;
|
|
129
|
+
if (!innermost || !isBlockDragData(source.data)) {
|
|
130
|
+
setClosestEdge(null);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setClosestEdge(extractClosestEdge(self.data));
|
|
134
|
+
},
|
|
135
|
+
onDragLeave: () => setClosestEdge(null),
|
|
136
|
+
onDrop: () => setClosestEdge(null),
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return () => { cancelled = true; cleanup?.(); };
|
|
141
|
+
}, [containerId, index, toColumn, enabled, allowedEdges]);
|
|
142
|
+
|
|
143
|
+
return { dropRef, closestEdge };
|
|
144
|
+
}
|