lightview
Version:
A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation
225 lines (190 loc) • 6.7 kB
JavaScript
/**
* Lightview Components - FileInput
* A file input component using DaisyUI 5 styling
* @see https://daisyui.com/components/file-input/
*
* Uses DaisyUI's fieldset pattern:
* <fieldset class="fieldset">
* <legend class="fieldset-legend">Label</legend>
* <input type="file" class="file-input" />
* <p class="label">Helper text</p>
* </fieldset>
*/
import '../daisyui.js';
/**
* FileInput Component
* @param {Object} props - FileInput properties
* @param {string} props.size - 'xs' | 'sm' | 'md' | 'lg' (default: 'md')
* @param {string} props.color - 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error'
* @param {boolean} props.ghost - Ghost style (no background)
* @param {boolean} props.disabled - Disable file input
* @param {boolean} props.required - Required field
* @param {string} props.accept - Accepted file types (e.g., '.pdf,.doc', 'image/*')
* @param {boolean} props.multiple - Allow multiple file selection
* @param {string} props.label - Label text
* @param {string} props.helper - Helper text
* @param {string|Function} props.error - Error message
* @param {Function} props.validate - Validation function (files) => errorMessage | null
* @param {Function} props.onChange - Change handler (files, event) => void
* @param {boolean} props.useShadow - Render in Shadow DOM
*/
const FileInput = (props = {}) => {
const { tags, signal } = globalThis.Lightview || {};
const LVX = globalThis.LightviewX || {};
if (!tags) {
console.error('Lightview not found');
return null;
}
const { div, input, fieldset, legend, p, span, shadowDOM } = tags;
const {
size = 'md',
color,
ghost = false,
disabled = false,
required = false,
accept,
multiple = false,
label: labelText,
helper,
error,
validate,
onChange,
name,
id,
class: className = '',
useShadow,
...rest
} = props;
const fileInputId = id || `file-input-${Math.random().toString(36).slice(2, 9)}`;
// Internal state
const internalError = signal ? signal(null) : { value: null };
const getError = () => {
if (error) {
const err = typeof error === 'function' ? error() : error;
if (err) return err;
}
return internalError.value;
};
const handleChange = (e) => {
const files = e.target.files;
// Validation
if (validate) {
const validationError = validate(files);
internalError.value = validationError;
} else if (required && (!files || files.length === 0)) {
internalError.value = 'Please select a file';
} else {
internalError.value = null;
}
if (onChange) onChange(files, e);
};
// Build DaisyUI file-input classes
const getFileInputClass = () => {
const classes = ['file-input', 'w-full'];
// Ghost style
if (ghost) {
classes.push('file-input-ghost');
}
// Size
if (size && size !== 'md') {
classes.push(`file-input-${size}`);
}
// Color
if (color) {
classes.push(`file-input-${color}`);
}
// Error state
const currentError = getError();
if (currentError) {
classes.push('file-input-error');
}
return classes.join(' ');
};
const fileInputEl = input({
type: 'file',
class: validate || error ? () => getFileInputClass() : getFileInputClass(),
accept,
multiple,
disabled,
required,
name,
id: fileInputId,
onchange: handleChange,
'aria-invalid': () => !!getError(),
...rest
});
// If no label and no helper, return just the file input
if (!labelText && !helper && !validate && !error) {
let usesShadow = false;
if (LVX.shouldUseShadow) {
usesShadow = LVX.shouldUseShadow(useShadow);
} else {
usesShadow = useShadow === true;
}
if (usesShadow) {
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
return div({ class: 'content', style: 'display: inline-block' },
shadowDOM({ mode: 'open', adoptedStyleSheets },
div({ 'data-theme': themeValue }, fileInputEl)
)
);
}
return fileInputEl;
}
// Build the component using DaisyUI fieldset pattern
const fieldsetContent = [];
// Legend/Label
if (labelText) {
fieldsetContent.push(
legend({ class: 'fieldset-legend' },
labelText,
required ? span({ class: 'text-error' }, ' *') : null
)
);
}
// File input element
fieldsetContent.push(fileInputEl);
// Helper or error text
if (helper || validate || error) {
fieldsetContent.push(
() => {
const currentError = getError();
if (currentError) {
return p({
class: 'label text-error',
role: 'alert'
}, currentError);
}
if (helper) {
return p({ class: 'label' }, helper);
}
return null;
}
);
}
// Wrapper
const wrapperEl = fieldset({
class: `fieldset ${className}`.trim()
}, ...fieldsetContent);
// Check if we should use shadow DOM
let usesShadow = false;
if (LVX.shouldUseShadow) {
usesShadow = LVX.shouldUseShadow(useShadow);
} else {
usesShadow = useShadow === true;
}
if (usesShadow) {
const adoptedStyleSheets = LVX.getAdoptedStyleSheets ? LVX.getAdoptedStyleSheets() : [];
const themeValue = LVX.themeSignal ? () => LVX.themeSignal.value : 'light';
return div({ class: 'contents' },
shadowDOM({ mode: 'open', adoptedStyleSheets },
div({ 'data-theme': themeValue }, wrapperEl)
)
);
}
return wrapperEl;
};
// Auto-register
globalThis.Lightview.tags.FileInput = FileInput;
export default FileInput;