1/** 2 * Feedback System Frontend JavaScript 3 */ 4 5(function($) { 6 'use strict'; 7 8 // ÐлаÑÑ Ð´Ð»Ñ ÑабоÑÑ Ñ ÑоÑмами обÑаÑной ÑвÑзи 9 class FeedbackForm { 10 constructor(formElement) { 11 this.form = $(formElement); 12 this.submitBtn = this.form.find('.feedback-form__submit'); 13 this.messageDiv = this.form.siblings('.feedback-form__message'); 14 this.type = this.form.data('type'); 15 16 this.init(); 17 } 18 19 init() { 20 this.bindEvents(); 21 this.setupValidation(); 22 } 23 24 bindEvents() { 25 this.form.on('submit', (e) => this.handleSubmit(e)); 26 27 // ÐалидаÑÐ¸Ñ Ð² ÑеалÑном вÑемени 28 this.form.find('input, textarea').on('blur', (e) => this.validateField($(e.target))); 29 this.form.find('input, textarea').on('input', (e) => this.clearFieldError($(e.target))); 30 } 31 32 setupValidation() { 33 // ÐаÑÑÑойка пÑавил валидаÑии 34 this.validationRules = { 35 name: { 36 required: true, 37 minLength: 2 38 }, 39 email: { 40 required: true, 41 email: true 42 }, 43 message: { 44 required: true, 45 minLength: 10, 46 maxLength: 5000 47 } 48 }; 49 } 50 51 handleSubmit(e) { 52 e.preventDefault(); 53 54 if (!this.validateForm()) { 55 this.showMessage(feedback_ajax.messages.required_fields, 'error'); 56 return; 57 } 58 59 this.sendForm(); 60 } 61 62 validateForm() { 63 let isValid = true; 64 const requiredFields = this.form.find('[required]'); 65 66 requiredFields.each((index, field) => { 67 const $field = $(field); 68 if (!this.validateField($field)) { 69 isValid = false; 70 } 71 }); 72 73 return isValid; 74 } 75 76 validateField($field) { 77 const fieldName = $field.attr('name'); 78 const value = $field.val().trim(); 79 const rules = this.validationRules[fieldName]; 80 81 if (!rules) return true; 82 83 let isValid = true; 84 let errorMessage = ''; 85 86 // ÐÑовеÑка обÑзаÑелÑноÑÑи 87 if (rules.required && !value) { 88 isValid = false; 89 errorMessage = this.getFieldLabel($field) + ' обÑзаÑелÑно Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ'; 90 } 91 // ÐÑовеÑка минималÑной Ð´Ð»Ð¸Ð½Ñ 92 else if (rules.minLength && value.length < rules.minLength) { 93 isValid = false; 94 errorMessage = this.getFieldLabel($field) + ` должно ÑодеÑжаÑÑ Ð½Ðµ менее ${rules.minLength} Ñимволов`; 95 } 96 // ÐÑовеÑка макÑималÑной Ð´Ð»Ð¸Ð½Ñ 97 else if (rules.maxLength && value.length > rules.maxLength) { 98 isValid = false; 99 errorMessage = this.getFieldLabel($field) + ` должно ÑодеÑжаÑÑ Ð½Ðµ более ${rules.maxLength} Ñимволов`; 100 } 101 // ÐÑовеÑка email 102 else if (rules.email && value && !this.isValidEmail(value)) { 103 isValid = false; 104 errorMessage = 'ÐожалÑйÑÑа, введиÑе коÑÑекÑнÑй email адÑеÑ'; 105 } 106 107 if (isValid) { 108 this.markFieldValid($field); 109 } else { 110 this.markFieldInvalid($field, errorMessage); 111 } 112 113 return isValid; 114 } 115 116 markFieldValid($field) { 117 const $group = $field.closest('.feedback-form__group'); 118 $group.removeClass('feedback-form__group--error'); 119 $group.addClass('feedback-form__group--success'); 120 $field.removeClass('feedback-form__input--error'); 121 this.removeFieldError($field); 122 } 123 124 markFieldInvalid($field, message) { 125 const $group = $field.closest('.feedback-form__group'); 126 $group.removeClass('feedback-form__group--success'); 127 $group.addClass('feedback-form__group--error'); 128 $field.addClass('feedback-form__input--error');
129 this.showFieldError($field, message); 130 } 131 132 clearFieldError($field) { 133 const $group = $field.closest('.feedback-form__group'); 134 $group.removeClass('feedback-form__group--error feedback-form__group--success'); 135 $field.removeClass('feedback-form__input--error'); 136 this.removeFieldError($field); 137 } 138 139 showFieldError($field, message) { 140 this.removeFieldError($field); 141 const errorHtml = `<div class="feedback-form__field-error" style="color: #dc3545; font-size: 12px; margin-top: 4px;">${message}</div>`; 142 $field.after(errorHtml); 143 } 144 145 removeFieldError($field) { 146 $field.siblings('.feedback-form__field-error').remove(); 147 } 148 149 getFieldLabel($field) { 150 const $label = $field.siblings('.feedback-form__label'); 151 return $label.length ? $label.text().replace(' *', '') : $field.attr('name'); 152 } 153 154 isValidEmail(email) { 155 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 156 return emailRegex.test(email); 157 } 158 159 sendForm() { 160 this.setLoading(true); 161 162 const formData = { 163 action: 'submit_feedback', 164 nonce: feedback_ajax.nonce, 165 type: this.type, 166 name: this.form.find('[name="name"]').val(), 167 email: this.form.find('[name="email"]').val(), 168 subject: this.form.find('[name="subject"]').val(), 169 message: this.form.find('[name="message"]').val() 170 }; 171 172 $.ajax({
173 url: feedback_ajax.ajax_url, 174 type: 'POST', 175 data: formData, 176 timeout: 30000 177 }) 178 .done((response) => this.handleSuccess(response)) 179 .fail((xhr, status, error) => this.handleError(xhr, status, error)) 180 .always(() => this.setLoading(false)); 181 } 182 183 handleSuccess(response) { 184 if (response.success) { 185 this.showMessage(response.data.message, 'success'); 186 this.form[0].reset(); 187 this.clearAllFieldErrors(); 188 189 // ÐÑпÑавиÑÑ Ð¿Ð¾Ð»ÑзоваÑелÑÑкое ÑобÑÑие 190 $(document).trigger('feedbackFormSuccess', { 191 type: this.type, 192 messageId: response.data.message_id 193 }); 194 } else { 195 this.showMessage(response.data.message || feedback_ajax.messages.error, 'error'); 196 } 197 } 198 199 handleError(xhr, status, error) { 200 let message = feedback_ajax.messages.error; 201 202 if (status === 'timeout') { 203 message = 'ÐÑÐµÐ¼Ñ Ð¾Ð¶Ð¸Ð´Ð°Ð½Ð¸Ñ Ð¸ÑÑекло. ÐопÑобÑйÑе еÑе Ñаз.'; 204 } else if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) { 205 message = xhr.responseJSON.data.message; 206 } 207 208 this.showMessage(message, 'error'); 209 210 // ÐÑпÑавиÑÑ Ð¿Ð¾Ð»ÑзоваÑелÑÑкое ÑобÑÑие об оÑибке 211 $(document).trigger('feedbackFormError', { 212 type: this.type, 213 error: error, 214 status: status 215 }); 216 } 217 218 clearAllFieldErrors() { 219 this.form.find('.feedback-form__group').removeClass('feedback-form__group--error feedback-form__group--success'); 220 this.form.find('.feedback-form__input').removeClass('feedback-form__input--error'); 221 this.form.find('.feedback-form__field-error').remove(); 222 } 223 224 setLoading(isLoading) { 225 if (isLoading) { 226 this.submitBtn 227 .prop('disabled', true) 228 .addClass('feedback-form__submit--disabled') 229 .text(feedback_ajax.messages.sending); 230 this.form.addClass('feedback-form__loading'); 231 } else { 232 this.submitBtn 233 .prop('disabled', false) 234 .removeClass('feedback-form__submit--disabled') 235 .text(this.getSubmitText()); 236 this.form.removeClass('feedback-form__loading'); 237 } 238 } 239 240 getSubmitText() { 241 return this.submitBtn.data('original-text') || 'ÐÑпÑавиÑÑ'; 242 } 243 244 showMessage(message, type = 'info') { 245 this.messageDiv 246 .removeClass('feedback-form__message--success feedback-form__message--error feedback-form__message--hidden') 247 .addClass(`feedback-form__message--${type}`) 248 .html(message) 249 .show(); 250 251 // ÐвÑомаÑиÑеÑки ÑкÑÑÑÑ ÑообÑение ÑеÑез 10 ÑекÑнд 252 setTimeout(() => { 253 this.messageDiv.fadeOut(); 254 }, 10000); 255 256 // ÐÑокÑÑÑиÑÑ Ðº ÑообÑÐµÐ½Ð¸Ñ 257 $('html, body').animate({ 258 scrollTop: this.messageDiv.offset().top - 100 259 }, 500); 260 } 261 } 262 263 // ÐлаÑÑ Ð´Ð»Ñ ÑабоÑÑ Ñ Ñабами 264 class FeedbackTabs { 265 constructor(container) { 266 this.container = $(container); 267 this.tabs = this.container.find('.feedback-tabs__nav-button'); 268 this.contents = this.container.find('.feedback-tabs__content'); 269 270 this.init(); 271 } 272 273 init() { 274 this.bindEvents(); 275 } 276 277 bindEvents() { 278 this.tabs.on('click', (e) => { 279 e.preventDefault(); 280 const $tab = $(e.currentTarget); 281 const targetTab = $tab.data('tab'); 282 this.switchTab(targetTab); 283 }); 284 285 // ÐоддеÑжка клавиаÑÑÑÑ 286 this.tabs.on('keydown', (e) => { 287 if (e.key === 'Enter' || e.key === ' ') { 288 e.preventDefault(); 289 $(e.currentTarget).click(); 290 } 291 }); 292 } 293 294 switchTab(targetTab) { 295 // ÐбновиÑÑ Ð°ÐºÑивнÑе кнопки 296 this.tabs.removeClass('feedback-tabs__nav-button--active'); 297 this.tabs.filter(`[data-tab="${targetTab}"]`).addClass('feedback-tabs__nav-button--active'); 298 299 // ÐбновиÑÑ Ð°ÐºÑивнÑй конÑÐµÐ½Ñ 300 this.contents.removeClass('feedback-tabs__content--active'); 301 this.container.find(`#tab-${targetTab}`).addClass('feedback-tabs__content--active'); 302 303 // ÐÑпÑавиÑÑ ÑобÑÑие ÑÐ¼ÐµÐ½Ñ Ñаба 304 $(document).trigger('feedbackTabChanged', { tab: targetTab });
305 } 306 } 307 308 // УÑилиÑÑ 309 const Utils = { 310 // ÐебаÑÐ½Ñ ÑÑнкÑÐ¸Ñ 311 debounce(func, wait) { 312 let timeout; 313 return function executedFunction(...args) { 314 const later = () => { 315 clearTimeout(timeout); 316 func(...args); 317 }; 318 clearTimeout(timeout); 319 timeout = setTimeout(later, wait); 320 }; 321 }, 322 323 // ТÑоÑÑлинг ÑÑнкÑÐ¸Ñ 324 throttle(func, limit) { 325 let inThrottle; 326 return function() { 327 const args = arguments; 328 const context = this; 329 if (!inThrottle) { 330 func.apply(context, args); 331 inThrottle = true; 332 setTimeout(() => inThrottle = false, limit); 333 } 334 }; 335 }, 336 337 // ÐÑовеÑка поддеÑжки localStorage 338 supportsLocalStorage() { 339 try { 340 return 'localStorage' in window && window['localStorage'] !== null; 341 } catch (e) { 342 return false; 343 } 344 }, 345 346 // Ð¡Ð¾Ñ Ñанение ÑеÑновика ÑоÑÐ¼Ñ 347 saveDraft(formId, data) { 348 if (this.supportsLocalStorage()) { 349 localStorage.setItem(`feedback_draft_${formId}`, JSON.stringify(data)); 350 } 351 }, 352 353 // ÐагÑÑзка ÑеÑновика ÑоÑÐ¼Ñ 354 loadDraft(formId) { 355 if (this.supportsLocalStorage()) { 356 const draft = localStorage.getItem(`feedback_draft_${formId}`); 357 return draft ? JSON.parse(draft) : null; 358 } 359 return null; 360 }, 361 362 // Удаление ÑеÑновика 363 removeDraft(formId) { 364 if (this.supportsLocalStorage()) { 365 localStorage.removeItem(`feedback_draft_${formId}`); 366 } 367 } 368 }; 369 370 // ÐниÑиализаÑÐ¸Ñ Ð¿Ñи загÑÑзке DOM 371 $(document).ready(function() { 372 // ÐниÑиализаÑÐ¸Ñ ÑоÑм обÑаÑной ÑвÑзи 373 $('.feedback-form__inner').each(function() { 374 const form = new FeedbackForm(this); 375 376 // Ð¡Ð¾Ñ ÑаниÑÑ Ð¾ÑигиналÑнÑй ÑекÑÑ ÐºÐ½Ð¾Ð¿ÐºÐ¸ 377 const submitBtn = $(this).find('.feedback-form__submit'); 378 submitBtn.data('original-text', submitBtn.text()); 379 380 // ÐагÑÑзиÑÑ ÑеÑновик еÑли еÑÑÑ 381 const formId = $(this).closest('.feedback-form').attr('id'); 382 if (formId) { 383 const draft = Utils.loadDraft(formId); 384 if (draft) { 385 $(this).find('[name="name"]').val(draft.name || ''); 386 $(this).find('[name="email"]').val(draft.email || ''); 387 $(this).find('[name="subject"]').val(draft.subject || ''); 388 $(this).find('[name="message"]').val(draft.message || ''); 389 } 390 391 // Ð¡Ð¾Ñ ÑанÑÑÑ ÑеÑновик пÑи изменении 392 const debouncedSave = Utils.debounce(() => { 393 const data = { 394 name: $(this).find('[name="name"]').val(), 395 email: $(this).find('[name="email"]').val(), 396 subject: $(this).find('[name="subject"]').val(), 397 message: $(this).find('[name="message"]').val() 398 }; 399 Utils.saveDraft(formId, data); 400 }, 1000); 401 402 $(this).find('input, textarea').on('input', debouncedSave); 403 } 404 }); 405 406 // ÐниÑиализаÑÐ¸Ñ Ñабов 407 $('.feedback-tabs').each(function() { 408 new FeedbackTabs(this); 409 }); 410 411 // ÐеÑеклÑÑение полного ÑекÑÑа ÑообÑений 412 $(document).on('click', '.feedback-table__message-toggle', function(e) { 413 e.preventDefault(); 414 const $toggle = $(this); 415 const $preview = $toggle.siblings('.feedback-table__message-preview'); 416 const $full = $toggle.siblings('.feedback-table__message-full'); 417 418 if ($full.is(':visible')) { 419 $full.hide(); 420 $preview.show(); 421 $toggle.text('ÐоказаÑÑ Ð¿Ð¾Ð»Ð½Ð¾ÑÑÑÑ'); 422 } else { 423 $preview.hide(); 424 $full.show(); 425 $toggle.text('СвеÑнÑÑÑ'); 426 } 427 }); 428 429 // ÐбÑабоÑка ÑÑпеÑной оÑпÑавки ÑоÑÐ¼Ñ 430 $(document).on('feedbackFormSuccess', function(e, data) { 431 // УдалиÑÑ ÑеÑновик поÑле ÑÑпеÑной оÑпÑавки 432 const formId = $(e.target).closest('.feedback-form').attr('id'); 433 if (formId) { 434 Utils.removeDraft(formId); 435 } 436 437 // Ðожно добавиÑÑ Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸ÑелÑнÑÑ Ð»Ð¾Ð³Ð¸ÐºÑ, напÑимеÑ, аналиÑÐ¸ÐºÑ 438 if (typeof gtag !== 'undefined') { 439 gtag('event', 'feedback_submitted', { 440 'event_category': 'feedback', 441 'event_label': data.type 442 }); 443 } 444 }); 445 446 // ÐÐ»Ð°Ð²Ð½Ð°Ñ Ð¿ÑокÑÑÑка к ÑкоÑÑм 447 $('a[href^="#feedback"]').on('click', function(e) { 448 e.preventDefault(); 449 const target = $($(this).attr('href')); 450 if (target.length) { 451 $('html, body').animate({ 452 scrollTop: target.offset().top - 100 453 }, 800); 454 } 455 }); 456 457 // ÐвÑомаÑиÑеÑкое изменение ÑазмеÑа textarea 458 $('textarea').each(function() {
459 this.setAttribute('style', 'height:' + (this.scrollHeight) + 'px;overflow-y:hidden;'); 460 }).on('input', function() { 461 this.style.height = 'auto'; 462 this.style.height = (this.scrollHeight) + 'px'; 463 }); 464 }); 465 466 // ÐкÑпоÑÑ Ð´Ð»Ñ Ð³Ð»Ð¾Ð±Ð°Ð»Ñного иÑполÑÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ 467 window.FeedbackSystem = { 468 FeedbackForm, 469 FeedbackTabs, 470 Utils 471 }; 472 473})(jQuery);
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.