1/** 2 * Calendar Field - Frontend Logic 3 * 4 * Ðогика вÑбоÑа даÑÑ Ð¸ вÑемени доÑÑавки на ÑÑÑаниÑе ÑекаÑÑа. 5 * ÐÑполÑзÑÐµÑ REST API Ð´Ð»Ñ Ð·Ð°Ð³ÑÑзки инÑеÑвалов вÑемени. 6 * 7 * @since 1.6.0 8 */ 9 10(function($) { 11 'use strict'; 12 13 /** 14 * ÐбÑÐµÐºÑ Calendar Field 15 */ 16 var CalendarField = { 17 /** 18 * ÐонÑигÑÑаÑÐ¸Ñ Ð¸Ð· PHP 19 */ 20 config: window.iikoCalendarConfig || {}, 21 22 /** 23 * Debounce ÑÐ°Ð¹Ð¼ÐµÑ 24 */ 25 debounceTimer: null, 26 27 /** 28 * Флаг пÑогÑаммного воÑÑÑÐ°Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ (не полÑзоваÑелÑÑкое изменение) 29 */ 30 isRestoring: false, 31 32 /** 33 * ÐниÑиализаÑÐ¸Ñ 34 */ 35 init: function() { 36 // ÐÑовеÑка конÑигÑÑаÑии 37 if (!this.config.restUrl) { 38 console.error('CalendarField: Config not found!'); 39 return; 40 } 41 42 // ÐÑивÑзка ÑобÑÑий 43 this.bindEvents(); 44 45 // ÐоÑÑÑановление даÑÑ Ð¸Ð· localStorage (ÑÑо ÑÑиггеÑÐ¸Ñ Ð·Ð°Ð³ÑÑÐ·ÐºÑ ÑлоÑов вÑемени) 46 this.restoreDateFromStorage(); 47 48 // ÐагÑÑзка инÑеÑвалов вÑемени Ð´Ð»Ñ ÑекÑÑей даÑÑ Ð¿Ñи пеÑвой загÑÑзке 49 // (вÑÐµÐ¼Ñ Ð±ÑÐ´ÐµÑ Ð²Ð¾ÑÑÑановлено поÑле загÑÑзки ÑлоÑов в renderTimeSlots) 50 this.loadInitialTimeSlots(); 51 52 // console.log('CalendarField: Initialized'); 53 }, 54 55 /** 56 * ÐÑивÑзка обÑабоÑÑиков ÑобÑÑий 57 */ 58 bindEvents: function() { 59 var self = this; 60 61 // Ðзменение даÑÑ Ð´Ð¾ÑÑавки (поддеÑжка Ð¾Ð±Ð¾Ð¸Ñ Ð²Ð°ÑианÑов: #date и #billing_date) 62 $(document).on('change', '#date, #billing_date', function() { 63 var date = $(this).val(); 64 65 // ÐалидаÑиÑ: пÑовеÑÑем, ÑÑо даÑа не пÑÑÑÐ°Ñ Ð¸ ÑооÑвеÑÑÑвÑÐµÑ ÑоÑмаÑÑ dmY 66 if (!date || date.length !== 8 || isNaN(date)) { 67 console.error('CalendarField: Invalid date format:', date); 68 return; 69 } 70 71 self.saveToStorage('Date', date); 72 73 // ÐÑли ÑÑо пÑогÑаммное воÑÑÑановление - загÑÑжаем ÑлоÑÑ ÐÐÐ ÑбÑоÑа вÑемени 74 // ÐÑли полÑзоваÑелÑÑкое изменение - загÑÑжаем ÑлоÑÑ Ð¡Ð ÑбÑоÑом вÑемени 75 self.debouncedLoadTimeSlots(date, !self.isRestoring); 76 }); 77 78 // Ðзменение вÑемени доÑÑавки (поддеÑжка Ð¾Ð±Ð¾Ð¸Ñ Ð²Ð°ÑианÑов: #time и #billing_time) 79 $(document).on('change', '#time, #billing_time', function() { 80 var time = $(this).val(); 81 82 if (time) { 83 self.saveToStorage('Time', time); 84 } 85 }); 86 87 // WooCommerce AJAX обновление ÑекаÑÑа 88 $(document.body).on('updated_checkout', function() { 89 // ÐÑи обновлении ÑекаÑÑа воÑÑÑанавливаем ÑолÑко даÑÑ 90 // ÐÑÐµÐ¼Ñ Ð±ÑÐ´ÐµÑ Ð²Ð¾ÑÑÑановлено авÑомаÑиÑеÑки пÑи загÑÑзке ÑлоÑов 91 self.restoreDateFromStorage(); 92 }); 93 }, 94 95 /** 96 * ÐагÑÑзка инÑеÑвалов вÑемени Ð´Ð»Ñ Ð´Ð°ÑÑ (Ñ debounce) 97 * @param {string} date - ÐаÑа в ÑоÑмаÑе dmY 98 * @param {boolean} resetTime - СбÑоÑиÑÑ Ð²ÑÐµÐ¼Ñ (true) или попÑÑаÑÑÑÑ Ð²Ð¾ÑÑÑановиÑÑ (false) 99 */ 100 debouncedLoadTimeSlots: function(date, resetTime) { 101 var self = this; 102 103 // Ðо ÑмолÑÐ°Ð½Ð¸Ñ resetTime = true (полÑзоваÑелÑÑкое изменение) 104 if (typeof resetTime === 'undefined') { 105 resetTime = true; 106 } 107 108 // ÐÑиÑаем пÑедÑдÑÑий ÑÐ°Ð¹Ð¼ÐµÑ 109 if (this.debounceTimer) { 110 clearTimeout(this.debounceTimer); 111 } 112 113 // УÑÑанавливаем новÑй ÑÐ°Ð¹Ð¼ÐµÑ (300ms) 114 this.debounceTimer = setTimeout(function() { 115 self.loadTimeSlots(date, resetTime); 116 }, 300); 117 }, 118 119 /** 120 * ÐагÑÑзка инÑеÑвалов вÑемени Ð´Ð»Ñ Ð´Ð°ÑÑ 121 */ 122 loadTimeSlots: function(date, resetTime) { 123 var self = this; 124 var $timeField = $('#time, #billing_time'); 125 126 // ÐоказÑваем индикаÑÐ¾Ñ Ð·Ð°Ð³ÑÑзки 127 $timeField.prop('disabled', true); 128 $timeField.html('<option value="">' + this.config.i18n.loading + '</option>'); 129 130 // REST API запÑÐ¾Ñ (даÑа Ñже в ÑоÑмаÑе dmY, напÑÐ¸Ð¼ÐµÑ "07112025") 131 $.ajax({ 132 url: this.config.restUrl + '/calendar/time-slots', 133 method: 'GET', 134 data: { 135 date: date 136 }, 137 beforeSend: function(xhr) { 138 xhr.setRequestHeader('X-WP-Nonce', self.config.nonce); 139 }, 140 success: function(response) { 141 if (response.success && response.data) { 142 self.renderTimeSlots(response.data, resetTime); 143 } else { 144 self.showError(); 145 } 146 }, 147 error: function(xhr, status, error) { 148 console.error('CalendarField: Error loading time slots:', error, xhr.responseText); 149 self.showError(); 150 }, 151 complete: function() { 152 $timeField.prop('disabled', false); 153 } 154 }); 155 }, 156 157 /** 158 * РендеÑинг инÑеÑвалов вÑемени 159 */ 160 renderTimeSlots: function(timeSlots, resetTime) { 161 var $timeField = $('#time, #billing_time'); 162 var savedTime = this.getFromStorage('Time'); 163 164 // ÐÑиÑаем поле 165 $timeField.empty(); 166
167 // ÐÑовеÑÑем, ÑвлÑеÑÑÑ Ð»Ð¸ timeSlots обÑекÑом или маÑÑивом 168 var isObject = typeof timeSlots === 'object' && !Array.isArray(timeSlots); 169 var isEmpty = isObject ? Object.keys(timeSlots).length === 0 : timeSlots.length === 0; 170 171 // ÐÑли ÑлоÑов Ð½ÐµÑ â показÑваем ÑообÑение "Closed" и вÑÑ Ð¾Ð´Ð¸Ð¼ 172 if (isEmpty) { 173 $timeField.append('<option value="">' + this.config.i18n.closed + '</option>'); 174 // ТÑиггеÑим change, ÑÑÐ¾Ð±Ñ Ð¾ÑиÑÑиÑÑ ÑÐ¾Ñ Ñаненное знаÑение, еÑли оно бÑло 175 $timeField.trigger('change'); 176 } else { 177 var availableValues = []; 178 179 // ÐобавлÑем placeholder "ÐÑбеÑиÑе вÑемÑ" Ñ Ð¿ÑÑÑÑм знаÑением 180 $timeField.append('<option value="">' + this.config.i18n.selectTime + '</option>'); 181 182 if (isObject) { 183 // ÐбÑабаÑÑваем обÑÐµÐºÑ {value: label} 184 $.each(timeSlots, function(value, label) { 185 $timeField.append('<option value="' + value + '">' + label + '</option>'); 186 availableValues.push(value); 187 }); 188 } else { 189 // ÐбÑаÑÐ½Ð°Ñ ÑовмеÑÑимоÑÑÑ: маÑÑив ÑÑÑок 190 $.each(timeSlots, function(index, time) { 191 $timeField.append('<option value="' + time + '">' + time + '</option>'); 192 availableValues.push(time); 193 }); 194 } 195 196 // ÐÑбиÑаем вÑÐµÐ¼Ñ Ð´Ð»Ñ ÑÑÑановки 197 var timeToSelect = null; 198 199 if (resetTime !== true) { 200 // ÐÑи иниÑиализаÑии или воÑÑÑановлении пÑÑаемÑÑ Ð²Ð¾ÑÑÑановиÑÑ ÑÐ¾Ñ Ñаненное вÑÐµÐ¼Ñ 201 if (this.restoreTimeFromStorage()) { 202 // ÐÑÐµÐ¼Ñ ÑÑпеÑно воÑÑÑановлено из localStorage, не ÑÑиггеÑим change 203 return; 204 } 205 // ÐÑли ÑÐ¾Ñ Ñаненного вÑемени Ð½ÐµÑ Ð¸Ð»Ð¸ оно недоÑÑÑпно, оÑÑавлÑем пÑÑÑое знаÑение 206 } 207 208 // ÐÑи Ñмене даÑÑ (resetTime = true) или еÑли Ð½ÐµÑ ÑÐ¾Ñ Ñаненного вÑемени - ÑÑÑанавливаем пÑÑÑое знаÑение 209 $timeField.val('').trigger('change'); 210 } 211 }, 212 213 /** 214 * ÐоказаÑÑ Ð¾ÑÐ¸Ð±ÐºÑ Ð·Ð°Ð³ÑÑзки 215 */ 216 showError: function() { 217 var $timeField = $('#time, #billing_time'); 218 $timeField.empty(); 219 $timeField.append('<option value="">' + this.config.i18n.error + '</option>'); 220 }, 221 222 /** 223 * Ð¡Ð¾Ñ Ñанение в localStorage Ñ TTL 224 */ 225 saveToStorage: function(key, value) { 226 var storageKey = 'iikoCalendar' + key; 227 var ttl = this.config.ttlMinutes * 60 * 1000; // РмиллиÑекÑÐ½Ð´Ð°Ñ 228 var now = new Date().getTime(); 229 230 var data = { 231 value: value, 232 expires: now + ttl 233 }; 234 235 try { 236 localStorage.setItem(storageKey, JSON.stringify(data)); 237 // console.log('CalendarField: Saved to storage:', storageKey, value); 238 } catch (e) { 239 console.error('CalendarField: Error saving to storage:', e); 240 } 241 }, 242 243 /** 244 * ÐолÑÑение из localStorage Ñ Ð¿ÑовеÑкой TTL 245 */ 246 getFromStorage: function(key) { 247 var storageKey = 'iikoCalendar' + key; 248 var now = new Date().getTime(); 249 250 try { 251 var item = localStorage.getItem(storageKey); 252 253 if (!item) { 254 return null; 255 } 256 257 var data = JSON.parse(item); 258 259 // ÐÑовеÑка TTL 260 if (data.expires && data.expires < now) { 261 localStorage.removeItem(storageKey); 262 return null; 263 } 264 265 // console.log('CalendarField: Restored from storage:', storageKey, data.value); 266 return data.value; 267 } catch (e) { 268 console.error('CalendarField: Error reading from storage:', e); 269 return null; 270 } 271 }, 272 273 /** 274 * ÐоÑÑÑановление ÑолÑко даÑÑ Ð¸Ð· localStorage 275 * ÐÑÐµÐ¼Ñ Ð²Ð¾ÑÑÑанавливаеÑÑÑ Ð² renderTimeSlots поÑле загÑÑзки ÑлоÑов 276 */ 277 restoreDateFromStorage: function() { 278 var savedDate = this.getFromStorage('Date'); 279 280 // ÐоÑÑÑановление даÑÑ 281 if (savedDate) { 282 var $dateField = $('#date, #billing_date'); 283 if ($dateField.length && $dateField.val() !== savedDate) { 284 // УÑÑанавливаем Ñлаг пÑогÑаммного воÑÑÑÐ°Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ 285 this.isRestoring = true; 286 287 $dateField.val(savedDate).trigger('change'); 288 289 // СбÑаÑÑваем Ñлаг поÑле неболÑÑой задеÑжки 290 // (ÑÑÐ¾Ð±Ñ Ð¾Ð±ÑабоÑÑик change ÑÑпел вÑполниÑÑÑÑ) 291 var self = this; 292 setTimeout(function() { 293 self.isRestoring = false;
294 }, 50); 295 } 296 } 297 }, 298 299 /** 300 * ÐоÑÑÑановление вÑемени из localStorage 301 * ÐÑзÑваеÑÑÑ Ð¿Ð¾Ñле загÑÑзки ÑлоÑов вÑемени в renderTimeSlots 302 */ 303 restoreTimeFromStorage: function() { 304 var savedTime = this.getFromStorage('Time'); 305 306 if (savedTime) { 307 var $timeField = $('#time, #billing_time'); 308 // ÐÑовеÑÑем, ÑÑо опÑÐ¸Ñ Ñ Ñаким знаÑением ÑÑÑеÑÑвÑÐµÑ 309 if ($timeField.length && $timeField.find('option[value="' + savedTime + '"]').length) { 310 // УÑÑанавливаем знаÑение ÐÐÐ trigger('change'), ÑÑÐ¾Ð±Ñ Ð½Ðµ вÑзваÑÑ Ð¿Ð¾Ð²ÑоÑное ÑÐ¾Ñ Ñанение 311 // и не ÑоздаÑÑ Ð±ÐµÑконеÑнÑй Ñикл пÑи updated_checkout 312 $timeField.val(savedTime); 313 314 // Ðо ÑÑиггеÑим ÑобÑÑие Ð´Ð»Ñ WooCommerce, ÑÑÐ¾Ð±Ñ Ð·Ð½Ð°Ñение попало в ÑоÑÐ¼Ñ 315 $timeField.trigger('input'); 316 317 return true; // ÐÑÐµÐ¼Ñ ÑÑпеÑно воÑÑÑановлено 318 } 319 } 320 return false; // ÐÑÐµÐ¼Ñ Ð½Ðµ воÑÑÑановлено 321 }, 322 323 /** 324 * ÐагÑÑзка инÑеÑвалов вÑемени пÑи пеÑвой загÑÑзке ÑÑÑаниÑÑ 325 * ÐÑзÑваеÑÑÑ Ð¾Ð´Ð¸Ð½ Ñаз пÑи иниÑиализаÑии 326 */ 327 loadInitialTimeSlots: function() { 328 var $dateField = $('#date, #billing_date'); 329 330 // ÐÑовеÑÑем, еÑÑÑ Ð»Ð¸ поле даÑÑ Ð¸ Ð¸Ð¼ÐµÐµÑ Ð»Ð¸ оно знаÑение 331 if ($dateField.length && $dateField.val()) { 332 var date = $dateField.val(); 333 334 // ÐалидаÑиÑ: пÑовеÑÑем, ÑÑо даÑа ÑооÑвеÑÑÑвÑÐµÑ ÑоÑмаÑÑ dmY 335 if (date.length === 8 && !isNaN(date)) { 336 this.loadTimeSlots(date, false); // resetTime = false пÑи иниÑиализаÑии 337 } else { 338 console.warn('CalendarField: Invalid initial date format:', date); 339 } 340 } 341 } 342 }; 343 344 /** 345 * ÐниÑиализаÑÐ¸Ñ Ð¿Ñи загÑÑзке DOM 346 */ 347 $(document).ready(function() { 348 CalendarField.init(); 349 }); 350 351})(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.