1/** 2 * Trip Review for WP Travel Engine - Frontend JavaScript 3 * 4 * @package TripReviewForWPTravelEngine 5 */ 6 7(function ($) { 8 'use strict'; 9 10 // Initialize when document is ready. 11 $( document ).ready( 12 function () { 13 TRPRE_Frontend.init(); 14 } 15 ); 16 17 // Main frontend object. 18 window.TRPRE_Frontend = { 19 20 // Flag to prevent duplicate initialization. 21 initialized: false, 22 23 /** 24 * Initialize frontend functionality 25 * Free version: Simplified initialization 26 */ 27 init: function () { 28 // Prevent duplicate initialization. 29 if (this.initialized) { 30 return; 31 } 32 33 this.initStarRating(); 34 this.initFormSubmission(); 35 this.initLoadMore(); 36 37 this.initialized = true; 38 }, 39 40 /** 41 * Initialize star rating interaction 42 */ 43 initStarRating: function () { 44 var self = this; 45 46 // Unbind first to avoid duplicate handlers. 47 $( '.trpre-star-input input[type="radio"]' ).off( 'change.trpre-rating' ); 48 $( '.trpre-star-input .star-label' ).off( 'mouseenter.trpre-rating mouseleave.trpre-rating' ); 49 50 // Bind change event. 51 $( '.trpre-star-input input[type="radio"]' ).on( 52 'change.trpre-rating', 53 function () { 54 var $radio = $( this ); 55 var rating = $radio.val(); 56 var text = ''; 57 58 switch (rating) { 59 case '1': text = trpre_frontend.strings.poor || 'Poor'; break; 60 case '2': text = trpre_frontend.strings.fair || 'Fair'; break; 61 case '3': text = trpre_frontend.strings.good || 'Good'; break; 62 case '4': text = trpre_frontend.strings.very_good || 'Very Good'; break; 63 case '5': text = trpre_frontend.strings.excellent || 'Excellent'; break; 64 } 65 66 // Update the rating text for this specific rating field. 67 $radio.closest( '.trpre-form-field' ).find( '.trpre-rating-text' ).text( text ); 68 69 // Update selected state for all stars in this container. 70 self.updateSelectedState( $radio ); 71 } 72 ); 73 74 // Handle hover effects. 75 $( '.trpre-star-input .star-label' ).on( 76 'mouseenter.trpre-rating', 77 function () { 78 var $starContainer = $( this ).closest( '.trpre-star-input' ); 79 var $stars = $starContainer.find( '.star-label' ); 80 var currentIndex = $stars.index( this ); 81 82 // Add hover class to current and all previous stars. 83 $stars.each( 84 function (index) { 85 if (index <= currentIndex) { 86 $( this ).addClass( 'trpre-star-hover' ); 87 } else { 88 $( this ).removeClass( 'trpre-star-hover' ); 89 } 90 } 91 ); 92 } 93 ); 94 95 $( '.trpre-star-input' ).on( 96 'mouseleave.trpre-rating', 97 function () { 98 // Remove hover class from all stars in this container. 99 $( this ).find( '.star-label' ).removeClass( 'trpre-star-hover' ); 100 } 101 ); 102 103 // Initialize selected state for any pre-selected ratings. 104 $( '.trpre-star-input input[type="radio"]:checked' ).each( 105 function () { 106 self.updateSelectedState( $( this ) ); 107 } 108 ); 109 110 // Initialize SVG states for all star inputs. 111 self.initializeStarStates(); 112 }, 113 114 /** 115 * Update selected state for star ratings 116 */ 117 updateSelectedState: function ($radio) { 118 var $starContainer = $radio.closest( '.trpre-star-input' ); 119 var $stars = $starContainer.find( '.star-label' ); 120 var selectedIndex = parseInt( $radio.val() ) - 1; // Convert to 0-based index. 121 122 // Remove selected class from all stars first. 123 $stars.removeClass( 'trpre-star-selected' ); 124 125 // Add selected class to stars up to the selected rating. 126 $stars.each( 127 function (index) { 128 if (index <= selectedIndex) { 129 $( this ).addClass( 'trpre-star-selected' ); 130 } 131 } 132 ); 133 }, 134 135 /** 136 * Initialize SVG star states 137 */ 138 initializeStarStates: function () { 139 $( '.trpre-star-input .star-label' ).each( 140 function () { 141 var $label = $( this ); 142 // Remove any existing state classes. 143 $label.removeClass( 'trpre-star-hover trpre-star-selected' ); 144 // Ensure default state shows line SVG and hides fill SVG. 145 $label.find( '.star-line' ).show(); 146 $label.find( '.star-fill' ).hide(); 147 } 148 ); 149 }, 150 151 // Image upload methods removed in free version. 152 153 /** 154 * Initialize form submission 155 */ 156 initFormSubmission: function () { 157 var self = this; 158 159 // Use object property for global isSubmitting flag. 160 if ( ! this.formSubmitting) { 161 this.formSubmitting = false;
162 } 163 164 // Remove any existing handlers first to prevent duplicates. 165 $( document ).off( 'submit.trpre-form', '.trpre-review-form-content' ); 166 167 $( document ).on( 168 'submit.trpre-form', 169 '.trpre-review-form-content', 170 function (e) { 171 e.preventDefault(); 172 173 var form = $( this ); 174 175 if (self.formSubmitting) { 176 console.log( 'Form already submitting, preventing duplicate' ); 177 return false; 178 } 179 180 self.formSubmitting = true; 181 182 var formData = new FormData( this ); 183 var submitBtn = form.find( '.trpre-submit-btn' ); 184 var btnText = submitBtn.find( '.trpre-btn-text' ); 185 var btnLoading = submitBtn.find( '.trpre-btn-loading' ); 186 187 submitBtn.prop( 'disabled', true ); 188 btnText.hide(); 189 btnLoading.show(); 190 form.find( '.trpre-form-messages' ).empty(); 191 192 $.ajax( 193 { 194 url: trpre_frontend.ajax_url, 195 type: 'POST', 196 data: formData, 197 processData: false, 198 contentType: false, 199 success: function (response) { 200 if (response.success) { 201 // SECURITY FIX: Use .text() to prevent DOM XSS. 202 var successMsg = $( '<div class="trpre-message trpre-success"></div>' ).text( response.data.message ); 203 form.find( '.trpre-form-messages' ).empty().append( successMsg ); 204 form[0].reset(); 205 form.find( '.trpre-rating-text' ).text( 'Select a rating' ); 206 207 setTimeout( 208 function () { 209 location.reload(); 210 }, 211 2000 212 ); 213 } else { 214 // SECURITY FIX: Use .text() to prevent DOM XSS. 215 var errorMsg = $( '<div class="trpre-message trpre-error"></div>' ).text( response.data.message ); 216 form.find( '.trpre-form-messages' ).empty().append( errorMsg ); 217 } 218 }, 219 error: function () { 220 form.find( '.trpre-form-messages' ).html( '<div class="trpre-message trpre-error">An error occurred. Please try again.</div>' ); 221 }, 222 complete: function () { 223 submitBtn.prop( 'disabled', false ); 224 btnText.show(); 225 btnLoading.hide(); 226 self.formSubmitting = false; 227 } 228 } 229 ); 230 } 231 ); 232 }, 233 234 /** 235 * Initialize Load More functionality 236 * Free version: Simple chronological loading (date DESC) 237 */ 238 initLoadMore: function () { 239 $( document ).on( 240 'click', 241 '.trpre-load-more-btn', 242 function (e) { 243 e.preventDefault(); 244 245 var button = $( this ); 246 var tripId = button.data( 'trip-id' ); 247 var currentPage = parseInt( button.data( 'current-page' ) ); 248 var totalPages = parseInt( button.data( 'total-pages' ) ); 249 var reviewsPerPage = parseInt( button.data( 'reviews-per-page' ) ); 250 251 // Calculate next page. 252 var nextPage = currentPage + 1; 253 254 if (nextPage > totalPages) { 255 return; 256 } 257 258 // Show loading state. 259 button.prop( 'disabled', true ); 260 button.find( '.trpre-load-more-text' ).hide(); 261 button.find( '.trpre-load-more-spinner' ).show(); 262 263 // Make AJAX request. 264 $.ajax( 265 { 266 url: trpre_frontend.ajax_url, 267 type: 'POST', 268 data: { 269 action: 'trpre_load_more_reviews', 270 nonce: trpre_frontend.nonce, 271 trip_id: tripId, 272 page: nextPage, 273 reviews_per_page: reviewsPerPage 274 }, 275 success: function (response) { 276 if (response.success) { 277 var newReviews = $( response.data.html ); 278 279 // Add fade-in animation. 280 newReviews.css( 281 { 282 opacity: 0, 283 transform: 'translateY(20px)' 284 } 285 ); 286 287 // Append new reviews. 288 var reviewsList = button.closest( '.trpre-reviews-container' ).find( '.trpre-reviews-list' ); 289 reviewsList.append( newReviews ); 290 291 // Animate in. 292 newReviews.animate( 293 { 294 opacity: 1, 295 transform: 'translateY(0)' 296 }, 297 400 298 ); 299 300 // Smooth scroll to first new review. 301 if (newReviews.length > 0) { 302 var firstNewReview = newReviews.first(); 303 $( 'html, body' ).animate( 304 { 305 scrollTop: firstNewReview.offset().top - 100 306 }, 307 400 308 ); 309 } 310 311 // Update button state. 312 button.data( 'current-page', response.data.current_page ); 313 314 // Update button text. 315 button.find( '.trpre-load-more-text' ).text( 316 trpre_frontend.strings.load_more_count 317 .replace( '%1$d', response.data.showing ) 318 .replace( '%2$d', response.data.total_reviews ) 319 ); 320 321 // Hide button if no more reviews. 322 if ( ! response.data.has_more) { 323 button.fadeOut( 324 400, 325 function () { 326 // Show the info div with "All reviews loaded" message. 327 button.siblings( '.trpre-load-more-info' ) 328 .text( trpre_frontend.strings.all_reviews_loaded || 'All reviews loaded' ) 329 .fadeIn( 400 ); 330 } 331 ); 332 } 333 334 } else { 335 TRPRE_Frontend.showMessage( response.data.message, 'error' ); 336 } 337 }, 338 error: function () { 339 TRPRE_Frontend.showMessage( 340 trpre_frontend.strings.error || 'An error occurred. Please try again.', 341 'error' 342 ); 343 }, 344 complete: function () { 345 // Hide loading state. 346 button.prop( 'disabled', false ); 347 button.find( '.trpre-load-more-text' ).show(); 348 button.find( '.trpre-load-more-spinner' ).hide(); 349 } 350 } 351 ); 352 } 353 ); 354 } 355 }; 356 357})( 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.