import { useEntitySetContext } from "components/EntitySetProvider";
import { useCallback, useEffect, useState } from "react";
import { useEntity, useReplicache } from "src/replicache";
import { useIsBlockSelected } from "src/useUIState";
import { BlockProps, BlockLayout } from "./Block";
import { addBlockBelow } from "src/utils/addBlockBelow";
import { useSmoker } from "components/Toast";
import { Separator } from "components/Layout";
import { Input } from "components/Input";
import { isUrl } from "src/utils/isURL";
import { focusBlock } from "src/utils/focusBlock";
import { ButtonPrimary } from "components/Buttons";
import { BlockButtonSmall } from "components/Icons/BlockButtonSmall";
import { CheckTiny } from "components/Icons/CheckTiny";
import { LinkSmall } from "components/Icons/LinkSmall";
import { ToggleGroup } from "components/ToggleGroup";
import {
AlignCenterSmall,
AlignJustifiedSmall,
AlignLeftSmall,
AlignRightSmall,
} from "components/Toolbar/TextAlignmentToolbar";
import { BlockSettings } from "./SettingsTriggerButton";
// Without a scheme the link would resolve relative to the post.
const normalizeButtonUrl = (url: string) =>
url.startsWith("http") || url.startsWith("mailto") || url.startsWith("tel:")
? url
: `https://${url}`;
export const ButtonBlock = (props: BlockProps & { preview?: boolean }) => {
let { permissions } = useEntitySetContext();
let text = useEntity(props.entityID, "button/text");
let url = useEntity(props.entityID, "button/url");
let isSelected = useIsBlockSelected(props.entityID);
let alignment = useEntity(props.entityID, "block/text-alignment")?.data.value;
if (!url) {
// Previews are inert; never mount the settings form in one.
if (!permissions.write || props.preview) return null;
return ;
}
return (
) : undefined
}
>
{text?.data.value}
);
};
const ButtonBlockSettings = (props: BlockProps) => {
let { rep, undoManager } = useReplicache();
let smoker = useSmoker();
let entity_set = useEntitySetContext();
let isSelected = useIsBlockSelected(props.entityID);
let [textValue, setTextValue] = useState("");
let [urlValue, setUrlValue] = useState("");
let text = textValue;
let url = urlValue;
let alignment = useEntity(props.entityID, "block/text-alignment")?.data.value;
let submit = async () => {
await undoManager.withUndoGroup(async () => {
let entity = props.entityID;
if (!entity && rep) {
entity = await addBlockBelow(rep, {
parent: props.parent,
position: props.position,
nextPosition: props.nextPosition,
permission_set: entity_set.set,
type: "card",
});
}
url = normalizeButtonUrl(urlValue);
// these mutations = simpler subset of addLinkBlock
if (!rep) return;
await rep.mutate.assertFact({
entity: entity,
attribute: "block/type",
data: { type: "block-type-union", value: "button" },
});
await rep?.mutate.assertFact({
entity: entity,
attribute: "button/text",
data: {
type: "string",
value: text,
},
});
await rep?.mutate.assertFact({
entity: entity,
attribute: "button/url",
data: {
type: "string",
value: url,
},
});
let textEntity = await addBlockBelow(rep, {
parent: props.parent,
position: props.position,
nextPosition: props.nextPosition,
permission_set: entity_set.set,
type: "text",
});
focusBlock(
{
entityID: textEntity,
type: "text",
parent: props.parent,
},
{ type: "start" },
);
});
};
return (
{text !== "" ? text : "Button"}
);
};
type Alignment = "left" | "center" | "right" | "justify";
function ButtonBlockOptions(props: { entityID: string }) {
let { rep } = useReplicache();
let smoker = useSmoker();
let text = useEntity(props.entityID, "button/text")?.data.value ?? "";
let url = useEntity(props.entityID, "button/url")?.data.value ?? "";
let alignment: Alignment =
useEntity(props.entityID, "block/text-alignment")?.data.value ?? "center";
// Drafts are committed on blur/Enter so each edit is one undo step.
let [textDraft, setTextDraft] = useState(text);
let [urlDraft, setUrlDraft] = useState(url);
useEffect(() => setTextDraft(text), [text]);
useEffect(() => setUrlDraft(url), [url]);
let commitText = () => {
if (textDraft === text) return;
if (!textDraft.trim()) {
setTextDraft(text);
return;
}
rep?.mutate.assertFact({
entity: props.entityID,
attribute: "button/text",
data: { type: "string", value: textDraft },
});
};
let commitUrl = (e: { currentTarget: Element }) => {
if (urlDraft === url) return;
if (!urlDraft || !isUrl(urlDraft)) {
let rect = e.currentTarget.getBoundingClientRect();
smoker({
error: true,
text: urlDraft ? "invalid url!" : "missing url!",
position: { y: rect.top, x: rect.left + 12 },
});
setUrlDraft(url);
return;
}
rep?.mutate.assertFact({
entity: props.entityID,
attribute: "button/url",
data: { type: "string", value: normalizeButtonUrl(urlDraft) },
});
};
return (
Alignment
value={alignment}
background="light"
onChange={(value) =>
rep?.mutate.assertFact({
entity: props.entityID,
attribute: "block/text-alignment",
data: { type: "text-alignment-type-union", value },
})
}
options={[
{ value: "left", label: },
{
value: "center",
label: ,
},
{ value: "right", label: },
{
value: "justify",
label: ,
},
]}
/>
);
}