PageSourceSearch

https://dashboard.tastevinapp.com/assets/controllers/beverage_pair…031c16ab9410de38a731356a581ac3ae26431804.js

js tastevinapp.com collected 2026-10-01 16:08:13 UTC 6,330 bytes, 175 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// UI v3 — Beverage pairings controller.
4//
5// Mirror of menu_item_pairings_controller.js for the opposite direction: this
6// drives the Beverage edit "Pairings" tab, where the available list is the
7// restaurant's MENU ITEMS and clicking a row toggles that menu item in/out of
8// the beverage's paired list. (menu_item_pairings_controller drives the menu
9// item edit page, where the available list is beverages.)
10//
11// A single menu item can appear in several rows (one per menu section it sits
12// in), so a click cascades to every row sharing the same data-menu_item_id and
13// the paired list keeps a single <li> per menu item — the legacy jQuery harness
14// in beverages/_all_pairings.html.haml behaved the same way.
15//
16// Wire contract preserved verbatim from the legacy partial:
17//   POST /menu_item_pairings/:id/toggle_visibility        (ipad-on-menu toggle)
18//   Form submit: beverage[menu_item_ids][]=:id            (paired items on save)
19//                beverage[visible_menu_item_ids][]=:id     (ipad-visible)
20// Server contract unchanged — no controller/route changes.
21//
22// Accept header for the toggle_visibility POST mirrors the menu_item controller
23// ("text/javascript, application/json") to satisfy require_api_version.
24export default class extends Controller {
25  static targets = ["pairedList", "noItems"]
26
27  connect() {
28    this._refreshNoItems()
29  }
30
31  // Click handler on the paired-list <ul>. Each paired <li> has a single pairing
32  // switch; toggling it off removes the pairing. preventDefault stops the
33  // label's native checkbox toggle so we manage removal manually.
34  pairedListClicked(event) {
35    const label = event.target && event.target.closest("label.ui-switch")
36    if (!label) return
37    event.preventDefault()
38    this._removePairedItem(label.closest("li"))
39  }
40
41  // Click delegated from the available menu-items table. Toggles the clicked
42  // menu item — and every sibling row for the same menu item — in/out of the
43  // paired list.
44  rowClicked(event) {
45    const row = event.target && event.target.closest("tr[data-menu_item_id]")
46    if (!row) return
47    const menuItemId = row.getAttribute("data-menu_item_id")
48    const isUnpairing = !!this._findPaired(menuItemId)
49    const siblings = this.element.querySelectorAll(
50      `tr[data-menu_item_id="${this._cssEscape(menuItemId)}"]`
51    )
52
53    if (isUnpairing) {
54      const existing = this._findPaired(menuItemId)
55      if (existing) existing.remove()
56      siblings.forEach((sib) => this._deselectRow(sib))
57    } else {
58      this._appendPaired(row)
59      siblings.forEach((sib) => this._selectRow(sib))
60    }
61    this._refreshNoItems()
62  }
63
64  // ---- internals ----
65
66  _findPaired(menuItemId) {
67    return this.pairedListTarget.querySelector(`#paired_wine_${this._cssEscape(menuItemId)}`)
68  }
69
70  // Build a paired <li> that matches the server-rendered ui-switch markup so a
71  // freshly added pairing looks identical to one loaded from the database.
72  _appendPaired(row) {
73    const menuItemId = row.getAttribute("data-menu_item_id")
74    const nameCell = row.querySelector("td.wine_name")
75    const name = (nameCell && (nameCell.getAttribute("title") || nameCell.textContent.trim())) || ""
76
77    const pairId = `beverage_menu_item_ids_${menuItemId}`
78
79    const li = document.createElement("li")
80    li.id = `paired_wine_${menuItemId}`
81    li.className = "flex items-center gap-3 px-4 py-2"
82    li.setAttribute("data-menu_item_id", menuItemId)
83
84    // Pairing switch (menu_item_ids[]) with the visible label — toggling off
85    // removes the pairing.
86    li.appendChild(this._buildSwitch({
87      id: pairId,
88      name: "beverage[menu_item_ids][]",
89      value: menuItemId,
90      checked: true,
91      label: name
92    }))
93
94    this.pairedListTarget.appendChild(li)
95  }
96
97  // Construct the same `<label.ui-switch>` structure the HAML emits.
98  _buildSwitch({ id, name, value, checked, label, srLabel, extraClass, dataId, wrapClass, labelClass, title }) {
99    const lbl = document.createElement("label")
100    lbl.className = class_names("ui-switch", wrapClass)
101    lbl.setAttribute("for", id)
102    if (title) lbl.title = title
103
104    const input = document.createElement("input")
105    input.type = "checkbox"
106    input.setAttribute("role", "switch")
107    input.id = id
108    input.name = name
109    input.value = value
110    input.checked = !!checked
111    input.className = class_names("ui-switch__input", extraClass)
112    if (dataId !== undefined) input.dataset.id = dataId
113    lbl.appendChild(input)
114
115    const track = document.createElement("span")
116    track.className = "ui-switch__track"
117    track.setAttribute("aria-hidden", "true")
118    const thumb = document.createElement("span")
119    thumb.className = "ui-switch__thumb"
120    track.appendChild(thumb)
121    lbl.appendChild(track)
122
123    if (label) {
124      const text = document.createElement("span")
125      text.className = class_names("ui-switch__label", labelClass)
126      text.textContent = label
127      lbl.appendChild(text)
128    } else if (srLabel) {
129      const sr = document.createElement("span")
130      sr.className = "sr-only"
131      sr.textContent = srLabel
132      lbl.appendChild(sr)
133    }
134    return lbl
135  }
136
137  _removePairedItem(li) {
138    if (!li) return
139    const menuItemId = li.getAttribute("data-menu_item_id")
140    li.remove()
141    if (menuItemId) {
142      this.element
143        .querySelectorAll(`tr[data-menu_item_id="${this._cssEscape(menuItemId)}"]`)
144        .forEach((sib) => this._deselectRow(sib))
145    }
146    this._refreshNoItems()
147  }
148
149  _selectRow(row) {
150    const iconCell = row.querySelector("td.icon")
151    if (iconCell) iconCell.classList.add("selected")
152  }
153
154  _deselectRow(row) {
155    const iconCell = row.querySelector("td.icon")
156    if (iconCell) iconCell.classList.remove("selected")
157  }
158
159  _refreshNoItems() {
160    if (!this.hasNoItemsTarget) return
161    const hasPaired =
162      this.pairedListTarget.querySelectorAll('li[id^="paired_wine_"]').length > 0
163    this.noItemsTarget.style.display = hasPaired ? "none" : ""
164  }
165
166  _cssEscape(value) {
167    if (window.CSS && CSS.escape) return CSS.escape(value)
168    return String(value).replace(/(["\\])/g, "\\$1")
169  }
170}
171
172// Tiny local class-name joiner (avoids a shared import for one call site).
173function class_names(...names) {
174  return names.filter(Boolean).join(" ")
175};

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.