PageSourceSearch

https://loar.ai/assets/controllers/challenge_form_controller-07b95…8362682baa884a97df00ce3cc8bd676310ca994e.js

js loar.ai collected 2026-10-05 15:23:37 UTC 8,093 bytes, 238 lines download raw bytes

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.