1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = [ 5 "container", 6 "template", 7 "challengeItem", 8 "title", 9 "photoFields", 10 "quizFields", 11 "scanFields", 12 "typeSelect" 13 ] 14 15 static values = { 16 nextId: Number 17 } 18 19 connect() { 20 console.log("Challenge form controller connected") 21 this.updatePositions() 22 23 // Initialize type fields visibility based on current selections 24 if (this.hasTypeSelectTarget) { 25 this.typeSelectTargets.forEach(select => { 26 const value = select.value 27 if (value) { 28 const item = select.closest("[data-challenge-form-target='challengeItem']") 29 this.toggleFieldsForItem(item, value) 30 } 31 }) 32 } 33 34 // Hide the template challenge form - it's only used as a template 35 const template = this.element.querySelector('[data-challenge-form-target="container"] > [data-f-index="__INDEX__"]') 36 if (template) { 37 template.style.display = 'none' 38 template.setAttribute('data-template', true) 39 } 40 41 // Initialize challenge types for existing items 42 this.initializeExistingChallenges() 43 } 44 45 initializeExistingChallenges() { 46 // For each challenge form that already exists, ensure type fields are correctly shown/hidden 47 const typeSelects = this.element.querySelectorAll('select[name*="[challenge_type]"]') 48 typeSelects.forEach(select => { 49 if (select.value) { 50 this.toggleTypeFieldsForSelect(select) 51 } 52 }) 53 } 54 55 addNew(event) { 56 event.preventDefault() 57 58 // Get the template and clone it 59 const template = this.element.querySelector('[data-template="true"]') 60 if (!template) return 61 62 const clone = template.cloneNode(true) 63 clone.style.display = '' 64 clone.removeAttribute('data-template') 65 66 // Generate a unique index for the new challenge 67 const newIndex = this.getNextIndex() 68 69 // Update all the field names and IDs in the clone to use the new index 70 this.updateFieldAttributes(clone, '__INDEX__', newIndex) 71 72 // Set initial position value 73 const positionField = clone.querySelector('input[name*="[position]"]') 74 if (positionField) { 75 positionField.value = newIndex 76 } 77 78 // Add the new challenge to the container 79 this.containerTarget.appendChild(clone) 80 } 81 82 getNextIndex() { 83 // Find all challenge items that aren't the template 84 const items = Array.from(this.element.querySelectorAll('[data-challenge-form-target="container"] > div')) 85 .filter(item => !item.hasAttribute('data-template')) 86 87 return items.length 88 } 89 90 updateFieldAttributes(element, placeholder, newValue) { 91 // Update name attributes 92 element.querySelectorAll('[name]').forEach(field => { 93 field.name = field.name.replace(placeholder, newValue) 94 }) 95 96 // Update id attributes 97 element.querySelectorAll('[id]').forEach(field => { 98 field.id = field.id.replace(placeholder, newValue) 99 100 // Also update the 'for' attribute of any label targeting this field 101 const label = element.querySelector(`label[for="${field.id}"]`) 102 if (label) { 103 label.htmlFor = field.id 104 } 105 }) 106 107 // Update data-f-index attribute 108 element.setAttribute('data-f-index', newValue) 109 } 110 111 removeChallenge(event) { 112 event.preventDefault() 113 114 // Find the challenge item container 115 const item = event.target.closest('[data-challenge-form-target="challengeItem"]') 116 if (!item) return 117 118 // Confirm deletion 119 if (confirm('Are you sure you want to remove this challenge?')) { 120 // If it has an ID field, mark it for destruction instead of removing 121 const idField = item.querySelector('input[name*="[id]"]') 122 if (idField && idField.value) { 123 // Add a hidden _destroy field 124 const destroyField = document.createElement('input') 125 destroyField.type = 'hidden' 126 destroyField.name = idField.name.replace('[id]', '[_destroy]') 127 destroyField.value = '1' 128 item.appendChild(destroyField) 129 130 // Hide the item 131 item.style.display = 'none' 132 } else { 133 // For new records, just remove from DOM 134 item.remove() 135 } 136 137 // Reindex positions 138 this.updatePositions() 139 } 140 } 141 142 updateTitle(event) { 143 // Find the nearest challenge item container 144 const item = event.target.closest('[data-challenge-form-target="challengeItem"]') 145 if (!item) return 146 147 // Find the title element 148 const titleElement = item.querySelector('[data-challenge-form-target="title"]') 149 if (!titleElement) return 150 151 // Update the title with the challenge name 152 const name = event.target.value.trim() 153 154 if (name) { 155 titleElement.textContent = name 156 } else { 157 // Find the index of this item 158 const items = Array.from(this.element.querySelectorAll('[data-challenge-form-target="challengeItem"]')) 159 .filter(el => !el.hasAttribute('data-template') && el.style.display !== 'none') 160 const index = items.indexOf(item) 161 162 titleElement.textContent = `Challenge ${index + 1}` 163 } 164 } 165 166 toggleTypeFields(event) { 167 this.toggleTypeFieldsForSelect(event.currentTarget) 168 } 169 170 toggleTypeFieldsForSelect(select) { 171 // Find the nearest challenge item container 172 const item = select.closest('[data-challenge-form-target="challengeItem"]') 173 if (!item) return 174 175 // Find the type-specific fields containers 176 const photoFields = item.querySelector('[data-challenge-form-target="photoFields"]') 177 const quizFields = item.querySelector('[data-challenge-form-target="quizFields"]') 178 const scanFields = item.querySelector('[data-challenge-form-target="scanFields"]') 179 180 // Hide all field containers first
181 if (photoFields) photoFields.classList.add('hidden') 182 if (quizFields) quizFields.classList.add('hidden') 183 if (scanFields) scanFields.classList.add('hidden') 184 185 // Show the appropriate container based on the selected type 186 const selectedType = select.value 187 if (selectedType === 'photo' && photoFields) { 188 photoFields.classList.remove('hidden') 189 } else if (selectedType === 'quiz' && quizFields) { 190 quizFields.classList.remove('hidden') 191 } else if (selectedType === 'qr_scan' && scanFields) { 192 scanFields.classList.remove('hidden') 193 } 194 } 195 196 toggleFieldsForItem(item, value) { 197 // Get all field containers for this specific challenge item 198 const photoFields = item.querySelector("[data-challenge-form-target='photoFields']") 199 const quizFields = item.querySelector("[data-challenge-form-target='quizFields']") 200 const scanFields = item.querySelector("[data-challenge-form-target='scanFields']") 201 202 // Hide all fields first 203 photoFields.classList.add("hidden") 204 quizFields.classList.add("hidden") 205 scanFields.classList.add("hidden") 206 207 // Show the relevant fields based on the challenge type 208 if (value === "photo") { 209 photoFields.classList.remove("hidden") 210 } else if (value === "quiz") { 211 quizFields.classList.remove("hidden") 212 } else if (value === "qr_scan") { 213 scanFields.classList.remove("hidden") 214 } 215 } 216 217 updatePositions() { 218 // Get all visible challenge items 219 const items = Array.from(this.element.querySelectorAll('[data-challenge-form-target="container"] > div:not([style*="display: none"])')) 220 .filter(item => !item.hasAttribute('data-template')) 221 222 // Update position fields 223 items.forEach((item, index) => { 224 const positionField = item.querySelector('input[name*="[position]"]') 225 if (positionField) { 226 positionField.value = index 227 } 228 229 // Also update the title if it's empty 230 const titleElement = item.querySelector('[data-challenge-form-target="title"]') 231 const nameField = item.querySelector('input[name*="[name]"]') 232 233 if (titleElement && nameField && !nameField.value.trim()) { 234 titleElement.textContent = `Challenge ${index + 1}` 235 } 236 }) 237 } 238} ;
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.