1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["rows", "row", "template", "removeButton"] 5 static values = { nextIndex: Number } 6 7 connect() { 8 this.ensureTrailingBlankRow() 9 } 10 11 fieldChanged() { 12 this.ensureTrailingBlankRow() 13 } 14 15 remove(event) { 16 event.preventDefault() 17 18 const row = event.currentTarget.closest("[data-employee-count-price-tiers-target~='row']") 19 if (!row) return 20 21 const destroyInput = row.querySelector("input[name$='[_destroy]']") 22 if (destroyInput) destroyInput.value = "1" 23 24 row.dataset.employeeCountPriceTiersRemoved = "true" 25 row.classList.add("hidden") 26 this.ensureTrailingBlankRow() 27 } 28 29 ensureTrailingBlankRow() { 30 this.removeExtraTrailingBlankRows() 31 32 const rows = this.activeRows() 33 if (rows.length === 0 || this.rowHasContent(rows[rows.length - 1])) { 34 this.addBlankRow() 35 } 36 37 this.updateRemoveButtons() 38 } 39 40 addBlankRow() { 41 if (!this.hasTemplateTarget || !this.hasRowsTarget) return 42 43 const index = this.nextIndexValue 44 const html = this.templateTarget.innerHTML.replaceAll("__INDEX__", String(index)) 45 this.nextIndexValue = index + 1 46 this.rowsTarget.insertAdjacentHTML("beforeend", html) 47 } 48 49 removeExtraTrailingBlankRows() { 50 const rows = this.activeRows() 51 let lastMeaningfulIndex = -1 52 rows.forEach((row, index) => { 53 if (this.rowHasContent(row) || this.persistedRow(row)) lastMeaningfulIndex = index 54 }) 55 56 const trailingBlankRows = rows.slice(lastMeaningfulIndex + 1) 57 const extraBlankRows = lastMeaningfulIndex === -1 ? trailingBlankRows.slice(1) : trailingBlankRows.slice(0, -1) 58 59 extraBlankRows.forEach((row) => row.remove()) 60 } 61 62 updateRemoveButtons() { 63 this.removeButtonTargets.forEach((button) => { 64 const row = button.closest("[data-employee-count-price-tiers-target~='row']") 65 const shouldShow = row && !this.removedRow(row) && (this.persistedRow(row) || this.rowHasContent(row)) 66 button.classList.toggle("invisible", !shouldShow) 67 button.disabled = !shouldShow 68 }) 69 } 70 71 activeRows() { 72 return this.rowTargets.filter((row) => !this.removedRow(row)) 73 } 74 75 removedRow(row) { 76 return row.dataset.employeeCountPriceTiersRemoved === "true" 77 } 78 79 persistedRow(row) { 80 return this.idInput(row)?.value.trim().length > 0 81 } 82 83 rowHasContent(row) { 84 return this.editableInputs(row).some((input) => input.value.trim().length > 0) 85 } 86 87 editableInputs(row) { 88 return Array.from(row.querySelectorAll("input")).filter((input) => { 89 const name = input.getAttribute("name") || "" 90 return input.type !== "hidden" && !name.endsWith("[_destroy]") 91 }) 92 } 93 94 idInput(row) { 95 return row.querySelector("input[name$='[id]']") 96 } 97}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.