Something went wrong. Try again.
leaflet fork for copy edit / bug fixes forked from leaflet.pub/leaflet
Something went wrong. Try again.
2.9 kB · 91 lines
TSX
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192import React, { useState } from "react";import { useEntity, useReplicache } from "src/replicache";import { localImages } from "src/utils/addImage";import { Modal } from "components/Modal";import { ButtonPrimary, ButtonTertiary } from "components/Buttons";
// Alt-text editor shown as a modal (desktop) or mobile sheet. Does not// autosave — the value is committed on Save or Enter. Shared by ImageBlock,// the gallery's per-image button, and the gallery lightbox.export function ImageAltModal(props: { entityID: string; trigger: React.ReactNode; title: string;}) { let [altEditorOpen, setAltEditorOpen] = useState(false);
// Mount fresh on open so the textarea seeds from the current alt text. return ( <Modal sheetOnMobile asChild trigger={props.trigger} title={props.title} open={altEditorOpen} onOpenChange={setAltEditorOpen} > <ImageAltModalContent entityID={props.entityID} onClose={() => setAltEditorOpen(false)} /> </Modal> );}
function ImageAltModalContent(props: { entityID: string; onClose: () => void;}) { let { rep } = useReplicache(); let image = useEntity(props.entityID, "block/image"); let altText = useEntity(props.entityID, "image/alt")?.data.value ?? ""; // `draft` stays null until the user edits, so the textarea reflects the live // alt text once Replicache's subscription resolves — the first render returns // a stale page-load snapshot that would otherwise get frozen into state. let [draft, setDraft] = useState<string | null>(null); let value = draft ?? altText; let src = image ? localImages.get(image.data.src) ?? image.data.src : null;
let save = async (next: string = value) => { await rep?.mutate.assertFact({ entity: props.entityID, attribute: "image/alt", data: { type: "string", value: next }, }); props.onClose(); };
return ( <div className="flex flex-col gap-3 sm:w-[360px]"> {src && ( <img src={src} alt={altText} className="max-w-full w-fit self-center max-h-[40vh] object-contain rounded-md" /> )} <textarea autoFocus className="input-with-border w-full resize-none min-h-[64px] p-2" placeholder="Describe this image…" value={value} onChange={(e) => setDraft(e.currentTarget.value)} onKeyDown={(e) => { // Enter submits; Shift+Enter inserts a newline. if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); save(); } }} /> <div className="flex gap-2 justify-end"> <ButtonTertiary compact onClick={() => save("")}> Remove </ButtonTertiary> <ButtonPrimary className="self-end" compact onClick={() => save()}> Save </ButtonPrimary> </div> </div> );}