"use client";
import { Fact, useEntity, useReplicache } from "src/replicache";
import { useUIState } from "src/useUIState";
import { useBlocks } from "src/hooks/queries/useBlocks";
import { useEditorStates } from "src/state/useEditorState";
import { useEntitySetContext } from "components/EntitySetProvider";
import { isTextBlock } from "src/utils/isTextBlock";
import { focusBlock } from "src/utils/focusBlock";
import { elementId } from "src/utils/elementId";
import { generateKeyBetween } from "fractional-indexing";
import { v7 } from "uuid";
import { Block } from "./Block";
import { useEffect } from "react";
import { addShortcut } from "src/shortcuts";
import { useHandleDrop } from "./useHandleDrop";
export function Blocks(props: { entityID: string }) {
let rep = useReplicache();
let isPageFocused = useUIState((s) => {
let focusedElement = s.focusedEntity;
let focusedPageID =
focusedElement?.entityType === "page"
? focusedElement.entityID
: focusedElement?.parent;
return focusedPageID === props.entityID;
});
let { permissions } = useEntitySetContext();
let entity_set = useEntitySetContext();
let blocks = useBlocks(props.entityID);
let foldedBlocks = useUIState((s) => s.foldedBlocks);
useEffect(() => {
if (!isPageFocused) return;
return addShortcut([
{
altKey: true,
metaKey: true,
key: "ArrowUp",
shift: true,
handler: () => {
let allParents = blocks.reduce((acc, block) => {
if (!block.listData) return acc;
block.listData.path.forEach((p) =>
!acc.includes(p.entity) ? acc.push(p.entity) : null,
);
return acc;
}, [] as string[]);
useUIState.setState((s) => {
let foldedBlocks = [...s.foldedBlocks];
allParents.forEach((p) => {
if (!foldedBlocks.includes(p)) foldedBlocks.push(p);
});
return { foldedBlocks };
});
},
},
{
altKey: true,
metaKey: true,
key: "ArrowDown",
shift: true,
handler: () => {
let allParents = blocks.reduce((acc, block) => {
if (!block.listData) return acc;
block.listData.path.forEach((p) =>
!acc.includes(p.entity) ? acc.push(p.entity) : null,
);
return acc;
}, [] as string[]);
useUIState.setState((s) => {
let foldedBlocks = [...s.foldedBlocks].filter(
(f) => !allParents.includes(f),
);
return { foldedBlocks };
});
},
},
]);
}, [blocks, isPageFocused]);
let lastRootBlock = blocks.findLast(
(f) => !f.listData || f.listData.depth === 1,
);
let lastVisibleBlock = blocks.findLast(
(f) =>
!f.listData ||
!f.listData.path.find(
(path) => foldedBlocks.includes(path.entity) && f.value !== path.entity,
),
);
return (
{
if (!permissions.write) return;
if (useUIState.getState().selectedBlocks.length > 1) return;
if (e.target === e.currentTarget) {
if (
!lastVisibleBlock ||
(lastVisibleBlock.type !== "text" &&
lastVisibleBlock.type !== "heading")
) {
let newEntityID = v7();
await rep.rep?.mutate.addBlock({
parent: props.entityID,
factID: v7(),
permission_set: entity_set.set,
type: "text",
position: generateKeyBetween(
lastRootBlock?.position || null,
null,
),
newEntityID,
});
setTimeout(() => {
document
.getElementById(elementId.block(newEntityID).text)
?.focus();
}, 10);
} else {
lastVisibleBlock && focusBlock(lastVisibleBlock, { type: "end" });
}
}
}}
>
{blocks
.filter(
(f) =>
!f.listData ||
!f.listData.path.find(
(path) =>
foldedBlocks.includes(path.entity) && f.value !== path.entity,
),
)
.map((f, index, arr) => {
let nextBlock = arr[index + 1];
let depth = f.listData?.depth || 1;
let nextDepth = nextBlock?.listData?.depth || 1;
let nextPosition: string | null;
if (depth === nextDepth) nextPosition = nextBlock?.position || null;
else nextPosition = null;
return (
);
})}
);
}
function NewBlockButton(props: { lastBlock: Block | null; entityID: string }) {
let { rep } = useReplicache();
let entity_set = useEntitySetContext();
let editorState = useEditorStates((s) =>
props.lastBlock?.type === "text"
? s.editorStates[props.lastBlock.value]
: null,
);
if (!entity_set.permissions.write) return null;
if (
(props.lastBlock?.type === "text" || props.lastBlock?.type === "heading") &&
(!editorState?.editor || editorState.editor.doc.content.size <= 2)
)
return null;
return (
{
let newEntityID = v7();
await rep?.mutate.addBlock({
parent: props.entityID,
type: "text",
factID: v7(),
permission_set: entity_set.set,
position: generateKeyBetween(
props.lastBlock?.position || null,
null,
),
newEntityID,
});
setTimeout(() => {
document.getElementById(elementId.block(newEntityID).text)?.focus();
}, 10);
}}
>
{/* this is here as a fail safe, in case a new page is created and there are no blocks in it yet,
we render a newblockbutton with a textblock-like placeholder instead of a proper first block. */}
{!props.lastBlock ? (
write something...
) : (
" "
)}
);
}
const BlockListBottom = (props: {
lastRootBlock: Block | undefined;
lastVisibleBlock: Block | undefined;
entityID: string;
}) => {
let { rep } = useReplicache();
let entity_set = useEntitySetContext();
let handleDrop = useHandleDrop({
parent: props.entityID,
position: props.lastRootBlock?.position || null,
nextPosition: null,
});
if (!entity_set.permissions.write) return;
return (
{
let newEntityID = v7();
if (
// if the last visible(not-folded) block is a text block, focus it
props.lastRootBlock &&
props.lastVisibleBlock &&
isTextBlock[props.lastVisibleBlock.type]
) {
focusBlock(
{ ...props.lastVisibleBlock, type: "text" },
{ type: "end" },
);
} else {
// else add a new text block at the end and focus it
rep?.mutate.addBlock({
permission_set: entity_set.set,
factID: v7(),
parent: props.entityID,
type: "text",
position: generateKeyBetween(
props.lastRootBlock?.position || null,
null,
),
newEntityID,
});
setTimeout(() => {
document.getElementById(elementId.block(newEntityID).text)?.focus();
}, 10);
}
}}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onDrop={handleDrop}
/>
);
};