1(function () { 2 "use strict"; 3 4 var ACTION_ENDPOINT = "/webservices/job_favorite_action.php"; 5 var STATUS_ENDPOINT = "/webservices/job_favorites_status.php"; 6 var stateByJobId = Object.create(null); 7 8 function toBool(value) { 9 return value === true || value === 1 || value === "1" || value === "true"; 10 } 11 12 function collectButtons() { 13 return Array.prototype.slice.call(document.querySelectorAll(".hp-favorite-toggle[data-job-id]")); 14 } 15 16 function buttonsForJob(jobId) { 17 return collectButtons().filter(function (btn) { 18 return btn.getAttribute("data-job-id") === jobId; 19 }); 20 } 21 22 function setButtonState(btn, isFavorite) { 23 var addLabel = btn.getAttribute("data-label-add") || "Ulozit"; 24 var removeLabel = btn.getAttribute("data-label-remove") || "Ulozeno"; 25 btn.setAttribute("data-is-favorite", isFavorite ? "1" : "0"); 26 btn.setAttribute("aria-pressed", isFavorite ? "true" : "false"); 27 btn.textContent = isFavorite ? removeLabel : addLabel; 28 29 var wrap = btn.closest(".hp-favorite-toggle-wrap"); 30 if (wrap) { 31 var hint = wrap.querySelector(".hp-favorite-hint"); 32 if (hint) { 33 hint.hidden = !!isFavorite; 34 } 35 var feedback = wrap.querySelector(".hp-favorite-feedback"); 36 if (feedback) { 37 if (feedback.__hideTimer) { 38 clearTimeout(feedback.__hideTimer); 39 feedback.__hideTimer = null; 40 } 41 if (isFavorite) { 42 feedback.hidden = false; 43 requestAnimationFrame(function () { 44 feedback.classList.add("is-visible"); 45 }); 46 } else { 47 feedback.classList.remove("is-visible"); 48 feedback.__hideTimer = setTimeout(function () { 49 feedback.hidden = true; 50 }, 170); 51 } 52 } 53 } 54 } 55 56 function pulseButtonsForJob(jobId) { 57 buttonsForJob(jobId).forEach(function (btn) { 58 btn.classList.remove("is-pop"); 59 void btn.offsetWidth; 60 btn.classList.add("is-pop"); 61 setTimeout(function () { 62 btn.classList.remove("is-pop"); 63 }, 420); 64 }); 65 } 66 67 function syncJobState(jobId, isFavorite) { 68 stateByJobId[jobId] = !!isFavorite; 69 buttonsForJob(jobId).forEach(function (btn) { 70 setButtonState(btn, !!isFavorite); 71 }); 72 } 73 74 function setJobBusy(jobId, isBusy) { 75 buttonsForJob(jobId).forEach(function (btn) { 76 btn.disabled = !!isBusy; 77 }); 78 } 79 80 async function loadFavoriteStatesForPage(jobIds) { 81 if (!jobIds.length) return; 82 try { 83 var res = await fetch(STATUS_ENDPOINT, { 84 method: "POST", 85 headers: { "Content-Type": "application/json" }, 86 body: JSON.stringify({ job_ids: jobIds }) 87 }); 88 var data = await res.json(); 89 if (!res.ok || !data || data.ok !== true) return; 90 var favSet = Object.create(null); 91 var list = data.favorited_ids || []; 92 for (var i = 0; i < list.length; i++) { 93 favSet[list[i]] = true; 94 } 95 for (var j = 0; j < jobIds.length; j++) { 96 var jid = jobIds[j]; 97 syncJobState(jid, !!favSet[jid]); 98 } 99 } catch (e) { 100 // keep server-rendered defaults 101 } 102 } 103 104 async function saveJobState(jobId, nextFavorite) { 105 var res = await fetch(ACTION_ENDPOINT, { 106 method: "POST", 107 headers: { "Content-Type": "application/json" }, 108 body: JSON.stringify({ job_id: jobId, favorite: !!nextFavorite }) 109 }); 110 var data = await res.json(); 111 if (!res.ok || !data || data.ok !== true) { 112 throw new Error((data && data.error) || "save_failed"); 113 } 114 return toBool(data.is_favorite); 115 } 116 117 function pushDataLayerEvent(jobId, isFavorite) { 118 window.dataLayer = window.dataLayer || []; 119 window.dataLayer.push({ 120 event: isFavorite ? "job_favorite_add" : "job_favorite_remove", 121 job_id: jobId 122 }); 123 } 124 125 async function onToggleClick(btn) { 126 var jobId = btn.getAttribute("data-job-id"); 127 if (!jobId) return; 128 129 var current = Object.prototype.hasOwnProperty.call(stateByJobId, jobId) 130 ? !!stateByJobId[jobId] 131 : toBool(btn.getAttribute("data-is-favorite")); 132 var next = !current; 133 134 setJobBusy(jobId, true); 135 syncJobState(jobId, next); // optimistic update 136 137 try { 138 var confirmed = await saveJobState(jobId, next); 139 syncJobState(jobId, confirmed); 140 if (confirmed && !current) { 141 pulseButtonsForJob(jobId); 142 } 143 pushDataLayerEvent(jobId, confirmed); 144 } catch (e) { 145 syncJobState(jobId, current); // rollback 146 } finally { 147 setJobBusy(jobId, false); 148 } 149 } 150 151 function initFavorites() { 152 var buttons = collectButtons(); 153 if (!buttons.length) return; 154 155 var seen = Object.create(null); 156 var jobIdsOnPage = []; 157 buttons.forEach(function (btn) { 158 var jobId = btn.getAttribute("data-job-id"); 159 if (!jobId) return;
160 setButtonState(btn, toBool(btn.getAttribute("data-is-favorite"))); 161 162 btn.addEventListener("click", function () { 163 onToggleClick(btn); 164 }); 165 166 if (!seen[jobId]) { 167 seen[jobId] = true; 168 jobIdsOnPage.push(jobId); 169 } 170 }); 171 loadFavoriteStatesForPage(jobIdsOnPage); 172 } 173 174 if (document.readyState === "loading") { 175 document.addEventListener("DOMContentLoaded", initFavorites); 176 } else { 177 initFavorites(); 178 } 179})();
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.