PageSourceSearch

https://internationaldayofzerowaste.org/assets/admin_controllers/invite_picker_controller-1ed1610a.js

js internationaldayofzerowaste.org collected 2026-09-29 11:08:42 UTC 3,740 bytes, 121 lines download raw bytes

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.