(function (app) { 'use strict'; const u = app.modules.util; // Build the standard label / description / input / help / error scaffold // shared by all primitive widgets. Returns { wrap, errEl }. function fieldContainer(opts) { const wrap = u.h('div', { className: 'form-field' }); if (opts.label) { const lbl = u.h('label', { className: 'form-field__label', for: opts.id }); lbl.appendChild(document.createTextNode(opts.label)); if (opts.required) { lbl.appendChild(u.h('span', { className: 'required-mark' }, '*')); } wrap.appendChild(lbl); } if (opts.description) { wrap.appendChild(u.h('div', { className: 'form-field__description' }, opts.description)); } wrap.appendChild(opts.input); if (opts.help) { wrap.appendChild(u.h('div', { className: 'form-field__help' }, opts.help)); } const errEl = u.h('div', { className: 'form-field__error', hidden: true }); wrap.appendChild(errEl); return { wrap: wrap, errEl: errEl }; } function coerceEnum(rawValue, options) { for (let i = 0; i < options.length; i++) { if (String(options[i]) === rawValue) { return options[i]; } } return rawValue; } function makePrimitive(schema, ui, path, value, options) { const id = u.uid('w'); const required = !!options.required; const label = (ui && ui['ui:title']) || schema.title || options.fieldName || ''; const description = (ui && ui['ui:description']) || schema.description || ''; const help = (ui && ui['ui:help']) || ''; const placeholder = (ui && ui['ui:placeholder']) || ''; const widget = (ui && ui['ui:widget']) || ''; // readonly is honored from either source: an explicit UI override // (ui:readonly: true) or the schema's readOnly field. The latter // is set by the server when augmenting from cascade-locked // records: entries and for audit fields declared readOnly in the // *.form.yaml. const readonly = !!(schema.readOnly) || !!(ui && ui['ui:readonly']); // x-labels: { code → label } turns a bare enum into a labeled // dropdown ("ACM — Acme Inc" rather than just "ACM"). Injected // by the server from the cascade's field_codes:codes map. const labels = (schema && schema['x-labels']) || null; const autofocus = !!(ui && ui['ui:autofocus']); let input; let read; const t = schema.type; if (t === 'boolean') { // Render boolean as a single checkbox with an inline label, suppressing // the standard label-above layout for cleaner UX. const cb = u.h('input', { type: 'checkbox', id: id }); if (value === true) { cb.checked = true; } if (readonly) { cb.disabled = true; } const wrap = u.h('div', { className: 'form-field form-field--boolean' }); const inlineLabel = u.h('label', { for: id, className: 'form-field__checkbox-inline' }); inlineLabel.appendChild(cb); inlineLabel.appendChild(document.createTextNode(' ')); inlineLabel.appendChild(document.createTextNode(label || '')); if (required) { inlineLabel.appendChild(u.h('span', { className: 'required-mark' }, '*')); } wrap.appendChild(inlineLabel); if (description) { wrap.appendChild(u.h('div', { className: 'form-field__description' }, description)); } if (help) { wrap.appendChild(u.h('div', { className: 'form-field__help' }, help)); } const errEl = u.h('div', { className: 'form-field__error', hidden: true }); wrap.appendChild(errEl); return widgetObject(wrap, errEl, path, function () { return cb.checked; }); } if (Array.isArray(schema.enum)) { const opts = schema.enum; if (widget === 'radio') { input = u.h('div', { className: 'form-field__radio-group' }); opts.forEach(function (opt, idx) { const codeStr = String(opt); const radioId = id + '-' + idx; const radio = u.h('input', { type: 'radio', name: id, id: radioId, value: codeStr }); if (value === opt) { radio.checked = true; } if (readonly) { radio.disabled = true; } let displayText = codeStr; if (labels && Object.prototype.hasOwnProperty.call(labels, codeStr)) { displayText = codeStr + ' — ' + labels[codeStr]; } const lbl = u.h('label', { for: radioId }); lbl.appendChild(radio); lbl.appendChild(document.createTextNode(' ' + displayText)); input.appendChild(lbl); }); read = function () { const checked = input.querySelector('input[type="radio"]:checked'); return checked ? coerceEnum(checked.value, opts) : undefined; }; } else { input = u.h('select', { id: id, className: 'form-field__select' }); if (!required) { input.appendChild(u.h('option', { value: '' }, '— select —')); } opts.forEach(function (opt) { const codeStr = String(opt); let displayText = codeStr; if (labels && Object.prototype.hasOwnProperty.call(labels, codeStr)) { displayText = codeStr + ' — ' + labels[codeStr]; } const o = u.h('option', { value: codeStr }, displayText); if (value === opt) { o.selected = true; } input.appendChild(o); }); if (readonly) { input.disabled = true; } read = function () { if (input.value === '') { return undefined; } return coerceEnum(input.value, opts); }; } } else if (t === 'number' || t === 'integer') { input = u.h('input', { type: 'number', id: id, className: 'form-field__input', step: t === 'integer' ? '1' : 'any' }); if (placeholder) { input.placeholder = placeholder; } if (value != null) { input.value = String(value); } if (readonly) { input.readOnly = true; } if (autofocus) { input.autofocus = true; } read = function () { const v = input.value.trim(); if (v === '') { return undefined; } const n = Number(v); // If the user typed something non-numeric, return the raw string and // let server validation produce a friendly error. return Number.isFinite(n) ? n : v; }; } else { // Default: string-shaped input. const fmt = schema.format; if (widget === 'textarea') { input = u.h('textarea', { id: id, className: 'form-field__textarea' }); } else { let inputType = 'text'; if (fmt === 'date') { inputType = 'date'; } else if (fmt === 'email') { inputType = 'email'; } input = u.h('input', { type: inputType, id: id, className: 'form-field__input' }); } if (placeholder) { input.placeholder = placeholder; } if (value != null) { input.value = String(value); } if (readonly) { input.readOnly = true; } if (autofocus) { input.autofocus = true; } // Schema-driven HTML pattern attribute. Used as a UX hint // only — authoritative validation runs server-side via the // cascade's field_codes. if (schema.pattern && input.tagName === 'INPUT') { input.pattern = schema.pattern; } read = function () { return input.value === '' ? undefined : input.value; }; } const built = fieldContainer({ id: id, label: label, description: description, help: help, required: required, input: input }); return widgetObject(built.wrap, built.errEl, path, read); } // Common widget shape used by both primitive and the wrapper above. function widgetObject(wrapEl, errEl, path, read) { return { el: wrapEl, path: path, type: 'primitive', read: read, setError: function (msg) { errEl.textContent = msg; errEl.hidden = false; wrapEl.classList.add('form-field--invalid'); }, clearErrors: function () { errEl.textContent = ''; errEl.hidden = true; wrapEl.classList.remove('form-field--invalid'); }, child: function () { return null; } }; } app.modules.widgets = { makePrimitive: makePrimitive }; })(window.formApp);