const ROW = '__row__';
function numberRow(root, row) {
for (const attr of Array.from(root.attributes || [])) {
if (attr.value.includes(ROW)) {
root.setAttribute(attr.name, attr.value.split(ROW).join(String(row)));
}
}
for (const child of Array.from(root.children || [])) numberRow(child, row);
}
function rowOrder(rows) {
return rows.map((row) => row.getAttribute('data-repeater-row')).join(',');
}
function partsOf(node) {
const repeater = node.closest('[data-repeater]');
if (!repeater) return null;
return {
repeater,
order: repeater.querySelector('[data-repeater-order]'),
list: repeater.querySelector('[data-repeater-rows]'),
blank: repeater.querySelector('template[data-repeater-blank]'),
add: repeater.querySelector('[data-repeater-add]'),
};
}
function maxRows(parts) {
const max = Number(parts.repeater.getAttribute('data-repeater-max'));
return max > 0 ? max : Infinity;
}
function minRows(parts) {
return Number(parts.repeater.getAttribute('data-repeater-min')) || 0;
}
function syncButtons(parts) {
const count = parts.list.children.length;
parts.add.disabled = count >= maxRows(parts);
for (const remove of parts.list.querySelectorAll('[data-repeater-remove]')) {
remove.disabled = count <= minRows(parts);
}
}
function addRow(parts) {
if (parts.list.children.length >= maxRows(parts)) return;
const row = parts.repeater.getAttribute('data-repeater-next');
const copy = parts.blank.content.cloneNode(true);
const added = copy.firstElementChild;
numberRow(added, row);
parts.repeater.setAttribute('data-repeater-next', String(Number(row) + 1));
parts.list.appendChild(copy);
const first = added.querySelector('input:not([type="hidden"]), select, textarea');
if (first) first.focus();
}
function act(button) {
const parts = partsOf(button);
if (!parts) return false;
if (button.hasAttribute('data-repeater-add')) {
addRow(parts);
} else {
const row = button.closest('[data-repeater-row]');
if (!row) return false;
if (button.hasAttribute('data-repeater-remove')) {
row.remove();
syncButtons(parts);
parts.add.focus();
} else if (button.hasAttribute('data-repeater-up')) {
const previous = row.previousElementSibling;
if (previous) parts.list.insertBefore(row, previous);
button.focus();
} else if (button.hasAttribute('data-repeater-down')) {
const next = row.nextElementSibling;
if (next) parts.list.insertBefore(next, row);
button.focus();
}
}
parts.order.value = rowOrder(Array.from(parts.list.children));
syncButtons(parts);
return true;
}
const BUTTONS =
'[data-repeater-add], [data-repeater-remove], [data-repeater-up], [data-repeater-down]';
function install() {
document.addEventListener('click', (e) => {
const button = e.target.closest && e.target.closest(BUTTONS);
if (button) act(button);
});
}
if (typeof document !== 'undefined') install();
if (typeof module !== 'undefined' && module.exports) {
module.exports = { ROW, act, numberRow, rowOrder };
}