Files
diyhrt/groups.io/g/MTFHRT/wiki/37530/209879.html
T

2614 lines
97 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en-US">
<head>
<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, viewport-fit=cover">
<meta name="theme-color" content="#1f4e79">
<meta name="pinterest" content="nopin" />
<meta name="pinterest" content="nohover" />
<meta name="3778517eb4810dfb5d143ed8f1b359b3b5a82923" content="0f56257c3db4a222e91b11bc6871c4df2e263b28" />
<link rel="apple-touch-icon" sizes="57x57" href="https://groups.io/orgimg/org.1/favicons/apple-icon-57x57.png?v=1411358049715440694">
<link rel="apple-touch-icon" sizes="60x60" href="https://groups.io/orgimg/org.1/favicons/apple-icon-60x60.png?v=1411358049715440694">
<link rel="apple-touch-icon" sizes="72x72" href="https://groups.io/orgimg/org.1/favicons/apple-icon-72x72.png?v=1411358049715440694">
<link rel="apple-touch-icon" sizes="76x76" href="https://groups.io/orgimg/org.1/favicons/apple-icon-76x76.png?v=1411358049715440694">
<link rel="apple-touch-icon" sizes="114x114" href="https://groups.io/orgimg/org.1/favicons/apple-icon-114x114.png?v=1411358049715440694">
<link rel="apple-touch-icon" sizes="120x120" href="https://groups.io/orgimg/org.1/favicons/apple-icon-120x120.png?v=1411358049715440694">
<link rel="apple-touch-icon" sizes="144x144" href="https://groups.io/orgimg/org.1/favicons/apple-icon-144x144.png?v=1411358049715440694">
<link rel="apple-touch-icon" sizes="152x152" href="https://groups.io/orgimg/org.1/favicons/apple-icon-152x152.png?v=1411358049715440694">
<link rel="apple-touch-icon" sizes="180x180" href="https://groups.io/orgimg/org.1/favicons/apple-icon-180x180.png?v=1411358049715440694">
<link rel="icon" type="image/png" sizes="192x192" href="../../../../orgimg/org.1/favicons/android-icon-192x192.png%3Fv=1411358049715440694">
<link rel="icon" type="image/png" sizes="32x32" href="../../../../orgimg/org.1/favicons/favicon-32x32.png%3Fv=1411358049715440694">
<link rel="icon" type="image/png" sizes="96x96" href="../../../../orgimg/org.1/favicons/favicon-96x96.png%3Fv=1411358049715440694">
<link rel="icon" type="image/png" sizes="16x16" href="../../../../orgimg/org.1/favicons/favicon-16x16.png%3Fv=1411358049715440694">
<link rel="manifest" href="../../../../orgimg/org.1/favicons/manifest.json%3Fv=1411358049715440694">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
<script>
/* stubs */
const hapticsImpactHeavy = async () => {
};
const hapticsImpactMedium = async () => {
};
const hapticsImpactLight = async () => {
};
const hapticsVibrate = async () => {
};
const hapticsSelectionStart = async () => {
};
const hapticsSelectionChanged = async () => {
};
const hapticsSelectionEnd = async () => {
};
</script>
<script>
function setPullToRefresh() {}
function gotoURL(inurl) {
window.location.href = inurl;
return;
}
function contentLoaded(isHTMX, f, caller = "") {
console.log("contentLoaded: " + isHTMX + " " + caller);
if (isHTMX == true) {
document.body.addEventListener('htmx:afterSettle', f, {once: true});
} else {
if (document.readyState !== 'loading') {
console.log('document is already ready, just execute code here');
f();
} else {
console.log('document was not ready, place code here');
document.addEventListener('DOMContentLoaded', f, {once: true});
}
}
}
function HideAppSidebarOnKeyboard() {
console.log("In HideAppSidebarOnKeyboard");
}
// stub
function logError(data, url, linenumber) {
console.log(data)
return;
}
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');
});
}
var gioBundleReported = false;
function gioReport(fields) {
try {
var pairs = [];
for (var k in fields) {
if (fields[k] !== undefined && fields[k] !== null) {
pairs.push(encodeURIComponent(k) + '=' + encodeURIComponent(fields[k]));
}
}
var xhr = new XMLHttpRequest();
xhr.open('POST', '/jsclienterror', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(pairs.join('&'));
} catch (e) {}
}
// The report cannot depend on the application bundle: when the bundle
// is what failed to load, a jQuery transport has nothing to stand on.
// The script tag's onerror stores the src if it fires before this
// script runs.
window.__gioBundleFailed = function(src) {
if (gioBundleReported) return;
gioBundleReported = true;
gioReport({
msg: "bundle-load-failure",
url: src,
client_id: "web.app01-g2.784352.1792548510401935322"
});
};
if (window.__gioBundleErr) {
window.__gioBundleFailed(window.__gioBundleErr);
}
window.onerror = function(errorMessage, errorUrl, errorLine, errorColumn, errorObj) {
let column;
let stack;
if (console && console.log) {
console.log("msg:", errorMessage);
console.log("url:", errorUrl);
console.log("line:", errorLine);
if (errorColumn !== undefined && errorObj != null) {
console.log("column:", errorColumn);
column = errorColumn;
}
if (errorObj !== undefined && errorObj != null) {
console.log("stack:", errorObj.stack);
stack = errorObj.stack;
}
console.log("client_id:", "web.app01-g2.784352.1792548510401935322");
console.log("last_error:", lastError);
console.log("stack:", stack);
}
if (errorUrl == "" || errorUrl === null) {
errorUrl = window.location.href;
}
if (errorMessage.includes("SecurityError:") == true) {
ignoreErrors = true;
}
if (ignoreErrors == true) {
console.log("cookies, disabled, ignoring");
lastError = errorMessage;
return;
}
// With the bundle absent, every reference to a bundle global and
// every style-injection into the missing sheet throws; the
// bundle-load-failure report already carries the cause, so the
// cascade is noise.
if ((typeof jQuery === 'undefined' || typeof htmx === 'undefined') &&
(errorMessage.indexOf("is not defined") !== -1 || errorMessage.indexOf("Can't find variable") !== -1 || errorMessage.indexOf("insertRule") !== -1)) {
lastError = errorMessage;
return;
}
var ignoreMessages = ["ResizeObserver loop limit exceeded",
"TypeError: can't redefine non-configurable property \"userAgent\"",
"TypeError: can't redefine non-configurable property \"getChannelData\"",
"ReferenceError: tinymce is not defined",
"Uncaught ReferenceError: timeoutId is not defined",
"ReferenceError: Can't find variable: tinymce"];
for (var i = 0; i < ignoreMessages.length; i++) {
if (errorMessage === ignoreMessages[i]) {
lastError = errorMessage;
return;
}
}
if (errorUrl.indexOf("safari-extension:") === 0 || errorUrl.indexOf("moz-extension:") === 0 || errorUrl.indexOf("chrome-extension:") === 0) {
lastError = errorMessage;
return;
}
if (errorUrl.indexOf("googletagmanager.com") !== -1 || errorUrl.indexOf("socialsearch.com") !== -1) {
lastError = errorMessage;
return;
}
if (errorLine === 0 || errorLine === 1) {
lastError = errorMessage;
return;
}
gioReport({
msg: errorMessage,
url: errorUrl,
line: errorLine,
column: column,
stack: stack,
last_error: lastError,
client_id: "web.app01-g2.784352.1792548510401935322"
});
lastError = errorMessage;
return true;
}
</script>
<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-c561a9115460dce982df05454b4b5404.css" rel="stylesheet" id="groupsio-css">
<script src="../../../../js/application-35e32ae82e031022fd4bf6938772adce.js" id="groupsio-js" onerror="(window.__gioBundleFailed||function(s){window.__gioBundleErr=s})(this.src)"></script>
<script src="../../../../js/run_prettify.js"></script>
<title>MTFHRT@groups.io | Wiki | Basic Estrogel Guide</title>
<link rel="stylesheet" type="text/css" href="../../../../css/cookieconsent-3.1.0.min.css" />
<script src="../../../../js/cookieconsent-3.1.0.min.js"></script>
<script>
window.addEventListener("load", function(){
window.cookieconsent.initialise({
"palette": {
"popup": {
"background": "#d9edf7",
"text": "#31708f"
},
"button": {
"background": "#1b7a9a",
"text": "#ffffff"
}
},
"cookie": {
"domain": ".groups.io"
},
"content": {
"message": "Our site uses cookies so that we can remember you and understand how you and other visitors use our site, to improve your browsing experience and help us improve our site. By continuing to use our website, you agree to our use of such cookies.",
"dismiss": "I Agree",
"href": "https://groups.io/static/cookie_policy"
},
"elements": {
"dismiss": '<a role=button tabindex="0" class="cc-btn cc-dismiss">{{dismiss}}</a>'
}
})});
</script>
<script>
htmx.config.scrollBehavior = "instant";
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');
});
// Bootstrap appends an open tooltip to <body>, outside the content HTMX
// replaces, so clicking a boosted link while its tooltip shows would leave
// the tooltip stranded on the new page. Dispose of the open tooltips
// whose triggers are about to be swapped out.
document.addEventListener('htmx:beforeSwap', function(evt) {
var target = evt.detail && evt.detail.target;
if (!target || typeof bootstrap === 'undefined') return;
target.querySelectorAll('[aria-describedby^="tooltip"]').forEach(function(el) {
var tip = bootstrap.Tooltip.getInstance(el);
if (tip) tip.dispose();
});
});
// The fixed #headerbar covers the top of the viewport, but Popper only
// knows about the viewport edge, so a dropup can open with its top items
// hidden under the navbar. Count the navbar's height as unavailable so
// such menus flip to open downward instead.
bootstrap.Dropdown.Default.popperConfig = function(config) {
var header = document.getElementById('headerbar');
var top = header ? header.getBoundingClientRect().height : 0;
config.modifiers.push({name: 'flip', options: {padding: {top: top}}});
return config;
};
// 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);
// A sortable column header inside a form marked data-sort-keeps-selection, while
// rows are selected, submits that form with the new sort, the same way the form's
// pager does, so the rows checked on this and other pages stay checked. The
// attribute's value is the name of the row checkboxes. With nothing selected,
// anywhere else, or when opened in a new tab, it is an ordinary link. This runs
// in the capture phase so that the form is submitted instead of HTMX following
// the link.
document.addEventListener('click', function(ev) {
if (ev.button !== 0 || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
var target = ev.target;
if (!target || !target.closest) return;
var link = target.closest('a[data-sort-p]');
if (!link) return;
var form = link.closest('form[data-sort-keeps-selection]');
if (!form) return;
var p = form.querySelector('input[name="p"]');
if (!p) return;
var rowName = form.getAttribute('data-sort-keeps-selection');
var otherIDs = form.querySelector('input[name="otherids"]');
var checkedHere = rowName && form.querySelector('input[type="checkbox"][name="' + rowName + '"]:checked');
if (!checkedHere && !(otherIDs && otherIDs.value)) return;
ev.preventDefault();
ev.stopPropagation();
p.value = link.getAttribute('data-sort-p');
form.submit();
}, true);
</script>
</head>
<body id="body" class="" hx-headers='{"Accept-Version": "748965f045751d20eb0270de43b14f09256f4e6b"}' hx-ext="preload" hx-boost="false">
<a id="skip-link" class="skip-link visually-hidden-focusable" href="209879.html#maincontent">Skip to main content</a>
<div id="page-announcer" class="visually-hidden" role="status"></div>
<div id="gio-progress-bar" aria-hidden="true"></div>
<script>
if (window.matchMedia) {
var darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (darkModeQuery.matches) {
document.body.classList.add('dark-mode');
}
var darkModeChanged = function(e) {
document.body.classList.toggle('dark-mode', e.matches);
};
if (darkModeQuery.addEventListener) {
darkModeQuery.addEventListener('change', darkModeChanged);
} else if (darkModeQuery.addListener) {
// Safari before 14 has no addEventListener on MediaQueryList
darkModeQuery.addListener(darkModeChanged);
}
}
</script>
<div class="navbar navbar-head navbar-fixed-top navbar-expand-md fixed-top" role="navigation" id="headerbar">
<div class="container-fluid" style="padding-left:0px;">
<a href="https://groups.io" hx-boost="false" hx-disinherit="*">
<img src="../../../../orgimg/org.1/mainlogo.png%3Fv=1411358049715440694" height="50" alt="Groups.io">
</a>
<ul class="nav navbar-nav d-none d-lg-flex">
<li>
<a href="https://groups.io/search" class="nav-link">
Find or Create a Group
</a>
</li>
</ul>
<ul class="nav navbar-nav float-end d-md-none" style="margin-top:5px;margin-bottom:5px;">
<li>
<a href="209879.html#" data-bs-toggle="modal" data-bs-target="#mobileAccount" class="nav-link" aria-label="Menu">
<i class="fa-fw fa fa-bars fa-lg"></i>
</a>
</li>
</ul>
<ul class="nav navbar-nav float-end d-none d-md-flex">
<li class="dropdown">
<a href="209879.html#" class="dropdown-toggle nav-link" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa-fw fa-regular fa-globe"></i>
English
<b class="caret"></b>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="https://groups.io/setlang/ar">
العربية
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/de">
Deutsch
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/unsetlang">
English
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/es">
Español
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/fr">
Français
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/it">
Italiano
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/nl">
Nederlands
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/pt-br">
Português do Brasil
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/pt">
Português
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/uk">
Українська
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/zh-hans">
简体中文
</a>
</li>
</ul>
</li>
<li>
<a href="https://groups.io/helpcenter" target="_blank" class="nav-link">
Help
</a>
</li>
<li>
<a id="login" href="https://groups.io/login" hx-boost="false" hx-disinherit="*" class="nav-link">
<i class="fa fa-sign-in-alt"></i>
Log In
</a>
</li>
<li>
<a href="https://groups.io/register" class="nav-link">
<i class="fa fa-user"></i>
Sign Up
</a>
</li>
</ul>
</div>
</div>
<div class="modal fade" id="mobileAccount" tabindex="-1" role="dialog" aria-label="Menu">
<div class="modal-dialog" style="margin-top:50px;">
<div class="modal-content">
<div class="modal-body">
<ul class="list-group noborderlist">
<li class="noborderitem list-group-item">
<a href="https://groups.io/login" hx-boost="false" hx-disinherit="*">
<i class="fa fa-sign-in-alt"></i>
Log In
</a>
</li>
<li class="noborderitem list-group-item">
<a href="https://groups.io/register">
<i class="fa fa-user"></i>
Sign Up
</a>
</li>
<li class="noborderitem list-group-item">
<a href="https://groups.io/helpcenter" target="_blank">
<i class="fa-fw fa fa-question-circle"></i>
Help
</a>
</li>
<li class="dropdown noborderitem list-group-item">
<a href="209879.html#" class="dropdown-toggle nav-link" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa-fw fa-regular fa-globe"></i>
English
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="https://groups.io/setlang/ar">
العربية
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/de">
Deutsch
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/unsetlang">
English
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/es">
Español
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/fr">
Français
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/it">
Italiano
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/nl">
Nederlands
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/pt-br">
Português do Brasil
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/pt">
Português
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/uk">
Українська
</a>
</li>
<li>
<a class="dropdown-item" href="https://groups.io/setlang/zh-hans">
简体中文
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
<script>
var groupnavigatorData = [
];
</script>
<!-- Modal Dialog for Autocomplete -->
<div class="modal fade" id="groupnavigatorModal" tabindex="-1" role="dialog" aria-label="Go to group..." aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<input type="text" class="form-control" id="groupnavigatorInput" placeholder="Go to group..." aria-label="Go to group..." role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="groupnavigatorContainer" autocomplete="off" style="margin-bottom:10px">
<div id="groupnavigatorContainer" class="autocompleteResults" role="listbox" aria-label="Go to group..." style="max-height:250px; overflow-y: auto;"></div>
<div id="groupnavigatorStatus" class="visually-hidden" role="status" data-results-label="results"></div>
<div class="text-center" style="margin-top:10px; font-size:12px;"><i class="fa-sharp fa-arrow-up-arrow-down"></i> to navigate&nbsp;&nbsp;&nbsp;<i class="fa-sharp fa-arrow-turn-down-left"></i> to use&nbsp;&nbsp;&nbsp;<strong>esc</strong> to dismiss</div>
</div>
</div>
</div>
</div>
<script>
// Keep track of the currently selected index
var selectedIndex = -1;
// Function to highlight the matched substring
function highlightSubstring(str, substring) {
var index = str.toLowerCase().indexOf(substring.toLowerCase());
if (index !== -1) {
var before = str.substring(0, index);
var middle = str.substring(index, index + substring.length);
var after = str.substring(index + substring.length);
return before + '<span class="autocomplete-highlight">' + middle + '</span>' + after;
}
return str;
}
// Function to highlight the selected item and ensure it is visible in the scrollable list
function highlightSelectedItem(modalName) {
var listContainer = document.getElementById(modalName + 'Container');
var items = document.getElementsByClassName(modalName + '-list-item');
var input = document.getElementById(modalName + 'Input');
Array.from(items).forEach(function(item, idx) {
item.className = idx === selectedIndex ? modalName + '-list-item autocomplete-list-item btn-primary no-hover' : modalName + '-list-item autocomplete-list-item no-hover'; // Apply btn-primary class to the selected item
item.setAttribute('aria-selected', idx === selectedIndex ? 'true' : 'false');
});
// Point the combobox at the highlighted option, so screen readers
// announce it as the arrow keys move.
if (selectedIndex >= 0 && selectedIndex < items.length) {
input.setAttribute('aria-activedescendant', items[selectedIndex].id);
} else {
input.removeAttribute('aria-activedescendant');
}
if (items.length > 0 && selectedIndex >= 0) {
var selectedItem = items[selectedIndex];
var containerTop = listContainer.scrollTop;
var containerBottom = containerTop + listContainer.clientHeight;
var itemTop = selectedItem.offsetTop;
var itemBottom = itemTop + selectedItem.offsetHeight;
const fudge = 59;
if (selectedIndex > 0 && itemTop > containerBottom) {
// If there are items below the selected item and the selected item is not fully visible, scroll down
listContainer.scrollTop = itemTop - listContainer.clientHeight + items[selectedIndex - 1].offsetHeight;
} else if (selectedIndex < items.length - 1 && (itemTop - fudge) < containerTop) {
// If there are items above the selected item and the selected item is not fully visible, scroll up
listContainer.scrollTop = itemTop - fudge - items[selectedIndex + 1].offsetHeight;
}
}
}
// function to populate the list based on the input
function populateList(modalName, data) {
var input = document.getElementById(modalName+'Input').value;
var listContainer = document.getElementById(modalName+'Container');
listContainer.innerHTML = '';
var count = 0;
data.forEach(function(item) {
if (item.name.toLowerCase().includes(input.toLowerCase())) {
var div = document.createElement('div');
div.innerHTML = highlightSubstring(item.name, input);
div.className = modalName+'-list-item autocomplete-list-item no-hover'; // Apply no-hover class
div.id = modalName + 'Option' + count;
div.setAttribute('role', 'option');
div.setAttribute('aria-selected', 'false');
count++;
div.addEventListener('click', function() {
// Navigate to the specified URL when a list item is clicked
window.location.href = item.url;
});
div.addEventListener('mousemove', function() {
// Remove no-hover class from all items when the mouse is moved over any item
console.log("mouse move");
var items = document.getElementsByClassName(modalName+'-list-item');
Array.from(items).forEach(function(item) {
item.classList.remove('no-hover');
});
});
listContainer.appendChild(div);
}
});
document.getElementById(modalName + 'Input').setAttribute('aria-expanded', count > 0 ? 'true' : 'false');
var status = document.getElementById(modalName + 'Status');
status.textContent = count + ' ' + status.getAttribute('data-results-label');
highlightSelectedItem(modalName);
}
function InitAutocomplete(modalName, data) {
// Focus the input field when the modal is shown and populate the list
$('#' + modalName + 'Modal').on('shown.bs.modal', function () {
$('#' + modalName + 'Input').focus().val('');
populateList(modalName, data);
selectedIndex = 0;
highlightSelectedItem(modalName);
});
// Event listener for input field typing
document.getElementById(modalName + 'Input').addEventListener('input', function() {
populateList(modalName, data);
selectedIndex = 0;
highlightSelectedItem(modalName);
});
// Event listener for arrow keys and enter key
document.getElementById(modalName + 'Input').addEventListener('keydown', function(event) {
var items = document.getElementsByClassName(modalName + '-list-item');
if (event.key === 'ArrowDown') {
event.preventDefault();
if (selectedIndex < items.length - 1) selectedIndex++;
highlightSelectedItem(modalName);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
if (selectedIndex > 0) selectedIndex--;
highlightSelectedItem(modalName);
} else if (event.key === 'Enter' && selectedIndex >= 0 && selectedIndex < items.length) {
event.preventDefault();
items[selectedIndex].click();
}
});
}
function showAutocompleteModal(modalName) {
$('#' + modalName + 'Modal').modal('show');
selectedIndex = -1;
}
</script>
<div id="content" class="container-fluid">
<script>
InitAutocomplete("groupnavigator", groupnavigatorData);
document.body.addEventListener("showReloadPageDialog", function(evt) {
console.log("Forced reload");
location.reload();
});
// 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-bs-toggle', 'modal');
a.setAttribute('data-bs-target', modalTarget);
}
var icon = document.createElement('i');
icon.className = iconClass;
a.appendChild(icon);
slot.appendChild(a);
slot.style.display = '';
var bar = document.getElementById("headerbar");
if (bar) {
bar.classList.add('has-header-action');
}
}
function HideHeaderAction() {
var slot = document.getElementById("header-action-slot");
if (slot) {
slot.innerHTML = '';
slot.style.display = 'none';
slot.style.float = 'left';
}
var bar = document.getElementById("headerbar");
if (bar) {
bar.classList.remove('has-header-action');
}
}
function goBack() {
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");
if (ele.startsWith('#')) {
ele = ele.slice(1);
}
document.getElementById(ele).submit();
return;
/*
const theEvent = new CustomEvent('submit', {
bubbles: true,
cancelable: true
});
if (typeof ele === 'string' || ele instanceof String) {
if (ele.startsWith("#") == true) {
ele = ele.slice(1);
}
if (document.getElementById(ele).dispatchEvent(theEvent) == true) {
// if not cancelled, do normal submit
console.log("calling submit");
document.getElementById(ele).submit();
}
return;
}
// assume ele is the element
if (ele.dispatchEvent(theEvent) == true) {
// if not cancelled, do normal submit
console.log("calling submit");
ele.submit();
}
*/
}
// gioDestroy is a convenience function for destroy callbacks
function gioDestroy(eventCB) {
let internalGioDestroy = function(evt) {
// do not activate on infinite scroll events
if (evt.detail.target !== undefined && evt.detail.target.id != "maincontent-refresh") {
console.log("Ignoring infinite scroll event");
return;
}
// Removed here rather than registered with {once: true}, which
// would discard the listener on the ignored swaps above.
document.body.removeEventListener("htmx:beforeSwap", internalGioDestroy);
document.body.removeEventListener("htmx:historyCacheMiss", internalGioDestroy);
console.log("Calling destroy callback");
eventCB(evt);
}
document.body.addEventListener("htmx:beforeSwap", internalGioDestroy);
document.body.addEventListener("htmx:historyCacheMiss", internalGioDestroy);
}
var drawerCloseFunc = function(evt) {
console.log("in drawerCloseFunc");
// Find all elements with the class 'drawer'
var drawers = document.querySelectorAll('.drawer');
// Iterate over each element and call closeDrawer() on each
drawers.forEach(function(obj) {
drawerObj.closeDrawer(obj);
});
$('.modal').modal('hide');
}
document.body.addEventListener('htmx:beforeSwap', drawerCloseFunc);
document.body.addEventListener('htmx:historyCacheMiss', drawerCloseFunc);
var keydownFunc = function(event) {
var tagName = (event.target.tagName || "").toLowerCase();
if (tagName === 'input' || tagName === 'textarea' || event.target.isContentEditable) {
return;
}
// check if the viewimages viewer is up
let element = document.getElementById('overlay');
if (element && element.classList.contains('visible')) {
return;
}
if (event.ctrlKey && event.shiftKey && (event.key === '?' || event.key === '/')) {
event.preventDefault(); // Prevent the default action
// Show the modal dialog for keyboard shortcuts
$('#shortcutModal').modal('show');
return;
}
// Pressing the chord above delivers a keydown for each key in it:
// Control, then Shift, then '?'. Without this guard the first two fall
// through to the hide below, so re-pressing the chord while the modal is
// open starts a hide that the '?' a moment later interrupts with a show.
// Bootstrap 3 tracks only its most recent backdrop element, so each such
// overlap strands the previous one in the DOM as an undismissable mask.
if (event.key === 'Control' || event.key === 'Shift' || event.key === 'Alt' || event.key === 'Meta') {
return;
}
// The shortcuts modal closes on Escape, which Bootstrap handles; Tab
// and other keys stay inside it.
if (event.key.toLowerCase() === 'g') {
// For Windows and Linux
if (event.ctrlKey) {
event.preventDefault();
$('#shortcutModal').modal('hide');
showAutocompleteModal("groupnavigator");
return;
}
}
// Check if the pressed key is '/'
if ((event.key === 'f' || event.key === 'F') && event.shiftKey) {
if (navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
if (event.metaKey) {
event.preventDefault(); // Prevent the default action
// Focus on the search box
let box = document.getElementById('searchbox')
if (box != null) {
document.getElementById('searchbox').focus();
}
}
} else {
// For Windows and Linux
if (event.ctrlKey) {
event.preventDefault(); // Prevent the default action
// Focus on the search box
let box = document.getElementById('searchbox')
if (box != null) {
document.getElementById('searchbox').focus();
}
}
}
}
}
document.addEventListener('keydown', keydownFunc);
var removeDrawerCloseFunc = function(evt) {
document.body.removeEventListener('htmx:beforeSwap', drawerCloseFunc);
document.body.removeEventListener('htmx:historyCacheMiss', drawerCloseFunc);
document.removeEventListener('keydown', keydownFunc);
}
document.body.addEventListener("htmx:historyCacheMiss", removeDrawerCloseFunc, {once: true});
// Check for passkey enrollment prompt after cross-device authentication
$(function() {
if (sessionStorage.getItem('passkeyPromptAfterCDA') === 'true') {
console.log('Passkey CDA flag detected, showing enrollment modal');
// Show the modal
$('#passkeyEnrollmentModal').modal('show');
// Clear the flag when modal is dismissed
$('#passkeyEnrollmentModal').on('hidden.bs.modal', function() {
console.log('Modal dismissed, clearing flag');
sessionStorage.removeItem('passkeyPromptAfterCDA');
});
// Set up enrollment button handler
var enrollBtn = document.getElementById('enrollLocalPasskeyBtn');
if (enrollBtn) {
enrollBtn.addEventListener('click', function() {
console.log('Enrollment button clicked');
// Redirect to settings page where they can add a passkey
sessionStorage.removeItem('passkeyPromptAfterCDA');
window.location.href = 'https://groups.io/settings/security';
});
}
}
});
</script>
<!-- Passkey Enrollment Modal -->
<div class="modal fade webauthn-only" id="passkeyEnrollmentModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<h4 class="modal-title">
<i class="fa fa-fingerprint"></i>
Add a Passkey to This Device
</h4>
</div>
<div class="modal-body">
<p>
You logged in using a passkey from another device. For faster access next time, add a passkey to this device.
</p>
<p>
<strong>Benefits:</strong>
</p>
<ul>
<li>Faster login with just your fingerprint or face</li>
<li>No need to use your phone each time</li>
<li>Works even when offline</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Maybe Later
</button>
<button type="button" class="btn btn-primary" id="enrollLocalPasskeyBtn">
<i class="fa fa-plus"></i>
Add Passkey Now
</button>
</div>
</div>
</div>
</div>
<!-- Modal Dialog for Keyboard Shortcuts -->
<div class="modal fade" id="shortcutModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="myModalLabel">Keyboard Shortcuts</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-3 text-right"><strong>ctrl</strong> + <strong>shift</strong> + <strong>?</strong>&nbsp;&nbsp;:</div>
<div class="col-9" style="margin-left: -23px;">Show all keyboard shortcuts</div>
</div>
<div class="row">
<div class="col-3 text-right"><strong>ctrl</strong> + <strong>g</strong>&nbsp;&nbsp;:</div>
<div class="col-9" style="margin-left: -23px;">Navigate to a group</div>
</div>
<div class="row">
<div class="col-3 text-right"><strong class="ctrlmeta">ctrl</strong> + <strong>shift</strong> + <strong>f</strong>&nbsp;&nbsp;:</div>
<div class="col-9" style="margin-left: -23px;">Find</div>
</div>
<div class="row">
<div class="col-3 text-right"><strong class="ctrlmeta">ctrl</strong> + <strong>/</strong>&nbsp;&nbsp;:</div>
<div class="col-9" style="margin-left: -23px;">Quick actions</div>
</div>
</div>
<div class="text-center" style="margin-top:10px; margin-bottom:10px; font-size:12px;"><strong>esc</strong> to dismiss</div>
</div>
</div>
</div>
<div id="mainrow" class="" 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 id="groupsidebar" class="scrollbar hidden-print">
<div class="scrollbar-content">
<div id="accordion" role="navigation" aria-label="Sidebar" style="margin-bottom:20px;">
<div class="list-group noborderlist" style="border:0;margin-bottom:0px">
<a title="Group Home" href="https://groups.io/g/MTFHRT" id="sidebar-home" class="list-group-item sidebar-item">
<i class="fa-fw fa fa-home fa-lg"></i>
Home
</a>
<a title="Messages" href="https://groups.io/g/MTFHRT/topics?sidebar=true" id="sidebar-archives" class="noborderitem list-group-item sidebar-item sidebar-item-archives">
<i class="fa-fw fa fa-comments fa-lg"></i>
Messages
<span id="sidebar-archives-badge" class="badge"></span>
</a>
<a title="Hashtags" href="https://groups.io/g/MTFHRT/hashtags" id="sidebar-hashtags" class="noborderitem list-group-item sidebar-item sidebar-item-hashtags">
<i class="fa-fw fa fa-hashtag fa-lg"></i>
Hashtags
<span id="sidebar-hashtags-badge" class="badge"></span>
</a>
<a title="Wiki" href="https://groups.io/g/MTFHRT/wiki" id="sidebar-wiki" class="noborderitem list-group-item sidebar-item sidebar-item-wiki active">
<i class="fa-fw fa fa-book fa-lg"></i>
Wiki
<span id="sidebar-wiki-badge" class="badge"></span>
</a>
</div>
</div>
<div class="text-center" style="font-size:13px;line-height:25px;margin-bottom:30px">
<div style="padding-bottom:10px;">
<strong>ctrl</strong> + <strong>shift</strong> + <strong>?</strong> for shortcuts
</div>
<div style="padding-bottom:5px;">
<a href="https://groups.io/static/about">
About
</a>
&middot;
<a href="https://groups.io/static/features">
Features
</a> &middot;
<a href="https://groups.io/static/pricing">
Pricing
</a>
</div>
<div style="padding-bottom:5px;">
<a href="https://groups.io/home">
Home
</a> &middot;
<a rel="nofollow" href="https://groups.io/changelog">
Changelog
</a> &middot;
<a href="https://groups.io/static/tos">
Terms
</a>
</div>
<div style="padding-bottom:5px;">
<a href="https://groups.io/helpcenter" style="display:inline-block;padding:4px 0;" target="_blank">
Help
</a>
</div>
<div style="padding-bottom:5px;">
&copy; 2026 Groups.io
</div>
</div>
</div>
</div>
<div id="maincontent" class="groupmaincontent" role="main">
<div id="maincontent-refresh">
<div id="maincontent-column" class="col-xs-12 maincontent-column">
<h1 class="visually-hidden" tabindex="-1">MTFHRT@groups.io | Wiki | Basic Estrogel Guide</h1>
<div id="alertdiv"></div>
<div class="noticetemplate template">
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<span class="flashmsg"></span>
</div>
</div>
<div class="alerttemplate template">
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<span class="flashmsg"></span>
</div>
</div>
<div class="alertnoclosetemplate template">
<div class="alert alert-danger" role="alert">
<span class="flashmsg"></span>
</div>
</div>
<script>
function dismiss() {
console.log("dismiss");
$.get( "/dismissalert");
}
var $alerttemplate = $(".alerttemplate");
var $alertnoclosetemplate = $(".alertnoclosetemplate");
var $noticetemplate = $(".noticetemplate");
// alertText holds translated messages for alerts raised by shared scripts
// (the editor and the ajax retry helper) that have no translator of their own.
var alertText = {
draftSaveError: "There was an error saving the draft.",
draftDeleted: "This draft was deleted in another window, so your changes are not being saved. Copy your text before you leave this page.",
drafts: "Drafts",
retryAutomatically: "We will retry automatically.",
retryCountdown: "We will retry again in %s seconds.",
retryNow: "Retry now",
sessionExpired: "Your login has expired, so your changes are not being saved. Do not close or reload this page. \u003ca href=\"/login\" target=\"_blank\" rel=\"noopener\">Log in again in a new tab\u003c/a>, then return here to send your message.",
uploadFailed: "Failed to upload %s",
imageUploadFailed: "1 image failed to upload. Please try again.",
imagesUploadFailed: "%s images failed to upload. Please try again.",
uploadLoginExpired: "Your login has expired, so this file was not uploaded. Log in again in a new tab, then try again.",
loginExpired: "Your login has expired, so your change was not saved. \u003ca href=\"%s\">Log in again\u003c/a>, then try again.",
changeNotSaved: "Your change was not saved. Please try again.",
voteNotSaved: "Your vote was not saved. Please try again."
};
// alertRequestFailed shows a persistent error alert for a failed background
// request. A 401 means the login has expired, so the alert links to the
// login page, which returns to this page afterwards. Otherwise failedMsg
// is shown.
function alertRequestFailed(xhr, failedMsg) {
if (xhr && xhr.status === 401) {
var loginURL = "/login?r=" + encodeURIComponent(window.location.href);
return createAlert(alertText.loginExpired.replace("%s", $("<div>").text(loginURL).html()), true, false);
}
return createAlert(failedMsg, true, false);
}
// alertReturnFocus moves focus back to where it was before it entered a
// closing alert, or to the page heading if that element is gone, so focus
// is never dropped onto the page body.
function alertReturnFocus(returnTo) {
if (returnTo && returnTo !== document.body && document.body.contains(returnTo)) {
// A rich-text editor lives in an iframe; focus the editor itself
// so the caret comes back with it.
if (returnTo.tagName === 'IFRAME' && window.tinymce && tinymce.get) {
var editors = tinymce.get();
for (var i = 0; i < editors.length; i++) {
var container = editors[i].getContainer && editors[i].getContainer();
if (container && container.contains(returnTo)) {
editors[i].focus();
return;
}
}
}
returnTo.focus();
return;
}
var heading = document.querySelector('#maincontent-column h1[tabindex="-1"]') || document.querySelector('h1[tabindex="-1"]');
if (heading) {
heading.focus();
}
}
// createAlert shows msg in #alertdiv. Error alerts stay until dismissed.
// An autoClose notice closes after 10 seconds, but waits while the pointer
// is over it or keyboard focus is inside it.
function createAlert(msg, isError, autoClose, noClose) {
var $newPanel;
if (isError == false) {
$newPanel = $noticetemplate.clone();
} else {
if (noClose) {
$newPanel = $alertnoclosetemplate.clone();
} else {
$newPanel = $alerttemplate.clone();
}
}
$newPanel.removeClass("template");
$newPanel.find(".flashmsg").html(msg);
var panel = $newPanel[0];
// Remember where focus came from so closing the alert can return it.
// Focus arriving from an iframe (the rich-text editor) or from script
// has no relatedTarget, so start with the element focused when the
// alert was raised.
var returnTo = document.activeElement;
var hovered = false;
var focused = false;
panel.addEventListener('focusin', function(e) {
if (!focused && e.relatedTarget && !panel.contains(e.relatedTarget)) {
returnTo = e.relatedTarget;
}
focused = true;
});
// Bootstrap removes the inner .alert before firing closed.bs.alert on
// it, so that event never reaches the wrapper; listen on the .alert.
var alertEl = panel.querySelector('.alert');
var hadFocus = false;
if (alertEl) {
alertEl.addEventListener('close.bs.alert', function() {
hadFocus = panel.contains(document.activeElement);
});
alertEl.addEventListener('closed.bs.alert', function() {
$newPanel.remove();
if (hadFocus) {
alertReturnFocus(returnTo);
}
});
}
// closeAlert removes the alert without dropping keyboard focus.
$newPanel.closeAlert = function() {
var focusInside = panel.contains(document.activeElement);
$newPanel.remove();
if (focusInside) {
alertReturnFocus(returnTo);
}
};
if (autoClose == true && isError == false) {
var closeTimer = null;
var scheduleClose = function() {
clearTimeout(closeTimer);
closeTimer = setTimeout(function() {
if (hovered || focused || !panel.isConnected) {
return;
}
$newPanel.slideUp(500, $newPanel.closeAlert);
}, 10000);
};
panel.addEventListener('mouseenter', function() {
hovered = true;
clearTimeout(closeTimer);
});
panel.addEventListener('mouseleave', function() {
hovered = false;
scheduleClose();
});
panel.addEventListener('focusin', function() {
clearTimeout(closeTimer);
});
panel.addEventListener('focusout', function(e) {
if (!panel.contains(e.relatedTarget)) {
focused = false;
scheduleClose();
}
});
scheduleClose();
} else {
panel.addEventListener('focusout', function(e) {
if (!panel.contains(e.relatedTarget)) {
focused = false;
}
});
}
$("#alertdiv").append($newPanel.fadeIn());
return $newPanel;
}
</script>
<div class="wiki-breadcrumb-wrap">
<div class="row breadcrumb-row" style="margin-bottom: 10px">
<div class="col-12 col-md-9 ps-0">
<span class="d-md-none float-end" style="padding:8px 15px;"><button type="button" class="btn btn-link p-0 border-0 align-baseline" data-bs-toggle="modal" data-bs-target="#searchModal" aria-label="Search"><i class="fa fa-search"></i></button></span>
<div class="breadcrumb-container">
<ol class="breadcrumb scroll-breadcrumb">
<li class="breadcrumb-item"><a href="https://groups.io/g/MTFHRT"><i class="fa fa-home"></i> MTFHRT</a></li>
<li class="breadcrumb-item"><a href="https://groups.io/g/MTFHRT/wiki"><i class="fa fa-book"></i> Wiki</a></li>
<li class="breadcrumb-item active"><i class="fa fa-book"></i> Basic Estrogel Guide</li>
</ol>
</div>
</div>
<div class="col-md-3 d-none d-md-block" style="padding-right:0px">
<form class="float-end" method="get" action="https://groups.io/g/MTFHRT/wikisearch">
<div class="input-group">
<input id="searchbox" type="text" class="form-control" placeholder="Search" title="Search" aria-label="Search" name="q" size="20" style="height:37px">
<button class="btn btn-primary" type="submit" style="height:37px;" aria-label="Search">
<span class="fa fa-search"></span>
</button>
</div>
</form>
</div>
</div>
<div class="modal fade" id="searchModal" tabindex="-1" role="dialog" aria-labelledby="searchModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="searchModalLabel">
Search
</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="get" action="https://groups.io/g/MTFHRT/wikisearch">
<div class="modal-body">
<div class="form-group">
<div class="col-sm-12">
<input type="text" class="form-control" placeholder="Search" title="Search" name="q">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary btn-sm" aria-label="Search">
<i class="fa fa-search"></i>
Search
</button>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">
<i class="fa fa-times"></i>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row" style="margin-bottom:20px;">
<div class="col-12">
<span class="d-print-none">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Pages
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="../37530.html">
Basic Estrogel Guide
</a>
</li>
<li>
<a class="dropdown-item" href="../30846.html">
Home
</a>
</li>
<li>
<a class="dropdown-item" href="../32288.html">
How to measure melting point of most organic powders using typical household items.
</a>
</li>
<li>
<a class="dropdown-item" href="../34293.html">
steam sterilization
</a>
</li>
<li>
<a class="dropdown-item" href="../29602.html">
Ultimate DIY
</a>
</li>
</ul>
</div>
</span>
<div class="float-end">
<a href="history.html">
<span class="visually-hidden">Page history, last edited</span>
<script id="timedispupdated209879">
$('#timedispupdated209879').replaceWith(DisplayShortTime(1724794152492350175, false
));
</script>
</a>
</div>
<h2 class="wiki-page-title">
Basic Estrogel Guide
</h2>
</div>
</div>
<div class="row">
<div class="col-md-12">
<span class="wikidisplay user-content">
<h1>DIY Estrogel Guide</h1>
<p>This guide will instruct you on the materials and tools needed for home DIY basic estrogel compounding. There are easier methods out there, such as mixing estradiol into pre-made hand sanitizers, but this is for those who would prefer to compound their own gels, or manufacture at a scale where buying hand sanitizer is cost prohibitive. There are also some concerns about hand sanitizer formulations containing denaturing agents or other additives that might be bad for transdermal drug delivery. If you are interested in more experimental gel formulations or ones based on hand sanitizer, you can check out reddit.com/r/estrogel/wiki, which is where I got most of this information. <br/><br/>With this guide, you can produce supersaturated estrogels that are suitable for high-dose estradiol monotherapy. This is a great alternative to injections for the needle-shy or paranoid. <br/><br/>This recipe is based off of a patent that includes a generic estrogel recipe, available here: <a href="https://patents.google.com/patent/US20070154533A1/en" rel="nofollow noopener" target="_blank">https://patents.google.com/patent/US20070154533A1/en</a> - page 39 of the PDF has the recipe. This recipe has been adjusted for higher concentrations and higher absorption, using 60% ethanol, which is known to have the best absorption for estradiol. (<a href="https://sci-hub.se/https://doi.org/10.1016/j.addr.2012.09.032" rel="nofollow noopener" target="_blank">https://sci-hub.se/https://doi.org/10.1016/j.addr.2012.09.032</a>)</p>
<p> </p>
<h3>Sourcing Estradiol</h3>
<p>The sources for estradiol are much the same as DIY injections, so I won&#39;t go into detail here. Look on made-in-china.com and search for 17b-estradiol, or more specifically, CAS number 50-28-2. For a single person, assuming they are doing monotherapy at 1-2mg a day, a typical yearly consumption is half a gram. This means 50 grams is enough to last you 100 years. I got 100 grams for $200 shipped from Hubei Vanz. You want 17 beta estradiol, not estradiol valerate, cypionate, enanthate, etc. </p>
<h3><br/>Other ingredients</h3>
<p>Other ingredients, including the quantity and price I got them before, are listed below.</p>
<ul>
<li>Carbomer/Carbopol 980 x 120g - $25 - amazon, ebay, cosmetic supply, aliexpress</li>
<li>Triethanolamine x 250ml - $20 - amazon, ebay, cosmetic supply</li>
<li>Distilled Water - $2-3 from your local grocery store</li>
<li>95% (190 proof) grain alcohol, such as everclear - 750 ml - $20 - liquor store (any alcohol over 60%/120 proof is okay)</li>
</ul>
<h3>Tools</h3>
<ul>
<li>Coffee frother wand - $5-10 on ebay, aliexpress, amazon. The longer and narrower the better.</li>
<li>Airless pump dispenser bottles - I have used this style(<a href="https://m.media-amazon.com/images/I/51D0UnNSu+L._SL1500_.jpg" rel="nofollow noopener" target="_blank">https://m.media-amazon.com/images/I/51D0UnNSu+L._SL1500_.jpg</a>) in the past because one pump = 225mg which is roughly equivalent to .5mg of estradiol (at .24% concentration). This works out well for me because it means two pumps a day is enough for monotherapy. It also means 1 80 ml bottle (80 ml is just under the 3 oz limit on airplanes) is enough to last me roughly a month. These might not be the easiest pumps to work with though, but more on that later.</li>
<li>100ml syringes with cap (like these <a href="https://www.amazon.com/100ml-Syringe-s-Caps-Industrial-Scientific/dp/B086H7TC1T" rel="nofollow noopener" target="_blank">https://www.amazon.com/100ml-Syringe-s-Caps-Industrial-Scientific/dp/B086H7TC1T</a>) - also might not be necessary, more on that later</li>
<li>milligram scale such as the gemini-20 - $20-30</li>
<li>mixing container of some kind, such as a mason jar, glass cup, beaker, etc.</li>
<li>optional - metal stirrers and weighing spatulas such as these: <a href="https://www.amazon.com/gp/product/B07HHWCNB9/" rel="nofollow noopener" target="_blank">https://www.amazon.com/gp/product/B07HHWCNB9/</a> - $10<br/><br/></li>
</ul>
<p><strong>Total:</strong> I spent about $350 to get started, but you can get everything much cheaper if you are willing to shop around and buy small quantities. The amount of carbomer, estradiol, triethanolamine, etc. per bottle is very little, so even small amounts will last you a long time. If you spent what I did, raw materials alone (not counting tools) and the pump bottle is about $5-10 per bottle. The most expensive components for me are ethanol and airless pumps. Buying airless pumps in bulk from alibaba or made-in-china sellers can get prices down lower, I know some also try using 99% isopropyl alcohol instead of ethanol, though I haven&#39;t tried this. One 80ml bottle will last you about a month at .5mg a day @ .24% concentration.</p>
<h3>Preparation of gel</h3>
<p>Gel preparation is quite simple, first you need to find out how much alcohol and water you need. You are trying to reach a concentration of 60/40 ethanol to water. You can easily find out how much water you need to reach an overall concentration of 60%(by volume). You can easily find online calculators for this, such as here: <a href="https://ezcalc.me/alcohol-dilution-calculator/" rel="nofollow noopener" target="_blank">https://ezcalc.me/alcohol-dilution-calculator/</a>. Assuming you want to make 100ml of gel, for example, and have 95%(190 proof) grain alcohol, you&#39;ll need ~63ml of 95% ethanol and 37ml of distilled water. From here on out I will assume you want 100ml of gel and have 95% ethanol.</p>
<p> </p>
<h4>Recipe</h4>
<p>63ml ethanol<br/>37ml distilled water<br/>1g Carbomer 980<br/>1.35g Triethanolamine<br/>Estradiol to your desired concentration, I usually do .24%(by weight of the entire solution, accounting for the density of 95% ethanol at ~.8g/ml), which comes to ~215mg.</p>
<p> </p>
<h4>Procedure</h4>
<ol>
<li>Pour the ethanol into a mixing container, such as a beaker or jar.</li>
<li>Weigh your estradiol on a weighing paper, weighing boat, or a piece of tinfoil with your weighing spatulas, or another instrument.</li>
<li>Dissolve your estradiol into your ethanol and stir with a stirring instrument or coffee frother. Your solution should be clear, without any undissolved material.</li>
<li>Add water and stir.</li>
<li>Add Carbomer 980, and mix with coffee frother. This can take a long time to dissolve completely. I usually stir it with the coffee frother until most of it dissolves, then I let the solution sit while covered for 20 minutes. Then, I come back and stir it again until there are no clumps or undissolved powder.</li>
<li>Remove plunger from 100ml syringe. Twist on luer lock cap.</li>
<li>Transfer gel solution into your 100ml syringe, from the plunger side.</li>
<li>Add triethanolamine to your solution, stirring with your coffee frother. The solution will now go from liquid to a thicker gel, much like hand sanitizer. Stir extremely thoroughly and try to distribute the triethanolamine throughout the entire solution.</li>
<li>Insert plunger into back of syringe and remove the luer lock cap.</li>
<li>Use syringe to dispense gel into your airless pump of choice. You may want to stop a few times to let the gel settle to avoid huge air bubbles. You can tap the bottom of the airless pump against a hard surface to make the gel settle.</li>
</ol>
<p> </p>
<h3>Potential improvements and challenges</h3>
<p>The hardest part of this procedure is working with the gel after triethanolamine has been added, as it almost immediately becomes very thick, clumpy, sticky, etc. You do not want to add triethanolamine to the gel before it is either in its final container, or in a syringe. If you add triethanolamine in the mixing container, it will be extremely difficult to transfer. If you get airless pumps of this style: <a href="https://www.aliexpress.com/item/2251832632413742.html" rel="nofollow noopener" target="_blank">https://www.aliexpress.com/item/2251832632413742.html</a> you may be able to add the triethanolamine to the solution after it&#39;s already in the pump, however the mouth needs to be wide enough to fit your coffee frother. You need to be able to thoroughly mix the triethanolamine so that it can neutralize the pH of the carbomer 980. I haven&#39;t tried these bottles, but I have some other candidates on order and will update if I find a better bottle.<br/><br/>One possible area of improvement is adding a penetration enhancer, such as oleic acid, d-limonene, or isopropyl myristate. I personally experimented with 2% isopropyl myristate but found it incredibly irritating to my skin, and made the gel burn really badly. It also fell out of solution in my pump bottle. Personally, I just apply the gel scrotally, which results in extremely high absorption, so I feel no need to use penetration enhancers.</p>
<p> </p>
<h3>Usage/my results</h3>
<p>With the bottles I recommend, each pump will dispense about 225mg of gel, or .5mg per pump. I used 4 pumps a day at first, putting 1 pump on my scrotum, and 1 on my thigh, morning and night, and after a month of this, I had the following blood test results:<br/>Testosterone: 29 ng/dL<br/>Estradiol: 326 pg/mL<br/><br/>I am currently lowering my dose to 2 pumps a day, on the scrotum only, to see if 1mg scrotally is enough for monotherapy. I will report back once I have blood test results. Although conventional wisdom with transdermal delivery says that transdermal gels have a depot effect, scrotal application has not been studied much and it could be the case that absorption is much quicker, so to be safe I apply twice a day rather than using 2 pumps once a day, and I also get my blood tests done at trough (before I do my morning dose).<br/><br/>This is a promising alternative to injections, and because it&#39;s alcohol based, the risk of contamination is low, even without a clean room or heat sterilization. This is also a great alternative for those who are needle-phobic or are for some reason not wanting to do injections. <br/><br/>[contact info removed at request of the author]</p>
<p>Discussion: <a href="https://groups.io/g/MTFHRT/topic/wiki_article_about_estrogel/93220481" rel="nofollow noopener" target="_blank">https://groups.io/g/MTFHRT/topic/wiki_article_about_estrogel/93220481</a></p>
<p />
</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
</div>
</div>
<script>
contentLoaded( false , function() {
UpdateSidebar("wiki");
if (document.documentElement.clientWidth <= 992) {
ShowHeaderAction("fa fa-search fa-lg", "#searchModal", "Search");
}
EnableTopbarAutoHide();
});
</script>
</div>
</div>
</div>
</div>
<div class="row hidden-print" id="mobileSidebar" 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 id="mobileSidebarSub" class="col-12 d-lg-none">
<div class="navbar fixed-bottom">
<div class="container" style="display: flex; justify-content: space-around;">
<a id="mobilesidebar-groups" class="mobilesidebar-item" style="margin-top:10px; padding-bottom:20px; flex: 1; text-align: center; color: inherit; text-decoration: none;" href="https://groups.io/groups" hx-boost="false">
<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>
</a>
<div id="mobilesidebar-archives" class="mobilesidebar-item" style="margin-top:10px; padding-bottom:20px; flex: 1; text-align: center;" role="link" tabindex="0" hx-trigger="click, keyup[key=='Enter']" hx-get="https://groups.io/g/MTFHRT/topics?sidebar=true" 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-comments fa-2x"></i>
</div><br>
<span style="font-size:12px;">Messages</span>
</div>
</div>
<div style="flex: 1; text-align: center;margin-top:10px" data-drawer-trigger role="button" tabindex="0" aria-controls="drawer-name" aria-expanded="false">
<i class="fa-fw fa-light fa-bars fa-2x"></i><br>
<span style="font-size:12px;">More</span>
</div>
</div>
</div>
<section class="drawer drawer-left" id="drawer-name" data-drawer-target>
<div class="drawer-overlay" data-drawer-close tabindex="-1"></div>
<div class="drawer-wrapper drawer-wrapper-top">
<div class="drawer-header">
<div class="drawer-title">
<i class="fa-fw fa-regular fa-bars fa-3x"></i>
<span class="visually-hidden">
More Options
</span>
More
</div>
<button class="drawer-close" data-drawer-close aria-label="Close Drawer"></button>
</div>
<div class="drawer-content">
<div class="list-group noborderlist">
<a title="Group Home" href="https://groups.io/g/MTFHRT" id="sidebar-home-mobile" class="list-group-item sidebar-item">
<i class="fa-fw fa fa-home fa-lg"></i>
Home
</a>
<a title="Messages" href="https://groups.io/g/MTFHRT/topics?sidebar=true" id="sidebar-archives-mobile" class="noborderitem list-group-item sidebar-item sidebar-item-archives">
<i class="fa-fw fa fa-comments fa-lg"></i>
Messages
<span id="sidebar-archives-mobile-badge" class="badge"></span>
</a>
<a title="Hashtags" href="https://groups.io/g/MTFHRT/hashtags" id="sidebar-hashtags-mobile" class="noborderitem list-group-item sidebar-item sidebar-item-hashtags">
<i class="fa-fw fa fa-hashtag fa-lg"></i>
Hashtags
<span id="sidebar-hashtags-mobile-badge" class="badge"></span>
</a>
<a title="Wiki" href="https://groups.io/g/MTFHRT/wiki" id="sidebar-wiki-mobile" class="noborderitem list-group-item sidebar-item sidebar-item-wiki active">
<i class="fa-fw fa fa-book fa-lg"></i>
Wiki
<span id="sidebar-wiki-mobile-badge" class="badge"></span>
</a>
<a class="noborderitem list-group-item sidebar-item" href="https://groups.io/static/about" style="border-top:5px solid #ddd;">
<i class="fa-fw fal fa-envelope fa-lg"></i>
About
</a>
<a class="noborderitem list-group-item sidebar-item" href="https://groups.io/static/features">
<i class="fa-fw far fa-tasks fa-lg"></i>
Features
</a>
<a class="noborderitem list-group-item sidebar-item" href="https://groups.io/static/pricing">
<i class="fa-fw fa fa-dollar-sign fa-lg"></i>
Pricing
</a>
<a class="noborderitem list-group-item sidebar-item" href="https://groups.io/changelog">
<i class="fa-fw fa fa-history fa-lg"></i>
Changelog
</a>
<a class="noborderitem list-group-item sidebar-item" href="https://groups.io/static/tos">
<i class="fa-fw fal fa-newspaper fa-lg"></i>
Terms
</a>
<a class="noborderitem list-group-item sidebar-item" href="https://groups.io/helpcenter">
<i class="fa-fw fa fa-info-circle fa-lg"></i>
Help
</a>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- Modal Dialog for Autocomplete -->
<div class="modal fade" id="quickActionsModal" tabindex="-1" role="dialog" aria-label="Go to page..." aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<input type="text" class="form-control" id="quickActionsInput" placeholder="Go to page..." aria-label="Go to page..." role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="quickActionsContainer" autocomplete="off" style="margin-bottom:10px">
<div id="quickActionsContainer" class="autocompleteResults" role="listbox" aria-label="Go to page..." style="max-height:250px; overflow-y: auto;"></div>
<div id="quickActionsStatus" class="visually-hidden" role="status" data-results-label="results"></div>
<div class="text-center" style="margin-top:10px; font-size:12px;"><i class="fa-sharp fa-arrow-up-arrow-down"></i> to navigate&nbsp;&nbsp;&nbsp;<i class="fa-sharp fa-arrow-turn-down-left"></i> to use&nbsp;&nbsp;&nbsp;<strong>esc</strong> to dismiss</div>
</div>
</div>
</div>
</div>
<script>
var quickActionsData = [
{name: "Group Home", url: "https://groups.io/g/MTFHRT"},
{name: "Topics", url: "https://groups.io/g/MTFHRT/topics"},
{name: "Messages", url: "https://groups.io/g/MTFHRT/messages"},
{name: "Expanded Messages", url: "https://groups.io/g/MTFHRT/messages?expanded=1"},
{name: "Polls", url: "https://groups.io/g/MTFHRT/polls"},
{name: "Hashtags", url: "https://groups.io/g/MTFHRT/hashtags"},
{name: "Wiki", url: "https://groups.io/g/MTFHRT/wiki"},
/* other pages */
{name: "Help", url: "/helpcenter"},
{name: "Feed", url: "/feed"},
{name: "Your Groups", url: "/groups"},
{name: "Your Calendar", url: "/calendar"},
{name: "Your Activity", url: "/activity"},
{name: "Your Pending Messages", url: "/pendingmsgs"},
{name: "Account Login", url: "/account"},
{name: "Account Preferences", url: "/account?page=prefs"},
{name: "Account Security", url: "/account?page=security"},
{name: "Account Identity", url: "/account?page=profile&member_info_id=0"},
{name: "Account Notifications", url: "/account?page=notifications"},
{name: "Account Other Services", url: "/account?page=oauth"},
{name: "Account Bounces", url: "/account?page=bounces"},
{name: "Account Billing", url: "/account?page=billing"},
{name: "Find a Group", url: "/search?p=SubsCount,,,20,2,0,0"},
];
InitAutocomplete("quickActions", quickActionsData);
document.addEventListener('keydown', function(event) {
var tagName = (event.target.tagName || "").toLowerCase();
if (tagName === 'input' || tagName === 'textarea' || event.target.isContentEditable) {
return;
}
// check if the viewimages viewer is up
let element = document.getElementById('overlay');
if (element && element.classList.contains('visible')) {
return;
}
if (event.key.toLowerCase() === '/') {
if (navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
// For Mac
// Check if Command (event.metaKey) is pressed, and either Option (event.altKey) is pressed or not
if (event.metaKey) {
event.preventDefault();
showAutocompleteModal("quickActions");
return;
}
} else {
// For Windows and Linux
if (event.ctrlKey) {
event.preventDefault();
showAutocompleteModal("quickActions");
return;
}
}
}
});
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) {
// Message bodies can contain pasted copies of this markup; skip items without an icon.
var iElement = item.querySelector('div i');
if (!iElement) {
return;
}
if (item.id == "mobilesidebar-" + id) {
console.log("making solid " + id)
iElement.classList.add('fa-solid');
iElement.classList.remove('fa-light');
} else {
iElement.classList.remove('fa-solid');
iElement.classList.add('fa-light');
}
});
var sidebarItems = document.querySelectorAll('.sidebar-item');
// remove 'active' class from all 'sidebar-item' elements
sidebarItems.forEach(function(item) {
if (item.id == "sidebar-" + id) {
console.log("making active " + id)
item.classList.add('active');
// A boosted link from outside the sidebar (e.g. Export on the
// topics page) doesn't redraw the sidebar, so open any
// collapsed menus around the item to show the selection.
var menu = item.parentElement ? item.parentElement.closest('.collapse:not(.show)') : null;
while (menu) {
bootstrap.Collapse.getOrCreateInstance(menu, {toggle: false}).show();
menu = menu.parentElement ? menu.parentElement.closest('.collapse:not(.show)') : null;
}
} else {
item.classList.remove('active');
}
});
}
</script>
</div>
<script>
// Pages without a sidebar layout have no #maincontent; their whole
// content container is the main region and the skip link's target.
if (!document.getElementById('maincontent')) {
document.getElementById('content').setAttribute('role', 'main');
document.getElementById('skip-link').setAttribute('href', '#content');
}
</script>
<script>
// Site-wide accessibility behaviour. The page-level parts run on load and
// again after every HTMX swap; the listeners are installed once per
// document (an HTMX history miss re-runs this script into the same one).
// isShown reports whether an element takes up space on the page, the
// test Bootstrap itself uses before returning focus to a trigger.
function a11yIsShown(el) {
return el.getClientRects().length > 0 && getComputedStyle(el).visibility !== 'hidden';
}
// a11yLinkDescription adds id to el's aria-describedby.
function a11yLinkDescription(el, id) {
var ids = (el.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
if (ids.indexOf(id) === -1) {
ids.push(id);
el.setAttribute('aria-describedby', ids.join(' '));
}
}
// a11yIdCounter only ever increases. No initializer: this script runs
// again when HTMX restores a page from history, after that page's
// content has been enhanced, and resetting the counter would reissue
// ids. Ids already in the page, such as ones in content restored from
// the history cache after a reload, are skipped.
var a11yIdCounter;
function a11yEnsureID(el, prefix) {
if (!el.id) {
var id;
do {
a11yIdCounter = (a11yIdCounter || 0) + 1;
id = prefix + '-' + a11yIdCounter;
} while (document.getElementById(id));
el.id = id;
}
return el.id;
}
// a11yFeedbackFor finds the .invalid-feedback that belongs to an
// .is-invalid element: a following sibling (BS5 shows the feedback only
// after an .is-invalid sibling), else a direct child of its parent.
function a11yFeedbackFor(el) {
for (var sib = el.nextElementSibling; sib; sib = sib.nextElementSibling) {
if (sib.classList.contains('invalid-feedback')) {
return sib;
}
}
if (el.parentElement) {
return el.parentElement.querySelector(':scope > .invalid-feedback');
}
return null;
}
var a11yControlSelector = 'input:not([type="hidden"]), select, textarea';
// a11yEnhance fixes up the markup inside root.
// a11yQueryAll returns the elements under root that match selector,
// root included: an element HTMX inserts is enhanced as a whole.
function a11yQueryAll(root, selector) {
var found = Array.prototype.slice.call(root.querySelectorAll(selector));
if (root.matches && root.matches(selector)) {
found.unshift(root);
}
return found;
}
function a11yEnhance(root) {
// Form errors: each invalid control says it is invalid and is
// described by its error message. The .is-invalid class sits on the
// control, or on a wrapper such as an .input-group.
a11yQueryAll(root, '.is-invalid').forEach(function(el) {
var control = el.matches(a11yControlSelector) ? el : el.querySelector(a11yControlSelector);
if (!control) {
return;
}
control.setAttribute('aria-invalid', 'true');
var feedback = a11yFeedbackFor(el);
if (feedback) {
a11yLinkDescription(control, a11yEnsureID(feedback, 'invalid-feedback'));
}
});
// Help text: a .form-text that follows a form group's only control
// describes it. Groups with several controls are left alone, since
// the help can't be tied to one of them.
a11yQueryAll(root, '.form-text').forEach(function(help) {
var group = help.parentElement;
if (!group) {
return;
}
var controls = group.querySelectorAll(a11yControlSelector);
if (controls.length !== 1) {
return;
}
var control = controls[0];
if (control.hasAttribute('aria-describedby') && control.getAttribute('aria-invalid') !== 'true') {
return;
}
// The help must come after the control (or its input group).
var anchor = control.closest('.input-group') || control;
if (!(anchor.compareDocumentPosition(help) & Node.DOCUMENT_POSITION_FOLLOWING)) {
return;
}
a11yLinkDescription(control, a11yEnsureID(help, 'form-text'));
});
// Font Awesome draws its glyphs as private-use characters, which
// screen readers read out or which end up in a control's name, so
// icons are hidden from them. An icon that carries a name, or holds
// visually hidden text, is kept; so is an icon that is the only
// content of an unnamed control, which would otherwise have no name.
a11yQueryAll(root, 'i[class*="fa-"]:not([aria-hidden]):not([aria-label]):not([role]):not([title]):not([data-bs-original-title])').forEach(function(icon) {
if (icon.querySelector('.visually-hidden, .sr-only')) {
return;
}
var control = icon.closest('a, button, [role="button"], [role="link"]');
var unlabelled = control && !control.hasAttribute('aria-label') && !control.hasAttribute('aria-labelledby') && !control.hasAttribute('title');
if (unlabelled && control.textContent.trim() === '' && !control.querySelector('img[alt]:not([alt=""])')) {
return;
}
// A control whose text is shown only at some widths (d-none
// d-md-inline) has no name where it is hidden, so the text
// becomes its label at every width.
if (unlabelled && control.querySelector('[class*="d-none"]')) {
control.setAttribute('aria-label', control.textContent.replace(/\s+/g, ' ').trim());
}
icon.setAttribute('aria-hidden', 'true');
});
// Dropzone drop areas open the file chooser only for a mouse. Where a
// page has no .dz-choose button, the drop area's message becomes a
// button; the role="button" key handler below clicks it, and Dropzone
// opens the chooser for a click on its message.
a11yQueryAll(root, 'form.dropzone').forEach(function(form) {
if (form.querySelector('.dz-choose')) {
return;
}
var message = form.querySelector('.dz-message');
if (message && !message.hasAttribute('role')) {
message.setAttribute('role', 'button');
message.setAttribute('tabindex', '0');
}
});
// Upload progress bars (Dropzone forms, the file input plugin) have
// no name of their own. Other progress bars are left to their pages,
// which know what they measure.
a11yQueryAll(root, '[role="progressbar"]:not([aria-label]):not([aria-labelledby]):not([title])').forEach(function(bar) {
if (bar.closest('form.dropzone, .dropzone, .file-input')) {
bar.setAttribute('aria-label', "Upload progress");
}
});
// A rich-text editor stands in for its textarea, so it carries the
// textarea's error state.
a11yQueryAll(root, 'textarea').forEach(function(textarea) {
a11ySyncEditor(textarea);
});
// The sidebars mark the current page with .active; say so to screen
// readers as well.
document.querySelectorAll('.sidebar-item').forEach(function(item) {
if (item.classList.contains('active')) {
item.setAttribute('aria-current', 'page');
} else {
item.removeAttribute('aria-current');
}
});
}
// a11yEditorFor returns the TinyMCE editor that replaces a textarea, if
// there is one. TinyMCE hides the textarea, so it takes focus instead.
function a11yEditorFor(el) {
if (el.tagName !== 'TEXTAREA' || !el.id || typeof tinymce === 'undefined') {
return null;
}
return tinymce.get(el.id);
}
// a11ySyncEditor copies a textarea's aria-invalid and aria-describedby
// to the TinyMCE editor that replaces it, once the editor is ready. The
// iframe takes both. The editing body inside it, which is what has focus,
// can't refer to ids in the page, so it takes aria-invalid and the
// described text itself.
function a11ySyncEditor(textarea) {
var editor = a11yEditorFor(textarea);
if (!editor || !editor.initialized) {
return;
}
var frame = document.getElementById(editor.id + '_ifr');
var body = editor.getBody();
var invalid = textarea.getAttribute('aria-invalid') === 'true';
var describedby = textarea.getAttribute('aria-describedby') || '';
var description = describedby.split(/\s+/).map(function(id) {
var el = id ? document.getElementById(id) : null;
return el ? el.textContent.replace(/\s+/g, ' ').trim() : '';
}).filter(Boolean).join(' ');
[frame, body].forEach(function(el) {
if (!el) {
return;
}
if (invalid) {
el.setAttribute('aria-invalid', 'true');
} else {
el.removeAttribute('aria-invalid');
}
});
if (frame) {
if (describedby) {
frame.setAttribute('aria-describedby', describedby);
} else {
frame.removeAttribute('aria-describedby');
}
}
if (body) {
if (description) {
body.setAttribute('aria-description', description);
} else {
body.removeAttribute('aria-description');
}
}
}
// a11yFocusFirstError moves focus to the first invalid control shown in
// root, so after a failed submit the user lands on the problem.
function a11yFocusFirstError(root) {
var invalid = Array.prototype.find.call(root.querySelectorAll('.is-invalid'), function(el) {
return a11yIsShown(el) || a11yEditorFor(el);
});
if (!invalid) {
return false;
}
var control = invalid.matches(a11yControlSelector) ? invalid : invalid.querySelector(a11yControlSelector);
if (!control) {
return false;
}
var editor = a11yEditorFor(control);
if (editor) {
if (editor.initialized) {
editor.focus();
} else {
editor.on('init', function() {
editor.focus();
});
}
return true;
}
control.focus();
return document.activeElement === control;
}
// a11yAnnounce speaks text through the page announcer. Clearing it
// first makes a repeat of the same text announce again.
function a11yAnnounce(text) {
var announcer = document.getElementById('page-announcer');
if (!announcer || !text) {
return;
}
announcer.textContent = '';
setTimeout(function() {
announcer.textContent = text;
}, 100);
}
function a11yOnLoad() {
a11yEnhance(document);
a11yFocusFirstError(document);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', a11yOnLoad, {once: true});
} else {
a11yOnLoad();
}
if (!window.gioA11yListeners) {
window.gioA11yListeners = true;
// Whether focus was on the page body, or inside the content about to
// be replaced, when an HTMX swap into #maincontent-refresh began.
var a11yFocusLost = false;
document.addEventListener('htmx:beforeSwap', function(evt) {
var target = evt.detail && evt.detail.target;
if (!target || target.id !== 'maincontent-refresh') {
return;
}
var active = document.activeElement;
a11yFocusLost = !active || active === document.body || target.contains(active);
});
// htmx fires htmx:load on every element a swap inserts, whatever
// the swap style. htmx:afterSettle only fires on the swap target,
// so rows that infinite scroll adds after it (hx-swap="afterend")
// are only reached here. The page itself was enhanced on load.
document.addEventListener('htmx:load', function(evt) {
var el = evt.target;
if (el === document.body || !el.isConnected || !el.querySelectorAll) {
return;
}
a11yEnhance(el);
});
// After a swap: fix up the markup page scripts added. A form
// submission that came back with errors moves focus to the first of
// them, even though HTMX has put focus back on the submit button. A swap of the main content
// is a page navigation, so announce the new page's title (and the
// result count of a paged list). When the swap took focus away (an
// in-content link), move focus to the new page's heading; when a
// control outside the content (the sidebar) made the request it keeps
// focus. Page scripts run first (their contentLoaded listeners are on
// body), so if one placed focus itself it is left there.
document.addEventListener('htmx:afterSettle', function(evt) {
var target = evt.detail && evt.detail.target;
if (!target) {
return;
}
if (!target.isConnected) {
// An outerHTML swap replaced the target; htmx fires
// afterSettle on each element that took its place.
target = evt.target;
if (!target.isConnected || !target.querySelectorAll) {
return;
}
}
// Enhance again now that page scripts, which run on
// afterSettle at the body, have added their own markup.
if (evt.target.isConnected && evt.target.querySelectorAll) {
a11yEnhance(evt.target);
}
// htmx fires afterSettle on the swap target and then on each
// out-of-band element; those only need their markup fixed up.
if (evt.target !== target) {
return;
}
var requestElt = evt.detail.requestConfig && evt.detail.requestConfig.elt;
var fromForm = !!requestElt && (requestElt.tagName === 'FORM' || !!requestElt.form);
var focusedError = false;
if (fromForm || document.activeElement === document.body) {
focusedError = a11yFocusFirstError(target);
}
if (target.id !== 'maincontent-refresh') {
return;
}
var heading = target.querySelector('h1');
var title = heading ? heading.textContent.trim() : document.title;
// The pager's count, or a page's own result message (marked
// data-a11y-count) when there is nothing to page.
var count = Array.prototype.find.call(target.querySelectorAll('.page-count, [data-a11y-count]'), function(el) {
return a11yIsShown(el);
});
// A swap that stays on the same page and announces its own result
// marks its content with data-a11y-no-announce.
if (!target.querySelector('[data-a11y-no-announce]')) {
a11yAnnounce(count ? title + '. ' + count.textContent.replace(/\s+/g, ' ').trim() : title);
}
// A form that saved without errors re-renders the page, and HTMX
// puts focus back on the field that submitted it (matched by id),
// which then looks like it is still being edited. Move focus to the
// heading instead, as for any other page load.
var active = document.activeElement;
var savedForm = !!requestElt && requestElt.tagName === 'FORM' && evt.detail.requestConfig.verb === 'post';
var focusRestored = savedForm && !!active && target.contains(active);
if (!focusedError && heading && ((a11yFocusLost && (!active || active === document.body)) || focusRestored)) {
heading.focus({preventScroll: true});
}
a11yFocusLost = false;
});
// Going back or forward restores a page from HTMX's history cache
// without a swap, so announce its title as a swap would.
document.addEventListener('htmx:historyRestore', function() {
var content = document.getElementById('maincontent-refresh');
var heading = content ? content.querySelector('h1') : null;
a11yAnnounce(heading ? heading.textContent.trim() : document.title);
});
// An element given role="button" that isn't a <button> (collapse
// toggles kept as anchors, clickable divs) must answer the keys a
// button does: Space, and Enter where the browser doesn't already
// turn it into a click (it does for links). Handlers that act on
// these keys themselves, such as the drawer triggers, prevent the
// default and are left alone.
document.addEventListener('keydown', function(evt) {
if ((evt.key !== ' ' && evt.key !== 'Enter') || evt.defaultPrevented) {
return;
}
var el = evt.target;
if (!el.matches || !el.matches('[role="button"]:not(button):not(input):not(textarea):not(select)')) {
return;
}
if (evt.key === 'Enter' && el.matches('a[href]')) {
return;
}
evt.preventDefault();
el.click();
});
// Return focus when a modal closes. Bootstrap does this only for a
// modal opened by a data-bs-toggle trigger that is still shown when
// it closes; a modal opened from script, or from a menu item that
// was hidden as the modal opened, would leave focus on the page
// body. The element to return to is the trigger, or whatever had
// focus when the modal opened; a hidden menu item gives way to its
// menu's toggle, and a drawer item to the drawer's trigger.
document.addEventListener('show.bs.modal', function(evt) {
var modal = evt.target;
var from = evt.relatedTarget || document.activeElement;
if (!from || from === document.body || modal.contains(from)) {
modal.gioReturnFocus = null;
return;
}
var menu = from.closest('.dropdown-menu');
if (menu && menu.parentElement) {
var toggle = menu.parentElement.querySelector('[data-bs-toggle="dropdown"]');
if (toggle) {
from = toggle;
}
}
var drawer = from.closest('[data-drawer-target]');
if (drawer) {
var drawerTrigger = drawer.drawerTrigger || document.querySelector('[aria-controls="' + drawer.id + '"]');
if (drawerTrigger) {
from = drawerTrigger;
}
}
modal.gioReturnFocus = from;
});
document.addEventListener('hidden.bs.modal', function(evt) {
var modal = evt.target;
var to = modal.gioReturnFocus;
modal.gioReturnFocus = null;
if (!to || !to.isConnected || !a11yIsShown(to)) {
return;
}
// Leave focus alone if something else took it meanwhile, such as
// a second modal or Bootstrap's own return to the trigger.
var active = document.activeElement;
if (active && active !== document.body && !modal.contains(active)) {
return;
}
to.focus();
});
// Tooltips are hoverable and dismissible (WCAG 1.4.13). Bootstrap
// hides a tooltip as soon as the pointer leaves its trigger, so it
// can't be moved onto the tooltip, and it ignores Escape. These
// listeners sit on the document so every tooltip on every page,
// however it was created, gets the same behaviour.
var a11yTooltipForceHide = false;
var a11yTooltipHideDelay = 300;
// a11yHideTooltip hides the tooltip of trigger without the hover
// checks below.
function a11yHideTooltip(trigger) {
var tip = bootstrap.Tooltip.getInstance(trigger);
if (!tip) {
return;
}
a11yTooltipForceHide = true;
try {
tip.hide();
} finally {
a11yTooltipForceHide = false;
}
}
// a11yKeepTooltip keeps a tooltip open while the pointer is over it,
// and hides it once the pointer leaves for anywhere but its trigger.
function a11yKeepTooltip(trigger, tipEl) {
if (tipEl.gioHoverWatched) {
return;
}
tipEl.gioHoverWatched = true;
tipEl.addEventListener('mouseleave', function(evt) {
tipEl.gioHoverWatched = false;
if (evt.relatedTarget && trigger.contains(evt.relatedTarget)) {
return;
}
a11yHideTooltip(trigger);
}, {once: true});
}
document.addEventListener('hide.bs.tooltip', function(evt) {
if (a11yTooltipForceHide || typeof bootstrap === 'undefined') {
return;
}
var trigger = evt.target;
var tip = bootstrap.Tooltip.getInstance(trigger);
var tipEl = tip && tip.tip;
if (!tipEl || !tipEl.isConnected) {
return;
}
if (tipEl.matches(':hover')) {
evt.preventDefault();
a11yKeepTooltip(trigger, tipEl);
return;
}
// A hide while the pointer is still on the trigger was asked
// for by the page or by focus leaving; let it happen.
if (trigger.matches(':hover')) {
return;
}
// The pointer has left the trigger. Give it a moment to cross
// the gap onto the tooltip before hiding.
evt.preventDefault();
if (trigger.gioHidePending) {
return;
}
trigger.gioHidePending = true;
setTimeout(function() {
trigger.gioHidePending = false;
if (!tipEl.isConnected) {
return;
}
if (tipEl.matches(':hover')) {
a11yKeepTooltip(trigger, tipEl);
return;
}
if (trigger.matches(':hover')) {
return;
}
a11yHideTooltip(trigger);
}, a11yTooltipHideDelay);
});
// Escape hides every open tooltip and leaves focus where it is. When
// it hid one, the key goes no further, so the same press doesn't
// also close the modal or menu the tooltip sits in.
window.addEventListener('keydown', function(evt) {
if (evt.key !== 'Escape' || typeof bootstrap === 'undefined') {
return;
}
var hidden = false;
document.querySelectorAll('.tooltip.show[id]').forEach(function(tipEl) {
var trigger = document.querySelector('[aria-describedby~="' + tipEl.id + '"]');
if (trigger && bootstrap.Tooltip.getInstance(trigger)) {
a11yHideTooltip(trigger);
hidden = true;
}
});
if (hidden) {
evt.stopPropagation();
}
}, true);
}
</script>
<div id="scroll-top-outer">
<div id="scroll-top" class="scroll-top-wrapper hidden-xs">
<span class="scroll-top-inner">
<i class="fa fa-arrow-circle-up fa-fw fa-2x"></i>
</span>
</div>
</div>
<script>
$(function() {
$(document).on('scroll', function() {
if ($(window).scrollTop() > 100) {
$('.scroll-top-wrapper').addClass('show');
} else {
$('.scroll-top-wrapper').removeClass('show');
}
});
$('.scroll-top-wrapper').on('click', scrollToTop);
});
function scrollToTop() {
verticalOffset = typeof(verticalOffset) != 'undefined' ? verticalOffset : 0;
element = $('body');
offset = element.offset();
offsetTop = offset.top;
$('html, body').animate({
scrollTop: offsetTop
}, 100, 'linear');
}
if (navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
let elements = document.querySelectorAll('strong.ctrlmeta');
elements.forEach((element) => {
// Check if the innerText is "ctrl"
if (element.innerText === 'ctrl') {
// Create a new "i" element
let newElement = document.createElement('i');
newElement.classList.add('fa-solid', 'fa-command');
newElement.setAttribute('aria-hidden', 'true');
// The icon has no text, so screen readers get the key's name
let keyName = document.createElement('span');
keyName.className = 'visually-hidden';
keyName.textContent = "command";
// Replace the old "strong" element with the new "i" element
element.parentNode.replaceChild(newElement, element);
newElement.after(keyName);
}
});
}
</script>
</body>
</html>