mirror of
https://github.com/soapingtime/diyhrt.git
synced 2026-09-03 11:30:18 +02:00
[automated] update groups.io wiki
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
|
||||
<meta name="theme-color" content="#2f6fa7">
|
||||
<meta name="pinterest" content="nopin" />
|
||||
<meta name="pinterest" content="nohover" />
|
||||
|
||||
@@ -86,6 +87,45 @@
|
||||
var pushSubToken;
|
||||
var ignoreErrors = false;
|
||||
var lastError = "";
|
||||
|
||||
// Auto-hide the fixed app top navbar on reading-heavy pages. The
|
||||
// navbar tracks scroll delta 1:1 in both directions: scrolling down
|
||||
// slides it off at the same pixel rate as the page; scrolling up —
|
||||
// even one pixel — starts bringing it back at the same rate. When
|
||||
// the user is at the very top, it's always fully visible. Opt-in
|
||||
// per page; xs only. Bottom tab bar is untouched.
|
||||
function EnableTopbarAutoHide() {
|
||||
if (document.documentElement.clientWidth > 767) return;
|
||||
var body = document.body;
|
||||
body.classList.add('auto-hide-topbar');
|
||||
// Measure the actual headerbar height so notched devices (iOS)
|
||||
// hide the full bar including the safe-area-inset-top padding,
|
||||
// not just the 50px content row. Fall back to 50 if the
|
||||
// element isn't found for any reason.
|
||||
var headerEl = document.getElementById('headerbar');
|
||||
var navHeight = headerEl ? headerEl.offsetHeight : 50;
|
||||
var offset = 0;
|
||||
var lastY = window.scrollY;
|
||||
function onScroll() {
|
||||
var y = window.scrollY;
|
||||
var dy = y - lastY;
|
||||
lastY = y;
|
||||
if (y <= 0) {
|
||||
offset = 0;
|
||||
} else {
|
||||
offset -= dy;
|
||||
if (offset > 0) offset = 0;
|
||||
if (offset < -navHeight) offset = -navHeight;
|
||||
}
|
||||
body.style.setProperty('--topbar-offset', offset + 'px');
|
||||
}
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
gioDestroy(function() {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
body.classList.remove('auto-hide-topbar');
|
||||
body.style.removeProperty('--topbar-offset');
|
||||
});
|
||||
}
|
||||
|
||||
window.onerror = function(errorMessage, errorUrl, errorLine, errorColumn, errorObj) {
|
||||
let column;
|
||||
@@ -102,7 +142,7 @@
|
||||
console.log("stack:", errorObj.stack);
|
||||
stack = errorObj.stack;
|
||||
}
|
||||
console.log("client_id:", "web.app02-g2.8459966.1776820350345234217");
|
||||
console.log("client_id:", "web.app02-g2.789189.1777426185360995598");
|
||||
console.log("last_error:", lastError);
|
||||
console.log("stack:", stack);
|
||||
}
|
||||
@@ -151,7 +191,7 @@
|
||||
column: column,
|
||||
stack: stack,
|
||||
last_error: lastError,
|
||||
client_id: "web.app02-g2.8459966.1776820350345234217"
|
||||
client_id: "web.app02-g2.789189.1777426185360995598"
|
||||
},
|
||||
success: function() {
|
||||
if (console && console.log) {
|
||||
@@ -174,7 +214,7 @@
|
||||
|
||||
<script src="../../../../tinymce-5.10.9/tinymce.min.js"></script>
|
||||
<script src="../../../../js/browser-image-compression-2.0.2.min.js"></script>
|
||||
<link href="../../../../css/application-f04f83f5851c7a0844717a3b496877d3.css" rel="stylesheet" id="groupsio-css">
|
||||
<link href="../../../../css/application-24dcf155f8cf9ca30214ea64f62961b4.css" rel="stylesheet" id="groupsio-css">
|
||||
<script src="../../../../js/application-7800e976860b357df405a3ea22aa72b1.js" id="groupsio-js"></script>
|
||||
<script src="../../../../js/run_prettify.js"></script>
|
||||
<title>MTFHRT@groups.io | Wiki</title>
|
||||
@@ -220,9 +260,51 @@
|
||||
htmx.config.historyCacheSize = 0;
|
||||
htmx.config.defaultSettleDelay = 0;
|
||||
console.log("FULL PAGE LOAD");
|
||||
|
||||
// Short fade-in on #maincontent-refresh after every HTMX swap — adds zero latency
|
||||
// because it runs AFTER the new content has arrived. We listen to afterRequest
|
||||
// (fires once per HTMX request) rather than afterSwap (fires once per swapped
|
||||
// element, including every hx-swap-oob badge — which would cancel the animation).
|
||||
// Listen on document because this script runs in <head> before <body> exists.
|
||||
document.addEventListener('htmx:afterRequest', function(evt) {
|
||||
if (!evt.detail || !evt.detail.successful) return;
|
||||
var refresh = document.getElementById('maincontent-refresh');
|
||||
if (!refresh) return;
|
||||
refresh.classList.remove('gio-fade-in');
|
||||
// force reflow so removing then adding re-starts the animation each time
|
||||
void refresh.offsetWidth;
|
||||
refresh.classList.add('gio-fade-in');
|
||||
});
|
||||
|
||||
// Top-of-viewport progress bar shown while an HTMX request is in flight.
|
||||
// Provides immediate visual feedback for taps even before content arrives.
|
||||
document.addEventListener('htmx:beforeRequest', function() {
|
||||
var bar = document.getElementById('gio-progress-bar');
|
||||
if (bar) bar.classList.add('active');
|
||||
});
|
||||
document.addEventListener('htmx:afterRequest', function() {
|
||||
var bar = document.getElementById('gio-progress-bar');
|
||||
if (bar) bar.classList.remove('active');
|
||||
});
|
||||
|
||||
// Haptic feedback on mobile-native chrome taps: bottom-nav tabs, back
|
||||
// chevron, and the navbar overflow action slot. Uses the shared helpers
|
||||
// from qtutils/appfunctions.qtpl — they no-op on non-Capacitor runtimes,
|
||||
// so this is safe on the plain web too.
|
||||
document.addEventListener('click', function(ev) {
|
||||
if (typeof hapticsSelectionChanged !== 'function') return;
|
||||
var target = ev.target;
|
||||
if (!target || !target.closest) return;
|
||||
if (target.closest('.mobilesidebar-item') ||
|
||||
target.closest('#logo a') ||
|
||||
target.closest('#header-action-slot a')) {
|
||||
hapticsSelectionChanged();
|
||||
}
|
||||
}, true);
|
||||
</script>
|
||||
</head>
|
||||
<body id="body" class="" hx-headers='{"Accept-Version": "b6bf5273f75920ed951f70f381c6d7e3177d7469"}' hx-ext="preload" hx-boost="false">
|
||||
<body id="body" class="" hx-headers='{"Accept-Version": "ca86cf933a8828e8bf670199572b45bc66bd1319"}' hx-ext="preload" hx-boost="false">
|
||||
<div id="gio-progress-bar" aria-hidden="true"></div>
|
||||
|
||||
<script>
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
@@ -582,18 +664,128 @@ function showAutocompleteModal(modalName) {
|
||||
location.reload();
|
||||
});
|
||||
|
||||
function ShowBackButton(label) {
|
||||
function ShowBackButton(label, url, subtitle) {
|
||||
var logoElement = document.getElementById("logo");
|
||||
if (logoElement) {
|
||||
html = `
|
||||
<span style="color:#fff;margin-top:14px;margin-left:12px;float:left !important; height:36px;">
|
||||
<a onclick="goBack()" style="color:#fff">
|
||||
<i class="fa-fw fa-solid fa-chevron-left fa-lg"></i>
|
||||
</a></span>`;
|
||||
if (label != "") {
|
||||
html += `<div style="position:absolute;left:50%;transform:translateX(-50%);text-align:center;color:#ecf0f1;margin-top:5px;"><h4>` + label + `</h4></div>`;
|
||||
if (!logoElement) {
|
||||
return;
|
||||
}
|
||||
logoElement.innerHTML = '';
|
||||
|
||||
var span = document.createElement('span');
|
||||
span.style.cssText = "color:#fff;margin-top:14px;margin-left:12px;float:left !important;height:36px;";
|
||||
var a = document.createElement('a');
|
||||
a.style.color = '#fff';
|
||||
if (url) {
|
||||
a.href = url;
|
||||
// HTMX-boost only when the target is still inside the same group — then the
|
||||
// bottom nav (which sits outside #maincontent-refresh) is the same layout and
|
||||
// can stay put. Cross-context targets (e.g. /groups) need a full page load so
|
||||
// the bottom nav actually switches.
|
||||
if (/\/g\//.test(url)) {
|
||||
a.setAttribute('hx-boost', 'true');
|
||||
a.setAttribute('hx-target', '#maincontent-refresh');
|
||||
a.setAttribute('hx-push-url', 'true');
|
||||
a.setAttribute('hx-history', 'false');
|
||||
// Reset window scroll to the top after the swap, otherwise the
|
||||
// user lands on the previous page at whatever Y they were at on
|
||||
// the current one — typically mid-page, behind the fixed
|
||||
// navbar/notch in the iOS app.
|
||||
a.setAttribute('hx-on::after-request', 'window.scrollTo({top:0, behavior:"instant"})');
|
||||
if (window.htmx) {
|
||||
htmx.process(a);
|
||||
}
|
||||
}
|
||||
logoElement.innerHTML = html;
|
||||
} else {
|
||||
a.onclick = goBack;
|
||||
a.style.cursor = 'pointer';
|
||||
}
|
||||
var icon = document.createElement('i');
|
||||
icon.className = 'fa-fw fa-solid fa-chevron-left fa-lg';
|
||||
a.appendChild(icon);
|
||||
span.appendChild(a);
|
||||
logoElement.appendChild(span);
|
||||
|
||||
if (label) {
|
||||
var titleDiv = document.createElement('div');
|
||||
// The navbar in the iOS app gets padding-top equal to the safe-area
|
||||
// inset, so the outer box can be taller than the visible content
|
||||
// strip. Centering at 50% of the outer box lands the title inside
|
||||
// the notch — offset the center down by half the inset so the
|
||||
// title sits in the visible strip. On non-app contexts
|
||||
// env(safe-area-inset-top) is 0 and this is a no-op.
|
||||
titleDiv.style.cssText = "position:absolute;left:50%;top:calc(50% + env(safe-area-inset-top) / 2);transform:translate(-50%,-50%);text-align:center;color:#ecf0f1;max-width:60%;overflow:hidden;";
|
||||
var h4 = document.createElement('h4');
|
||||
h4.style.cssText = "margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.1;";
|
||||
h4.textContent = label;
|
||||
titleDiv.appendChild(h4);
|
||||
if (subtitle) {
|
||||
var sub = document.createElement('div');
|
||||
sub.style.cssText = "font-size:11px;line-height:1.2;margin-top:1px;opacity:0.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
||||
sub.textContent = subtitle;
|
||||
titleDiv.appendChild(sub);
|
||||
}
|
||||
logoElement.appendChild(titleDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// ShowNavbarTitle sets a centered title (and optional subtitle) in the
|
||||
// mobile navbar without any back arrow — used on top-level tab pages
|
||||
// (/feed, /groups) where there's nowhere to go back to.
|
||||
function ShowNavbarTitle(label, subtitle) {
|
||||
var logoElement = document.getElementById("logo");
|
||||
if (!logoElement) {
|
||||
return;
|
||||
}
|
||||
logoElement.innerHTML = '';
|
||||
if (label) {
|
||||
var titleDiv = document.createElement('div');
|
||||
titleDiv.style.cssText = "position:absolute;left:50%;top:calc(50% + env(safe-area-inset-top) / 2);transform:translate(-50%,-50%);text-align:center;color:#ecf0f1;max-width:70%;overflow:hidden;";
|
||||
var h4 = document.createElement('h4');
|
||||
h4.style.cssText = "margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.1;";
|
||||
h4.textContent = label;
|
||||
titleDiv.appendChild(h4);
|
||||
if (subtitle) {
|
||||
var sub = document.createElement('div');
|
||||
sub.style.cssText = "font-size:11px;line-height:1.2;margin-top:1px;opacity:0.85;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
|
||||
sub.textContent = subtitle;
|
||||
titleDiv.appendChild(sub);
|
||||
}
|
||||
logoElement.appendChild(titleDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// ShowHeaderAction adds an icon button (e.g. settings cog) to the mobile navbar,
|
||||
// to the left of the profile icon. modalTarget is optional — if given, clicking
|
||||
// opens that Bootstrap modal. ariaLabel is for accessibility.
|
||||
function ShowHeaderAction(iconClass, modalTarget, ariaLabel) {
|
||||
var slot = document.getElementById("header-action-slot");
|
||||
if (!slot) {
|
||||
return;
|
||||
}
|
||||
slot.innerHTML = '';
|
||||
var a = document.createElement('a');
|
||||
a.href = '#';
|
||||
a.className = 'navbar-action-xs';
|
||||
if (ariaLabel) {
|
||||
a.setAttribute('aria-label', ariaLabel);
|
||||
}
|
||||
if (modalTarget) {
|
||||
a.setAttribute('data-toggle', 'modal');
|
||||
a.setAttribute('data-target', modalTarget);
|
||||
}
|
||||
var icon = document.createElement('i');
|
||||
icon.className = iconClass;
|
||||
a.appendChild(icon);
|
||||
slot.appendChild(a);
|
||||
slot.style.display = '';
|
||||
}
|
||||
|
||||
function HideHeaderAction() {
|
||||
var slot = document.getElementById("header-action-slot");
|
||||
if (slot) {
|
||||
slot.innerHTML = '';
|
||||
slot.style.display = 'none';
|
||||
slot.style.float = 'left';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,6 +803,90 @@ function showAutocompleteModal(modalName) {
|
||||
history.back();
|
||||
}
|
||||
|
||||
// --- Native date/time picker helpers -----------------------------------
|
||||
// On touch devices we swap flatpickr-wrapped <input type="text"> fields
|
||||
// for native <input type="date"> / type="time" so iOS / Android bring up
|
||||
// their OS pickers (wheel, spinner, Material dialog). On pointer
|
||||
// devices we leave the flatpickr popover in place, since Chrome's
|
||||
// default date popover looks out of place. Both helpers are global so
|
||||
// every page with a date/time input can call them the same way.
|
||||
window.__isTouchPicker = function() {
|
||||
return window.matchMedia && window.matchMedia('(any-pointer: coarse)').matches;
|
||||
};
|
||||
// Fallback formats if data.User is nil on this page (logged out, etc).
|
||||
window.__userDateFmt = "M/D/YYYY";
|
||||
window.__userTimeFmt = "h:mmA";
|
||||
// Hybrid native picker: the field is shown to the user as a plain
|
||||
// type=text input holding the date/time formatted per the user's
|
||||
// Groups.io preference (e.g., "19/04/2026" for an International
|
||||
// user). When the user taps the field, we briefly swap the input's
|
||||
// type to 'date' or 'time' so the OS brings up its native picker
|
||||
// (iOS wheel, Android calendar/spinner). When the user finishes
|
||||
// picking and blurs the field, we swap back to text and reformat
|
||||
// the value in the user's chosen format. The server sees the
|
||||
// user-format value on submit, same as with flatpickr.
|
||||
//
|
||||
// `kind` is 'date' or 'time'. `onHumanChange(humanStr, momentObj)`
|
||||
// fires whenever the user picks a new value, with the value already
|
||||
// in the user's format.
|
||||
window.useNativePicker = function(id, kind, onHumanChange) {
|
||||
var input = document.getElementById(id);
|
||||
if (!input) return null;
|
||||
var userFmt = kind === 'time' ? window.__userTimeFmt : window.__userDateFmt;
|
||||
var isoFmt = kind === 'time' ? 'HH:mm' : 'YYYY-MM-DD';
|
||||
// Page loads with input type=text holding a user-format value —
|
||||
// leave it as is until the user actually taps.
|
||||
function toPicker() {
|
||||
if (input.type !== kind) {
|
||||
var m = moment(input.value, userFmt);
|
||||
input.type = kind;
|
||||
input.value = m.isValid() ? m.format(isoFmt) : '';
|
||||
}
|
||||
}
|
||||
function toDisplay() {
|
||||
if (input.type === kind) {
|
||||
var m = moment(input.value, isoFmt);
|
||||
input.type = 'text';
|
||||
input.value = m.isValid() ? m.format(userFmt) : '';
|
||||
}
|
||||
}
|
||||
// pointerdown fires before focus/click so we can swap the type
|
||||
// in time for the browser's default tap-handling to open the
|
||||
// correct native picker. Fall back to 'focus' for keyboard users.
|
||||
input.addEventListener('pointerdown', toPicker);
|
||||
input.addEventListener('focus', toPicker);
|
||||
input.addEventListener('change', function() {
|
||||
if (input.type === kind) {
|
||||
var m = moment(input.value, isoFmt);
|
||||
if (m.isValid() && onHumanChange) {
|
||||
onHumanChange(m.format(userFmt), m);
|
||||
}
|
||||
}
|
||||
});
|
||||
input.addEventListener('blur', toDisplay);
|
||||
// Make sure the server always sees the user-format value.
|
||||
var form = input.closest('form');
|
||||
if (form) form.addEventListener('submit', toDisplay);
|
||||
return input;
|
||||
};
|
||||
// Set a field's value as a user-format string, whether the field is a
|
||||
// flatpickr-wrapped text input (desktop) or a native type=date/time
|
||||
// input (touch). Used by onChange handlers that update sibling fields.
|
||||
window.setFieldHuman = function(id, humanStr, kind) {
|
||||
var input = document.getElementById(id);
|
||||
if (!input) return;
|
||||
if (input._flatpickr) {
|
||||
input._flatpickr.setDate(humanStr);
|
||||
} else if (input.type === 'date' || input.type === 'time') {
|
||||
var userFmt = kind === 'time' ? window.__userTimeFmt : window.__userDateFmt;
|
||||
var isoFmt = kind === 'time' ? 'HH:mm' : 'YYYY-MM-DD';
|
||||
var m = moment(humanStr, userFmt);
|
||||
if (m.isValid()) input.value = m.format(isoFmt);
|
||||
} else {
|
||||
input.value = humanStr;
|
||||
}
|
||||
};
|
||||
|
||||
// submitEvent acts like .submit(), except it is compatible with the app, by using events instead.
|
||||
function submitEvent(ele) {
|
||||
console.log("in submitEvent");
|
||||
@@ -1444,6 +1720,19 @@ function countChecked(id) {
|
||||
<div class="navbar navbar-default navbar-fixed-bottom">
|
||||
<div class="container" style="display: flex; justify-content: space-around;">
|
||||
|
||||
|
||||
|
||||
<div id="mobilesidebar-groups" class="mobilesidebar-item" style="margin-top:10px; padding-bottom:20px; flex: 1; text-align: center;" hx-get="https://groups.io/groups" hx-boost="true" hx-target="#maincontent-refresh" hx-push-url="true" hx-history="false" hx-on::after-request="document.getElementById('maincontent').scrollTo({top: 0, behavior: 'instant'})" >
|
||||
<div style="display: inline-block; position: relative;">
|
||||
<div style="position: relative;display: inline-block;">
|
||||
<i class="fa-fw fa-light fa-users fa-2x"></i>
|
||||
|
||||
</div><br>
|
||||
<span style="font-size:12px;">Groups</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1693,6 +1982,14 @@ function countChecked(id) {
|
||||
});
|
||||
function UpdateSidebar(id) {
|
||||
console.log(id + " was clicked");
|
||||
if (typeof HideHeaderAction === 'function') {
|
||||
HideHeaderAction();
|
||||
}
|
||||
// restore mobile bottom nav in case a previous page (e.g. /post) hid it
|
||||
var mobSidebar = document.getElementById("mobileSidebar");
|
||||
if (mobSidebar) mobSidebar.style.display = '';
|
||||
var mainContent = document.getElementById("maincontent");
|
||||
if (mainContent) mainContent.style.bottom = '';
|
||||
var sidebarItems = document.querySelectorAll('.mobilesidebar-item');
|
||||
sidebarItems.forEach(function(item) {
|
||||
var iElement = item.getElementsByTagName('div')[0].getElementsByTagName('i')[0];
|
||||
|
||||
Reference in New Issue
Block a user