1import { Controller } from "@hotwired/stimulus" 2 3// The Invite Guests window: step 1 picks people from lists that load into a 4// Turbo Frame, step 2 reviews them and adds a message. 5// 6// The selection lives here, not in the checkboxes, because every rail click 7// replaces the frame. On each frame load the ticks are put back, so moving 8// between Suggestions, Everyone and a past event keeps them. The rail and the 9// typed-emails box are outside the frame, so a load never touches them. 10export default class extends Controller { 11 static targets = ["checkbox", "railItem", "listPanel", "emailsPanel", "emails", 12 "step1", "step2", "nextButton", "reviewList", "reviewCount", "form"] 13 14 connect() { 15 this.selected = new Map() // user id -> { name, email } 16 this.emailsText = "" 17 this.restore = this.restore.bind(this) 18 this.element.addEventListener("turbo:frame-load", this.restore) 19 } 20 21 disconnect() { 22 this.element.removeEventListener("turbo:frame-load", this.restore) 23 } 24 25 restore() { 26 this.checkboxTargets.forEach(box => { box.checked = this.selected.has(box.value) }) 27 this.update() 28 } 29 30 toggle(event) { 31 const box = event.target 32 if (box.checked) { 33 this.selected.set(box.value, { name: box.dataset.name, email: box.dataset.email }) 34 } else { 35 this.selected.delete(box.value) 36 } 37 this.update() 38 } 39 40 showList(event) { 41 this.markCurrent(event.currentTarget) 42 this.listPanelTarget.hidden = false 43 this.emailsPanelTarget.hidden = true 44 } 45 46 showEmails(event) { 47 this.markCurrent(event.currentTarget) 48 this.listPanelTarget.hidden = true 49 this.emailsPanelTarget.hidden = false 50 this.emailsTarget.focus() 51 } 52 53 markCurrent(item) { 54 this.railItemTargets.forEach(el => el.removeAttribute("aria-current")) 55 item.setAttribute("aria-current", "true") 56 } 57 58 emailsChanged() { 59 this.emailsText = this.emailsTarget.value 60 this.update() 61 } 62 63 typedEmails() { 64 return [...new Set(this.emailsText.split(/[\s,;]+/).map(e => e.trim().toLowerCase()).filter(Boolean))] 65 } 66 67 update() { 68 const count = this.selected.size + this.typedEmails().length 69 this.nextButtonTarget.disabled = count === 0 70 } 71 72 next() { 73 const people = [...this.selected.values()] 74 const emails = this.typedEmails() 75 this.reviewCountTarget.textContent = `Inviting ${people.length + emails.length} ${people.length + emails.length === 1 ? "Person" : "People"}` 76 77 this.reviewListTarget.replaceChildren( 78 ...people.map(p => this.reviewRow(p.name, p.email)), 79 ...emails.map(e => this.reviewRow(e, null)) 80 ) 81 82 const form = this.formTarget 83 form.querySelectorAll("input[data-generated]").forEach(el => el.remove()) 84 this.selected.forEach((_, id) => form.appendChild(this.hidden("user_ids[]", id))) 85 form.appendChild(this.hidden("emails", emails.join("\n"))) 86 87 this.step1Target.hidden = true 88 this.step2Target.hidden = false 89 } 90 91 back() { 92 this.step2Target.hidden = true 93 this.step1Target.hidden = false 94 } 95 96 // textContent only: names and emails are user-entered. 97 reviewRow(primary, secondary) { 98 const li = document.createElement("li") 99 li.className = "py-1" 100 const name = document.createElement("p") 101 name.className = "text-sm font-medium" 102 name.textContent = primary 103 li.appendChild(name) 104 if (secondary) { 105 const email = document.createElement("p") 106 email.className = "text-xs opacity-70" 107 email.textContent = secondary 108 li.appendChild(email) 109 } 110 return li 111 } 112 113 hidden(name, value) { 114 const input = document.createElement("input") 115 input.type = "hidden" 116 input.name = name 117 input.value = value 118 input.dataset.generated = "" 119 return input 120 } 121}
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.