{# macros/forms.html - Form component macros for APEP. Import with: {% from "apep/macros/forms.html" import input, checkbox, radio, dropdown, date_picker, toggle, range_slider, file_upload %} Macros: input(name, label, type, allowed_chars, validate, placeholder, value, required, hint, resize) checkbox(name, options, hidden_name) radio(name, options, selected) dropdown(name, label, options, selected, required) date_picker(name, id, format, display, value, required, min_date, max_date, placeholder) toggle(name, label, checked, disabled, hint) range_slider(name, label, min, max, value, step, unit, hint) file_upload(name, label, url, accept, multiple, required, hint) CSS dependency: forms.css (loaded via ApepLoader.register("forms.css")) Author: sora7672 #} {% from "apep/macros/helper.html" import alert %} {# Renders a labeled text, email, password input, or resizable textarea with optional validation, character filtering, hint text, and inline error state. When type is "email", allowed_chars is ignored and email format validation is applied automatically regardless of the validate param. When resize is set, a is rendered instead of . @param name {string} - field name attribute, also used to build the id and error element id @param label {string} - visible label text @param type {string} - "text" | "email" | "password", default "text". Ignored when resize is set. @param allowed_chars {list} - list of allowed char ranges, e.g. ["a-z", "A-Z", "0-9"]. Ignored when type is "email" or resize is set. @param validate {string} - pass "email" to enforce email format on a text field explicitly. Automatically set when type is "email". Ignored when resize is set. @param placeholder {string} - placeholder text, default "" @param value {string} - prefilled value, default "" @param required {bool} - adds required and aria-required attributes, marks label with form-label-required class, default false @param hint {string} - muted helper text rendered below the input, optional @param resize {string} - renders a instead of . Allowed: "vertical" | "horizontal" | "both" | "none". Optional. #} {% macro input(name, label, type="text", allowed_chars=None, validate=None, placeholder="", value="", required=False, hint=None, resize=None) %} {% if not name %} {{ alert("input: name is required", "Pass a non-empty string as the first argument.") }} {% elif not label %} {{ alert("input: label is required", "Pass a non-empty string as the second argument.") }} {% elif resize is not none and resize not in ["vertical", "horizontal", "both", "none"] %} {{ alert("input: invalid resize \"" ~ resize ~ "\"", "Allowed values: \"vertical\", \"horizontal\", \"both\", \"none\".") }} {% elif resize is none and type not in ["text", "email", "password"] %} {{ alert("input: invalid type \"" ~ type ~ "\"", "Allowed values: \"text\", \"email\", \"password\".") }} {% else %} {% set _validate = "email" if type == "email" else validate %} {{ label }} {% if resize is not none %} {{ value }} {% else %} {% endif %} {% if hint %} {{ hint }} {% endif %} {% endif %} {% endmacro %} {# Renders a group of checkboxes. Each row is fully clickable, not just the label. Optionally writes a comma-joined string of all selected values into a hidden input, useful when the consuming backend expects a single field instead of repeated keys. @param name {string} - base name attribute used for all checkbox inputs @param options {list} - list of {label, value, checked} dicts. value falls back to label when omitted. checked defaults to false when omitted. @param hidden_name {string} - if provided, renders a hidden input with this name that is kept in sync with the selected values via checkbox.js. Optional. #} {% macro checkbox(name, options, hidden_name=None) %} {% if not name %} {{ alert("checkbox: name is required", "Pass a non-empty string as the first argument.") }} {% elif not options %} {{ alert("checkbox: options is required", "Pass a list of {label, value} dicts.") }} {% else %} {% for option in options %} {% set opt_value = option.value if option.value is defined else option.label %} {% set is_checked = option.checked is defined and option.checked %} {{ option.label }} {% endfor %} {% if hidden_name %} {% endif %} {% endif %} {% endmacro %} {# Renders a group of radio buttons. Each row is fully clickable, not just the label. Each option also gets a hidden input that is enabled only when its radio is selected, so the true value is always submitted even if the browser strips radio names on submit. The selected param takes priority over option.checked when both are present. @param name {string} - input name attribute shared by all radio inputs in the group @param options {list} - list of {label, value, checked} dicts. value falls back to label when omitted. checked defaults to false when omitted. @param selected {string} - pre-selected value. Takes priority over option.checked. Optional. #} {% macro radio(name, options, selected=None) %} {% if not name %} {{ alert("radio: name is required", "Pass a non-empty string as the first argument.") }} {% elif not options %} {{ alert("radio: options is required", "Pass a list of {label, value} dicts.") }} {% else %} {% for option in options %} {% set opt_value = option.value if option.value is defined else option.label %} {% if selected %} {% set is_checked = (selected == opt_value) %} {% else %} {% set is_checked = (option.checked is defined and option.checked) %} {% endif %} {{ option.label }} {% endfor %} {% endif %} {% endmacro %} {# Renders a labeled native dropdown. Display labels are shown in the select element; option values are used as the submitted form value directly. A placeholder "Please select" option is always prepended with an empty value. @param name {string} - select name attribute, also used to build the id and error element id @param label {string} - visible label text @param options {list} - list of {label, value} dicts. value falls back to label when omitted. @param selected {string} - pre-selected value matched against option values. Optional. @param required {bool} - adds required and aria-required attributes, marks label with form-label-required class, default false #} {% macro dropdown(name, label, options, selected=None, required=False) %} {% if not name %} {{ alert("dropdown: name is required", "Pass a non-empty string as the first argument.") }} {% elif not label %} {{ alert("dropdown: label is required", "Pass a non-empty string as the second argument.") }} {% elif not options %} {{ alert("dropdown: options is required", "Pass a list of {label, value} dicts.") }} {% else %} {{ label }} Please select {% for option in options %} {% set opt_value = option.value if option.value is defined else option.label %} {{ option.label }} {% endfor %} {% endif %} {% endmacro %} {# Renders a styled date picker with a text input, calendar icon button, and an overlay panel containing year/month navigation and a day grid. The overlay supports quick-select grids for year (5x5) and month (3x4). Validation and formatting are handled by date_picker.js. @param name {string} - input name attribute for form submit @param id {string} - explicit id for the input element. Falls back to name when omitted. @param format {string} - date format token string, e.g. "yyyy.mm.dd". Default "yyyy.mm.dd". Shown as placeholder when placeholder is not set. @param display {string} - display mode passed to date_picker.js. Default "readable". @param value {string} - prefilled value matching the chosen format. Default "". @param required {bool} - adds required attribute to the input. Default false. @param min_date {string} - earliest selectable date in the chosen format. Default "". @param max_date {string} - latest selectable date in the chosen format. Default "". @param placeholder {string} - placeholder text. Falls back to the format string when omitted. #} {% macro date_picker( name, id=none, format="yyyy.mm.dd", display="readable", value="", required=false, min_date="", max_date="", placeholder="" ) %} {% set _id = id if id else name %} {% set _placeholder = placeholder if placeholder else format %} ‹ › ‹ › {% for m in ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"] %} {{ m }} {% endfor %} {% for d in ["Mo","Tu","We","Th","Fr","Sa","Su"] %} {{ d }} {% endfor %} {% endmacro %} {# Renders a toggle switch as an accessible checkbox with a styled track and thumb. Visually replaces a standard checkbox for on/off states. @param name {string} - input name attribute, also used to build the id @param label {string} - visible label text @param checked {bool} - initial checked state, default false @param disabled {bool} - disables the toggle, default false @param hint {string} - muted helper text rendered below the toggle. Optional. #} {% macro toggle(name, label, checked=False, disabled=False, hint=None) %} {% if not name %} {{ alert("toggle: name is required", "Pass a non-empty string as the first argument.") }} {% elif not label %} {{ alert("toggle: label is required", "Pass a non-empty string as the second argument.") }} {% else %} {{ label }} {% if hint %} {{ hint }} {% endif %} {% endif %} {% endmacro %} {# Renders a range slider with a synchronized number input. The number input allows precise entry; the slider allows quick sweeping. Both stay in sync via range-slider.js. required is intentionally omitted - a range input always has a value (minimum min). @param name {string} - input name attribute for both the range and number input @param label {string} - visible label text @param min {number} - minimum value, default 0 @param max {number} - maximum value, default 100 @param value {number} - initial value, default equal to min @param step {number} - step increment, default 1 @param unit {string} - optional unit label shown inside the number box, e.g. "px" or "%" @param hint {string} - muted helper text rendered below the slider. Optional. #} {% macro range_slider(name, label, min=0, max=100, value=None, step=1, unit=None, hint=None) %} {% if not name %} {{ alert("range_slider: name is required", "Pass a non-empty string as the first argument.") }} {% elif not label %} {{ alert("range_slider: label is required", "Pass a non-empty string as the second argument.") }} {% else %} {% set _value = value if value is not none else min %} {{ label }} {% if unit %} {{ unit }} {% endif %} {% if hint %} {{ hint }} {% endif %} {% endif %} {% endmacro %} {# Renders a styled file upload field with drag-and-drop zone. Supports two upload modes controlled by the mode param: "direct" (default) Files are POSTed immediately after selection to url as multipart/form-data via file-upload.js. Expects JSON response: {ok: true} or {ok: false, message: "..."}. "cache" Files are held in memory until the surrounding is submitted. On submit, file-upload.js injects the cached files into the hidden input so they travel with the normal form POST. url is not required in this mode. Must be placed inside a element. File type validation uses extension + MIME type (browser-provided). Final authoritative type check must always happen server-side. @param name {string} - input name attribute for the file input @param label {string} - visible label text @param url {string} - upload endpoint URL. Required for mode="direct". Optional for mode="cache". @param mode {string} - "direct" | "cache", default "direct" @param accept {string} - accepted file types, e.g. ".pdf,.docx" or "image/*". Optional. @param multiple {bool} - allow selecting multiple files, default false @param max_size {number} - max file size in bytes. Validated client-side before upload. Optional. @param required {bool} - marks label as required, default false @param hint {string} - muted helper text rendered below the zone. Optional. #} {% macro file_upload(name, label, url=None, mode="direct", accept=None, multiple=False, max_size=None, required=False, hint=None) %} {% if not name %} {{ alert("file_upload: name is required", "Pass a non-empty string as the first argument.") }} {% elif not label %} {{ alert("file_upload: label is required", "Pass a non-empty string as the second argument.") }} {% elif mode not in ["direct", "cache"] %} {{ alert("file_upload: invalid mode \"" ~ mode ~ "\"", "Allowed values: \"direct\", \"cache\".") }} {% elif mode == "direct" and not url %} {{ alert("file_upload: url is required for mode=\"direct\"", "Pass the upload endpoint URL or switch to mode=\"cache\".") }} {% else %} {{ label }} Drop file here or browse {% if accept %} {{ accept }} {% endif %} {% if hint %} {{ hint }} {% endif %} {% endif %} {% endmacro %}
{{ hint }}