1/** 2 * Shared Macaly AI builder prompt box. 3 * 4 * Registers the `wlgpiMacalyPrompt` Alpine component used by every theme that 5 * renders a Macaly prompt. It keeps the typed prompt in localStorage so it 6 * survives navigation, lets idea badges prefill it and adds the AI builder 7 * product to the cart with the prompt appended as an item property. 8 * 9 * Themes that render the shared add-to-cart button (which carries its own 10 * `x-on:click="...addItem(...)"`) are supported through a capture phase click 11 * listener: it pre-empts that handler and calls the cart store with 12 * `properties.prompt` added, parsing product / properties / period back out of 13 * the button's own expression instead of duplicating them. Mutating the 14 * attribute (the configurator's approach) is deliberately avoided: Alpine only 15 * rebinds on the next microtask and retains every compiled expression, so 16 * rewriting a long prompt into the attribute leaks memory on each edit. 17 * 18 * @package Wlgpi 19 */ 20 21/** 22 * Matches control characters the Macaly API rejects. Tab, line feed and 23 * carriage return are explicitly allowed and therefore excluded. 24 * 25 * @type {RegExp} 26 */ 27// eslint-disable-next-line no-control-regex -- stripping control characters is the point 28const DISALLOWED_CONTROL_CHARS = 29 /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g; 30 31/** 32 * Matches the `x-on:click` expression the add-to-cart button renders, capturing 33 * the product slug, the properties JSON and the period. 34 * 35 * @type {RegExp} 36 */ 37const ADD_ITEM_EXPRESSION = 38 /addItem\(\s*'([^']*)'\s*,\s*'(\{.*\})'\s*,\s*'([^']*)'\s*\)/; 39 40/** @type {number} Fallback used when the localized config is missing. */ 41const DEFAULT_MAX_LENGTH = 5000; 42 43/** @type {string} Fallback used when the localized config is missing. */ 44const DEFAULT_STORAGE_KEY = "macaly_prompt"; 45 46/** 47 * Localized plugin config, tolerating themes that do not enqueue it. 48 * 49 * @return {Object} Config object, empty when unavailable. 50 */ 51const promptConfig = () => window.wlgpiMacalyPromptConfig || {}; 52 53/** 54 * Read a value from localStorage, tolerating restricted browser contexts. 55 * 56 * @param {string} key Storage key. 57 * @return {string} Stored value or an empty string. 58 */ 59const readStorage = (key) => { 60 try { 61 return window.localStorage.getItem(key) || ""; 62 } catch (error) { 63 return ""; 64 } 65}; 66 67/** 68 * Persist a value to localStorage, tolerating restricted browser contexts. 69 * 70 * @param {string} key Storage key. 71 * @param {string} value Value to store. 72 * @return {void} 73 */ 74const writeStorage = (key, value) => { 75 try { 76 window.localStorage.setItem(key, value); 77 } catch (error) { 78 // Storage is unavailable (private mode, blocked cookies), skip persisting. 79 } 80}; 81 82/** 83 * Read the add-to-cart arguments back out of a CTA's Alpine expression. 84 * 85 * @param {HTMLElement} cta Add-to-cart button. 86 * @return {?{product: string, properties: Object, period: string}} Parsed 87 * arguments, or null when the expression is missing or unrecognised. 88 */ 89const readAddToCartArguments = (cta) => { 90 const expression = 91 cta.getAttribute("x-on:click") || cta.getAttribute("@click") || ""; 92 const match = expression.match(ADD_ITEM_EXPRESSION); 93 94 if (!match) { 95 return null; 96 } 97 98 try { 99 return { 100 product: match[1], 101 properties: JSON.parse(match[2]), 102 period: match[3], 103 }; 104 } catch (error) { 105 return null; 106 } 107}; 108 109/** 110 * Alpine component factory. 111 * 112 * Options may be passed inline (`x-data="wlgpiMacalyPrompt({ product: '...' })"`) 113 * or, preferably, as `data-product`, `data-period` and `data-storage-key` on 114 * the root element with the cart item properties in a child 115 * `<script type="application/json" class="js-macaly-prompt-properties">`. 116 * 117 * @param {Object} [options] Inline options, taking precedence over the dataset. 118 * @return {Object} Alpine component definition. 119 */ 120const wlgpiMacalyPrompt = (options = {}) => ({ 121 prompt: "", 122 product: "", 123 period: "1", 124 properties: {}, 125 storageKey: DEFAULT_STORAGE_KEY, 126 maxLength: DEFAULT_MAX_LENGTH, 127 128 init() { 129 const data = this.$el.dataset; 130 const config = promptConfig(); 131 132 this.storageKey = 133 options.storageKey || 134 data.storageKey || 135 config.storage_key || 136 DEFAULT_STORAGE_KEY; 137 this.maxLength = 138 Number(options.maxLength || data.maxLength || config.max_length) || 139 DEFAULT_MAX_LENGTH; 140 this.product = options.product || data.product || ""; 141 this.period = options.period || data.period || "1"; 142 this.properties = options.properties || this.readProperties(); 143 144 this.prompt = readStorage(this.storageKey).slice(0, this.maxLength); 145 146 this.bindCta(); 147 }, 148 149 /** 150 * Read the cart item properties for the add-to-cart call. 151 * 152 * The `data-properties` attribute is the primary source: block markup on some 153 * platforms passes through wptexturize(), which corrupts JSON inside a 154 * <script> tag with smart quotes. The JSON script child stays as a fallback. 155 * 156 * @return {Object} Properties, empty when absent or malformed. 157 */ 158 readProperties() { 159 if (this.$el.dataset.properties) {
160 try { 161 return JSON.parse(this.$el.dataset.properties); 162 } catch (error) { 163 // Fall through to the JSON script child. 164 } 165 } 166 167 const script = this.$el.querySelector(".js-macaly-prompt-properties"); 168 169 try { 170 return script ? JSON.parse(script.textContent || "{}") : {}; 171 } catch (error) { 172 return {}; 173 } 174 }, 175 176 /** 177 * Set the prompt, respecting the maximum length, and persist it. 178 * 179 * @param {string} value Prompt text. 180 * @return {void} 181 */ 182 setPrompt(value) { 183 this.prompt = (value || "").slice(0, this.maxLength); 184 this.persist(); 185 }, 186 187 /** 188 * Persist the current prompt. 189 * 190 * @return {void} 191 */ 192 persist() { 193 writeStorage(this.storageKey, (this.prompt || "").slice(0, this.maxLength)); 194 }, 195 196 /** 197 * Strip the control characters the Macaly API rejects and trim the result. 198 * 199 * @param {string} [value] Raw value, defaults to the current prompt. 200 * @return {string} Value safe to send as the `prompt` property. 201 */ 202 sanitize(value) { 203 return (value ?? this.prompt ?? "") 204 .replace(DISALLOWED_CONTROL_CHARS, "") 205 .trim(); 206 }, 207 208 /** 209 * Add the product to the cart with the prompt appended as an item property. 210 * 211 * @param {Object} [overrides] Per-call product, properties and period. 212 * @return {?Promise} Cart store promise, or null when the store is missing. 213 */ 214 addToCart(overrides = {}) { 215 this.persist(); 216 217 const cart = window.Alpine ? window.Alpine.store("cart") : null; 218 219 if (!cart) { 220 return null; 221 } 222 223 const prompt = this.sanitize(); 224 const properties = { 225 ...this.properties, 226 ...(overrides.properties || {}), 227 // An empty prompt must not send the property at all. 228 ...(prompt ? { prompt } : {}), 229 }; 230 231 return cart.addItem( 232 overrides.product || this.product, 233 JSON.stringify(properties), 234 overrides.period ?? this.period 235 ); 236 }, 237 238 /** 239 * Intercept the shared add-to-cart button rendered inside the prompt box. 240 * 241 * Capture phase on the root, so this runs before the button's own Alpine 242 * click handler and can replace it. Anything unexpected (no prompt, 243 * unparsable expression, cart store missing) falls through to that handler. 244 * 245 * @return {void} 246 */ 247 bindCta() { 248 this.$el.addEventListener( 249 "click", 250 (event) => { 251 const cta = event.target.closest(".js-macaly-prompt-cta"); 252 253 if (!cta || !this.$el.contains(cta)) { 254 return; 255 } 256 257 // An empty prompt leaves the button's own handler in charge. 258 if (!this.sanitize()) { 259 return; 260 } 261 262 const args = readAddToCartArguments(cta); 263 const cart = window.Alpine ? window.Alpine.store("cart") : null; 264 265 if (!args || !cart) { 266 return; 267 } 268 269 event.stopImmediatePropagation(); 270 event.preventDefault(); 271 272 this.addToCart(args); 273 }, 274 true 275 ); 276 }, 277}); 278 279document.addEventListener("alpine:init", () => 280 window.Alpine.data("wlgpiMacalyPrompt", wlgpiMacalyPrompt) 281);
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.