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.