1import { Controller } from "@hotwired/stimulus" 2 3// Phase 8 â pos_sku_chooser_controller. 4// 5// Page-level companion to surfaces that need the "pick a POS SKU" modal. 6// Two trigger APIs: 7// 8// 1) Custom event (legacy, used by `beverage_sku_list_controller`): 9// element.dispatchEvent(new CustomEvent("beverage-sku-list:open-pos-chooser", 10// { detail: { url, triggerEl } })) 11// The trigger has a `.sku_edit` ancestor with `.sku_input` + `.price_input` 12// hidden fields that the controller writes back on row select. 13// 14// 2) Direct action (Phase 8 session 8 â used by MenuItem SKU tab): 15// <a data-action="click->pos-sku-chooser#trigger" 16// data-pos-chooser-url="/point_of_sale_skus/for_assigning?..." 17// data-pos-chooser-id-input="<CSS selector>" 18// data-pos-chooser-id-input-scope="closest:fieldset" 19// data-pos-chooser-label-display="<CSS selector, optional>"> 20// The controller stashes references to those fields/spans and writes 21// them back on row click. No `.sku_edit` ancestor needed. 22// 23// Either path opens the same shared modal (`shared/ui/_pos_sku_chooser`) 24// and reuses the same row-click delegation logic. 25// 26// Replaces the legacy jQuery-UI `.dialog({...}).load(url)` flow 27// (pos_sku_chooser_dialog.js). 28export default class extends Controller { 29 static targets = ["frame"] 30 31 connect() { 32 this._activeTrigger = null 33 // Per-open writeback state. Either resolved from declarative attrs 34 // on the trigger element (path 2) or computed from the trigger's 35 // ancestor chain at selectRow time (path 1). 36 this._writeback = null 37 this._openHandler = (event) => this.open(event) 38 this._rowClickHandler = (event) => this.selectRow(event) 39 document.addEventListener("beverage-sku-list:open-pos-chooser", this._openHandler) 40 if (this.hasFrameTarget) { 41 this.frameTarget.addEventListener("click", this._rowClickHandler) 42 } 43 } 44 45 disconnect() { 46 document.removeEventListener("beverage-sku-list:open-pos-chooser", this._openHandler) 47 if (this.hasFrameTarget) { 48 this.frameTarget.removeEventListener("click", this._rowClickHandler) 49 } 50 } 51 52 // ---- Open paths ---- 53 54 // Custom-event entry â legacy `beverage-sku-list:open-pos-chooser`. 55 open(event) { 56 const { url, triggerEl } = event.detail || {} 57 if (!url || !this.hasFrameTarget) return 58 this._activeTrigger = triggerEl 59 this._writeback = null // legacy path uses on-demand sku_edit lookup 60 this._loadAndShow(url) 61 } 62 63 // Direct action entry â used by MenuItem SKU tab. 64 trigger(event) { 65 const el = event.currentTarget 66 if (!el) return 67 event.preventDefault() 68 const url = el.getAttribute("data-pos-chooser-url") || el.getAttribute("href") 69 if (!url) return 70 this._activeTrigger = el 71 this._writeback = this._buildDeclarativeWriteback(el) 72 this._loadAndShow(url) 73 } 74 75 _loadAndShow(url) { 76 if (!this.hasFrameTarget) return 77 this.frameTarget.setAttribute("src", url) 78 const modal = document.getElementById("pos-sku-chooser-modal") 79 if (modal && typeof modal.showModal === "function") { 80 modal.showModal() 81 } 82 } 83 84 _buildDeclarativeWriteback(triggerEl) { 85 const idInputSel = triggerEl.getAttribute("data-pos-chooser-id-input") 86 if (!idInputSel) return null 87 const scopeSpec = triggerEl.getAttribute("data-pos-chooser-id-input-scope") || "" 88 const scope = this._resolveScope(triggerEl, scopeSpec) 89 const root = scope || document 90 const idInput = root.querySelector(idInputSel) 91 if (!idInput) return null 92 const labelSel = triggerEl.getAttribute("data-pos-chooser-label-display") 93 const labelEl = labelSel ? root.querySelector(labelSel) : null 94 // #2069 â optional price carry-over: fill this SKU's price input with the 95 // chosen POS SKU's price on select. 96 const priceSel = triggerEl.getAttribute("data-pos-chooser-price-input") 97 const priceInput = priceSel ? root.querySelector(priceSel) : null 98 return { idInput: idInput, labelEl: labelEl, priceInput: priceInput, triggerEl: triggerEl } 99 } 100 101 _resolveScope(el, spec) { 102 if (!spec) return null 103 const m = /^closest:(.+)$/.exec(spec) 104 if (m) return el.closest(m[1]) 105 return el.closest(spec) || null 106 } 107 108 // ---- Row click ---- 109 110 // Row click delegated from the <turbo-frame> content â the 111 // for_assigning partial renders `<tr data-id="..." data-sku="..." 112 // data-price="...">`. 113 selectRow(event) {
114 const row = event.target && event.target.closest("tr[data-sku], tr[data-id]") 115 if (!row) return 116 event.preventDefault() 117 const skuId = row.dataset.id 118 const sku = row.dataset.sku 119 const price = row.dataset.price 120 121 if (this._writeback) { 122 // Declarative writeback (path 2). 123 this._writeback.idInput.value = skuId || "0" 124 this._writeback.idInput.dispatchEvent(new Event("change", { bubbles: true })) 125 if (this._writeback.labelEl) { 126 this._writeback.labelEl.textContent = sku ? this._truncateSku(sku) : "None" 127 } 128 // #2069 â carry the chosen POS SKU's price into this SKU's price input. 129 if (this._writeback.priceInput && price) { 130 this._writeback.priceInput.value = price 131 this._writeback.priceInput.dispatchEvent(new Event("change", { bubbles: true })) 132 } 133 this._setTriggerLabel(this._activeTrigger, sku) 134 } else if (this._activeTrigger) { 135 // Legacy `.sku_edit` lookup (path 1 â BeverageSku editor). 136 this._writebackToSkuEditRow(skuId, sku, price) 137 } 138 this._closeModal() 139 } 140 141 _writebackToSkuEditRow(skuId, sku, price) { 142 const skuEdit = this._activeTrigger.closest(".sku_edit") 143 const readRowAncestor = this._activeTrigger.closest("[data-id]") 144 const readRowId = readRowAncestor ? readRowAncestor.dataset.id : null 145 const editRow = skuEdit || 146 (readRowId && document.querySelector(`.sku_edit[data-id="${this._cssEscape(readRowId)}"]`)) 147 if (editRow) { 148 const skuInput = editRow.querySelector(".sku_input") 149 const priceInput = editRow.querySelector(".price_input") 150 if (skuInput) { 151 skuInput.value = skuId || "0" 152 skuInput.dispatchEvent(new Event("change", { bubbles: true })) 153 } 154 if (priceInput && price && priceInput.value === "0.00") { 155 priceInput.value = price 156 } 157 } 158 this._setTriggerLabel(this._activeTrigger, sku) 159 } 160 161 _setTriggerLabel(triggerEl, sku) { 162 if (!triggerEl) return 163 const buttonText = sku ? this._truncateSku(sku) : "None" 164 triggerEl.textContent = buttonText 165 } 166 167 _closeModal() { 168 const modal = document.getElementById("pos-sku-chooser-modal") 169 if (modal && typeof modal.close === "function") { 170 modal.close() 171 } 172 this._activeTrigger = null 173 this._writeback = null 174 } 175 176 _truncateSku(value) { 177 const str = String(value) 178 return str.length > 10 ? "..." + str.slice(-7) : str 179 } 180 181 _cssEscape(value) { 182 if (window.CSS && CSS.escape) return CSS.escape(value) 183 return String(value).replace(/(["\\])/g, "\\$1") 184 } 185};
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.