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.