PageSourceSearch

https://pip.pipanz.com/assets/controllers/brand-site/product_stock_controller-dMMu7p9.js

js pipanz.com collected 2026-10-07 02:29:31 UTC 7,129 bytes, 239 lines download raw bytes

1/* stimulusFetch: 'lazy' */
2import { Controller } from '@hotwired/stimulus';
3
4/**
5 * @interface OptionDisplayType
6 */
7/**
8 * @function
9 * @name OptionDisplayType#getControls
10 * @returns {NodeListOf<HTMLElement>}
11 */
12/**
13 * @function
14 * @name OptionDisplayType#getValue
15 * @returns {string|undefined|null}
16 */
17/**
18 * @function
19 * @name OptionDisplayType#styleElm
20 * @param {HTMLElement} elm
21 * @returns {HTMLElement}
22 */
23
24/**
25 * Radio option driver, radio options are displayed as a group of radio buttons enveloped in a label element which is
26 * used as the stylable element
27 * @implements {OptionDisplayType}
28 */
29class RadioOption {
30    elm;
31    name;
32    constructor(attrElm) {
33        this.elm = attrElm;
34        this.name = attrElm.dataset.attr;
35    }
36    getControls() {
37        return this.elm.querySelectorAll('input[type="radio"][name="' + controlName(this.name) + '"]');
38    }
39    styleElm(elm) {
40        return elm.closest('label');
41    }
42    getValue() {
43        return this.elm.querySelector('input[type="radio"][name="' + controlName(this.name) + '"]:not([disabled]):checked')?.value;
44    }
45}
46
47/**
48 * Select option driver, select options are displayed as a select dropdown with the options themselves being the
49 * stylable elements
50 * @implements {OptionDisplayType}
51 */
52class SelectOption {
53    elm;
54    name;
55    constructor(attrElm) {
56        this.elm = attrElm;
57        this.name = attrElm.dataset.attr;
58    }
59    getControls() {
60        return this.elm.querySelectorAll('select[name="' + controlName(this.name) + '"] option:not([disabled])');
61    }
62    styleElm(elm) {
63        return elm;
64    }
65    getValue() {
66        return this.elm.querySelector('select[name="' + controlName(this.name) + '"]:not([disabled])')?.value;
67    }
68}
69
70class HiddenOption {
71    elm;
72    name;
73    constructor(attrElm) {
74        this.elm = attrElm;
75        this.name = attrElm.dataset.attr;
76    }
77    getControls() {
78        return [this.elm];
79    }
80    styleElm(elm) {
81        return elm;
82    }
83    getValue() {
84        return this.elm.value;
85    }
86}
87
88/**
89 * Defines the control name, modify this if you change the form field names.
90 * @param name
91 * @returns {string}
92 * @constructor
93 */
94const controlName = function (name) {
95    return 'attribute[' + name + ']';
96}
97
98/**
99 * Option type driver mapping, maps to the "display" field of ProductDetailsComponent
100 */
101const optionTypes = {
102    'I': RadioOption,
103    'B': RadioOption,
104    'S': SelectOption,
105    'H': HiddenOption
106};
107
108// noinspection JSUnusedGlobalSymbols loaded via stimulus
109/**
110 * Provides a stock-aware product attributes output.
111 * Requires attribute as a DOM element with [data-attr="attribute id"] containing the attribute label and options. Each
112 * option value is the option id
113 * @property {{[string]: int}} stockValue JSON serialized product stock key/value store with normalized attribute
114 *                                        combination stock levels @see ProductTemplateController
115 */
116export default class extends Controller {
117    /**
118     * @type {SelectOption[]|RadioOption[]}
119     */
120    #attrs = [];
121
122    /**
123     * @type {HTMLInputElement|HTMLButtonElement}
124     */
125    #submitElm;
126
127    /**
128     * @type {HTMLInputElement|null}
129     */
130    #qtyElm;
131
132    static values = {
133        'stock': Object,
134    }
135    connect() {
136        /** @type {HTMLElement} */
137        const element = this.element;
138        this.#attrs = [];
139        this.#submitElm = element.querySelector('button[type="submit"],input[type="submit"]');
140        this.#qtyElm = element.querySelector('[data-qty]');
141        element.querySelectorAll('[data-attr]').forEach(attrElm => {
142            const type = attrElm.dataset.type ?? 'H';
143            const attr = new optionTypes[type](attrElm);
144            this.#attrs.push(attr);
145            attr.getControls().forEach(input => {
146                input.addEventListener('change', () => this.onChange(input));
147            });
148        });
149        this.applyAttrs();
150    }
151
152    /**
153     * Converts an array of attribute option ids into a key for the stock value map.
154     * @param {(string|undefined)[]} optionSet
155     * @returns {string}
156     */
157    getKey(optionSet) {
158        return '_' + optionSet.filter(v => v !== undefined).sort(this.naturalSort).join('_');
159    }
160
161    /**
162     * Returns the current active options for a given attribute.
163     * @returns {(string|undefined)[]}
164     */
165    getOptionSet() {
166        return this.#attrs.map((attr, index) => {
167            // a specific nuance, we show/hide available options based on configured override in the data model, so we
168            // make sure he last attr doesn't affect the display of preceding attrs
169            if (index === this.#attrs.length - 1) {
170                return undefined;
171            }
172            return attr.getValue() || undefined;
173        });
174    }
175
176    onChange(input) {
177        const attrElm = input.closest('[data-attr]');
178        if (!attrElm) return;
179        const optionSet = this.getOptionSet();
180        this.#attrs.forEach((attr, index) => {
181            attr.getControls().forEach(control => {
182                // apply current option value to active set and check if it's available/has stock
183                let currentSet = optionSet.slice();
184                currentSet[index] = control.value;
185                const key = this.getKey(currentSet);
186                // availability check
187                if (this.stockValue[key] === undefined) {
188                    attr.styleElm(control).classList.add('hidden');
189                } else {
190                    attr.styleElm(control).classList.remove('hidden');
191                }
192                // stock check
193                if ((this.stockValue[key]||0) === 0) {
194                    control.disabled = true;
195                    attr.styleElm(control).classList.add('unavailable');
196                } else {
197                    control.disabled = false;
198                    attr.styleElm(control).classList.remove('unavailable');
199                }
200            });
201        });
202        this.applyAttrs();
203    }
204
205    naturalSort(a, b) {
206        if (parseFloat(a) && parseFloat(b)) {
207            return parseFloat(a) - parseFloat(b);
208        } else {
209            return a < b ? -1 : a > b ? 1 : 0;
210        }
211    }
212
213    /**
214     * Enables/disables the qty field and submit button depending on the attr validity
215     */
216    applyAttrs () {
217        if (!this.validAttrs()) {
218            this.#submitElm.disabled = true;
219            this.#qtyElm?.classList.add('hidden');
220        } else {
221            this.#submitElm.disabled = false;
222            if (!this.#qtyElm) {
223                return;
224            }
225            const max = this.stockValue[this.getKey(this.#attrs.map(attr => attr.getValue()))];
226            this.#qtyElm.querySelector('input')?.setAttribute('max', max);
227            this.#qtyElm.querySelector('[data-qty-max]').innerHTML = max;
228            this.#qtyElm.classList.remove('hidden');
229        }
230    }
231
232    /**
233     * Check all attrs have valid values
234     * @returns {boolean}
235     */
236    validAttrs() {
237        return this.#attrs.reduce((carry, attr) => carry && attr.getValue(), true);
238    }
239}

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.