1(function (root) { 2 'use strict'; 3 4 root.MKPwaUi = root.MKPwaUi || { layers: {} }; 5 6 root.MKPwaUi.layers.createSwBridgeLayer = function createSwBridgeLayer(context) { 7 // Keep disabled until cross-browser notice delivery and UX are settled. 8 var STALE_HOME_UI_ENABLED = context.config.staleHomeUiEnabled === true; 9 var HOME_PATH = '/'; 10 var STALE_HOME_MIN_AGE_MS = 60 * 60 * 1000; 11 var WARMUP_URL_LIMIT = 20; 12 var WARMUP_SOURCE_TIMEOUT_MS = 10 * 1000; 13 var WARMUP_SELECTORS = [ 14 '.news-listing__day-list .news-listing__item-link', 15 '.top10__item .article-preview__content' 16 ]; 17 var staleHomeBanner = null; 18 var staleHomeDismissed = false; 19 var warmupSourcePromise = null; 20 var registration = null; 21 var cacheErrorGoalSent = false; 22 var pendingCacheErrorGoal = null; 23 24 function isStandaloneClient() { 25 return !!(context.install && context.install.isStandalone && context.install.isStandalone()); 26 } 27 28 function resolveServiceWorkerTarget(preferredTarget) { 29 if (preferredTarget && typeof preferredTarget.postMessage === 'function') { 30 return preferredTarget; 31 } 32 33 if (root.navigator.serviceWorker.controller) { 34 return root.navigator.serviceWorker.controller; 35 } 36 37 if (!registration) { 38 return null; 39 } 40 41 return registration.active || null; 42 } 43 44 function postToServiceWorker(message, preferredTarget) { 45 var target = resolveServiceWorkerTarget(preferredTarget); 46 if (!target) { 47 return false; 48 } 49 50 try { 51 target.postMessage(message); 52 return true; 53 } catch (_) { 54 return false; 55 } 56 } 57 58 function announceClientReady(preferredTarget, standaloneOverride) { 59 postToServiceWorker({ 60 type: 'PWA_CLIENT_READY', 61 standalone: typeof standaloneOverride === 'boolean' ? standaloneOverride : isStandaloneClient(), 62 url: root.location.href 63 }, preferredTarget); 64 } 65 66 function requestStaleHomeNotice() { 67 if (root.location.pathname === HOME_PATH) { 68 announceClientReady(null, false); 69 } 70 } 71 72 function normalizeWarmupUrl(rawUrl) { 73 var url; 74 75 if (!rawUrl) { 76 return ''; 77 } 78 79 try { 80 url = new root.URL(rawUrl, root.location.origin); 81 } catch (_) { 82 return ''; 83 } 84 85 if (url.origin !== root.location.origin) { 86 return ''; 87 } 88 89 url.hash = ''; 90 return url.href; 91 } 92 93 function findAnchor(node) { 94 if (!node) { 95 return null; 96 } 97 98 if (String(node.tagName || '').toLowerCase() === 'a' && node.getAttribute('href')) { 99 return node; 100 } 101 102 return node.querySelector ? node.querySelector('a[href]') : null; 103 } 104 105 function extractWarmupUrls(html) { 106 var parser; 107 var doc; 108 var seen = {}; 109 var urls = []; 110 var nodes; 111 var nodeIndex; 112 var anchor; 113 var normalizedUrl; 114 115 if (typeof root.DOMParser !== 'function') { 116 throw new Error('dom_parser_unavailable'); 117 } 118 119 parser = new root.DOMParser(); 120 doc = parser.parseFromString(html, 'text/html'); 121 if (!doc || typeof doc.querySelectorAll !== 'function') { 122 throw new Error('dom_parse_failed'); 123 } 124 125 nodes = doc.querySelectorAll(WARMUP_SELECTORS.join(',')); 126 for (nodeIndex = 0; nodeIndex < nodes.length && urls.length < WARMUP_URL_LIMIT; nodeIndex += 1) { 127 anchor = findAnchor(nodes[nodeIndex]); 128 normalizedUrl = anchor ? normalizeWarmupUrl(anchor.getAttribute('href')) : ''; 129 130 if (!normalizedUrl || seen[normalizedUrl]) { 131 continue; 132 } 133 134 seen[normalizedUrl] = true; 135 urls.push(normalizedUrl); 136 } 137 138 return urls; 139 } 140 141 function reportWarmupSourceFailure(target, reason) { 142 postToServiceWorker({ 143 type: 'PWA_WARMUP_SOURCE_FAILED', 144 reason: reason || 'unknown' 145 }, target); 146 } 147 148 function provideWarmupUrls(target) { 149 var requestOptions; 150 var abortController = null; 151 var timeoutId = null; 152 153 if (!isStandaloneClient()) { 154 reportWarmupSourceFailure(target, 'client_not_standalone'); 155 return; 156 } 157 158 if (!warmupSourcePromise) { 159 requestOptions = { 160 method: 'GET', 161 cache: 'no-store', 162 credentials: 'same-origin', 163 headers: { 164 Accept: 'text/html,application/xhtml+xml' 165 } 166 }; 167 168 if (typeof root.AbortController === 'function') { 169 abortController = new root.AbortController(); 170 requestOptions.signal = abortController.signal; 171 timeoutId = root.setTimeout(function () { 172 abortController.abort(); 173 }, WARMUP_SOURCE_TIMEOUT_MS); 174 } 175 176 warmupSourcePromise = root.fetch(HOME_PATH, requestOptions).then(function (response) { 177 if (!response || !response.ok) { 178 throw new Error('http_status_' + (response ? response.status : 'no_response')); 179 } 180 return response.text(); 181 }).then(function (html) { 182 return extractWarmupUrls(html); 183 }).then(function (urls) { 184 if (timeoutId !== null) { 185 root.clearTimeout(timeoutId); 186 } 187 return urls; 188 }, function (error) { 189 if (timeoutId !== null) { 190 root.clearTimeout(timeoutId); 191 } 192 throw error; 193 }); 194 195 warmupSourcePromise.then(function () { 196 warmupSourcePromise = null; 197 }, function () { 198 warmupSourcePromise = null; 199 }); 200 } 201 202 warmupSourcePromise.then(function (urls) { 203 postToServiceWorker({ 204 type: 'PWA_WARMUP_URLS', 205 urls: urls 206 }, target); 207 }).catch(function (error) {
208 reportWarmupSourceFailure(target, String((error && error.message) || 'source_failed')); 209 }); 210 } 211 212 function formatCacheDateTime(timestamp) { 213 var date = new Date(timestamp); 214 215 function pad(value) { 216 return value < 10 ? '0' + value : String(value); 217 } 218 219 if (!isFinite(date.getTime())) { 220 return ''; 221 } 222 223 return [ 224 pad(date.getDate()), 225 pad(date.getMonth() + 1), 226 pad(date.getFullYear() % 100) 227 ].join('.') + ' ' + [ 228 pad(date.getHours()), 229 pad(date.getMinutes()) 230 ].join(':'); 231 } 232 233 function isHomeUrl(rawUrl) { 234 var url; 235 236 if (!rawUrl) { 237 return false; 238 } 239 240 try { 241 url = new root.URL(rawUrl, root.location.origin); 242 } catch (_) { 243 return false; 244 } 245 246 return url.origin === root.location.origin && url.pathname === HOME_PATH; 247 } 248 249 function setStaleHomeScrollLocked(doc, locked) { 250 var method = locked ? 'add' : 'remove'; 251 252 if (doc.documentElement && doc.documentElement.classList) { 253 doc.documentElement.classList[method]('mk-pwa-stale-home-open'); 254 } 255 if (doc.body && doc.body.classList) { 256 doc.body.classList[method]('mk-pwa-stale-home-open'); 257 } 258 } 259 260 function closeStaleHomeBanner() { 261 var banner = staleHomeBanner; 262 var previousFocus; 263 264 if (!banner) { 265 return; 266 } 267 268 staleHomeDismissed = true; 269 staleHomeBanner = null; 270 previousFocus = banner.previousFocus; 271 banner.document.removeEventListener('keydown', banner.keydownListener); 272 setStaleHomeScrollLocked(banner.document, false); 273 274 if (banner.element.parentNode) { 275 banner.element.parentNode.removeChild(banner.element); 276 } 277 278 if (previousFocus && typeof previousFocus.focus === 'function') { 279 try { 280 previousFocus.focus(); 281 } catch (_) { 282 // The previously focused node may have left the document. 283 } 284 } 285 } 286 287 function handleStaleHomeKeydown(event) { 288 var banner = staleHomeBanner; 289 var activeElement; 290 291 if (!banner || !event) { 292 return; 293 } 294 295 if (event.key === 'Escape' || event.keyCode === 27) { 296 event.preventDefault(); 297 closeStaleHomeBanner(); 298 return; 299 } 300 301 if (event.key !== 'Tab' && event.keyCode !== 9) { 302 return; 303 } 304 305 activeElement = banner.document.activeElement; 306 if (event.shiftKey && (activeElement === banner.closeButton || !banner.dialog.contains(activeElement))) { 307 event.preventDefault(); 308 banner.refreshButton.focus(); 309 return; 310 } 311 312 if (!event.shiftKey && (activeElement === banner.refreshButton || !banner.dialog.contains(activeElement))) { 313 event.preventDefault(); 314 banner.closeButton.focus(); 315 } 316 } 317 318 function ensureStaleHomeBanner() { 319 var doc = root.document; 320 var style; 321 var overlay; 322 var dialog; 323 var text; 324 var title; 325 var date; 326 var status; 327 var closeButton; 328 var refreshButton; 329 var previousFocus; 330 331 if (staleHomeBanner || !doc || !doc.body) { 332 return staleHomeBanner; 333 } 334 335 if (!doc.getElementById('mk-pwa-stale-home-style')) { 336 style = doc.createElement('style'); 337 style.id = 'mk-pwa-stale-home-style'; 338 style.textContent = [ 339 'html.mk-pwa-stale-home-open,body.mk-pwa-stale-home-open{overflow:hidden!important;}', 340 '.mk-pwa-stale-home{position:fixed;z-index:2147483647;inset:0;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:14px;background:rgba(0,0,0,.55);}', 341 '.mk-pwa-stale-home__dialog{position:relative;display:flex;flex-direction:column;align-items:center;box-sizing:border-box;width:356px;max-width:100%;min-height:161px;padding:18px 24px 16px;border-radius:8px;background:linear-gradient(91.2deg,#e13836 0%,#952524 100%);
341box-shadow:0 4px 14.1px rgba(0,0,0,.25);color:#fff;text-align:center;font-family:Arial,Helvetica,sans-serif;}', 342 '.mk-pwa-stale-home__text{margin:0;color:#fff;font-size:16px;line-height:26px;font-weight:400;}', 343 '.mk-pwa-stale-home__text>span,.mk-pwa-stale-home__text>strong{display:block;}', 344 '.mk-pwa-stale-home__date{font-weight:700;}', 345 '.mk-pwa-stale-home__close{position:absolute;top:2px;right:2px;width:40px;height:40px;margin:0;padding:0;border:0;background:transparent;color:#fff;cursor:pointer;}', 346 '.mk-pwa-stale-home__close:before,.mk-pwa-stale-home__close:after{content:"";position:absolute;top:19px;left:10px;width:20px;height:2px;border-radius:1px;background:#fff;}', 347 '.mk-pwa-stale-home__close:before{transform:rotate(45deg);}', 348 '.mk-pwa-stale-home__close:after{transform:rotate(-45deg);}', 349 '.mk-pwa-stale-home__refresh{flex:0 0 auto;box-sizing:border-box;width:165px;height:29px;margin-top:19px;padding:0;border:0;border-radius:14.5px;background:#fff;color:#962524;font:400 12px/29px Arial,Helvetica,sans-serif;cursor:pointer;}', 350 '.mk-pwa-stale-home__close:focus,.mk-pwa-stale-home__refresh:focus{outline:none;}', 351 '.mk-pwa-stale-home__close:focus-visible,.mk-pwa-stale-home__refresh:focus-visible{outline:2px solid #fff;outline-offset:2px;}', 352 '@media(max-width:380px){.mk-pwa-stale-home{padding:12px}.mk-pwa-stale-home__dialog{padding-right:18px;padding-left:18px}.mk-pwa-stale-home__text{font-size:15px;}}' 353 ].join(''); 354 (doc.head || doc.documentElement).appendChild(style); 355 } 356 357 previousFocus = doc.activeElement; 358 359 overlay = doc.createElement('div'); 360 overlay.className = 'mk-pwa-stale-home'; 361 362 dialog = doc.createElement('div'); 363 dialog.className = 'mk-pwa-stale-home__dialog'; 364 dialog.setAttribute('role', 'dialog'); 365 dialog.setAttribute('aria-modal', 'true'); 366 dialog.setAttribute('aria-labelledby', 'mk-pwa-stale-home-title'); 367 dialog.setAttribute('aria-describedby', 'mk-pwa-stale-home-date mk-pwa-stale-home-status'); 368 369 text = doc.createElement('p'); 370 text.className = 'mk-pwa-stale-home__text'; 371 372 title = doc.createElement('span'); 373 title.id = 'mk-pwa-stale-home-title'; 374 title.textContent = 'Ðоказана веÑÑÐ¸Ñ ÑайÑа'; 375 376 date = doc.createElement('strong'); 377 date.id = 'mk-pwa-stale-home-date'; 378 date.className = 'mk-pwa-stale-home__date'; 379 380 status = doc.createElement('span'); 381 status.id = 'mk-pwa-stale-home-status'; 382 status.textContent = 'Ð½ÐµÑ Ð´Ð¾ÑÑÑпа в ÐнÑеÑнеÑ.'; 383 384 closeButton = doc.createElement('button'); 385 closeButton.type = 'button'; 386 closeButton.className = 'mk-pwa-stale-home__close'; 387 closeButton.setAttribute('aria-label', 'ÐакÑÑÑÑ'); 388 closeButton.addEventListener('click', closeStaleHomeBanner); 389 390 refreshButton = doc.createElement('button'); 391 refreshButton.type = 'button'; 392 refreshButton.className = 'mk-pwa-stale-home__refresh'; 393 refreshButton.textContent = 'ÐÐÐÐÐÐТЬ'; 394 refreshButton.addEventListener('click', function () { 395 root.location.reload(); 396 }); 397 398 text.appendChild(title); 399 text.appendChild(date); 400 text.appendChild(status); 401 dialog.appendChild(closeButton); 402 dialog.appendChild(text); 403 dialog.appendChild(refreshButton); 404 overlay.appendChild(dialog); 405 doc.body.appendChild(overlay); 406 setStaleHomeScrollLocked(doc, true); 407 doc.addEventListener('keydown', handleStaleHomeKeydown); 408 409 staleHomeBanner = { 410 element: overlay, 411 dialog: dialog, 412 date: date, 413 closeButton: closeButton, 414 refreshButton: refreshButton, 415 previousFocus: previousFocus, 416 keydownListener: handleStaleHomeKeydown, 417 document: doc 418 }; 419 420 try { 421 closeButton.focus(); 422 } catch (_) { 423 // Focus management is an enhancement; rendering must survive. 424 } 425 426 return staleHomeBanner; 427 } 428 429 function showStaleHomeBanner(notice) { 430 var cachedAt = Number(notice && notice.cachedAt) || 0; 431 var ageMs = Number(notice && (notice.ageMs || notice.cacheAgeMs)) || 0; 432 var formattedDate; 433 var banner; 434 435 if (staleHomeDismissed) { 436 return; 437 } 438 439 if (root.location.pathname !== HOME_PATH) { 440 return; 441 } 442 443 if (notice && (notice.url || notice.cacheKey) && !isHomeUrl(notice.url || notice.cacheKey)) { 444 return; 445 } 446 447 if (!ageMs && cachedAt > 0) { 448 ageMs = Math.max(0, Date.now() - cachedAt); 449 } 450 451 if (!cachedAt || ageMs <= STALE_HOME_MIN_AGE_MS) { 452 return; 453 } 454 455 formattedDate = formatCacheDateTime(cachedAt); 456 if (!formattedDate) { 457 return; 458 } 459 460 banner = ensureStaleHomeBanner(); 461 if (!banner) { 462 return; 463 } 464 465 banner.date.textContent = 'Ð¾Ñ ' + formattedDate + ','; 466 } 467 468 function flushCacheErrorGoal() { 469 if (cacheErrorGoalSent || !pendingCacheErrorGoal || typeof root.ym !== 'function') { 470 return; 471 } 472 try { 473 root.ym(1598573, 'reachGoal', 'cache_storage_error_pwa', pendingCacheErrorGoal); 474 cacheErrorGoalSent = true; 475 pendingCacheErrorGoal = null; 476 } catch (_) { 477 // Analytics must never break the page or worker bridge. 478 } 479 } 480 481 root.addEventListener('load', flushCacheErrorGoal); 482 root.addEventListener('pageshow', flushCacheErrorGoal); 483 484 root.navigator.serviceWorker.addEventListener('message', function (event) { 485 var data = event && event.data ? event.data : null; 486 487 if (!data || !data.type) { 488 return; 489 } 490 491 if (data.type === 'PWA_CACHE_STORAGE_ERROR') { 492 if (!cacheErrorGoalSent && !pendingCacheErrorGoal) { 493 pendingCacheErrorGoal = {
494 sw_version: String(data.version || '').slice(0, 64), 495 cache: String(data.cache || '').slice(0, 64), 496 operation: String(data.operation || '').slice(0, 24), 497 error_name: String(data.errorName || '').slice(0, 32) 498 }; 499 } 500 flushCacheErrorGoal(); 501 return; 502 } 503 504 if (data.type === 'PWA_WARMUP_REQUEST') { 505 provideWarmupUrls(event.source); 506 return; 507 } 508 509 if (data.type === 'PWA_STALE_HOME') { 510 if (STALE_HOME_UI_ENABLED) { 511 showStaleHomeBanner(data); 512 } 513 return; 514 } 515 }); 516 517 function register() { 518 // An existing controller can deliver an offline-page notice even 519 // if the registration update itself cannot reach the network. 520 announceClientReady(); 521 root.navigator.serviceWorker.register(context.config.swUrl, { 522 scope: '/', 523 updateViaCache: 'none' 524 }).then(function (readyRegistration) { 525 registration = readyRegistration; 526 announceClientReady(); 527 528 root.navigator.serviceWorker.ready.then(function (activeRegistration) { 529 registration = activeRegistration; 530 announceClientReady(activeRegistration.active); 531 }).catch(function () { 532 // Registration errors are handled by the outer promise. 533 }); 534 535 root.navigator.serviceWorker.addEventListener('controllerchange', function () { 536 announceClientReady(); 537 }); 538 539 context.logger.info('[PWA]', 'sw.registered', readyRegistration.scope); 540 }).catch(function (error) { 541 context.logger.warn('[PWA]', 'sw.register.failed', { 542 name: error && error.name, 543 message: error && error.message 544 }); 545 }); 546 } 547 548 return { 549 register: register, 550 requestStaleHomeNotice: requestStaleHomeNotice 551 }; 552 }; 553})(window);
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.