Files
livewire-material/resources/js/field.js
T
Andreas Reinhold / reiniandClaude Opus 5 0341f9d4ca Draw the text field's chrome without Tailwind
<x-field> renders data-md-field with its props and parts as data-md-*
attributes (box, control, label, outline, support, counter, trailing
buttons), and field.css moves into material.components on tokens: px
geometry, spacing and state tokens, the 600px breakpoint (plan step 36).
Its icons take a size prop, 24/20/16 by the field's size.

Every control the field wraps now marks itself data-md-field-control,
so the inputs, the pickers, choices, field.js, menu.css's select and
listbox rules and timepicker.css follow the renamed hooks. Browser tests
cover the error icon, the disabled field's hover, the counter and the
width bound from 600px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 14:08:22 +02:00

84 lines
3.0 KiB
JavaScript

/**
* What the form controls need from script, which is little: both are kept off Alpine so they work
* in any markup and before Alpine starts.
*
* `<x-textarea>` grows with its text. CSS does that where the browser can size a field to its
* content (`field-sizing: content`, components/field.css); elsewhere the height is set here from
* the scroll height, on every input and whenever a Livewire render or navigation puts a new value
* in.
*
* `<x-checkbox indeterminate>`: HTML has no attribute for a checkbox's mixed state, only the
* `indeterminate` property, so the component marks the input `data-indeterminate` and this keeps
* the property in step with the mark — when the page loads, and when a render adds or removes the
* mark. A person pressing the box clears the property as the browser always does; the mark stays
* until the server decides again.
*/
const GROWING = 'textarea[data-md-autogrow]'
const MIXED = 'input[type="checkbox"][data-indeterminate]'
const growsByItself = window.CSS?.supports?.('field-sizing', 'content') ?? false
function grow(textarea) {
textarea.style.height = 'auto'
textarea.style.height = `${textarea.scrollHeight + textarea.offsetHeight - textarea.clientHeight}px`
}
function growAll(root) {
if (! growsByItself) {
root.querySelectorAll(GROWING).forEach(grow)
}
}
function markAll(root) {
root.querySelectorAll(MIXED).forEach((input) => (input.indeterminate = true))
}
if (! growsByItself) {
document.addEventListener('input', (event) => {
if (event.target instanceof HTMLTextAreaElement && event.target.matches(GROWING)) {
grow(event.target)
}
})
document.addEventListener('livewire:init', () => {
// A render that only puts a new value in a textarea changes no attribute or node.
window.Livewire.hook('morphed', ({ el }) => growAll(el))
})
}
new MutationObserver((records) => {
for (const record of records) {
if (record.type === 'attributes') {
if (record.attributeName === 'data-indeterminate' && record.target instanceof HTMLInputElement) {
record.target.indeterminate = record.target.hasAttribute('data-indeterminate')
} else if (! growsByItself && record.target.matches(GROWING)) {
grow(record.target)
}
continue
}
for (const node of record.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) {
continue
}
for (const input of [node, ...node.querySelectorAll(MIXED)].filter((element) => element.matches(MIXED))) {
input.indeterminate = true
}
if (! growsByItself) {
;[node, ...node.querySelectorAll(GROWING)].filter((element) => element.matches(GROWING)).forEach(grow)
}
}
}
}).observe(document.documentElement, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['data-indeterminate', 'data-md-autogrow'],
})
markAll(document)
growAll(document)