Widget:ScrollToTopSidebar: Difference between revisions

From Bodhicitta
((by SublimeText.Mediawiker))
(Scroll-to-top: 2px progress ring drawn clockwise from the top centre, alongside the fill)
 
(11 intermediate revisions by the same user not shown)
Line 1: Line 1:
<style>
/* Floating pill at the bottom centre of the viewport. A darker band fills it from the
  left as you read down the content column (--stt-p, set below); the off-white base
  keeps it legible over the text it floats above. */
#scrollToTop {
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translateX(-50%);
width: 11rem;
height: 44px;
border-radius: 22px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, .18);
background:
linear-gradient(to right, rgba(186, 27, 29, .3) var(--stt-p, 0%), rgba(186, 27, 29, .1) var(--stt-p, 0%)),
#fffcf5;
}
#scrollToTop:hover {
background:
linear-gradient(to right, rgba(186, 27, 29, .5) var(--stt-p, 0%), rgba(186, 27, 29, .3) var(--stt-p, 0%)),
#fffcf5;
}
/* A 2px ring drawn clockwise from the top centre with the same progress. */
#scrollToTop .stt-ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: visible;
}
#scrollToTop .stt-ring path {
fill: none;
stroke: rgba(186, 27, 29, .65);
stroke-width: 2;
stroke-dasharray: 100;
stroke-dashoffset: 100;
}
#scrollToTop:hover .stt-ring path {
stroke: rgba(186, 27, 29, .85);
}
@media (max-width: 767.98px) {
#scrollToTop { display: none !important; }
}
</style>
<script>
<script>
// Create the scroll to top button
// Create the scroll to top button
const scrollToTopButton = document.createElement('button');
const scrollToTopButtonSb = document.createElement('button');
scrollToTopButton.innerHTML = 'Top <i class="fa-sharp-duotone fa-solid fa-up-to-bracket"></i>'; // Text and FA to top icon
scrollToTopButtonSb.innerHTML = 'to top <i class="fa-sharp-duotone fa-solid fa-arrow-up-to-line ml-2"></i>';
scrollToTopButton.setAttribute('id', 'scrollToTop');
scrollToTopButtonSb.setAttribute('id', 'scrollToTop');
scrollToTopButton.style.cssText = `
 
width: 100%;
// The progress ring: one path around the pill, starting at the top centre and
text-align: center;
// running clockwise, measured in pathLength units of 100 so the dash offset is
height: 50px;
// simply 100 minus the progress percentage.
font-size: 1em;
const SVG_NS = 'http://www.w3.org/2000/svg';
border: none;
const sttRing = document.createElementNS(SVG_NS, 'svg');
cursor: pointer;
const sttRingPath = document.createElementNS(SVG_NS, 'path');
display: none;
sttRing.setAttribute('class', 'stt-ring');
z-index: 1000;
sttRing.setAttribute('aria-hidden', 'true');
background: transparent;
sttRingPath.setAttribute('pathLength', '100');
`;
sttRing.appendChild(sttRingPath);
scrollToTopButton.addEventListener('mouseover', () => {
scrollToTopButtonSb.appendChild(sttRing);
scrollToTopButton.style.cssText = `
 
background: rgba(186,27,29,0.4);
function sttDrawRing() {
text-decoration: none;
const w = scrollToTopButtonSb.offsetWidth, h = scrollToTopButtonSb.offsetHeight;
`;
if (!w || !h) return;
// Inset by half the stroke so the whole 2px line sits inside the button.
const x0 = 1, y0 = 1, x1 = w - 1, y1 = h - 1, r = (y1 - y0) / 2, cx = w / 2;
sttRing.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
sttRingPath.setAttribute('d', [
'M', cx, y0,
'H', x1 - r,
'A', r, r, 0, 0, 1, x1 - r, y1,
'H', x0 + r,
'A', r, r, 0, 0, 1, x0 + r, y0,
'Z'
].join(' '));
}
window.addEventListener('resize', sttDrawRing);
 
// The button floats over the page, so it lives on the body; the sidebar slot that
// invokes this widget is left empty and hidden.
const container = document.getElementById('scrollToTopContainer');
document.body.appendChild(scrollToTopButtonSb);
if (container) container.style.display = 'none';
 
// Handle initial hash navigation
window.addEventListener('load', () => {
if (window.location.hash) {
setTimeout(() => {
const element = document.querySelector(window.location.hash);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
}
});
});


// Add the button to the document body
// Progress runs from the moment the button appears (STT_SHOW) to the end of the main
const container = document.getElementById('scrollToTopContainer');
// content column (the .col-md-9 beside the sidebar, or the whole page).
container.appendChild(scrollToTopButton);
const STT_SHOW = 300;
const sttRow = container && container.closest('.row');
const sttContent = (sttRow && sttRow.querySelector(':scope > .col-md-9')) || document.documentElement;
 
function sttProgress() {
const y = window.pageYOffset;
const end = y + sttContent.getBoundingClientRect().bottom - window.innerHeight;
if (end <= STT_SHOW) return 1;
return Math.min(1, Math.max(0, (y - STT_SHOW) / (end - STT_SHOW)));
}


// Show/hide the button based on scroll position
// Show/hide the button and advance its fill with scroll position
let sttTicking = false;
function sttUpdate() {
sttTicking = false;
if (window.pageYOffset > STT_SHOW) {
const wasHidden = scrollToTopButtonSb.style.display !== 'block';
const pct = (sttProgress() * 100).toFixed(2);
scrollToTopButtonSb.style.display = 'block';
if (wasHidden) sttDrawRing();
scrollToTopButtonSb.style.setProperty('--stt-p', pct + '%');
sttRingPath.style.strokeDashoffset = 100 - pct;
} else {
scrollToTopButtonSb.style.display = 'none';
}
}
window.addEventListener('scroll', () => {
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
if (!sttTicking) {
scrollToTopButton.style.display = 'block';
sttTicking = true;
} else {
window.requestAnimationFrame(sttUpdate);
scrollToTopButton.style.display = 'none';
}
}
});
}, { passive: true });


