Folder reorganize 1
This commit is contained in:
261
assets/js/refresh.js
Normal file
261
assets/js/refresh.js
Normal file
@@ -0,0 +1,261 @@
|
||||
class LivePanel {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.url = element.dataset.refreshUrl;
|
||||
|
||||
const requestedInterval = Number(element.dataset.refreshInterval || 2000);
|
||||
this.baseInterval = Number.isFinite(requestedInterval) && requestedInterval > 0 ? requestedInterval : 2000;
|
||||
|
||||
const minIntervalAttr = Number(element.dataset.refreshMinInterval);
|
||||
const maxIntervalAttr = Number(element.dataset.refreshMaxInterval);
|
||||
|
||||
this.minInterval = Number.isFinite(minIntervalAttr) && minIntervalAttr > 0 ? minIntervalAttr : this.baseInterval;
|
||||
this.maxInterval = Number.isFinite(maxIntervalAttr) && maxIntervalAttr > 0 ? Math.max(maxIntervalAttr, this.minInterval) : this.baseInterval * 4;
|
||||
|
||||
this.currentInterval = this.baseInterval;
|
||||
this.lastEtag = null;
|
||||
this.lastModified = null;
|
||||
this.timeoutId = null;
|
||||
this.isFetching = false;
|
||||
|
||||
if (!this.url) {
|
||||
console.warn('LivePanel requires a data-refresh-url attribute.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderLoadingState();
|
||||
this.start();
|
||||
LivePanel.instances.push(this);
|
||||
}
|
||||
|
||||
renderLoadingState() {
|
||||
if (!this.element.querySelector('.panel-loading')) {
|
||||
this.element.innerHTML = `
|
||||
<div class="panel-loading" role="status" aria-live="polite">
|
||||
<span class="spinner" aria-hidden="true"></span>
|
||||
<span>Loading latest data…</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.stop();
|
||||
this.currentInterval = this.baseInterval;
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.timeoutId) {
|
||||
window.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleNextFetch(delay) {
|
||||
if (this.baseInterval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.timeoutId) {
|
||||
window.clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
let nextDelay = typeof delay === 'number' ? delay : this.currentInterval;
|
||||
|
||||
if (nextDelay !== 0) {
|
||||
nextDelay = Math.min(Math.max(nextDelay, this.minInterval), this.maxInterval);
|
||||
}
|
||||
|
||||
this.timeoutId = window.setTimeout(() => this.fetchData(), nextDelay);
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
if (this.isFetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isFetching = true;
|
||||
|
||||
try {
|
||||
const headers = {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
};
|
||||
|
||||
if (this.lastEtag) {
|
||||
headers['If-None-Match'] = this.lastEtag;
|
||||
}
|
||||
|
||||
if (this.lastModified) {
|
||||
headers['If-Modified-Since'] = this.lastModified;
|
||||
}
|
||||
|
||||
const response = await fetch(this.url, {
|
||||
headers,
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
if (response.status === 304) {
|
||||
this.currentInterval = Math.min(this.currentInterval * 1.25, this.maxInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const etag = response.headers.get('ETag');
|
||||
if (etag) {
|
||||
this.lastEtag = etag;
|
||||
}
|
||||
|
||||
const lastModified = response.headers.get('Last-Modified');
|
||||
if (lastModified) {
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
if (html.trim().length) {
|
||||
this.element.innerHTML = html;
|
||||
}
|
||||
|
||||
const updateEvent = new CustomEvent('livepanel:updated', {
|
||||
detail: {
|
||||
panel: this,
|
||||
html
|
||||
}
|
||||
});
|
||||
this.element.dispatchEvent(updateEvent);
|
||||
|
||||
this.currentInterval = this.baseInterval;
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh panel', error);
|
||||
this.element.innerHTML = `
|
||||
<div class="panel-error" role="alert">
|
||||
<strong>Unable to load data.</strong>
|
||||
<span>Please check your connection.</span>
|
||||
</div>
|
||||
`;
|
||||
this.lastEtag = null;
|
||||
this.lastModified = null;
|
||||
this.currentInterval = Math.min(
|
||||
Math.max(this.currentInterval * 1.5, this.minInterval),
|
||||
this.maxInterval
|
||||
);
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
if (this.baseInterval > 0) {
|
||||
this.scheduleNextFetch(this.currentInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static pauseAll() {
|
||||
LivePanel.instances.forEach((panel) => panel.stop());
|
||||
}
|
||||
|
||||
static resumeAll() {
|
||||
LivePanel.instances.forEach((panel) => panel.start());
|
||||
}
|
||||
}
|
||||
|
||||
LivePanel.instances = [];
|
||||
|
||||
function initialiseLivePanels() {
|
||||
const panels = document.querySelectorAll('[data-refresh-url]');
|
||||
panels.forEach((panel) => {
|
||||
if (!panel.dataset.livePanelInitialised) {
|
||||
panel.dataset.livePanelInitialised = 'true';
|
||||
new LivePanel(panel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupBackToTop() {
|
||||
const backToTop = document.querySelector('[data-back-to-top]');
|
||||
if (!backToTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toggleVisibility = () => {
|
||||
if (window.scrollY > 320) {
|
||||
backToTop.classList.add('is-visible');
|
||||
} else {
|
||||
backToTop.classList.remove('is-visible');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', toggleVisibility, { passive: true });
|
||||
backToTop.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
function setupMobileNav() {
|
||||
const toggle = document.querySelector('[data-menu-toggle]');
|
||||
const sidebar = document.querySelector('.app-sidebar');
|
||||
|
||||
if (!toggle || !sidebar) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
||||
toggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
|
||||
sidebar.classList.toggle('is-open');
|
||||
});
|
||||
}
|
||||
|
||||
window.popupCenter = function popupCenter(pageURL, title = 'Details', width = 900, height = 640) {
|
||||
const screenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX;
|
||||
const screenTop = window.screenTop !== undefined ? window.screenTop : window.screenY;
|
||||
const dualScreenWidth = window.innerWidth || document.documentElement.clientWidth || screen.width;
|
||||
const dualScreenHeight = window.innerHeight || document.documentElement.clientHeight || screen.height;
|
||||
|
||||
const left = screenLeft + Math.max(0, (dualScreenWidth - width) / 2);
|
||||
const top = screenTop + Math.max(0, (dualScreenHeight - height) / 2);
|
||||
|
||||
const features = [
|
||||
'toolbar=no',
|
||||
'location=no',
|
||||
'directories=no',
|
||||
'status=no',
|
||||
'menubar=no',
|
||||
'scrollbars=yes',
|
||||
'resizable=yes',
|
||||
`width=${width}`,
|
||||
`height=${height}`,
|
||||
`top=${top}`,
|
||||
`left=${left}`
|
||||
].join(',');
|
||||
|
||||
const newWindow = window.open(pageURL, title, features);
|
||||
if (newWindow && newWindow.focus) {
|
||||
newWindow.focus();
|
||||
}
|
||||
return newWindow;
|
||||
};
|
||||
|
||||
window.popitup = function popitup(url, title = 'Details') {
|
||||
const popup = window.open(url, title, 'width=420,height=520,scrollbars=yes');
|
||||
if (popup && popup.focus) {
|
||||
popup.focus();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
initialiseLivePanels();
|
||||
setupBackToTop();
|
||||
setupMobileNav();
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
LivePanel.pauseAll();
|
||||
} else {
|
||||
LivePanel.resumeAll();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user