PageSourceSearch

https://legal.wargaming.net/assets/js/zendesk-form.js

js wargaming.net collected 2026-09-24 08:28:27 UTC 27,977 bytes, 759 lines download raw bytes

1function dd(...args) {
2    console.group('🛑 Dump');
3    args.forEach((arg, i) => {
4        console.log(`Arg ${i + 1}:`, arg);
5    });
6    console.groupEnd();
7}
8
9function initializeFormBuilderForms() {
10    const forms = $('.zendesk-form-builder');
11    if (forms.length > 0) {
12        $(forms).each((i) => {
13            const selector = $(forms[i]);
14            const formId = selector.attr("id");
15            const formData = JSON.parse(selector.attr('data-form-json'));
16            const form = new ZendeskForm(formData, formId);
17            form.render();
18            form.initValidation();
19        });
20    }
21}
22
23class ZendeskForm {
24    formId = null;
25    formStepsId = null;
26    currentStep = 1;
27    uploadedFiles = [];
28    zendeskFormId = null;
29    ticketWrapperId = null;
30    allFieldsPartOfValidation = [];
31
32    constructor(formData, formSelector) {
33        if (!formSelector) {
34            alert("Invalid Form Provided");
35            return;
36        }
37        this.formData = formData;
38        this.formId = formSelector;
39        this.zendeskFormId = this.formData?.form?.form_id;
40        this.ticketWrapperId = `#ticket-wrapper-${this.zendeskFormId}`;
41        this.formStepsId = `#form-steps-${this.zendeskFormId}`;
42        this.formElement = document.getElementById(this.formId);
43        Dropzone.autoDiscover = false;
44    }
45
46    render() {
47        const steps = this.formData.steps;
48        const createDescriptionStep = this.formData?.entry?.content != null;
49        const createFilesStep = this.formData?.form?.configurations?.attachments === true;
50
51        if (createDescriptionStep) {
52            steps.unshift([{ content: this.formData.entry.content, type: 'text', validations: [] }]);
53        }
54
55        if (createFilesStep) {
56            steps.push([{ validations: [{ type: "required", message: "This field is required" }] }]);
57        }
58
59        this.createFormStepsList(this.getFormStepsLength());
60
61        steps.forEach((stepFields, index) => {
62            let stepElement;
63            if (createDescriptionStep && index === 0) {
64                stepElement = this.addDescriptionStep(stepFields[0]);
65            } else if (createFilesStep && index === this.getFormStepsLength() - 1) {
66                stepElement = this.createFileUploadingStep();
67            } else {
68                stepElement = this.createStepElement(index + 1, stepFields);
69            }
70            this.formElement.appendChild(stepElement);
71        });
72
73        this.initDropzone();
74        this.setupValidations();
75        this.createThanksStep();
76        this.createSubmitButtons();
77        this.moveTicketToNextStep();
78        this.moveTicketToPrevStep();
79        this.initializeFirstDropdown();
80    }
81
82    addDescriptionStep(content) {
83        const stepElement = document.createElement('div');
84        stepElement.className = `step step-1 description`;
85        stepElement.innerHTML = content.content;
86        return stepElement;
87    }
88
89    extractUniqueIds(data) {
90        const ids = new Set();
91        Object.values(data).forEach(group => {
92            group.forEach(item => {
93                if (item.id) {
94                    ids.add(item.id);
95                }
96            });
97        });
98
99        return Array.from(ids);
100    }
101
102    getFormWrapperElement() {
103        return $(`#${this.formId}`);
104    }
105
106    getFormElement() {
107        return document.getElementById(this.formId);
108    }
109
110    initializeFirstDropdown() {
111        const $form = this.getFormWrapperElement();
112        const $firstInput = $form.find(':input').not(':button, [type=hidden]').first();
113
114        if ($firstInput.is('select')) {
115            if ($firstInput.val() !== $firstInput.find('option:first').val()) {
116                $firstInput.val($firstInput.find('option:first').val());
117            }
118            $firstInput.trigger('change');
119        }
120    }
121
122    validateOnChange(parentId, parentValue) {
123        const conditions = this.formData.conditionalFields[parentId] || {};
124        const rules = conditions[parentValue] || [];
125
126        // Check if rules exist then proceed next
127        if (!rules.length) return;
128
129        const affectedFields = rules.reduce((acc, rule) => {
130            acc[rule.id] = {
131                required: rule.required || false
132            };
133            return acc;
134        }, {});
135
136        const affectedFieldIds = this.extractUniqueIds(conditions);
137        console.log(affectedFieldIds);
138
139        // Only select fields that are in affectedFieldIds
140        const formFields = $(`${this.ticketWrapperId}`).find(':input').not(':button, [type=hidden]')
141            .filter((index, field) => {
142                const fieldName = field.name;
143                return affectedFieldIds.includes(Number(fieldName)) && this.isFieldPartOfValidation(fieldName);
144            });
145
146        formFields.each((index, field) => {
147            const fieldName = field.name;
148            const $field = $(field);
149            const $fieldContainer = $field.closest('.field-wrapper');
150            const fieldRules = affectedFields[fieldName];
151
152            if (fieldRules) {
153                $fieldContainer.show();
154                $fieldContainer.find(':input').removeClass('hidden-field');
155                const validator = $field.closest('form').validate();
156                if (fieldRules.required) {
157                    $field.rules('add', {
158                        required: true,
159                        messages: {
160                            required: "This field is required"
161                        }
162                    });
163                    $fieldContainer.find('label').addClass('required-field');
164                } else {
165                    $field.rules('remove', 'required');
166                    $fieldContainer.find('label').removeClass('required-field');
167                    validator.resetForm();
168                }
169            } else {
170                $fieldContainer.hide();
171                $fieldContainer.find(':input').addClass('hidden-field');
172                $field.rules('remove', 'required');
173                $field.closest('form').validate().resetForm();
174            }
175        });
176    }
177
178    setupValidations() {
179        const map = this.transformToMap(this.formData.conditionalFields);
180        this.allFieldsPartOfValidation = this.getAllFieldIds(map);
181        const selectTags = $('.form-select-field');
182        // Active option change event
183        selectTags.change(($event) => {
184            const parentId = $event.target.name;
185            const parentValue = $event.target.value;
186            this.validateOnChange(parentId, parentValue);
187        });
188
189        selectTags.each((index, field) => {
190            const select = $(field);
191            const firstOption = select.find('option:first');
192            if (firstOption.length) {
193                const parentValue = firstOption.val();
194                select.val(parentValue).trigger('change').trigger('input');
195            }
196        });
197    }
198
199    transformToMap(conditionalFields) {
200        const resultMap = new Map();
201        for (const fieldId in conditionalFields) {
202            if (Object.prototype.hasOwnProperty.call(conditionalFields, fieldId)) {
203                const innerMap = new Map();
204                const fieldData = conditionalFields[fieldId];
205                for (const category in fieldData) {
206                    if (Object.prototype.hasOwnProperty.call(fieldData, category)) {
207                        innerMap.set(category, fieldData[category]);
208                    }
209                }
210                resultMap.set(fieldId, innerMap);
211            }
212        }
213        return resultMap;
214    }
215
216    isFieldPartOfValidation(fieldId) {
217        return this.allFieldsPartOfValidation.includes(Number(fieldId));
218    }
219
220    getAllFieldIds(fieldMap) {
221        const fieldIds = [];
222        for (const innerMap of fieldMap.values()) {
223            for (const fields of innerMap.values()) {
224                fields.forEach(field => {
225                    if (field.id) {
226                        fieldIds.push(field.id);
227                    }
228                });
229            }
230        }
231        return Array.from(new Set(fieldIds));
232    }
233
234    getFormStepsLength() {
235        return this.formData['steps'].length;
236    }
237
238    getFormStepListWrapper() {
239        return $(`${this.formStepsId}`);
240    }
241
242    getFormStepListItems() {
243        return $(`${this.formStepsId} li`);
244    }
245
246    createFormStepsList(stepCount) {
247        const steps = this.getFormStepListWrapper();
248        Array(stepCount).fill().forEach((_, index) => {
249            steps.append(`<li class="${index === 0 ? 'current' : ''}">${index + 1}</li>`);
250            if (index < stepCount - 1) {
251                steps.append(`<span class="dash-line"></span>`);
252            }
253        });
254    }
255
256    createSubmitButtons() {
257        const temp = document.createElement('div');
258        temp.className = 'd-flex gap-3 flex-wrap flex-lg-nowrap mt-4';
259        const buttons = `<button class="btn ticket-cta back-btn" type="button" id="move-back">
260                Back
261            </button>
262            <button class="btn ticket-cta next-btn" type="button" id="move-next">
263                Next
264            </button>`;
265        temp.innerHTML = buttons;
266        this.formElement.appendChild(temp);
267    }
268
269    createFileUploadingStep() {
270        const temp = document.createElement('div');
271        temp.className = `step step-${this.getFormStepsLength()} d-none`;
272
273        const dropzone =
274            `<div class="row">
275                <div class="col-12">
276                        <label class="form-label input-label">Upload file</label>
277                        <div class="dropzone upload-doc" id="${this.formId}-dropzone-file-uploader">
278                            <div class="dz-message text-center my-4" data-dz-message>
279                                <img src="/assets/images/ticket/upload-file.png" alt="Wargaming-file"
280                                     class="img-fluid m-auto mb-3">
281                                    <div>
282                                        Drag and drop file here or
283                                        <span class="text-decoration-underline"> Choose file</span>
284                                    </div>
285                            </div>
286                        </div>
287                        <div class="d-flex mt-1">
288                            <label class="form-label input-label">Supported formats: PNG, JPG, DOC, PDF, WOTREPLAY</label>
289                            <label class="form-label input-label ms-auto">Maximum size: 25MB</label>
290                        </div>
291                        <div class="inline-error"></div>
292                    </div>
293                </div>
294            </div>
295            <div class="row">
296                <div class="col-12">
297                    <div id="${this.formId}-uploaded-files"></div>
298                </div>
299            </div>`;
300
301        temp.innerHTML = dropzone;
302        return temp;
303    }
304
305    createThanksStep() {
306        const temp = document.createElement('div');
307        temp.className = `step step-${this.getFormStepsLength() + 1} text-center d-none`;
308        const div = `<img src="/assets/images/ticket/thanku.png" class="img-fluid m-auto mb-4" alt="Wargaming-Legal">
309            <h1 class="thanks-title">REQUEST CREATED SUCCESSFULLY!</h1>`;
310
311        temp.innerHTML = div;
312        this.formElement.appendChild(temp);
313    }
314
315    isFormValid() {
316        return this.getFormWrapperElement().valid();
317    }
318
319    initValidation() {
320        // Initialize jQuery Validation
321        this.getFormWrapperElement().validate({
322            errorClass: "inline-error", errorElement: "div", highlight: function (element) {
323                $(element).addClass("isInvalid");
324            }, unhighlight: function (element) {
325                $(element).removeClass("isInvalid");
326            }, errorPlacement: function (error, element) {
327                error.addClass("error-message");
328                error.insertAfter(element);
329            },
330        });
331
332        // Add custom validation rules for each field
333        this.formData.steps.forEach(step => {
334            step.forEach(field => {
335                if (field.required) {
336                    $(`#${field.name}`).rules('add', {
337                        required: true, messages: {
338                            required: this.getFieldRequiredMessage(field)
339                        }
340                    });
341                }
342
343                // Add regex validation if specified
344                const regexValidation = this.getFieldRegexValidations(field);
345                if (regexValidation) {
346                    $(`#${field.name}`).rules('add', {
347                        pattern: regexValidation.pattern, messages: {
348                            pattern: regexValidation.message || 'Invalid format'
349                        }
350                    });
351                }
352            });
353        });
354    }
355
356    getFieldRequiredMessage(field) {
357        let validations = field?.validations;
358        if (!Array.isArray(field?.validations)) {
359            validations = Object.values(field?.validations);
360        }
361        return validations?.find(v => v.type === 'required')?.message || 'This field is required';
362    }
363
364    getFieldRegexValidations(field) {
365        let validations = field?.validations;
366        if (!Array.isArray(field?.validations)) {
367            validations = Object.values(field?.validations);
368        }
369        return validations?.find(v => v.type === 'required');
370    }
371
372    addSuccessEventRedirections() {
373        $(`${this.ticketWrapperId} .header .form-step-title`).addClass("d-none");
374        this.getFormStepListWrapper().addClass("d-none");
375        $(`${this.ticketWrapperId} .header .ticket-title`).text("Thank You").addClass("mb-0");
376        this.getBackBtn().text("CREATE NEW");
377        this.getNextBtn().text("BACK TO MAIN PAGE");
378        this.getBackBtn().on('click', function (event) {
379            location.reload();
380        });
381        this.getNextBtn().on('click', function (event) {
382            location.assign('/');
383        });
384    }
385
386    createStepElement(stepNumber, fields) {
387        const stepElement = document.createElement('div');
388        stepElement.className = `step step-${stepNumber}`;
389        if (stepNumber > 1) {
390            stepElement.className = `${stepElement.className} d-none`;
391        }
392
393        const row = document.createElement('div');
394        row.className = 'row';
395        fields.forEach(field => {
396            const fieldElement = this.createFieldElement(field);
397            row.appendChild(fieldElement);
398            stepElement.appendChild(row);
399        });
400
401        return stepElement;
402    }
403
404    createFieldElement(field) {
405        let classes = "";
406        if (field.attributes != null) {
407            classes = Array.isArray(field.attributes?.class) ? field.attributes.class.join(' ') : field.attributes?.class;
408        }
409        const fieldElement = document.createElement('div');
410        fieldElement.className = `field-wrapper mb-3 col-12 col-lg-6 ${classes}`;
411        if (field.type !== 'checkbox') {
412            const label = document.createElement('label');
413            label.htmlFor = field.name;
414            label.textContent = field.label;
415            label.className = 'form-label input-label';
416            // Add required check
417            if (field.required) {
418                label.appendChild(this.createRequiredCheck());
419            }
420            fieldElement.appendChild(label);
421        }
422        const inputElement = this.createInputElement(field);
423        const description = document.createElement('div');
424
425        if (field.description) {
426            description.className = 'input-description';
427            description.textContent = field.description;
428            inputElement.appendChild(description);
429        }
430        fieldElement.appendChild(inputElement);
431        fieldElement.appendChild(description);
432        return fieldElement;
433    }
434
435    createInputElement(field) {
436        let inputElement;
437
438        switch (field.type) {
439            case 'select':
440                inputElement = document.createElement('select');
441                inputElement.id = field.name;
442                inputElement.name = field.id;
443                inputElement.className = 'form-control form-select-field';
444                inputElement.required = field.required;
445
446                if (field?.options) {
447                    field.options.forEach(option => {
448                        const optionElement = document.createElement('option');
449                        optionElement.value = option.value;
450                        optionElement.textContent = option.label;
451                        inputElement.appendChild(optionElement);
452                    });
453                }
454                break;
455
456            case 'checkbox':
457                const container = document.createElement('div');
458                const subContainer = document.createElement('div');
459                container.className = 'form-check-container flex-column align-items-start';
460                subContainer.className = 'd-flex gap-3';
461
462                const checkbox = document.createElement('input');
463                checkbox.type = 'checkbox';
464                checkbox.id = field.name;
465                checkbox.name = field.id;
466                checkbox.className = 'form-check-input';
467                checkbox.required = field.required;
468
469                const checkboxLabel = document.createElement('label');
470                checkboxLabel.className = 'form-check-label input-label';
471                checkboxLabel.htmlFor = field.name;
472                checkboxLabel.textContent = field.label;
473                if (field.required) {
474                    checkboxLabel.appendChild(this.createRequiredCheck());
475                }
476                subContainer.appendChild(checkbox);
477                subContainer.appendChild(checkboxLabel);
478                container.appendChild(subContainer);
479                inputElement = container;
480                break;
481
482            case 'richtext':
483                inputElement = document.createElement('textarea');
484                inputElement.id = field.name;
485                inputElement.name = field.id;
486                inputElement.required = field.required;
487                inputElement.className = 'ckeditor form-control form-input-field';
488                break;
489
490            case 'textarea':
491                inputElement = document.createElement('textarea');
492                inputElement.id = field.name;
493                inputElement.name = field.id;
494                inputElement.className = 'form-control form-input-field';
495                inputElement.required = field.required;
496                break;
497
498            default: // text, number, email, etc.
499                inputElement = document.createElement('input');
500                inputElement.type = field.type;
501                inputElement.id = field.name;
502                inputElement.className = 'form-control form-input-field';
503                inputElement.name = field.id;
504                inputElement.required = field.required;
505        }
506
507        return inputElement;
508    }
509
510    createRequiredCheck() {
511        const span = document.createElement('span');
512        span.className = 'required';
513        span.textContent = ' *';
514        return span;
515    }
516
517    getStepList() {
518        return $(`${this.ticketWrapperId} .step`);
519    }
520
521    hideCurrentStep() {
522        Array.from(this.getStepList()).forEach((step, index) => {
523            index++;
524            if (index === this.currentStep) {
525                $(step).removeClass('d-none').addClass('d-block');
526            } else {
527                $(step).addClass('d-none').removeClass('d-block');
528            }
529        });
530    }
531
532    getNextBtn() {
533        return $(`${this.ticketWrapperId} #move-next`);
534    }
535
536    getBackBtn() {
537        return $(`${this.ticketWrapperId} #move-back`);
538    }
539
540    moveTicketToNextStep() {
541        try {
542            const steps = this.getFormStepsLength();
543            this.getNextBtn().on('click', async (e) => {
544                this.initValidation();
545                if (!this.isFormValid()) {
546                    return;
547                }
548                if ((steps + 1) === this.currentStep) {
549                    return;
550                }
551                if (steps === this.currentStep) {
552                    await this.saveTicket();
553                    return;
554                }
555                this.currentStep++;
556                this.scrollToRequestForm();
557                this.activeCurrentProgressStep();
558                this.hideCurrentStep();
559                setScalesHeight();
560            })
561        } catch (e) {
562            console.log(e)
563        }
564    }
565
566    moveTicketToPrevStep() {
567        this.getBackBtn().on('click', (e) => {
568            if (this.currentStep === 1 || this.currentStep === (this.getFormStepsLength() + 1)) {
569                history.back();
570                return;
571            }
572            this.currentStep--;
573            this.scrollToRequestForm();
574            this.activeCurrentProgressStep();
575            this.hideCurrentStep();
576            setScalesHeight();
577        });
578    }
579
580    activeCurrentProgressStep() {
581        const steps = Array.from(this.getFormStepListItems());
582        steps.forEach((step, index) => {
583            index++;
584            const currentElement = $(step);
585            if (index === this.currentStep) {
586                currentElement.removeClass('completed').addClass('current');
587            }
588            if (index < this.currentStep) {
589                currentElement.removeClass('current').addClass('completed');
590            }
591            if (index > this.currentStep) {
592                currentElement.removeClass('current', 'completed');
593            }
594        });
595    }
596
597    scrollToRequestForm() {
598        this.getFormElement().scrollIntoView({ behavior: "smooth" });
599    }
600
601    saveTicket() {
602        $('#errors').addClass('d-none');
603        const self_ = this;
604
605        const dataTransfer = new DataTransfer();
606        const fileInput = document.getElementById(`zendesk-form-files-${this.zendeskFormId}`);
607        const form = this.getFormElement();
608        self_.uploadedFiles.forEach((file) => {
609            dataTransfer.items.add(file);
610        });
611        fileInput.files = dataTransfer.files;
612
613        let formData = new FormData(form);
614        formData.append('form_id', this.zendeskFormId);
615
616        const formElements = form.querySelectorAll('.hidden-field');
617        formElements.forEach(element => {
618            formData.delete(element.name);
619        });
620        const submitBtn = this.getNextBtn();
621        submitBtn.prop('disabled', true);
622        return new Promise((resolve, reject) => {
623            $.ajax({
624                url: `/ticket`,
625                method: 'POST',
626                headers: {
627                    'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'),
628                },
629                data: formData,
630                processData: false,
631                contentType: false,
632                async: true,
633                success: function (response) {
634                    self_.currentStep++;
635                    resolve(response);
636                    submitBtn.prop('disabled', false);
637                    self_.addSuccessEventRedirections();
638                    self_.hideCurrentStep();
639                    setScalesHeight();
640                },
641                error: function (error) {        
642                    submitBtn.prop('disabled', false);
643                    const errors = error.responseJSON?.message ?? Object.values(error.responseJSON);
644                
645                    let validationError = "";
646                    if (Array.isArray(errors) && errors.length) {
647                        errors.forEach(err => {                            
648                            if (Array.isArray(err) && err.length) {
649                                err.forEach(e => validationError += `<p class="mb-0">${e}</p>`);
650                            }
651                        });
652                    } else {
653                        validationError = `<p class="mb-0">${errors}</p>`;
654                    }
655                    $('#errors').removeClass('d-none').html(validationError);
656                    setScalesHeight();
657                    reject(validationError);
658                }
659            });
660        });
661    }
662
663    initDropzone() {
664        const fileUploadWrapper = document.getElementById(`${this.formId}-dropzone-file-uploader`);
665        const fileList = document.getElementById(`${this.formId}-uploaded-files`);
666
667        if (fileUploadWrapper == null) {
668            return;
669        }
670        const self_ = this;
671
672        new Dropzone(fileUploadWrapper, {
673            url: "#",
674            autoProcessQueue: false,
675            previewsContainer: false,
676            uploadMultiple: true,
677            acceptedFiles: '.png, .jpg, .pdf, .doc, .docx, .wotreplay',
678            maxFilesize: 25,
679            init: function () {
680                this.on("addedfile", function (file) {
681                    self_.uploadedFiles.push(file);
682
683                    const fileItem = document.createElement("div");
684                    fileItem.className = "file-item my-3";
685
686                    const fileInfo = document.createElement("div");
687                    fileInfo.className = "file-info mb-2";
688
689                    const fileName = document.createElement("span");
690                    fileName.className = "file-name";
691                    fileName.textContent = file.name;
692
693                    const removeButton = document.createElement("img");
694                    removeButton.className = "remove-btn cursor-pointer";
695                    removeButton.src = "/assets/images/ticket/cross.png";
696                    removeButton.addEventListener("click", () => {
697                        this.removeFile(file);
698                        fileList.removeChild(fileItem);
699                    });
700
701                    const progressBarContainer = document.createElement("div");
702                    progressBarContainer.className = "progress-bar";
703
704                    const progressBar = document.createElement("div");
705                    progressBar.className = "progress";
706
707                    fileItem.appendChild(fileInfo);
708                    fileInfo.appendChild(fileName);
709                    fileInfo.appendChild(removeButton);
710                    progressBarContainer.appendChild(progressBar);
711                    fileItem.appendChild(progressBarContainer);
712                    fileList.appendChild(fileItem);
713                    self_.simulateProgress(progressBar, file.size);
714                    setScalesHeight();
715                });
716
717                this.on("uploadprogress", function (file, progress) {
718                    if (file.progressBar) {
719                        file.progressBar.style.width = `${progress}%`;
720                    }
721                });
722
723                this.on("removedfile", function (file) {
724                    const index = self_.uploadedFiles.indexOf(file);
725                    if (index > -1) {
726                        self_.uploadedFiles.splice(index, 1);
727                        setScalesHeight();
728                    }
729                });
730            },
731        });
732    }
733
734    simulateProgress(progressBar, fileSize) {
735        const fileSizeMB = fileSize / 1024 / 1024;
736        const totalDuration = Math.max(fileSizeMB * 1000, 2000);
737        let progress = 0;
738
739        const interval = setInterval(() => {
740            progress += (100 / (totalDuration / 100));
741            progressBar.style.width = `${Math.min(progress, 100)}%`;
742
743            if (progress >= 100) {
744                clearInterval(interval);
745            }
746        }, 100);
747    }
748}
749
750// Add regex validation method
751$.validator.addMethod("pattern", function (value, element, param) {
752    if (this.optional(element)) {
753        return true;
754    }
755    if (typeof param === 'string') {
756        param = new RegExp(param);
757    }
758    return param.test(value);
759}, "Invalid format");

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.