import * as S from "./format.js";
import { h } from "./tree.js";
export function field(described, written, form, view) {
const id = `${form.id}-${described.name}`;
const error = form.submitted?.errors?.[described.name];
const posted = form.submitted && !["password", "file"].includes(described.control);
const source = posted
? { posted: true, value: form.submitted.input[described.name] }
: { posted: false, present: "value" in written, value: written.value };
const control = {
id,
name: described.name,
describedBy: `${id}-error`,
required: described.required,
constraints: described.constraints,
invalid: error !== undefined,
};
const hidden = described.attributes.some(([name, value]) => name === "type" && value === "hidden");
return h(
"div",
{ class: "sofie-field" },
hidden ? null : h("label", { for: id }, written.label ?? described.label ?? ""),
controlOf(described, written, control, source, view),
h("p", { class: "sofie-field-error", id: `${id}-error`, "aria-live": "polite" }, error ?? ""),
);
}
function controlOf(described, written, control, source, view) {
switch (described.control) {
case "password":
return input([["type", "password"]], control, source);
case "file":
return input([["type", "file"]], control, source);
case "textarea":
return h("textarea", attributes(control, ""), S.text(source.value));
case "checkbox":
return checkbox(control, source);
case "select":
return select(described, written, control, source, view);
case "money":
return moneyInput(written, control, source);
case "percentage":
return percentageInput(control, source);
case "parts":
return parts(described, control, source);
default:
return input(described.attributes, control, source, view);
}
}
function attributes(control, suffix, identity = { id: control.id }, constraints = control.constraints) {
const written = { ...identity, name: control.name + suffix, "aria-describedby": control.describedBy };
if (control.required) written.required = true;
for (const [name, bound] of constraints) written[name] = String(bound);
if (control.invalid) written["aria-invalid"] = "true";
return written;
}
function partAttributes(control, part, first) {
return attributes(control, `.${part.name}`, first ? { id: control.id } : { "aria-label": part.label }, []);
}
function instantControl(value, view) {
const written = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::(\d{2}(?:\.(\d+))?))?Z$/.exec(value);
if (!written) return null;
const instant = new Date(value);
if (Number.isNaN(instant.getTime())) return null;
const minute = S.wallClock(view.conventions, instant);
const seconds = written[1] ?? "00";
const offset = S.repeatedOffset(view.conventions, instant);
if (offset !== null) return { type: "text", value: `${minute}:${seconds}${offset}` };
if ((written[2] ?? "").length > 3) {
return { type: "text", value: `${minute}:${seconds}${S.offsetOf(view.conventions, instant)}` };
}
return /^00(\.0*)?$/.test(seconds) ? { value: minute } : { step: "any", value: `${minute}:${seconds}` };
}
function postedInstantControl(text, view) {
const wallClock = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(?:\.(\d+))?)?$/.exec(text);
if (!wallClock) return { type: "text", value: text };
const fraction = wallClock[2] ?? "";
if (fraction.length > 3 || S.repeats(view.conventions, text)) return { type: "text", value: text };
return wallClock[1] ? { step: "any", value: text } : { value: text };
}
function instantControlOf(inputAttributes, source, view) {
const isDateTime = inputAttributes.some(([name, kind]) => name === "type" && kind === "datetime-local");
if (!isDateTime || !S.knowsZone(view.conventions)) return null;
if (source.posted) return source.value === undefined ? null : postedInstantControl(S.text(source.value), view);
return source.present ? instantControl(S.text(source.value), view) : null;
}
function input(inputAttributes, control, source, view) {
const written = Object.fromEntries(inputAttributes);
const instant = instantControlOf(inputAttributes, source, view);
if (instant?.type) written.type = instant.type;
if (instant?.step) written.step = instant.step;
Object.assign(written, attributes(control, ""));
if (source.posted ? source.value !== undefined : source.present) {
written.value = instant?.value ?? S.text(source.value);
}
if (written.type === "datetime-local" && !S.knowsZone(view.conventions)) {
written.type = "text";
written.placeholder = S.offsetPlaceholder;
}
return h("input", written);
}
function checkbox(control, source) {
const checked = source.posted ? source.value === "true" : source.value === true;
const box = { type: "checkbox", ...attributes(control, ""), value: "true" };
if (checked) box.checked = true;
return [h("input", { type: "hidden", name: control.name, value: "false" }), h("input", box)];
}
function choiceOf(value) {
return typeof value === "number" || typeof value === "string" ? S.text(value) : null;
}
function option(value, label, selected) {
const written = { value };
if (selected === value) written.selected = true;
return h("option", written, label);
}
function select(described, written, control, source, view) {
const selected = choiceOf(source.value);
const options = written.options;
const enumeration = typeof options === "string" ? view.enums?.[options] ?? [] : [];
const choices = !("options" in written)
? described.options
: Array.isArray(options)
? options.map(choiceOf).filter((choice) => choice !== null)
: [];
return h(
"select",
attributes(control, ""),
control.required ? null : h("option", { value: "" }),
enumeration.map((variant) => option(variant.name, variant.label, selected)),
choices.map((choice) => option(choice, choice, selected)),
);
}
function moneyInput(written, control, source) {
const amount = S.moneyOf(written.value);
const code = amount?.code ?? (typeof written.currency === "string" ? written.currency : null);
const typed = (part) => (source.posted ? S.text(source.value?.[part]) : null);
const amountInput = { type: "text", inputmode: "decimal", ...attributes(control, ".amount") };
if (typed("amount") !== null) {
if (typed("amount")) amountInput.value = typed("amount");
} else if (amount) {
amountInput.value = S.major(amount);
}
return h("div", { class: "sofie-money-input" }, h("input", amountInput), currency(control.name, code, typed("currency")));
}
function currency(name, code, typed) {
if (!code) {
const typedCurrency = {
type: "text",
class: "sofie-currency",
maxlength: "3",
"aria-label": "Currency",
name: `${name}.currency`,
};
if (typed) typedCurrency.value = typed;
return h("input", typedCurrency);
}
return [
h("span", { class: "sofie-currency" }, code),
h("input", { type: "hidden", name: `${name}.currency`, value: code }),
];
}
function percentageInput(control, source) {
const written = { type: "text", inputmode: "decimal", ...attributes(control, "") };
if (!source.posted && Number.isInteger(source.value)) {
const magnitude = Math.abs(source.value);
const sign = source.value < 0 ? "-" : "";
written.value = `${sign}${Math.floor(magnitude / 100)}.${String(magnitude % 100).padStart(2, "0")}`;
} else if (source.value !== null && source.value !== undefined) {
written.value = S.text(source.value);
}
return h("div", { class: "sofie-percentage-input" }, h("input", written), h("span", { class: "sofie-unit" }, "%"));
}
function parts(described, control, source) {
return h(
"div",
{ class: "sofie-parts" },
described.parts.map((part, index) => {
const value = source.value?.[part.name];
const written = partAttributes(control, part, index === 0);
if (part.options.length === 0) {
const partInput = { ...Object.fromEntries(part.attributes), ...written };
if (value !== null && value !== undefined) partInput.value = S.text(value);
return h("input", partInput);
}
const selected = typeof value === "string" ? value : null;
return h(
"select",
written,
control.required ? null : h("option", { value: "" }),
part.options.map((choice) => option(choice, choice, selected)),
);
}),
);
}