1/** 2 * CV autofill: the §6 Education confirmation pop-ups. 3 * 4 * Phase 2B of the developer brief. 5 * 6 * Nothing the CV produced has been written to the profile when this runs. 7 * The server parked it; one pop-up per extracted degree collects the four 8 * fields that must be exactly right, and only the final Confirm triggers the 9 * single write (action: cv_ai_confirm_autofill). 10 * 11 * The three picker boxes behave like the ones in the Education form: they 12 * search the same lists through get_taxonomy_lists, and offer "Create it" at 13 * the bottom through create_custom_select. 14 */ 15(function ($) { 16 'use strict'; 17 18 /* 19 * Which list each box searches, and the "type" create_custom_select wants 20 * in order to create into that same list. These strings are not arbitrary 21 * -- they are the keys of the $taxonomies map in create_custom_select(), 22 * and 'service[...]' are the Education form's own field names, so the 23 * Create-it behaviour here is identical to the Education form's. 24 */ 25 var BOXES = { 26 degree: { 27 taxonomy: 'response_time', 28 createType: 'degree', 29 label: 'Degree Type', 30 idKey: 'degree_id', 31 labelKey: 'degree_label' 32 }, 33 university: { 34 taxonomy: 'delivery', 35 createType: 'service[delivery_time]', 36 label: 'University/Institute Name', 37 idKey: 'university_id', 38 labelKey: 'university_label' 39 }, 40 department: { 41 taxonomy: 'project_experience', 42 createType: 'service[title]', 43 label: 'Department Name', 44 idKey: 'department_id', 45 labelKey: 'department_label' 46 } 47 }; 48 49 var CvAutofill = { 50 51 rows: [], 52 index: 0, 53 busy: false, 54 55 /** 56 * Ask the server whether a CV is waiting, and start the sequence. 57 * 58 * @param {function} [onNothing] called when there was nothing pending 59 */ 60 start: function (onNothing) { 61 var self = this; 62 63 $.ajax({ 64 type: 'POST', 65 url: scripts_vars.ajaxurl, 66 dataType: 'json', 67 data: { 68 action: 'cv_ai_pending_popups', 69 security: scripts_vars.ajax_nonce 70 }, 71 success: function (response) { 72 if (!response || response.type !== 'success') { 73 return; 74 } 75 if (!response.rows || !response.rows.length) { 76 /* 77 * A CV with no degrees in it still has five tabs worth 78 * of data parked, so confirm with an empty list rather 79 * than leave it stranded. 80 */ 81 if (response.context) { 82 self.rows = []; 83 self.submit(); 84 } else if (typeof onNothing === 'function') { 85 onNothing(); 86 } 87 return; 88 } 89 self.rows = response.rows; 90 self.index = 0; 91 self.render(); 92 } 93 }); 94 }, 95 96 /** Draw the pop-up for the current degree. */ 97 render: function () { 98 var row = this.rows[this.index]; 99 var total = this.rows.length; 100 var step = this.index + 1; 101 102 var heading = total > 1 103 ? 'Confirm your education (' + step + ' of ' + total + ')' 104 : 'Confirm your education'; 105 106 var said = []; 107 if (row.raw_degree) { said.push(this.esc(row.raw_degree)); } 108 if (row.raw_university) { said.push(this.esc(row.raw_university)); } 109 110 var html = 111 '<div class="cv-confirm-overlay">' + 112 '<div class="cv-confirm-box" role="dialog" aria-modal="true">' + 113 '<h3 class="cv-confirm-title">' + heading + '</h3>' + 114 (said.length 115 ? '<p class="cv-confirm-said">From your CV: <em>' + said.join(' — ') + '</em></p>' 116 : '') + 117 '<p class="cv-confirm-help">Please check these four fields. We only filled in what we could verify — anything left empty needs you.</p>' + 118 '<div class="cv-confirm-fields">' + 119 this.pickerHtml('degree', row) + 120 this.gpaHtml(row) + 121 this.pickerHtml('university', row) + 122 this.pickerHtml('department', row) + 123 '</div>' + 124 '<p class="cv-confirm-error" style="display:none;"></p>' + 125 '<div class="cv-confirm-actions">' + 126 '<button type="button" class="wt-btn cv-confirm-go">' + 127 (step === total ? 'Confirm and fill my profile' : 'Confirm') + 128 '</button>' + 129 '</div>' + 130 '</div>' + 131 '</div>'; 132 133 $('.cv-confirm-overlay').remove(); 134 $('body').append(html).addClass('cv-confirm-open'); 135 }, 136 137 /** One autocomplete box with a hidden term id behind it. */ 138 pickerHtml: function (key, row) { 139 var box = BOXES[key]; 140 var id = row[box.idKey] || 0; 141 var label = row[box.labelKey] || ''; 142 143 return '' + 144 '<div class="cv-confirm-field" data-box="' + key + '">' + 145 '<label>' + box.label + '</label>' + 146 '<input type="hidden" class="cv-term-id" value="' + (id ? parseInt(id, 10) : 0) + '">' + 147 '<input type="text" class="form-control cv-term-text" autocomplete="off"' + 148 ' value="' + this.esc(label) + '"' + 149 ' placeholder="Type to search, at least 3 letters">' + 150 '<ul class="cv-term-results" style="display:none;"></ul>' + 151 '</div>'; 152 }, 153 154 /** 155 * The same control as the Create/Edit Education page: a picker, never 156 * a free-text box, so a GPA can only ever be N/A or a whole number one 157 * unit apart. Highest first, with N/A kept at the top of the list. 158 */ 159 gpaHtml: function (row) { 160 var current = $.trim(String(row.gpa == null ? '' : row.gpa)); 161 var selectedNumber = (current !== '' && current.toUpperCase() !== 'N/A') 162 ? Math.round(parseFloat(current)) : null; 163 164 var html = 165 '<div class="cv-confirm-field" data-box="gpa">' + 166 '<label>Overall GPA (%)</label>' + 167 '<select class="form-control cv-gpa">' + 168 '<option value="">Overall GPA (%)</option>' + 169 '<option value="N/A"' + 170 (current.toUpperCase() === 'N/A' ? ' selected' : '') + '>N/A</option>'; 171 172 for (var n = 100; n >= 50; n--) { 173 html += '<option value="' + n + '"' + 174 (selectedNumber === n ? ' selected' : '') + 175 '>' + n + '</option>'; 176 } 177 178 return html + '</select></div>'; 179 }, 180 181 /** Read the pop-up back into the row, refusing anything incomplete. */ 182 collect: function () { 183 var $box = $('.cv-confirm-box'); 184 var row = this.rows[this.index]; 185 var missing = []; 186 187 $.each(BOXES, function (key, box) { 188 var $f = $box.find('.cv-confirm-field[data-box="' + key + '"]'); 189 var id = parseInt($f.find('.cv-term-id').val(), 10) || 0; 190 var text = $.trim($f.find('.cv-term-text').val()); 191 192 // A typed name that was never picked from the list has no id, 193 // and an id with the text cleared is stale. Both are refused. 194 if (!id || !text) { 195 missing.push(box.label); 196 return; 197 } 198 row[box.idKey] = id; 199 row[box.labelKey] = text; 200 }); 201 202 // The picker only offers N/A and whole numbers, so the one thing 203 // left to refuse is the placeholder. 204 var gpa = $.trim($box.find('.cv-gpa').val() || ''); 205 if (gpa === '') { 206 missing.push('Overall GPA (%)'); 207 } else { 208 row.gpa = gpa; 209 } 210 211 if (missing.length) { 212 $box.find('.cv-confirm-error') 213 .text('Please complete: ' + missing.join(', ') + '.') 214 .show(); 215 return false;
216 } 217 218 return true; 219 }, 220 221 /** Confirm pressed: next degree, or write everything. */ 222 next: function () { 223 if (!this.collect()) { 224 return; 225 } 226 if (this.index + 1 < this.rows.length) { 227 this.index += 1; 228 this.render(); 229 return; 230 } 231 this.submit(); 232 }, 233 234 /** The single write. */ 235 submit: function () { 236 var self = this; 237 if (this.busy) { 238 return; 239 } 240 this.busy = true; 241 242 $('.cv-confirm-box').find('.cv-confirm-go') 243 .prop('disabled', true).text('Saving...'); 244 245 $.ajax({ 246 type: 'POST', 247 url: scripts_vars.ajaxurl, 248 dataType: 'json', 249 data: { 250 action: 'cv_ai_confirm_autofill', 251 security: scripts_vars.ajax_nonce, 252 rows: JSON.stringify(self.rows) 253 }, 254 success: function (response) { 255 self.busy = false; 256 self.close(); 257 258 if (response && response.type === 'success') { 259 if (typeof $.sticky === 'function') { 260 $.sticky(response.message, { 261 classList: 'success', speed: 200, 262 autoclose: scripts_vars.sticky_speed 263 }); 264 } 265 /* 266 * The tabs on screen were rendered before any of this 267 * existed, so they still look empty. Every route out 268 * of the What's Next pop-up therefore navigates or 269 * reloads -- which also stops a later Save posting 270 * that stale empty form back over what was written. 271 */ 272 if (response.whats_next && response.whats_next.lines 273 && response.whats_next.lines.length) { 274 self.whatsNext(response.whats_next); 275 } else { 276 setTimeout(function () { window.location.reload(); }, 1200); 277 } 278 return; 279 } 280 281 if (typeof $.sticky === 'function') { 282 $.sticky( 283 (response && response.message) ? response.message 284 : 'We could not save your CV data. Please fill your profile manually.', 285 { classList: 'important', speed: 200, autoclose: scripts_vars.sticky_speed } 286 ); 287 } 288 }, 289 error: function () { 290 self.busy = false; 291 self.close(); 292 if (typeof $.sticky === 'function') { 293 $.sticky('We could not save your CV data. Please fill your profile manually.', 294 { classList: 'important', speed: 200, autoclose: scripts_vars.sticky_speed }); 295 } 296 } 297 }); 298 }, 299 300 /** 301 * The final "What's Next?" pop-up (section 9). 302 * 303 * Which lines appear depends on the degree the student is applying 304 * for; the server has already applied the priority rule. A line with 305 * no url is informational (the a-index), everything else navigates. 306 * 307 * @param {{degree: string, lines: Array}} next 308 */ 309 whatsNext: function (next) { 310 var html = 311 '<div class="cv-confirm-overlay cv-next-overlay">' + 312 '<div class="cv-confirm-box cv-next-box" role="dialog" aria-modal="true">' + 313 '<h3 class="cv-confirm-title">What’s Next?</h3>' + 314 '<p class="cv-confirm-help">Your profile has been filled in from your CV.</p>' + 315 '<ul class="cv-next-list">'; 316 317 $.each(next.lines, function (i, line) { 318 var icon = '<span class="cv-next-icon">' + (line.icon || '') + '</span>'; 319 320 if (!line.url) { 321 html += '<li class="cv-next-info">' + icon + 322 '<span>' + CvAutofill.esc(line.text) + '</span></li>'; 323 return; 324 } 325 html += '<li class="cv-next-item">' + 326 '<a href="' + CvAutofill.esc(line.url) + '">' + icon + 327 '<span>' + CvAutofill.esc(line.text) + '</span>' + 328 '</a>' + 329 '<a class="wt-btn cv-next-go" href="' + CvAutofill.esc(line.url) + '">Go</a>' + 330 '</li>'; 331 }); 332 333 html += '</ul></div></div>'; 334 335 $('.cv-confirm-overlay').remove(); 336 $('body').append(html).addClass('cv-confirm-open'); 337 }, 338 339 close: function () { 340 $('.cv-confirm-overlay').remove(); 341 $('body').removeClass('cv-confirm-open'); 342 }, 343 344 esc: function (value) { 345 return $('<div>').text(value == null ? '' : value).html(); 346 } 347 }; 348 349 window.CvAutofill = CvAutofill; 350 351 /* ------------------------------------------------------------------ 352 * The autocomplete boxes 353 * --------------------------------------------------------------- */ 354 355 var searchTimer = null; 356 var searchXhr = null; 357 358 $(document).on('input', '.cv-confirm-box .cv-term-text', function () { 359 var $input = $(this); 360 var $field = $input.closest('.cv-confirm-field'); 361 var $list = $field.find('.cv-term-results'); 362 var box = BOXES[$field.data('box')]; 363 var value = $.trim($input.val()); 364 365 // Typing invalidates whatever was picked before. 366 $field.find('.cv-term-id').val(0); 367 368 if (searchTimer) { clearTimeout(searchTimer); } 369 if (value.length < 3) { 370 $list.hide().empty(); 371 return; 372 } 373 374 searchTimer = setTimeout(function () { 375 if (searchXhr) { searchXhr.abort(); } 376 searchXhr = $.ajax({ 377 type: 'POST', 378 url: scripts_vars.ajaxurl, 379 dataType: 'json', 380 data: { 381 action: 'get_taxonomy_lists', 382 security: scripts_vars.ajax_nonce, 383 taxonomy: box.taxonomy, 384 value: value 385 }, 386 success: function (response) { 387 var items = (response && response.terms) ? response.terms : []; 388 var html = ''; 389 390 $.each(items.slice(0, 20), function (i, term) { 391 html += '<li class="cv-term-hit" data-id="' + parseInt(term.term_id, 10) + '">' + 392 CvAutofill.esc(term.name) + '</li>'; 393 }); 394 395 // "Create it", exactly as the Education form offers it. 396 html += '<li class="cv-term-create">' +
397 'Not in the list? <strong>Create it</strong>: “' + 398 CvAutofill.esc(value) + '”</li>'; 399 400 $list.html(html).show(); 401 } 402 }); 403 }, 250); 404 }); 405 406 $(document).on('click', '.cv-term-hit', function () { 407 var $hit = $(this); 408 var $field = $hit.closest('.cv-confirm-field'); 409 $field.find('.cv-term-id').val($hit.data('id')); 410 $field.find('.cv-term-text').val($hit.text()); 411 $field.find('.cv-term-results').hide().empty(); 412 $('.cv-confirm-error').hide(); 413 }); 414 415 $(document).on('click', '.cv-term-create', function () { 416 var $field = $(this).closest('.cv-confirm-field'); 417 var box = BOXES[$field.data('box')]; 418 var name = $.trim($field.find('.cv-term-text').val()); 419 var $list = $field.find('.cv-term-results'); 420 421 if (name.length < 3) { 422 return; 423 } 424 $list.html('<li class="cv-term-busy">Creating…</li>'); 425 426 $.ajax({ 427 type: 'POST', 428 url: scripts_vars.ajaxurl, 429 dataType: 'json', 430 data: { 431 action: 'create_custom_select', 432 security: scripts_vars.ajax_nonce, 433 name: name, 434 type: box.createType 435 }, 436 success: function (response) { 437 if (response && response.type === 'success' && response.term_id) { 438 $field.find('.cv-term-id').val(response.term_id); 439 $field.find('.cv-term-text').val(response.name); 440 $list.hide().empty(); 441 $('.cv-confirm-error').hide(); 442 return; 443 } 444 $list.html('<li class="cv-term-busy">' + 445 CvAutofill.esc((response && response.message) ? response.message : 'Could not create that.') + 446 '</li>'); 447 }, 448 error: function () { 449 $list.html('<li class="cv-term-busy">Could not create that.</li>'); 450 } 451 }); 452 }); 453 454 $(document).on('click', '.cv-confirm-go', function () { 455 CvAutofill.next(); 456 }); 457 458 /* ------------------------------------------------------------------ 459 * Entry point: a CV parked during registration is confirmed on the 460 * first dashboard load. The Personal Details path calls start() 461 * itself, straight after extraction. 462 * --------------------------------------------------------------- */ 463 $(function () { 464 if (typeof scripts_vars === 'undefined' || 465 typeof cv_autofill_vars === 'undefined' || 466 !cv_autofill_vars.pending) { 467 return; 468 } 469 CvAutofill.start(); 470 }); 471 472})(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.