| 1 |
/* eslint-disable no-undef */ |
| 2 |
/** |
| 3 |
* GutSlider admin dashboard. |
| 4 |
* |
| 5 |
* Staged-change editing: toggles update the UI immediately, a save bar |
| 6 |
* collects the diff, and one request persists everything. |
| 7 |
*/ |
| 8 |
(function () { |
| 9 |
'use strict'; |
| 10 |
|
| 11 |
var settings = window.gutslider || {}; |
| 12 |
var i18n = settings.i18n || {}; |
| 13 |
var app = document.querySelector('.gs-app'); |
| 14 |
|
| 15 |
if (!app) { |
| 16 |
return; |
| 17 |
} |
| 18 |
|
| 19 |
/* ---------------------------------------------------------------- |
| 20 |
* Utilities |
| 21 |
* -------------------------------------------------------------- */ |
| 22 |
|
| 23 |
function $(selector, scope) { |
| 24 |
return (scope || document).querySelector(selector); |
| 25 |
} |
| 26 |
|
| 27 |
function $$(selector, scope) { |
| 28 |
return Array.prototype.slice.call((scope || document).querySelectorAll(selector)); |
| 29 |
} |
| 30 |
|
| 31 |
function sprintf(template, values) { |
| 32 |
var i = 0; |
| 33 |
return String(template) |
| 34 |
.replace(/%(\d)\$d/g, function (match, index) { |
| 35 |
return values[parseInt(index, 10) - 1]; |
| 36 |
}) |
| 37 |
.replace(/%d|%s/g, function () { |
| 38 |
return values[i++]; |
| 39 |
}); |
| 40 |
} |
| 41 |
|
| 42 |
function post(endpoint, body) { |
| 43 |
return window |
| 44 |
.fetch(settings.restUrl + endpoint, { |
| 45 |
method: 'POST', |
| 46 |
credentials: 'same-origin', |
| 47 |
headers: { |
| 48 |
'Content-Type': 'application/json', |
| 49 |
'X-WP-Nonce': settings.nonce |
| 50 |
}, |
| 51 |
body: JSON.stringify(body || {}) |
| 52 |
}) |
| 53 |
.then(function (response) { |
| 54 |
if (!response.ok) { |
| 55 |
throw new Error('HTTP ' + response.status); |
| 56 |
} |
| 57 |
return response.json(); |
| 58 |
}); |
| 59 |
} |
| 60 |
|
| 61 |
/* ---------------------------------------------------------------- |
| 62 |
* Toasts |
| 63 |
* -------------------------------------------------------------- */ |
| 64 |
|
| 65 |
var ICON_OK = '<path d="m5 13 4 4L19 7"/>'; |
| 66 |
var ICON_ERR = '<path d="M18 6 6 18M6 6l12 12"/>'; |
| 67 |
|
| 68 |
function toast(message, type) { |
| 69 |
var host = $('[data-gs-toasts]'); |
| 70 |
|
| 71 |
if (!host) { |
| 72 |
return; |
| 73 |
} |
| 74 |
|
| 75 |
var el = document.createElement('div'); |
| 76 |
el.className = 'gs-toast' + (type === 'error' ? ' gs-toast--error' : ''); |
| 77 |
el.setAttribute('role', 'status'); |
| 78 |
el.innerHTML = |
| 79 |
'<span class="gs-toast__icon"><svg class="gs-icon" viewBox="0 0 24 24" fill="none" ' + |
| 80 |
'stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">' + |
| 81 |
(type === 'error' ? ICON_ERR : ICON_OK) + |
| 82 |
'</svg></span><span></span>'; |
| 83 |
el.lastChild.textContent = message; |
| 84 |
|
| 85 |
host.appendChild(el); |
| 86 |
|
| 87 |
window.setTimeout(function () { |
| 88 |
el.classList.add('is-out'); |
| 89 |
window.setTimeout(function () { |
| 90 |
if (el.parentNode) { |
| 91 |
el.parentNode.removeChild(el); |
| 92 |
} |
| 93 |
}, 240); |
| 94 |
}, 2600); |
| 95 |
} |
| 96 |
|
| 97 |
/* ---------------------------------------------------------------- |
| 98 |
* Colour scheme |
| 99 |
* -------------------------------------------------------------- */ |
| 100 |
|
| 101 |
(function theme() { |
| 102 |
var KEY = 'gutslider-theme'; |
| 103 |
var stored = null; |
| 104 |
|
| 105 |
try { |
| 106 |
stored = window.localStorage.getItem(KEY); |
| 107 |
} catch (e) { |
| 108 |
stored = null; |
| 109 |
} |
| 110 |
|
| 111 |
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; |
| 112 |
var mode = stored || (prefersDark ? 'dark' : 'light'); |
| 113 |
|
| 114 |
app.setAttribute('data-gs-theme', mode); |
| 115 |
|
| 116 |
var button = $('[data-gs-theme-toggle]'); |
| 117 |
|
| 118 |
if (!button) { |
| 119 |
return; |
| 120 |
} |
| 121 |
|
| 122 |
button.addEventListener('click', function () { |
| 123 |
mode = app.getAttribute('data-gs-theme') === 'dark' ? 'light' : 'dark'; |
| 124 |
app.setAttribute('data-gs-theme', mode); |
| 125 |
|
| 126 |
try { |
| 127 |
window.localStorage.setItem(KEY, mode); |
| 128 |
} catch (e) { |
| 129 |
/* Storage unavailable — the choice just will not persist. */ |
| 130 |
} |
| 131 |
}); |
| 132 |
})(); |
| 133 |
|
| 134 |
/* ---------------------------------------------------------------- |
| 135 |
* Walkthrough — a facade, so YouTube only loads if it is asked for |
| 136 |
* -------------------------------------------------------------- */ |
| 137 |
|
| 138 |
(function walkthrough() { |
| 139 |
var trigger = $('[data-gs-video]'); |
| 140 |
|
| 141 |
if (!trigger) { |
| 142 |
return; |
| 143 |
} |
| 144 |
|
| 145 |
trigger.addEventListener('click', function () { |
| 146 |
var frame = document.createElement('iframe'); |
| 147 |
|
| 148 |
frame.src = trigger.getAttribute('data-gs-video'); |
| 149 |
frame.title = trigger.getAttribute('data-gs-video-title') || ''; |
| 150 |
frame.allow = |
| 151 |
'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'; |
| 152 |
frame.referrerPolicy = 'strict-origin-when-cross-origin'; |
| 153 |
frame.allowFullscreen = true; |
| 154 |
|
| 155 |
trigger.innerHTML = ''; |
| 156 |
trigger.appendChild(frame); |
| 157 |
trigger.disabled = true; |
| 158 |
trigger.style.cursor = 'default'; |
| 159 |
}); |
| 160 |
})(); |
| 161 |
|
| 162 |
/* ---------------------------------------------------------------- |
| 163 |
* Save bar — shared by the blocks and settings screens |
| 164 |
* -------------------------------------------------------------- */ |
| 165 |
|
| 166 |
function createSaveBar(handler) { |
| 167 |
var bar = $('[data-gs-savebar]'); |
| 168 |
var text = $('[data-gs-savebar-text]'); |
| 169 |
var saveBtn = $('[data-gs-save]'); |
| 170 |
var discardBtn = $('[data-gs-discard]'); |
| 171 |
var kbd = $('[data-gs-save-kbd]'); |
| 172 |
var count = 0; |
| 173 |
var onDiscard = function () {}; |
| 174 |
|
| 175 |
if (!bar) { |
| 176 |
return null; |
| 177 |
} |
| 178 |
|
| 179 |
if (kbd && navigator.platform.indexOf('Mac') === -1) { |
| 180 |
kbd.textContent = 'Ctrl+S'; |
| 181 |
} |
| 182 |
|
| 183 |
function render(next) { |
| 184 |
count = next; |
| 185 |
|
| 186 |
if (count > 0) { |
| 187 |
text.textContent = sprintf(count === 1 ? i18n.changeSingle : i18n.changePlural, [count]); |
| 188 |
bar.classList.add('is-visible'); |
| 189 |
bar.setAttribute('aria-hidden', 'false'); |
| 190 |
} else { |
| 191 |
bar.classList.remove('is-visible'); |
| 192 |
bar.setAttribute('aria-hidden', 'true'); |
| 193 |
} |
| 194 |
} |
| 195 |
|
| 196 |
function save() { |
| 197 |
if (count === 0 || saveBtn.classList.contains('is-busy')) { |
| 198 |
return; |
| 199 |
} |
| 200 |
|
| 201 |
saveBtn.classList.add('is-busy'); |
| 202 |
|
| 203 |
handler() |
| 204 |
.then(function () { |
| 205 |
render(0); |
| 206 |
toast(i18n.saved); |
| 207 |
}) |
| 208 |
.catch(function () { |
| 209 |
toast(i18n.saveFailed, 'error'); |
| 210 |
}) |
| 211 |
.then(function () { |
| 212 |
saveBtn.classList.remove('is-busy'); |
| 213 |
}); |
| 214 |
} |
| 215 |
|
| 216 |
saveBtn.addEventListener('click', save); |
| 217 |
|
| 218 |
discardBtn.addEventListener('click', function () { |
| 219 |
onDiscard(); |
| 220 |
render(0); |
| 221 |
toast(i18n.discarded); |
| 222 |
}); |
| 223 |
|
| 224 |
document.addEventListener('keydown', function (event) { |
| 225 |
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') { |
| 226 |
if (count > 0) { |
| 227 |
event.preventDefault(); |
| 228 |
save(); |
| 229 |
} |
| 230 |
} |
| 231 |
}); |
| 232 |
|
| 233 |
window.addEventListener('beforeunload', function (event) { |
| 234 |
if (count > 0) { |
| 235 |
event.preventDefault(); |
| 236 |
event.returnValue = i18n.leaveWarning; |
| 237 |
return i18n.leaveWarning; |
| 238 |
} |
| 239 |
|
| 240 |
return undefined; |
| 241 |
}); |
| 242 |
|
| 243 |
return { |
| 244 |
update: render, |
| 245 |
onDiscard: function (fn) { |
| 246 |
onDiscard = fn; |
| 247 |
} |
| 248 |
}; |
| 249 |
} |
| 250 |
|
| 251 |
/* ---------------------------------------------------------------- |
| 252 |
* Blocks screen |
| 253 |
* -------------------------------------------------------------- */ |
| 254 |
|
| 255 |
(function blocksScreen() { |
| 256 |
var screen = $('[data-gs-blocks]'); |
| 257 |
|
| 258 |
if (!screen) { |
| 259 |
return; |
| 260 |
} |
| 261 |
|
| 262 |
var cards = $$('[data-gs-block]', screen); |
| 263 |
var searchInput = $('[data-gs-search]', screen); |
| 264 |
var counter = $('[data-gs-counter]', screen); |
| 265 |
var summary = $('[data-gs-summary]', screen); |
| 266 |
var metricValue = $('[data-gs-metric-enabled]', screen); |
| 267 |
var metricNote = $('[data-gs-metric-note]', screen); |
| 268 |
var empty = $('[data-gs-empty]', screen); |
| 269 |
var grid = $('[data-gs-grid]', screen); |
| 270 |
var filter = 'all'; |
| 271 |
var query = ''; |
| 272 |
|
| 273 |
var freeTotal = cards.filter(function (card) { |
| 274 |
return card.getAttribute('data-gs-tier') === 'free'; |
| 275 |
}).length; |
| 276 |
var proTotal = cards.length - freeTotal; |
| 277 |
|
| 278 |
var state = {}; |
| 279 |
var initial = {}; |
| 280 |
|
| 281 |
cards.forEach(function (card) { |
| 282 |
var name = card.getAttribute('data-gs-block'); |
| 283 |
var on = card.getAttribute('data-gs-initial') === '1'; |
| 284 |
initial[name] = on; |
| 285 |
state[name] = on; |
| 286 |
}); |
| 287 |
|
| 288 |
var bar = createSaveBar(function () { |
| 289 |
var changed = diff(); |
| 290 |
|
| 291 |
return post('dashboard/blocks', { blocks: changed }).then(function () { |
| 292 |
Object.keys(changed).forEach(function (name) { |
| 293 |
initial[name] = changed[name]; |
| 294 |
}); |
| 295 |
markDirty(); |
| 296 |
}); |
| 297 |
}); |
| 298 |
|
| 299 |
function diff() { |
| 300 |
var changed = {}; |
| 301 |
|
| 302 |
Object.keys(state).forEach(function (name) { |
| 303 |
if (state[name] !== initial[name]) { |
| 304 |
changed[name] = state[name]; |
| 305 |
} |
| 306 |
}); |
| 307 |
|
| 308 |
return changed; |
| 309 |
} |
| 310 |
|
| 311 |
function markDirty() { |
| 312 |
var changed = diff(); |
| 313 |
|
| 314 |
cards.forEach(function (card) { |
| 315 |
var name = card.getAttribute('data-gs-block'); |
| 316 |
card.classList.toggle('is-dirty', Object.prototype.hasOwnProperty.call(changed, name)); |
| 317 |
}); |
| 318 |
|
| 319 |
if (bar) { |
| 320 |
bar.update(Object.keys(changed).length); |
| 321 |
} |
| 322 |
} |
| 323 |
|
| 324 |
function updateCount() { |
| 325 |
var enabled = 0; |
| 326 |
|
| 327 |
cards.forEach(function (card) { |
| 328 |
if (state[card.getAttribute('data-gs-block')]) { |
| 329 |
enabled += 1; |
| 330 |
} |
| 331 |
}); |
| 332 |
|
| 333 |
if (counter) { |
| 334 |
counter.textContent = sprintf(i18n.enabledCount, [enabled, cards.length]); |
| 335 |
} |
| 336 |
|
| 337 |
if (summary) { |
| 338 |
summary.textContent = sprintf(i18n.blockSummary, [ |
| 339 |
freeTotal, |
| 340 |
proTotal, |
| 341 |
enabled, |
| 342 |
cards.length |
| 343 |
]); |
| 344 |
} |
| 345 |
|
| 346 |
if (metricValue) { |
| 347 |
metricValue.textContent = String(enabled); |
| 348 |
} |
| 349 |
|
| 350 |
if (metricNote) { |
| 351 |
var off = cards.length - enabled; |
| 352 |
metricNote.textContent = off > 0 ? sprintf(i18n.someDisabled, [off]) : i18n.allEnabled; |
| 353 |
} |
| 354 |
} |
| 355 |
|
| 356 |
function applyFilters() { |
| 357 |
var visible = 0; |
| 358 |
|
| 359 |
cards.forEach(function (card) { |
| 360 |
var name = card.getAttribute('data-gs-block'); |
| 361 |
var tier = card.getAttribute('data-gs-tier'); |
| 362 |
var haystack = card.getAttribute('data-gs-name') || ''; |
| 363 |
var on = !!state[name]; |
| 364 |
|
| 365 |
var matchesQuery = query === '' || haystack.indexOf(query) !== -1; |
| 366 |
var matchesFilter = |
| 367 |
filter === 'all' || |
| 368 |
(filter === 'free' && tier === 'free') || |
| 369 |
(filter === 'pro' && tier === 'pro') || |
| 370 |
(filter === 'enabled' && on) || |
| 371 |
(filter === 'disabled' && !on); |
| 372 |
|
| 373 |
var show = matchesQuery && matchesFilter; |
| 374 |
card.classList.toggle('is-hidden', !show); |
| 375 |
|
| 376 |
if (show) { |
| 377 |
visible += 1; |
| 378 |
} |
| 379 |
}); |
| 380 |
|
| 381 |
if (empty) { |
| 382 |
empty.hidden = visible !== 0; |
| 383 |
} |
| 384 |
|
| 385 |
if (grid) { |
| 386 |
grid.hidden = visible === 0; |
| 387 |
} |
| 388 |
} |
| 389 |
|
| 390 |
function setToggle(card, on) { |
| 391 |
var name = card.getAttribute('data-gs-block'); |
| 392 |
var input = $('[data-gs-toggle]', card); |
| 393 |
|
| 394 |
state[name] = on; |
| 395 |
|
| 396 |
if (input) { |
| 397 |
input.checked = on; |
| 398 |
} |
| 399 |
|
| 400 |
card.classList.toggle('is-on', on); |
| 401 |
card.classList.toggle('is-off', !on); |
| 402 |
} |
| 403 |
|
| 404 |
// Toggles. |
| 405 |
$$('[data-gs-toggle]', screen).forEach(function (input) { |
| 406 |
input.addEventListener('change', function () { |
| 407 |
var card = input.closest('[data-gs-block]'); |
| 408 |
setToggle(card, input.checked); |
| 409 |
markDirty(); |
| 410 |
updateCount(); |
| 411 |
applyFilters(); |
| 412 |
}); |
| 413 |
}); |
| 414 |
|
| 415 |
// Search. |
| 416 |
if (searchInput) { |
| 417 |
searchInput.addEventListener('input', function () { |
| 418 |
query = searchInput.value.trim().toLowerCase(); |
| 419 |
applyFilters(); |
| 420 |
}); |
| 421 |
|
| 422 |
document.addEventListener('keydown', function (event) { |
| 423 |
var tag = document.activeElement ? document.activeElement.tagName : ''; |
| 424 |
|
| 425 |
if (event.key === '/' && tag !== 'INPUT' && tag !== 'TEXTAREA') { |
| 426 |
event.preventDefault(); |
| 427 |
searchInput.focus(); |
| 428 |
} |
| 429 |
|
| 430 |
if (event.key === 'Escape' && document.activeElement === searchInput) { |
| 431 |
searchInput.value = ''; |
| 432 |
query = ''; |
| 433 |
applyFilters(); |
| 434 |
} |
| 435 |
}); |
| 436 |
} |
| 437 |
|
| 438 |
// Filter segments. |
| 439 |
$$('[data-gs-filter]', screen).forEach(function (button) { |
| 440 |
button.addEventListener('click', function () { |
| 441 |
filter = button.getAttribute('data-gs-filter'); |
| 442 |
|
| 443 |
$$('[data-gs-filter]', screen).forEach(function (other) { |
| 444 |
var active = other === button; |
| 445 |
other.classList.toggle('is-active', active); |
| 446 |
other.setAttribute('aria-selected', active ? 'true' : 'false'); |
| 447 |
}); |
| 448 |
|
| 449 |
applyFilters(); |
| 450 |
}); |
| 451 |
}); |
| 452 |
|
| 453 |
// Clear filters from the empty state. |
| 454 |
var clearBtn = $('[data-gs-clear-filters]', screen); |
| 455 |
|
| 456 |
if (clearBtn) { |
| 457 |
clearBtn.addEventListener('click', function () { |
| 458 |
query = ''; |
| 459 |
filter = 'all'; |
| 460 |
|
| 461 |
if (searchInput) { |
| 462 |
searchInput.value = ''; |
| 463 |
} |
| 464 |
|
| 465 |
$$('[data-gs-filter]', screen).forEach(function (other) { |
| 466 |
var active = other.getAttribute('data-gs-filter') === 'all'; |
| 467 |
other.classList.toggle('is-active', active); |
| 468 |
other.setAttribute('aria-selected', active ? 'true' : 'false'); |
| 469 |
}); |
| 470 |
|
| 471 |
applyFilters(); |
| 472 |
}); |
| 473 |
} |
| 474 |
|
| 475 |
// Bulk actions apply to whatever is currently visible. |
| 476 |
$$('[data-gs-bulk]', screen).forEach(function (button) { |
| 477 |
button.addEventListener('click', function () { |
| 478 |
var on = button.getAttribute('data-gs-bulk') === 'on'; |
| 479 |
|
| 480 |
cards.forEach(function (card) { |
| 481 |
if (card.classList.contains('is-hidden') || card.getAttribute('data-gs-locked') === '1') { |
| 482 |
return; |
| 483 |
} |
| 484 |
|
| 485 |
setToggle(card, on); |
| 486 |
}); |
| 487 |
|
| 488 |
markDirty(); |
| 489 |
updateCount(); |
| 490 |
applyFilters(); |
| 491 |
}); |
| 492 |
}); |
| 493 |
|
| 494 |
if (bar) { |
| 495 |
bar.onDiscard(function () { |
| 496 |
cards.forEach(function (card) { |
| 497 |
setToggle(card, initial[card.getAttribute('data-gs-block')]); |
| 498 |
card.classList.remove('is-dirty'); |
| 499 |
}); |
| 500 |
|
| 501 |
updateCount(); |
| 502 |
applyFilters(); |
| 503 |
}); |
| 504 |
} |
| 505 |
|
| 506 |
updateCount(); |
| 507 |
applyFilters(); |
| 508 |
})(); |
| 509 |
|
| 510 |
/* ---------------------------------------------------------------- |
| 511 |
* Settings screen |
| 512 |
* -------------------------------------------------------------- */ |
| 513 |
|
| 514 |
(function settingsScreen() { |
| 515 |
var screen = $('[data-gs-settings]'); |
| 516 |
|
| 517 |
if (!screen) { |
| 518 |
return; |
| 519 |
} |
| 520 |
|
| 521 |
var inputs = $$('[data-gs-setting]', screen); |
| 522 |
var initial = {}; |
| 523 |
var state = {}; |
| 524 |
|
| 525 |
inputs.forEach(function (input) { |
| 526 |
var key = input.getAttribute('data-gs-setting'); |
| 527 |
|
| 528 |
if (input.type === 'radio') { |
| 529 |
initial[key] = input.getAttribute('data-gs-initial'); |
| 530 |
|
| 531 |
if (input.checked) { |
| 532 |
state[key] = input.value; |
| 533 |
} |
| 534 |
} else { |
| 535 |
initial[key] = input.getAttribute('data-gs-initial') === '1'; |
| 536 |
state[key] = input.checked; |
| 537 |
} |
| 538 |
}); |
| 539 |
|
| 540 |
var bar = createSaveBar(function () { |
| 541 |
var changed = diff(); |
| 542 |
|
| 543 |
return post('dashboard/settings', { settings: changed }).then(function () { |
| 544 |
Object.keys(changed).forEach(function (key) { |
| 545 |
initial[key] = changed[key]; |
| 546 |
}); |
| 547 |
}); |
| 548 |
}); |
| 549 |
|
| 550 |
function diff() { |
| 551 |
var changed = {}; |
| 552 |
|
| 553 |
Object.keys(state).forEach(function (key) { |
| 554 |
if (state[key] !== initial[key]) { |
| 555 |
changed[key] = state[key]; |
| 556 |
} |
| 557 |
}); |
| 558 |
|
| 559 |
return changed; |
| 560 |
} |
| 561 |
|
| 562 |
function markDirty() { |
| 563 |
if (bar) { |
| 564 |
bar.update(Object.keys(diff()).length); |
| 565 |
} |
| 566 |
} |
| 567 |
|
| 568 |
function syncRadioCards() { |
| 569 |
$$('.gs-radio-card', screen).forEach(function (card) { |
| 570 |
var input = $('input', card); |
| 571 |
card.classList.toggle('is-active', !!input && input.checked); |
| 572 |
}); |
| 573 |
} |
| 574 |
|
| 575 |
inputs.forEach(function (input) { |
| 576 |
input.addEventListener('change', function () { |
| 577 |
var key = input.getAttribute('data-gs-setting'); |
| 578 |
|
| 579 |
if (input.type === 'radio') { |
| 580 |
if (!input.checked) { |
| 581 |
return; |
| 582 |
} |
| 583 |
state[key] = input.value; |
| 584 |
syncRadioCards(); |
| 585 |
} else { |
| 586 |
state[key] = input.checked; |
| 587 |
} |
| 588 |
|
| 589 |
markDirty(); |
| 590 |
}); |
| 591 |
}); |
| 592 |
|
| 593 |
if (bar) { |
| 594 |
bar.onDiscard(function () { |
| 595 |
inputs.forEach(function (input) { |
| 596 |
var key = input.getAttribute('data-gs-setting'); |
| 597 |
|
| 598 |
if (input.type === 'radio') { |
| 599 |
input.checked = input.value === initial[key]; |
| 600 |
state[key] = initial[key]; |
| 601 |
} else { |
| 602 |
input.checked = initial[key]; |
| 603 |
state[key] = initial[key]; |
| 604 |
} |
| 605 |
}); |
| 606 |
|
| 607 |
syncRadioCards(); |
| 608 |
}); |
| 609 |
} |
| 610 |
|
| 611 |
// Clear generated stylesheets. |
| 612 |
var clearCache = $('[data-gs-clear-cache]', screen); |
| 613 |
|
| 614 |
if (clearCache) { |
| 615 |
clearCache.addEventListener('click', function () { |
| 616 |
if (clearCache.classList.contains('is-busy')) { |
| 617 |
return; |
| 618 |
} |
| 619 |
|
| 620 |
clearCache.classList.add('is-busy'); |
| 621 |
|
| 622 |
post('dashboard/clear-cache') |
| 623 |
.then(function () { |
| 624 |
toast(i18n.cacheCleared); |
| 625 |
}) |
| 626 |
.catch(function () { |
| 627 |
toast(i18n.saveFailed, 'error'); |
| 628 |
}) |
| 629 |
.then(function () { |
| 630 |
clearCache.classList.remove('is-busy'); |
| 631 |
}); |
| 632 |
}); |
| 633 |
} |
| 634 |
|
| 635 |
/* |
| 636 |
* Section navigation. |
| 637 |
* |
| 638 |
* Deliberately not an IntersectionObserver: the trailing sections are |
| 639 |
* often too short to ever cross a fixed detection band, so they could |
| 640 |
* never become active. Instead, the current section is the last one |
| 641 |
* whose top has passed the offset line, and the bottom of the page |
| 642 |
* always owns the final section. |
| 643 |
*/ |
| 644 |
var sections = $$('[data-gs-sidenav]', screen) |
| 645 |
.map(function (link) { |
| 646 |
var panel = $(link.getAttribute('href'), screen); |
| 647 |
return panel ? { link: link, panel: panel } : null; |
| 648 |
}) |
| 649 |
.filter(Boolean); |
| 650 |
|
| 651 |
if (sections.length) { |
| 652 |
// Distance from the viewport top, clearing the sticky bar. |
| 653 |
var SPY_OFFSET = 140; |
| 654 |
var spyFrame = null; |
| 655 |
var spyLockedUntil = 0; |
| 656 |
|
| 657 |
var setActiveSection = function (link) { |
| 658 |
sections.forEach(function (section) { |
| 659 |
section.link.classList.toggle('is-active', section.link === link); |
| 660 |
}); |
| 661 |
}; |
| 662 |
|
| 663 |
var syncSpy = function () { |
| 664 |
spyFrame = null; |
| 665 |
|
| 666 |
// A click owns the highlight until its smooth scroll settles. |
| 667 |
if (Date.now() < spyLockedUntil) { |
| 668 |
return; |
| 669 |
} |
| 670 |
|
| 671 |
var scrollable = document.documentElement.scrollHeight - window.innerHeight; |
| 672 |
var current = sections[0]; |
| 673 |
|
| 674 |
if (scrollable > 4 && window.pageYOffset >= scrollable - 2) { |
| 675 |
current = sections[sections.length - 1]; |
| 676 |
} else { |
| 677 |
sections.forEach(function (section) { |
| 678 |
if (section.panel.getBoundingClientRect().top <= SPY_OFFSET) { |
| 679 |
current = section; |
| 680 |
} |
| 681 |
}); |
| 682 |
} |
| 683 |
|
| 684 |
setActiveSection(current.link); |
| 685 |
}; |
| 686 |
|
| 687 |
var queueSpy = function () { |
| 688 |
if (spyFrame === null) { |
| 689 |
spyFrame = window.requestAnimationFrame(syncSpy); |
| 690 |
} |
| 691 |
}; |
| 692 |
|
| 693 |
sections.forEach(function (section) { |
| 694 |
section.link.addEventListener('click', function (event) { |
| 695 |
event.preventDefault(); |
| 696 |
setActiveSection(section.link); |
| 697 |
spyLockedUntil = Date.now() + 700; |
| 698 |
section.panel.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| 699 |
}); |
| 700 |
}); |
| 701 |
|
| 702 |
window.addEventListener('scroll', queueSpy, { passive: true }); |
| 703 |
window.addEventListener('resize', queueSpy); |
| 704 |
syncSpy(); |
| 705 |
} |
| 706 |
|
| 707 |
syncRadioCards(); |
| 708 |
})(); |
| 709 |
})(); |
| 710 |
|