1class ReDiCustomFields { 2 static instance = null; 3 4 static getInstance() { 5 if (!ReDiCustomFields.instance) { 6 ReDiCustomFields.instance = new ReDiCustomFields(); 7 } 8 return ReDiCustomFields.instance; 9 } 10 11 constructor() { 12 this.container = jQuery('#custom_fields_container'); 13 this.loadedFields = new Map(); // Track loaded fields by placeId 14 } 15 16 isLoaded(placeId) { 17 return this.loadedFields.has(placeId); 18 } 19 20 renderFields(customFields, placeId) { 21 // If fields are already loaded for this place, skip 22 if (this.isLoaded(placeId)) { 23 return; 24 } 25 26 // API can return an empty object or wrapped payload when there are no custom fields. 27 const normalizedFields = Array.isArray(customFields) 28 ? customFields 29 : Array.isArray(customFields?.CustomFields) 30 ? customFields.CustomFields 31 : []; 32 33 if (!normalizedFields.length) { 34 this.loadedFields.set(placeId, true); 35 return; 36 } 37 38 // Sort custom fields by DisplayOrder 39 normalizedFields.sort((a, b) => { 40 return (a.DisplayOrder || 0) - (b.DisplayOrder || 0); 41 }); 42 43 // Process each custom field 44 normalizedFields.forEach(field => { 45 // Generate HTML for this field 46 const fieldHtml = this.renderField(field); 47 48 // Find all elements with data-display-order 49 const orderedElements = jQuery('[data-display-order]'); 50 51 // Find the element with the closest lower or equal display-order 52 let targetElement = null; 53 let targetOrder = -1; 54 55 orderedElements.each(function() { 56 const elementOrder = parseInt(jQuery(this).data('display-order')); 57 if (elementOrder <= field.DisplayOrder && elementOrder > targetOrder) { 58 targetElement = jQuery(this); 59 targetOrder = elementOrder; 60 } 61 }); 62 63 if (targetElement) { 64 // Insert after the target element 65 targetElement.after(fieldHtml); 66 } else { 67 // If no target found, append to container 68 this.container.append(fieldHtml); 69 } 70 }); 71 72 // Mark these fields as loaded for this place 73 this.loadedFields.set(placeId, true); 74 } 75 76 renderField(field) { 77 let html = `<div data-display-order="${field.DisplayOrder}">`; 78 79 80 if ( 81 (field.Type === 'dropdown' || field.Type === 'options') && 82 (!field.Values || field.Values.trim() === '') 83 ) { 84 console.warn(`Skipping field "${field.Text}" (ID: ${field.Id}) â no values defined. Defined values are required for dropdown and options types. Go to Settings > Custom Fields to define values.`); 85 return ''; // Don't render anything 86 } 87 88 switch (field.Type) { 89 case 'options': 90 html += `<label for="field_${field.Id}">${field.Text}`; 91 if (field.Required) { 92 html += `<span class="redi_required"> *</span> 93 <input type="hidden" 94 id="field_${field.Id}_message" 95 value="${field.Message || 'Custom field is required'}">`; 96 } 97 html += '</label>'; 98 html += this.renderRadioField(field); 99 break; 100 case 'dropdown': 101 html += `<label for="field_${field.Id}">${field.Text}`; 102 if (field.Required) { 103 html += `<span class="redi_required"> *</span> 104 <input type="hidden" 105 id="field_${field.Id}_message" 106 value="${field.Message || 'Custom field is required'}">`; 107 } 108 html += '</label>'; 109 html += this.renderDropdownField(field); 110 break; 111 case 'newsletter': 112 case 'reminder': 113 case 'allowsms': 114 case 'checkbox': 115 case 'allowwhatsapp': 116 case 'gdpr': 117 html += this.renderCheckboxField(field); // label will be inside this 118 break; 119 default: 120 html += `<label for="field_${field.Id}">${field.Text}`; 121 if (field.Required) { 122 html += `<span class="redi_required"> *</span> 123 <input type="hidden" 124 id="field_${field.Id}_message" 125 value="${field.Message || 'Custom field is required'}">`; 126 } 127 html += '</label>'; 128 html += this.renderTextField(field); 129 } 130 131 html += '</div>'; 132 return html; 133 } 134 135 renderTextField(field) { 136 return `<input type="text" 137 value="" 138 id="field_${field.Id}" 139 name="field_${field.Id}" 140 ${field.Required ? 'class="field_required"' : ''}>`; 141 } 142 143 renderCheckboxField(field) { 144 return `
145 <div class="redi-custom-checkbox"> 146 <label for="field_${field.Id}" style="display:block;"> 147 ${field.Text} 148 ${field.Required ? '<span class="redi_required"> *</span>' : ''} 149 </label> 150 <input type="checkbox" 151 id="field_${field.Id}" 152 name="field_${field.Id}" 153 ${field.Required ? 'class="field_required"' : ''} 154 ${field.Default === 'True' ? 'checked' : ''}> 155 ${field.Required ? `<input type="hidden" id="field_${field.Id}_message" value="${field.Message || 'Custom field is required'}">` : ''} 156 </div> 157 `; 158 } 159 160 renderRadioField(field) { 161 return field.Values.split(',') 162 .filter(value => value) 163 .map(value => ` 164 <input type="radio" 165 value="${value}" 166 name="field_${field.Id}" 167 id="field_${field.Id}_${value}" 168 class="redi-radiobutton${field.Required ? ' field_required' : ''}"> 169 <label class="redi-radiobutton-label" 170 for="field_${field.Id}_${value}">${value}</label> 171 <br/>`) 172 .join(''); 173 } 174 175 renderDropdownField(field) { 176 const options = field.Values.split(',') 177 .filter(value => value) 178 .map(value => `<option value="${value}">${value}</option>`) 179 .join(''); 180 181 return `<select 182 id="field_${field.Id}" 183 name="field_${field.Id}" 184 ${field.Required ? 'class="field_required"' : ''}> 185 <option value="">Select</option> 186 ${options} 187 </select>`; 188 } 189}
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.