Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
301
packages/plugins/forms/src/astro/FormEmbed.astro
Normal file
301
packages/plugins/forms/src/astro/FormEmbed.astro
Normal file
@@ -0,0 +1,301 @@
|
||||
---
|
||||
/**
|
||||
* Form embed component for Portable Text blocks.
|
||||
*
|
||||
* Server-renders the full form with all pages as <fieldset> elements.
|
||||
* Without JavaScript, all pages are visible as one long form.
|
||||
* The client-side script enhances with multi-page navigation, AJAX, etc.
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
node: { formId: string };
|
||||
}
|
||||
|
||||
interface FormField {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
helpText?: string;
|
||||
required: boolean;
|
||||
validation?: {
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
pattern?: string;
|
||||
patternMessage?: string;
|
||||
accept?: string;
|
||||
maxFileSize?: number;
|
||||
};
|
||||
options?: Array<{ label: string; value: string }>;
|
||||
defaultValue?: string;
|
||||
width: "full" | "half";
|
||||
condition?: { field: string; op: string; value?: string };
|
||||
}
|
||||
|
||||
interface FormPage {
|
||||
title?: string;
|
||||
fields: FormField[];
|
||||
}
|
||||
|
||||
interface FormDefinition {
|
||||
name: string;
|
||||
slug: string;
|
||||
pages: FormPage[];
|
||||
settings: {
|
||||
spamProtection: string;
|
||||
submitLabel: string;
|
||||
nextLabel?: string;
|
||||
prevLabel?: string;
|
||||
};
|
||||
status: string;
|
||||
_turnstileSiteKey?: string | null;
|
||||
}
|
||||
|
||||
const { node } = Astro.props;
|
||||
const formId = node.formId;
|
||||
|
||||
// Fetch form definition server-side
|
||||
const response = await fetch(
|
||||
new URL("/_emdash/api/plugins/emdash-forms/definition", Astro.url),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: formId }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) return;
|
||||
|
||||
const form = (await response.json()) as FormDefinition;
|
||||
if (!form || form.status !== "active") return;
|
||||
|
||||
const submitUrl = `/_emdash/api/plugins/emdash-forms/submit`;
|
||||
const isMultiPage = form.pages.length > 1;
|
||||
const turnstileSiteKey = form._turnstileSiteKey;
|
||||
const hasFiles = form.pages.some((p: FormPage) =>
|
||||
p.fields.some((f: FormField) => f.type === "file")
|
||||
);
|
||||
|
||||
/** Generate an element ID for a field */
|
||||
function fieldId(name: string): string {
|
||||
return `${formId}-${name}`;
|
||||
}
|
||||
---
|
||||
|
||||
<form
|
||||
class="ec-form"
|
||||
method="POST"
|
||||
action={submitUrl}
|
||||
enctype={hasFiles ? "multipart/form-data" : undefined}
|
||||
data-form-id={formId}
|
||||
data-ec-form
|
||||
data-pages={isMultiPage ? form.pages.length : undefined}
|
||||
>
|
||||
{
|
||||
form.pages.map((page: FormPage, pageIndex: number) => (
|
||||
<fieldset
|
||||
class="ec-form-page"
|
||||
data-page={pageIndex}
|
||||
aria-label={page.title || `Page ${pageIndex + 1}`}
|
||||
>
|
||||
{isMultiPage && page.title && (
|
||||
<legend class="ec-form-page-title">{page.title}</legend>
|
||||
)}
|
||||
|
||||
{page.fields.map((field: FormField) => (
|
||||
<div
|
||||
class:list={[
|
||||
"ec-form-field",
|
||||
`ec-form-field--${field.type}`,
|
||||
field.width === "half" && "ec-form-field--half",
|
||||
]}
|
||||
data-condition={
|
||||
field.condition ? JSON.stringify(field.condition) : undefined
|
||||
}
|
||||
>
|
||||
{field.type !== "hidden" && field.type !== "checkbox" && (
|
||||
<label class="ec-form-label" for={fieldId(field.name)}>
|
||||
{field.label}
|
||||
{field.required && (
|
||||
<span class="ec-form-required" aria-label="required">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
{[
|
||||
"text",
|
||||
"email",
|
||||
"tel",
|
||||
"url",
|
||||
"number",
|
||||
"date",
|
||||
"hidden",
|
||||
].includes(field.type) && (
|
||||
<input
|
||||
type={field.type as astroHTML.JSX.HTMLInputTypeAttribute}
|
||||
class={field.type !== "hidden" ? "ec-form-input" : undefined}
|
||||
id={fieldId(field.name)}
|
||||
name={field.name}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
minlength={field.validation?.minLength}
|
||||
maxlength={field.validation?.maxLength}
|
||||
min={field.validation?.min}
|
||||
max={field.validation?.max}
|
||||
pattern={field.validation?.pattern}
|
||||
value={field.defaultValue}
|
||||
/>
|
||||
)}
|
||||
{field.type === "file" && (
|
||||
<input
|
||||
type="file"
|
||||
class="ec-form-input"
|
||||
id={fieldId(field.name)}
|
||||
name={field.name}
|
||||
required={field.required}
|
||||
accept={field.validation?.accept}
|
||||
/>
|
||||
)}
|
||||
{field.type === "textarea" && (
|
||||
<textarea
|
||||
class="ec-form-input"
|
||||
id={fieldId(field.name)}
|
||||
name={field.name}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
minlength={field.validation?.minLength}
|
||||
maxlength={field.validation?.maxLength}
|
||||
>
|
||||
{field.defaultValue || ""}
|
||||
</textarea>
|
||||
)}
|
||||
{field.type === "select" && (
|
||||
<select
|
||||
class="ec-form-input"
|
||||
id={fieldId(field.name)}
|
||||
name={field.name}
|
||||
required={field.required}
|
||||
>
|
||||
{(field.options || []).map((o) => (
|
||||
<option
|
||||
value={o.value}
|
||||
selected={o.value === field.defaultValue}
|
||||
>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{field.type === "radio" && (
|
||||
<fieldset class="ec-form-radio-group" role="radiogroup">
|
||||
{(field.options || []).map((o) => (
|
||||
<label class="ec-form-radio-label">
|
||||
<input
|
||||
type="radio"
|
||||
name={field.name}
|
||||
value={o.value}
|
||||
checked={o.value === field.defaultValue}
|
||||
required={field.required}
|
||||
/>{" "}
|
||||
{o.label}
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
)}
|
||||
{field.type === "checkbox" && (
|
||||
<label class="ec-form-checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="ec-form-input"
|
||||
id={fieldId(field.name)}
|
||||
name={field.name}
|
||||
value={field.defaultValue || "1"}
|
||||
required={field.required}
|
||||
/>{" "}
|
||||
{field.label}
|
||||
</label>
|
||||
)}
|
||||
{field.type === "checkbox-group" && (
|
||||
<fieldset class="ec-form-checkbox-group">
|
||||
{(field.options || []).map((o) => (
|
||||
<label class="ec-form-checkbox-label">
|
||||
<input type="checkbox" name={field.name} value={o.value} />{" "}
|
||||
{o.label}
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
)}
|
||||
{field.helpText && (
|
||||
<span class="ec-form-help">{field.helpText}</span>
|
||||
)}
|
||||
<span
|
||||
class="ec-form-error"
|
||||
data-error-for={field.name}
|
||||
aria-live="polite"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</fieldset>
|
||||
))
|
||||
}
|
||||
|
||||
{
|
||||
form.settings.spamProtection === "honeypot" && (
|
||||
<div
|
||||
class="ec-form-field"
|
||||
style="position:absolute;left:-9999px;"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<label for={`${formId}-_hp`}>Leave blank</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`${formId}-_hp`}
|
||||
name="_hp"
|
||||
tabindex="-1"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
form.settings.spamProtection === "turnstile" && turnstileSiteKey && (
|
||||
<div
|
||||
class="ec-form-turnstile"
|
||||
data-ec-turnstile
|
||||
data-sitekey={turnstileSiteKey}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<input type="hidden" name="formId" value={formId} />
|
||||
|
||||
<div class="ec-form-nav">
|
||||
<button type="button" class="ec-form-prev" data-ec-prev hidden>
|
||||
{form.settings.prevLabel || "Previous"}
|
||||
</button>
|
||||
<button type="button" class="ec-form-next" data-ec-next hidden>
|
||||
{form.settings.nextLabel || "Next"}
|
||||
</button>
|
||||
<button type="submit" class="ec-form-submit">
|
||||
{form.settings.submitLabel || "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{
|
||||
isMultiPage && (
|
||||
<div class="ec-form-progress" data-ec-progress aria-live="polite" />
|
||||
)
|
||||
}
|
||||
|
||||
<div class="ec-form-status" data-form-status aria-live="polite"></div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
import { initForms } from "@emdash-cms/plugin-forms/client";
|
||||
initForms();
|
||||
</script>
|
||||
Reference in New Issue
Block a user