1import { JobRecordsToolbar } from "job_records/toolbar"; 2import { JobRecordsFileInput } from "job_records/file_input"; 3import { BeforeUnloadService } from 'job_records/before_unload_service'; 4 5export class JobRecordProjects { 6 constructor() { 7 this.el = '.js-job-record-projects'; 8 this.$el = $(this.el); 9 10 this.$projectImage = $('#job_record_project_image'); 11 this.$removeImageLink = $('.image-container .js-clear-fieldset'); 12 13 this.bindEvents(); 14 this.initialize(); 15 } 16 17 bindEvents() { 18 // Event delegation for dynamically generated elements 19 this.$el.on('click', '.js-add-location', (event) => { 20 this.addLocation(event); 21 }); 22 23 this.$el.on('click', '.js-remove-location', (event) => { 24 this.removeLocation(event); 25 }); 26 27 this.$el.on('click', '.js-clear-fieldset', (event) => { 28 this.clearFieldset(event); 29 }); 30 31 this.$el.on('click', '.js-add-product', (event) => { 32 this.addProduct(event); 33 }); 34 35 // Direct event bindings 36 if (this.$projectImage.length) { 37 this.$projectImage.on('change', () => this.toggleRemoveImageLinkState()); 38 } 39 40 // Form input change events - select all form controls 41 const $formInputs = this.$el.find('form input, form select, form textarea, form button'); 42 $formInputs.each((index, input) => { 43 if ($(input).attr('id') != "form-submit") { 44 $(input).on('change', () => this.bindBeforeUnload()); 45 } 46 }); 47 } 48 49 initialize() { 50 new JobRecordsToolbar(); 51 new JobRecordsFileInput(); 52 53 this.toggleRemoveImageLinkState(); 54 this._resetLocationRemoveButton(); 55 this._resetLocationAddButton(); 56 } 57 58 bindBeforeUnload() { 59 // Use the shared service to bind the beforeunload event 60 BeforeUnloadService.bind(); 61 } 62 63 addLocation(event) { 64 const $this = $(event.target); 65 66 // Create location element from template 67 const $location = $(this._locationTemplate()).hide(); 68 69 if ($this.hasClass('js-add-location-top')) { 70 $this.parent().after($location); 71 } else { 72 $this.parent().before($location); 73 } 74 75 // Fade in effect 76 $location.fadeIn(1000); 77 78 this._resetPositions(); 79 this._resetSerialNumbers(); 80 this._resetLocationRemoveButton(); 81 this._resetLocationAddButton(); 82 } 83 84 removeLocation(event) { 85 event.preventDefault(); 86 87 const $this = $(event.target); 88 const $location = $this.closest('.js-location'); 89 90 const _locationName = () => { 91 const $nameInput = $location.find('.job_record_project_locations_name input'); 92 return $nameInput.length && $nameInput.val() ? $nameInput.val() : 'this location'; 93 }; 94 95 if (confirm(`Are you sure you want to delete ${_locationName()}?`)) { 96 $location.remove(); 97 this._resetSerialNumbers(); 98 } 99 100 this._resetLocationRemoveButton(); 101 this._resetLocationAddButton(); 102 } 103 104 clearFieldset(event) { 105 event.preventDefault(); 106 107 const $this = $(event.target); 108 const $removeImageLink = $this.next().find('.js-file-remove'); 109 const selector = $this.data('target'); 110 const $fieldset = $this.closest(selector); 111 112 $fieldset.find('input').val(''); 113 $fieldset.find('textarea').val(''); 114 $fieldset.find('select').each((index, select) => { 115 $(select).val(''); 116 // Assuming you're using something like tom-select or similar instead of selecter 117 if (window.Selecter && select.selecter) { 118 select.selecter('update'); 119 } 120 }); 121 122 this.toggleRemoveImageLinkState(); 123 if ($removeImageLink.length) { 124 $removeImageLink.click(); 125 } 126 } 127 128 toggleRemoveImageLinkState() { 129 if (this.$projectImage.length && this.$projectImage[0].files && this.$projectImage[0].files.length > 0) { 130 this.$removeImageLink.show(); 131 } else if (this.$removeImageLink.length) { 132 this.$removeImageLink.hide(); 133 } 134 } 135 136 addProduct(event) { 137 event.preventDefault(); 138 const $this = $(event.target); 139 140 const $hiddenInput = $('<input>', { 141 class: 'hidden', 142 type: 'hidden', 143 name: 'add_product' 144 }); 145 $this.after($hiddenInput); 146
147 const $locationContainer = $this.closest('.location-details'); 148 const $nameInput = $locationContainer.find('.job_record_project_locations_name input'); 149 const name = $nameInput.length ? $nameInput.val() : ''; 150 151 const $nameHiddenInput = $('<input>', { 152 class: 'hidden', 153 type: 'hidden', 154 name: 'product_name', 155 value: name 156 }); 157 158 const $buttonContainer = $this.closest('.add-product-button-container'); 159 $buttonContainer.append($nameHiddenInput); 160 161 BeforeUnloadService.unbind(); 162 163 const $saveButton = this.$el.find('#save'); 164 if ($saveButton.length) { 165 $saveButton.click(); 166 } 167 } 168 169 // Private methods 170 171 _resetLocationRemoveButton() { 172 const $locations = this.$el.find('.js-location'); 173 const $buttons = this.$el.find('.js-location .button-remove'); 174 175 $buttons.each((index, button) => { 176 $(button).toggle($locations.length > 1); 177 }); 178 } 179 180 _resetLocationAddButton() { 181 const $locations = this.$el.find('.js-location'); 182 const $addLocationBtn = this.$el.find('.add-location-button-container'); 183 184 if ($addLocationBtn.length) { 185 console.log('$addLocationBtn.length: ', $addLocationBtn.length); 186 console.log('$$locations.length: ', $locations.length); 187 $addLocationBtn.toggle($locations.length >= 1); 188 } 189 } 190 191 _locationTemplate() { 192 const n = this.$el.find('.js-location').length + 1; 193 194 return ` 195 <div class="js-location"> 196 <div class="location-details"> 197 <h4><span class="js-serial-number">${this._pad(n)}</span> Location Details</h4> 198 <a href="" class="js-clear-fieldset" data-target=".location-details">Clear</a> 199 <a href="#" class="button-remove js-remove-location"></a> 200 <div class="input hidden job_record_project_locations__position"><input class="hidden js-position" type="hidden" value="" name="job_record_project[locations_attributes][${n}][_position]" id="job_record_project_locations_attributes_${n}__position"></div> 201 <div class="grid two-columns"> 202 <div class="column"> 203 <div class="input string required job_record_project_locations_name"><label class="string required" for="job_record_project_locations_attributes_2_name"><abbr title="required">*</abbr> Location Title* (Max 54 characters)<div class="notes">Example: Walls, Ceiling, Crown Moulding</div></label><input class="string required" required="required" type="text" name="job_record_project[locations_attributes][${n}][name]" id="job_record_project_locations_attributes_2_name" maxlength="54"></div> 204 </div> 205 206 <div class="column"> 207 <div class="input text optional job_record_project_locations_note"><label class="text optional" for="job_record_project_locations_attributes_2_note">Notes<div class="notes">Add notes about this location</div></label><textarea class="text optional" name="job_record_project[locations_attributes][${n}][note]" id="job_record_project_locations_attributes_2_note" maxlength="250" rows="7"></textarea></div> 208 <p class="notes character-limit">(250)</p> 209 </div> 210 </div> 211 212 <div class="add-product-button-container"> 213 <p class="notes">Add products to this location</p> 214 <a href="#" class="js-add-product button-regular">Add product</a> 215 </div> 216 </div> 217 </div> 218 `; 219 } 220 221 _resetPositions() { 222 const $positions = this.$el.find('.js-position'); 223 $positions.each((index, position) => { 224 $(position).val(index); 225 }); 226 } 227 228 _resetSerialNumbers() { 229 const $serialNumbers = this.$el.find('.js-serial-number'); 230 $serialNumbers.each((index, serialNumber) => { 231 $(serialNumber).text(this._pad(index + 1)); 232 }); 233 } 234 235 _pad(value, padding = 2) { 236 let result = ''; 237 for (let i = 0; i < padding; i++) { 238 result += '0'; 239 } 240 result += value; 241 return result.slice(-padding); 242 } 243} 244 245// Initialize the component when DOM is loaded 246$(document).ready(() => { 247 if ($('.js-job-record-projects').length) { 248 new JobRecordProjects(); 249 } 250});
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.