/** * AKSRT Input Expand * * Lightweight input field enhancements: * - Textarea auto-resize on input * - Character counter with limit display * - Validation feedback (min/max length, required) * * No external dependencies — pure vanilla JS. * * Usage: * * * * * @package AKSRT */ (function () { 'use strict'; var STYLE_ID = 'aksrt-input-expand-style'; function injectStyle() { if (document.getElementById(STYLE_ID)) return; var style = document.createElement('style'); style.id = STYLE_ID; style.textContent = '.aksrt-counter{display:block;text-align:right;font-size:12px;color:#8a909c;margin-top:4px;transition:color 0.2s;}' + '.aksrt-counter.is-over{color:#e74c3c;}' + '.aksrt-counter.is-warn{color:#f39c12;}' + '.aksrt-validate-msg{display:block;font-size:12px;margin-top:4px;color:#e74c3c;opacity:0;max-height:0;overflow:hidden;transition:all 0.2s;}' + '.aksrt-validate-msg.is-visible{opacity:1;max-height:30px;}' + 'textarea[data-aksrt-autoresize]{overflow:hidden;resize:none;}'; document.head.appendChild(style); } // ── Auto-resize Textarea ── function queryAll(root, selector) { var nodes = Array.prototype.slice.call(root.querySelectorAll(selector)); if (root.nodeType === 1 && root.matches(selector)) nodes.unshift(root); return nodes; } function initAutoResize(root) { var textareas = queryAll(root, 'textarea[data-aksrt-autoresize]'); textareas.forEach(function (ta) { if (ta.dataset.aksrtAutoresizeBound === '1') return; ta.dataset.aksrtAutoresizeBound = '1'; function resize() { ta.style.height = 'auto'; ta.style.height = ta.scrollHeight + 'px'; } ta.addEventListener('input', resize); // Initial resize resize(); }); } // ── Character Counter ── function initCounters(root) { var fields = queryAll(root, '[data-aksrt-counter]'); fields.forEach(function (field) { if (field.dataset.aksrtCounterBound === '1') return; field.dataset.aksrtCounterBound = '1'; var max = parseInt(field.getAttribute('data-aksrt-maxlength') || field.getAttribute('maxlength') || '0', 10); var counter = document.createElement('span'); counter.className = 'aksrt-counter'; field.parentNode.insertBefore(counter, field.nextSibling); function update() { var len = (field.value || '').length; if (max > 0) { counter.textContent = len + ' / ' + max; counter.classList.toggle('is-over', len > max); counter.classList.toggle('is-warn', len > max * 0.9 && len <= max); } else { counter.textContent = len + ' 字'; counter.classList.remove('is-over', 'is-warn'); } } field.addEventListener('input', update); update(); }); } // ── Validation Feedback ── function initValidation(root) { var fields = queryAll(root, '[data-aksrt-validate]'); fields.forEach(function (field) { if (field.dataset.aksrtValidateBound === '1') return; field.dataset.aksrtValidateBound = '1'; var minLen = parseInt(field.getAttribute('data-aksrt-minlength') || field.getAttribute('minlength') || '0', 10); var maxLen = parseInt(field.getAttribute('data-aksrt-maxlength') || field.getAttribute('maxlength') || '0', 10); var required = field.hasAttribute('required'); var msg = document.createElement('span'); msg.className = 'aksrt-validate-msg'; field.parentNode.insertBefore(msg, field.nextSibling); function validate() { var val = (field.value || '').trim(); var error = ''; if (required && val === '') { error = '此项为必填'; } else if (minLen > 0 && val.length > 0 && val.length < minLen) { error = '至少需要 ' + minLen + ' 个字符'; } else if (maxLen > 0 && val.length > maxLen) { error = '不能超过 ' + maxLen + ' 个字符'; } msg.textContent = error; msg.classList.toggle('is-visible', error !== ''); field.classList.toggle('is-invalid', error !== ''); field.classList.toggle('is-valid', error === '' && val !== ''); } field.addEventListener('input', validate); field.addEventListener('blur', validate); }); } // ── Main Init ── function init(root) { root = root || document; injectStyle(); initAutoResize(root); initCounters(root); initValidation(root); } window.AksrtFrontend.register('aksrt-input-expand', init); })();