PageSourceSearch

https://secure.redlineart.org/nx/portal-static/js/portal.all.js

js redlineart.org collected 2026-09-25 14:22:36 UTC 186,220 bytes, 4,468 lines download raw bytes

1window.NeonMultiselect = {
2    name: 'neon-multiselect',
3    props: {
4        placeholder: {
5            type: String,
6            default: 'All'
7        },
8        multiSelect: {
9            type: Boolean,
10            default: true
11        },
12        items: {
13            type: Array,
14            default() {
15                return []
16            }
17        },
18        listClass: {
19            type: String,
20            default: ''
21        },
22        initValue: {
23            type: String,
24            default: ''
25        },
26        clearable: {
27            type: Boolean,
28            default: true
29        }
30    },
31    data() {
32        return {
33            uuid: new Date().getTime() + '-' + ((Math.random() + '').replace('0.', '')),
34            show: false,
35            selectedItems: []
36        }
37    },
38    methods: {
39        toggleItem(item) {
40            let idx = this.selectedItems.findIndex(item_ => item.value === item_.value)
41            if (idx === -1) {
42
43                if (!this.multiSelect) {
44                    this.clear(false)
45                }
46
47                this.selectedItems.push(item)
48            } else if (this.multiSelect) {
49                this.selectedItems.splice(idx, 1)
50            }
51
52            if (!this.multiSelect) {
53                this.show = false
54            }
55        },
56        isSelectedItem(item) {
57            return this.selectedItems.findIndex(item_ => item.value === item_.value) !== -1
58        },
59        clear(hide) {
60            this.selectedItems.splice(0, this.selectedItems.length)
61            if (hide) {
62                this.show = false
63            }
64        },
65        handleDocumentClick(e) {
66            let container = e.target.closest('.neon-multi-select')
67            if (!container || container.getAttribute('uuid') !== this.uuid) {
68                this.show = false
69            }
70        },
71        selectItemsByInitValue(value) {
72            let arr = value.split(',')
73            for (let i = 0; i < arr.length; i++) {
74                let res = this.items.filter(item => item.value === arr[i])
75                if (res && res.length > 0) {
76                    this.toggleItem(res[0])
77                }
78            }
79        }
80    },
81    computed: {
82        displayText() {
83            if (this.selectedItems.length === 0) {
84                return this.placeholder
85            }
86            return this.selectedItems[0].name
87        }
88    },
89    watch: {
90        show() {
91            if (!this.show) {
92                this.$emit('hide', this.selectedItems)
93            }
94        },
95        initValue(newVal) {
96            if (newVal) {
97                this.selectItemsByInitValue(newVal)
98            }
99        }
100    },
101    mounted() {
102        if (this.initValue) {
103            this.selectItemsByInitValue(this.initValue)
104        }
105        document.addEventListener('mouseup', this.handleDocumentClick)
106    },
107    unmounted() {
108        document.removeEventListener('mouseup', this.handleDocumentClick)
109    },
110    template: `
111       <div class="neon-multi-select" :uuid="uuid">
112           <div class="placeholder" @click="show=!show">
113            <span :class="{'has-val' : selectedItems.length >0}">{{displayText}}</span>
114            <span :class="{'has-val' : selectedItems.length >0}" style="margin-left: 3px;" v-if="multiSelect && selectedItems.length>1">+{{selectedItems.length - 1}}</span>
115            <span style="margin-left: 3px;">
116                <i :class="{'fas fa-caret-down' : !show, 'fas fa-caret-up':show}"></i>
117            </span>
118           </div>
119           <div class="list" :class="listClass" v-show="show">
120                <div class="up-arrow"></div>
121                <ul class="items">
122                    <li v-for="item in items" 
123                        class="item"
124                        :class="{'is-selected': isSelectedItem(item)}" 
125                        @click="toggleItem(item)">
126                        <slot name="item" :item="item">{{item.name}}</slot>
127                    </li>
128                </ul>
129                <div class="footer" v-if="selectedItems.length >0 && clearable">
130                    <span @click="clear(true)">Clear</span>
131                </div>
132           </div>
133       </div>`
134}
135window.NeonFileupload= {
136    name: 'neon-fileupload',
137    props: {
138        uploadId: {
139            type: String,
140            default: ''
141        },
142        multiple: {
143            type: Boolean,
144            default: true,
145        },
146        accept: {
147            type: String,
148            default: '',
149        },
150        /**
151         * Comma-separated extensions from a CO FILE field (allowFileType), e.g. "pdf,doc".
152         * When set, this list replaces the default extension allow-list for both picker and
153         * addFile validation. Not the same as accept (HTML MIME / dotted extensions).
154         */
155        allowedTypes: {
156            type: String,
157            default: '',
158        },
159        uploadurl: {
160            type: String,
161            default: '',
162        },
163        uwidth: {
164            type: Number,
165            default: null,
166        },
167        uheight: {
168            type: Number,
169            default: null,
170        },
171        borderstyle: {
172            type: String,
173            default: 'dashed',
174        },
175        filesize: {
176            type: Number,
177            default: 1000,
178        },
179        unit: {
180            type: String,
181            default: 'MB'
182        },
183        isupload: {
184            type: Boolean,
185            default: false,
186        },
187        filename: {
188            type: Boolean,
189            default: 'file',
190        },
191        paramsobj: {
192            type: Object,
193            default: {},
194        },
195        defaultFiles: {
196            type: Array,
197            default: [],
198        },
199        async: {
200            type: Boolean,
201            default: true,
202        },
203        clearWithInit: {
204            type: Boolean,
205            default: false,
206        },
207        extension: {
208            type: Array,
209            default: ['mobi', 'km', 'me', 'ppk ', 'jtl', 'dat', 'one', 'kdc', 'stl', 'midi', 'png', 'thmx', 'conf', 'pnm', 'mht', 'dwg', 'mj2', 'xroles', 'onepkg', 'csv', 'jar', 'exr', 'exp', 'fh50', 'crw', 'srf', 'scala', 'aifc', 'slddrw', 'ecelp7470', 'aiff', 'jb2', 'iso19139', 'css', 'perl', 'f90', 'fh40', 'am', 'amr', 'f77', 'as', 'au', 'mef', 'sql', 'pkg', 'bpg', 'ad', 'ac', 'dtd', 'bash', 'dts', 'heif', 'junit', 'al', 'bpm', 'properties', 'kar', 'aj', 'bpk', 'heic', 'mli', 'sxi', 'sxw', 'webp', 'html', 'idl', 'data', 'mmpt', 'jfi', 'pro', 'mp4v', 'xbm', 'psd', 'mhtml', 'icb', 'mkd', 'asx', 'cs', 'rw2', 'mp4a', 'mkv', 'ics', 'webm', 'ico', 'cc', 'cxx', 'sxc', 'sxd', 'cl', 'ppj', 'arw', 'rtf', 'svg', 'lha', 'pps', 'txt', 'flac', 'ppm', 'ppz', 'ft12', 'ft11', 'asm', 'ft10', 'lhs', 'ppt', 'asf', 'r3d', 'xsamples', 'mka', 'asp', 'htaccess', 'groovy', 'mmmp', 'mid', 'abs-menulinks', 'g3', 'pod', 'dxf', 'rst', 'dxb', 'pom', 'mp2a', 'pot', 'jfif', 'el', 'ppa', 'ft', 'mjp2', 'pict', 'mpg', 'fv', 'nrw', 'mpt', 'mpp', 'xmind', 'fbs', 'fh', 'mpx', 'wdb', 'sldasm', 'mime', 'fn', 'gv', 'vsdm', 'markdown', 'jif', 'xdp', 'distz', 'ost', 'mos', 'diff', 'install', 'mov', 'mpe', 'wcm', 'zip', 'i3', 'vsdx', 'go', 'mng', 'sldprt', 'roles', 'ifb', 'hx', 'mp3', 'mp4', 'wax', 'xweb', 'mp2', 'wav', 'npx', 'fb2', 'ptx', 'awk', 'hh', 'mod', 'hs', 'xfdf', 'hp', 'ief', 'mmr', 'mmp', 'pst', 'xcf', 'ig', 'rest', 'avi', 'rwz', 'orf', 'in', 'xlog', 'jx', 'fff', 'list', 'lrm', 'fh4', 'fh5', 'dwfx', 'm2v', 'jp2', 'sldm', 'mdtext', 'jbig2', 'm3a', 'potm', 'fh8', 'jpgm', 'fh9', 'jl', 'fh7', 'sldx', 'coffee', 'xargs', 'jpgv', 'js', 'pptx', 'msp', 'owl', 'xht', 'm1v', 'mst', 'pyv', 'xif', 'jmx', 'm4', 'lisp', 'm3', 'jng', 'lrf', 'm2a', 'ppsx', 'pxn', 'vcs', 'keys', 'icns', 'mrw', 'md', 'vda', 'jpeg', 'xcat', 'mb', 'ma', 'wsdd', 'mg', 'mf', 'pya', 'pptm', 'ecelp9600', 'xltx', 'msi', 'xltm', 'n3', 'msg', 'rexx', 'epub', 'kml', 'opus', 'log', 'dotm', 'nb', 'iiq', 'readme', 'xlsb', 'ml', 'dotx', 'kmz', 'vcf', 'ppsm', 'xlsx', 'haml', 'xlsm', 'ntf', 'mxu', 'raw', 'lvp', 'tex', 'ras', 'config', 'cdr', 'bibtex', 'xquery', 'movie', 'project', 'makefile', 'aac', 'adoc', 'text', '3fr', 'cwiki', 'xlz', 'def', 'ocaml', 'smil', 'h263', 'h261', 'h264', 'ipa', 'bay', 'xmp', 'bau', 'xml', 'raf', 'bas', 'bat', 'rar', 'viv', 'ram', 'wks', 'scm', '3g2', 'jpm', 'ini', 'sas', 'vhd', 'xlf', 'json', 'inx', 'xld', 'xwelcome', 'k25', 'm4u', 'xlc', 'm4v', 'xla', 'xhtml', 'cbl', 'btif', 'xll', 'lua', 'pm', 'xlm', 'pl', 'pp', 'xlw', 'xlt', 'xlr', 'xls', 'py', 'ra', 'abs-linkmap', 'caf', 'h++', 'lsp', 'cab', 'gif', 'rb', 'ims', 'm3u', 'potx', 'dcl', 'm4a', 'mseq', 'm4b', 'dcx', 'cat', 'qt', 'dcs', 'jpf', 'jpg', 'fhc', 'jpe', 'tcl', 'chm', 'dist', 'wpl', 'sh', 'djv', 'ads', 'adp', 'wps', 'pbm', 'php3', 'oxps', 'shw', 'wbmp', 'emf', 'aspx', 'cgm', 'dtshd', 'cgi', 'tk', 'tif', 'mpeg', 'lzh', 'so', 'mpg4', 'st', 'pas', 'itk', 'less', 'ada', 'adb', 'elc', 'cfm', 'hpp', 'xgrm', 'cfg', 'flv', 'asnd', 'xps', 'rdf', 'nitf', 'xpm', 'restx', 'dif', 'dib', 'wma', 'cfml', 'jsp', 'vm', 'sed', 'classpath', 'wmf', 'tga', 'ihtml', 'j2c', 'wmx', 'fli', 'cfc', 'wmv', '3gp', 'flc', 'ft8', 'sfdu', 'ft9', 'ft7', 'java', 'wm', 'odf', 'xlam', 'cmd', 'doc', 'ppam', 'patch', 'docm', 'aif', 'odt', 'xegrm', 'dms', 'eot', 'x3f', 'bin', 'pef', 'vstx', 'xq', 'meta', 'ibooks', 'pen', 'clj', 'nar', 'dng', 'applescript', 'ent', 'xmap', 'pcx', 'pdf', 'fpx', 'vssx', 'tld', 'vstm', 'bib', 'manifest', 'tiff', 'eol', 'c++', 'dump', 'xsd', 'for', 'cil', 'mpga', 'eml', 'emz', 'php4', 'types', 'xsl', 'djvu', 'rgb', 'mmas', 'vssm', 'mmat', 'asciidoc', 'mmap', 'xsp', 'grm', 'pct', 'htm', 'xlex', 'mdb', 'rng', '4th', 'f4v', 'etx', 'asice', 'rnc', 'fvt', 'yaml', 'mdi', 'sr2', 'spx', 'deploy', 'vhdl', 'rnx', 'c', 'pic', 'd', 'e', 'f', 'oga', 'g', 'xliff', 'ecelp4800', 'h', 'bmp', 'l', 'm', 'rmi', 'p', 'ogg', 'nef', 'r', 's', 'asics', 'drf', 'v', 'svgz', 'y', 'rmp', 'drc', 'ogm', 'cr2', 'cpp', 'ogv', 'egrm', 'hxx', 'snd', 'rlc', 'fh12', 'dpr', 'fh11', 'fh10', 'aart', 'dpx', 'php', 'wvx', 'vtt', 'cob', 'xconf', 'dot', 'vsd', 'fst', 'docx', 'xwd', 'sml', 'cmx', 'smi', 'vsl', 'vsw', 'erf', 'vss', 'pgm', 'vst', 'erl']
210        },
211        showFileList: {
212            type: Boolean,
213            default: true,
214        },
215        preview: {
216            type: Boolean,
217            default: false,
218        },
219        autoPreview: {
220            type: Boolean,
221            default: true,
222        },
223        previewUrl: {
224            type: String,
225            default: '',
226        },
227        previewImageClass: {
228            type: String,
229            default: '',
230        }
231    },
232    data() {
233        return {
234            isDragging: false,
235            dragCount: 0,
236            isBig: false,
237            showProcess: false,
238            files: this.defaultFiles,
239            uploadPrecent: 0,
240            isNull: false,
241            isType: false,
242            rejectedType: '',
243            errorMessage: '',
244            isError: false,
245            imageUrl: '',
246        }
247    },
248    methods: {
249        uploadFile() {
250            this.$refs.fileinput.dispatchEvent(new MouseEvent('click'));
251        },
252        onInputChange(e) {
253            const files = e.target.files;
254            Array.from(files).forEach(file => this.addFile(file));
255        },
256        addFile(file) {
257            this.isNull = false;
258            this.isType = false;
259            this.rejectedType = '';
260            this.isBig = false;
261            this.isError = false;
262            if (file) {
263                const fileSize = file.size;
264                const fileName = file.name;
265                if (fileSize === 0){
266                    this.isNull = true;
267                    return;
268                }
269                const supportedFileTypes = this.resolvedExtensions()
270                if (supportedFileTypes.length > 0) {
271                    if (fileName.indexOf('.') > -1) {
272                        let type = fileName.substring(fileName.lastIndexOf('.') + 1)
273                        if(supportedFileTypes.indexOf(type.toLowerCase()) < 0) {
274                            this.isType = true;
275                            this.rejectedType = type.toLowerCase();
276                            return;
277                        }
278                    } else if (this.allowedTypes) {
279                        this.isType = true;
280                        this.rejectedType = '';
281                        return;
282                    }
283                }
284                const isExceed = ("KB" === this.unit? fileSize / 1024 : f
284ileSize / 1024 / 1024)  < this.filesize;
285                if (!isExceed) {
286                    this.isBig = true;
287                    file = '';
288                    return
289                }
290            }
291            if (this.files.length === 1 && !this.multiple){
292                this.files[0] = file;
293            }else {
294                this.files.push(file);
295            }
296            if (this.preview && this.autoPreview) {
297                this.imageUrl = URL.createObjectURL(file);
298            }
299            if (this.async) {
300                this.upload();
301            }else {
302                this.$emit('fileselected',this.getResponse(file));
303            }
304        },
305        getFileSize(size) {
306            const fSExt = ['Bytes', 'KB', 'MB', 'GB'];
307            let i = 0;
308            while(size > 900) {
309                size /= 1024;
310                i++;
311            }
312            return `${(Math.round(size * 100) / 100)} ${fSExt[i]}`;
313        },
314        getFileName(str) {
315            return str.split('_copyCustomerFile_')[0]
316        },
317        removeFile (v,i) {
318            this.files.splice(i,1);
319            this.$refs.fileinput.value = '';
320            this.imageUrl = ''
321            this.$emit('remove',this.getResponse(v));
322        },
323        onDragEnter(e) {
324            e.preventDefault();
325            this.dragCount++;
326            this.isDragging = true;
327            return false;
328        },
329        onDragLeave(e) {
330            e.preventDefault();
331            this.dragCount--;
332            if (this.dragCount <= 0)
333            this.isDragging = false;
334        },
335        onDrop(e) {
336            e.preventDefault();
337            e.stopPropagation();
338            this.isDragging = false;
339            const files = e.dataTransfer.files;
340            Array.from(files).forEach(file => this.addFile(file));
341        },
342        clearFileList() {
343          this.files = [];
344        },
345        async upload() {
346            const formData = new FormData();
347            this.showProcess = true;
348            let config = {
349                headers: {
350                    'Content-Type': 'multipart/form-data'
351                },
352                onUploadProgress: (progressEvent) => {
353                    this.uploadPrecent = (((progressEvent.loaded / progressEvent.total).toFixed(2) * 100) | 0);
354                }
355            };
356            this.files.forEach(file => {
357                formData.append(this.filename, file);
358            });
359            if (this.paramsobj){
360                for (let i in this.paramsobj) {
361                    formData.append(i, this.paramsobj[i])
362                }
363            }
364            await axios.post(`${this.uploadurl}`, formData, config)
365                .then(response => {
366                    this.uploadPrecent = 0;
367                    this.showProcess = false;
368                    let data = response.data;
369                    if(!data.success){
370                        this.isError = true;
371                        this.files = [];
372                    }
373                    this.$emit('uploaded', this.getResponse(response.data));
374                })
375                .catch(error => {
376                    this.showProcess = false;
377                    this.$emit('uploaderror',error);
378                })
379        },
380        getResponse(obj) {
381            return {key: this.uploadId, obj: obj}
382        },
383        resolvedExtensions() {
384            if (this.allowedTypes) {
385                return String(this.allowedTypes).split(',').map(function (s) {
386                    return s.trim().toLowerCase();
387                }).filter(Boolean);
388            }
389            const ext = this.extension;
390            if (Array.isArray(ext)) {
391                return ext;
392            }
393            if (ext) {
394                return String(ext).split(',').map(function (s) {
395                    return s.trim().toLowerCase();
396                }).filter(Boolean);
397            }
398            return [];
399        },
400    },
401    computed: {
402        uploadPrecentStyle() {
403            return `width:${this.uploadPrecent}%;`
404        },
405        fileSizeFormat(){
406            if (!this.filesize) return 0
407            const value = this.filesize + ''
408            const regex = /[^0-9]/g;
409            const cleanValue = value.replace(regex, '');
410            return cleanValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
411        },
412        acceptedFileTypes() {
413            if (this.accept) {
414                return this.accept;
415            }
416            const types = this.resolvedExtensions()
417            if (types.length === 0) {
418                return undefined;
419            }
420            // Explicit CO allow-list: always reflect it on the picker. The length < 10 cap is only
421            // to avoid stuffing the huge default extension array into the accept attribute.
422            if (this.allowedTypes || types.length < 10) {
423                return types.map(function (v) { return '.' + v; }).join(',');
424            }
425            return undefined;
426        },
427    },
428    watch: {
429        defaultFiles: {
430            handler: function(newVal, oldVal) {
431                if(newVal && newVal.length>0) {
432                    this.files = newVal
433                }
434            },
435            deep: true
436        },
437        previewUrl(newVal) {
438            this.imageUrl = newVal;
439        }
440    },
441    mounted() {
442        if(this.clearWithInit){
443            this.clearFileList();
444        }
445        this.imageUrl = this.previewUrl;
446    },
447    unmounted() {
448
449    },
450    template: `
451       <div class="neon-file-upload"
452            tabindex="0"
453            :id="'neon-file-upload-box-' + uploadId"
454            @dragenter="onDragEnter"
455            @dragleave="onDragLeave"
456            @dragover.prevent
457            @drop="onDrop"
458            :style="{'width':uwidth+'px'}"
459            >
460           <div class="placeholder" 
461                :class="{dragging: isDragging,error: isupload}"
462                :style="{'min-height':uheight+'px','border-style':borderstyle}"
463                >
464               <div class="upload-content" v-if="!(preview && imageUrl)">
465                <i class="fas fa-cloud-upload-alt fa-fw"></i>
466                <p>Drop file here or <a style="color: #0074D9;">click to upload</a></p>
467               </div>
468             <label @click.self="uploadFile"></label>
469             <div class="upload-loading" v-if="showProcess">
470                <span>
471                    <i class="fad fa-spinner"></i> 
472                    <p>Uploading...</p>
473                </span>
474            </div>
475            <input 
476                  type="file" 
477                  style="display: none;" 
478                  name="file" 
479                  :multiple="multiple" 
480                  ref="fileinput" 
481                  :accept="acceptedFileTypes" 
482                  class="upload-input" 
483                  @change="onInputChange">
484              <img v-if="preview && imageUrl" :src="imageUrl" class="image-preview" :class="previewImageClass"/>
485           </div>
486           <div class="upload-tip error-text">
487                <span v-if="isBig"><i class="fa fa-exclamation-triangle"></i>The file size must be less than {{fileSizeFormat}}{{unit}}</span>
488                <span v-if="isNull"><i class="fa fa-exclamation-triangle"></i>Can't upload empty file.</span>
489                <span v-if="isType"><i class="fa fa-exclamation-triangle"></i>{{ rejectedType ? (rejectedType + ' is not an allowed file type') : 'This file type is not allowed' }}</span>
490                <span v-if="isError"><i class="fa fa-exclamation-triangle"></i>This file is invalid,virus scan failed.</span>
491                <span v-if="isupload">
492                    <slot name="error"></slot>
493                </span>
494           </div>
495           <div class="upload-list" v-if="showFileList">
496               <ul>
497                   <li v-for="(v,i) in files" :key="i">
498                    <a class="upload-list-item-name">
499                      <span>
500                       <i class="fa-solid fa-file"></i>          
501                      </span>
502                      <span class="file-name">{{getFileName(v.name)}}</span>
503                    </a>
504                    <span class="file-size" v-if="v.size">{{getFileSize(v.size)}}</span>
505                    <label class="upload-list-item-status-label"><i class="upload-success"></i></label>
506                    <span class="upload-remove" @click="removeFile(v,i)"><i class="fal fa-times"></i></span>
507                   </li>
508               </ul>
509               <div class="upload-precent" v-show="showProcess">
510                    <div class="upload-progress-bar" :style="uploadPrecentStyle"></div>           
511                </div>
512           </div>
513       </div>`
514}
515window.NeonImgCutter= {
516    name: 'neon-img-cutter',
517    props: {
518        boxWidth: {
519            type: Number,
520            default: 372,
521            required: false,
522        },
523        boxHeight: {
524            type: Number,
525            default: 279,
526            required: false,
527        },
528        isModal: {
529            type: Boolean,
530            default: true,
531            required: false,
532        },
533        isUplodedImg: {
534            type: String,
535            default: ''
536        },
537        onlyCutter: {
538            type: Boolean,
539            default: false
540        }
541    },
542    data() {
543        return {
544            cropper: null,
545            imgSrc: null,
546            originalImgSrc: '',
547            isPreviewImg: false,
548            customFile: null,
549            drawImg:{
550                img:''
551            },
552            showToolBox: true,
553        }
554    },
555    watch: {
556       isModal: {
557          handler(newVal){
558              if(newVal){
559                  this.$nextTick(() => {
560                      this.initializeCropper()
561                  })
562              }
563          }
564       },
565        isUplodedImg: {
566           handler(newVal) {
567               if(newVal) {
568                   this.imgSrc = this.isUplodedImg
569               }
570           }
571        }
572    },
573    methods: {
574        initializeCropper() {
575            this.showToolBox = false
576            if (this.cropper) {
577                this.cropper.destroy()
578            }
579            if (this.originalImgSrc) {
580                this.$nextTick(()=>{
581                    const canvas = this.$refs.cropperImg
582                    this.cropper = new Cropper(canvas, {
583                        aspectRatio: 1,
584                        viewMode: 1,
585                        cropBoxResizable: false,
586                        dragMode: 'move',
587                        background: false,
588                        guides: false,
589                        scalable: false,
590                        zoomOnWheel: false,
591                        movable: false,
592                        center: false,
593                        minCropBoxWidth: 200,
594                        minCropBoxHeight: 200,
595                        autoCropArea: 0.4,
596                        responsive: true,
597                    });
598                })
599            }
600        },
601        putImgToCanv(e) {
602            let file
603            if (e.target.files) {
604                file = e.target.files[0] || null
605            } else {
606                return false
607            }
608            if(file){
609                if(file.type!=='image/png' && file.type!=='image/gif' && file.type!=='image/jpeg'){
610                    alert('Please choose jpg/gif/png format picture.')
611                    app.resetProfilePhoto()
612                    return
613                }
614                const fileSize = file.size
615                const isLt10M = fileSize/1024 < 500
616                if (!isLt10M) {
617                    alert('The uploaded photo size cannot exceed 500KB.')
618                    file = ''
619                    app.resetProfilePhoto()
620                    return
621                }
622                this.isPreviewImg = false
623                const reader = new FileReader();
624                reader.onload = (e) => {
625                    this.originalImgSrc  = e.target.result
626                    this.initializeCropper()
627                };
628                reader.readAsDataURL(file)
629            }
630        },
631        ediitProfilePicture() {
632            this.isPreviewImg = false
633            this.initializeCropper()
634        },
635        cropPicture() {
636            if (this.cropper) {
637                const croppedCanvas = this.cropper.getCroppedCanvas({
638                    width: 372,
639                    height: 279
640                });
641                const croppedImage = croppedCanvas.toDataURL("image/png")
642                this.customFile = this.dataURLtoFile(croppedImage, "cropped.png");
643                this.imgSrc = croppedImage
644                this.isPreviewImg = true
645                this.showToolBox = true
646                this.cropper.destroy()
647            }
648        },
649        dataURLtoFile(dataurl, filename) {
650            let arr = dataurl.split(','),
651                mime = arr[0].match(/:(.*?);/)[1],
652                bstr = atob(arr[1]),
653                n = bstr.length,
654                u8arr = new Uint8Array(n)
655
656            while (n--) {
657                u8arr[n] = bstr.charCodeAt(n)
658            }
659            return new File([u8arr], filename, { type: mime })
660        },
661        handleClosed(){
662            this.closedModal()
663            this.$emit('neonimgclearall')
664        },
665        closedModal() {
666            if(this.isUplodedImg) {
667                this.imgSrc = this.isUplodedImg
668            }else {
669                this.imgSrc = null
670            }
671            this.originalImgSrc = ''
672            this.isPreviewImg = false
673            this.customFile = null
674            if(this.cropper){
675                this.cropper.clear()
676                this.cropper.destroy()
677            }
678        },
679        saveProfilePicture() {
680            app.uploadProfilePhoto(this.customFile)
681            this.handleClosed()
682        },
683        backButton(){
684           if(this.originalImgSrc && !this.isPreviewImg){
685             this.showToolBox = true
686               this.$nextTick(function (){
687                   const dom = document.getElementById('account-profile-photo')
688                   if (dom) {
689                          dom.value = ''
690                   }
691               })
692             this.closedModal()
693               if (this.onlyCutter) {
694                   this.$emit('neonimgclearall');
695               }
696           }else {
697              this.ediitProfilePicture()
698           }
699        }
700
701    },
702    mounted() {
703        if(this.isUplodedImg) {
704            this.imgSrc = this.isUplodedImg
705        }
706        if(this.onlyCutter){
707            this.originalImgSrc = this.isUplodedImg
708            this.initializeCropper()
709        }
710    },
711    computed: {
712
713    },
714    template: `<div class="neon-img-cutter">      
715    <div class="nic-header">
716        <span @click="handleClosed"><i class="fal fa-times"></i></span>
717    </div>
718    <div class="nic-body">
719        <div class="nicb-title">
720           <span>Add profile picture</span>
721        </div>
722        <div class="nicb-body" ref="copperbody">
723            <div class="toolMain"> 
724                <div ref="toolBox" class="toolBox" v-show="showToolBox">                       
725                    <div class="tips">                
726                      <div class="imgCutDownBox">
727                            <div class="imgCutDownBoxContainer">
728                                <img ref="image" :src="imgSrc" v-if="!!imgSrc" alt="" />
729                                <i v-else class="fal fa-user"></i>
730                            </div>
731                       </div>                    
732                    </div>                                
733                </div>
734            </div>        
735            <img :src="originalImgSrc" class="img-canvas" ref="cropperImg">      
736        </div>
737        <div class="nicb-footer">
738        <div v-if="!isPreviewImg">
739           <label for="account-profile-photo" class="nicbf-default-button btn btn-outline-secondary btn-lg neonfront-btn-outline-primary w-auto preview-action-control">
740            <span>Upload</span>
741          </label>
742           <input class="form-control d-none" type="file" id="account-profile-photo" style="width: 1px; height: 1px; border: none; opacity: 0" accept="image/jpeg,image/png,image/gif" @change="putImgToCanv">
743        </div>
744        <div v-else>
745            <label @click="ediitProfilePicture" class="nicbf-default-button btn btn-outline-secondary btn-lg neonfront-btn-outline-primary w-auto preview-action-control">
746            <span>Edit profile picture</span>
747          </label>
748        </div>
749        </div>
750    </div>
751    <div class="nic-footer">
752       <div class="nicf-cancel">
753            <span @click="backButton" v-show="originalImgSrc">Back</span>
754        </div>
755       <div class="nicf-next">
756           <button @click="cropPicture" :disabled="!originalImgSrc" :class="{'disabled-button':!originalImgSrc}" v-if="!isPreviewImg">Next</button>
757           <button v-else @click="saveProfilePicture">Save as profile picture</button>
758        </div>
759    </div>
760</div>
761  <div class="neon-img-cutter-mask"></div>
762`
763}
764window.NeonAdvancedsearch = {
765    name: 'neon-advancedsearch',
766    props: {
767        criteriaFields: {
768            type: Array,
769            default() {
770                return []
771            }
772        },
773        sortByItems: {
774            type: Array,
775            default() {
776                return []
777            }
778        },
779        defaultSortBy: {
780            type: String,
781            default: ''
782        },
783        className: {
784            String: String,
785            default: ''
786        },
787    },
788    data() {
789        return {
790            showPopover: false,
791            uuid: new Date().getTime() + '-' + ((Math.random() + '').replace('0.', '')),
792            selectedItems: [],
793            currentActive: -1,
794            selectSortByText: '',
795            top: 0,
796            left: 0,
797            scrollTop: 0,
798            miniObject: {
799                showOverlay: false,
800                tempShow: '',
801                tempSelect: [],
802                selectedField: {}
803            },
804            dateFieldInit: false
805        }
806    },
807    methods: {
808        miniClickToShow($key) {
809            this.miniObject.tempSelect = []
810            if (this.miniObject.selectedField[$key]) {
811                this.miniObject.tempSelect.push(...this.miniObject.selectedField[$key])
812            }
813            this.miniObject.tempShow = $key
814            this.miniObject.showOverlay = true
815        },
816        miniDoneSelect($key) {
817            this.miniObject.selectedField[$key] = []
818            this.miniObject.selectedField[$key].push(...this.miniObject.tempSelect)
819            this.miniCloseShow()
820        },
821        miniCloseShow() {
822            this.miniObject.tempSelect = []
823            this.miniObject.tempShow = ''
824            this.miniObject.showOverlay = false
825        },
826        miniSelectItem(item, $single) {
827            if ($single) {
828                this.miniObject.tempSelect = []
829                this.miniObject.tempSelect.push(item.value)
830            } else {
831                let idx = this.miniObject.tempSelect.findIndex($v => $v === item.value)
832                if (idx === -1) {
833                    this.miniObject.tempSelect.push(item.value)
834                } else {
835                    this.miniObject.tempSelect.splice(idx, 1)
836                }
837            }
838        },
839        isMiniSelectItem(item) {
840            return this.miniObject.tempSelect.findIndex(e => e === item.value) !== -1
841        },
842        isSelectAll($items) {
843            return this.miniObject.tempSelect.length === $items.length
844        },
845        miniSelectAll($items) {
846            if ($items && $items.length > 0 && this.miniObject.tempSelect.length !== $items.length) {
847                this.miniObject.tempSelect = []
848                $items.forEach($e => {
849                    this.miniObject.tempSelect.push($e.value)
850                })
851            } else {
852                this.miniObject.tempSelect = []
853            }
854        },
855        miniClear() {
856            for (let k in this.miniObject.selectedField) {
857                if (k !== 'sort') {
858                    delete this.miniObject.selectedField[k]
859                }
860            }
861        },
862        miniCancelSearch() {
863            this.initSearchFields()
864            this.showPopover = false
865        },
866        miniDoSearch() {
867            let sortBy = this.miniObject.selectedField['sort']
868            this.criteriaFields.forEach($field => {
869                let $key = 'f_' + $field.fieldId
870                let vls = this.miniObject.selectedField[$key] || []
871                $field.value = vls
872                if ('1,2,3'.indexOf($field.displayType) > -1) {
873                    $field.value = []
874                    if ($field.options && $field.options.length > 0) {
875                        $field.value.push(...vls)
876                        $field.options.forEach(opt => {
877                            opt.selected = vls.indexOf(opt) > -1
878                        })
879                    }
880                }
881            })
882            this.$emit('selected', this.criteriaFields, sortBy && sortBy.length > 0 ? sortBy[0] : null)
883            this.showPopover = false
884            this.$emit('popovercalbk', this.showPopover)
885        },
886        miniUpdateCriteria($field) {
887            let $key = 'f_' + $field.fieldId
888            if ('1,2,3'.indexOf($field.displayType) > -1) {
889                this.miniObject.selectedField[$key] = []
890                this.miniObject.selectedField[$key].push(...$field.value)
891            } else {
892                this.miniObject.selectedField[$key] = $field.value
893            }
894        },
895        togglePopoverBox(e) {
896            let container = e.target.closest(`.${this.className}`)
897            if (this.showPopover && !this.$el.contains(e.target) && !container) {
898                this.showPopover = false
899                this.$emit('popovercalbk', this.showPopover)
900            }
901        },
902        closePopoverBox() {
903            this.initSearchFields()
904            this.showPopover = false
905            this.$emit('popovercalbk', this.showPopover)
906        },
907        handleDocumentClick(e) {
908            let container = e.target.closest('.npb-multi-select')
909            if (!container || container.getAttribute('uuid') !== this.uuid) {
910                this.criteriaFields.forEach($field => {
911                    if ($field.keyword) {
912                        $field.keyword = ''
913                    }
914                })
915                this.currentActive = -1
916            }
917        },
918        changeCalbk($field) {
919            if ($field) {
920                let $p = [$field]
921                this.miniUpdateCriteria($field)
922                this.$emit('selected', $p)
923            } else {
924                this.criteriaFields.forEach(e => {
925                    this.miniUpdateCriteria(e)
926                })
927                this.$emit('selected')
928            }
929        },
930        toggleItem($field, item) {
931            item.selected = !item.selected
932
933            if (item.selected) {
934                $field.value.push(item.value)
935            } else {
936                let idx = $field.value.findIndex(val => val === item.value)
937                $field.value.splice(idx, 1)
938            }
939
940            $field.selectAll = $field.value.length === $field.options.length
941            this.changeCalbk($field)
942        },
943        toggleAllItem($field) {
944            $field.selectAll = !$field.selectAll
945            $field.value = []
946            $field.options.forEach(opt => {
947                opt.selected = $field.selectAll
948                if (opt.selected) {
949                    $field.value.push(opt.value)
950                }
951            })
952            this.changeCalbk($field)
953        },
954        clearData() {
955            this.criteriaFields.forEach($field => {
956                $field.value = ''
957                $field.selectAll = false
958                if ($field.options && $field.options.length > 0) {
959                    $field.value = []
960                    $field.options.forEach(opt => {
961                        opt.selected = false
962                    })
963                }
964            })
965            this.miniClear()
966            this.changeCalbk()
967        },
968        sortBy($selectedVal) {
969            this.miniObject.selectSortBy = $selectedVal
970            this.$emit("sortby", $selectedVal)
971        },
972        togglePopOver() {
973            this.initDateField()
974            this.showPopover = !this.showPopover
975            this.buttonPosition()
976            this.$emit('popovercalbk', this.showPopover)
977        },
978        buttonPosition() {
979            this.$nextTick(() => {
980                let dom = ''
981                let $dom = document.querySelectorAll(`.${this.className}`);
982                $dom.forEach(e => {
983                    if (e.getBoundingClientRect().height > 0) return dom = e
984                })
985                if (dom) {
986                    if (this.scrollTop > 0) {
987                        this.top = dom.getBoundingClientRect().y + dom.getBoundingClientRect().height + 10 + this.scrollTop
988                    } else {
989                        this.top = dom.getBoundingClientRect().y + dom.getBoundingClientRect().height + 10
990                    }
991                }
992            })
993        },
994        handleScroll() {
995            this.scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
996        },
997        initSearchFields() {
998            if (this.criteriaFields && this.criteriaFields.length > 0) {
999                this.criteriaFields.forEach($f => {
1000                    let $key = 'f_' + $f.fieldId
1001                    if ('1,2,3'.indexOf($f.displayType) > -1) {
1002                        this.miniObject.selectedField[$key] = $f.value || []
1003                    } else {
1004                        this.miniObject.selectedField[$key] = $f.value || ''
1005                    }
1006                })
1007            }
1008        },
1009        initDateField() {
1010            if (this.dateFieldInit) {
1011                return
1012            }
1013            this.$nextTick(function () {
1014                for (let f of this.criteriaFields) {
1015                    this.registerLayDate(f)
1016                }
1017                this.dateFieldInit = true
1018            })
1019        },
1020        registerLayDate($field) {
1021            if ($field.dataType !== '4' && $field.dataType !== '5') {
1022                return
1023            }
1024            let _this = this
1025            let format = $field.dataType === '4' ? 'MM/dd/yyyy' : 'HH:mm:ss'
1026            let type = $field.dataType === '4' ? 'date' : 'time'
1027            let elem = document.getElementById('cf_' + $field.fieldId)
1028            let miniElem = document.getElementById('minicf_' + $field.fieldId)
1029            let rangeField = ['membership_stats.firstJoinDate','membership_listing.startDate','membership_listing.endDate']
1030            let $range = rangeField.indexOf($field.fieldCode) > -1 ? '~' : false
1031            laydate.render({
1032                elem: elem,
1033                lang: 'en',
1034                type: type,
1035                format: format,
1036                range: $range,
1037                ready: function (value) {
1038                },
1039                done: function (value) {
1040                    $field.value = value
1041                    _this.changeCalbk($field)
1042                }
1043            })
1044            let $key = 'f_' + $field.fieldId
1045            laydate.render({
1046                elem: miniElem,
1047                lang: 'en',
1048                type: type,
1049                format: format,
1050                range: $range,
1051                ready: function (value) {
1052                },
1053                done: function (value) {
1054                    _this.miniObject.selectedField[$key] = value
1055                }
1056            })
1057        }
1058    },
1059    computed: {
1060        selectedOptNum() {
1061            return ($field) => {
1062                return this.miniObject.selectedField['f_' + $field.fieldId] && this.miniObject.selectedField['f_' + $field.fieldId].length > 0 ? this.miniObject.selectedField['f_' + $field.fieldId].length : 0
1063            }
1064        },
1065        getValueLabel() {
1066            return ($key, $items) => {
1067                let selectedVals = this.miniObject.selectedField[$key]
1068                if (selectedVals && selectedVals.length > 0) {
1069                    let $item = $items.filter($itm => $itm.value === selectedVals[0])
1070                    return $item && $item.length > 0 ? $item[0].name : ''
1071                }
1072                return ''
1073            }
1074        },
1075        searchData() {
1076            return ($options, $searchValue) => {
1077                if (!$searchValue) return $options
1078                return $options.filter(v => v.name.toLowerCase().includes($searchValue.toLowerCase().trim()))
1079            }
1080        }
1081    },
1082    watch: {
1083        defaultSortBy: {
1084            immediate: true,
1085            handler($newVal) {
1086                this.miniObject.selectedField['sort'] = []
1087                this.miniObject.selectedField['sort'].push($newVal)
1088            }
1089        }
1090    },
1091    mounted() {
1092        this.initSearchFields()
1093        window.onresize = () => {
1094            this.buttonPosition()
1095        }
1096        window.addEventListener('scroll', this.handleScroll)
1097    },
1098    created() {
1099        this.$nextTick(() => {
1100            let $dom = document.querySelectorAll(`.${this.className}`);
1101            $dom.forEach(e => {
1102                e.addEventListener('click', this.togglePopOver);
1103            })
1104        })
1105        document.addEventListener('click', this.togglePopoverBox)
1106        document.addEventListener('mouseup', this.handleDocumentClick)
1107    },
1108    beforeDestroy() {
1109        this.$nextTick(() => {
1110            let $dom = document.querySelectorAll(`.${this.className}`);
1111            $dom.forEach(e => {
1112                e.removeEventListener('click', this.togglePopOver);
1113            })
1114        })
1115        document.removeEventListener('click', this.togglePopoverBox)
1116        document.removeEventListener('mouseup', this.handleDocumentClick)
1117        window.removeEventListener('scroll', this.handleScroll)
1118    },
1119    template: `
1120        <div class="neon-advanced-search-box" :style="[{top:top + 'px'},{left:left + 'px'}]">
1121            <div class="neon-advanced-search">  
1122                <div class="nas-max-show-body">
1123                    <transition name="slideLeft">
1124                        <div class="neon-advanced-search-popover" v-show="showPopover">                  
1125                            <div class="nas-popover-header">
1126                                <span class="title-text nas-max-show">Advanced Settings</span> 
1127                                <span class="title-clear" @click="clearData">Clear</span>
1128                            </div>
1129                            <div class="nas-popover-body">
1130                                <div class="nas-mini-show-sort-by npb-options" v-if="sortByItems && sortByItems.length > 0">
1131                                    <span class="npb-options-lab">Sort-by</span>
1132                                    <neon-select class="select-sort-by" :items="sortByItems" :default-val="defaultSortBy" @selected="sortBy"></neon-select>                     
1133                                </div>                                            
1134                                <div class="npb-options" v-for="($field, index) in criteriaFields" :key="$field.fieldId">
1135                                    <span class="npb-options-lab">{{$field.isCustom === '1' ? $field.fieldPublicName : $field.fieldDisplayName}}</span>
1136                                    <div class="input-group" v-if="$field.type === 'text' && ($field.dataType === '4' || $field.dataType === '5')">
1137                                        <input :id="'cf_' + $field.fieldId" class="npb-options-input calendar-input" type="text" :placeholder="$field.placeHolder" v-model="$field.value"/>
1138                                        <span class="input-group-text far" :class="[$field.dataType === '4' ? 'fa-calendar-alt':'fa-clock']" style=""></span>
1139                                    </div>
1140                                    <div class="input-group" v-else-if="$field.type === 'text'">
1141                                        <input :id="'cf_' + $field.fieldId" class="npb-options-input calendar-input" type="text" :placeholder="$field.placeHolder" @blur="changeCalbk($field)" @keydown.enter.prevent="changeCalbk($field)" v-model="$field.value"/>
1142                                    </div>
1143                                    <div class="npb-multi-select" v-if="$field.type === 'dropdown'" :uuid="uuid">
1144                                       <div class="placeholder" @click="currentActive = index">
1145                                        <input :placeholder="$field.placeHolder" type="text" v-model="$field.keyword" class="npb-options-input">
1146                                        <div class="input-right-text">
1147                                          <span style="margin-left: 3px;" v-show="$field.value.length > 0">+{{$field.value.length}}</span>
1148                                          <span style="margin-left: 7px;">
1149                                             <i class="fas fa-caret-down"></i>
1150                                           </span>
1151                                        </div>                       
1152                                       </div>
1153                                       <div class="list" v-show="currentActive === index">
1154                                            <div class="list-scrollbar">                                                                        
1155                                                <div class="up-arrow"></div>
1156                                                <ul class="items">
1157                                                <li class="item"                   
1158                                                            @click.prevent="toggleAllItem($field)"
1159                                                            :class="{'is-selected': $field.selectAll}" 
1160                                                            v-show="!$field.keyword"
1161                                                            >
1162                                                        <label>
1163                                                            <span class="list-checkbox-input">
1164                                                                <span class="list-checkbox-inner"></span>
1165                                                                <input type="checkbox" class="item-checked" v-model="$field.selectAll">
1166                                                            </span>
1167                                                            <sapn name="item" :item="item">Select All</sapn>
1168                                                        </label>
1169                                                </li>     
1170                                                    <li class="item" 
1171                                                            v-for="(option,i) in searchData($field.options,$field.keyword)"
1172                                                            :key="i"
1173                                                            @click.prevent="toggleItem($field,option)"
1174                                                            :class="{'is-selected': option.selected}" 
1175                                                            >
1176                                                        <label>
1177                                                             <span class="list-checkbox-input">
1178                                                                   <span class="list-checkbox-inner"></span>
1179                                                                   <input type="checkbox" class="item-checked" :value="option.value" v-model="option.selected">
1180                                                             </span>                                                        
1181                                                            <sapn class="item-text" name="item">{{option.name}}</sapn>
1182                                                        </label>
1183                                                 </li>                                           
1184                                              </ul>  
1185                                              <div class="filter-seach-empty" v-if="!searchData($field.options,$field.keyword).length > 0">No results match "{{$field.keyword}}"</div> 
1186                                            </div>                                                
1187                                       </div>
1188                                   </div>                  
1189                                </div>         
1190                            </div>
1191                            <div class="nas-popper-arrow"></div>              
1192                        </div>           
1193                    </transition>
1194                </div>
1195                <div class="nas-mini-show-transition">
1196                    <transition name="slideLeft" appear>
1197                        <div class="neon-advanced-search-popover" v-show="showPopover">
1198                            <div class="nas-mini-show nas-mini-show-header">
1199                                <span @click="closePopoverBox" class="clickable"><i class="fas fa-arrow-left" style="color: #0074d9;"></i></span>
1200                                <span class="header-title-text">Search</span>
1201                                <span @click="closePopoverBox" class="clickable"><i class="fal fa-times" style="color: #002d41;"></i></span>
1202                            </div>
1203                            <div class="nas-popover-header">
1204                                <span class="title-text nas-mini-show">Advanced Search</span>
1205                                <span class="title-clear" @click="miniClear">Clear</span>
1206                            </div>
1207                            <div class="nas-popover-body">
1208                                <div class="nas-mini-show npb-options">
1209                                    <span class="npb-options-lab">Sort By</span>
1210                                     <div class="npb-multi-select">
1211                                       <div class="placeholder" @click="miniClickToShow('sort')">
1212                                        <input type="text" class="npb-options-input" readonly="readonly" :value="getValueLabel('sort', sortByItems)">
1213                                        <div class="input-right-text">
1214                                          <span style="margin-left: 10px;">
1215                                             <i class="fas fa-caret-down"></i>
1216                                           </span>
1217                                        </div>                       
1218                                       </div>
1219                                       <div class="list" v-show="miniObject.tempShow === 'sort'">
1220                                            <div class="nas-mini-show nas-dropdown-list-header">
1221                                                <span class="mini-dropdown-cancel" @click="miniCloseShow()"><i class="fal fa-times" style="color: #002d41;font-size: 14px"></i></span>
1222                                                <span class="mini-dropdown-done" @click="miniDoneSelect('sort')">Done</span>
1223                                            </div>
1224                                            <div class="up-arrow"></div>
1225                                            <ul class="items">
1226                                                <li v-for="item in sortByItems" class="item" @click.prevent="miniSelectItem(item, true)" :class="{'is-selected': isMiniSelectItem(item)}" >
1227                                                    <label>
1228                                                        <sapn name="item" class="item-text">{{item.name}}</sapn>
1229                                                    </label>
1230                                                </li>
1231                                            </ul>                    
1232                                       </div>
1233                                   </div>  
1234                                </div>
1235                                            
1236                                <div class="npb-options" v-for="($field, index) in criteriaFields" :key="$field.fieldId">
1237                                    <span class="npb-options-lab">{{$field.isCustom === '1' ? $field.fieldPublicName : $field.fieldDisplayName}}</span>
1238                                    <div class="input-group" v-if="$field.type === 'text' && ($field.dataType === '4' || $field.dataType === '5')">
1239                                        <input :id="'minicf_' + $field.fieldId" class="npb-options-input calendar-input" type="text" :placeholder="$field.placeHolder" v-model="miniObject.selectedField['f_'+ $field.fieldId]"/>
1240                                        <span class="input-group-text far" :class="[$field.dataType === '4' ? 'fa-calendar-alt':'fa-clock']" style=""></span>
1241                                    </div>
1242                                    <div class="input-group" v-else-if="$field.type === 'text'">
1243                                        <input :id="'minicf_' + $field.fieldId" class="npb-options-input" type="text" :placeholder="$field.placeHolder" v-model="miniObject.
1243selectedField['f_'+ $field.fieldId]"/>
1244                                    </div>
1245                                    <div class="npb-multi-select" v-if="$field.type === 'dropdown'" :uuid="uuid">
1246                                       <div class="placeholder" @click="miniClickToShow('f_' + $field.fieldId)">
1247                                        <input :placeholder="$field.placeHolder" type="text" v-model="$field.keyword" readonly="readonly" class="npb-options-input">
1248                                        <div class="input-right-text">
1249                                          <span style="margin-left: 3px;" v-show="selectedOptNum($field) > 0">+{{selectedOptNum($field)}}</span>
1250                                          <span style="margin-left: 7px;">
1251                                             <i class="fas fa-caret-down"></i>
1252                                           </span>
1253                                        </div>                       
1254                                       </div>
1255                                       <div class="list" v-show="miniObject.tempShow === 'f_' + $field.fieldId">
1256                                            <div class="list-scrollbar">
1257                                                <div class="nas-mini-show nas-dropdown-list-header">
1258                                                    <span class="mini-dropdown-cancel" @click="miniCloseShow()"><i class="fal fa-times" style="color: #002d41;font-size: 14px"></i></span>
1259                                                    <span class="mini-dropdown-done" @click="miniDoneSelect('f_' + $field.fieldId)">Done</span>
1260                                                </div>
1261                                                <div class="up-arrow"></div>
1262                                                <ul class="items">
1263                                                    <li class="item"                   
1264                                                                @click.prevent="miniSelectAll($field.options)"
1265                                                                :class="{'is-selected': isSelectAll($field.options)}" 
1266                                                                v-show="!$field.keyword">
1267                                                            <label>
1268                                                                <span class="list-checkbox-input">
1269                                                                    <span class="list-checkbox-inner"></span>
1270                                                                    <input type="checkbox" class="item-checked" :checked="isSelectAll($field.options)">
1271                                                                </span>                                                           
1272                                                                <sapn name="item" :item="item">Select All</sapn>
1273                                                            </label>
1274                                                    </li>     
1275                                                    <li class="item" 
1276                                                            v-for="(option,i) in $field.options"
1277                                                            :key="i"
1278                                                            @click.prevent="miniSelectItem(option)"
1279                                                            :class="{'is-selected': isMiniSelectItem(option)}" 
1280                                                            >
1281                                                        <label>
1282                                                            <span class="list-checkbox-input">
1283                                                                    <span class="list-checkbox-inner"></span>
1284                                                                    <input type="checkbox" class="item-checked" :checked="isMiniSelectItem(option)">
1285                                                            </span>      
1286                                                            <sapn class="item-text" name="item">{{option.name}}</sapn>
1287                                                        </label>
1288                                                    </li>                                                                             
1289                                                </ul>  
1290                                            </div>                                                
1291                                       </div>
1292                                   </div>                  
1293                                </div>         
1294                            </div>
1295                            <div class="nas-popper-arrow"></div>
1296                            <div class="nas-mini-show nas-mini-showoverlay" v-show="miniObject.
1296showOverlay"></div>
1297                            <div class="nas-mini-show nas-nini-show-footer">
1298                                <span class="footer-cancle clickable" @click="closePopoverBox()">Cancel</span>
1299                                <span class="footer-button" @click="miniDoSearch()"><button>Apply</button></span>
1300                            </div>
1301                        </div>           
1302                    </transition>
1303                </div>
1304           </div>
1305        </div>`
1306}
1307window.NeonSelect = {
1308    name: 'neon-select',
1309    props: {
1310        defaultVal: {
1311            type: String,
1312            default: ''
1313        },
1314        items: {
1315            type: Array,
1316            default() {
1317                return []
1318            }
1319        },
1320        listClass: {
1321            type: String,
1322            default: ''
1323        },
1324        showLabel: {
1325            type: Boolean,
1326            default: true
1327        },
1328        sortLabel: {
1329            type: String,
1330            default: 'Sort by'
1331        }
1332    },
1333    data() {
1334        return {
1335            uuid: new Date().getTime() + '-' + ((Math.random() + '').replace('0.', '')),
1336            show: false,
1337            selectedItems: []
1338        }
1339    },
1340    methods: {
1341        toggleItem(item) {
1342            let idx = this.selectedItems.findIndex(item_ => item.value === item_.value)
1343            if (idx === -1) {
1344                this.selectedItems.splice(0, this.selectedItems.length)
1345                this.selectedItems.push(item)
1346            }
1347            this.show = false
1348            this.$emit('selected', item.value)
1349        },
1350        isSelectedItem(item) {
1351            return this.selectedItems.findIndex(item_ => item.value === item_.value) !== -1
1352        },
1353        handleDocumentClick(e) {
1354            let container = e.target.closest('.neon-multi-select')
1355            if (!container || container.getAttribute('uuid') !== this.uuid) {
1356                this.show = false
1357            }
1358        }
1359    },
1360    computed: {
1361        displayText() {
1362            let selectedItem = this.items && this.items.length > 0 && this.selectedItems && this.selectedItems.length > 0 ? this.items.filter(e => e.value === this.selectedItems[0].value) : null
1363            return selectedItem && selectedItem.length > 0 ? selectedItem[0].name : ''
1364        }
1365    },
1366    watch: {
1367        defaultVal: {
1368            immediate: true,
1369            handler($newVal) {
1370                if (this.selectedItems.length > 0) {
1371                    this.selectedItems = []
1372                }
1373                let def = this.items.filter(e => e.value === $newVal)
1374                if (def && def.length > 0) {
1375                    this.selectedItems.push(def[0])
1376                }
1377            }
1378        }
1379    },
1380    mounted() {
1381        document.addEventListener('mouseup', this.handleDocumentClick)
1382    },
1383    unmounted() {
1384        document.removeEventListener('mouseup', this.handleDocumentClick)
1385    },
1386    template: `
1387       <div class="neon-select" :class="{'isfous' : show}" :uuid="uuid">
1388           <div class="placeholder" @click="show=!show">
1389            <span class="span-text" :class="{'has-val' : selectedItems.length >0}"><span v-if="showLabel">{{sortLabel}}: </span>{{displayText}}</span>
1390            <span style="margin-left: 3px;">
1391                <i :class="{'fas fa-caret-down' : !show, 'fas fa-caret-up':show}"></i>
1392            </span>
1393           </div>
1394           <div class="list" :class="listClass" v-show="show">
1395                <div class="up-arrow"></div>
1396                <ul class="items">
1397                    <li v-for="item in items" 
1398                        class="item"
1399                        :class="{'is-selected': isSelectedItem(item)}" 
1400                        @click="toggleItem(item)">
1401                        <slot name="item" :item="item">{{item.name}}</slot>
1402                    </li>
1403                </ul>
1404           </div>
1405       </div>`
1406}
1407window.NeonSearchBox = {
1408    name: 'neon-search-box',
1409    props: {
1410        placeholder: {
1411            type: String,
1412            default: 'All'
1413        },
1414        multiSelect: {
1415            type: Boolean,
1416            default: true
1417        },
1418        items: {
1419            type: Array,
1420            default() {
1421                return []
1422            }
1423        },
1424        listClass: {
1425            type: String,
1426            default: ''
1427        },
1428        initValue: {
1429            type: String,
1430            default: ''
1431        },
1432    },
1433    data() {
1434        return {
1435            uuid: new Date().getTime() + '-' + ((Math.random() + '').replace('0.', '')),
1436            show: false,
1437            selectedItems: [],
1438            searchKeyword: '',
1439            itemList: []
1440        }
1441    },
1442    methods: {
1443        toggleItem(item) {
1444            let idx = this.selectedItems.findIndex(item_ => item.value === item_.value)
1445            if (idx === -1) {
1446
1447                if (!this.multiSelect) {
1448                    this.clear(false)
1449                }
1450
1451                this.selectedItems.push(item)
1452            } else {
1453                this.selectedItems.splice(idx, 1)
1454            }
1455
1456            if (!this.multiSelect) {
1457                this.show = false
1458            }
1459        },
1460        isSelectedItem(item) {
1461            return this.selectedItems.findIndex(item_ => item.value === item_.value) !== -1
1462        },
1463        clear(hide) {
1464            this.selectedItems.splice(0, this.selectedItems.length)
1465            if (hide) {
1466                this.show = false
1467            }
1468        },
1469        handleDocumentClick(e) {
1470            let container = e.target.closest('.neon-search-box')
1471            if (!container || container.getAttribute('uuid') !== this.uuid) {
1472                this.show = false
1473            }
1474        },
1475        searchItem() {
1476            if (this.searchKeyword.length > 0) {
1477                let list = []
1478                for (const item of this.items) {
1479                    let itemName = item.name.toLowerCase()
1480                    if (itemName.includes(this.searchKeyword.toLowerCase())) {
1481                        list.push(item)
1482                    }
1483                }
1484                this.itemList = list
1485            } else {
1486                this.itemList = this.items
1487            }
1488        },
1489        clearSearch() {
1490            this.searchKeyword = ''
1491            this.itemList = this.items
1492        }
1493    },
1494    computed: {
1495        displayText() {
1496            if (this.selectedItems.length === 0) {
1497                return this.placeholder
1498            }
1499            return this.selectedItems[0].name
1500        }
1501    },
1502    watch: {
1503        show() {
1504            if (!this.show) {
1505                this.clearSearch()
1506                this.$emit('hide', this.selectedItems)
1507            }
1508        },
1509        initValue(newVal){
1510            if (newVal) {
1511                let arr = newVal.split(',')
1512                for (let i = 0; i < arr.length; i++) {
1513                    let res = this.items.filter(item => item.value === arr[i])
1514                    if (res && res.length > 0) {
1515                        this.toggleItem(res[0])
1516                    }
1517                }
1518            }
1519        }
1520    },
1521    mounted() {
1522        this.itemList.push(...this.items)
1523        document.addEventListener('mouseup', this.handleDocumentClick)
1524    },
1525    unmounted() {
1526        document.removeEventListener('mouseup', this.handleDocumentClick)
1527    },
1528    template: `
1529       <span class="neon-search-box" :uuid="uuid">
1530           <div class="placeholder" @click="show=!show">
1531            <span :class="{'has-val' : selectedItems.length >0}">{{displayText}}</span>
1532            <span :class="{'has-val' : selectedItems.length >0}" style="margin-left: 3px;" v-if="multiSelect && selectedItems.length>1">+{{selectedItems.length - 1}}</span>
1533            <span style="margin-left: 3px;">
1534                <i :class="{'fas fa-caret-down' : !show, 'fas fa-caret-up':show}"></i>
1535            </span>
1536           </div>
1537           <div class="list" :class="listClass" v-show="show">
1538                <div class="up-arrow"></div>
1539                <div class="search-box">
1540                    <input type="text" v-model="searchKeyword" :oninput="searchItem"/>
1541                    <i class="fa-solid fa-magnifying-glass" v-if="searchKeyword.length === 0"></i>
1542                </div>
1543                <div class="list-items-container">
1544                    <ul class="items">
1545                        <div v-if="searchKeyword.length > 0 && itemList.length === 0" class="empty">
1546                            <i>No results match "{{searchKeyword}}"</i>
1547                       </div>
1548                        <li v-for="item in itemList" 
1549                            class="item"
1550                            :class="{'is-selected': isSelectedItem(item)}" 
1551                            @click="toggleItem(item)">
1552                            <slot name="item" :item="item">{{item.name}}</slot>
1553                        </li>
1554                    </ul>
1555                </div>
1556                <div class="footer" v-if="selectedItems.length >0">
1557                    <span @click="clear(true)">Clear</span>
1558                </div>
1559           </div>
1560       </span>`
1561}
1562window.NeonPagination = {
1563    name: 'neon-pagination',
1564    props: {
1565       page: {
1566           type: Object,
1567           default () {
1568               return {}
1569           }
1570       }
1571    },
1572    data() {
1573        return {
1574        }
1575    },
1576    methods: {
1577        clkPre() {
1578            if (this.page.pageNo === 1) {
1579                return;
1580            }
1581            this.$emit('clkpage', this.page.pageNo - 1)
1582        },
1583        clkNext() {
1584            if (this.page.pageNo === this.totalPage) {
1585                return;
1586            }
1587            this.$emit('clkpage', this.page.pageNo + 1)
1588        },
1589        clkPageNo($num) {
1590            if($num === this.page.pageNo) {
1591                return;
1592            }
1593            this.$emit('clkpage', $num)
1594        }
1595    },
1596    computed: {
1597        totalPage() {
1598            if (!this.page || !this.page.total) {
1599                return 0
1600            }
1601            let pages = Math.floor(this.page.total / this.page.pageSize)
1602            return this.page.total % this.page.pageSize === 0 ? pages : pages + 1
1603        },
1604        displayNums() {
1605            let pageNums = []
1606            let totalPage = this.totalPage
1607            let start = 1
1608            let end = 1
1609            if (totalPage <= 5 || this.page.pageNo <= 3) {
1610                end = totalPage > 5 ? 5 : totalPage
1611            }
1612            if (this.page.pageNo > 3) {
1613                start = this.page.pageNo - 2
1614                end = this.page.pageNo + 2
1615                if (end > totalPage) {
1616                    end = totalPage
1617                    start = end - 4
1618                }
1619                if (start < 1) {
1620                    start = 1
1621                }
1622            }
1623            for (let idx = start; idx <= end; idx ++) {
1624                pageNums.push(idx)
1625            }
1626            return pageNums
1627        }
1628    },
1629    watch: {
1630    },
1631    mounted() {
1632    },
1633    created() {
1634    },
1635    beforeDestroy() {
1636    },
1637    template: `
1638        <div class="neon-portal-pagination" v-show="page && page.total > 0">
1639            <span @click="clkPre()" class="pagination-pre" :class="page.pageNo === 1 ? 'disabled' : ''">
1640                <i class="fas fa-arrow-left" style="margin-right: 8px"></i> Previous Page
1641            </span>
1642            <span @click="clkPageNo(pageNo)" v-for="pageNo in displayNums" class="pagination-number" :class="pageNo === page.pageNo ? 'selected' : ''">
1643            {{pageNo}}
1644            </span>
1645            <span @click="clkNext()" class="pagination-next" :class="page.pageNo === totalPage ? 'disabled' : ''">
1646                Next Page <i class="fas fa-arrow-right" style="margin-left: 8px"></i>
1647            </span>
1648        </div>`
1649}
1650window.NeonPaginationPlus = {
1651    name: 'neon-pagination-plus',
1652    props: {
1653        pages: {
1654            type: Object,
1655            default () {
1656                return {
1657                    pageNo: 1,
1658                    pageSize:6,
1659                    total:60
1660                }
1661            }
1662        },
1663        pageSizeList: {
1664            type: Array,
1665            default () {
1666                return [10, 20, 30, 40, 50, 100]
1667            }
1668        },
1669        pageSize: {
1670            type: Number,
1671            default: 10
1672        },
1673        showLabel: {
1674            type: Boolean,
1675            default: true
1676        }
1677    },
1678    data() {
1679        return {
1680            currentPage: this.pages.pageNo,
1681            pageLen: this.pageSize,
1682        }
1683    },
1684    watch: {
1685        pages: {
1686            handler(newVal) {
1687                this.currentPage = newVal.pageNo
1688                this.pageLen = newVal.pageSize
1689            },
1690            deep: true
1691        }
1692    },
1693    methods: {
1694        clkPres() {
1695            if (this.currentPage === 1) return;
1696            this.currentPage -= 1
1697            this.onPageChange();
1698        },
1699        clkNexts() {
1700            if (this.currentPage === this.totalPage) return;
1701            this.currentPage += 1
1702            this.onPageChange();
1703        },
1704        clkPageNos($num) {
1705            if ($num === this.currentPage) return;
1706            if ($num === 'ellipsis-next') {
1707                let next = this.currentPage + 5;
1708                this.currentPage = next > this.totalPage ? this.totalPage : next;
1709                this.onPageChange();
1710                return;
1711            }
1712            if ($num === 'ellipsis-prev') {
1713                let prev = this.currentPage - 5;
1714                this.currentPage = prev < 1 ? 1 : prev;
1715                this.onPageChange();
1716                return;
1717            }
1718            this.currentPage = $num;
1719            this.onPageChange();
1720        },
1721        onPageChange () {
1722            this.$emit('page-change', this.currentPage,this.pageLen);
1723        },
1724    },
1725    mounted() {
1726        this.currentPage = this.pages.pageNo
1727        this.pageLen = this.pages.pageSize
1728    },
1729    computed: {
1730        totalPage() {
1731            if (!this.pages || !this.pages.total) return 0
1732            let pages = Math.floor(this.pages.total / this.pages.pageSize)
1733            return this.pages.total % this.pages.pageSize === 0 ? pages : pages + 1
1734        },
1735        displayNum() {
1736            const total = this.totalPage
1737            const current = this.currentPage
1738            const nums = []
1739            if (total <= 7) {
1740                for (let i = 1; i <= total; i++) nums.push(i)
1741            } else if (current <= 3) {
1742                for (let i = 1; i <= 5; i++) nums.push(i)
1743                nums.push('ellipsis-next')
1744                nums.push(total)
1745            } else if (current >= total - 2) {
1746                nums.push(1)
1747                nums.push('ellipsis-prev')
1748                for (let i = total - 4; i <= total; i++) nums.push(i)
1749            } else {
1750                nums.push(1)
1751                nums.push('ellipsis-prev')
1752                for (let i = current - 2; i <= current + 2; i++) nums.push(i)
1753                nums.push('ellipsis-next')
1754                nums.push(total)
1755            }
1756            return nums
1757        },
1758        pageInfoItems () {
1759            if (this.pageLen <= 0) return ''
1760            if (this.pages.total === 0) return `|&nbsp;&nbsp;0 Items`
1761
1762            let startIndex = (this.pages.pageNo - 1) * this.pageLen + 1
1763            let endIndex = startIndex + this.pageLen - 1
1764            if (endIndex > this.pages.total) endIndex = this.pages.total
1765
1766            return `|&nbsp;&nbsp;${startIndex} - ${endIndex} of ${this.pages.total} items`
1767        },
1768    },
1769    template: `
1770        <div class="neon-portal-pagination-plus" v-show="pages && pages.total > 0">
1771            <div class="pagination-left">
1772                <span @click="clkPres()" class="pagination-pre" :class="currentPage === 1 ? 'disabled' : ''">
1773                    <i class="fa-solid fa-chevron-left" :style="showLabel ? '' : 'margin-right: 0;'"></i> {{showLabel ? 'Back' : ''}}
1774                 </span>
1775                <ul class="pagination-pager">
1776                    <li v-for="item in displayNum"
1777                          :key="item + '-'"
1778                          @click="clkPageNos(item)"
1779                          class="pagination-number"
1780                          :class=" {selected:item === currentPage} ">
1781                         <template v-if="item === 'ellipsis-prev' || item === 'ellipsis-next'">
1782                            <i class="fa-solid fa-ellipsis"></i>
1783                        </template>
1784                        <template v-else>
1785                            {{item}}
1786                        </template>
1787                    </li>
1788                </ul>
1789                <span @click="clkNexts()" class="pagination-next" :class="currentPage === totalPage ? 'disabled' : ''" >
1790                    {{showLabel ? 'Next' : ''}} <i class="fa-solid fa-chevron-right" :style="showLabel ? '' : 'margin-left: 0;'"></i>
1791                </span>
1792            </div>
1793            <div class="pagination-right">
1794                <div class="page-item">
1795                    <span>Items per page: </span>
1796                    <span>
1797                    <select class="page-size" v-model="pageLen" @change="onPageChange">
1798                        <option v-for="p in pageSizeList" :value="p">{{p}}</option>
1799                    </select>
1800                    </span>
1801                    <span style="margin-left: 12px;" v-html="pageInfoItems"></span></div>
1802            </div>
1803        </div>`
1804}
1805window.NeonShareModal = {
1806    name: 'neon-share-modal',
1807    props: {
1808        shareUrlMeta: {
1809            type: Object,
1810            required: true
1811        },
1812        shareModalColor: {
1813            type: String,
1814            default: ''
1815        }
1816    },
1817    data() {
1818        return {
1819            shareModalId: 'neon-share-' + new Date().getTime()
1820        }
1821    },
1822    methods: {
1823        open() {
1824            this.$nextTick(() => {
1825                const modalElement = document.getElementById(this.shareModalId)
1826                document.body.appendChild(modalElement);
1827                const modal = new bootstrap.Modal(modalElement)
1828                modal.show()
1829            })
1830        },
1831        async copyLink() {
1832            let value = this.shareUrlMeta.url
1833            if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
1834                await navigator.clipboard.writeText(value).then(() => {
1835                    alert('Copied!')
1836                }).catch(e => {
1837                    console.log(e)
1838                    alert('Copy Error!')
1839                });
1840            }
1841        },
1842        sharePageToMedia(type) {
1843            let u = this.shareUrlMeta.url
1844            let t = this.shareUrlMeta.title && this.shareUrlMeta.title !== '' ? this.shareUrlMeta.title : this.shareUrlMeta.description
1845            if (type === 'F') {
1846                window.open("https://www.facebook.com/sharer.php?u=" + encodeURIComponent(u) + "&t="+ encodeURIComponent(t), "sharer","toolbar=0,status=0,width=626,height=436")
1847            } else if (type === 'L') {
1848                window.open('https://www.linkedin.com/shareArticle?mini=true&url=' + encodeURIComponent(u) + '&title=' + encodeURIComponent(t),"sharer","toolbar=0,status=0,width=626,height=436")
1849            } else if (type === 'T') {
1850                window.open("https://twitter.com/intent/tweet?text=" + encodeURIComponent(t) + "&url=" + encodeURIComponent(u), "sharer", "toolbar=0,status=0,width=626,height=436")
1851            }
1852        }
1853    },
1854    computed: {
1855        retrieveShareEmailLink() {
1856            if (this.shareUrlMeta && this.shareUrlMeta.title) {
1857                return 'mailto:?body=' + this.shareUrlMeta.url + "&subject=" + (this.shareUrlMeta.title && this.shareUrlMeta.title !== '' ? this.shareUrlMeta.title : this.shareUrlMeta.description)
1858            }
1859            return '#'
1860        },
1861        modalTitle() {
1862            return this.shareUrlMeta && this.shareUrlMeta.shareModalTitle ? this.shareUrlMeta.shareModalTitle : 'Share this page'
1863        },
1864        urlLabel() {
1865            return this.shareUrlMeta && this.shareUrlMeta.urlLabel ? this.shareUrlMeta.urlLabel : 'Page URL'
1866        },
1867    },
1868    watch: {
1869    },
1870    mounted() {
1871        if (this.shareModalColor) {
1872            this.$nextTick(function () {
1873                let r = document.querySelector(':root')
1874                r.style.setProperty('--share-modal-color', this.shareModalColor);
1875            })
1876        }
1877    },
1878    created() {
1879    },
1880    beforeDestroy() {
1881    },
1882    template:
1883        `
1884        <div class="modal fade neonfront-confirm-modal neon-page-sharer-modal" aria-hidden="true" :id="shareModalId" data-bs-backdrop="static" tabindex="-1">
1885            <div class="modal-dialog" style="max-width: 580px;">
1886                <div class="modal-content px-sm-4 px-1">
1887                    <div class="modal-header">
1888                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1889                    </div>
1890                    <div class="modal-body">
1891                        <div class="row mt-1">
1892                            <div class="col shared-body-title">
1893                                <p>{{modalTitle}}</p>
1894                            </div>
1895                        </div>
1896                        <div class="row mt-1">
1897                            <div class="share-body-col">
1898                                <div class="share-button-box" @click="sharePageToMedia('F')">
1899                                    <i class="fa-brands fa-facebook"></i>
1900                                    <div class="share-body-text">Facebook</div>
1901                                </div>
1902                                <div class="share-button-box" @click="sharePageToMedia('L')">
1903                                    <i class="fa-brands fa-linkedin"></i>
1904                                    <div class="share-body-text">LinkedIn</div>
1905                                </div>
1906                                <div class="share-button-box" @click="sharePageToMedia('T')">
1907                                    <i class="fa-brands fa-x-twitter"></i>
1908                                    <div class="share-body-text">Twitter</div>
1909                                </div>
1910                                <div class="share-button-box">
1911                                    <a class="share-button-box-link" :href="retrieveShareEmailLink"><i class="fa-light fa-envelope"></i><div>Email</div></a>
1912                                </div>
1913                            </div>
1914                            <div class="share-body-col">
1915                                <div class="share-button-label" v-text="urlLabel">
1916                                </div>
1917                            </div>
1918                            <div class="share-body-col">
1919                                <div class="share-url-container" v-text="shareUrlMeta.url"></div>
1920                            </div>
1921                            <div class="share-body-col">
1922                                <div class="share-button-copy" @click="copyLink()">
1923                                    <i class="fa-regular fa-copy"></i>
1924                                    <div class="share-body-text">Copy to Clipboard</div>
1925                                </div>
1926                            </div>
1927                        </div>
1928                    </div>
1929                    <div class="modal-footer share-footer" style="justify-content: end">
1930                    </div>
1931                </div>
1932            </div>
1933        </div>
1934        `
1935}
1936window.NeonSearchSelect = {
1937    name: 'neon-search-select',
1938    props: {
1939        placeholder: {
1940            type: String,
1941            default: ''
1942        },
1943        options: {
1944            type: Array,
1945            default() {
1946                return []
1947            }
1948        },
1949        inputClass: {
1950            type: String,
1951            default: ''
1952        },
1953        listClass: {
1954            type: String,
1955            default: ''
1956        },
1957        disabled: {
1958            type: Boolean,
1959            default: false
1960        },
1961        modelValue: {
1962            type: Object,
1963            default: null
1964        },
1965        isMultiple: {
1966            type: Boolean,
1967            default: false
1968        }
1969    },
1970    data() {
1971        return {
1972            showList: false,
1973            isSearch: false,
1974            keyword: '',
1975            isClickingOptions: false,
1976            selectedValues: [],
1977            uuid: new Date().getTime() + '-' + ((Math.random() + '').replace('0.', '')),
1978        }
1979    },
1980    methods: {
1981        select(e, item) {
1982            this.keyword = item.name
1983
1984            document.querySelectorAll('.options li.selected').forEach(li => li.classList.remove('selected'))
1985            e.target.classList.add('selected')
1986
1987            this.modelValue = item.id
1988            this.$emit('update:modelValue', item.id)
1989            this.$emit('change', item)
1990            this.closeOptions()
1991            this.$refs.seachInput.blur();
1992
1993            setTimeout(() => {
1994                this.isClickingOptions = false
1995            }, 100)
1996        },
1997        closeOptions() {
1998            this.showList = false
1999        },
2000        handleInputBlur(e) {
2001            if(this.isMultiple) return
2002            setTimeout(() => {
2003                if (!this.isClickingOptions) {
2004                    if (this.displayList.length === 0) {
2005                        this.keyword = ''
2006                    }
2007
2008                    this.isSearch = false;
2009                    this.closeOptions();
2010
2011                    const value = e.target.value
2012                    if (value) {
2013                        const selectedOption = this.options.find(option => option.name === value)
2014                        if (selectedOption) {
2015                            this.modelValue = selectedOption.id
2016                            this.$emit('update:modelValue', selectedOption.id)
2017                            this.$emit('change', selectedOption)
2018                        } else {
2019                            this.keyword = ''
2020                            this.modelValue = null
2021                            this.$emit('update:modelValue', null)
2022                            this.$emit('change', null)
2023                            this.unselect();
2024                        }
2025                    } else {
2026                        this.modelValue = null
2027                        this.$emit('update:modelValue', null)
2028                        this.$emit('change', null)
2029                        this.unselect();
2030                    }
2031                }
2032            }, 100);
2033        },
2034        handleInput(e) {
2035            this.isSearch = true
2036        },
2037        handelMouseUp() {
2038            this.isClickingOptions = false
2039            this.$refs.seachInput.focus();
2040        },
2041        clear() {
2042            this.keyword = ''
2043            this.modelValue = null
2044            this.options = []
2045            this.unselect();
2046        },
2047        unselect() {
2048            document.querySelectorAll('.options li.selected').forEach(li => li.classList.remove('selected'))
2049        },
2050        toggleAllItem($field) {
2051            $field.selectAll = !$field.selectAll;
2052            this.selectedValues = [];
2053            $field.forEach(opt => {
2054                opt.selected = $field.selectAll;
2055                if (opt.selected) {
2056                    this.selectedValues.push(opt.value);
2057                }
2058            });
2059            this.changeCalbk($field);
2060        },
2061        toggleItem($field, item) {
2062            item.selected = !item.selected
2063
2064            if (item.selected) {
2065                this.selectedValues.push(item.value)
2066            } else {
2067                let idx = this.selectedValues.findIndex(val => val === item.value)
2068                this.selectedValues.splice(idx, 1)
2069            }
2070
2071            $field.selectAll = this.selectedValues.length === $field.length
2072            this.changeCalbk($field)
2073        },
2074        changeCalbk() {
2075            this.$emit('update:modelValue', [...this.selectedValues])
2076        },
2077        handleDocumentClick(e) {
2078            if(!this.isMultiple) return
2079            let container = e.target.closest('.npb-multi-select')
2080            if (!container || container.getAttribute('uuid') !== this.uuid) {
2081                this.keyword = ''
2082                this.showList = false
2083            }
2084        },
2085    },
2086    mounted() {
2087        if (this.modelValue && this.options) {
2088            if(this.isMultiple) {
2089                this.selectedValues = Array.isArray(this.modelValue) ? [...this.modelValue] : []
2090                this.options.forEach(item => {
2091                    item.selected = this.selectedValues.includes(item.value)
2092                })
2093            } else {
2094                const selectedOption = this.options.find(option => option.id === this.modelValue)
2095                if (selectedOption) {
2096                    this.keyword = selectedOption.name
2097                    document.querySelector('li[data="' + this.modelValue + '"]').classList.add('selected')
2098                }
2099            }
2100        }
2101    },
2102    created() {
2103        document.addEventListener('mouseup', this.handleDocumentClick)
2104    },
2105    beforeDestroy() {
2106        document.removeEventListener('mouseup', this.handleDocumentClick)
2107    },
2108    computed: {
2109        displayList() {
2110            if (!this.isSearch) {
2111                return this.options
2112            }
2113            return this.options.filter(option => option.name.toLowerCase().includes(this.keyword.trim().toLowerCase()))
2114        }
2115    },
2116    watch: {
2117        modelValue(newValue) {
2118            if (newValue && this.options) {
2119                const selectedOption = this.options.find(option => option.id === newValue)
2120                if (selectedOption) {
2121                    this.keyword = selectedOption.name
2122                }
2123            }
2124        }
2125    },
2126    template: `
2127          <div :class="['neon-search-select', { 'npb-multi-select': isMultiple }]" :uuid="uuid">
2128            <input
2129              v-model="keyword"
2130              type="text"
2131              ref="seachInput"
2132              :placeholder="placeholder"
2133              :class="inputClass"
2134              :disabled="disabled"
2135              @focus="showList = true"
2136              @blur="handleInputBlur"
2137              @input="handleInput"
2138            />
2139             <div class="input-right-text" v-if="isMultiple">
2140              <span style="margin-left: 3px;" v-show="selectedValues.length > 0">+{{selectedValues.length}}</span>
2141              <span style="margin-left: 12px;">
2142                 <i class="fas fa-caret-down"></i>
2143               </span>
2144            </div>  
2145            <span class="select-arrow" v-else>
2146              <i class="fas fa-caret-down"></i>
2147            </span>
2148            <ul v-show="showList" class="options items" :class="listClass" @mousedown="isClickingOptions = true" @mouseup="handelMouseUp">
2149             <template v-if="isMultiple">
2150             <li class="item"                   
2151                            @click.prevent="toggleAllItem(displayList)"
2152                             :class="{'is-selected': displayList.selectAll}" 
2153                            v-show="!keyword"
2154                            >
2155                        <label>
2156                            <span class="list-checkbox-input">
2157                                <span class="list-checkbox-inner"></span>
2158                                <input type="checkbox" class="item-checked" v-model="displayList.selectAll">
2159                            </span>
2160                            <sapn name="item" :item="item">Select All</sapn>
2161                        </label>
2162                </li> 
2163              <li class="item"
2164                    v-for="item in displayList"
2165                    :key="item.id"
2166                     @click.prevent="toggleItem(displayList,item)"
2167                    :class="{'is-selected': item.selected}" 
2168                >
2169                  <label>
2170                    <span class="list-checkbox-input">
2171                      <span class="list-checkbox-inner"></span>
2172                      <input type="checkbox" class="item-checked" :value="item.value" v-model="item.selected">
2173                    </span>
2174                    <span class="item-text" name="item">{{ item.name }}</span>
2175                  </label>
2176                </li>
2177            </template>
2178              <template v-else>
2179                    <li
2180                    v-for="item in displayList"
2181                    :key="item.id"
2182                    @click="select($event, item)"
2183                    :data="item.id"
2184                  >
2185                    {{ item.name }}
2186                  </li>
2187              </template>
2188                <li v-if="displayList.length === 0" class="empty">No results match</li>
2189            </ul>
2190          </div>`
2191}
2192window.NeonSearchLocation = {
2193    name: 'neon-search-location',
2194    props: {
2195        placeholder: {
2196            type: String,
2197            default: 'Search by Location'
2198        },
2199        modelValue: ''
2200    },
2201    emits: ['update:modelValue', 'selectAddress', 'clearAddress'],
2202    data() {
2203        return {
2204            show: false,
2205            address: '',
2206            uuid: new Date().getTime() + '-' + ((Math.random() + '').replace('0.', '')),
2207            addressList: [],
2208            loading: false,
2209            searchTimer: null,
2210            isSelecting: false,
2211            selectedAddress: null
2212        }
2213    },
2214    methods: {
2215        handleDocumentClick(e) {
2216            let container = e.target.closest('.neon-search-location-box')
2217            if (!container || container.getAttribute('uuid') !== this.uuid) {
2218                this.show = false
2219            }
2220        },
2221        querySearchLocation() {
2222            if(this.address){
2223                this.show = true
2224            }
2225            clearTimeout(this.searchTimer)
2226            if (!this.address || !this.address.trim()) {
2227                this.addressList = []
2228                this.loading = false
2229                return
2230            }
2231            this.loading = true
2232            this.searchTimer = setTimeout(() => {
2233                axios.get('/nx/publicaccess/geocoding/search', {
2234                    params: { q: this.address }
2235                }).then(response => {
2236                    this.addressList = response.data || []
2237                    this.loading = false
2238                }).catch(error => {
2239                    console.error(error)
2240                    this.addressList = []
2241                    this.loading = false
2242                })
2243            }, 300)
2244        },
2245        keywordOnblur() {
2246            this.querySearchLocation()
2247        },
2248        clearKeyword() {
2249            this.address = ''
2250            this.selectedAddress = null;
2251            this.$emit('update:modelValue', '')
2252            this.$emit('clearAddress')
2253        },
2254        choseAddress(item) {
2255            this.isSelecting = true;
2256            this.address = item.display_name
2257            this.selectedAddress = item
2258            this.$emit('update:modelValue', item.display_name)
2259            this.$emit('selectAddress', item)
2260            this.show = false;
2261            setTimeout(_ => {
2262                this.isSelecting = false
2263            }, 150)
2264        },
2265        handleSearchInputBlur(e) {
2266            setTimeout(_ => {
2267                if (this.isSelecting) {
2268                    return;
2269                }
2270                if (!this.selectedAddress) {
2271                    this.address = ''
2272                } else if (this.address !== this.selectedAddress.display_name) {
2273                    this.address = this.selectedAddress.display_name
2274                }
2275            }, 100)
2276        }
2277    },
2278    mounted() {
2279
2280    },
2281    created() {
2282        document.addEventListener('mouseup', this.handleDocumentClick)
2283    },
2284    beforeDestroy() {
2285        document.removeEventListener('mouseup', this.handleDocumentClick)
2286    },
2287    computed: {
2288
2289    },
2290    watch: {
2291        modelValue(newValue) {
2292            if (newValue) {
2293                this.address = newValue
2294            }
2295        }
2296    },
2297    template: `
2298        <div class="neon-search-location-box" :uuid="uuid">
2299            <div class="input-box">         
2300                <input class="search-name" 
2301                       type="text" 
2302                       v-model="address"
2303                       @input="querySearchLocation"
2304                       @focus="querySearchLocation"
2305                       @blur="handleSearchInputBlur"
2306                       @keydown.enter.prevent="keywordOnblur"
2307                       :placeholder="placeholder">
2308                <span class="msd-input-prefix">      
2309                        <i class="fa-solid fa-location-dot" v-show="!address"></i>
2310                        <i class="fal fa-times" v-show="address" @click="clearKeyword" style="cursor: pointer;"></i>
2311                </span>
2312            </div>
2313            <div class="location-list" v-show="show">
2314                  <div class="loading-item" v-if="loading">
2315                    <i class="fa fa-spinner fa-spin"></i>
2316                  </div>
2317                  <div class="pac-item empty" v-else-if="addressList.length === 0">
2318                    <span class="pac-icon pac-icon-marker"></span>
2319                    <span class="pac-item-query">
2320                      <span class="pac-matched">No Results</span>                 
2321                  </div>
2322                  <div class="pac-item" v-else v-for="(item, index) in addressList"
2323                    :title="item.display_name"
2324                    @click="choseAddress(item)">
2325                    <span class="pac-icon pac-icon-marker"></span>
2326                    <span class="pac-item-query">
2327                      <span class="pac-matched">{{item.display_name}}</span>                 
2328                  </div>
2329            </div>
2330        </div>
2331          `
2332}
2333window.NeonInput = {
2334    name: 'neon-input',
2335    props: {
2336        modelValue: {
2337            type: String,
2338            default: ''
2339        },
2340        clearable: {
2341            type: Boolean,
2342            default: false
2343        },
2344        type: {
2345            type: String,
2346            default: 'text'
2347        },
2348        placeholder: {
2349            type: String,
2350        },
2351        field: {
2352            type: Object,
2353        }
2354    },
2355    data() {
2356        return {
2357            showClear: false
2358        }
2359    },
2360    methods: {
2361        clear() {
2362            this.$emit('update:modelValue', '');
2363        },
2364        clearInput() {
2365            this.clear();
2366        },
2367        registerLayDate($field) {
2368            if ($field.dataType !== '4' && $field.dataType !== '5') {
2369                return
2370            }
2371            let _this = this
2372            let format = $field.dataType === '4' ? 'MM/dd/yyyy' : 'HH:mm:ss'
2373            let type = $field.dataType === '4' ? 'date' : 'time'
2374            let elem = document.getElementById('cf_' + $field.fieldId)
2375            let rangeField = ['membership_stats.firstJoinDate','membership_listing.startDate','membership_listing.endDate']
2376            let $range = rangeField.indexOf($field.fieldCode) > -1 ? '~' : false
2377            laydate.render({
2378                elem: elem,
2379                lang: 'en',
2380                type: type,
2381                format: format,
2382                range: $range,
2383                ready: function (value) {
2384                },
2385                done: function (value) {
2386                    $field.value = value
2387                    _this.changeCalbk($field)
2388                }
2389            })
2390        },
2391        changeCalbk($field) {
2392            this.$emit('update:modelValue', $field.value)
2393            this.$emit('change', $field)
2394        }
2395    },
2396    computed: {},
2397    watch: {},
2398    mounted() {
2399        if (this.field) {
2400            this.registerLayDate(this.field)
2401        }
2402    },
2403    unmounted() {
2404
2405    },
2406    template: `
2407        <div class="neon-input input-group" 
2408            @mouseenter="showClear = true"
2409            @mouseleave="showClear = false">
2410            <template v-if="type === 'text'">
2411                <input class="form-control input" v-bind="$attrs" :placeholder="placeholder" :type="type" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
2412                <span
2413                     v-if="clearable && modelValue && showClear"
2414                    @click="clearInput"
2415                    class="clear-icon"
2416                    title="Clear">
2417                    <i class="fal fa-times"></i>
2418                </span>
2419            </template>
2420            <template v-if="type === 'calendar'">
2421              <input :id="'cf_' + field.fieldId" v-bind="$attrs" class="npb-options-input calendar-input" type="text" :placeholder="placeholder" :value="modelValue" autocomplete="off"/>
2422              <span class="input-group-text far" :class="[field.dataType === '4' ? 'fa-calendar-alt':'fa-clock']" style=""></span>
2423            </template>
2424        </div>`
2425}
2426//===========AJAX settings==============================
2427
2428axios.defaults.headers.post['X-Requested-With'] = 'XMLHttpRequest'
2429axios.defaults.headers.patch['X-Requested-With'] = 'XMLHttpRequest'
2430axios.defaults.headers.get['X-Requested-With'] = 'XMLHttpRequest'
2431
2432// Add request interceptor
2433axios.interceptors.request.use(function (config) {
2434    if(appData.formEmbedEnv || appData.popDomainUrl) {
2435        config.headers['Authorization']=localStorage.getItem('embedToken') || ''
2436    }
2437    // Do something before sending the request
2438    return config
2439}, function (error) {
2440    // Do something with request errors
2441    return Promise.reject(error)
2442});
2443
2444// Add response interceptor
2445axios.interceptors.response.use(function (response) {
2446    // Do something with response data
2447    return response
2448}, function (error) {
2449    const original = error.config
2450    if (error.response && error.response.status === 401 && original && !original._retried
2451        && original.url && original.url.indexOf('/nx/auth/') === -1) {
2452        original._retried = true
2453        return axios.post('/nx/auth/refresh' + (document.cookie.indexOf('neon-portal-session=') >= 0 ? '?portal=1' : '')).then(function (r) {
2454            if (r.data && r.data.success) { return axios(original) }
2455            window.location = '/login'
2456            return Promise.reject(error)
2457        }).catch(function () {
2458            window.location = '/login'
2459            return Promise.reject(error)
2460        })
2461    }
2462    return Promise.reject(error)
2463});
2464
2465/**
2466 * Modern social login entry. options: { targetUrl, orgId, custom, beforeNavigate(url) }.
2467 * If beforeNavigate returns false, navigation is skipped (e.g. iframe postMessage handled by caller).
2468 */
2469window.neonSocialLogin = function (provider, options) {
2470    options = options || {}
2471    var loginUrl = new URL('/nx/auth/social/' + provider, window.location.origin)
2472    if (options.targetUrl) {
2473        loginUrl.searchParams.set('targetUrl', options.targetUrl)
2474    }
2475    if (options.orgId) {
2476        loginUrl.searchParams.set('ins', options.orgId)
2477    }
2478    if (options.custom) {
2479        loginUrl.searchParams.set('custom', options.custom)
2480    }
2481    var href = loginUrl.toString()
2482    if (typeof options.beforeNavigate === 'function' && options.beforeNavigate(href) === false) {
2483        return
2484    }
2485    window.location.href = href
2486}
2487
2488String.prototype.replaceAll = function (s1, s2) {
2489    return this.replace(new RegExp(s1, "gm"), s2)
2490}
2491
2492//=========JSON flatten==============================
2493
2494/**
2495 * un-flatten form include un-checked checkbox
2496 * @param formId
2497 * @returns un-flatten json
2498 */
2499JSON.formAll = function (formId) {
2500    let form = document.getElementById(formId)
2501    let formData = new FormData(form)
2502    // checkbox
2503    for(let e of form.elements){
2504        if(formData.has(e.getAttribute('name'))){
2505            continue
2506        }
2507        formData.append(e.getAttribute('name'), '')
2508    }
2509
2510    return JSON.parseForm(formData)
2511}
2512
2513/**
2514 * un-flatten form
2515 * @param formId Form ID
2516 * @returns un-flatten json
2517 */
2518JSON.form = function(formId){
2519    let formData = new FormData(document.getElementById(formId))
2520    return JSON.parseForm(formData)
2521}
2522
2523JSON.parseForm = function(formData){
2524    let objData = {}
2525    for(let key of formData.keys()){
2526        if(formData.getAll(key).length>1){
2527            objData[key]=formData.getAll(key)
2528        }else{
2529            var ele = formData.get(key);
2530            if(ele instanceof File){
2531                objData[key]=ele.fileInfo
2532            }
2533            objData[key]=ele
2534        }
2535    }
2536    return JSON.unflatten(objData)
2537}
2538
2539JSON.unflatten = function(data) {
2540    "use strict";
2541    if (Object(data) !== data || Array.isArray(data))
2542        return data;
2543    var regex = /\.?([^.\[\]]+)|\[(\d+)\]/g,
2544        resultholder = {};
2545    for (var p in data) {
2546        var cur = resultholder,
2547            prop = "",
2548            m;
2549        while (m = regex.exec(p)) {
2550            cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}));
2551            prop = m[2] || m[1];
2552        }
2553        cur[prop] = data[p];
2554    }
2555    return resultholder[""];
2556};
2557
2558//扁平化
2559JSON.flatten = function(data) {
2560    var result = {};
2561    function recurse (cur, prop) {
2562        if (Object(cur) !== cur) {
2563            result[prop] = cur;
2564        } else if (Array.isArray(cur)) {
2565            for(var i=0, l=cur.length; i<l; i++)
2566                recurse(cur[i], prop + "[" + i + "]");
2567            if (l == 0)
2568                result[prop] = [];
2569        } else {
2570            var isEmpty = true;
2571            for (var p in cur) {
2572                isEmpty = false;
2573                recurse(cur[p], prop ? prop+"."+p : p);
2574            }
2575            if (isEmpty)
2576                result[prop] = {};
2577        }
2578    }
2579    recurse(data, "");
2580    return result;
2581}
2582
2583
2584function setObjectValueByPath(obj, path, val) {
2585    let ths = []
2586    for (let prop of path.split(".")){
2587        if(prop.includes('[') && prop.includes(']')){
2588            ths.push("['")
2589            ths.push(prop.substr(0,prop.indexOf('[')))
2590            ths.push("']")
2591            ths.push(prop.substr(prop.indexOf('[')))
2592        }else{
2593            ths.push("['")
2594            ths.push(prop)
2595            ths.push("']")
2596        }
2597    }
2598    let th = ths.join('')
2599
2600    let isObj = new Function("return " + obj + th);
2601    let e = new Function();
2602    if (typeof isObj() == "object") {
2603        e = new Function(obj + th + "=" + val);
2604    } else if (typeof isObj() == "string") {
2605        e = new Function(obj + th + '="' + val + '"');
2606    }
2607    e();
2608}
2609
2610function getObjectValueByPath(obj, path){
2611       if (!obj || !path) {
2612           return undefined
2613       }
2614        const keys = path.split('.')
2615        let current = obj
2616        for (let i = 0; i < keys.length; i++) {
2617            if (current[keys[i]] === undefined || current[keys[i]] === null && i !== keys.length - 1){
2618                return undefined
2619            }
2620            current = current[keys[i]]
2621        }
2622        return current
2623}
2624function uuid () {
2625    let s = []
2626    var hexDigits = '0123456789abcdef'
2627    for (let i = 0; i < 36; i++) {
2628        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
2629    }
2630    s[14] = '4'
2631    s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1)
2632    s[8] = s[13] = s[18] = s[23] = '-'
2633
2634    return s.join('')
2635}
2636
2637function formatDateToYYYYMMDD(date) {
2638    let y = date.getFullYear();
2639    let m = date.getMonth() + 1;
2640    let d = date.getDate();
2641
2642    m = m.toString().padStart(2, '0');
2643    d = d.toString().padStart(2, '0');
2644
2645    return `${y}-${m}-${d}`;
2646}
2647
2648const Formatter= {
2649    numberFormat: function (number, places, currencySign) {
2650        places = !isNaN(places = Math.abs(places)) ? places : 0
2651        currencySign = currencySign !== undefined ? currencySign : '$'
2652        let negative = number < 0 ? '-' : ''
2653        let i = parseInt(number = Math.abs(+number || 0).toFixed(places), 10) + ''
2654        let j = i.length
2655        j = j > 3 ? j % 3 : 0
2656        return currencySign + negative + (j ? i.substr(0, j) + ',' : '') + i.substr(j).replace(/(\d{3})(?=\d)/g, '$1' + ',') + (places ? '.' + Math.abs(number - i).toFixed(places).slice(2) : '')
2657    },
2658    // Dec 31, 2024
2659    formatDateToEnglish: function (date) {
2660        let nowDate = new Date()
2661        let dd = new Date(date);
2662        if(nowDate.getUTCFullYear() === dd.getUTCFullYear()) {
2663            return dd.toLocaleDateString('en-US', {month: 'short',day: 'numeric'})
2664        }
2665        return dd.toLocaleDateString('en-US', {year: 'numeric',month: 'short',day: 'numeric'})
2666    },
2667    // Dec 31, 2024
2668    formatDateStringToEnglish: function (dateStr) {
2669        let nowDate = new Date()
2670        let dd = new Date(dateStr);
2671        let mom = moment(dateStr)
2672        if(nowDate.getUTCFullYear() === dd.getUTCFullYear()) {
2673            return mom.format("MMM D")
2674        }
2675        return mom.format("MMM D, YYYY")
2676    },
2677    // Monday,Tuesday...
2678    formatDateToWeek: function (date) {
2679        let dd = new Date(date);
2680        return dd.toLocaleDateString('en-US', {weekday: 'long'})
2681    },
2682    formatTime24To12: function (time24h) {
2683        if (!time24h)
2684            return ''
2685        let [hours, minutes] = time24h.split(':');
2686        let period = 'AM';
2687        let hour12;
2688        hours = parseInt(hours, 10);
2689        if (hours >= 12) {
2690            period = 'PM';
2691            if (hours !== 12) {
2692                hours -= 12;
2693            }
2694        } else if (hours === 0) {
2695            hours = 12;
2696        }
2697        hour12 = hours.toString();
2698        return `${hour12}:${minutes} ${period}`;
2699    },
2700    formatMillisecondsToDateTime: function (millisecond) {
2701        let nowDate = new Date()
2702        let dd = new Date(millisecond);
2703        let mom = moment(millisecond).tz('America/Chicago');
2704        if(nowDate.getUTCFullYear() === dd.getUTCFullYear()) {
2705            return mom.format("MMM D") + ' at ' + mom.format("hh:mm A")
2706        }
2707        return mom.format("MMM D, YYYY") + ' at ' + mom.format("hh:mm A")
2708    }
2709}
2710
2711const Validator ={
2712    required:{
2713        check:function(str){
2714            return (str.length == 0)
2715        },
2716        message:function(fieldLabel){
2717            return fieldLabel +' is required.'
2718        }
2719    },
2720    email:{
2721        emailRegex: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
2722        check: function (str){
2723            if(str.length == 0){
2724                return false
2725            }
2726            return !this.emailRegex.test(str)
2727        },
2728        message:function(fieldLabel){
2729            return fieldLabel +' must be an email.'
2730        }
2731    },
2732    number:{
2733        numberRegex: /^([+\-])?[1-9]\d*(\.\d+)?$|^([+\-])?0(\.\d+)?$/,
2734        check:function (str){
2735            if(str.length == 0){
2736                return false
2737            }
2738            return !this.numberRegex.test(str)
2739        },
2740        message:function(fieldLabel){
2741            return fieldLabel +' must be a number.'
2742        }
2743    },
2744    amount:{
2745        amountRegexp: /^([+])?[1-9]\d*(\.\d{1,2})?$|^([+])?0(\.\d{1,2})?$/,
2746        check:function(str){
2747            if (str.length == 0) {
2748                return false
2749            }
2750            return !this.amountRegexp.test(str)
2751        },
2752        message: function (fieldLabel) {
2753            return fieldLabel + ' must be a positive number with two decimal places.'
2754        }
2755    },
2756    date: {
2757        /** check mm/dd/yyyy format */
2758        check: function (str) {
2759            if (str.length == 0) {
2760                return false
2761            }
2762            const regex = /^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/;
2763            if (!regex.test(str)) return true;
2764            var bits = str.split('/');
2765            var d = new Date(bits[2], bits[0] - 1, bits[1])
2766            return !(d && (d.getMonth() + 1) === parseInt(bits[0]))
2767        },
2768        message: function (fieldLabel) {
2769            return fieldLabel + ' must be a date in the format mm/dd/yyyy.'
2770        }
2771    },
2772    future: {
2773        check: function (str){
2774            let inputMoment = moment(str).tz("America/Chicago")
2775            let nowMoment = moment(moment().tz("America/Chicago").format('yyyy-MM-DD')).tz("America/Chicago")
2776            return inputMoment.isSameOrBefore(nowMoment)
2777        },
2778        message: function(fieldLabel) {
2779            return fieldLabel + ' need a future date.'
2780        }
2781    },
2782    integer: {
2783        integerRegex: /^([+\-])?(([1-9][0-9]*)|0)$/,
2784        check: function (str) {
2785            if (str.length == 0) {
2786                return false
2787            }
2788            return !this.integerRegex.test(str)
2789        },
2790        message: 'This field is invalid.',
2791        msg: function(fieldLabel){
2792            return fieldLabel + 'must be a whole number.'
2793        }
2794    },
2795    max: {
2796        check: function (str, args) {
2797            let r = isNaN(parseFloat(str))
2798            if (!r) {
2799                if (parseFloat(str) > parseFloat(args[0].replaceAll(',',''))) {
2800                    return true
2801                }
2802            }
2803            return false
2804        },
2805        message: 'This field is invalid.',
2806        msg: function(fieldLabel){
2807            return fieldLabel + 'is invalid.'
2808        }
2809    },
2810    min: {
2811        check: function (str, args) {
2812            let r = isNaN(parseFloat(str))
2813            if(!args[2]){
2814                return false
2815            }
2816            if(!r && args[2] === '0'){
2817                return parseFloat(str) <= parseFloat(args[2])
2818            }
2819            if (!r) {
2820                if (parseFloat(str) < parseFloat(args[2].replaceAll(',',''))) {
2821                    return true
2822                }
2823            }
2824            return false
2825        },
2826        msg: function(fieldLabel){
2827            return fieldLabel + 'is invalid.'
2828        }
2829    },
2830    maxLength: {
2831        check: function (str, args) {
2832            if (str.length > parseInt(args[0])) {
2833                return true
2834            }
2835            return false
2836        },
2837        message: 'This field is invalid.',
2838        msg: function(fieldLabel){
2839            return fieldLabel + 'is invalid.'
2840        }
2841    },
2842    creditCardNumber: {
2843        cardRegex: /^\d+$/,
2844        check:function(str){
2845            if(str.length==0){
2846                return false
2847            }
2848            let cardNumber = str.replace(/\s/g,"");
2849            return !this.cardRegex.test(cardNumber) || cardNumber.length < 12 || cardNumber.length > 19
2850        }
2851    },
2852    password: {
2853        includeNumberRegex: /\d+/,
2854        check:function(str, arg){
2855            let argVal = arg && arg.length > 0 ? document.getElementById(arg).value : ''
2856            if(str.length === 0 && argVal.length === 0){
2857                return false
2858            }
2859            return !this.includeNumberRegex.test(str) || str.length < 8 || str.indexOf(' ')>-1
2860        }
2861    },
2862    passwordLengthConfirmation:{
2863        includeNumberRegex: /\d+/,
2864        check:function(str){
2865            if(!this.includeNumberRegex.test(str) || str.length < 8 || str.indexOf(' ')>-1){
2866                return false
2867            }
2868            if(str){
2869                return str.length > 25
2870            }else {
2871                return false
2872            }
2873        }
2874    },
2875    passwordConfirmation: {
2876        check: function(str,arg){
2877            let argVal = document.getElementById(arg).value
2878            if(str.length === 0 && argVal.length === 0) {
2879                return false
2880            }
2881            return argVal!==str
2882        }
2883    },
2884    // returning false means pass
2885    loginName: {
2886        check: function(str,arg) {
2887            const regexp = /^[\da-zA-Z_@.+-]*$/g
2888            let argVal = arg && arg.length > 0 ? document.getElementById(arg).value : ''
2889            if (str !== null && str !== '') {
2890                return !regexp.test(str)
2891            } else {
2892                return argVal && !str
2893            }
2894        },
2895        message: "Login name may consist of a-z, A-Z, 0-9, '.', '@', '_', '-' and '+' only"
2896    },
2897    loginNameLengthConfirmation: {
2898        check: function(str) {
2899            return str && (str.length < 4 || str.length > 80)
2900        },
2901        message: 'Login name must be at least 4 characters and at most 80 characters'
2902    },
2903    dateRangeComparator:{
2904        check: function (str,extra){
2905            const targetDate = moment(str).tz("America/Chicago")
2906            const extraLength = extra.length
2907            const [comparatorDateFieldErrorMessage,comparatorDateFieldName,operator] = [extra[extraLength-1],extra[extraLength-2],extra[extraLength-3]]
2908            const comparatorDate = moment(getObjectValueByPath(app.formData,comparatorDateFieldName)).tz("America/Chicago")
2909            let result = false
2910            // When the result is true, it indicates that the value is invali
2910d and an error message appears
2911            if(operator === 'GREATER_THAN'){
2912                 result =  targetDate.isAfter(comparatorDate);
2913            }else if(operator === 'LESS_THAN'){
2914                 result = targetDate.isBefore(comparatorDate);
2915            }
2916            if(!result){
2917                // clear error message
2918                const fieldErrorMessages = app.invalidField[comparatorDateFieldName] || []
2919                const targetIndex = fieldErrorMessages.findIndex(message => message === comparatorDateFieldErrorMessage)
2920                if(targetIndex !== -1 && fieldErrorMessages.splice(targetIndex,1)){
2921                    if(fieldErrorMessages.length === 0){
2922                        app.invalidField[comparatorDateFieldName] = null
2923                    }
2924                }
2925            }
2926            return result
2927        }
2928    },
2929    presentFuture: {
2930        check: function (str){
2931            let inputMoment = moment(str).tz("America/Chicago")
2932            let nowMoment = moment(moment().tz("America/Chicago").format('yyyy-MM-DD')).tz("America/Chicago")
2933            return inputMoment.isBefore(nowMoment)
2934        },
2935    },
2936}
2937
2938const Visibility = {
2939    MATCH_MODE:{
2940        ALL:'ALL',
2941        ANY:'ANY'
2942    },
2943    OPERATOR:{
2944        IS: 'IS',
2945        IS_NOT: 'IS_NOT',
2946        ANY_OF: 'ANY_OF',
2947        ALL_OF: 'ALL_OF'
2948    },
2949    /**
2950     * Judge fields visibility
2951     * @param contexts Array of visibility context
2952     * @param formData Current form data
2953     */
2954    judgeFieldsVisibility: function(contexts = [], formData){
2955        if(!contexts || contexts.length === 0){
2956            return {
2957                visibleFields: [],
2958                hiddenFields: []
2959            }
2960        }
2961        const visibleFields = []
2962        const hiddenFields = []
2963        contexts.filter(c => c.isActive).forEach(context => {
2964            const {matchMode,rules,fieldName,childFields} = context
2965            let isVisible = false
2966            if(matchMode === this.MATCH_MODE.ALL){
2967                isVisible = rules.every(rule => this.matchConditionalRule(rule, formData.getAll(rule.fieldName)))
2968            }else if(matchMode === this.MATCH_MODE.ANY){
2969                isVisible = rules.some(rule => this.matchConditionalRule(rule, formData.getAll(rule.fieldName)))
2970            }
2971            isVisible ? visibleFields.push(fieldName,...childFields) : hiddenFields.push(fieldName,...childFields)
2972        })
2973        // return visible and hidden fields
2974        return {
2975            visibleFields,
2976            hiddenFields
2977        }
2978    },
2979    matchConditionalRule: function(rule,inputValues){
2980        const {operator:ruleOperator,value:ruleValue} = rule
2981        const inputVal = inputValues.toString()
2982        if(ruleOperator === this.OPERATOR.IS){
2983            return inputVal === ruleValue
2984        }
2985
2986        if(ruleOperator === this.OPERATOR.IS_NOT){
2987            return inputVal !== ruleValue
2988        }
2989
2990        const ruleValues = ruleValue ? ruleValue.split(',') : []
2991        if(ruleOperator === this.OPERATOR.ANY_OF){
2992            return ruleValues.some(val => inputValues.includes(val))
2993        }
2994
2995        if(ruleOperator === this.OPERATOR.ALL_OF){
2996            return ruleValues.every(val => inputValues.includes(val))
2997        }
2998
2999        return false
3000    },
3001    deleteObjectValueByPath: function (obj, path) {
3002        if (!obj || !path) {
3003            return
3004        }
3005        const keys = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(k => k !== '')
3006        if (keys.length === 0) {
3007            return
3008        }
3009        let current = obj
3010        for (let i = 0; i < keys.length - 1; i++) {
3011            if (current == null) {
3012                return
3013            }
3014            current = current[keys[i]]
3015        }
3016        if (current == null) {
3017            return
3018        }
3019        const lastKey = keys[keys.length - 1]
3020        if (!Object.prototype.hasOwnProperty.call(current, lastKey)) {
3021            return
3022        }
3023        if (Array.isArray(current) && /^\d+$/.test(lastKey)) {
3024            current.splice(Number(lastKey), 1)
3025        } else {
3026            delete current[lastKey]
3027        }
3028    },
3029
3030
3031    setObjectValueByPath: function (obj, path, value) {
3032        if (!obj || !path) {
3033            return
3034        }
3035        const keys = path.replace(/\[(\d+)\]/g, '.$1').split('.')
3036        let current = obj
3037        for (let i = 0; i < keys.length - 1; i++) {
3038            const key = keys[i]
3039            if (!key) {
3040                continue
3041            }
3042            // Create the nested object/array if it doesn't exist
3043            if (current[key] == null) {
3044                current[key] = /^\d+$/.test(keys[i + 1]) ? [] : {}
3045            }
3046            current = current[key]
3047        }
3048        const lastKey = keys[keys.length - 1]
3049        current[lastKey] = value
3050    }
3051}
3052
3053const NeonFA = {
3054
3055    track(eventType, eventData) {
3056        try { window.neonone_fa?.push(eventType, eventData); } catch { /* swallow */ }
3057    },
3058
3059    /** Track a successful form submission. */
3060    trackSubmission(data) {
3061        this.track('submission', data);
3062    },
3063
3064    /**
3065     * Track a successful purchase.
3066     */
3067    trackPurchase(data) {
3068        this.track('purchase', data);
3069    },
3070
3071    /**
3072     * Track a field-level validation error.
3073     */
3074    trackFieldError(fieldName, fieldLabel) {
3075        if (!fieldName) return;
3076        this.track('field_error', { field_name: fieldName, field_label: fieldLabel });
3077    },
3078
3079    /**
3080     * Track a field-level field interaction.
3081     */
3082    trackFieldInteraction(data) {
3083        if (!data.field_name) return;
3084        this.track('field_interaction', data);
3085    },
3086
3087    /**
3088     * Track a payment failure from an axios error.
3089     */
3090    trackPaymentFailed(error,formType) {
3091        if(formType !== 'DONATION' && formType !== 'MEMBERSHIP'){
3092            return;
3093        }
3094        if(!error.response || !error.response.data){
3095            return;
3096        }
3097        const {data: errors} = error.response
3098        const global = errors.find(e => e.type === 'GLOBAL')
3099        this.track('payment_failed', {
3100            error_code:    global?.code    ?? '',
3101            error_message: global?.message ?? '',
3102        });
3103    },
3104};
3105try {
3106    window.onerror = function (errorMessage, scriptURI, lineNo, columnNo, error) {
3107        var errorObj = {
3108            sourceType: 'front-forms',
3109            errorMessage: errorMessage || null,
3110            scriptURI: scriptURI || null,
3111            lineNo: lineNo || null,
3112            columnNo: columnNo || null,
3113            stack: error && error.stack ? error.stack : null
3114        };
3115
3116        sendErrorMessage(errorObj);
3117    };
3118} catch (e) {
3119    console.log('Initialize error collector failed.');
3120}
3121
3122function handleVueJSError(err, vm, info) {
3123    try {
3124        var errorObj = {
3125            sourceType: 'front-forms',
3126            errorMessage: err.message,
3127            stack: err.stack
3128        };
3129        sendErrorMessage(errorObj);
3130    } catch (e) {
3131        console.log('Sending error stack failed.');
3132    }
3133}
3134
3135// Throttle state: only applies to the opaque cross-origin "Script error." pattern
3136// (no stack/scriptURI/lineNo available) that caused a runaway report flood in
3137// WORK-31834 - caps how many such reports a single page load can send and
3138// enforces a minimum gap between them. Errors carrying a real stack trace are
3139// always sent immediately, since they are useful diagnostic signal rather than
3140// the anonymous cross-origin noise this throttle is meant to suppress.
3141const FRONT_ERROR_MAX_REPORTS = 5;
3142const FRONT_ERROR_MIN_INTERVAL_MS = 10000;
3143let frontErrorReportCount = 0;
3144let frontErrorLastReportTime = 0;
3145
3146function isOpaqueScriptError(errorObj) {
3147    return errorObj.errorMessage === 'Script error.' && !errorObj.stack;
3148}
3149
3150function sendErrorMessage(errorObj) {
3151    try {
3152        if (isOpaqueScriptError(errorObj)) {
3153            const now = Date.now();
3154            const tooFrequent = now - frontErrorLastReportTime < FRONT_ERROR_MIN_INTERVAL_MS;
3155            if (frontErrorReportCount >= FRONT_ERROR_MAX_REPORTS || tooFrequent) {
3156                // Reporting to the backend is throttled, but keep the error visible
3157                // locally so it is not silently lost during local debugging.
3158                console.warn('[err-collect] report throttled, error still occurred:', errorObj);
3159                return;
3160            }
3161            frontErrorLastReportTime = now;
3162            frontErrorReportCount++;
3163        }
3164        axios.post('/nx/publicaccess/frontErrors', Qs.stringify({errMsg: JSON.stringify(errorObj)}));
3165    } catch (e) {
3166        console.log('Sending script error failed.');
3167    }
3168}
3169/**
3170 * New Storefront cart: sessionStorage persistence + field-level account merge/hydrate
3171 * (design spec `docs/superpowers/specs/2026-07-23-new-storefront-form-flow-design.md`
3172 * §5 cart DTO, §6 account merge, §8.5 sessionStorage key, §12 integration contract).
3173 *
3174 * Runtime notes:
3175 * - This is a plain classic `<script>` (no build step, no ES module), matching the other
3176 *   files in this directory (e.g. `common.js`, `err-collect.js`) that are merged into
3177 *   `portal.all.js` in production and served individually under `/nx/portal-static/js/`
3178 *   in dev (see `function/common/js.html`).
3179 * - It exposes a single global namespace, `window.NeonStoreCart`, to be consumed by the
3180 *   per-item-type form JS (`js/product.html`, `js/membership.html`,
3181 *   `js/event_registration.html`) and by `store-flow.js` (`window.NeonStore`, Task 5.2)
3182 *   and the shared Summary/Payment page.
3183 *
3184 * @module store-cart
3185 */
3186(function (global) {
3187  'use strict';
3188
3189  /**
3190   * Resolve the current org id for namespacing the sessionStorage key.
3191   *
3192   * There is no standalone `window.orgId` global in this codebase. Every per-page inline
3193   * Vue bootstrap script (`function/form/js/base.html:25`, `js/exit_base.html:10`,
3194   * `function/p2p-portal/js/base.html:19`, `function/campaign/common/js/main.html:350`, ...)
3195   * declares its own top-level `const appData = { ..., orgId: [[${userContext.orgId}]], ... }`
3196   * followed by `const app = vueApp.mount('#app')`. Because these are classic (non-module)
3197   * `<script>` blocks, their top-level `const`/`let` bindings live in the single Global
3198   * Declaration Record shared by every classic script in the document — so later classic
3199   * scripts (this file, or `common.js`) can read the bare identifiers `appData`/`app`
3200   * directly. This is not a new trick: `portal-templates/static/js/common.js:9` already
3201   * reads bare `appData.formEmbedEnv`/`appData.popDomainUrl`, and `common.js:448-462`
3202   * already reads bare `app.formData`/`app.invalidField` the same way.
3203   *
3204   * `appData` is checked first (and preferred) because it is declared/initialized near the
3205   * top of the bootstrap script, before `app`, so it is safely available even if a cart
3206   * operation were ever triggered synchronously during the Vue `mount()` call itself (while
3207   * `const app = vueApp.mount(...)` is still being evaluated, i.e. before `app` there is
3208   * initialized). Both accesses are wrapped in try/catch because referencing an
3209   * uninitialized top-level `let`/`const` (temporal dead zone) throws `ReferenceError`,
3210   * unlike a plain undeclared identifier.
3211   *
3212   * @returns {string|number} the org id, or `'default'` if it cannot be resolved (e.g. a
3213   *   page that has not defined `appData`/`app` yet, or a standalone test harness).
3214   */
3215  function resolveOrgId() {
3216    try {
3217      // eslint-disable-next-line no-undef
3218      if (typeof appData !== 'undefined' && appData && appData.orgId != null) {
3219        // eslint-disable-next-line no-undef
3220        return appData.orgId;
3221      }
3222    } catch (e) { /* appData in TDZ or not defined on this page; fall through */ }
3223    try {
3224      // eslint-disable-next-line no-undef
3225      if (typeof app !== 'undefined' && app && app.orgId != null) {
3226        // eslint-disable-next-line no-undef
3227        return app.orgId;
3228      }
3229    } catch (e) { /* app in TDZ or not defined on this page; fall through */ }
3230    return 'default';
3231  }
3232
3233  /**
3234   * Build the sessionStorage key for the current org's cart (spec §8.5 / §12.5:
3235   * `neon-store-cart:<orgId>`).
3236   * @returns {string}
3237   */
3238  function storageKey() {
3239    return 'neon-store-cart:' + resolveOrgId();
3240  }
3241
3242  /**
3243   * Resolve the currently logged-in constituent's account id from the shared portal bootstrap
3244   * (`function/common/constituent_portal_js.html`, which sets `appData.currentAccountId` from
3245   * `userContext.accountId`). Named `currentAccountId` (not `accountId`) on `appData` to avoid
3246   * colliding with any per-page appData field that may already carry that generic name.
3247   *
3248   * @returns {string|number|null} the logged-in constituent's account id, or `null` if the
3249   *   shopper is a guest / bootstrap hasn't populated it yet.
3250   */
3251  function resolveCurrentAccountId() {
3252    try {
3253      // eslint-disable-next-line no-undef
3254      if (typeof appData !== 'undefined' && appData && appData.currentAccountId != null) {
3255        // eslint-disable-next-line no-undef
3256        return appData.currentAccountId;
3257      }
3258    } catch (e) { /* appData in TDZ or not defined on this page; fall through */ }
3259    return null;
3260  }
3261
3262  /**
3263   * @returns {Object} a fresh, empty `StoreShoppingCartForm`-shaped cart (spec §5.2),
3264   *   plus the front-end-only `pendingItem` (spec §12.4), `customFieldAnswers`
3265   *   (front-end-only, see `upsertCustomFieldAnswers` below), and `editingItemIndex`
3266   *   (front-end-only, Summary page "pencil" icon edit flow -- see `store-flow.js`'s `editItem`).
3267   */
3268  function emptyCart() {
3269    return {
3270      account: {},
3271      payment: null,
3272      shipping: null,
3273      shippingMethodId: null,
3274      shippingMethod: null,
3275      carrierCode: null,
3276      shipHandlingCost: null,
3277      // New Storefront (product checkout-flow redesign): whether the currently-selected shipping
3278      // method/rate is up to date with everything in cart.items right now. false/absent means "a
3279      // PRODUCT item requiring shipping is in the cart but its rate has not been (re)confirmed on
3280      // the Shipping page yet" -- see `needsShippingCalculation`/`mergeProductItem` below and
3281      // `storeCompleteItemForm`'s `if (json.shipping)` branch (`form/js/product.html`), which sets
3282      // this true once the shopper actually completes the Shipping page.
3283      shippingCalculated: false,
3284      items: [],
3285      pendingItem: null,
3286      editingItemIndex: null,
3287      extraDonation: null,
3288      source: null,
3289      discountCode: null,
3290      accountId: null,
3291      continueShoppingUrl: null,
3292      shoppingCartId: null,
3293      customFieldAnswers: {}
3294    };
3295  }
3296
3297  /**
3298   * Load the cart from sessionStorage, or a fresh empty cart if absent/corrupt.
3299   *
3300   * Also lazily populates `cart.accountId` from the currently logged-in constituent
3301   * (see {@link resolveCurrentAccountId}) so that the backend
3302   * {@code StoreCartMerger.resolveBuyerAccountId} has the right buyer context for
3303   * discount/tax lookup. If the resolved current account id DIFFERS from a previously
3304   * persisted `cart.accountId` (e.g. logout+relogin as a different constituent, or a shared
3305   * device swap), the persisted cart is discarded and a fresh empty cart bound to the new
3306   * account is returned -- avoids cross-account contamination of items/account/shipping/etc.
3307   * @returns {Object}
3308   */
3309  function loadCart() {
3310    var cart;
3311    try {
3312      var raw = global.sessionStorage.getItem(storageKey());
3313      cart = raw ? JSON.parse(raw) : emptyCart();
3314    } catch (e) {
3315      cart = emptyCart();
3316    }
3317    var currentAid = resolveCurrentAccountId();
3318    if (currentAid != null) {
3319      if (cart.accountId != null && String(cart.accountId) !== String(currentAid)) {
3320        // Account switch detected -- discard the prior shopper's cart entirely.
3321        cart = emptyCart();
3322      }
3323      if (cart.accountId == null) {
3324        cart.accountId = currentAid;
3325      }
3326      saveCart(cart);
3327    }
3328    return cart;
3329  }
3330
3331  /**
3332   * Persist the cart to sessionStorage.
3333   * @param {Object} cart
3334   */
3335  function saveCart(cart) {
3336    global.sessionStorage.setItem(storageKey(), JSON.stringify(cart));
3337  }
3338
3339  /** Clear the cart from sessionStorage (e.g. after successful checkout, spec §8.5). */
3340  function clearCart() {
3341    global.sessionStorage.removeItem(storageKey());
3342  }
3343
3344  // --- nested path helpers (mirrors the dotted-path convention already used by
3345  // portal-static/js/common.js's setObjectValueByPath/getObjectValueByPath, but operating
3346  // on plain JS objects rather than DOM-bound `Function` construction, since cart state
3347  // is a detached JSON object, not a live Vue binding path expression) ---
3348
3349  /**
3350   * Set a value at a dotted path on a plain object, creating intermediate objects as needed.
3351   * @param {Object} obj
3352   * @param {string} path e.g. `"name.firstName"`
3353   * @param {*} val
3354   */
3355  function setByPath(obj, path, val) {
3356    var parts = path.split('.'), cur = obj;
3357    for (var i = 0; i < parts.length - 1; i++) {
3358      if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
3359      cur = cur[parts[i]];
3360    }
3361    cur[parts[parts.length - 1]] = val;
3362  }
3363
3364  /**
3365   * Read a value at a dotted path on a plain object.
3366   * @param {Object} obj
3367   * @param {string} path
3368   * @returns {*} `undefined` if any intermediate segment is missing.
3369   */
3370  function getByPath(obj, path) {
3371    var parts = path.split('.'), cur = obj;
3372    for (var i = 0; i < parts.length; i++) {
3373      if (cur == null) return undefined;
3374      cur = cur[parts[i]];
3375    }
3376    return cur;
3377  }
3378
3379  /**
3380   * Enumerate leaf paths of a nested plain object (arrays are treated as leaves, not
3381   * recursed into, since account/address fields are all scalar or object-valued per
3382   * `AccountForm`/`AddressForm`).
3383   *
3384   * Exported (not just used internally by `upsertAccount`/`hydrateAccount`): the per-type item
3385   * form runtimes (`function/form/js/product.html` etc.) also call this directly to find which
3386   * hydrated `account.*`/`shipping.*` leaves correspond to `<select>` DROP_DOWN fields (Country/
3387   * State-Province/Territory) so they can sync the actual DOM element too -- see those files'
3388   * `storeSyncDropdownFromCart` for why a plain `formData` write alone isn't enough for a
3389   * `<select>` with no `v-model` (`function/form/field.html`'s `DROP_DOWN` case).
3390   * @param {Object} obj
3391   * @param {string} [prefix]
3392   * @returns {string[]}
3393   */
3394  function leafPaths(obj, prefix) {
3395    var out = [];
3396    Object.keys(obj || {}).forEach(function (k) {
3397      var p = prefix ? prefix + '.' + k : k, v = obj[k];
3398      if (v != null && typeof v === 'object' && !Array.isArray(v)) {
3399        out = out.concat(leafPaths(v, p));
3400      } else {
3401        out.push(p);
3402      }
3403    });
3404    return out;
3405  }
3406
3407  /**
3408   * Field-level, non-destructive merge: write ONLY the account paths present in
3409   * `formAccountJson` into `cart.account`; leave every other existing `cart.account` field
3410   * untouched (spec §6.2 — "form 上没有的字段不受影响").
3411   *
3412   * Bug fix: EXCLUDES the `customFields` leaf (`AccountForm.customFields`, inherited from
3413   * `BaseForm` -- account-LEVEL custom fields configured on a form's own Account page, distinct
3414   * from that form's top-level item-specific `customFields`). `leafPaths` treats arrays as a
3415   * single opaque leaf, so blindly copying it here carried one form's ENTIRE account-custom-field
3416   * answer array into the shared cart -- and `hydrateAccount` below would then carry that SAME
3417   * array into a LATER, unrelated form's `formData.account.customFields`, submitting a custom
3418   * field id that form never configured and crashing `CustomFieldFormConverter`/`FormService
3419   * #retrieveCustomField` (`Optional.get()` on no match) wherever that account got submitted
3420   * (shipping-rates preview, calculate, checkout). Account-level custom field ANSWERS are instead
3421   * carried through the same id-keyed `cart.customFieldAnswers` map already used for item-level
3422   * customFields -- see `upsertCustomFieldAnswers`/`hydrateCustomFields` below and their call
3423   * sites (`storeCompleteItemForm`/the storeMode==='ITEM' init hooks in each per-type form file),
3424   * which only ever touch a field id a given form's OWN rendered array already has.
3425   * @param {Object} cart
3426   * @param {Object} formAccountJson the `account` sub-object of a form's `getNeonFormJson()`
3427   *   output (i.e. shaped like `AccountForm`).
3428   */
3429  function upsertAccount(cart, formAccountJson) {
3430    if (!formAccountJson) return;
3431    if (!cart.account) cart.account = {};
3432    leafPaths(formAccountJson).forEach(function (path) {
3433      if (path === 'customFields') return;
3434      var v = getByPath(formAccountJson, path);
3435      if (v !== undefined) setByPath(cart.account, path, v);
3436    });
3437  }
3438
3439  /**
3440   * Fill a Vue `formData.account` from `cart.account` (spec §6.1 guest cross-purchase
3441   * hydrate). Only sets paths already present in `cart.account`; fields the current form
3442   * doesn't render simply aren't touched (harmless — hydrate is the mirror image of
3443   * `upsertAccount`'s non-destructive write).
3444   *
3445   * Skips null/undefined/empty-string values: a logged-in form is server-rendered from the
3446   * CRM account (often blank for phone / custom questions the shopper just filled on this
3447   * cart item). Copying those empty cart leaves would wipe the snapshot we are about to
3448   * overlay from the item itself. `0` and `false` still copy.
3449   *
3450   * Excludes the `customFields` leaf -- see `upsertAccount`'s doc comment for why (that array is
3451   * never written into `cart.account` in the first place, but this mirrors the same exclusion
3452   * defensively in case `cart.account.customFields` exists from an older/mixed cart state).
3453   * @param {Object} formData the form's reactive `formData` object (must already have the
3454   *   shape produced by `JSON.formAll(...)`, per `js/base.html:16`).
3455   * @param {Object} cart
3456   */
3457  function hydrateAccount(formData, cart) {
3458    if (!cart || !cart.account || !formData) return;
3459    if (!formData.account) formData.account = {};
3460    leafPaths(cart.account).forEach(function (path) {
3461      if (path === 'customFields') return;
3462      var v = getByPath(cart.account, path);
3463      if (v === undefined || v === null || v === '') return;
3464      setByPath(formData.account, path, v);
3465    });
3466  }
3467
3468  /**
3469   * Fill a Vue `formData.shipping` (+ top-level `shippingMethodId`/`shippingMethod`/
3470   * `carrierCode`/`shipHandlingCost`) from `cart.shipping`, mirroring `hydrateAccount`'s
3471   * field-level fill: every subsequent product purchase must pre-fill previously-collected
3472   * shipping info (still editable), not just account (design revision: the Seam-2 "skip
3473   * straight to summary" fast path was removed, so the shopper always sees this form's own
3474   * Shipping page again and it must not start blank if shipping is already known).
3475   * @param {Object} formData the form's reactive `formData` object.
3476   * @param {Object} cart
3477   */
3478  function hydrateShipping(formData, cart) {
3479    if (!cart || !cart.shipping || !formData) return;
3480    if (!formData.shipping) formData.shipping = {};
3481    leafPaths(cart.shipping).forEach(function (path) {
3482      setByPath(formData.shipping, path, getByPath(cart.shipping, path));
3483    });
3484    if (cart.shippingMethodId != null) formData.shippingMethodId = cart.shippingMethodId;
3485    if (cart.shippingMethod != null) formData.shippingMethod = cart.shippingMethod;
3486    if (cart.carrierCode != null) formData.carrierCode = cart.carrierCode;
3487    if (cart.shipHandlingCost != null) formData.shipHandlingCost = cart.shipHandlingCost;
3488  }
3489
3490  /**
3491   * Append an item to the cart (spec §5.2 `items[]`: `{type, formId, customFields,
3492   * hiddenFields, payload}`).
3493   * @param {Object} cart
3494   * @param {Object} item
3495   */
3496  function addItem(cart, item) {
3497    if (!cart.items) cart.items = [];
3498    cart.items.push(item);
3499  }
3500
3501  /**
3502   * Remove one item from the cart by index (Summary page "trash" icon, spec revision:
3503   * delete-item + recalculate).
3504   * @param {Object} cart
3505   * @param {number} index
3506   */
3507  function removeItem(cart, index) {
3508    if (!cart.items || index == null || index < 0 || index >= cart.items.length) return;
3509    cart.items.splice(index, 1);
3510  }
3511
3512  /**
3513   * Total item quantity across the cart, used by the giftstore / product-detail
3514   * cart-icon badge (bottom-right corner, disabled when the total is 0).
3515   *
3516   * Per-item quantity rules:
3517   * - PRODUCT items store their qty on `payload.productLines[i].quantity`
3518   *   (built by product.html's `onSubmit` via `window.NeonStore.addProduct`).
3519   *   Sum every line so quantity-picker choices are honoured — a single cart
3520   *   entry can carry qty>1 (or, defensively, multiple product lines).
3521   * - Every other item type (MEMBERSHIP / EVENT / DONATION / package parent, ...)
3522   *   is atomic: one cart entry == one purchase, so it always contributes 1.
3523   *   These types do not persist a shopper-editable quantity anywhere on the
3524   *   cart item, so summing anything else would be inventing data.
3525   *
3526   * @param {Object} cart cart object returned by {@link loadCart}
3527   * @returns {number} total quantity (0 for an empty / missing cart)
3528   */
3529  function totalQuantity(cart) {
3530    if (!cart || !Array.isArray(cart.items) || cart.items.length === 0) {
3531      return 0;
3532    }
3533    var total = 0;
3534    for (var i = 0; i < cart.items.length; i++) {
3535      var it = cart.items[i];
3536      if (it && it.type === 'PRODUCT' && it.payload && Array.isArray(it.payload.productLines)) {
3537        for (var j = 0; j < it.payload.productLines.length; j++) {
3538          var q = Number(it.payload.productLines[j] && it.payload.productLines[j].quantity);
3539          if (!isNaN(q) && q > 0) {
3540            total += q;
3541          }
3542        }
3543      } else if (it) {
3544        total += 1;
3545      }
3546    }
3547    return total;
3548  }
3549
3550  /**
3551   * Whether the cart's account satisfies the minimum identity fields needed to skip
3552   * re-collecting account data on a repeat purchase (spec §12.3 Seam 2).
3553   *
3554   * Basis: `account/model/AccountForm.java` (and its nested `NameForm`) declare no
3555   * `@NotNull`/`@NotEmpty` bean-validation constraints on any field — required-ness for
3556   * account fields is entirely form-builder-config-driven (the same `fieldRules`/
3557   * `fieldConditionalRules` mechanism validated client-side in `js/base.html`'s
3558   * `validateField`), not a fixed set baked into the model.
3559   *
3560   * Bug fix: email1 was previously part of this baseline too, but when a form's own config
3561   * leaves the Email field optional, a shopper who legitimately submits the Account page
3562   * without one would have this permanently return false -- Summary's "Check out" button
3563   * (`storeCheckoutClick`) sends them back to editAccount() on every click, an infinite loop
3564   * they can never escape by re-submitting the exact same (valid) data. First/last name are
3565   * the only fields this can safely assume are always present once an account has actually
3566   * been submitted here.
3567   *
3568   * Bug fix (user-reported): company accounts (`AccountForm.asCompany === true`, an admin-config
3569   * choice available to ANY form type via `AccountModeEnum.COMPANY`/`INDIVIDUAL_COMPANY` --
3570   * `AsCompanyFieldProcessor`, not Product-specific) submit `account.company.name` instead of
3571   * `name.firstName`/`lastName` (`NameForm`'s fields stay blank for a company submission -- see
3572   * the Individual/Company toggle's own field-hiding rules, `js/base.html`'s `asCompany` watcher).
3573   * The name-only check above previously treated every company account as having "no account"
3574   * forever, sending them back to `editAccount()` on every Check Out click with no way to escape
3575   * (same class of infinite-loop bug the email1 fix above already closed for individual
3576   * accounts) -- now accepted as an equally valid "account known" baseline.
3577   * @param {Object} cart
3578   * @returns {boolean}
3579   */
3580  function hasAccount(cart) {
3581    var a = cart && cart.account;
3582    if (!a) return false;
3583    if (a.asCompany) {
3584      return !!getByPath(a, 'company.name');
3585    }
3586    var first = getByPath(a, 'name.firstName'),
3587        last = getByPath(a, 'name.lastName');
3588    return !!(first && last);
3589  }
3590
3591  /**
3592   * Whether the cart already has shipping info (spec §12.3 Seam 2).
3593   * @param {Object} cart
3594   * @returns {boolean}
3595   */
3596  function hasShipping(cart) {
3597    return !!(cart && cart.shipping);
3598  }
3599
3600  /**
3601   * Whether two ProductLine-shaped objects (`{productId, choice1, choice2, choice3,
3602   * accessCode}`) represent the exact same product configuration -- ported verbatim from
3603   * `form/js/product.html`'s `storeProductLineMatches` so `mergeProductItem` below (the
3604   * product-detail page's direct Add to Cart, which has no item-form Vue instance to call that
3605   * method on) can apply the identical "same product -> sum quantity" rule. `quantity` is
3606   * intentionally excluded (that's the field being summed, not matched on).
3607   * @param {Object} a
3608   * @param {Object} b
3609   * @returns {boolean}
3610   */
3611  function productLineMatches(a, b) {
3612    if (!a || !b) return false;
3613    function norm(v) { return (v === undefined || v === null || v === '') ? null : String(v); }
3614    return norm(a.productId) === norm(b.productId)
3615        && norm(a.choice1) === norm(b.choice1)
3616        && norm(a.choice2) === norm(b.choice2)
3617        && norm(a.choice3) === norm(b.choice3)
3618        && norm(a.accessCode) === norm(b.accessCode);
3619  }
3620
3621  /**
3622   * Sums the quantity already reserved in `cart.items[]` for a PRODUCT configuration matching
3623   * `line` (by `productLineMatches` -- same productId+choice1/2/3+accessCode rule `mergeProductItem`
3624   * and `storeCompleteItemForm` (`form/js/product.html`) already use for "same product -> sum
3625   * quantity"). Bug fix (user-reported): the Giftstore product-detail page's own stock check
3626   * (`quantityError`/`recomputeCombo`, `giftstore/product/js/product.html`) only ever validated
3627   * the freshly-typed quantity against the product's total inventory -- it never accounted for
3628   * quantity of the SAME product+choice combo already sitting in the cart from an earlier visit
3629   * to this page, so a shopper could buy up to the stock limit, come back to the same product
3630   * page, and add more on top without ever being blocked (inventory only gets enforced once, at
3631   * checkout, server-side -- this closes the client-side gap that made the page's own stock
3632   * message misleading).
3633   *
3634   * `excludeItemIndex` (typically `cart.editingItemIndex`, when set) excludes one cart item's own
3635   * reserved quantity from the sum -- used when the shopper is editing that exact item (Summary
3636   * page pencil icon -> product-detail page -> `NeonStore.addProduct`), since `mergeProductItem`
3637   * REPLACES that item's line on save rather than adding to it; without the exclusion, editing an
3638   * item down to a lower quantity would still count its own pre-edit quantity as "already
3639   * reserved" and could wrongly block a value that is actually valid.
3640   * @param {Object} cart
3641   * @param {Object} line `{productId, choice1, choice2, choice3, accessCode}` (quantity ignored)
3642   * @param {number} [excludeItemIndex] index into `cart.items[]` to skip, or `null`/`undefined`
3643   *   to sum across every item.
3644   * @returns {number}
3645   */
3646  function reservedQuantityForProductLine(cart, line, excludeItemIndex) {
3647    if (!cart || !Array.isArray(cart.items) || !line) return 0;
3648    var total = 0;
3649    for (var i = 0; i < cart.items.length; i++) {
3650      if (excludeItemIndex != null && i === excludeItemIndex) continue;
3651      var it = cart.items[i];
3652      if (!it || it.type !== 'PRODUCT' || !it.payload || !Array.isArray(it.payload.productLines)) continue;
3653      for (var j = 0; j < it.payload.productLines.length; j++) {
3654        var existingLine = it.payload.productLines[j];
3655        if (productLineMatches(existingLine, line)) {
3656          total += Number(existingLine.quantity) || 0;
3657        }
3658      }
3659    }
3660    return total;
3661  }
3662
3663  /**
3664   * Add (or merge into an existing matching line) a PRODUCT selection straight into
3665   * `cart.items[]`, bypassing the item-form entirely (New Storefront product checkout-flow
3666   * redesign, confirmed with user: Add to Cart no longer visits the item-form's Account/Shipping
3667   * pages -- those are now collected lazily from the Summary page's Check Out button instead, see
3668   * `store-flow.js`'s `editAccount`/`editShipping`). Mirrors `storeCompleteItemForm`'s (`form/js/
3669   * product.html`) non-editing "add" branch: same-configuration lines (by `productLineMatches`)
3670   * sum quantity in place; otherwise a new item is appended with an empty `customFields`/
3671   * `hiddenFields` (this store's Product forms have no per-product custom fields configured today
3672   * -- if that ever changes, the shopper will simply never be asked, by explicit user decision,
3673   * once account info already exists).
3674   *
3675   * Marks `cart.shippingCalculated = false` whenever the merged/added line itself requires
3676   * shipping (user-confirmed rule): the cart's composition just changed, so any previously
3677   * confirmed shipping rate is potentially stale and must be reconfirmed on the Shipping page
3678   * before checkout can proceed to Payment (see `needsShippingCalculation`, and `storeCheckout`
3679   * in `form/js/product.html`). Does NOT reset when the item does not require shipping -- adding
3680   * a no-shipping product can't invalidate an already-confirmed rate.
3681   *
3682   * Bug fix: if `cart.editingItemIndex` is set (Summary page "pencil" icon -> product-detail page
3683   * -> `onSubmit` -> `NeonStore.addProduct` -> here), this call is completing an EDIT of that
3684   * existing item, not a fresh add -- it must REPLACE that item's line in place (same rule
3685   * `storeCompleteItemForm`'s `form/js/product.html` editingItemIndex branch already applies for
3686   * the item-form flow), never merge-by-match/sum quantity against it or any other line. Without
3687   * this branch, editing an item to a different quantity/selection routed through here (since the
3688   * checkout-flow redesign made this the only Add to Cart path for PRODUCT) would `productLineMatches`
3689   * the pre-edit entry it is supposed to be replacing and silently sum quantities instead (e.g.
3690   * qty 2 edited down to 1 becomes 3), and `editingItemIndex` would never be cleared.
3691   * @param {Object} cart
3692   * @param {Object} meta same shape as `enterItemForm`'s PRODUCT meta (`store-flow.js`):
3693   *   `{formId, productId, quantity, choice1, choice2, choice3, accessCode, requiresShipping}`.
3694   */
3695  function mergeProductItem(cart, meta) {
3696    if (!cart.items) cart.items = [];
3697    var newLine = {
3698      productId: meta.productId,
3699      quantity: meta.quantity,
3700      choice1: meta.choice1,
3701      choice2: meta.choice2,
3702      choice3: meta.choice3,
3703      accessCode: meta.accessCode
3704    };
3705    var editingIndex = cart.editingItemIndex;
3706    if (editingIndex != null && editingIndex >= 0 && editingIndex < cart.items.length) {
3707      var editingItem = cart.items[editingIndex];
3708      if (editingItem && editingItem.type === 'PRODUCT') {
3709        editingItem.formId = meta.formId;
3710        editingItem.payload = editingItem.payload || {};
3711        editingItem.payload.productLines = [newLine];
3712        editingItem.payload.requiresShipping = !!meta.requiresShipping;
3713        cart.editingItemIndex = null;
3714        if (meta.requiresShipping) {
3715          cart.shippingCalculated = false;
3716        }
3717        return;
3718      }
3719      // Defensive: stale/mismatched editingItemIndex (not a PRODUCT item) -- clear it rather than
3720      // silently falling through to the merge-by-match branch below with it still set.
3721      cart.editingItemIndex = null;
3722    }
3723    var merged = false;
3724    for (var i = 0; i < cart.items.length && !merged; i++) {
3725      var it = cart.items[i];
3726      if (!it || it.type !== 'PRODUCT' || !it.payload || !Array.isArray(it.payload.productLines)) continue;
3727      for (var j = 0; j < it.payload.productLines.length; j++) {
3728        var existingLine = it.payload.productLines[j];
3729        if (productLineMatches(existingLine, newLine)) {
3730          existingLine.quantity = (Number(existingLine.quantity) || 0) + (Number(newLine.quantity) || 0);
3731          merged = true;
3732          break;
3733        }
3734      }
3735    }
3736    if (!merged) {
3737      addItem(cart, {
3738        type: 'PRODUCT',
3739        formId: meta.formId,
3740        customFields: [],
3741        hiddenFields: [],
3742        payload: {
3743          productLines: [newLine],
3744          discountCode: '',
3745          requiresShipping: !!meta.requiresShipping
3746        }
3747      });
3748    }
3749    if (meta.requiresShipping) {
3750      cart.shippingCalculated = false;
3751    }
3752  }
3753
3754  /**
3755   * Whether the Summary page's Check Out button (`storeCheckout`, `form/js/product.html`) must
3756   * route the shopper to the product item-form's Shipping page before checkout can proceed to
3757   * Payment: the cart has a PRODUCT item requiring shipping, and its rate has not been confirmed
3758   * (or was invalidated by a later add — see `mergeProductItem`) since.
3759   *
3760   * Deliberately re-checks "does the cart currently have a shipping-requiring PRODUCT item" fresh
3761   * every time rather than trusting `shippingCalculated` alone: if the shopper deletes the only
3762   * such item, this correctly returns false regardless of the flag's stale value (no explicit
3763   * reset needed on delete).
3764   * @param {Object} cart
3765   * @returns {boolean}
3766   */
3767  function needsShippingCalculation(cart) {
3768    if (!cart || !Array.isArray(cart.items)) return false;
3769    var hasShippingProduct = cart.items.some(function (it) {
3770      return !!(it && it.type === 'PRODUCT' && it.payload && it.payload.requiresShipping);
3771    });
3772    return hasShippingProduct && !cart.shippingCalculated;
3773  }
3774
3775  /**
3776   * New Storefront (user-requested): whether the cart's current MEMBERSHIP item (at most one, per
3777   * the existing single-membership-instance cart constraint -- see `js/membership.html`'s
3778   * `storeCompleteItemForm` doc comment) requires a specific account type. `true` = the shopper
3779   * must submit as a company (this term's membership is an Organization/company GROUP
3780   * membership), `false` = must submit as an individual (a Household/individual GROUP
3781   * membership), `null` = no constraint -- either no membership in the cart at all, or its term
3782   * is a regular (non-group) membership.
3783   *
3784   * The constraint is computed once, when the membership item-form completes
3785   * (`js/membership.html`'s `selectTerm()`/`storeCompleteItemForm`, from the backend's real
3786   * `Membership.isOrganizationGroupMembership()`/`isIndividualGroupMembership()` on the selected
3787   * term), and persisted onto THAT membership cart item's own `payload.forcedAsCompany` --
3788   * deliberately not a separate cart-level field, so the constraint disappears automatically the
3789   * moment that item is deleted or replaced with a different term, with no separate cleanup path
3790   * to maintain.
3791   *
3792   * Consumed by `js/product.html`/`js/event_registration.html`'s own ITEM-mode init hooks (after
3793   * `hydrateAccount`) to force+hide THEIR OWN "Submit as a company" toggle the same way, so a
3794   * shopper who already locked in an account type via a household/company group membership sees
3795   * the same forced type (and no editable toggle) on every other item-form they subsequently
3796   * visit in the same cart -- mirroring the reverse direction (buy the item first, then a group
3797   * membership) that `js/membership.html`'s own `selectTerm()` already handles for a single form.
3798   * @param {Object} cart
3799   * @returns {(boolean|null)}
3800   */
3801  function resolveMembershipAccountTypeConstraint(cart) {
3802    if (!cart || !Array.isArray(cart.items)) return null;
3803    var membershipItem = cart.items.find(function (it) { return it && it.type === 'MEMBERSHIP'; });
3804    if (!membershipItem || !membershipItem.payload) return null;
3805    var forced = membershipItem.payload.forcedAsCompany;
3806    return (forced === true || forced === false) ? forced : null;
3807  }
3808
3809  /**
3810   * Remember custom-field answers, keyed BY CUSTOM FIELD ID (not per-form): a form's custom
3811   * questions are account-scoped data (`ProductFormConverter.applyAccountCustomFields` merges
3812   * them onto the buyer's Account/User by field id, "existing custom data ... preserved rather
3813   * than clobbered" -- mirroring `MembershipFormAssistant`/event equivalents), so the SAME
3814   * customFieldId answered on one form should hydrate onto ANY later form that asks the same
3815   * question, exactly like `hydrateAccount`'s leaf-path fields are recognized across forms by
3816   * identity rather than "only if the same form collected it." See `hydrateCustomFields` for the
3817   * read side (spec revision: pre-fill AND still show the question again on every purchase, the
3818   * same "回填,可修改" treatment as account/shipping -- there is no longer a Seam-2-style skip).
3819   * @param {Object} cart
3820   * @param {Array} customFields the `CustomFieldForm[]`-shaped answers (`{id, value}`), i.e. the
3821   *   completed item-form's own `getNeonFormJson().customFields`.
3822   */
3823  function upsertCustomFieldAnswers(cart, customFields) {
3824    if (!customFields || customFields.length === 0) return;
3825    if (!cart.customFieldAnswers) cart.customFieldAnswers = {};
3826    customFields.forEach(function (cf) {
3827      if (cf && cf.id != null) {
3828        cart.customFieldAnswers[String(cf.id)] = cf.value;
3829      }
3830    });
3831  }
3832
3833  /**
3834   * Fill a Vue `formData.customFields` (`CustomFieldForm[]`-shaped, `{id, value}`) from
3835   * previously-recorded answers, by matching each entry's `id` -- mirrors `hydrateAccount`'s
3836   * "only touch what the current form already has" contract: `formData.customFields` already
3837   * has one entry per custom-field THIS form renders (server-rendered ids, default/blank
3838   * values); entries whose id has no recorded answer are left untouched (unanswered before, or
3839   * a question this cart has never seen).
3840   * @param {Object} formData
3841   * @param {Object} cart
3842   */
3843  function hydrateCustomFields(formData, cart) {
3844    if (!cart || !cart.customFieldAnswers || !formData || !Array.isArray(formData.customFields)) return;
3845    formData.customFields.forEach(function (cf) {
3846      if (!cf || cf.id == null) return;
3847      var key = String(cf.id);
3848      if (Object.prototype.hasOwnProperty.call(cart.customFieldAnswers, key)) {
3849        var v = cart.customFieldAnswers[key];
3850        if (v === undefined || v === null) return;
3851        cf.value = v;
3852      }
3853    });
3854  }
3855
3856  global.NeonStoreCart = {
3857    loadCart: loadCart,
3858    saveCart: saveCart,
3859    clearCart: clearCart,
3860    emptyCart: emptyCart,
3861    upsertAccount: upsertAccount,
3862    hydrateAccount: hydrateAccount,
3863    hydrateShipping: hydrateShipping,
3864    addItem: addItem,
3865    removeItem: removeItem,
3866    hasAccount: hasAccount,
3867    hasShipping: hasShipping,
3868    mergeProductItem: mergeProductItem,
3869    reservedQuantityForProductLine: reservedQuantityForProductLine,
3870    needsShippingCalculation: needsShippingCalculation,
3871    resolveMembershipAccountTypeConstraint: resolveMembershipAccountTypeConstraint,
3872    upsertCustomFieldAnswers: upsertCustomFieldAnswers,
3873    hydrateCustomFields: hydrateCustomFields,
3874    getByPath: getByPath,
3875    setByPath: setByPath,
3876    leafPaths: leafPaths,
3877    totalQuantity: totalQuantity
3878  };
3879})(window);
3880/**
3881 * New Storefront orchestration API for the store-list page (design spec
3882 * `docs/superpowers/specs/2026-07-23-new-storefront-form-flow-design.md` §12
3883 * — store-list integration contract).
3884 *
3885 * Runtime notes:
3886 * - Plain classic `<script>` (no build step, no ES module), matching
3887 *   `store-cart.js` (Task 5.1) and the other files in this directory that are
3888 *   merged into `portal.all.js` in production and served individually under
3889 *   `/nx/portal-static/js/` in dev (see `function/common/js.html` and the
3890 *   `merge-js` antrun execution in `neon-gui/backend/pom.xml`).
3891 * - Depends on `window.NeonStoreCart` (Task 5.1, `store-cart.js`) being
3892 *   loaded first. This file must be included on the store-list page (which
3893 *   calls into `window.NeonStore` below) as well as on the item-form pages
3894 *   (`product-form`, `membership-form`, `event-registration-form`) and the
3895 *   shared Summary/Payment page, since Task 5.3 reuses `_enterItemForm`.
3896 *   See the report (`task-5.2-report.md`) for the exact recommended
3897 *   `<script>` include points and `pom.xml` `merge-js` registration (both
3898 *   left for a later task per this task's constraints).
3899 *
3900 * @module store-flow
3901 */
3902(function (global) {
3903  'use strict';
3904
3905  var Cart = global.NeonStoreCart;
3906
3907  /**
3908   * Module-level configuration, set by the store-list page via `configure()`.
3909   *
3910   * `contextPath` defaults to `/nx`, confirmed as the backend's
3911   * `server.servlet.context-path` (`neon-gui/backend/src/main/resources/
3912   * application-dev.yml:3`). `summaryFormId` has no default — `goToSummary`
3913   * throws until the store-list page supplies one (spec §12.5 open
3914   * coordination item; see report).
3915   *
3916   * @type {{summaryFormId: (number|string|null), contextPath: string}}
3917   */
3918  var config = {
3919    summaryFormId: null,
3920    contextPath: '/nx'
3921  };
3922
3923  /**
3924   * Merge store-list-supplied options into the module config.
3925   *
3926   * `summaryFormId` is ALSO persisted onto the cart (sessionStorage, via
3927   * `NeonStoreCart`) so it survives full-page navigation. `config` is a
3928   * plain module-level variable re-initialized to its defaults every time
3929   * this script is loaded on a fresh page (the store-list page, each
3930   * trimmed item-form page, and the shared Summary page all load their own
3931   * independent copy of this file) — so a `configure()` call made once on
3932   * the store-list page would otherwise be invisible by the time an item
3933   * form's `storeCompleteItemForm` calls `goToSummary()` on an entirely
3934   * different page load. Persisting it on the cart (which already
3935   * round-trips through every page in the flow) closes that gap without
3936   * requiring every item-form template to call `configure()` itself.
3937   *
3938   * @param {Object} [opts]
3939   * @param {(number|string)} [opts.summaryFormId] default form id for the
3940   *   shared Summary/Payment step (spec §12.5 — no backend default-lookup
3941   *   endpoint exists; the store-list page must supply this explicitly).
3942   * @param {string} [opts.contextPath] override for the app's context path,
3943   *   e.g. if it ever differs from `/nx` in a given environment.
3944   */
3945  function configure(opts) {
3946    if (!opts) return;
3947    Object.assign(config, opts);
3948    if (opts.summaryFormId != null) {
3949      var cart = Cart.loadCart();
3950      cart.summaryFormId = opts.summaryFormId;
3951      Cart.saveCart(cart);
3952    }
3953  }
3954
3955  function resolveFlowOrgId() {
3956    try {
3957      if (typeof appData !== 'undefined' && appData && appData.orgId != null) {
3958        return appData.orgId;
3959      }
3960    } catch (e) { /* TDZ */ }
3961    return 'default';
3962  }
3963
3964  function flowStorageKey() {
3965    return 'neon-store-flow:' + resolveFlowOrgId();
3966  }
3967
3968  /**
3969   * Persist return-copy that MUST survive NeonStoreCart.clearCart().
3970   * @param {Object} [ctx]
3971   * @param {string|null} [ctx.continueShoppingLabel]
3972   * @param {string|null} [ctx.returnUrl]
3973   * @param {string|null} [ctx.returnLabel]
3974   */
3975  function setFlowContext(ctx) {
3976    var next = {
3977      continueShoppingLabel: ctx && ctx.continueShoppingLabel != null ? ctx.continueShoppingLabel : null,
3978      returnUrl: ctx && ctx.returnUrl != null ? ctx.returnUrl : null,
3979      returnLabel: ctx && ctx.returnLabel != null ? ctx.returnLabel : null
3980    };
3981    try {
3982      global.sessionStorage.setItem(flowStorageKey(), JSON.stringify(next));
3983    } catch (e) { /* ignore quota / private mode */ }
3984  }
3985
3986  function getFlowContext() {
3987    try {
3988      var raw = global.sessionStorage.getItem(flowStorageKey());
3989      if (!raw) return {};
3990      var parsed = JSON.parse(raw);
3991      return parsed && typeof parsed === 'object' ? parsed : {};
3992    } catch (e) {
3993      return {};
3994    }
3995  }
3996
3997  /**
3998   * Resolve the shared Summary/Payment form id: prefers this page's own
3999   * in-memory `config.summaryFormId` (set by an explicit `configure()` call
4000   * on THIS page load), falling back to the value persisted on the cart by
4001   * an earlier `configure()` call on a different page (see `configure`'s
4002   * doc comment above for why the module-level config alone isn't enough).
4003   * @returns {(number|string|null)}
4004   */
4005  function resolveSummaryFormId() {
4006    if (config.summaryFormId != null) return config.summaryFormId;
4007    var cart = Cart.loadCart();
4008    return cart.summaryFormId != null ? cart.summaryFormId : null;
4009  }
4010
4011  /**
4012   * Store the "continue shopping" URL (store-list page) into the cart so the
4013   * Summary/Payment page's "Continue Shopping" link can send the shopper
4014   * back to it.
4015   * @param {string} url
4016   */
4017  function setContinueShoppingUrl(url) {
4018    var cart = Cart.loadCart();
4019    cart.continueShoppingUrl = url;
4020    Cart.saveCart(cart);
4021  }
4022
4023  /** @returns {Object} the current cart (see `NeonStoreCart.loadCart`). */
4024  function getCart() {
4025    return Cart.loadCart();
4026  }
4027
4028  /** Clear the current cart (see `NeonStoreCart.clearCart`). */
4029  function clearCart() {
4030    Cart.clearCart();
4031  }
4032
4033  /**
4034   * Build the item-form URL for a given store item type (spec §12.2).
4035   *
4036   * Uses the canonical short link, `host/forms/{formId}` (the SAME form
4037   * `FormListController#formUrlPrefix`/`ShareMetaDataProcessor` already build for admin
4038   * share-link display -- `HtmlUtils.getSchemeDomain(request) + "/forms/"`), per design
4039   * feedback that every form's browser-visible URL should be this short link regardless of
4040   * type, not the per-type path (`/portal/product-form/{formId}` etc.) or `config.contextPath`.
4041   * Deliberately built from `window.location.origin` rather than `config.contextPath` +
4042   * `/portal/forms/` (`CommonFormController#get`, `@GetMapping("/portal/forms/{formId}")`):
4043   * the short link is resolved a layer above that Spring mapping (reverse-proxy rewrite, already
4044   * configured, out of this codebase's scope) down to whatever backend path actually serves it,
4045   * so hardcoding `/portal` here would be redundant with (and could drift from) that rewrite.
4046   * `storeMode`/`termId` survive regardless of how many hops resolve the short link, since query
4047   * strings are preserved by both an HTTP redirect and a servlet forward.
4048   *
4049   * @param {('PRODUCT'|'MEMBERSHIP'|'EVENT_REGISTRATION')} type matches the
4050   *   backend `StoreItemType` enum (`store/model/StoreItemType.java`); only used to decide
4051   *   whether `termId` applies, not to select a path segment (the short link is type-agnostic).
4052   * @param {(number|string)} formId
4053   * @param {(number|string)} [termId] MEMBERSHIP only: the selected membership term id.
4054   *   The chosen term only ever lives in this browser's cart (`pendingItem.membershipTermId`,
4055   *   see `enterItemForm`), so it must be forwarded as a query param for the server to preload
4056   *   it (`StoreItemFormProcessor`, `store/processor/StoreItemFormProcessor.java`) — otherwise
4057   *   the sub-member page renders with no term/hasSubMembers info at all.
4058   * @returns {string}
4059   */
4060  function itemFormUrl(type, formId, termId) {
4061    var url = global.location.origin + '/forms/' + formId + '?storeMode=ITEM';
4062    if (type === 'MEMBERSHIP' && termId != null) {
4063      url += '&termId=' + encodeURIComponent(termId);
4064    }
4065    return url;
4066  }
4067
4068  /**
4069   * Resolve the item-form id for this add-to-cart action.
4070   *
4071   * Per spec §12.5 (open store-list/backend coordination item, still
4072   * unresolved as of this task): there is no backend "default form id for
4073   * this item type" lookup endpoint. The store-list page is the only place
4074   * that knows which form id to use (it is the page rendering the product/
4075   * membership/event catalog), so it MUST pass `meta.formId` explicitly. No
4076   * silent fallback or invented endpoint is used here — an absent
4077   * `meta.formId` is a caller bug and throws immediately.
4078   *
4079   * @param {Object} meta
4080   * @param {(number|string)} [meta.formId]
4081   * @returns {(number|string)}
4082   * @throws {Error} if `meta.formId` is missing.
4083   */
4084  function resolveFormId(meta) {
4085    if (meta && meta.formId != null) return meta.formId;
4086    throw new Error('NeonStore: meta.formId is required (default form id resolution — see spec §12.5)');
4087  }
4088
4089  /**
4090   * Save the selected-item context into `cart.pendingItem` so the trimmed
4091   * item form (product/membership/event, `storeMode=ITEM`) can preload it
4092   * once loaded (spec §12.4 — the product/membership home pages were
4093   * removed, so this is the only channel carrying the shopper's selection
4094   * into the form). Task 5.3 reads `cart.pendingItem` to populate hidden
4095   * fields on the item form.
4096   *
4097   * @param {('PRODUCT'|'MEMBERSHIP'|'EVENT_REGISTRATION')} type
4098   * @param {Object}
4098 meta caller-supplied selection context (shape varies by
4099   *   `type`; see field notes below).
4100   * @param {(number|string)} meta.formId required (see `resolveFormId`).
4101   * @param {number} [meta.productId] PRODUCT: matches `ProductLine.productId`
4102   *   (`product/model/ProductLine.java:21`).
4103   * @param {number} [meta.quantity] PRODUCT: matches `ProductLine.quantity`
4104   *   (`product/model/ProductLine.java:22`).
4105   * @param {string} [meta.choice1] PRODUCT: matches `ProductLine.choice1`
4106   *   (`product/model/ProductLine.java:23`).
4107   * @param {string} [meta.choice2] PRODUCT: matches `ProductLine.choice2`
4108   *   (`product/model/ProductLine.java:24`).
4109   * @param {string} [meta.choice3] PRODUCT: matches `ProductLine.choice3`
4110   *   (`product/model/ProductLine.java:25`).
4111   * @param {string} [meta.accessCode] PRODUCT: matches `ProductLine.accessCode`
4112   *   (`product/model/ProductLine.java:26`). Only required when the product's
4113   *   own `requiresAccessCode` (see `GET /nx/portal/product-detail/{formId}/
4114   *   {productId}`) is true; the backend validates it case-insensitively at
4115   *   checkout and rejects the whole cart if missing/wrong for such a product.
4116   * @param {boolean} [meta.requiresShipping] PRODUCT: REQUIRED, store-list-
4117   *   supplied ground truth for whether this specific product needs shipping
4118   *   (mirrors the product's own `shippingRequired`/`ProductItemDTO.
4119   *   shippingRequired`, `GET /nx/portal/product-detail/{formId}/{productId}`
4120   *   or the catalog listing). The trimmed item-form's own product catalog
4121   *   lookup (`GET /nx/portal/product-form/{formId}/products`, scoped to
4122   *   THIS form's own configured PRODUCT_SELECTION_MODE/PRODUCT_IDS) is not
4123   *   guaranteed to include an arbitrary store-list productId — this default
4124   *   store form exists only to collect Account/Shipping data, the store
4125   *   list's own catalog is authoritative for which products exist — so the
4126   *   item-form cannot reliably derive shipping need from its own catalog
4127   *   match and needs it supplied explicitly. Omitting it defaults to
4128   *   `false` (no Shipping page/fields shown or submitted for this item).
4129   * @param {(number|string)} [meta.membershipTermId] MEMBERSHIP: selected
4130   *   membership term (no verified backend "MembershipLine" model exists yet
4131   *   — Task 5.3/membership item-form task should confirm the real field
4132   *   name before consuming this).
4133   * @param {(number|string)} [meta.eventId] EVENT_REGISTRATION: selected
4134   *   event (same caveat as `membershipTermId` — unverified against a
4135   *   backend model in this task's scope).
4136   */
4137  function enterItemForm(type, meta) {
4138    var cart = Cart.loadCart();
4139    var formId = resolveFormId(meta);
4140    var pendingItem = { type: type, formId: formId };
4141    if (type === 'PRODUCT') {
4142      pendingItem.productId = meta.productId;
4143      pendingItem.quantity = meta.quantity;
4144      pendingItem.choice1 = meta.choice1;
4145      pendingItem.choice2 = meta.choice2;
4146      pendingItem.choice3 = meta.choice3;
4147      pendingItem.accessCode = meta.accessCode;
4148      pendingItem.requiresShipping = !!meta.requiresShipping;
4149    } else if (type === 'MEMBERSHIP') {
4150      pendingItem.membershipTermId = meta.membershipTermId;
4151      pendingItem.quantity = meta.quantity;
4152    } else {
4153      pendingItem.eventId = meta.eventId;
4154      pendingItem.quantity = meta.quantity;
4155    }
4156    cart.pendingItem = pendingItem;
4157    Cart.saveCart(cart);
4158    global.location.href = itemFormUrl(type, formId, pendingItem.membershipTermId);
4159  }
4160
4161  /**
4162   * Navigate to the shared Summary/Payment step (spec §12.2/§12.3).
4163   *
4164   * Resolves to `product-form` with `storeMode=SUMMARY` server-side (confirmed in Task 3.2: only
4165   * `ProductFormController.get()` wires the `"SUMMARY"` branch, `applyForSharedSummary` —
4166   * `MembershipFormController`/`EventRegistrationFormController` do not, so the product form
4167   * doubles as the shared default store form regardless of the item type(s) already in the
4168   * cart) -- but navigates via the canonical short link (`host/forms/{formId}`, see
4169   * `itemFormUrl`'s doc comment for why this is built from `window.location.origin` rather than
4170   * `config.contextPath`/`/portal`) rather than the per-type path directly, per design feedback
4171   * that every form's browser-visible URL should be the short link.
4172   *
4173   * @throws {Error} if `summaryFormId` has not been set via `configure()`
4174   *   on any page in this cart's flow so far (spec §12.5 open coordination
4175   *   item) — see `resolveSummaryFormId`.
4176   */
4177  function goToSummary() {
4178    var summaryFormId = resolveSummaryFormId();
4179    if (summaryFormId == null) {
4180      throw new Error('NeonStore: summaryFormId not configured (NeonStore.configure) — spec 
4180§12.5');
4181    }
4182    global.location.href = global.location.origin + '/forms/'
4183      + summaryFormId + '?storeMode=SUMMARY';
4184  }
4185
4186  /**
4187   * Add a product to the cart (spec §12.1; product checkout-flow redesign, confirmed with user
4188   * -- supersedes the "always routes through the item-form" revision this doc comment previously
4189   * described).
4190   *
4191   * Add to Cart no longer enters the item-form at all. It merges the selection directly into
4192   * `cart.items[]` (`NeonStoreCart.mergeProductItem` — the SAME "same productId+choices
4193   * +accessCode sums quantity" rule `storeCompleteItemForm`'s add branch already used) and goes
4194   * straight to the shared Summary page. Account/shipping (and, per user's explicit call, any
4195   * per-product custom fields — none configured on this store's Product forms today, and if that
4196   * ever changes the shopper simply won't be asked again once account info exists, by design) are
4197   * no longer collected at add-to-cart time; they are collected lazily instead, from the Summary
4198   * page's Check Out button (`storeCheckoutClick` in `form/js/product.html`, which calls
4199   * `editAccount`/`editShipping` below on demand — that's also where the "recalculate shipping
4200   * against the full cart" requirement this old fast-path removal was protecting is now enforced,
4201   * via `mergeProductItem`/`needsShippingCalculation` marking shipping stale on every new
4202   * shipping-requiring add instead of silently reusing a stale rate).
4203   *
4204   * @param {Object} meta see `enterItemForm` for field shape.
4205   */
4206  function addProduct(meta) {
4207    var cart = Cart.loadCart();
4208    Cart.mergeProductItem(cart, meta);
4209    Cart.saveCart(cart);
4210    return goToSummary();
4211  }
4212
4213  /**
4214   * Navigate to edit an existing cart item (Summary page "pencil" icon). Sets
4215   * `cart.editingItemIndex` so that, once the shopper eventually completes the item-form again,
4216   * `storeCompleteItemForm` replaces `cart.items[index]` in place instead of appending/merging a
4217   * new item — this survives regardless of which page the edit routes through below, since it is
4218   * only ever cleared when actually consumed (`storeCompleteItemForm`) or when the shopper
4219   * explicitly abandons the edit (`continueShopping`, product.html).
4220   *
4221   * MEMBERSHIP/EVENT_REGISTRATION: unchanged — routes straight to the item-form's own Account
4222   * page (`itemFormUrl`), which pre-fills from `cart.items[index]`'s payload itself (see
4223   * membership.html/event_registration.html's storeMode==='ITEM' init hooks); those forms have no
4224   * product-detail-style page where the shopper could change the item's own selection anyway
4225   * (MEMBERSHIP's level-selection page is removed by StorePageFilter in store mode; the term/
4226   * ticket choice was fixed at add-to-cart time by the store list).
4227   *
4228   * PRODUCT (design revision): routes to the Giftstore product-detail page
4229   * (`giftstore/product/product.html`, `GiftstoreProductPageController` `/portal/product?id=`)
4230   * instead, using the item's own `payload.productLines[0].productId` — that page already lets
4231   * the shopper change quantity/options/access code and, on "Add to Cart", calls
4232   * `window.NeonStore.addProduct(meta)` itself (same as a fresh add), which re-enters the item-form
4233   * Account/Shipping pages exactly like any other add. `cart.editingItemIndex` (set below) rides
4234   * along through that whole chain untouched (`enterItemForm` never touches it) so the eventual
4235   * `storeCompleteItemForm` still replaces rather than merges/appends. Falls back to `itemFormUrl`
4236   * if the item has no recorded productId (defensive — should not happen for a real PRODUCT item).
4237   *
4238   * @param {number} index index into `cart.items[]` of the item to edit.
4239   */
4240  function editItem(index) {
4241    var cart = Cart.loadCart();
4242    var item = cart.items && cart.items[index];
4243    if (!item) {
4244      return;
4245    }
4246    cart.editingItemIndex = index;
4247    Cart.saveCart(cart);
4248    if (item.type === 'PRODUCT') {
4249      var line = item.payload && Array.isArray(item.payload.productLines) ? item.payload.productLines[0] : null;
4250      var productId = line ? line.productId : null;
4251      if (productId != null) {
4252        global.location.href = '/giftstore/product?id=' + encodeURIComponent(productId);
4253        return;
4254      }
4255    }
4256    // Bug fix: MEMBERSHIP editing must forward the item's OWN termId the same way a fresh
4257    // enterItemForm() does -- itemFormUrl only appends `&termId=` when given one, and
4258    // MembershipFormController#shouldAppendStoreDiscountCodePage reads termId straight off the
4259    // request query param (there is no cart.pendingItem on an edit, see that method's own doc
4260    // comment). Without this, re-editing a membership item always requested the page with no
4261    // termId at all, so the backend could never determine the term has a coupon-code discount
4262    // configured -- the Discount Code page silently never got appended, and continuing without
4263    // re-typing the code then overwrote the item's existing (now-blank) discountCode on save.
4264    var termId = item.type === 'MEMBERSHIP' && item.payload ? item.payload.termId : null;
4265    global.location.href = itemFormUrl(item.type, item.formId, termId);
4266  }
4267
4268  /**
4269   * Pick which PRODUCT-type `cart.items[]` entry should serve as the "vehicle" for a full-page
4270   * round-trip into the product item-form to collect/re-collect cart-wide account or shipping
4271   * data (used by both `editShipping` and `editAccount` below — account/shipping are cart-level
4272   * fields, not item-level, so which PRODUCT item rides along is otherwise arbitrary).
4273   *
4274   * Prefers the first PRODUCT-type item whose own payload is flagged `requiresShipping` (mirrors
4275   * `StoreCartMerger#merge`'s server-side "first PRODUCT item with non-null shipping wins" rule,
4276   * `store/service/StoreCartMerger.java` — keeps the Shipping-page entry point consistent with
4277   * whichever item actually drives the shown Delivery Cost), falling back to the first PRODUCT
4278   * item at all if none are flagged.
4279   * @param {Array} items `cart.items`
4280   * @returns {number} index, or -1 if no PRODUCT item exists.
4281   */
4282  function pickProductItemIndex(items) {
4283    items = items || [];
4284    for (var i = 0; i < items.length; i++) {
4285      var it = items[i];
4286      if (it && it.type === 'PRODUCT' && it.payload && it.payload.requiresShipping) {
4287        return i;
4288      }
4289    }
4290    for (var j = 0; j < items.length; j++) {
4291      if (items[j] && items[j].type === 'PRODUCT') {
4292        return j;
4293      }
4294    }
4295    return -1;
4296  }
4297
4298  /**
4299   * Set `cart.editingItemIndex` + rebuild `cart.pendingItem` from the picked item's own
4300   * `payload.productLines[0]` (mirroring what `enterItemForm` would have set), so
4301   * `storeApplyPendingItemSelection` (`form/js/product.html`) restores this item's EXISTING
4302   * product selection on load -- without this, the item-form would see no `pendingItem` and no
4303   * in-progress Products-page selection (removed by `StorePageFilter` in ITEM mode), and
4304   * `storeCompleteItemForm` would replace the cart item with an empty product line on the next
4305   * Continue.
4306   * @param {Object} cart
4307   * @param {number} index
4308   * @returns {Object|null}
4308 the picked item, or null if index is invalid.
4309   */
4310  function primeProductItemForRecollection(cart, index) {
4311    var item = cart.items && cart.items[index];
4312    if (!item) {
4313      return null;
4314    }
4315    var line = item.payload && Array.isArray(item.payload.productLines) ? item.payload.productLines[0] : null;
4316    cart.editingItemIndex = index;
4317    if (line) {
4318      cart.pendingItem = {
4319        type: 'PRODUCT',
4320        formId: item.formId,
4321        productId: line.productId,
4322        quantity: line.quantity,
4323        choice1: line.choice1,
4324        choice2: line.choice2,
4325        choice3: line.choice3,
4326        accessCode: line.accessCode,
4327        requiresShipping: !!(item.payload && item.payload.requiresShipping)
4328      };
4329    }
4330    return item;
4331  }
4332
4333  /**
4334   * Navigate straight to the product item-form's own Shipping page (Summary page "Delivery Cost"
4335   * pencil icon, and the Check Out button's shipping gate — `storeCheckoutClick`, `form/js/
4336   * product.html`), so the shopper can reselect the shipping method/rate without walking back
4337   * through the Account page or the Giftstore product-detail page `editItem` uses.
4338   *
4339   * The `startPage=SHIPPING` query param is read directly off `window.location.search` by
4340   * `product.html`'s own `storeMode==='ITEM'` init hook (no backend/controller change needed --
4341   * the query string already survives the `/forms/{id}` short-link redirect+forward chain
4342   * untouched, see `itemFormUrl`'s doc comment) to jump `currentPage` straight to the Shipping
4343   * page once the form has mounted and hydrated.
4344   */
4345  function editShipping() {
4346    var cart = Cart.loadCart();
4347    var index = pickProductItemIndex(cart.items);
4348    if (index === -1) {
4349      return;
4350    }
4351    var item = primeProductItemForRecollection(cart, index);
4352    Cart.saveCart(cart);
4353    global.location.href = itemFormUrl('PRODUCT', item.formId) + '&startPage=SHIPPING';
4354  }
4355
4356  /**
4357   * Navigate to the product item-form's own Account page (Check Out button's account gate —
4358   * `storeCheckoutClick`, `form/js/product.html`), for the case where the cart has no account info
4359   * yet at all (e.g. the shopper's first-ever Add to Cart, now that Add to Cart itself skips the
4360   * item-form entirely — see `addProduct`'s doc comment). Plain entry (no `startPage` query
4361   * param): lands on page 1 (Account) like any normal item-form visit, and the item-form's own
4362   * nextPage() naturally carries on to its Shipping page afterwards if THIS item also needs
4363   * shipping, exactly like any other item-form visit -- see primeProductItemForRecollection's
4364   * doc comment for why the picked item's existing selection is preserved rather than lost.
4365   */
4366  function editAccount() {
4367    var cart = Cart.loadCart();
4368    var index = pickProductItemIndex(cart.items);
4369    if (index === -1) {
4370      return;
4371    }
4372    var item = primeProductItemForRecollection(cart, index);
4373    Cart.saveCart(cart);
4374    global.location.href = itemFormUrl('PRODUCT', item.formId);
4375  }
4376
4377  /**
4378   * Add a membership to the cart — always routes through the trimmed
4379   * membership item-form (no Seam 2 fast path defined for memberships in
4380   * spec §12.3).
4381   * @param {Object} meta see `enterItemForm` for field shape.
4382   */
4383  function addMembership(meta) {
4384    return enterItemForm('MEMBERSHIP', meta);
4385  }
4386
4387  /**
4388   * Cart index of an existing EVENT_REGISTRATION for the same event, or -1.
4389   * Same event = same eventId (payload.eventId / item.eventId). Items added
4390   * before eventId was persisted fall back to matching formId only when they
4391   * have no eventId of their own.
4392   * @param {Object} cart
4393   * @param {Object} meta
4394   * @returns {number}
4395   */
4396  function findExistingEventItemIndex(cart, meta) {
4397    var items = cart && cart.items ? cart.items : [];
4398    var eventId = meta && meta.eventId != null ? String(meta.eventId) : null;
4399    var formId = meta && meta.formId != null ? String(meta.formId) : null;
4400    var i;
4401    var it;
4402    var existingEventId;
4403    if (eventId) {
4404      for (i = 0; i < items.length; i++) {
4405        it = items[i];
4406        if (!it || it.type !== 'EVENT_REGISTRATION') continue;
4407        existingEventId = it.payload && it.payload.eventId != null ? it.payload.eventId : it.eventId;
4408        if (existingEventId != null && String(existingEventId) === eventId) {
4409          return i;
4410        }
4411      }
4412    }
4413    if (formId) {
4414      for (i = 0; i < items.length; i++) {
4415        it = items[i];
4416        if (!it || it.type !== 'EVENT_REGISTRATION') continue;
4417        existingEventId = it.payload && it.payload.eventId != null ? it.payload.eventId : it.eventId;
4418        if (existingEventId != null) continue;
4419        if (it.formId != null && String(it.formId) === formId) {
4420          return i;
4421        }
4422      }
4423    }
4424    return -1;
4425  }
4426
4427  /**
4428   * Add an event registration to the cart — always routes through the
4429   * trimmed event-registration item-form (no Seam 2 fast path defined for
4430   * event registrations in spec §12.3).
4431   *
4432   * One-event-once: if this event is already in the cart, set
4433   * `editingItemIndex` so the item-form hydrates the previous payload
4434   * (same path as the Summary pencil) and `storeCompleteItemForm` replaces
4435   * that row instead of appending. Always assign editingItemIndex (index
4436   * or null) so a stale edit from an abandoned re-register cannot replace
4437   * the wrong row.
4438   * @param {Object} meta see `enterItemForm` for field shape.
4439   */
4440  function addEvent(meta) {
4441    var cart = Cart.loadCart();
4442    var index = findExistingEventItemIndex(cart, meta);
4443    cart.editingItemIndex = index >= 0 ? index : null;
4444    Cart.saveCart(cart);
4445    return enterItemForm('EVENT_REGISTRATION', meta);
4446  }
4447
4448  global.NeonStore = {
4449    configure: configure,
4450    setFlowContext: setFlowContext,
4451    getFlowContext: getFlowContext,
4452    setContinueShoppingUrl: setContinueShoppingUrl,
4453    getCart: getCart,
4454    clearCart: clearCart,
4455    addProduct: addProduct,
4456    addMembership: addMembership,
4457    addEvent: addEvent,
4458    editItem: editItem,
4459    editShipping: editShipping,
4460    editAccount: editAccount,
4461    goToSummary: goToSummary,
4462    // Exposed for Task 5.3 (item-form completion) reuse — not part of the
4463    // store-list-facing §12.1 surface, but kept on the same public object
4464    // since this is a single global namespace, not a module with private
4465    // scope shared across files.
4466    _enterItemForm: enterItemForm
4467  };
4468})(window);

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.