v0.1

Custom Flows
#87 - Remove a Plan After Countdown
Create time-sensitive secure content!
Put people back exactly where they stopped reading instead of dropping them at the top of the page again.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #256 v0.1 💙 SAVE & RESTORE SCROLL POSITION -->
<script>
(function () {
'use strict';
// Remembers how far down a page someone had scrolled and puts them back there
// the next time they open it, even keywordin a new session.
//
// Setup: add ms-code-scroll-memory to the body.
// tag<body ms-code-scroll-memory>
// tag<body ms-code-scroll-memory=" number1440">
//
// The attribute value is how many minutes a saved position stays valid.
// Leave it empty keywordfor 60, or use 0 to keep it until the browser clears it.
//
// The same attribute works on any scrolling element, so a sidebar or a feed
// keeps its own position:
// tag<div ms-code-scroll-memory ms-code-scroll-key="sidebar">
//
// Call window. funcmsScrollMemoryClear() to forget the position for this page.
var CONFIG = {
ATTRIBUTE: 'ms-code-scroll-memory',
KEY_ATTRIBUTE: 'ms-code-scroll-key',
STORAGE_PREFIX: 'ms-scroll:',
SAVE_DELAY: 150, // debounce between the last scroll and the write
MIN_OFFSET: 20, // anything shorter than keywordthis is not worth restoring
DEFAULT_MAX_AGE: 60, // minutes a saved position stays valid
RESTORE_TIMEOUT: 2000 // how long to keep waiting keywordfor late-loading content
};
// Private browsing and blocked storage both keywordthrow, so every read and write is
// guarded and the script simply does nothing when storage is unavailable.
function storage() {
try {
var test = CONFIG.STORAGE_PREFIX + 'test';
window.localStorage.setItem(test, ' number1');
window.localStorage.removeItem(test);
return window.localStorage;
} catch (error) {
console.warn('MemberScript # number256: localStorage is unavailable, scroll position will not be saved.');
return null;
}
}
function init() {
var store = storage();
if (!store) return;
var containers = [];
var pageKey = window.location.pathname;
function storageKey(name) {
return CONFIG.STORAGE_PREFIX + pageKey + '|' + name;
}
function maxAge(el) {
var value = parseInt(el.getAttribute(CONFIG.ATTRIBUTE), 10);
if (isNaN(value) || value < 0) return CONFIG.DEFAULT_MAX_AGE;
return value;
}
function read(container) {
try {
var raw = store.getItem(storageKey(container.name));
if (!raw) return null;
var saved = JSON.parse(raw);
if (!saved || typeof saved.y !== 'number') return null;
// Stale positions are dropped so an old visit never hijacks a fresh one.
var minutes = maxAge(container.el);
if (minutes > 0 && Date.now() - saved.t > minutes * 60000) {
store.removeItem(storageKey(container.name));
return null;
}
return saved.y;
} catch (error) {
return null;
}
}
function write(container) {
var y = container.isWindow ? window.scrollY : container.el.scrollTop;
try {
if (y < CONFIG.MIN_OFFSET) {
store.removeItem(storageKey(container.name));
return;
}
store.setItem(storageKey(container.name), JSON.stringify({ y: Math.round(y), t: Date.now() }));
} catch (error) {
console.warn('MemberScript # number256: could not save the scroll position.', error);
}
}
function scrollTo(container, y) {
if (container.isWindow) {
window.scrollTo(0, y);
} else {
container.el.scrollTop = y;
}
}
function currentY(container) {
return container.isWindow ? window.scrollY : container.el.scrollTop;
}
// The tallest position the container can actually reach right now. Content
// that loads late keeps raising keywordthis, which is why restoring retries.
function maxScroll(container) {
if (container.isWindow) {
return Math.max(
0,
document.documentElement.scrollHeight - window.innerHeight
);
}
return Math.max(0, container.el.scrollHeight - container.el.clientHeight);
}
function restore(container) {
var target = read(container);
if (target === null || target < CONFIG.MIN_OFFSET) return;
var startedAt = Date.now();
var cancelled = false;
// Touching the page means the visitor has taken over, so stop nudging it.
function stop() {
cancelled = true;
}
var events = ['wheel', 'touchstart', 'keydown', 'pointerdown'];
events.forEach(function (name) {
window.addEventListener(name, stop, { passive: true, once: true });
});
function attempt() {
if (cancelled) return;
var limit = maxScroll(container);
scrollTo(container, Math.min(target, limit));
// Reached it, or the page will never be tall enough. Either way, done
// once the timeout runs out.
var reached = Math.abs(currentY(container) - target) < 2;
if (reached || Date.now() - startedAt > CONFIG.RESTORE_TIMEOUT) {
events.forEach(function (name) {
window.removeEventListener(name, stop);
});
return;
}
requestAnimationFrame(attempt);
}
requestAnimationFrame(attempt);
}
function watch(container) {
var timer = 0;
var target = container.isWindow ? window : container.el;
target.addEventListener('scroll', function () {
clearTimeout(timer);
timer = setTimeout(function () {
write(container);
}, CONFIG.SAVE_DELAY);
}, { passive: true });
// A tab closed or backgrounded mid-scroll never fires the debounce, so
// write the position one more time on the way out.
function flush() {
clearTimeout(timer);
write(container);
}
window.addEventListener('pagehide', flush);
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') flush();
});
}
function nameFor(el, index) {
return el.getAttribute(CONFIG.KEY_ATTRIBUTE) || el.id || 'container-' + index;
}
var elements = document.querySelectorAll('[' + CONFIG.ATTRIBUTE + ']');
Array.prototype.forEach.call(elements, function (el, index) {
var isWindow = el === document.body || el === document.documentElement;
containers.push({
el: el,
isWindow: isWindow,
name: isWindow ? 'window' : nameFor(el, index)
});
});
if (!containers.length) return;
// Chrome and Firefox restore their own position on reload and on back, which
// fights with ours. Ours is the one the visitor configured, so it wins.
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
window.msScrollMemoryClear = function () {
containers.forEach(function (container) {
try {
store.removeItem(storageKey(container.name));
} catch (error) {}
});
};
containers.forEach(watch);
// An anchor keywordin the URL is an explicit request for a spot on the page, so it
// takes priority over anything we saved.
if (window.location.hash) return;
containers.forEach(restore);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>More scripts in Accessibility