1/** 2 * Reusable toast / alert helper. Lives in jobsearch-common.js because that 3 * file is enqueued in BOTH frontend and wp-admin contexts. 4 * 5 * jobsearchAlert('success', 'Saved.'); 6 * jobsearchAlert('error', 'Something went wrong.'); 7 * jobsearchAlert('warning', 'Heads up.'); 8 * jobsearchAlert('info', 'FYI.', { duration: 8000, title: 'Notice' }); 9 * 10 * Styles live in css/plugin.css (jobsearch-alert-* classes). Localized strings 11 * pulled from window.jobsearch_plugin_vars.alert_close_label (set via 12 * wp_localize_script). Empty / undefined messages are silently skipped â 13 * callers should pass only dynamic, already-localized strings; this helper 14 * never invents fallback text. 15 */ 16(function () { 17 if (typeof window === 'undefined' || window.jobsearchAlert) { return; } 18 19 window.jobsearchAlert = function (type, message, options) { 20 var $ = window.jQuery; 21 if (!$) { return; } 22 if (message === undefined || message === null || message === '') { return; } 23 24 // De-dupe: suppress an identical toast fired within the last ~1.2s. This 25 // lets the global err_msg safety-net (below) run unconditionally without 26 // double-toasting alongside a handler that already showed the same text. 27 var _ddKey = type + '|' + message; 28 var _ddNow = (new Date()).getTime(); 29 if (window.__jobsearch_last_toast 30 && window.__jobsearch_last_toast.key === _ddKey 31 && (_ddNow - window.__jobsearch_last_toast.at) < 1200) { 32 return; 33 } 34 window.__jobsearch_last_toast = { key: _ddKey, at: _ddNow }; 35 36 var allowed = { success: 1, error: 1, warning: 1, info: 1 }; 37 type = allowed[type] ? type : 'info'; 38 options = options || {}; 39 var duration = (typeof options.duration === 'number' && options.duration > 0) ? options.duration : 5000; 40 var title = options.title || ''; 41 42 var $container = $('#jobsearch-alert-container'); 43 if (!$container.length) {
44 $container = $('<div id="jobsearch-alert-container" class="jobsearch-alert-container"></div>').appendTo('body'); 45 } 46 47 var iconMap = { 48 success: 'fa-check', 49 error: 'fa-exclamation', 50 warning: 'fa-exclamation', 51 info: 'fa-info' 52 }; 53 54 var l10n = (window.jobsearch_plugin_vars || {}); 55 var closeLabel = l10n.alert_close_label || 'Close'; 56 57 var $toast = $( 58 '<div class="jobsearch-alert jobsearch-alert-' + type + '" role="alert" aria-live="polite">' + 59 '<i class="jobsearch-alert-icon fa ' + iconMap[type] + '"></i>' + 60 '<div class="jobsearch-alert-body">' + 61 (title ? '<div class="jobsearch-alert-title"></div>' : '') + 62 '<div class="jobsearch-alert-msg"></div>' + 63 '</div>' + 64 '<span class="jobsearch-alert-close" role="button" tabindex="0">×</span>' + 65 '</div>' 66 ); 67 $toast.find('.jobsearch-alert-close').attr('aria-label', closeLabel); 68 if (title) { $toast.find('.jobsearch-alert-title').text(title); } 69 // Accept either plain text or HTML; existing AJAX responses sometimes 70 // include markup like <i class="fa fa-times"></i> inside response.msg. 71 $toast.find('.jobsearch-alert-msg').html(message); 72 73 $container.append($toast); 74 void $toast[0].offsetWidth; 75 $toast.addClass('is-visible'); 76 77 var dismiss = function () { 78 if ($toast.data('dismissed')) { return; } 79 $toast.data('dismissed', true); 80 $toast.removeClass('is-visible').addClass('is-leaving'); 81 window.setTimeout(function () { $toast.remove(); }, 350); 82 }; 83 84 $toast.on('click', '.jobsearch-alert-close', dismiss); 85 $toast.on('keydown', '.jobsearch-alert-close', function (e) { 86 if (e.key === 'Enter' || e.key === ' ' || e.keyCode === 13 || e.keyCode === 32) { e.preventDefault(); dismiss(); } 87 }); 88 var t = window.setTimeout(dismiss, duration); 89 $toast.on('mouseenter', function () { window.clearTimeout(t); }); 90 $toast.on('mouseleave', function () { t = window.setTimeout(dismiss, 1500); }); 91 92 return { dismiss: dismiss, element: $toast[0] }; 93 }; 94 95 /** 96 * Global safety net: surface a server `err_msg` as an error toast for ANY 97 * jobsearch_* AJAX response whose own done() handler doesn't display it. 98 * Many dashboard remove/save handlers only read `success` and silently 99 * swallow the restriction message (demo user, capability, nonce). Bound 100 * once; jobsearchAlert's de-dupe (above) prevents double-toasting with the 101 * handlers that already show the message themselves. 102 * 103 * Only catches requests whose serialized data is a query string (the plain 104 * data:{} / data:'...' handlers). FormData uploads (processData:false) are 105 * skipped here and handled by their own per-handler toasts. 106 */ 107 (function bindJobsearchErrMsgNet() { 108 var $ = window.jQuery; 109 if (!$ || $.__jobsearch_errmsg_net) { return; } 110 $.__jobsearch_errmsg_net = true; 111 $(document).ajaxComplete(function (event, jqxhr, settings) { 112 try { 113 var data = (settings && typeof settings.data === 'string') ? settings.data : ''; 114 if (data.indexOf('action=jobsearch_') === -1) { return; } 115 var resp = jqxhr && jqxhr.responseJSON; 116 if (!resp && jqxhr && jqxhr.responseText) { 117 try { resp = JSON.parse(jqxhr.responseText); } catch (e) { return; } 118 } 119 if (!resp || typeof resp !== 'object' || !resp.err_msg) { return; } 120 if (typeof window.jobsearchAlert === 'function') { 121 window.jobsearchAlert('error', resp.err_msg); 122 } 123 } catch (e) {} 124 }); 125 })(); 126 127 /** 128 * Reusable confirmation modal â accepts an HTML body and custom button 129 * labels, returns a Promise that resolves true on confirm, false on 130 * cancel / dismiss. 131 * 132 * jobsearchConfirm('Delete this item?').then(function (ok) { ... }); 133 * jobsearchConfirm('<p>Detach <b>foo</b>?</p>', { 134 * confirmText: 'Yes, detach', 135 * cancelText: 'Keep', 136 * title: 'Confirm' 137 * }).then(function (ok) { ... }); 138 * 139 * Default labels are pulled from window.jobsearch_plugin_vars 140 * (confirm_yes / confirm_no / alert_close_label) and fall back to plain 141 * English if not localized. 142 */ 143 window.jobsearchConfirm = function (message, options) { 144 var $ = window.jQuery; 145 options = options || {}; 146 147 if (!$ || message === undefined || message === null || message === '') { 148 return { then: function (cb) { cb(false); return this; } }; 149 } 150 151 var l10n = (window.jobsearch_plugin_vars || {}); 152 var confirmText = options.confirmText || l10n.confirm_yes || 'Yes'; 153 var cancelText = options.cancelText || l10n.confirm_no || 'No'; 154 var closeLabel = l10n.alert_close_label || 'Close'; 155 var title = options.title || ''; 156 var type = options.type || 'danger'; 157 var iconClass = options.icon || (type === 'danger' ? 'fa fa-tras
157h-o' : 'fa fa-exclamation'); 158 159 var $backdrop = $('<div class="jobsearch-confirm-backdrop" role="dialog" aria-modal="true"></div>'); 160 var $box = $( 161 '<div class="jobsearch-confirm-box">' + 162 '<div class="jobsearch-confirm-icon"><i></i></div>' + 163 (title ? '<div class="jobsearch-confirm-title"></div>' : '') + 164 '<div class="jobsearch-confirm-body"></div>' + 165 '<div class="jobsearch-confirm-actions">' + 166 '<button type="button" class="jobsearch-confirm-cancel"></button>' + 167 '<button type="button" class="jobsearch-confirm-ok"></button>' + 168 '</div>' + 169 '<span class="jobsearch-confirm-close" role="button" tabindex="0">×</span>' + 170 '</div>' 171 ); 172 $box.attr('data-type', type); 173 $box.find('.jobsearch-confirm-icon i').attr('class', iconClass); 174 if (options.icon === '' || options.icon === false) { $box.find('.jobsearch-confirm-icon').remove(); } 175 if (title) { $box.find('.jobsearch-confirm-title').text(title); } 176 $box.find('.jobsearch-confirm-body').html(message); 177 $box.find('.jobsearch-confirm-ok').text(confirmText); 178 $box.find('.jobsearch-confirm-cancel').text(cancelText); 179 $box.find('.jobsearch-confirm-close').attr('aria-label', closeLabel); 180 181 $backdrop.append($box).appendTo('body'); 182 // force reflow so the transition runs 183 void $box[0].offsetWidth; 184 $backdrop.addClass('is-visible'); 185 186 var settled = false; 187 var resolveFn; 188 var promise = (typeof Promise !== 'undefined') 189 ? new Promise(function (r) { resolveFn = r; }) 190 : (function () { 191 var cbs = []; 192 resolveFn = function (v) { while (cbs.length) { cbs.shift()(v); } }; 193 return { then: function (cb) { cbs.push(cb); return this; } }; 194 })(); 195 196 var done = function (result) { 197 if (settled) { return; } 198 settled = true; 199 $backdrop.removeClass('is-visible').addClass('is-leaving'); 200 $(document).off('keydown.jobsearchConfirm'); 201 window.setTimeout(function () { $backdrop.remove(); }, 200); 202 resolveFn(!!result); 203 }; 204 205 $box.on('click', '.jobsearch-confirm-ok', function () { done(true); }); 206 $box.on('click', '.jobsearch-confirm-cancel', function () { done(false); }); 207 $box.on('click', '.jobsearch-confirm-close', function () { done(false); }); 208 $backdrop.on('click', function (e) { if (e.target === $backdrop[0]) { done(false); } }); 209 $(document).on('keydown.jobsearchConfirm', function (e) { 210 if (e.key === 'Escape' || e.keyCode === 27) { done(false); } 211 if (e.key === 'Enter' || e.keyCode === 13) { done(true); } 212 }); 213 $box.find('.jobsearch-confirm-ok').trigger('focus'); 214 215 return promise; 216 }; 217})(); 218 219var jobsearch_common_getJSON = function (url, callback) { 220 var xhr = new XMLHttpRequest(); 221 xhr.open('GET', url, true); 222 xhr.responseType = 'json'; 223 xhr.onload = function () { 224 var status = xhr.status; 225 if (status === 200) { 226 callback(null, xhr.response); 227 } else { 228 callback(status, xhr.response); 229 } 230 }; 231 xhr.send(); 232}; 233 234 235 236jQuery(document).on('click', '.load_users_field', function (e) { 237 e.preventDefault(); 238 var this_id = jQuery(this).data('randid'), 239 loaded = jQuery(this).data('loaded'), 240 custom_field = jQuery('#load_users_field_' + this_id), 241 ajax_url = jobsearch_plugin_vars.ajax_url, 242 force_std = jQuery(this).data('forcestd'); 243 if (loaded != true) { 244 jQuery('.load_users_loader_' + this_id).html('<i class="fa fa-refresh fa-spin"></i>'); 245 var request = jQuery.ajax({ 246 url: ajax_url, 247 method: "POST", 248 data: { 249 force_std: force_std, 250 _nonce: jobsearch_comon_script_vars.nonce, 251 action: 'jobsearch_load_all_users_list_opts', 252 }, 253 dataType: "json" 254 }); 255 256 request.done(function (response) { 257 if ('undefined' !== typeof response.html) { 258 custom_field.html(response.html);
259 jQuery('.load_users_loader_' + this_id).html(''); 260 custom_field.data('loaded', true); 261 } 262 }); 263 264 request.fail(function (jqXHR, textStatus) { 265 }); 266 } 267 return false; 268 269}); 270 271function jobsearch_multicap_all_functions() { 272 if (typeof grecaptcha === 'undefined' || typeof grecaptcha.render !== 'function') { 273 return; 274 } 275 var all_elements = jQuery(".g-recaptcha"); 276 for (var i = 0; i < all_elements.length; i++) { 277 var el = all_elements[i]; 278 var id = el.getAttribute('id'); 279 var site_key = el.getAttribute('data-sitekey'); 280 if (!id || !site_key) { 281 continue; 282 } 283 if (el.getAttribute('data-jobsearch-captcha-rendered') === '1' || el.childElementCount > 0) { 284 continue; 285 } 286 try { 287 grecaptcha.render(id, { 'sitekey': site_key }); 288 el.setAttribute('data-jobsearch-captcha-rendered', '1'); 289 } catch (e) { 290 el.setAttribute('data-jobsearch-captcha-rendered', '1'); 291 } 292 } 293} 294 295function jobsearch_captcha_reload(admin_url, captcha_id) { 296 "use strict"; 297 var dataString = '&action=jobsearch_captcha_reload&captcha_id=' + captcha_id; 298 jQuery.ajax({ 299 type: "POST", 300 url: admin_url, 301 data: dataString, 302 dataType: 'html', 303 success: function (data) { 304 jQuery("#" + captcha_id + "_div").html(data); 305// jQuery('.g-recaptcha').each(function () { 306// jQuery(this).find('iframe:first') 307// .removeAttr('width') 308// .addClass('img-responsive') 309// .parent().parent() 310// .css({'width': 'auto'}); 311// }); 312 } 313 }); 314} 315 316window.djangoReCaptcha = { 317 list: [], 318 setup: function () { 319 jQuery('.g-recaptcha').each(function () { 320 var $container = jQuery(this); 321 var config = $container.data(); 322 323 jobsearchAlert('info', $container.attr('class')); 324 325 djangoReCaptcha.init($container, config); 326 }); 327 328 jQuery(window).on('resize orientationchange', function () { 329 jQuery(djangoReCaptcha.list).each(function (idx, el) { 330 djangoReCaptcha.resize.apply(null, el); 331 }); 332 }); 333 }, 334 init: function ($container, config) { 335 grecaptcha.render($container.get(0), config); 336 var captchaSize, scaleFactor; 337 var $iframe = $container.find('iframe').eq(0); 338 339 $iframe.on('load', function () { 340 $container.addClass('g-recaptcha-initted'); 341 captchaSize = captchaSize || {w: $iframe.width() - 2, h: $iframe.height()}; 342 djangoReCaptcha.resize($container, captchaSize); 343 djangoReCaptcha.list.push([$container, captchaSize]); 344 }); 345 }, 346}; 347 348window.djangoReCaptchaSetup = window.djangoReCaptcha.setup; 349 350jQuery(document).on('click', '.load-more-team', function () { 351 352 var _this = jQuery(this), 353 total_pages = _this.attr('data-pages'), 354 cur_page = _this.attr('data-page'), 355 this_rand = _this.attr('data-rand'), 356 this_view = _this.attr('data-view'), 357 employer_id = _this.attr('data-id'), 358 class_pref = _this.attr('data-pref'), 359 ajax_url = jobsearch_plugin_vars.ajax_url; 360 361 var team_view = 'default'; 362 if ('undefined' !== typeof this_view && this_view != '') { 363 team_view = this_view; 364 } 365 366 367 368 var members_holder = jQuery('#members-holder-' + this_rand); 369 var this_html = _this.html(); 370 371 if (!_this.hasClass('jobsearch-loading')) { 372 _this.addClass('jobsearch-loading'); 373 _this.html('<i class="fa fa-refresh fa-spin"></i> ' + jobsearch_plugin_vars.loading); 374 var request = jQuery.ajax({ 375 url: ajax_url, 376 method: "POST", 377 data: { 378 total_pages: total_pages, 379 cur_page: cur_page, 380 employer_id: employer_id, 381 class_pref: class_pref, 382 team_style: team_view, 383 _nonce: jobsearch_comon_script_vars.nonce, 384 action: 'jobsearch_load_employer_team_next_page', 385 }, 386 dataType: "json" 387 }); 388 389 request.done(function (response) { 390 if ('undefined' !== typeof response.html && response.html != '') { 391 members_holder.append(response.html); 392 members_holder.find('.new-entries').slideDown().removeClass('new-entries'); 393 var current_page = parseInt(cur_page) + 1; 394 _this.attr('data-page', current_page); 395 if (current_page == total_pages) { 396 _this.hide(); 397 } 398 } 399 _this.html(this_html); 400 _this.removeClass('jobsearch-loading'); 401 }); 402 403 request.fail(function (jqXHR, textStatus) { 404 _this.html(this_html); 405 _this.removeClass('jobsearch-loading'); 406 }); 407 } 408 return false; 409 410}); 411 412jQuery(document).on('click', ".jobsearch-click-btn", function () { 413 var t_tihs = jQuery(this); 414 var filtr_cname = t_tihs.attr('data-cname'); 415 var filtr_cval = t_tihs.attr('data-cval');
416 if (filtr_cval == 'close') { 417 filtr_cval = 'open'; 418 } else { 419 filtr_cval = 'close'; 420 } 421 422 423 t_tihs.parents('.jobsearch-search-filter-toggle').find('.jobsearch-checkbox-toggle').slideToggle("slow", function () { 424 var c_date = new Date(); 425 c_date.setTime(c_date.getTime() + (60 * 60 * 1000)); 426 var c_expires = "; c_expires=" + c_date.toGMTString(); 427 console.info(filtr_cname + "=" + filtr_cval + c_expires + "; path=/"); 428 document.cookie = filtr_cname + "=" + filtr_cval + c_expires + "; path=/"; 429 //console.info(document.cookie); 430 }); 431 432 t_tihs.parents('.jobsearch-search-filter-toggle').toggleClass("jobsearch-remove-padding"); 433 return false; 434}); 435 436if (jQuery('.jobsearch-mobile-btn').length > 0) { 437 jQuery(document).on('click', '.jobsearch-mobile-btn', function () { 438 jQuery('.jobsearch-mobile-section').slideToggle(1000); 439 jQuery(this).toggleClass("open"); 440 }); 441} 442 443jQuery(document).on('change', '.jobsearch-depndfield-srchange', function() { 444 var _this = jQuery(this); 445 var field_parent = _this.parents('.jobsearch-depndetfield-con'); 446 var field_mid = field_parent.attr('data-mid'); 447 var field_type = field_parent.attr('data-ftype'); 448 var field_place = field_parent.attr('data-plc'); 449 var field_thid = field_parent.attr('data-thid'); 450 var is_multi_on = field_parent.attr('data-mism'); 451 // Read depend / optid from the source <option> element directly. The 452 // previous code looked these up in the Selectize-rendered DOM 453 // (`.selectize-control .selectize-dropdown-content div[data-value=â¦]`), 454 // which no longer exists since the SelectizeâSelect2 migration. The 455 // shim flattens Selectize-era `data-data` JSON into real data-<key> 456 // attributes on each <option>, so they're readable here. 457 var slectd_optionval = _this.val(); 458 var slectd_optionval_esc = String(slectd_optionval == null ? '' : slectd_optionval).replace(/"/g, '\\"'); 459 var selctd_option = _this.find('option[value="' + slectd_optionval_esc + '"]').first(); 460 var has_depnd = selctd_option.attr('data-depend'); 461 var opt_id = selctd_option.attr('data-optid'); 462 var all_nextdep_fields = field_parent.nextAll('.jobsearch-deparent-field.deparent-' + field_thid); 463 if (all_nextdep_fields.length > 0) { 464 all_nextdep_fields.each(function() { 465 jQuery(this).remove(); 466 }); 467 } 468 if (has_depnd == 'true') { 469 field_parent.addClass('disabled-field'); 470 field_parent.append('<span class="jobsearch-depfield-loder"><i class="fa fa-refresh fa-spin"></i></span>'); 471 var loc_counts_request = jQuery.ajax({ 472 url: jobsearch_plugin_vars.ajax_url, 473 method: "POST", 474 data: { 475 opt_id: opt_id, 476 field_mid: field_mid, 477 field_type: field_type, 478 field_plc: field_place, 479 is_multi_on: is_multi_on, 480 _nonce: jobsearch_comon_script_vars.nonce, 481 action: 'jobsearch_get_depend_fields_infront', 482 }, 483 dataType: "json" 484 }); 485 loc_counts_request.done(function (response) { 486 if (typeof response.html !== undefined) { 487 field_parent.after(response.html); 488 } 489 }); 490 loc_counts_request.complete(function () { 491 field_parent.removeClass('disabled-field'); 492 field_parent.find('.jobsearch-depfield-loder').remove(); 493 }); 494 } 495}); 496 497jQuery(document).on('change', '.jobsearch-depndfield-mulchange', function() { 498 var _this = jQuery(this); 499 var field_parent = _this.parents('.jobsearch-depndetfield-con'); 500 var field_mid = field_parent.attr('data-mid'); 501 var field_type = field_parent.attr('data-ftype'); 502 var field_place = field_parent.attr('data-plc'); 503 var field_thid = field_parent.attr('data-thid'); 504 var selectize_drpdown = field_parent.find('.hiden-multiselc-opts'); 505 var is_multi_on = field_parent.attr('data-mism'); 506 507 var slectd_optionval = _this.val(); 508 509 var opt_ids = ''; 510 if (slectd_optionval.length > 0) { 511 var opt_ids_arr = []; 512 jQuery(slectd_optionval).each(function(index, _this_val) { 513 var selctize_val_div = selectize_drpdown.find('div[data-value="' + _this_val + '"]'); 514 if (selctize_val_div.attr('data-depend') == 'true') { 515 opt_ids_arr.push(selctize_val_div.attr('data-optid')); 516 } 517 }); 518 if (opt_ids_arr.length > 0) { 519 opt_ids = opt_ids_arr.join(','); 520 } 521 } 522 523 // 524 var all_nextdep_fields = field_parent.nextAll('.jobsearch-deparent-field.deparent-' + field_thid); 525 if (all_nextdep_fields.length > 0) { 526 all_nextdep_fields.each(function() { 527 jQuery(this).remove(); 528 }); 529 } 530 531 if (opt_ids != '') { 532 field_parent.addClass('disabled-field'); 533 field_parent.append('<span class="jobsearch-depfield-loder"><i class="fa fa-refresh fa-spin"></i></span>'); 534 var loc_counts_request = jQuery.ajax({ 535 url: jobsearch_plugin_vars.ajax_url, 536 method: "POST", 537 data: { 538 opt_id: opt_ids, 539 field_mid: field_mid, 540 field_type: field_type, 541 field_plc: field_place, 542 is_multi_on: is_multi_on, 543 _nonce: jobsearch_comon_script_vars.nonce, 544 action: 'jobsearch_get_depend_fields_infront', 545 }, 546 dataType: "json" 547 }); 548 loc_counts_request.done(function (response) { 549 if (typeof response.html !== undefined) { 550 field_parent.after(response.html); 551 } 552 }); 553 loc_counts_request.complete(function () { 554 field_parent.removeClass('disabled-field'); 555 field_parent.find('.jobsearch-depfield-loder').remove(); 556 }); 557 } 558}); 559 560jQuery(document).on('change', '.jobsearch-depndfield-rchchange', function() { 561 var _this = jQuery(this); 562 var field_parent = _this.parents('.jobsearch-depndetfield-con'); 563 var field_mid = field_parent.attr('data-mid'); 564 var field_type = field_parent.attr('data-ftype');
565 var field_place = field_parent.attr('data-plc'); 566 var field_thid = field_parent.attr('data-thid'); 567 var is_multi_on = field_parent.attr('data-mism'); 568 569 var opt_ids = ''; 570 var opt_ids_arr = []; 571 field_parent.find('.jobsearch-depndfield-rchchange').each(function() { 572 if (jQuery(this).is(':checked') && jQuery(this).attr('data-depend') == 'true') { 573 opt_ids_arr.push(jQuery(this).attr('data-optid')); 574 } 575 }); 576 if (opt_ids_arr.length > 0) { 577 opt_ids = opt_ids_arr.join(','); 578 } 579 580 // 581 var all_nextdep_fields = field_parent.nextAll('.jobsearch-deparent-field.deparent-' + field_thid); 582 if (all_nextdep_fields.length > 0) { 583 all_nextdep_fields.each(function() { 584 jQuery(this).remove(); 585 }); 586 } 587 588 if (opt_ids != '') { 589 field_parent.addClass('disabled-field'); 590 field_parent.append('<span class="jobsearch-depfield-loder"><i class="fa fa-refresh fa-spin"></i></span>'); 591 var loc_counts_request = jQuery.ajax({ 592 url: jobsearch_plugin_vars.ajax_url, 593 method: "POST", 594 data: { 595 opt_id: opt_ids, 596 field_mid: field_mid, 597 field_type: field_type, 598 field_plc: field_place, 599 is_multi_on: is_multi_on, 600 _nonce: jobsearch_comon_script_vars.nonce, 601 action: 'jobsearch_get_depend_fields_infront', 602 }, 603 dataType: "json" 604 }); 605 loc_counts_request.done(function (response) { 606 if (typeof response.html !== undefined) { 607 field_parent.after(response.html); 608 } 609 }); 610 loc_counts_request.complete(function () { 611 field_parent.removeClass('disabled-field'); 612 field_parent.find('.jobsearch-depfield-loder').remove(); 613 }); 614 } 615}); 616 617jQuery(document).on('click', '.show-applyjob-questsbtn', function () { 618 var _this_id = jQuery(this).attr('data-id'); 619 var data_html = jQuery('#cand-ansdata-' + _this_id).html(); 620 jQuery('#JobSearchModalApplyJobQuests').find('.jobsearch-applyjobans-con').html(data_html); 621 jobsearch_modal_popup_open('JobSearchModalApplyJobQuests'); 622}); 623 624/** 625 * Unified front-end form-required validator. 626 * 627 * var ok = window.jobsearchValidateRequired($form); 628 * if (!ok) { return false; } // toast + red borders already shown 629 * 630 * Covers every check the legacy inline blocks did, in one place: 631 * - .phone-input-error (intl-tel-input error state) 632 * - API location selects (jobsearch_field_location_location1/2/3) 633 * when jobsearch_plugin_vars.locations_type === 'api' && 634 * required_api_locs === 'yes' 635 * - select[name^="user_sector"].profile-req-field 636 * - .jobsearch-reqtext-editor (TinyMCE-backed textareas) 637 * - .cusfield-checkbox-required groups (at least one checkbox required) 638 * - input[type=file].jobsearch-cusfieldatt-req 639 * - Generic [required], .profile-req-field, .multiselect-req 640 * â widget-agnostic so it works for Select2, Selectize, and plain <select>. 641 * 642 * Fires one jobsearchAlert('error', 'Required fields: â¦') on failure listing 643 * the missing field labels, applies the existing 1px red border styling, 644 * scrolls to the first missing field, and returns false. 645 * 646 * opts.skip â array of check IDs to skip, e.g. ['phone', 'api_locations'] 647 * opts.scroll â false to skip the scroll-to-first-error 648 * opts.toast â false to suppress the toast (caller will show its own) 649 */ 650(function ($) { 651 if (!$) { return; } 652 653 function l10nVar(key, fallback) { 654 var bags = [ 655 window.jobsearch_plugin_vars, 656 window.jobsearch_dashboard_vars, 657 window.jobsearch_posting_vars, 658 window.jobsearch_admin_vars 659 ]; 660 for (var i = 0; i < bags.length; i++) { 661 if (bags[i] && bags[i][key]) { return bags[i][key]; } 662 } 663 return fallback; 664 } 665 666 function humanize(s) { 667 if (!s) { return ''; } 668 return String(s) 669 .replace(/^(jobsearch[_-])?/, '') 670 .replace(/_field$|^u_/, '') 671 .replace(/[_\-]+/g, ' ') 672 .replace(/\b\w/g, function (c) { return c.toUpperCase(); }); 673 } 674 675 function cleanLabelText($label) { 676 if (!$label || !$label.length) { return ''; } 677 return $.trim( 678 $label.clone() 679 .children('.help-icon-con, .jobsearch-tooltipcon, .chk-loder') 680 .remove() 681 .end() 682 .text() 683 .replace(/\*+\s*$/, '') 684 .replace(/\s+/g, ' ') 685 ); 686 } 687 688 // Extract a *real* human-readable label for the field. Lookup order: 689 // 1. <label for="<this field's id>"> anywhere on the page 690 // â works even when the label is positioned outside the field's 691 // container (sidebar, sticky help panel, etc.). 692 // 2. First <label> inside the field's closest container 693 // (<li>, .form-row, .form-group, .jobsearch-form-row, 694 // .jobsearch-depndetfield-con). 695 // 3. data-label attribute on the field itself. 696 // Returns '' if none of those yielded a label. The caller treats '' 697 // as the signal to fall back to the generic "Please fill the 698 // required fields." toast â we deliberately don't fabricate labels 699 // from placeholder text or humanized name attrs because that 700 // produced unreadable noise like "Sector Multi" for many fields. 701 function labelFor($el) { 702 var id = $el.attr('id'); 703 if (id) { 704 var $byFor = $('label[for="' + id.replace(/"/g, '\\"') + '"]').first(); 705 var byForText = cleanLabelText($byFor); 706 if (byForText) { return byForText; } 707 } 708 var $container = $el.closest('li, .form-row, .form-group, .jobsearch-form-row, .jobsearch-depndetfield-con'); 709 if (!$container.length) { $container = $el.parent(); } 710 var containerText = cleanLabelText($container.find('label').first()); 711 if (containerText) { return containerText; } 712 return $.trim($el.attr('data-label') || ''); 713 } 714 715 716 // Apply the legacy red-border treatment so existing CSS targeting these 717 // exact rules keeps working. Selects get their .jobsearch-profile-select 718 // parent flagged (covers Select2 visible container); plain inputs get the 719 // border directly. 720 function markInvalid($el) { 721 var $parentSelect = $el.closest('.jobsearch-profile-select'); 722 if ($parentSelect.length) { 723 $parentSelect.css({ 'border': '1px solid #ff0000' }); 724 } else { 725 $el.css({ 'border': '1px solid #ff0000' }); 726 } 727 } 728 729 function clearInvalid($el) { 730 var $parentSelect = $el.closest('.jobsearch-profile-select'); 731 if ($parentSelect.length) { 732 $parentSelect.removeAttr('style'); 733 } else { 734 $el.css({ 'border': '' }); 735 } 736 } 737 738 function isVisible($el) { 739 // jQuery :visible is too eager â required fields inside hidden tabs 740 // or collapsed accordions still need to validate. Just check for 741 // explicit display:none / hidden type. 742 if (!$el.length) { return false; } 743 if ($el.attr('type') === 'hidden') { return false; } 744 var el = $el[0]; 745 if (el && (el.offsetParent === null) && $el.css('position') !== 'fixed') { 746 // hidden via display:none somewhere up the tree 747 return false; 748 } 749 return true; 750 } 751 752 function isEmpty(val) { 753 if (val === undefined || val === null) { return true; } 754 if (Array.isArray(val)) { return val.length === 0; } 755 return $.trim(String(val)) === ''; 756 } 757 758 // Surface HTML5 [required] failures as a top-right toast alongside the 759 // native browser tooltip. When the browser rejects a submit because of 760 // a missing [required] field, the form's `submit` event never fires â 761 // so the JS submit handler (and its missing-fields toast call) is 762 // skipped. We attach an `invalid` event listener instead: each 763 // invalid input fires its own `invalid` event during the 764 // pre-submit validity check, and we aggregate them into one toast. 765 // 766 // `invalid` does not bubble, so capture-phase delegation is required. 767 // First invalid of a batch schedules a microtask that builds one 768 // toast for every invalid field seen in this pass. The HTML5 769 // tooltip + the toast both render; nothing about native validation 770 // is suppressed. 771 // Toast message strategy: 772 // - If labelFor() yielded a real <label>
772 / data-label string for 773 // EVERY missing field, we render the comma-list: 774 // "Required fields: Email, Phone" 775 // - As soon as any one field can't surface a clean label, we drop 776 // the list and emit the generic message: 777 // "Please fill the required fields." 778 // This avoids "Required fields: Email, ?, ?" style noise â when 779 // even one label is unknown, listing the rest is misleading 780 // because the user can't reconcile it with what's red on screen. 781 function buildMissingMessage(labels) { 782 if (!labels || !labels.length) { return ''; } 783 var allHaveLabel = true; 784 for (var i = 0; i < labels.length; i++) { 785 if (!labels[i] || labels[i] === '?') { allHaveLabel = false; break; } 786 } 787 if (!allHaveLabel) { 788 return l10nVar('fill_nec_fields', 'Please fill the required fields.'); 789 } 790 var cap = 6; 791 var shown = labels.slice(0, cap); 792 var prefix = l10nVar('required_fields_label', 'Required fields: '); 793 var moreTpl = l10nVar('and_n_more', ' and %d more'); 794 var msg = prefix + shown.join(', '); 795 if (labels.length > cap) { 796 msg += moreTpl.replace('%d', labels.length - cap); 797 } 798 return msg; 799 } 800 801 function attachInvalidToast(formEl) { 802 if (!formEl || formEl.__jobsearchInvalidBound) { return; } 803 formEl.__jobsearchInvalidBound = true; 804 var pendingTimer = null; 805 var pending = []; 806 formEl.addEventListener('invalid', function (ev) { 807 var el = ev.target; 808 if (!el || pending.indexOf(el) !== -1) { return; } 809 pending.push(el); 810 if (pendingTimer) { return; } 811 pendingTimer = window.setTimeout(function () { 812 pendingTimer = null; 813 var batch = pending.slice(); 814 pending = []; 815 var labels = []; 816 batch.forEach(function (it) { 817 var lbl = labelFor($(it)); 818 // Push '?' for unlabeled fields so buildMissingMessage 819 // sees the incomplete list and flips to generic. If 820 // we silently skipped them, the toast would lie about 821 // how many fields are missing. 822 if (!lbl) { lbl = '?'; } 823 if (labels.indexOf(lbl) === -1) { 824 labels.push(lbl); 825 } 826 }); 827 var msg = buildMissingMessage(labels); 828 if (msg && window.jobsearchAlert) { 829 window.jobsearchAlert('error', msg); 830 } 831 }, 0); 832 }, true); 833 } 834 835 $(function () { 836 $('form#employer-profilesetings-form,' 837 + 'form#candidate-profilesetings-form,' 838 + 'form#job-posting-form' 839 ).each(function () { attachInvalidToast(this); }); 840 }); 841 842 // Live border clearing: as soon as the user types something into a 843 // flagged input or picks a value on a flagged select / checkbox, drop 844 // the red border so it doesn't linger after they've fixed the field. 845 // Delegated against document so it covers dynamically-added fields 846 // (Select2 re-renders, AJAX-loaded location dropdowns, etc.). 847 var liveClearSelector = 848 'form#employer-profilesetings-form,' 849 + 'form#candidate-profilesetings-form,' 850 + 'form#job-posting-form,' 851 // Resume-add modals (education / experience / skill / language / 852 // award / portfolio variants) â their <input>s aren't inside a 853 // <form>, the wrapper is a popup div, so we scope the live-clear 854 // behavior to it explicitly. 855 + '.jobsearch-add-resume-item-popup'; 856 857 $(document).on('input change', liveClearSelector + ' input, ' 858 + liveClearSelector + ' select, ' 859 + liveClearSelector + ' textarea', function () { 860 var $el = $(this); 861 var type = ($el.attr('type') || '').toLowerCase(); 862 // Checkbox/radio handled by the group block below. 863 if (type === 'checkbox' || type === 'radio') { return; } 864 var val = $el.val(); 865 if (val === null || val === undefined || $.trim(String(val)) === '') { return; } 866 clearInvalid($el); 867 // Editor and file-upload wrappers carry the border on a parent; 868 // those wrappers are also cleared so the visual state matches the 869 // field state. 870 $el.closest('.wp-editor-container, .jobsearch-fileUpload').removeAttr('style'); 871 });
872 873 // Checkbox groups: one checked box clears the group's red border. 874 $(document).on('change', liveClearSelector + ' .cusfield-checkbox-required input[type=checkbox]', function () { 875 var $group = $(this).closest('.cusfield-checkbox-required'); 876 if ($group.find('input[type=checkbox]:checked').length > 0) { 877 $group.removeAttr('style'); 878 } 879 }); 880 881 // TinyMCE Visual-mode live border clear. The underlying <textarea> 882 // is display:none while Visual mode is active and only syncs on 883 // submit, so the input/change handlers above never fire while the 884 // user types in the iframe. Hooking TinyMCE's own editor events 885 // gives us per-keystroke updates from inside the iframe document. 886 function bindTinyMCEEditorBorderClear(editor) { 887 if (!editor || editor.__jobsearchBorderBound) { return; } 888 editor.__jobsearchBorderBound = true; 889 var handler = function () { 890 // getContent({format:'text'}) returns the visible text only 891 // (no <p>, no ), so an "empty" editor with stray 892 // paragraph tags doesn't keep the border in place. 893 var txt = ''; 894 try { txt = editor.getContent({ format: 'text' }); } catch (e) { return; } 895 if (!$.trim(String(txt))) { return; } 896 var $ta = $('#' + editor.id); 897 var $container = $ta.closest('.wp-editor-container'); 898 if ($container.length) { 899 $container.removeAttr('style'); 900 } 901 }; 902 editor.on('input change keyup NodeChange', handler); 903 } 904 905 function attachTinyMCEBorderClear() { 906 if (typeof window.tinymce === 'undefined' || !window.tinymce) { 907 // TinyMCE script not yet loaded â try again shortly. 908 // Bounded retry to avoid an infinite loop on pages with no 909 // editor (in which case tinymce never loads at all). 910 attachTinyMCEBorderClear.retries = (attachTinyMCEBorderClear.retries || 0) + 1; 911 if (attachTinyMCEBorderClear.retries < 20) { 912 window.setTimeout(attachTinyMCEBorderClear, 500); 913 } 914 return; 915 } 916 if (window.tinymce.editors && window.tinymce.editors.length) { 917 for (var i = 0; i < window.tinymce.editors.length; i++) { 918 bindTinyMCEEditorBorderClear(window.tinymce.editors[i]); 919 } 920 } 921 // Late-initialized editors (e.g. an editor added by an AJAX 922 // dialog after page load) â bind once they fire AddEditor. 923 if (typeof window.tinymce.on === 'function' && !window.tinymce.__jobsearchAddListener) { 924 window.tinymce.__jobsearchAddListener = true; 925 window.tinymce.on('AddEditor', function (e) { 926 bindTinyMCEEditorBorderClear(e && e.editor); 927 }); 928 } 929 } 930 931 $(function () { attachTinyMCEBorderClear(); }); 932 933 window.jobsearchValidateRequired = function ($form, opts) { 934 opts = opts || {}; 935 var skip = {}; 936 ($.isArray(opts.skip) ? opts.skip : []).forEach(function (k) { skip[k] = 1; }); 937 var missing = []; // [{ $el, label }] 938 var addMissing = function ($el, label) { 939 if (!label) { label = labelFor($el); } 940 if (!label) { label = '?'; } 941 // Dedupe by label so a sector field doesn't appear twice if 942 // both .profile-req-field and an underlying [required] caught it. 943 for (var i = 0; i < missing.length; i++) { 944 if (missing[i].label === label) { return; } 945 } 946 missing.push({ $el: $el, label: label }); 947 markInvalid($el); 948 }; 949 950 // 1. Intl-tel-input phone error state â no per-field label here, the 951 // error class itself is the signal. We surface a generic "Phone". 952 if (!skip.phone) { 953 var $phone = $form.find('input.phone-input-error').filter(':visible'); 954 if ($phone.length) { 955 addMissing($phone.first(), labelFor($phone.first(), 'phone')); 956 } 957 } 958 959 // 2. API location selects gated by the same flags the legacy block used. 960 if (!skip.api_locations) { 961 var pv = window.jobsearch_plugin_vars || {}; 962 var postv = window.jobsearch_posting_vars || {}; 963 var loc_type = pv.locations_type || postv.locations_type; 964 var req_api = pv.required_api_locs || postv.required_api_locs; 965 var switch_loc = postv.switch_location_fields || pv.switch_location_fields; 966 // Settings forms: api + required_api_locs=yes. 967 // Job posting: api + required_api_locs=yes (OR manual) + switch_location_fields=on. 968 var settings_gate = (loc_type === 'api' && req_api === 'yes'); 969 var posting_gate = ((loc_type === 'api' && req_api === 'yes') || loc_type === 'manual') && switch_loc === 'on'; 970 if (settings_gate || posting_gate) { 971 ['jobsearch_field_location_location1', 972 'jobsearch_field_location_location2', 973 'jobsearch_field_location_location3'].forEach(function (name) { 974 var $sel = $form.find('select[name="' + name + '"]'); 975 if ($sel.length && isEmpty($sel.val())) { 976 addMissing($sel); 977 } else if ($sel.length) { 978 clearInvalid($sel); 979 } 980 }); 981 } 982 } 983 984 // 3. Sector multi-select. 985 if (!skip.sector) { 986 var $sector = $form.find('select[name^="user_sector"]').filter('.profile-req-field'); 987 if ($sector.length && isEmpty($sector.val())) { 988 addMissing($sector); 989 } else if ($sector.length) { 990 clearInvalid($sector); 991 } 992 } 993 994 // 4. TinyMCE-backed required text editors. 995 if (!skip.editor) { 996 $form.find('.jobsearch-reqtext-editor').each(function () { 997 var $ed = $(this); 998 var $container = $ed.closest('.wp-editor-container'); 999 if (isEmpty($ed.val())) { 1000 if ($container.length) { $container.css({ 'border': '1px solid #ff0000' }); } 1001 addMissing($ed, labelFor($ed)); 1002 } else if ($container.length) { 1003 $container.removeAttr('style'); 1004 } 1005 }); 1006 } 1007 1008 // 5. Required-checkbox groups â at least one checkbox in the group 1009 // must be checked. The group container itself gets the label. 1010 if (!skip.checkbox_group) { 1011 $form.find('.cusfield-checkbox-required').each(function () { 1012 var $group = $(this); 1013 var $boxes = $group.find('input[type=checkbox]'); 1014 if (!$boxes.length) { return; } 1015 var anyChecked = false; 1016 $boxes.each(function () { if ($(this).is(':checked')) { anyChecked = true; } }); 1017 if (!anyChecked) { 1018 $group.css({ 'border': '1px solid #ff0000' }); 1019 addMissing($group, labelFor($group)); 1020 } else { 1021 $group.removeAttr('style'); 1022 } 1023 }); 1024 } 1025 1026 // 6. Required file attachments. 1027 if (!skip.file_attach) { 1028 $form.find('input[type=file].jobsearch-cusfieldatt-req').each(function () { 1029 var $f = $(this); 1030 var $wrap = $f.parent('.jobsearch-fileUpload'); 1031 if (isEmpty($f.val())) { 1032 if ($wrap.length) { $wrap.css({ 'border': '1px solid #ff0000' }); } 1033 addMissing($f, labelFor($f));
1034 } else if ($wrap.length) { 1035 $wrap.css({ 'border': 'none' }); 1036 } 1037 }); 1038 } 1039 1040 // 7. Generic markers: HTML5 [required], .profile-req-field, 1041 // .multiselect-req. Reading .val() is widget-agnostic so this 1042 // catches Select2, Selectize, and native <select> alike. 1043 if (!skip.generic) { 1044 var $generic = $form.find('[required], .profile-req-field, .multiselect-req'); 1045 $generic.each(function () { 1046 var $el = $(this); 1047 if (!isVisible($el)) { return; } 1048 // Skip checkboxes/radios with [required] â they're handled 1049 // by the group check (5) above when relevant. 1050 var type = ($el.attr('type') || '').toLowerCase(); 1051 if (type === 'checkbox' || type === 'radio') { return; } 1052 if (isEmpty($el.val())) { 1053 addMissing($el); 1054 } else { 1055 clearInvalid($el); 1056 } 1057 }); 1058 } 1059 1060 if (missing.length === 0) { return true; } 1061 1062 // Toast with the missing labels (cap at 6 to keep the message readable). 1063 if (opts.toast !== false && window.jobsearchAlert) { 1064 var msg = buildMissingMessage(missing.map(function (m) { return m.label; })); 1065 if (msg) { window.jobsearchAlert('error', msg); } 1066 } 1067 1068 if (opts.scroll !== false) { 1069 var $first = missing[0].$el; 1070 var $scrollTo = $first.closest('.jobsearch-profile-select, .wp-editor-container, .jobsearch-fileUpload, .cusfield-checkbox-required'); 1071 if (!$scrollTo.length) { $scrollTo = $first; } 1072 if ($scrollTo.length && $scrollTo.offset()) { 1073 $('html, body').animate({ scrollTop: $scrollTo.offset().top - 110 }, 600); 1074 } 1075 } 1076 1077 return false; 1078 }; 1079 1080 // Public exports so other plugin scripts (legacy job-posting validator 1081 // in particular) can reuse the same label extraction + message 1082 // building logic. Keeps the toast format consistent across both the 1083 // new validator and the older field-by-field validators that pre-date 1084 // jobsearchValidateRequired(). 1085 window.jobsearchFieldLabel = labelFor; 1086 window.jobsearchBuildMissingMessage = buildMissingMessage; 1087})(window.jQuery); 1088 1089/** 1090 * Chunked applicants Excel export â popup, state machine and chunk loop. 1091 * Drives all six contexts via the clicked button's data-export-context 1092 * attribute. Flow: status -> (resume | configure) -> chunk loop -> 1093 * finalize -> download. 1094 */ 1095(function ($) { 1096 if (!$ || window.jobsearchExportPopup) { return; } 1097 1098 function i18n(key, fallback) { 1099 var src = window.jobsearch_export_i18n || {}; 1100 return (typeof src[key] === 'string' && src[key] !== '') ? src[key] : fallback; 1101 } 1102 1103 function ajaxUrl() { 1104 if (window.jobsearch_plugin_vars && window.jobsearch_plugin_vars.ajax_url) { 1105 return window.jobsearch_plugin_vars.ajax_url; 1106 } 1107 if (typeof window.ajaxurl !== 'undefined' && window.ajaxurl) { 1108 return window.ajaxurl; 1109 } 1110 return '/wp-admin/admin-ajax.php'; 1111 } 1112 1113 function ajaxNonce() { 1114 if (window.jobsearch_comon_script_vars && window.jobsearch_comon_script_vars.nonce) { 1115 return window.jobsearch_comon_script_vars.nonce; 1116 } 1117 return ''; 1118 } 1119 1120 function collectFilterArgs($btn) { 1121 // Read the LIVE filter controls so the export honours the current 1122 // job / employer / date selections without requiring a Submit/reload 1123 // first. The selects are identified by data-posttype (consistent 1124 // across all contexts); employer select is absent on the dashboard. 1125 var args = {}; 1126 var $scope = $btn.closest('.filter-allreps-mainholdr'); 1127 if (!$scope.length) { $scope = $(document); } 1128 1129 var $job = $scope.find('select[data-posttype="job"]').first(); 1130 if ($job.length && $job.val()) { args.job_id = $job.val(); } 1131 1132 var $emp = $scope.find('select[data-posttype="employer"]').first(); 1133 if ($emp.length && $emp.val()) { args.employer_id = $emp.val(); } 1134 1135 var dfrom = $scope.find('input[name="alljobs_wafrom_date"]').val(); 1136 if (dfrom) { args.date_from = dfrom; } 1137 1138 var dto = $scope.find('input[name="alljobs_wato_date"]').val(); 1139 if (dto) { args.date_to = dto; } 1140 1141 return args; 1142 } 1143 1144 function formatInt(n) { 1145 n = parseInt(n, 10) || 0; 1146 return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); 1147 } 1148 1149 function ExportSession(context) { 1150 this.context = context; 1151 this.prefix = 'jobsearch_export_' + context; 1152 this.token = ''; 1153 this.total = 0; 1154 this.offset = 0; 1155 this.chunkSize = 30000; 1156 this.aborted = false; 1157 this.filterArgs = {}; 1158 } 1159 1160 ExportSession.prototype.action = function (suffix) { 1161 return this.prefix + '_' + suffix; 1162 }; 1163 1164 ExportSession.prototype.post = function (suffix, extra) { 1165 var data = $.extend({ 1166 action: this.action(suffix), 1167 _nonce: ajaxNonce() 1168 }, extra || {}); 1169 return $.ajax({ url: ajaxUrl(), type: 'POST', dataType: 'json', data: data }); 1170 }; 1171 1172 function buildModal() { 1173 var $existing = $('#jobsearch-export-modal'); 1174 if ($existing.length) { $existing.remove(); } 1175 1176 var sheetIcon = '<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline>
1176<line x1="8" y1="13" x2="16" y2="13"></line><line x1="8" y1="17" x2="16" y2="17"></line></svg>'; 1177 1178 var html = '' 1179 + '<div class="jobsearch-export-modal" id="jobsearch-export-modal">' 1180 + ' <div class="jobsearch-export-overlay"></div>' 1181 + ' <div class="jobsearch-export-box" role="dialog" aria-modal="true">' 1182 + ' <button type="button" class="jobsearch-export-close" aria-label="' + i18n('close', 'Close') + '">×</button>' 1183 1184 + ' <div class="export-state state-loading">' 1185 + ' <div class="jsx-spinner"></div>' 1186 + ' <p class="jsx-muted export-loading-text">' + i18n('loading', 'Loadingâ¦') + '</p>' 1187 + ' </div>' 1188 1189 + ' <div class="export-state state-resume">' 1190 + ' <div class="jsx-head"><span class="jsx-badge jsx-badge-amber">' + sheetIcon + '</span><h3>' + i18n('in_progress_title', 'Export in progress') + '</h3></div>' 1191 + ' <p class="jsx-muted export-resume-text"></p>' 1192 + ' <div class="export-actions">' 1193 + ' <button type="button" class="jsx-btn jsx-btn-primary export-resume-btn">' + i18n('resume', 'Resume') + '</button>' 1194 + ' <button type="button" class="jsx-btn jsx-btn-ghost export-startfresh-btn">' + i18n('start_fresh', 'Start Fresh') + '</button>' 1195 + ' </div>' 1196 + ' </div>' 1197 1198 + ' <div class="export-state state-configure">' 1199 + ' <div class="jsx-head"><span class="jsx-badge jsx-badge-green">' + sheetIcon + '</span><h3>' + i18n('export_title', 'Export to Excel') + '</h3></div>' 1200 + ' <div class="jsx-stat-card">' 1201 + ' <span class="jsx-stat-label">' + i18n('total_applicants', 'Total Applicants') + '</span>' 1202 + ' <span class="jsx-stat-value export-total-num">0</span>' 1203 + ' </div>' 1204 + ' <div class="jsx-field">' 1205 + ' <label class="jsx-field-label">' + i18n('chunk_size', 'Chunk size') + '</label>' 1206 + ' <input type="number" class="jsx-input export-chunk-input" value="30000" min="100" step="100">' 1207 + ' </div>' 1208 + ' <div class="export-actions">' 1209 + ' <button type="button" class="jsx-btn jsx-btn-primary export-start-btn">' + i18n('start_export', 'Start Export') + '</button>' 1210 + ' <button type="button" class="jsx-btn jsx-btn-ghost export-cancel-btn">' + i18n('cancel', 'Cancel') + '</button>' 1211 + ' </div>' 1212 + ' </div>' 1213 1214 + ' <div class="export-state state-progress">' 1215 + ' <div class="jsx-head"><h3>' + i18n('exporting', 'Exportingâ¦') + '</h3></div>' 1216 + ' <div class="jsx-percent"><span class="export-percent-num">0</span><span class="jsx-percent-sign">%</span></div>' 1217 + ' <div class="jobsearch-export-progress-bar"><div class="jobsearch-export-progress-fill"></div></div>' 1218 + ' <p class="jsx-muted export-progress-text">0 / 0</p>' 1219 + ' <div class="export-actions">' 1220 + ' <button type="button" class="jsx-btn jsx-btn-ghost export-cancel-btn">' + i18n('cancel', 'Cancel') + '</button>' 1221 + ' </div>' 1222 + ' </div>' 1223 1224 + ' <div class="export-state state-done">' 1225 + ' <div class="jsx-badge jsx-badge-green jsx-badge-lg jsx-pop"><svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg></div>' 1226 + ' <h3>' + i18n('complete', 'Export complete') + '</h3>' 1227 + ' <p class="jsx-muted">' + i18n('downloading', 'Downloadingâ¦') + '</p>' 1228 + ' </div>' 1229 1230 + ' <div class="export-state state-error">' 1231 + ' <div class="jsx-badge jsx-badge-red jsx-badge-lg"><svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="8" x2="12" y2="13"></line>
1231<line x1="12" y1="17" x2="12.01" y2="17"></line><circle cx="12" cy="12" r="10"></circle></svg></div>' 1232 + ' <p class="export-error-text"></p>' 1233 + ' <div class="export-actions">' 1234 + ' <button type="button" class="jsx-btn jsx-btn-ghost export-close-btn">' + i18n('close', 'Close') + '</button>' 1235 + ' </div>' 1236 + ' </div>' 1237 + ' </div>' 1238 + '</div>'; 1239 1240 var $modal = $(html); 1241 $('body').append($modal); 1242 return $modal; 1243 } 1244 1245 function setState($modal, state) { 1246 $modal.removeClass(function (i, cls) { 1247 return (cls.match(/(^|\s)state-active-\S+/g) || []).join(' '); 1248 }); 1249 $modal.attr('data-state', state); 1250 $modal.find('.export-state').hide(); 1251 $modal.find('.state-' + state).show(); 1252 } 1253 1254 var ctrl = {}; 1255 1256 ctrl.open = function (btn) { 1257 var $btn = $(btn); 1258 var context = $btn.data('export-context'); 1259 if (!context) { return; } 1260 1261 var session = new ExportSession(context); 1262 session.filterArgs = collectFilterArgs($btn); 1263 1264 var $modal = buildModal(); 1265 bindModal($modal, session); 1266 $modal.addClass('open'); 1267 setState($modal, 'loading'); 1268 1269 // Status check first â offers resume if an export is in flight. 1270 session.post('status').done(function (resp) { 1271 if (resp && resp.success && resp.data && resp.data.in_progress) { 1272 var d = resp.data; 1273 session.token = d.token || ''; 1274 session.total = parseInt(d.total, 10) || 0; 1275 session.offset = parseInt(d.offset, 10) || 0; 1276 session.chunkSize = parseInt(d.chunk_size, 10) || 30000; 1277 // Cap both: offset advances by chunk_size and can exceed total. 1278 var done = Math.min(session.offset, session.total); 1279 var pct = session.total > 0 ? Math.min(100, Math.round((done / session.total) * 100)) : 0; 1280 var tpl = i18n('resume_text', 'You have an export in progress (%s%% complete).'); 1281 $modal.find('.export-resume-text').text(tpl.replace('%s', pct).replace(/%%/g, '%')); 1282 setState($modal, 'resume'); 1283 } else { 1284 showConfigure($modal, session); 1285 } 1286 }).fail(function () { 1287 showConfigure($modal, session); 1288 }); 1289 }; 1290 1291 function showConfigure($modal, session) { 1292 setState($modal, 'loading'); 1293 session.post('init', session.filterArgs).done(function (resp) { 1294 if (resp && resp.success && resp.data) { 1295 session.token = resp.data.token; 1296 session.total = parseInt(resp.data.total, 10) || 0; 1297 session.offset = 0; 1298 $modal.find('.export-total-num').text(formatInt(session.total)); 1299 $modal.find('.export-chunk-input').val(session.chunkSize); 1300 setState($modal, 'configure'); 1301 } else { 1302 showError($modal, (resp && resp.data && resp.data.msg) ? resp.data.msg 1303 : i18n('no_applicants', 'No applicants match the selected filters.')); 1304 } 1305 }).fail(function () { 1306 showError($modal, i18n('generic_error', 'Something went wrong. Please try again.')); 1307 }); 1308 } 1309 1310 function showError($modal, msg) { 1311 $modal.find('.export-error-text').text(msg || i18n('generic_error', 'Something went wrong. Please try again.')); 1312 setState($modal, 'error'); 1313 } 1314 1315 function runChunkLoop($modal, session) { 1316 if (session.aborted) { return; } 1317 setState($modal, 'progress'); 1318 // is-working animates the bar's stripes while the chunk is in flight. 1319 $modal.find('.jobsearch-export-progress-bar').addClass('is-working'); 1320 1321 session.post('chunk', { 1322 token: session.token, 1323 offset: session.offset, 1324 chunk_size: session.chunkSize 1325 }).done(function (resp) { 1326 if (session.aborted) { return; } 1327 if (!resp || !resp.success || !resp.data) { 1328 $modal.find('.jobsearch-export-progress-bar').removeClass('is-working'); 1329 showError($modal, (resp && resp.data && resp.data.msg) ? resp.data.msg 1330 : i18n('generic_error', 'Something went wrong. Please try again.')); 1331 return; 1332 } 1333 var d = resp.data; 1334 session.offset = parseInt(d.offset, 10) || session.offset; 1335 var pct = parseFloat(d.percent) || 0; 1336 updateProgress($modal, session, pct); 1337 1338 if (d.done) { 1339 $modal.find('.jobsearch-export-progress-bar').removeClass('is-working'); 1340 finalizeExport($modal, session); 1341 } else { 1342 runChunkLoop($modal, session); 1343 } 1344 }).fail(function () { 1345 if (session.aborted) { return; } 1346 $modal.find('.jobsearch-export-progress-bar').removeClass('is-working'); 1347 showError($modal, i18n('generic_error', 'Something went wrong. Please try again.')); 1348 }); 1349 } 1350 1351 function updateProgress($modal, session, pct) { 1352 pct = Math.min(100, Math.max(0, pct)); 1353 var shown = Math.min(session.offset, session.total); 1354 // Floor so it never reads 100% before finalize flips to the done state. 1355 var pctShown = (pct >= 100) ? 100 : Math.floor(pct); 1356 $modal.find('.jobsearch-export-progress-fill').css('width', pct + '%'); 1357 $modal.find('.export-percent-num').text(pctShown); 1358 $modal.find('.export-progress-text').text(formatInt(shown) + ' / ' + formatInt(session.total)); 1359 } 1360 1361 function finalizeExport($modal, session) { 1362 session.post('finalize', { token: session.token }).done(function (resp) { 1363 if (resp && resp.success && resp.data && resp.data.download_url) { 1364 setState($modal, 'done'); 1365 window.location.href = resp.data.download_url; 1366 setTimeout(function () { closeModal($modal); }, 1500); 1367 } else { 1368 showError($modal, (resp && resp.data && resp.data.msg) ? resp.data.msg 1369 : i18n('generic_error', 'Something went wrong. Please try again.')); 1370 } 1371 }).fail(function () { 1372 showError($modal, i18n('generic_error', 'Something went wrong. Please try again.')); 1373 }); 1374 } 1375 1376 function abortExport(session) { 1377 session.aborted = true; 1378 if (session.token) { 1379 session.post('abort', { token: session.token }); 1380 } 1381 } 1382 1383 function closeModal($modal) { 1384 $modal.removeClass('open'); 1385 setTimeout(function () { $modal.remove(); }, 200); 1386 } 1387 1388 function bindModal($modal, session) {
1389 $modal.on('click', '.export-start-btn', function () { 1390 var size = parseInt($modal.find('.export-chunk-input').val(), 10); 1391 if (!size || size < 1) { size = 30000; } 1392 session.chunkSize = size; 1393 session.offset = 0; 1394 runChunkLoop($modal, session); 1395 }); 1396 1397 $modal.on('click', '.export-resume-btn', function () { 1398 runChunkLoop($modal, session); 1399 }); 1400 1401 $modal.on('click', '.export-startfresh-btn', function () { 1402 if (session.token) { 1403 session.post('abort', { token: session.token }); 1404 } 1405 session.token = ''; 1406 session.offset = 0; 1407 showConfigure($modal, session); 1408 }); 1409 1410 $modal.on('click', '.export-cancel-btn', function () { 1411 abortExport(session); 1412 closeModal($modal); 1413 }); 1414 1415 $modal.on('click', '.export-close-btn, .jobsearch-export-close, .jobsearch-export-overlay', function () { 1416 var state = $modal.attr('data-state'); 1417 if (state === 'progress') { 1418 abortExport(session); 1419 } 1420 closeModal($modal); 1421 }); 1422 } 1423 1424 // Delegated so it survives AJAX re-renders of the applicants lists. 1425 $(document).on('click', '.allplics-export-excel[data-export-context]', function (e) { 1426 e.preventDefault(); 1427 ctrl.open(this); 1428 }); 1429 1430 window.jobsearchExportPopup = ctrl; 1431})(window.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.