// Scroll to top when the button is clicked
// Scroll to top when the button is clicked
scrollToTopButton.addEventListener('click', () => {
scrollToTopButtonSb.addEventListener('click', () => {
window.scrollTo({
window.scrollTo({
top: 0,
top: 0,

Latest revision as of 13:11, 1 October 2026

<style> /* Floating pill at the bottom centre of the viewport. A darker band fills it from the

  left as you read down the content column (--stt-p, set below); the off-white base
  keeps it legible over the text it floats above. */
  1. scrollToTop {

position: fixed; left: 50%; bottom: 1.5rem; transform: translateX(-50%); width: 11rem; height: 44px; border-radius: 22px; overflow: hidden; box-shadow: 0 2px 10px rgba(0, 0, 0, .18); background: linear-gradient(to right, rgba(186, 27, 29, .3) var(--stt-p, 0%), rgba(186, 27, 29, .1) var(--stt-p, 0%)), #fffcf5; }

  1. scrollToTop:hover {

background: linear-gradient(to right, rgba(186, 27, 29, .5) var(--stt-p, 0%), rgba(186, 27, 29, .3) var(--stt-p, 0%)), #fffcf5; } /* A 2px ring drawn clockwise from the top centre with the same progress. */

  1. scrollToTop .stt-ring {

position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }

  1. scrollToTop .stt-ring path {

fill: none; stroke: rgba(186, 27, 29, .65); stroke-width: 2; stroke-dasharray: 100; stroke-dashoffset: 100; }

  1. scrollToTop:hover .stt-ring path {

stroke: rgba(186, 27, 29, .85); } @media (max-width: 767.98px) { #scrollToTop { display: none !important; } } </style> <script> // Create the scroll to top button const scrollToTopButtonSb = document.createElement('button'); scrollToTopButtonSb.innerHTML = 'to top '; scrollToTopButtonSb.setAttribute('id', 'scrollToTop');

// The progress ring: one path around the pill, starting at the top centre and // running clockwise, measured in pathLength units of 100 so the dash offset is // simply 100 minus the progress percentage. const SVG_NS = 'http://www.w3.org/2000/svg'; const sttRing = document.createElementNS(SVG_NS, 'svg'); const sttRingPath = document.createElementNS(SVG_NS, 'path'); sttRing.setAttribute('class', 'stt-ring'); sttRing.setAttribute('aria-hidden', 'true'); sttRingPath.setAttribute('pathLength', '100'); sttRing.appendChild(sttRingPath); scrollToTopButtonSb.appendChild(sttRing);

function sttDrawRing() { const w = scrollToTopButtonSb.offsetWidth, h = scrollToTopButtonSb.offsetHeight; if (!w || !h) return; // Inset by half the stroke so the whole 2px line sits inside the button. const x0 = 1, y0 = 1, x1 = w - 1, y1 = h - 1, r = (y1 - y0) / 2, cx = w / 2; sttRing.setAttribute('viewBox', '0 0 ' + w + ' ' + h); sttRingPath.setAttribute('d', [ 'M', cx, y0, 'H', x1 - r, 'A', r, r, 0, 0, 1, x1 - r, y1, 'H', x0 + r, 'A', r, r, 0, 0, 1, x0 + r, y0, 'Z' ].join(' ')); } window.addEventListener('resize', sttDrawRing);

// The button floats over the page, so it lives on the body; the sidebar slot that // invokes this widget is left empty and hidden. const container = document.getElementById('scrollToTopContainer'); document.body.appendChild(scrollToTopButtonSb); if (container) container.style.display = 'none';

// Handle initial hash navigation window.addEventListener('load', () => { if (window.location.hash) { setTimeout(() => { const element = document.querySelector(window.location.hash); if (element) { element.scrollIntoView({ behavior: 'smooth' }); } }, 100); } });

// Progress runs from the moment the button appears (STT_SHOW) to the end of the main // content column (the .col-md-9 beside the sidebar, or the whole page). const STT_SHOW = 300; const sttRow = container && container.closest('.row'); const sttContent = (sttRow && sttRow.querySelector(':scope > .col-md-9')) || document.documentElement;

function sttProgress() { const y = window.pageYOffset; const end = y + sttContent.getBoundingClientRect().bottom - window.innerHeight; if (end <= STT_SHOW) return 1; return Math.min(1, Math.max(0, (y - STT_SHOW) / (end - STT_SHOW))); }

// Show/hide the button and advance its fill with scroll position let sttTicking = false; function sttUpdate() { sttTicking = false; if (window.pageYOffset > STT_SHOW) { const wasHidden = scrollToTopButtonSb.style.display !== 'block'; const pct = (sttProgress() * 100).toFixed(2); scrollToTopButtonSb.style.display = 'block'; if (wasHidden) sttDrawRing(); scrollToTopButtonSb.style.setProperty('--stt-p', pct + '%'); sttRingPath.style.strokeDashoffset = 100 - pct; } else { scrollToTopButtonSb.style.display = 'none'; } } window.addEventListener('scroll', () => { if (!sttTicking) { sttTicking = true; window.requestAnimationFrame(sttUpdate); } }, { passive: true });

// Scroll to top when the button is clicked scrollToTopButtonSb.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); </script>