MemberScripts
An attribute-based solution to add features to your Webflow site.
Simply copy some code, add some attributes, and you're done.
Need help with MemberScripts?
All Memberstack customers can ask for assistance in the 2.0 Slack. Please note that these are not official features and support cannot be guaranteed.
#114 - Scroll To Top Button
Add a button which will scroll to the top of the page when clicked,
<!-- 💙 MEMBERSCRIPT #114 v0.1 💙 - SCROLL TO TOP BUTTON -->
<script>
document.addEventListener('DOMContentLoaded', function() {
var scrollTopButton = document.querySelector('[ms-code-scroll-top="button"]');
if (scrollTopButton) {
// Set initial styles
scrollTopButton.style.opacity = '0';
scrollTopButton.style.visibility = 'hidden';
scrollTopButton.style.transition = 'opacity 0.3s, visibility 0.3s';
// Function to check scroll position and toggle button visibility
function toggleButtonVisibility() {
if (window.pageYOffset > 300) {
scrollTopButton.style.opacity = '1';
scrollTopButton.style.visibility = 'visible';
} else {
scrollTopButton.style.opacity = '0';
scrollTopButton.style.visibility = 'hidden';
}
}
// Initial check on page load
toggleButtonVisibility();
// Check on scroll
window.addEventListener('scroll', toggleButtonVisibility);
// Scroll to top when button is clicked
scrollTopButton.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
});
</script>
#113 - RSS Feeds
Use a Webflow UI to display an RSS feed directly on your Website.
<!-- 💙 MEMBERSCRIPT #113 v0.1 💙 - RSS FEEDS IN WEBFLOW -->
<script>
(function() {
console.log('RSS Feed Script starting...');
const CORS_PROXY = 'https://cors-anywhere.herokuapp.com/';
function loadScript(src, onLoad, onError) {
const script = document.createElement('script');
script.src = src;
script.onload = onLoad;
script.onerror = onError;
document.head.appendChild(script);
}
function initRSSFeed() {
if (typeof RSSParser === 'undefined') {
console.error('RSSParser is not defined.');
return;
}
const parser = new RSSParser({
customFields: {
item: [
['media:content', 'mediaContent', {keepArray: true}],
['media:thumbnail', 'mediaThumbnail', {keepArray: true}],
['enclosure', 'enclosure', {keepArray: true}],
]
}
});
document.querySelectorAll('[ms-code-rss-feed]').forEach(element => {
const url = element.getAttribute('ms-code-rss-url');
const limit = parseInt(element.getAttribute('ms-code-rss-limit')) || 5;
parser.parseURL(CORS_PROXY + url, (err, feed) => {
if (err) {
console.error('Error parsing RSS feed:', err);
element.textContent = `Failed to load RSS feed from ${url}. Error: ${err.message}`;
} else {
renderRSSItems(element, feed.items.slice(0, limit), {
showImage: element.getAttribute('ms-code-rss-show-image') !== 'false',
showDate: element.getAttribute('ms-code-rss-show-date') !== 'false',
dateFormat: element.getAttribute('ms-code-rss-date-format') || 'short',
target: element.getAttribute('ms-code-rss-target') || '_self'
});
}
});
});
}
function renderRSSItems(element, items, options) {
const templateItem = element.querySelector('[ms-code-rss-item]');
if (!templateItem) return;
element.innerHTML = ''; // Clear existing items
items.forEach(item => {
const itemElement = templateItem.cloneNode(true);
const title = itemElement.querySelector('[ms-code-rss-title]');
if (title) {
const titleLength = parseInt(title.getAttribute('ms-code-rss-title-length')) || Infinity;
title.textContent = truncate(item.title, titleLength);
}
const description = itemElement.querySelector('[ms-code-rss-description]');
if (description) {
const descriptionLength = parseInt(description.getAttribute('ms-code-rss-description-length')) || Infinity;
description.textContent = truncate(stripHtml(item.content || item.description), descriptionLength);
}
const date = itemElement.querySelector('[ms-code-rss-date]');
if (date && options.showDate && item.pubDate) {
date.textContent = formatDate(new Date(item.pubDate), options.dateFormat);
}
const img = itemElement.querySelector('[ms-code-rss-image]');
if (img && options.showImage) {
const imgUrl = getImageUrl(item);
if (imgUrl) {
img.src = imgUrl;
img.alt = item.title;
img.removeAttribute('srcset');
}
}
const linkElement = itemElement.querySelector('[ms-code-rss-link]');
if (linkElement) {
linkElement.setAttribute('href', item.link);
}
element.appendChild(itemElement);
});
}
function getImageUrl(item) {
const sources = ['mediaContent', 'mediaThumbnail', 'enclosure'];
for (let source of sources) {
if (item[source] && item[source][0]) {
return item[source][0].$ ? item[source][0].$.url : item[source][0].url;
}
}
return null;
}
function truncate(str, length) {
if (!str) return '';
if (length === Infinity) return str;
return str.length > length ? str.slice(0, length) + '...' : str;
}
function stripHtml(html) {
const tmp = document.createElement('DIV');
tmp.innerHTML = html || '';
return tmp.textContent || tmp.innerText || '';
}
function formatDate(date, format) {
if (!(date instanceof Date) || isNaN(date)) return '';
const options = format === 'long' ?
{ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' } :
undefined;
return format === 'relative' ? getRelativeTimeString(date) : date.toLocaleDateString(undefined, options);
}
function getRelativeTimeString(date, lang = navigator.language) {
const timeMs = date.getTime();
const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);
const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];
const units = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'];
const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));
const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });
return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
}
loadScript('https://cdn.jsdelivr.net/npm/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
console.error('Error loading RSS Parser script');
loadScript('https://unpkg.com/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
console.error('Error loading RSS Parser script from backup CDN');
});
});
})();
</script>
#112 - Before & After Sliders
Easily add a before/after photo slider to your Webflow site.
<!-- 💙 MEMBERSCRIPT #112 v0.1 💙 - BEFORE & AFTER SLIDERS -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const wraps = document.querySelectorAll('[ms-code-ba-wrap]');
wraps.forEach(wrap => {
const before = wrap.querySelector('[ms-code-ba-before]');
const after = wrap.querySelector('[ms-code-ba-after]');
// Create slider element
const slider = document.createElement('div');
slider.setAttribute('ms-code-ba-slider', wrap.getAttribute('ms-code-ba-wrap'));
wrap.appendChild(slider);
let isDown = false;
// Ensure proper positioning
wrap.style.position = 'relative';
wrap.style.overflow = 'hidden';
before.style.width = '100%';
before.style.display = 'block';
after.style.position = 'absolute';
after.style.top = '0';
after.style.left = '0';
after.style.width = '100%';
after.style.height = '100%';
slider.style.position = 'absolute';
slider.style.top = '0';
slider.style.bottom = '0';
slider.style.width = '4px';
slider.style.background = 'white';
slider.style.cursor = 'ew-resize';
slider.style.zIndex = '3';
const setPosition = (position) => {
const clampedPosition = Math.max(0, Math.min(1, position));
slider.style.left = `${clampedPosition * 100}%`;
after.style.clipPath = `inset(0 0 0 ${clampedPosition * 100}%)`;
};
const move = (e) => {
if (!isDown && e.type !== 'mousemove') return;
e.preventDefault();
const x = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
const rect = wrap.getBoundingClientRect();
const position = (x - rect.left) / rect.width;
setPosition(position);
};
const easeBack = () => {
setPosition(0.5); // Move back to center
};
wrap.addEventListener('mousedown', () => isDown = true);
wrap.addEventListener('mouseup', () => isDown = false);
wrap.addEventListener('mouseleave', () => {
isDown = false;
easeBack();
});
wrap.addEventListener('mousemove', move);
wrap.addEventListener('touchstart', (e) => {
isDown = true;
move(e);
});
wrap.addEventListener('touchmove', move);
wrap.addEventListener('touchend', () => {
isDown = false;
easeBack();
});
// Initialize position
setPosition(0.5);
});
});
</script>
#111 - Auto Rotating Tabs
The easiest way to make your tabs auto-rotate on a timer.
<!-- 💙 MEMBERSCRIPT #111 v0.1 💙 - AUTO-ROTATING TABS -->
<script>
// Function to rotate tabs
function initializeTabRotator() {
// Find all tab containers with the ms-code-rotate-tabs attribute
const tabContainers = document.querySelectorAll('[ms-code-rotate-tabs]');
tabContainers.forEach(container => {
const interval = parseInt(container.getAttribute('ms-code-rotate-tabs'), 10);
const tabLinks = container.querySelectorAll('.w-tab-link');
const tabContent = container.closest('.w-tabs').querySelector('.w-tab-content');
const tabPanes = tabContent.querySelectorAll('.w-tab-pane');
let currentIndex = Array.from(tabLinks).findIndex(link => link.classList.contains('w--current'));
let rotationTimer;
// ANIMATION CONFIGURATION
// Modify these values to adjust the animation behavior
const FADE_OUT_DURATION = 300; // Duration for fading out the current tab (in milliseconds)
const FADE_IN_DURATION = 100; // Duration for fading in the new tab (in milliseconds)
const EASING_FUNCTION = 'ease'; // Choose from: 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'
// or use a cubic-bezier function like 'cubic-bezier(0.1, 0.7, 1.0, 0.1)'
// Additional easing options (uncomment to use):
// const EASING_FUNCTION = 'ease-in-quad';
// const EASING_FUNCTION = 'ease-out-quad';
// const EASING_FUNCTION = 'ease-in-out-quad';
// const EASING_FUNCTION = 'ease-in-cubic';
// const EASING_FUNCTION = 'ease-out-cubic';
// const EASING_FUNCTION = 'ease-in-out-cubic';
// const EASING_FUNCTION = 'ease-in-quart';
// const EASING_FUNCTION = 'ease-out-quart';
// const EASING_FUNCTION = 'ease-in-out-quart';
// const EASING_FUNCTION = 'ease-in-quint';
// const EASING_FUNCTION = 'ease-out-quint';
// const EASING_FUNCTION = 'ease-in-out-quint';
// const EASING_FUNCTION = 'ease-in-sine';
// const EASING_FUNCTION = 'ease-out-sine';
// const EASING_FUNCTION = 'ease-in-out-sine';
// const EASING_FUNCTION = 'ease-in-expo';
// const EASING_FUNCTION = 'ease-out-expo';
// const EASING_FUNCTION = 'ease-in-out-expo';
// const EASING_FUNCTION = 'ease-in-circ';
// const EASING_FUNCTION = 'ease-out-circ';
// const EASING_FUNCTION = 'ease-in-out-circ';
// const EASING_FUNCTION = 'ease-in-back';
// const EASING_FUNCTION = 'ease-out-back';
// const EASING_FUNCTION = 'ease-in-out-back';
// END OF ANIMATION CONFIGURATION
function switchToTab(index) {
// Fade out current tab
tabPanes[currentIndex].style.transition = `opacity ${FADE_OUT_DURATION}ms ${EASING_FUNCTION}`;
tabPanes[currentIndex].style.opacity = '0';
setTimeout(() => {
// Remove active classes and update ARIA attributes for current tab and pane
tabLinks[currentIndex].classList.remove('w--current');
tabLinks[currentIndex].setAttribute('aria-selected', 'false');
tabLinks[currentIndex].setAttribute('tabindex', '-1');
tabPanes[currentIndex].classList.remove('w--tab-active');
// Update current index
currentIndex = index;
// Add active classes and update ARIA attributes for new current tab and pane
tabLinks[currentIndex].classList.add('w--current');
tabLinks[currentIndex].setAttribute('aria-selected', 'true');
tabLinks[currentIndex].setAttribute('tabindex', '0');
tabPanes[currentIndex].classList.add('w--tab-active');
// Fade in new tab
tabPanes[currentIndex].style.transition = `opacity ${FADE_IN_DURATION}ms ${EASING_FUNCTION}`;
tabPanes[currentIndex].style.opacity = '1';
// Update the data-current attribute on the parent w-tabs element
const wTabsElement = container.closest('.w-tabs');
if (wTabsElement) {
wTabsElement.setAttribute('data-current', tabLinks[currentIndex].getAttribute('data-w-tab'));
}
}, FADE_OUT_DURATION);
}
function rotateToNextTab() {
const nextIndex = (currentIndex + 1) % tabLinks.length;
switchToTab(nextIndex);
}
function startRotation() {
clearInterval(rotationTimer);
rotationTimer = setInterval(rotateToNextTab, interval);
}
// Add click event listeners to tab links
tabLinks.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
switchToTab(index);
startRotation(); // Restart rotation from this tab
});
});
// Start the initial rotation
startRotation();
});
}
// Run the function when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initializeTabRotator);
</script>
#110 - Tooltips for Webflow
Easily add Tippy.js tooltips to your Webflow site with attributes.
<!-- 💙 MEMBERSCRIPT #110 v0.1 💙 - TOOLTIPS FOR WEBFLOW -->
<script>
// Function to load Tippy.js, its CSS, and additional theme/animation CSS
function loadTippy(callback) {
// Load Tippy.js script
const script = document.createElement('script');
script.src = 'https://unpkg.com/@popperjs/core@2';
script.onload = function() {
const tippyScript = document.createElement('script');
tippyScript.src = 'https://unpkg.com/tippy.js@6';
tippyScript.onload = function() {
// Load Tippy.js CSS
const cssFiles = [
'https://unpkg.com/tippy.js@6/dist/tippy.css',
'https://unpkg.com/tippy.js@6/themes/light.css',
'https://unpkg.com/tippy.js@6/themes/light-border.css',
'https://unpkg.com/tippy.js@6/animations/shift-away.css',
'https://unpkg.com/tippy.js@6/animations/shift-toward.css',
'https://unpkg.com/tippy.js@6/animations/scale.css',
'https://unpkg.com/tippy.js@6/animations/perspective.css'
];
let loadedCount = 0;
cssFiles.forEach(file => {
const link = document.createElement('link');
link.href = file;
link.rel = 'stylesheet';
link.onload = function() {
loadedCount++;
if (loadedCount === cssFiles.length) {
// Call the callback function when everything is loaded
callback();
}
};
document.head.appendChild(link);
});
};
document.head.appendChild(tippyScript);
};
document.head.appendChild(script);
}
// Function to initialize Tippy tooltips
function initializeTippyTooltips() {
// Select all elements with any ms-code-tooltip-* attribute
const elements = document.querySelectorAll('[ms-code-tooltip-top], [ms-code-tooltip-bottom], [ms-code-tooltip-left], [ms-code-tooltip-right], [ms-code-tooltip-content]');
elements.forEach(element => {
const tippyOptions = {};
// Content and Placement
if (element.hasAttribute('ms-code-tooltip-top')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-top');
tippyOptions.placement = 'top';
} else if (element.hasAttribute('ms-code-tooltip-bottom')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-bottom');
tippyOptions.placement = 'bottom';
} else if (element.hasAttribute('ms-code-tooltip-left')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-left');
tippyOptions.placement = 'left';
} else if (element.hasAttribute('ms-code-tooltip-right')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-right');
tippyOptions.placement = 'right';
} else if (element.hasAttribute('ms-code-tooltip-content')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-content');
}
if (element.hasAttribute('ms-code-tooltip-placement')) {
tippyOptions.placement = element.getAttribute('ms-code-tooltip-placement');
}
// Theme
if (element.hasAttribute('ms-code-tooltip-theme')) {
tippyOptions.theme = element.getAttribute('ms-code-tooltip-theme');
}
// Animation
if (element.hasAttribute('ms-code-tooltip-animation')) {
tippyOptions.animation = element.getAttribute('ms-code-tooltip-animation');
}
// Max Width
if (element.hasAttribute('ms-code-tooltip-maxwidth')) {
tippyOptions.maxWidth = parseInt(element.getAttribute('ms-code-tooltip-maxwidth'));
}
// Delay
if (element.hasAttribute('ms-code-tooltip-delay')) {
tippyOptions.delay = JSON.parse(element.getAttribute('ms-code-tooltip-delay'));
}
// Duration
if (element.hasAttribute('ms-code-tooltip-duration')) {
tippyOptions.duration = JSON.parse(element.getAttribute('ms-code-tooltip-duration'));
}
// Interactive
if (element.hasAttribute('ms-code-tooltip-interactive')) {
tippyOptions.interactive = element.getAttribute('ms-code-tooltip-interactive') === 'true';
}
// Arrow
if (element.hasAttribute('ms-code-tooltip-arrow')) {
tippyOptions.arrow = element.getAttribute('ms-code-tooltip-arrow') === 'true';
}
// Trigger
if (element.hasAttribute('ms-code-tooltip-trigger')) {
tippyOptions.trigger = element.getAttribute('ms-code-tooltip-trigger');
}
// Hide On Click
if (element.hasAttribute('ms-code-tooltip-hideOnClick')) {
tippyOptions.hideOnClick = element.getAttribute('ms-code-tooltip-hideOnClick') === 'true';
}
// Follow Cursor
if (element.hasAttribute('ms-code-tooltip-followCursor')) {
tippyOptions.followCursor = element.getAttribute('ms-code-tooltip-followCursor');
}
// Offset
if (element.hasAttribute('ms-code-tooltip-offset')) {
tippyOptions.offset = JSON.parse(element.getAttribute('ms-code-tooltip-offset'));
}
// Z-Index
if (element.hasAttribute('ms-code-tooltip-zIndex')) {
tippyOptions.zIndex = parseInt(element.getAttribute('ms-code-tooltip-zIndex'));
}
// Allow HTML
if (element.hasAttribute('ms-code-tooltip-allowHTML')) {
tippyOptions.allowHTML = element.getAttribute('ms-code-tooltip-allowHTML') === 'true';
}
// Touch
if (element.hasAttribute('ms-code-tooltip-touch')) {
const touchValue = element.getAttribute('ms-code-tooltip-touch');
tippyOptions.touch = touchValue === 'true' || touchValue === 'false' ? (touchValue === 'true') : JSON.parse(touchValue);
}
// Initialize Tippy instance
tippy(element, tippyOptions);
});
}
// Wait for the DOM to be fully loaded, then load Tippy and initialize tooltips
document.addEventListener('DOMContentLoaded', function() {
loadTippy(initializeTippyTooltips);
});
</script>
#109 - Custom Multi Selects
Custom-styled multi selects with search, keyboard selection, and more.
<!-- 💙 MEMBERSCRIPT #109 v0.1 💙 - CUSTOM MULTI SELECT -->
<script>
$(document).ready(function() {
$('[ms-code-select-wrapper]').each(function() {
const $wrapper = $(this);
const isMulti = $wrapper.attr('ms-code-select-wrapper') === 'multi';
const $input = $wrapper.find('[ms-code-select="input"]');
const $list = $wrapper.find('[ms-code-select="list"]');
const $selectedWrapper = $wrapper.find('[ms-code-select="selected-wrapper"]');
const $emptyState = $wrapper.find('[ms-code-select="empty-state"]');
const options = $input.attr('ms-code-select-options').split(',').map(opt => opt.trim());
let selectedOptions = [];
let highlightedIndex = -1;
const $templateSelectedTag = $selectedWrapper.find('[ms-code-select="tag"]');
const templateSelectedTagHTML = $templateSelectedTag.prop('outerHTML');
$templateSelectedTag.remove();
const $templateNewTag = $list.find('[ms-code-select="tag-name-new"]');
const templateNewTagHTML = $templateNewTag.prop('outerHTML');
$templateNewTag.remove();
function createSelectedTag(value) {
const $newTag = $(templateSelectedTagHTML);
$newTag.find('[ms-code-select="tag-name-selected"]').text(value);
$newTag.find('[ms-code-select="tag-close"]').on('click', function(e) {
e.stopPropagation();
removeTag(value);
});
return $newTag;
}
function addTag(value) {
if (!selectedOptions.includes(value) && options.includes(value)) {
selectedOptions.push(value);
$selectedWrapper.append(createSelectedTag(value));
updateInput();
filterOptions();
}
}
function removeTag(value) {
selectedOptions = selectedOptions.filter(option => option !== value);
$selectedWrapper.find(`[ms-code-select="tag-name-selected"]:contains("${value}")`).closest('[ms-code-select="tag"]').remove();
updateInput();
if (isMulti && selectedOptions.length > 0) {
$input.val($input.val() + ', ');
}
filterOptions();
}
function updateInput() {
$input.val(selectedOptions.join(', '));
}
function toggleList(show) {
$list.toggle(show);
}
function createOptionElement(value) {
const $option = $(templateNewTagHTML);
$option.text(value);
$option.on('click', function() {
selectOption(value);
});
return $option;
}
function selectOption(value) {
if (isMulti) {
addTag(value);
$input.val(selectedOptions.join(', ') + (selectedOptions.length > 0 ? ', ' : ''));
$input.focus();
} else {
selectedOptions = [value];
$selectedWrapper.empty().append(createSelectedTag(value));
updateInput();
toggleList(false);
}
filterOptions();
}
function filterOptions() {
const inputValue = $input.val();
const searchTerm = isMulti ? inputValue.split(',').pop().trim() : inputValue.trim();
let visibleOptionsCount = 0;
$list.find('[ms-code-select="tag-name-new"]').each(function() {
const $option = $(this);
const optionText = $option.text().toLowerCase();
const matches = optionText.includes(searchTerm.toLowerCase());
const isSelected = selectedOptions.includes($option.text());
$option.toggle(matches && !isSelected);
if (matches && !isSelected) visibleOptionsCount++;
});
$emptyState.toggle(visibleOptionsCount === 0 && searchTerm !== '');
highlightedIndex = -1;
updateHighlight();
}
function cleanInput() {
const inputValues = $input.val().split(',').map(v => v.trim()).filter(v => v);
const validValues = inputValues.filter(v => options.includes(v));
selectedOptions = validValues;
$selectedWrapper.empty();
selectedOptions.forEach(value => $selectedWrapper.append(createSelectedTag(value)));
updateInput();
filterOptions();
}
function handleInputChange() {
const inputValue = $input.val();
const inputValues = inputValue.split(',').map(v => v.trim());
const lastValue = inputValues[inputValues.length - 1];
if (inputValue.endsWith(',') || inputValue.endsWith(', ')) {
inputValues.pop();
const newValidValues = inputValues.filter(v => options.includes(v) && !selectedOptions.includes(v));
newValidValues.forEach(addTag);
$input.val(selectedOptions.join(', ') + (selectedOptions.length > 0 ? ', ' : ''));
} else if (options.includes(lastValue) && !selectedOptions.includes(lastValue)) {
addTag(lastValue);
$input.val(selectedOptions.join(', ') + ', ');
}
filterOptions();
}
function initializeWithValue() {
const initialValue = $input.val();
if (initialValue) {
const initialValues = initialValue.split(',').map(v => v.trim());
initialValues.forEach(value => {
if (options.includes(value)) {
addTag(value);
}
});
updateInput();
filterOptions();
}
}
function updateHighlight() {
$list.find('[ms-code-select="tag-name-new"]').removeClass('highlighted').css('background-color', '');
if (highlightedIndex >= 0) {
$list.find('[ms-code-select="tag-name-new"]:visible').eq(highlightedIndex)
.addClass('highlighted')
.css('background-color', '#e0e0e0');
}
}
function handleKeyDown(e) {
const visibleOptions = $list.find('[ms-code-select="tag-name-new"]:visible');
const optionCount = visibleOptions.length;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
highlightedIndex = (highlightedIndex + 1) % optionCount;
updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
highlightedIndex = (highlightedIndex - 1 + optionCount) % optionCount;
updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (highlightedIndex >= 0) {
const selectedValue = visibleOptions.eq(highlightedIndex).text();
selectOption(selectedValue);
}
break;
}
}
$.each(options, function(i, option) {
$list.append(createOptionElement(option));
});
$input.on('focus', function() {
toggleList(true);
if (isMulti) {
const currentVal = $input.val().trim();
if (currentVal !== '' && !currentVal.endsWith(',')) {
$input.val(currentVal + ', ');
}
this.selectionStart = this.selectionEnd = this.value.length;
}
filterOptions();
});
$input.on('click', function(e) {
e.preventDefault();
this.selectionStart = this.selectionEnd = this.value.length;
});
$input.on('blur', function() {
setTimeout(function() {
if (!$list.is(':hover')) {
toggleList(false);
cleanInput();
}
}, 100);
});
$input.on('input', handleInputChange);
$input.on('keydown', handleKeyDown);
$list.on('mouseenter', '[ms-code-select="tag-name-new"]', function() {
$(this).css('background-color', '#e0e0e0');
});
$list.on('mouseleave', '[ms-code-select="tag-name-new"]', function() {
if (!$(this).hasClass('highlighted')) {
$(this).css('background-color', '');
}
});
initializeWithValue();
toggleList(false);
});
});
</script>
#108 - Custom Form Submit Buttons
Create any element in Webflow and use it to submit any kind of form.
<!-- 💙 MEMBERSCRIPT #108 v0.1 💙 CUSTOM FORM SUBMIT BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Find all elements with the ms-code-submit-new attribute
const newSubmitButtons = document.querySelectorAll('[ms-code-submit-new]');
// Add click event listeners to each new submit button
newSubmitButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default action if it's a link
// Get the value of the ms-code-submit-new attribute
const submitId = this.getAttribute('ms-code-submit-new');
// Find the corresponding old submit button
const oldSubmitButton = document.querySelector(`[ms-code-submit-old="${submitId}"]`);
// If found, trigger a click on the old submit button
if (oldSubmitButton) {
oldSubmitButton.click();
} else {
console.error(`No matching old submit button found for ID: ${submitId}`);
}
});
});
});
</script>
#107 - Select Plan With Radios
Add a plan selector radio to sign up forms & plan update forms.
<!-- 💙 MEMBERSCRIPT #107 v0.1 💙 SELECT PLAN WITH RADIO BUTTONS -->
<script>
(function() {
const PRICE_ATTRIBUTES = [
'data-ms-plan:add',
'data-ms-plan:update',
'data-ms-price:add',
'data-ms-price:update'
];
function findElementWithAttribute(form) {
// First, check if the form itself has one of the attributes
for (let attr of PRICE_ATTRIBUTES) {
if (form.hasAttribute(attr)) {
return { element: form, attribute: attr };
}
}
// If not found on form, search child elements
for (let attr of PRICE_ATTRIBUTES) {
let element = Array.from(form.querySelectorAll('*')).find(el => el.hasAttribute(attr));
if (element) {
return { element, attribute: attr };
}
}
return null;
}
function updateAttribute(radio) {
const form = radio.closest('form');
if (!form) return;
const result = findElementWithAttribute(form);
if (result) {
result.element.setAttribute(result.attribute, radio.value);
}
}
function handleRadioChange(e) {
updateAttribute(e.target);
}
function initializeRadioButtons() {
const forms = document.querySelectorAll('[ms-code-radio-plan="form"]');
forms.forEach(form => {
const radios = form.querySelectorAll('input[type="radio"]');
radios.forEach(radio => {
radio.addEventListener('change', handleRadioChange);
if (radio.checked) {
updateAttribute(radio);
}
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeRadioButtons);
} else {
initializeRadioButtons();
}
})();
// Only keep this section if you have an update plan form on the page
(function() {
function updateRadioButtonState(radio) {
radio.checked = true;
radio.dispatchEvent(new Event('change'));
// Update custom radio button UI if present
const customRadio = radio.parentElement.querySelector('.w-radio-input');
if (customRadio) {
customRadio.classList.add('w--redirected-checked');
}
}
function checkAndSelectPlan() {
const msMemData = localStorage.getItem('_ms-mem');
if (!msMemData) return;
try {
const memberData = JSON.parse(msMemData);
const activePlanConnections = memberData.planConnections?.filter(conn => conn.active) || [];
if (activePlanConnections.length === 0) return;
const forms = document.querySelectorAll('[ms-code-radio-plan="form"]');
forms.forEach(form => {
const radios = form.querySelectorAll('input[type="radio"]');
radios.forEach(radio => {
const matchingPlan = activePlanConnections.find(conn => conn.payment.priceId === radio.value);
if (matchingPlan) {
updateRadioButtonState(radio);
}
});
});
} catch (error) {
console.error('Error processing _ms-mem data:', error);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAndSelectPlan);
} else {
checkAndSelectPlan();
}
})();
</script>
#106 - Liking & Saving CMS Items
Allow your members to save CMS items to their profile.
<!-- 💙 MEMBERSCRIPT #106 v0.2 💙 SAVING & UNSAVING CMS ITEMS -->
<style>
[ms-code-save], [ms-code-unsave] {
display: none;
}
[ms-code-save-item] {
display: none;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
let isLoggedIn = false;
let savedItems = [];
async function checkMemberLogin() {
try {
const member = await memberstack.getCurrentMember();
return !!member;
} catch (error) {
return false;
}
}
function getSavedItems(memberData) {
return memberData.savedItems || [];
}
function updateButtonVisibility() {
const saveButtons = document.querySelectorAll('[ms-code-save]');
const unsaveButtons = document.querySelectorAll('[ms-code-unsave]');
saveButtons.forEach(button => {
const itemId = button.getAttribute('ms-code-save');
button.style.display = !savedItems.includes(itemId) ? 'block' : 'none';
});
unsaveButtons.forEach(button => {
const itemId = button.getAttribute('ms-code-unsave');
button.style.display = savedItems.includes(itemId) ? 'block' : 'none';
});
}
function updateItemVisibility() {
const saveLists = document.querySelectorAll('[ms-code-save-list]');
saveLists.forEach(list => {
const filter = list.getAttribute('ms-code-save-list');
const items = list.querySelectorAll('[ms-code-save-item]');
items.forEach(item => {
const saveButton = item.querySelector('[ms-code-save]');
if (!saveButton) {
item.style.display = 'block';
return;
}
const itemId = saveButton.getAttribute('ms-code-save');
if (!isLoggedIn || filter === 'all') {
item.style.display = 'block';
} else if (filter === 'saved' & savedItems.includes(itemId)) {
item.style.display = 'block';
} else if (filter === 'unsaved' & !savedItems.includes(itemId)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
}
async function handleButtonClick(event) {
if (!isLoggedIn) return;
const button = event.currentTarget;
const action = button.getAttribute('ms-code-save') ? 'save' : 'unsave';
const itemId = button.getAttribute(action === 'save' ? 'ms-code-save' : 'ms-code-unsave');
if (action === 'save' && !savedItems.includes(itemId)) {
savedItems.push(itemId);
} else if (action === 'unsave') {
savedItems = savedItems.filter(id => id !== itemId);
}
try {
await memberstack.updateMemberJSON({ json: { savedItems } });
} catch (error) {
// Silently handle the error
}
updateButtonVisibility();
updateItemVisibility();
}
function addClickListeners() {
const saveButtons = document.querySelectorAll('[ms-code-save]');
const unsaveButtons = document.querySelectorAll('[ms-code-unsave]');
saveButtons.forEach(button => button.addEventListener('click', handleButtonClick));
unsaveButtons.forEach(button => button.addEventListener('click', handleButtonClick));
}
async function initializeScript() {
isLoggedIn = await checkMemberLogin();
if (isLoggedIn) {
try {
const result = await memberstack.getMemberJSON();
const memberData = result.data || {};
savedItems = getSavedItems(memberData);
} catch (error) {
// Silently handle the error
}
}
updateButtonVisibility();
updateItemVisibility();
addClickListeners();
// Set up a MutationObserver to watch for changes in the DOM
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
shouldUpdate = true;
}
});
if (shouldUpdate) {
updateButtonVisibility();
updateItemVisibility();
addClickListeners();
}
});
// Start observing the document with the configured parameters
observer.observe(document.body, { childList: true, subtree: true });
}
initializeScript();
});
</script>
#105 - Checkout After Login
Automatically launch the checkout if a member selects a price before logging in.
<!-- 💙 MEMBERSCRIPT #105 v0.1 💙 CHECKOUT AFTER LOGIN -->
<script>
/* Checks if the current URL matches the configured redirect URL, or if no specific URL is required */
function isCorrectPage() {
return redirectOnLoginURL === '' || window.location.pathname === redirectOnLoginURL;
}
/* Checks if Memberstack is fully loaded before running any Memberstack-specific code.*/
function memberstackReady(callback) {
function checkAndExecute() {
if (window.$memberstackDom) {
callback(); // Memberstack is ready, run the callback function.
} else {
setTimeout(checkAndExecute, 100); // Wait for 100ms and check again.
}
}
checkAndExecute(); // Start checking if Memberstack is ready.
}
/* Initiates the Stripe checkout process with a specified price ID.*/
async function initiateCheckout(priceId) {
try {
// Set a flag in session storage to indicate that the checkout page was accessed.
sessionStorage.setItem('ms_checkout_viewed', 'true');
await window.$memberstackDom.purchasePlansWithCheckout({
priceId, // The price ID for the product being purchased.
returnUrl: window.location.href, // Redirect the user back here after completing the checkout.
});
} catch (error) {
console.error('Failed to initiate payment:', error); // Provide error details in the console.
}
}
/* Main execution flow that starts once Memberstack is confirmed to be ready */
memberstackReady(() => {
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
if (member && sessionStorage.getItem('ms_price') && !sessionStorage.getItem('ms_checkout_viewed')) {
initiateCheckout(sessionStorage.getItem('ms_price')); // Start the checkout process if conditions are met.
}
}).catch(error => {
console.error('Failed to retrieve user data:', error); // Log an error if fetching member data fails.
});
});
</script>
#104 - Online Indicator
Show your site visitors your online status based on time zones.
<!-- 💙 MEMBERSCRIPT #104 v0.1 💙 ONLINE INDICATOR -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const businessHours = {
start: 9, // Business hours start at 9 AM
end: 17, // Business hours end at 5 PM
days: [1, 2, 3, 4, 5] // Monday to Friday
};
const colors = {
businessHours: '#34b426',
outsideBusinessHours: '#F25022'
};
const wrappers = document.querySelectorAll('[ms-code-online-wrapper]');
wrappers.forEach(wrapper => {
const timeZone = wrapper.getAttribute('ms-code-online-wrapper');
const dot = wrapper.querySelector('[ms-code-online="dot"]');
const timeSpan = wrapper.querySelector('[ms-code-online="time"]');
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
timeZone: timeZone
});
const formattedTime = formatter.format(now);
if (timeSpan) timeSpan.textContent = formattedTime;
const currentDay = now.getDay();
const currentHour = new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
hour12: false,
timeZone: timeZone
});
const isBusinessDay = businessHours.days.includes(currentDay);
const isBusinessHour = currentHour >= businessHours.start && currentHour < businessHours.end;
if (dot) {
dot.style.backgroundColor = (isBusinessDay && isBusinessHour) ? colors.businessHours : colors.outsideBusinessHours;
}
});
});
</script>
#103 - Custom Booking Form
Add a custom booking form to your website which creates a Google calendar event.
<!-- 💙 MEMBERSCRIPT #103 v0.1 💙 CUSTOM BOOKING FORM -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.33/moment-timezone-with-data.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
function getNextBusinessDays() {
let businessDays = [];
let currentDate = moment();
currentDate.add(1, 'days');
while (businessDays.length < 14) {
if (currentDate.day() !== 0 && currentDate.day() !== 6) {
let formattedDay = currentDate.format('dddd, MMMM D');
let rawDay = currentDate.format('YYYY-MM-DD');
businessDays.push({formattedDay, rawDay});
}
currentDate.add(1, 'days');
}
return businessDays;
}
function generateTimeSlots() {
let slots = [];
let startHour = 9;
let endHour = 16.5;
let currentTime = moment().startOf('day').add(startHour, 'hours');
while (currentTime.hour() + (currentTime.minute() / 60) <= endHour) {
let formattedTime = currentTime.format('h:mm A');
let timeValue = currentTime.format('HH:mm');
slots.push({formattedTime, timeValue});
currentTime.add(30, 'minutes');
}
return slots;
}
function updateTimestamp(day, time, timezone) {
let timestampInput = document.getElementById('timestamp');
if (!timestampInput) {
timestampInput = document.createElement('input');
timestampInput.type = 'hidden';
timestampInput.id = 'timestamp';
timestampInput.name = 'timestamp';
document.querySelector('form').appendChild(timestampInput);
}
let datetime = moment.tz(`${day} ${time}`, "YYYY-MM-DD HH:mm", timezone);
timestampInput.value = datetime.valueOf();
}
function populateFields() {
const days = getNextBusinessDays();
const times = generateTimeSlots();
const daySelect = document.querySelector('[ms-code-booking="day"]');
const timeSelect = document.querySelector('[ms-code-booking="time"]');
const form = daySelect.closest('form');
const timezone = form.getAttribute('ms-code-booking-timezone') || moment.tz.guess();
days.forEach(({formattedDay, rawDay}) => {
let option = new Option(formattedDay, rawDay);
daySelect.appendChild(option);
});
times.forEach(({formattedTime, timeValue}) => {
let option = new Option(formattedTime, timeValue);
timeSelect.appendChild(option);
});
function handleSelectChange() {
if (daySelect.value && timeSelect.value) {
updateTimestamp(daySelect.value, timeSelect.value, timezone);
}
}
daySelect.addEventListener('change', handleSelectChange);
timeSelect.addEventListener('change', handleSelectChange);
}
populateFields();
});
</script>
#102 - Automatically Resize Textarea Height
Increase or decrease a textarea's height based on its content.
<!-- 💙 MEMBERSCRIPT #102 v0.1 💙 RESIZE TEXTAREA VERTICALLY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const elements = document.querySelectorAll('[data-ms-post="content"], [ms-code-resize-input="height"]');
elements.forEach(element => {
if (element.tagName.toLowerCase() === 'textarea') {
element.addEventListener('input', function() {
autoResize(this);
}, false);
}
});
function autoResize(element) {
const maxHeight = parseInt(getComputedStyle(element).maxHeight, 10);
element.style.height = 'auto';
element.style.overflow = 'hidden'; // Prevents scrollbar appearance during height adjustment
if (element.scrollHeight > maxHeight) {
element.style.height = `${maxHeight}px`;
element.style.overflow = 'auto'; // Adds scrollbar when content exceeds max height
} else {
element.style.height = `${element.scrollHeight}px`;
}
}
});
</script>
#101 - Automatically Resize Input Width
Increase or decrease an input's width based on content.
<!-- 💙 MEMBERSCRIPT #101 v0.1 💙 RESIZE INPUT HORIZONTALLY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const elements = document.querySelectorAll('[ms-code-resize-input="width"]');
// Store the initial widths
const initialWidths = new Map();
elements.forEach(element => {
initialWidths.set(element, element.offsetWidth);
});
elements.forEach(element => {
element.addEventListener('input', function() {
autoResizeWidth(this);
});
});
function autoResizeWidth(element) {
// Find the nearest hidden measure element
const measurer = element.nextElementSibling.getAttribute('ms-code-resize-input') === 'hidden-measure'
? element.nextElementSibling
: null;
if (!measurer) return; // Exit if no measurer is found
measurer.textContent = element.value;
const maxWidth = parseInt(getComputedStyle(element).maxWidth, 10);
const minWidth = initialWidths.get(element);
const contentWidth = measurer.offsetWidth;
if (contentWidth > minWidth && contentWidth < maxWidth) {
element.style.width = `${contentWidth}px`;
} else if (contentWidth >= maxWidth) {
element.style.width = `${maxWidth}px`;
} else {
element.style.width = `${minWidth}px`;
}
}
});
</script>
#100 - Auto-Compress Image Uploads
Compress image uploads, including profile images.
<!-- 💙 MEMBERSCRIPT #100 v0.1 💙 AUTO-COMPRESSED IMAGE UPLOADS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/compressorjs/1.2.1/compressor.min.js" integrity="sha512-MgYeYFj8R3S6rvZHiJ1xA9cM/VDGcT4eRRFQwGA7qDP7NHbnWKNmAm28z0LVjOuUqjD0T9JxpDMdVqsZOSHaSA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const compressibleInputs = document.querySelectorAll('input[type="file"][ms-code-file_compress]');
compressibleInputs.forEach(fileInput => {
let isCompressing = false;
fileInput.addEventListener('change', function (event) {
if (isCompressing) {
isCompressing = false;
return;
}
if (fileInput.files.length === 0) {
return;
}
const originalFile = fileInput.files[0];
const compressionLevel = parseFloat(fileInput.getAttribute('ms-code-file_compress'));
new Compressor(originalFile, {
quality: compressionLevel,
maxWidth: 2000,
maxHeight: 2000,
success(compressedResult) {
const compressedFile = new File([compressedResult], originalFile.name, {
type: compressedResult.type,
lastModified: Date.now(),
});
const dataTransfer = new DataTransfer();
dataTransfer.items.add(compressedFile);
fileInput.files = dataTransfer.files;
isCompressing = true;
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
},
error(err) {
console.error('Compression Error: ', err.message);
},
});
event.stopPropagation();
}, true);
});
});
</script>
#99 - Custom File Inputs
Turn anything into a file input!
<!-- 💙 MEMBERSCRIPT #99 v0.1 💙 CUSTOM FILE UPLOAD INPUT -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const uploadButtons = document.querySelectorAll('[ms-code-file_uploader]');
uploadButtons.forEach(button => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
fileInput.name = button.getAttribute('ms-code-file_uploader');
fileInput.accept = button.getAttribute('ms-code-file_types');
document.body.appendChild(fileInput);
button.addEventListener('click', function (e) {
e.preventDefault();
fileInput.click();
});
fileInput.addEventListener('change', function () {
const fileName = fileInput.files[0].name;
button.querySelector('div').textContent = fileName;
});
});
});
</script>
#98 - Age Gating
Make users confirm their age before proceeding.
<!-- 💙 MEMBERSCRIPT #98 v0.1 💙 AGE GATE -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const form = document.querySelector('form[ms-code-age-gate]');
const dayInput = document.querySelector('input[ms-code-age-gate="day"]');
const monthInput = document.querySelector('input[ms-code-age-gate="month"]');
const yearInput = document.querySelector('input[ms-code-age-gate="year"]');
const backButton = document.querySelector('a[ms-code-age-gate="back"]');
const wrapper = document.querySelector('[ms-code-age-gate="wrapper"]');
const errorDiv = document.querySelector('div[ms-code-age-gate="error"]');
if (localStorage.getItem('ageVerified') === 'true') {
wrapper.remove();
return;
}
backButton.addEventListener('click', (e) => {
e.preventDefault();
window.history.back();
});
const inputs = [dayInput, monthInput, yearInput];
inputs.forEach((input, index) => {
input.addEventListener('keyup', (e) => {
const maxChars = input === yearInput ? 4 : 2;
let value = e.target.value;
if (input === dayInput && value.length === maxChars) {
value = value > 31 ? '31' : value.padStart(2, '0');
} else if (input === monthInput && value.length === maxChars) {
value = value > 12 ? '12' : value.padStart(2, '0');
}
e.target.value = value;
if (value.length === maxChars) {
const nextInput = inputs[index + 1];
if (nextInput) {
nextInput.focus();
}
}
});
});
form.addEventListener('submit', (e) => {
e.preventDefault();
const enteredDate = new Date(yearInput.value, monthInput.value - 1, dayInput.value);
const currentDate = new Date();
const ageDifference = new Date(currentDate - enteredDate);
const age = Math.abs(ageDifference.getUTCFullYear() - 1970);
const ageLimit = parseInt(form.getAttribute('ms-code-age-gate').split('-')[1], 10);
if (age >= ageLimit) {
console.log('Age verified.');
errorDiv.style.display = 'none';
localStorage.setItem('ageVerified', 'true');
wrapper.remove();
} else {
console.log('Age verification failed, user is under the age limit.');
errorDiv.style.display = 'block';
}
});
});
</script>
#97 - Upload Files To S3 Bucket
Allow uploads to an S3 bucket from a Webflow form.
<!-- 💙 MEMBERSCRIPT #97 v0.1 💙 S3 FILE UPLOADS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
document.querySelectorAll('input[ms-code-s3-uploader]').forEach(input => {
input.addEventListener('change', function() {
if (this.files.length > 0) {
const file = this.files[0];
const uuid = generateUUID();
const extension = file.name.split('.').pop();
const newFileName = `${uuid}.${extension}`;
const wrapper = this.closest('div[ms-code-s3-wrapper]');
const s3FileInput = wrapper.querySelector('input[ms-code-s3-file]');
s3FileInput.value = s3FileInput.getAttribute('ms-code-s3-file') + encodeURIComponent(newFileName);
const apiGatewayUrl = wrapper.getAttribute('ms-code-s3-wrapper').replace('${encodeURIComponent(fileName)}', encodeURIComponent(newFileName));
fetch(apiGatewayUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type }
})
.then(response => {
if (response.status !== 200) {
throw new Error(`Upload failed with status: ${response.status}`);
}
console.log('File uploaded successfully:', newFileName);
})
.catch(error => {
console.error('Upload error:', error);
alert('Upload failed.');
});
}
});
});
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', function(event) {
const s3Inputs = Array.from(form.querySelectorAll('input[ms-code-s3-file]'));
const allUrlsSet = s3Inputs.every(input => input.value);
if (!allUrlsSet) {
event.preventDefault();
alert('Please wait for all files to finish uploading before submitting.');
}
});
});
});
</script>
#96 - Save a Tally
Create and update a count/tally which saves to a custom field!
<!-- 💙 MEMBERSCRIPT #96 v0.1 💙 KEEPING A TALLY -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
const addButtons = document.querySelectorAll("[ms-code-add-tally]");
addButtons.forEach(button => {
button.addEventListener("click", async () => {
const tallyKey = button.getAttribute("ms-code-add-tally");
const tallyText = document.querySelector(`[ms-code-tally="${tallyKey}"]`);
if(tallyText){
let currentCount = parseInt(tallyText.textContent, 10);
currentCount += 1;
tallyText.textContent = currentCount;
// Store the new tally count to Memberstack
let newFields = {};
newFields[tallyKey] = currentCount;
await memberstack.updateMember({customFields: newFields});
}
});
});
});
</script>
#95 - Confetti On Click
Make some fun confetti fly on click!
<!-- 💙 MEMBERSCRIPT #95 v0.1 💙 CONFETTI -->
<script src="https://cdn.jsdelivr.net/npm/tsparticles-confetti@2.12.0/tsparticles.confetti.bundle.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const confettiElems = document.querySelectorAll("[ms-code-confetti]");
confettiElems.forEach(item => {
item.addEventListener("click", () => {
const effect = item.getAttribute("ms-code-confetti");
switch (effect) {
case "falling":
const makeFall = () => {
confetti({
particleCount: 100,
startVelocity: 30,
spread: 360,
origin: { x: Math.random(), y: 0 },
colors: ['#ffffff','#ff0000','#00ff00','#0000ff']
});
}
setInterval(makeFall, 2000);
break;
case "single":
confetti({
particleCount: 1,
startVelocity: 30,
spread: 360,
origin: { x: Math.random(), y: Math.random() }
});
break;
case "sides":
confetti({
particleCount: 100,
startVelocity: 30,
spread: 360,
origin: { x: Math.random(), y: 0.5 }
});
break;
case "explosions":
confetti({
particleCount: 100,
startVelocity: 50,
spread: 360
});
break;
case "bottom":
confetti({
particleCount: 100,
startVelocity: 30,
spread: 360,
origin: { x: 0.5, y: 1 }
});
break;
default:
console.log("Unknown confetti effect");
}
});
});
});
</script>
#94 - Set href Attribute
Dynamically set a link using the Webflow CMS (or anything else)
<!-- 💙 MEMBERSCRIPT #94 v0.1 💙 SET HREF ATTRIBUTE -->
<script>
window.onload = function(){
var elements = document.querySelectorAll('[ms-code-href]');
elements.forEach(function(element) {
var url = element.getAttribute('ms-code-href');
element.setAttribute('href', url);
});
};
</script>
#93 - Force Valid URLs in Form Input
Automatically convert anything in your input to a valid URL.
<!-- 💙 MEMBERSCRIPT #93 v0.1 💙 FORCE INPUT TO BE A VALID URL -->
<script>
// Get all form fields with attribute ms-code-convert="link"
const formFields = document.querySelectorAll('input[ms-code-convert="link"], textarea[ms-code-convert="link"]');
// Add event listener to each form field
formFields.forEach((field) => {
field.addEventListener('input', convertToLink);
});
// Function to convert input to a link
function convertToLink(event) {
const input = event.target;
// Get user input
const userInput = input.value.trim();
// Check if input starts with http:// or https://
if (userInput.startsWith('http://') || userInput.startsWith('https://')) {
input.value = userInput; // No conversion needed for valid links
} else {
input.value = `http://${userInput}`; // Prepend http:// for simplicity
}
}
</script>
#92 - Change Page on Click
Change the current page URL when clicking any element.
<!-- 💙 MEMBERSCRIPT #92 v0.1 💙 TURN ANYTHING INTO A LINK -->
<script>
document.addEventListener('click', function(event) {
let target = event.target;
// Traverse up the DOM tree to find an element with the ms-code-navigate attribute
while (target && !target.getAttribute('ms-code-navigate')) {
target = target.parentElement;
}
// If we found an element with the ms-code-navigate attribute
if (target) {
const navigateUrl = target.getAttribute('ms-code-navigate');
if (navigateUrl) {
event.preventDefault();
// Always open in a new tab
window.open(navigateUrl, '_blank');
}
}
});
</script>
#91 - Hide Popup For Set Duration
Hide a popup for X time when a button is clicked.
<!-- 💙 MEMBERSCRIPT #91 v0.1 💙 HIDE POPUP FOR SET DURATION -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
// Look for elements with 'ms-code-hide-popup' attribute
var items = $('[ms-code-hide-popup]');
var button;
var timeElement;
// Determine which element is the button and which is the timer
items.each(function(index, item) {
var value = $(item).attr('ms-code-hide-popup');
if (value === "button") {
button = $(item);
} else {
timeElement = $(item);
}
});
// Calculate the target date
var calculateTargetDate = function(timeStr) {
var splitTime = timeStr.split(':');
var now = new Date();
now.setDate(now.getDate() + parseInt(splitTime[0])); // add days
now.setHours(now.getHours() + parseInt(splitTime[1])); // add hours
now.setMinutes(now.getMinutes() + parseInt(splitTime[2])); // add minutes
now.setSeconds(now.getSeconds() + parseInt(splitTime[3])); // add seconds
return now;
};
// Check if element should be removed from DOM
var checkTimeAndRemoveElement = function() {
var targetDate = localStorage.getItem('targetDate');
if (targetDate && new Date() < new Date(targetDate)) {
timeElement.remove();
} else {
localStorage.removeItem('targetDate');
}
};
// Action on button click
button.on('click', function() {
var time = timeElement.attr('ms-code-hide-popup');
localStorage.setItem('targetDate', calculateTargetDate(time));
checkTimeAndRemoveElement();
});
// Initial check
checkTimeAndRemove AndRemoveElement();
});
</script>
#90 - Show Elements On Input Change
Display 1 or more elements when a user changes the input value.
<!-- 💙 MEMBERSCRIPT #90 v0.1 💙 SHOW ELEMENTS ON INPUT CHANGE -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
// Initially hide all elements
$('[ms-code-show-item]').css('display', 'none');
setTimeout(function() {
$('[ms-code-show-field]').change(function() {
var field = $(this).attr('ms-code-show-field');
$('[ms-code-show-item=' + field + ']').css('display', 'block');
});
}, 500); // Wait 500ms before starting, you can change this time based on your needs
});
</script>
#89 - Custom Context Menus
Show a custom, built-in-Webflow context menu when your element is right-clicked.
<!-- 💙 MEMBERSCRIPT #89 v0.1 💙 CUSTOM CONTEXT MENUS -->
<script>
// Cache elements
const items = document.querySelectorAll("[ms-code-context-item]");
const menus = document.querySelectorAll("[ms-code-context-menu]");
// Disable default context menu on item right click and show custom context menu
items.forEach(element => {
element.addEventListener('contextmenu', event => {
event.preventDefault(); // Prevents showing the default context menu
hideAllMenus(); // Make sure other menus are hidden
// fetch the related menu, make it visible
const menuItemId = element.getAttribute("ms-code-context-item");
const menu = document.querySelector(`[ms-code-context-menu="${menuItemId}"]`);
if (menu) {
menu.classList.remove('hidden');
menu.classList.add('visible');
}
});
});
// Add click event on custom menus to stop event propagation
menus.forEach(menu => {
menu.addEventListener('click', event => {
event.stopPropagation();
});
});
// Close custom context menu on outside click
document.body.addEventListener('click', hideAllMenus);
// Helper function to hide all custom context menus
function hideAllMenus() {
menus.forEach(menu => {
menu.classList.remove('visible');
menu.classList.add('hidden');
});
}
</script>
#88 - Show Current State For CMS, Folder Links
Display the Webflow "current" state on your nested pages & CMS items.
<!-- 💙 MEMBERSCRIPT #88 v0.1 💙 SHOW CURRENT STATE FOR NESTED URLS -->
<script>
window.onload = function() {
var currentUrl = window.location.href;
var elements = document.querySelectorAll('[ms-code-nested-link]'); // get all elements with ms-code-nested-link attribute
elements.forEach(function (element) {
var linkAttrValue = element.getAttribute('ms-code-nested-link'); // get the ms-code-nested-link value
if (currentUrl.includes(linkAttrValue)) { // check if current url matches the attribute value
element.classList.add('w--current'); // apply the class
}
});
};
</script>
#87 - Remove a Plan After Countdown
Create time-sensitive secure content!
<!-- 💙 MEMBERSCRIPT #87 v0.1 💙 REMOVE PLAN AFTER COUNTDOWN -->
<script>
const memberstack = window.$memberstackDom;
const countdown = new Date(localStorage.getItem('countdownDateTime'));
// Check if date has passed
const checkDate = async () => {
const now = new Date();
if (now > countdown) {
// Remove member's free plan
await memberstack.removePlan({
planId: "pln_10-minutes-of-gif-access-rw1fh0ktg"
});
console.log("Plan removed");
// Reload the page
location.reload();
}
}
// Execute checkDate every 10s
const intervalId = setInterval(checkDate, 10000);
</script>
#86 - Free & Simple Text-To-Speech
Add a button which allows visitors to listen to your article.
<!-- 💙 MEMBERSCRIPT #86 v0.1 💙 VOICE TO TEXT BUTTON -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const textDiv = document.querySelector('[ms-code-text-to-speech="text"]');
const speakButton = document.querySelector('[ms-code-text-to-speech="button"]');
let utterance = new SpeechSynthesisUtterance();
speakButton.addEventListener('click', () => {
if(speechSynthesis.speaking || speechSynthesis.paused) {
speechSynthesis.cancel(); // stops current speech
} else {
utterance.text = textDiv.innerText;
speechSynthesis.speak(utterance); // starts speaking
}
});
});
</script>
#85 - "Add a row" Form Inputs
Allow members to add and delete rows from a form input.
<!-- 💙 MEMBERSCRIPT #85 v0.1 💙 ADD A ROW FORM INPUTS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
// Hide all rows except the original row
$('[ms-code-row-input="new"]').hide();
// Add row button click event
$('[ms-code-row-input="add-row"]').click(function(e) {
e.preventDefault();
var clonedRow = $('[ms-code-row-input="new"]').first().clone();
clonedRow.find('input').val('');
clonedRow.show().appendTo('[ms-code-row-input="row-container"]');
updateHolderValue();
});
// Delete row button click event
$(document).on('click', '[ms-code-row-input="delete"]', function(e) {
e.preventDefault();
$(this).closest('[ms-code-row-input="new"]').remove();
updateHolderValue();
});
// Event for all inputs
$(document).on('input', '[ms-code-row-input="original"], [ms-code-row-input="new-input"], [ms-code-row-input="holder"]', function() {
if ($(this).is('[ms-code-row-input="holder"]')) {
updateRowsFromHolder();
} else {
updateHolderValue();
}
});
// Function to update the holder input value
function updateHolderValue() {
var values = [];
$('[ms-code-row-input="original"], [ms-code-row-input="new-input"]').each(function() {
var value = $(this).val().trim();
if (value) {
values.push(value);
}
});
$('[ms-code-row-input="holder"]').val(values.join(','));
}
// Function to update rows from the holder field
function updateRowsFromHolder() {
var holderValue = $('[ms-code-row-input="holder"]').val();
var values = holderValue.split(',');
$('[ms-code-row-input="new"]').not(':first').remove();
// For each holder value, create a new row
values.forEach(function(val, idx) {
if (idx === 0) {
$('[ms-code-row-input="original"]').val(val);
} else {
var newRow = $('[ms-code-row-input="new"]').first().clone().appendTo('[ms-code-row-input="row-container"]');
newRow.find('input').val(val);
newRow.show();
}
});
}
// Initial update of the holder input value
updateHolderValue();
// Adding MutationObserver to call updateRowsFromHolder on changes to the holder field
var targetNode = $('[ms-code-row-input="holder"]')[0];
var config = { attributes: true, childList: true, subtree: true };
var callback = function(mutationsList, observer) {
for(let mutation of mutationsList) {
if (mutation.type === 'childList')
{
updateRowsFromHolder();
}
}
};
var observer = new MutationObserver(callback);
observer.observe(targetNode, config);
});
</script>
#84 - Clear Inputs OnLoad
Add this script to any page to clear the value of a custom field on page load.
<!-- 💙 MEMBERSCRIPT #84 v0.1 💙 CLEAR INPUT VALUES ONLOAD -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
const fieldsToClear = ["phone", "last-name"]; // Specify the fields to clear
// Clear inputs and Memberstack fields on page load
memberstack.getCurrentMember().then(async ({ data: member }) => {
if (member) {
const customFieldsToUpdate = {};
fieldsToClear.forEach(fieldName => {
customFieldsToUpdate[fieldName] = '';
});
try {
await memberstack.updateMember({
customFields: customFieldsToUpdate
});
console.log("Fields cleared on page load.");
} catch (error) {
console.error('Error clearing fields on page load:', error);
}
}
// Clear input values on page load for specified fields
fieldsToClear.forEach(fieldName => {
const inputField = document.querySelector(`[data-ms-member="${fieldName}"]`);
if (inputField) {
inputField.value = '';
}
});
});
});
</script>
#83 - Cross-Device Cookie Preferences
Allow members to save their cookie preferences to their account.
<!-- 💙 MEMBERSCRIPT #83 v0.1 💙 CROSS-DEVICE COOKIE PREFERENCES -->
<script>
// Function to retrieve a cookie value by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift());
}
async function updateMemberConsentPreferences(fsCcCookieValue) {
try {
const memberstack = window.$memberstackDom;
const userData = await memberstack.getCurrentMember();
if (userData && userData.data.customFields) {
if (!userData.data.customFields['cookie-consent']) {
const decodedFsCcCookieValue = decodeURIComponent(fsCcCookieValue);
await memberstack.updateMember({
customFields: {
'cookie-consent': decodedFsCcCookieValue
}
});
} else {
document.cookie = `fs-cc=${encodeURIComponent(userData.data.customFields['cookie-consent'])}`;
}
}
} catch (error) {}
}
async function initialize() {
const fsCcCookieValue = getCookie('fs-cc');
if (fsCcCookieValue) {
await updateMemberConsentPreferences(fsCcCookieValue);
const checkboxes = document.querySelectorAll('[fs-cc-checkbox]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', async () => {
const memberstack = window.$memberstackDom;
const userData = await memberstack.getCurrentMember();
if (userData && userData.data.customFields) {
const customFieldKey = 'cookie-consent';
const checkboxName = checkbox.getAttribute('fs-cc-checkbox');
if (userData.data.customFields[customFieldKey]) {
const consentData = JSON.parse(userData.data.customFields[customFieldKey]);
consentData.consents[checkboxName] = checkbox.checked;
const updatedCustomField = JSON.stringify(consentData);
await memberstack.updateMember({
customFields: {
[customFieldKey]: updatedCustomField
}
});
document.cookie = `fs-cc=${encodeURIComponent(updatedCustomField)}`;
}
}
});
});
}
}
// Initialize the script
initialize();
</script>
#82 - License Keys
Secure your downloadable content with license keys.
<!-- 💙 MEMBERSCRIPT #82 v0.1 💙 LICENSE KEYS -->
<script>
const memberstack = window.$memberstackDom;
// Initialize MutationObserver
const observer = new MutationObserver(async (mutations) => {
const downloadBtn = document.getElementById("download");
if (downloadBtn) {
// Element exists, so add event listener
downloadBtn.addEventListener("click", async () => {
await memberstack.removePlan({
planId: "pln_activate-license-key-952c0d8u"
});
console.log("Plan removed");
});
// Stop observing since we found the element
observer.disconnect();
}
});
// Observe the whole document
observer.observe(document.body, { childList: true, subtree: true });
</script>
#81 - Custom Checkbox Values
Pass through a unique value based on whether or not the box is checked.
<!-- 💙 MEMBERSCRIPT #81 v0.1 💙 CUSTOM CHECKBOX VALUES -->
<script>
document.addEventListener('submit', function(e) {
var checkboxes = document.querySelectorAll('[ms-code-custom-checkbox]');
checkboxes.forEach(function(checkbox) {
var values = checkbox.getAttribute('ms-code-custom-checkbox').split(',');
var valueToSubmit = checkbox.checked ? values[0] : values[1];
var hiddenInput = document.createElement('input');
// Copy all attributes except type and ms-code-custom-checkbox
for (var i = 0; i < checkbox.attributes.length; i++) {
var attr = checkbox.attributes[i];
if (attr.name !== 'type' && attr.name !== 'ms-code-custom-checkbox') {
hiddenInput.setAttribute(attr.name, attr.value);
}
}
hiddenInput.type = 'hidden';
hiddenInput.value = valueToSubmit;
checkbox.form.appendChild(hiddenInput);
checkbox.remove(); // Remove the original checkbox so it doesn't interfere with submission
});
});
</script>
#79 - Trigger Click onHover
Trigger a click event onHover.
<!-- 💙 MEMBERSCRIPT #79 v0.1 💙 HOVER BASED TABS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const hoverTabElements = document.querySelectorAll('[ms-code-onhover="click"]');
hoverTabElements.forEach(hoverTabElement => {
hoverTabElement.addEventListener('mouseenter', function() {
hoverTabElement.click(); // Click on the element when hovering
});
});
});
</script>
#78 - Clear Inputs OnClick
Create a button which can clear the values of one or more inputs.
<!-- 💙 MEMBERSCRIPT #78 v0.1 💙 CLEAR INPUT VALUES ONCLICK -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const clearBtns = document.querySelectorAll('[ms-code-clear-value]');
clearBtns.forEach(btn => {
btn.addEventListener('click', () => {
const fieldIds = btn.getAttribute('ms-code-clear-value').split(',');
fieldIds.forEach(fieldId => {
const input = document.querySelector(`[data-ms-member="${fieldId}"]`);
if (input) {
input.value = '';
}
});
});
});
});
</script>
#77 - Universal Emojis
Make your on-site emojis the same on all devices/OS.
<!-- 💙 MEMBERSCRIPT #77 v0.1 💙 UNIVERSAL EMOJIS -->
<script>
document.querySelectorAll('[ms-code-emoji]').forEach(element => {
var imageUrl = element.getAttribute('ms-code-emoji');
var img = document.createElement('img');
img.src = imageUrl;
var textStyle = window.getComputedStyle(element);
var adjustedHeight = parseFloat(textStyle.fontSize) * 1.0;
img.style.height = adjustedHeight + 'px';
img.style.width = 'auto';
img.style.verticalAlign = 'text-top';
element.innerHTML = ''; // Clears the text content inside the span
element.appendChild(img);
});
</script>
#76 - Time-Based Visibility
Show different elements based on the current time of day.
<!-- 💙 MEMBERSCRIPT #76 v0.1 💙 TIME-BASED VISIBILITY -->
<script>
function hideElements() {
const elements = document.querySelectorAll('[ms-code-time]');
elements.forEach(element => {
element.style.display = 'none';
});
}
function displayBasedOnTime() {
const elements = document.querySelectorAll('[ms-code-time]');
const currentTime = new Date();
elements.forEach(element => {
const timeRange = element.getAttribute('ms-code-time');
const [start, end] = timeRange.split(' - ');
const [startHour, startMinute] = start.split(':').map(Number);
const [endHour, endMinute] = end.split(':').map(Number);
let startTime = new Date(currentTime);
startTime.setHours(startHour, startMinute, 0, 0);
let endTime = new Date(currentTime);
endTime.setHours(endHour, endMinute, 0, 0);
// If the end time is earlier than the start time, add a day to the end time
if (endTime < startTime) {
endTime.setDate(endTime.getDate() + 1);
}
if (currentTime >= startTime && currentTime <= endTime) {
element.style.display = 'flex';
}
});
}
// Call the functions
hideElements();
displayBasedOnTime();
</script>
#75 - Disallowed Character Inputs
Show a custom error message if a user enters something you set in an input.
<!-- 💙 MEMBERSCRIPT #75 v0.1 💙 DISALOWED CHARACTER INPUTS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const inputFields = document.querySelectorAll('[ms-code-disallow]');
inputFields.forEach(inputField => {
const errorBlock = inputField.nextElementSibling;
errorBlock.innerHTML = ''; // Use innerHTML to interpret <br> tags
inputField.addEventListener('input', function() {
const rules = inputField.getAttribute('ms-code-disallow').split(')');
let errorMessage = '';
rules.forEach(rule => {
const parts = rule.trim().split('=');
const ruleType = parts[0].substring(1); // Remove the opening parenthesis
const disallowedValue = parts[1];
if (ruleType.startsWith('custom')) {
const disallowedChar = ruleType.split('-')[1]; // Extract the character after the '-'
if (inputField.value.includes(disallowedChar)) {
errorMessage += disallowedValue + '<br>'; // Add line break
}
} else if (ruleType === 'space' && inputField.value.includes(' ')) {
errorMessage += disallowedValue + '<br>'; // Add line break
} else if (ruleType === 'number' && /\d/.test(inputField.value)) {
errorMessage += disallowedValue + '<br>'; // Add line break
} else if (ruleType === 'special' && /[^a-zA-Z0-9\s]/.test(inputField.value)) { // Notice the \s here
errorMessage += disallowedValue + '<br>'; // Add line break
}
});
errorBlock.innerHTML = errorMessage || ''; // Use innerHTML to interpret <br> tags
});
});
});
</script>
#74 - Styling with Link Parameters
Update page styling based on a link parameter. Ex. ?ms-code-target=CLASSNAME&ms-code-style=display:block
<!-- 💙 MEMBERSCRIPT #74 v0.1 💙 UPDATE STYLING WITH LINK PARAMS -->
<script>
// Function to parse URL parameters
function getURLParameter(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
}
// Function to apply styles
function applyStylesFromURL() {
const targetClass = getURLParameter('ms-code-target');
const rawStyles = getURLParameter('ms-code-style');
if (targetClass && rawStyles) {
const elements = document.querySelectorAll(`.${targetClass}`);
const styles = rawStyles.split(';').filter(style => style.trim() !== ''); // filter out any empty strings
styles.forEach(style => {
const [property, value] = style.split(':');
elements.forEach(element => {
element.style[property] = value;
});
});
}
}
// Call the function once the DOM is loaded
window.addEventListener('DOMContentLoaded', (event) => {
applyStylesFromURL();
});
</script>
#73 - Display Date & Time
Display the current time, time-of-day, day, month, or year to a user. Works if logged in or logged out.
<!-- 💙 MEMBERSCRIPT #73 v0.1 💙 DATES AND TIMES -->
function getCurrentDateInfo(attribute) {
const now = new Date();
const options = { hour12: true };
switch(attribute) {
case "day":
return now.toLocaleDateString('en-US', { weekday: 'long' });
case "time":
return now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase();
case "month":
return now.toLocaleDateString('en-US', { month: 'long' });
case "year":
return now.getFullYear().toString();
case "time-of-day":
const hour = now.getHours();
if (5 <= hour && hour < 12) return "morning";
if (12 <= hour && hour < 17) return "afternoon";
if (17 <= hour && hour < 21) return "evening";
return "night";
default:
return "Invalid attribute";
}
}
function updateDateInfoOnPage() {
const spanTags = document.querySelectorAll('span[ms-code-date]');
spanTags.forEach(tag => {
const attributeValue = tag.getAttribute('ms-code-date');
const dateInfo = getCurrentDateInfo(attributeValue);
tag.textContent = dateInfo;
});
}
// Call the function to update the content on the page
updateDateInfoOnPage();
</script>
#72 - Validate Original Values
Only allow a form to be submitted if the input value is original (ie. Usernames)
<!-- 💙 MEMBERSCRIPT #72 v0.1 💙 VALIDATE ORIGINAL VALUES -->
<style>
[ms-code-available="true"],
[ms-code-available="false"],
[ms-code-available="invalid"]{
display: none;
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
let input = document.querySelector('[ms-code-available="input"]');
let trueElement = document.querySelector('[ms-code-available="true"]');
let falseElement = document.querySelector('[ms-code-available="false"]');
let invalidElement = document.querySelector('[ms-code-available="invalid"]');
let listElements = Array.from(document.querySelectorAll('[ms-code-available="list"]'));
let submitButton = document.querySelector('[ms-code-available="submit"]');
function checkUsername() {
// Check if the input matches any of the list items
let isTaken = listElements.some(elem => elem.textContent.trim() === input.value.trim());
if (isTaken) {
trueElement.style.display = 'none';
falseElement.style.display = 'flex';
submitButton.classList.add('disabled'); // disable the button if username is taken
} else {
trueElement.style.display = 'flex';
falseElement.style.display = 'none';
submitButton.classList.remove('disabled');
}
}
input.addEventListener('input', function() {
// Display the invalid element if input length is between 1 and 3
if (input.value.length >= 1 && input.value.length <= 3) {
invalidElement.style.display = 'flex';
} else {
invalidElement.style.display = 'none';
}
// Add the .disabled class to the submit button if input is empty or less than 3 characters
if (input.value.length <= 3) {
submitButton.classList.add('disabled');
trueElement.style.display = 'none';
falseElement.style.display = 'none';
} else {
checkUsername();
}
});
});
</script>
#71 - Redirect if Certain Fields are Empty
Redirect a member to an onboarding page if certain custom fields are empty.
<!-- 💙 MEMBERSCRIPT #71 v0.1 💙 REDIRECT IF FIELDS ARE EMPTY -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
const onboardingPageUrl = '/onboarding'; // replace
const customFieldKeys = 'custom-field-1,custom-field-2'; // replace
// No need to edit past this line
const member = await memberstack.getCurrentMember();
if (!member) {
return;
}
// If current page slug matches the redirect slug, exit the script
const currentPageSlug = window.location.pathname;
if (currentPageSlug === onboardingPageUrl) {
return;
}
async function checkOnboardingStatus() {
try {
const memberData = await memberstack.updateMember({});
const customFields = customFieldKeys.split(',');
for (let field of customFields) {
if (!memberData.data.customFields[field.trim()]) {
// Redirect to onboarding page if the custom field is empty
window.location.href = onboardingPageUrl;
return;
}
}
} catch (error) {
console.error(`Error in checkOnboardingStatus function: ${error}`);
}
}
// Check onboarding status and potentially redirect
checkOnboardingStatus().catch(error => {
console.error(`Error in MemberScript #71 initial functions: ${error}`);
});
});
</script>
#70 - Hide Old/Seen CMS Items
Only show CMS items which are new to a particular member. If they've seen it, hide it.
<!-- 💙 MEMBERSCRIPT #70 v0.1 💙 HIDE OLD CMS ITEMS -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
// Only proceed if a member is found
const member = await memberstack.getCurrentMember();
if (!member) {
console.log('No member found in MemberScript #70, exiting script');
return;
}
async function getCmsItemsFromJson() {
try {
const memberData = await memberstack.getMemberJSON();
return memberData?.data?.cmsItems || [];
} catch (error) {
console.error(`Error in getCmsItemsFromJson function: ${error}`);
}
}
async function updateCmsItemsInJson(newCmsItems) {
try {
const memberData = await memberstack.getMemberJSON();
memberData.data = memberData.data || {};
memberData.data.cmsItems = newCmsItems;
console.log(`CMS items in JSON after update: ${JSON.stringify(newCmsItems)}`);
await memberstack.updateMemberJSON({ json: memberData.data });
} catch (error) {
console.error(`Error in updateCmsItemsInJson function: ${error}`);
}
}
async function hideSeenCmsItems() {
try {
const cmsItemsElements = document.querySelectorAll('[ms-code-cms-item]');
const cmsItemsFromJson = await getCmsItemsFromJson();
cmsItemsElements.forEach(element => {
const cmsValue = element.getAttribute('ms-code-cms-item');
if (cmsItemsFromJson.includes(cmsValue)) {
element.style.display = 'none';
} else {
cmsItemsFromJson.push(cmsValue);
}
});
// Update the CMS items in JSON after the checks
await updateCmsItemsInJson(cmsItemsFromJson);
} catch (error) {
console.error(`Error in hideSeenCmsItems function: ${error}`);
}
}
// Hide seen CMS items when the page loads
hideSeenCmsItems().catch(error => {
console.error(`Error in MemberScript #70 initial functions: ${error}`);
});
});
</script>
#69 - Notify Members of New CMS Items
Display an element when there are new CMS items.
<!-- 💙 MEMBERSCRIPT #69 v0.1 💙 DISPLAY ELEMENT IF NEW CMS ITEMS -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
// Set this variable to 'YES' or 'NO' depending on whether you want the UI to be displayed for new users
const displayForNewUsers = 'YES';
// Only proceed if a member is found
const member = await memberstack.getCurrentMember();
if (!member) {
console.log('No member found, exiting script');
return;
}
async function getUpdatesIDFromJson() {
try {
const memberData = await memberstack.getMemberJSON();
console.log(`Member data: ${JSON.stringify(memberData)}`);
return memberData?.data?.updatesID || '';
} catch (error) {
console.error(`Error in getUpdatesIDFromJson function: ${error}`);
}
}
async function updateUpdatesIDInJson(newUpdatesID) {
try {
const memberData = await memberstack.getMemberJSON();
memberData.data = memberData.data || {};
memberData.data.updatesID = newUpdatesID;
console.log(`Updates ID in JSON after update: ${newUpdatesID}`);
await memberstack.updateMemberJSON({ json: memberData.data });
} catch (error) {
console.error(`Error in updateUpdatesIDInJson function: ${error}`);
}
}
async function checkAndUpdateUI() {
try {
const element = document.querySelector('[ms-code-update-item]');
const cmsItem = element.textContent;
console.log(`CMS item: ${cmsItem}`);
// Get the current updates ID from JSON
const updatesIDFromJson = await getUpdatesIDFromJson();
console.log(`Updates ID from JSON: ${updatesIDFromJson}`);
// Check displayForNewUsers variable to decide behavior
if (displayForNewUsers === 'NO' && !updatesIDFromJson) {
console.log('Updates ID from JSON is undefined, null, or empty, not changing UI');
return;
}
if (cmsItem !== updatesIDFromJson) {
const uiElements = document.querySelectorAll('[ms-code-update-ui]');
uiElements.forEach(uiElement => {
uiElement.style.display = 'block';
uiElement.style.opacity = '1';
});
}
// Update the updates ID in JSON after the UI has been updated
await updateUpdatesIDInJson(cmsItem);
} catch (error) {
console.error(`Error in checkAndUpdateUI function: ${error}`);
}
}
// Check and update UI when the page loads
checkAndUpdateUI().catch(error => {
console.error(`Error in initial functions: ${error}`);
});
});
</script>
#67 - Prefill Form Based On URL Parameters
Easily fill inputs using URL parameters.
<!-- 💙 MEMBERSCRIPT #67 v0.1 💙 PREFILL INPUTS WITH URL PARAMETERS -->
<script>
// Function to get URL parameters
function getURLParams() {
const urlParams = new URLSearchParams(window.location.search);
return Object.fromEntries(urlParams.entries());
}
// Function to prefill inputs based on URL parameters
function prefillInputs() {
const urlParams = getURLParams();
const inputElements = document.querySelectorAll('[ms-code-prefill-param]');
inputElements.forEach((inputElement) => {
const paramKey = inputElement.getAttribute('ms-code-prefill-param');
if (paramKey && urlParams[paramKey]) {
inputElement.value = urlParams[paramKey];
}
});
}
// Call the function to prefill inputs when the page loads
prefillInputs();
</script>
#66 - Member ID Invite Links
Create custom, unique invite/referral links.
<!-- 💙 MEMBERSCRIPT #66 v0.1 💙 MEMBER ID INVITE LINKS -->
<script>
// Function to get the member ID from local storage
function getMemberIDFromLocalStorage() {
// Assuming "_ms-mem" is the key that holds the member object in local storage
const memberObject = JSON.parse(localStorage.getItem("_ms-mem"));
if (memberObject && memberObject.id) {
return memberObject.id;
}
return null;
}
// Function to update the invite link with the member ID as a URL parameter
function updateInviteLink() {
const inviteLinkElement = document.querySelector('[ms-code-invite-link]');
if (inviteLinkElement) {
const inviteLinkBase = inviteLinkElement.getAttribute('ms-code-invite-link');
const memberID = getMemberIDFromLocalStorage();
if (memberID) {
const inviteLinkWithID = `${inviteLinkBase}?inviteCode=${memberID}`;
inviteLinkElement.textContent = inviteLinkWithID;
inviteLinkElement.href = inviteLinkWithID; // If it's an anchor link
}
}
}
// Call the function to update the invite link when the page loads
updateInviteLink();
</script>
#65 - Exit Intent Popup
Show visitors a popup when their mouse leaves to the top.
<!-- 💙 MEMBERSCRIPT #65 v0.1 💙 EXIT INTENT POPUP -->
<script>
const CookieService = {
setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = days ? '; expires=' + date.toUTCString() : '';
document.cookie = name + '=' + (value || '') + expires + ';';
},
getCookie(name) {
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith(name))
?.split('=')[1];
return cookieValue || null;
}
};
const exitPopup = document.querySelector('[ms-code-popup="exit-intent"]');
const mouseEvent = e => {
const shouldShowExitIntent =
!e.toElement &&
!e.relatedTarget &&
e.clientY < 10;
if (shouldShowExitIntent) {
document.removeEventListener('mouseout', mouseEvent);
exitPopup.style.display = 'flex';
CookieService.setCookie('exitIntentShown', true, 30);
}
};
if (!CookieService.getCookie('exitIntentShown')) {
document.addEventListener('mouseout', mouseEvent);
document.addEventListener('keydown', exit);
exitPopup.addEventListener('click', exit);
}
</script>
#64 - Radio Form Logic
Show set elements depending on which radio is selected.
<!-- 💙 MEMBERSCRIPT #64 v0.1 💙 RADIO FORM LOGIC -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
// initially hide all divs with 'ms-code-more-info' attribute
$("div[ms-code-more-info]").hide();
// listen for change events on all radios with 'ms-code-radio-option' attribute
$("input[ms-code-radio-option]").change(function() {
// hide all divs again
$("div[ms-code-more-info]").hide();
// get the value of the selected radio button
var selectedValue = $(this).attr("ms-code-radio-option");
// find the div with the 'ms-code-more-info' attribute that matches the selected value and show it
$("div[ms-code-more-info=" + selectedValue + "]").show();
});
});
</script>
#63 - Date Range Picker
Create a date range input in Webflow!
<!-- 💙 MEMBERSCRIPT #62 v0.1 💙 DATE RANGE PICKER -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
<style>
.daterangepicker td.active {
background-color: #006cfa !important ;
}
</style>
<script type="text/javascript">
$(function() {
$('input[ms-code-input="date-range"]').daterangepicker({
"opens": "center",
"locale": {
"format": "MM/DD/YYYY",
"separator": " - ",
"applyLabel": "Apply",
"cancelLabel": "Cancel",
"fromLabel": "From",
"toLabel": "To",
"customRangeLabel": "Custom",
"weekLabel": "W",
"daysOfWeek": [
"Su",
"Mo",
"Tu",
"We",
"Th",
"Fr",
"Sa"
],
"monthNames": [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
},
});
$('input[name="datefilter"]').on('apply.daterangepicker', function(ev, picker) {
$(this).val(picker.startDate.format('MM/DD/YYYY') + ' - ' + picker.endDate.format('MM/DD/YYYY'));
});
$('input[name="datefilter"]').on('cancel.daterangepicker', function(ev, picker) {
$(this).val('');
});
});
</script>
#62 - Upvote Button
Add upvote functionality to the Webflow CMS.
<!-- 💙 MEMBERSCRIPT #62 v0.2 💙 UPVOTE FORM -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const memberstack = window.$memberstackDom;
const upvoteButtons = document.querySelectorAll('[ms-code="upvote-button"]');
const upvoteForms = document.querySelectorAll('[ms-code="upvote-form"]');
const upvotedValues = document.querySelectorAll('[ms-code="upvoted-value"]');
const upvoteCounts = document.querySelectorAll('[ms-code="upvote-count"]');
let clickTimeout; // Variable to store the timer
let lastClickedButton = null; // Variable to store the last clicked button
// Function to handle upvote button click
function handleUpvoteButtonClick(event) {
event.preventDefault();
const button = event.currentTarget;
// Clear the timer if the same button is clicked
if (button === lastClickedButton) {
clearTimeout(clickTimeout);
}
lastClickedButton = button; // Store the reference to the currently clicked button
// Set a new timer
clickTimeout = setTimeout(function() {
const form = button.closest('form');
const cmsId = button.getAttribute('data-cms-id');
const upvotedValue = form.querySelector('[ms-code="upvoted-value"]');
const upvoteCount = form.querySelector('[ms-code="upvote-count"]');
if (button.classList.contains('is-true')) {
// Remove upvote
button.classList.remove('is-true');
upvotedValue.value = 'false';
upvoteCount.textContent = parseInt(upvoteCount.textContent) - 1;
memberstack.getMemberJSON()
.then(function(memberData) {
if (memberData.data && memberData.data.upvotes) {
const upvotes = memberData.data.upvotes;
const index = upvotes.indexOf(cmsId);
if (index !== -1) {
upvotes.splice(index, 1);
memberstack.updateMemberJSON({ json: memberData.data });
}
}
})
.catch(function(error) {
console.error('Error retrieving/updating member data:', error);
});
} else {
// Add upvote
button.classList.add('is-true');
upvotedValue.value = 'true';
upvoteCount.textContent = parseInt(upvoteCount.textContent) + 1;
memberstack.getMemberJSON()
.then(function(memberData) {
memberData.data = memberData.data || {};
memberData.data.upvotes = memberData.data.upvotes || [];
memberData.data.upvotes.push(cmsId);
memberstack.updateMemberJSON({ json: memberData.data });
})
.catch(function(error) {
console.error('Error retrieving/updating member data:', error);
});
}
// Make the API call
fetch(form.action, {
method: form.method,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(new FormData(form))
})
.then(function(response) {
if (response.ok) {
// Handle successful API response
return response.json();
} else {
// Handle API error
throw new Error('API Error');
}
})
.then(function(data) {
// Handle API response to update vote count
upvoteCount.textContent = data.upvoteCount; // Replace with the actual property holding the updated vote count
})
.catch(function(error) {
console.error('API Error:', error);
});
}, 200); // 0.2 seconds
}
// Attach event listeners to upvote buttons
upvoteButtons.forEach(function(button) {
button.addEventListener('click', handleUpvoteButtonClick);
});
// Check if member has upvotes on page load
memberstack.getMemberJSON()
.then(function(memberData) {
if (memberData.data && memberData.data.upvotes) {
const upvotes = memberData.data.upvotes;
upvoteButtons.forEach(function(button) {
const cmsId = button.getAttribute('data-cms-id');
if (upvotes.includes(cmsId)) {
button.classList.add('is-true');
const form = button.closest('form');
const upvotedValue = form.querySelector('[ms-code="upvoted-value"]');
upvotedValue.value = 'true';
}
});
}
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
});
</script>
#61 - Display Element If Checkbox Is Checked
Create conditional visibility based on a checkbox field.
<!-- 💙 MEMBERSCRIPT #61 v0.1 💙 SHOW ELEMENT IF CHECKBOX IS CHECKED -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script>
$(document).ready(function() {
// Initially hide all elements with the 'ms-code-checkbox-display' attribute
$("[ms-code-checkbox-display]").hide();
// When a checkbox with 'ms-code-checkbox-input' attribute is clicked, perform the following
$("[ms-code-checkbox-input]").click(function() {
// Get the value of the 'ms-code-checkbox-input' attribute
var checkboxVal = $(this).attr('ms-code-checkbox-input');
// Find the corresponding element with the 'ms-code-checkbox-display' attribute and same value
var displayElement = $("[ms-code-checkbox-display=" + checkboxVal + "]");
// If this checkbox is checked, show the corresponding element
if ($(this).is(":checked")) {
displayElement.show();
} else {
// If this checkbox is unchecked, hide the corresponding element
displayElement.hide();
}
});
});
</script>
#60 - Increase/Decrease Select Value
Create previous & next buttons for a select field.
<!-- 💙 MEMBERSCRIPT #60 v0.1 💙 INCREASE/DECREASE SELECT VALUE -->
<script>
var select = document.querySelector('[ms-code-select="input"]');
var prev = document.querySelector('[ms-code-select="prev"]');
var next = document.querySelector('[ms-code-select="next"]');
function updateButtons() {
prev.style.opacity = select.selectedIndex === 0 ? '0.5' : '1';
next.style.opacity = select.selectedIndex === select.options.length - 1 ? '0.5' : '1';
}
prev.addEventListener('click', function() {
if (select.selectedIndex > 0) {
select.selectedIndex--;
}
updateButtons();
});
next.addEventListener('click', function() {
if (select.selectedIndex < select.options.length - 1) {
select.selectedIndex++;
}
updateButtons();
});
updateButtons();
</script>
#59 - Restart GIF On Hover
Start a GIF from the beginning on hover.
<!-- 💙 MEMBERSCRIPT #59 v0.1 💙 RESTART GIF -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const hoverElements = document.querySelectorAll('[data-gif-hover]');
hoverElements.forEach((element) => {
element.addEventListener('mouseover', function() {
const gifNum = this.getAttribute('data-gif-hover');
const gifElement = document.querySelector(`[data-gif="${gifNum}"]`);
if (gifElement) {
const gifSrc = gifElement.getAttribute('src');
gifElement.setAttribute('src', '');
gifElement.setAttribute('src', gifSrc);
}
});
});
});
</script>
#58 - Price Range Slider Inputs
Create a price range input with individual inputs for min and max.
<!-- 💙 MEMBERSCRIPT #58 v0.1 💙 RANGE SLIDER INPUT -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/css/ion.rangeSlider.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/js/ion.rangeSlider.min.js"></script>
<style>
.irs {
font-family: inherit;
}
</style>
<script>
$(document).ready(function() {
var rangeSlider = $('[ms-code-input="range-slider"]');
var priceFromInput = $('[ms-code-input="price-from"]');
var priceToInput = $('[ms-code-input="price-to"]');
// Set the default range values
var defaultFrom = 20000;
var defaultTo = 50000;
rangeSlider.ionRangeSlider({
skin: "round", // You can also try "flat", "big", "modern", "sharp", or "square". Default styles can be overridden with CSS.
type: "double",
grid: true,
min: 0,
max: 100000,
from: defaultFrom,
to: defaultTo,
prefix: "$",
onStart: function(data) {
priceFromInput.val(data.from);
priceToInput.val(data.to);
},
onChange: function(data) {
priceFromInput.val(data.from);
priceToInput.val(data.to);
}
});
// Get the initial range values and update the inputs
var initialRange = rangeSlider.data("ionRangeSlider");
var initialData = initialRange.result;
priceFromInput.val(initialData.from);
priceToInput.val(initialData.to);
// Update the range slider and inputs when the inputs lose focus
priceFromInput.on("blur", function() {
var value = $(this).val();
var toValue = priceToInput.val();
// Perform validation
if (
value < initialRange.options.min ||
value > initialRange.options.max ||
value >= toValue ||
value > initialData.to // Check if fromValue is higher than the current toValue
) {
value = defaultFrom;
}
rangeSlider.data("ionRangeSlider").update({
from: value
});
priceFromInput.val(value);
});
priceToInput.on("blur", function() {
var value = $(this).val();
var fromValue = priceFromInput.val();
// Perform validation
if (
value < initialRange.options.min ||
value > initialRange.options.max ||
value <= fromValue ||
value < initialData.from // Check if toValue is lower than the current fromValue
) {
value = defaultTo;
}
rangeSlider.data("ionRangeSlider").update({
to: value
});
priceToInput.val(value);
});
});
</script>
#57 - Time Picker Input
Add a time picker to your form and prefill the time into a field.
<!-- 💙 MEMBERSCRIPT #57 v0.1 💙 TIME PICKER -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.css">
<script src="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.js"> </script>
<script>
$(document).ready(function() {
var tpSpinbox = new tui.TimePicker(document.querySelector('[ms-code-timepicker="box"]'), {
inputType: 'spinbox',
showMeridiem: true // If you don't use AM/PM remove this line
});
// Setup an event handler for when the time is selected
tpSpinbox.on('change', function() {
// Get the selected time
var hour = tpSpinbox.getHour();
var minute = tpSpinbox.getMinute();
var selectedTime = hour + ':' + (minute < 10 ? '0' : '') + minute;
// Update the value of the input element
document.querySelector('[ms-code-timepicker="input"]').value = selectedTime;
});
});
</script>
#56 - Input Option Pairs
Combine the values of multiple inputs into one field.
<!-- 💙 MEMBERSCRIPT #56 v0.1 💙 INPUT OPTION PAIRS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script>
$(document).ready(function() {
var groups = {};
// Get all inputs with the attribute ms-code-combine-inputs
var inputs = $('input[ms-code-combine-inputs], select[ms-code-combine-inputs]');
// For each input
inputs.each(function() {
// Split the attribute value at the dash
var parts = $(this).attr('ms-code-combine-inputs').split('-');
// If the group doesn't exist yet, create it
if (!groups[parts[0]]) {
groups[parts[0]] = {
targets: [],
values: [],
};
}
// If it's a target, add it to the targets
if (parts[1] == 'target') {
groups[parts[0]].targets.push($(this));
} else {
// It's an input, add it to the values and attach a listener
groups[parts[0]].values.push($(this));
$(this).on('input change', function() {
// On input or change, combine all values with a space in between
// and set the targets' value
var combinedValue = '';
$.each(groups[parts[0]].values, function(index, value) {
combinedValue += $(this).val();
if (index < groups[parts[0]].values.length - 1) {
combinedValue += ' '; // Add a space between values
}
});
$.each(groups[parts[0]].targets, function() {
$(this).val(combinedValue);
});
});
}
});
});
</script>
#55 - Change Checkbox Parent Styles
Change the styles of another element when a checkbox is checked.
<!-- 💙 MEMBERSCRIPT #55 v0.1 💙 UPDATE CHECKBOX PARENT STYLES -->
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
// Get all the checkbox elements
var checkboxes = document.querySelectorAll('[ms-code-checkbox="check"]');
// Iterate over each checkbox element
checkboxes.forEach(function(checkbox) {
// Get the boolean wrap element associated with the current checkbox
var booleanWrap = checkbox.closest('[ms-code-checkbox="wrap"]');
// Function to update the boolean wrap class based on checkbox state
function updateBooleanWrapClass() {
if (checkbox.checked) {
booleanWrap.classList.add('checked');
} else {
booleanWrap.classList.remove('checked');
}
}
// Check the initial value of the checkbox
updateBooleanWrapClass();
// Add an event listener to the checkbox to handle changes
checkbox.addEventListener('change', function() {
updateBooleanWrapClass();
});
});
});
</script>
#54 - Checkbox Form Field Logic
Block other fields/items if a checkbox is not checked.
<!-- 💙 MEMBERSCRIPT #54 v0.1 💙 CHECKBOX FIELD FORM LOGIC -->
<style>
.disabled {
pointer-events: none;
opacity: 0.5;
}
</style>
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
// Get all the trigger checkboxes
var triggerCheckboxes = document.querySelectorAll('[ms-code-field-logic-trigger]');
// Iterate over each trigger checkbox
triggerCheckboxes.forEach(function(checkbox) {
// Get the value of the trigger checkbox's attribute
var triggerValue = checkbox.getAttribute('ms-code-field-logic-trigger');
// Function to update the target elements' class based on checkbox state
function updateTargetElementsClass() {
// Find the associated target elements based on the attribute value
var targetElements = document.querySelectorAll('[ms-code-field-logic-target="' + triggerValue + '"]');
// Check the new value of the trigger checkbox
if (!checkbox.checked) {
// Add the "disabled" class to each target element
targetElements.forEach(function(targetElement) {
targetElement.classList.add('disabled');
});
} else {
// Remove the "disabled" class from each target element
targetElements.forEach(function(targetElement) {
targetElement.classList.remove('disabled');
});
}
}
// Check the initial value of the trigger checkbox
updateTargetElementsClass();
// Add an event listener to the trigger checkbox to handle changes
checkbox.addEventListener('change', function() {
updateTargetElementsClass();
});
});
});
</script>
#53 - Update JSON Items With A Form
Allow your members to change details about their JSON items.
<!-- 💙 MEMBERSCRIPT #53 v0.1 💙 UPDATE JSON ITEMS WITH A FORM -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
// Add click event listener to the document
document.addEventListener("click", async function(event) {
const target = event.target;
// Check if the clicked element has ms-code-edit-item attribute
const editItem = target.closest('[ms-code-edit-item="prompt"]');
if (editItem) {
// Get the item key from the closest ancestor element with ms-code-item-key attribute
const key = editItem.closest('[ms-code-item-key]').getAttribute('ms-code-item-key');
// Retrieve the current member JSON data
const member = await memberstack.getMemberJSON();
// SET THE TARGET - EDIT ME
let targetObject = member.data.projects; // Update this line with the desired target location
if (member.data && targetObject && targetObject[key]) {
// Get the form element with the ms-code-edit-item="form" attribute
const form = document.querySelector('form[ms-code-edit-item="form"]');
if (form) {
// Loop through the form fields
for (const field of form.elements) {
const jsonName = field.getAttribute('ms-code-json-name');
if (jsonName && targetObject[key].hasOwnProperty(jsonName)) {
// Pre-fill the form field with the corresponding value from the JSON item
field.value = targetObject[key][jsonName];
}
}
// Get the modal element with the ms-code-edit-item="modal" attribute
const modal = document.querySelector('[ms-code-edit-item="modal"]');
if (modal) {
// Set the display property of the modal to flex
modal.style.display = 'flex';
}
// Add submit event listener to the form
form.addEventListener("submit", async function(event) {
event.preventDefault(); // Prevent the form from submitting normally
// Create an object to hold the updated values
const updatedValues = {};
// Loop through the form fields
for (const field of form.elements) {
const jsonName = field.getAttribute('ms-code-json-name');
if (jsonName) {
// Update the corresponding value in the updatedValues object
updatedValues[jsonName] = field.value;
}
}
// Update the target object with the new values
targetObject[key] = { ...targetObject[key], ...updatedValues };
// Update the member JSON using the Memberstack SDK
await memberstack.updateMemberJSON({
json: member.data
});
// Optional: Display a success message or perform any other desired action
console.log('Member JSON updated successfully');
});
} else {
console.error('Form element not found');
}
} else {
console.error(`Could not find item with key: ${key}`);
}
}
});
});
</script>
#52 - Delayed Page Redirect
Redirect members to a new page with an optional delay.
<!-- 💙 MEMBERSCRIPT #52 v0.1 💙 DELAYED REDIRECT TO NEW PAGE -->
<script>
const redirectToNewPage = function() {
setTimeout(function() {
window.location.href = "/your-page";
}, 1000); // Delay in milliseconds
};
redirectToNewPage();
</script>
#51 - Display Member Metadata
Display member metadata dynamically on your website.
<!-- 💙 MEMBERSCRIPT #51 v0.2 💙 DISPLAY MEMBER METADATA -->
<script>
function replaceTextWithMetadata(metadata) {
var els = Array.from(document.querySelectorAll('[ms-code-member-meta]'));
els.forEach((el) => {
const key = el.getAttribute('ms-code-member-meta');
const value = metadata[key];
if (value !== undefined) {
el.innerHTML = value;
el.value = value;
el.src = value;
}
});
}
const memberstack = window.$memberstackDom;
memberstack.getCurrentMember()
.then(({ data: member }) => {
if (member && member.metaData) {
replaceTextWithMetadata(member.metaData);
}
})
.catch((error) => {
console.error('Error retrieving member data:', error);
});
</script>
#50 - Cross-Device Dark Mode
Persistent dark mode option which stays working on your members' different devices.
Head Code
Put this in the <head> section of your site.
<!-- 💙 MEMBERSCRIPT #50 HEAD CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const themePreference = localStorage.getItem('themePreference');
if (themePreference === 'dark') {
document.body.classList.add('dark');
}
});
</script>
Body Code
Put this in the </body> section of your site.
<!-- 💙 MEMBERSCRIPT #50 BODY CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const darkModeToggle = document.querySelector('[ms-code-dark-mode="toggle"]');
const bodyElement = document.querySelector('.body');
// Function to check if theme preference is saved in member JSON
function checkThemePreference() {
const memberstack = window.$memberstackDom;
memberstack.getMemberJSON()
.then(function(memberData) {
const themePreference = memberData.data?.themePreference;
if (themePreference === 'dark') {
enableDarkMode();
} else {
disableDarkMode();
}
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
}
// Function to enable dark mode
function enableDarkMode() {
darkModeToggle.classList.add('dark');
bodyElement.classList.add('dark');
updateThemePreference('dark');
}
// Function to disable dark mode
function disableDarkMode() {
darkModeToggle.classList.remove('dark');
bodyElement.classList.remove('dark');
updateThemePreference('light');
}
// Function to update theme preference in member JSON
function updateThemePreference(themePreference) {
const memberstack = window.$memberstackDom;
memberstack.getMemberJSON()
.then(function(memberData) {
memberData.data = memberData.data || {};
memberData.data.themePreference = themePreference;
memberstack.updateMemberJSON({ json: memberData.data })
.then(function() {
localStorage.setItem('themePreference', themePreference);
})
.catch(function(error) {
console.error('Error updating member data:', error);
});
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
}
// Event listener for dark mode toggle
darkModeToggle.addEventListener('click', function() {
if (darkModeToggle.classList.contains('dark')) {
disableDarkMode();
} else {
enableDarkMode();
}
});
// Apply transition duration and timing to all elements
const transitionDuration = '0.0s';
const transitionTiming = 'ease';
const elementsToTransition = [darkModeToggle, bodyElement];
elementsToTransition.forEach(function(element) {
element.style.transitionDuration = transitionDuration;
element.style.transitionTimingFunction = transitionTiming;
});
// Check theme preference on page load
const savedThemePreference = localStorage.getItem('themePreference');
if (savedThemePreference === 'dark') {
enableDarkMode();
} else {
disableDarkMode();
}
checkThemePreference();
});
</script>
#49 - Disable First Option in a Select Input
Prevent users from selecting the placeholder option in your select inputs.
<!-- 💙 MEMBERSCRIPT #49 v0.1 💙 DISABLE FIRST OPTION IN SELECT INPUT -->
<script>
let selects = document.querySelectorAll("select[ms-code=hide-first-option]");
selects.forEach((select) => {
let options = select.getElementsByTagName("option");
options[0].hidden = true;
});
</script>
#48 - Autocomplete Address Inputs
Prefill all address inputs using the Google Places API!
Head Code
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #48 HEAD CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR-API-KEY&libraries=places&callback=initAutocomplete" async defer> </script>
<style>
.pac-logo::after {
display: none;
}
.pac-container {
border-radius: 5px;
border: 1px solid #ccc;
}
.pac-item {
padding: 0 10px;
}
</style>
Body Code
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #48 BODY CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script>
let autocomplete;
function initAutocomplete() {
autocomplete = new google.maps.places.Autocomplete(
document.querySelector('input[ms-code-input="address"]'),
{
componentRestrictions: { country: ['US'] },
fields: ['address_components'],
types: ['address']
}
);
autocomplete.addListener('place_changed', function() {
const place = autocomplete.getPlace();
if (place) {
const addressInput = document.querySelector('input[ms-code-input="address"]');
const cityInput = document.querySelector('input[ms-code-input="city"]');
const regionInput = document.querySelector('input[ms-code-input="region"]');
const countryInput = document.querySelector('input[ms-code-input="country"]');
const postalCodeInput = document.querySelector('input[ms-code-input="postal-code"]');
addressInput.value = extractAddress(place);
cityInput.value = extractCity(place);
regionInput.value = extractRegion(place);
countryInput.value = extractCountry(place);
postalCodeInput.value = extractPostalCode(place);
}
});
}
function extractAddress(place) {
let address = '';
const streetNumber = extractComponent(place, 'street_number');
const route = extractComponent(place, 'route');
if (streetNumber) {
address += streetNumber + ' ';
}
if (route) {
address += route;
}
return address.trim();
}
function extractComponent(place, componentType) {
for (const component of place.address_components) {
if (component.types.includes(componentType)) {
return component.long_name;
}
}
return '';
}
function extractCity(place) {
for (const component of place.address_components) {
if (component.types.includes('locality')) {
return component.long_name;
}
}
return '';
}
function extractRegion(place) {
for (const component of place.address_components) {
if (component.types.includes('administrative_area_level_1')) {
return component.long_name;
}
}
return '';
}
function extractCountry(place) {
for (const component of place.address_components) {
if (component.types.includes('country')) {
return component.long_name;
}
}
return '';
}
function extractPostalCode(place) {
for (const component of place.address_components) {
if (component.types.includes('postal_code')) {
return component.long_name;
}
}
return '';
}
</script>
#47 - Display Date From Member JSON
Show members a date - for example, when their plan will expire!
<!-- 💙 MEMBERSCRIPT #47 v0.1 💙 DISPLAY ONE TIME DATE -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
const memberstack = window.$memberstackDom;
const formatDate = function(date) {
const options = { month: 'long', day: 'numeric', year: 'numeric' };
return new Date(date).toLocaleDateString('en-US', options);
// Replace 'en-US' with one of these depending on your locale: en-US, en-GB, en-CA, en-AU, fr-FR, de-DE, es-ES, it-IT, ja-JP, ko-KR, pt-BR, ru-RU, zn-CH, ar-SA
};
const updateTextSpans = async function() {
const member = await memberstack.getMemberJSON();
if (!member.data || !member.data['one-time-date']) {
// Member data or one-time date not available, do nothing
return;
}
const oneTimeDate = formatDate(member.data['one-time-date']);
const textSpans = document.querySelectorAll('[ms-code-display-text="one-time-date"]');
textSpans.forEach(span => {
span.textContent = oneTimeDate;
});
};
updateTextSpans();
});
</script>
#46 - Confirm Password
Add a confirm password input to your signup & password reset forms.
<!-- 💙 MEMBERSCRIPT #46 v0.1 💙 CONFIRM PASSWORD INPUT -->
<script>
var password = document.querySelector('[data-ms-member=password]')
, confirm_password = document.querySelector('[ms-code-password=confirm]')
function validatePassword(){
if(password.value != confirm_password.value) {
confirm_password.setCustomValidity("Passwords Don't Match");
confirm_password.classList.add("invalid")
confirm_password.classList.remove("valid")
} else {
confirm_password.setCustomValidity('');
confirm_password.classList.remove("invalid")
confirm_password.classList.add("valid")
}
}
password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
</script>
#45 - Show/Hide Password
Add a show/hide password button to any form with a password input.
<!-- 💙 MEMBERSCRIPT #45 v0.2 💙 SHOW AND HIDE PASSWORD -->
<script>
document.querySelectorAll("[ms-code-password='transform']").forEach(function(button) {
button.addEventListener("click", transform);
});
var isPassword = true;
function transform() {
var passwordInputs = document.querySelectorAll("[data-ms-member='password'], [data-ms-member='new-password'], [data-ms-member='current-password']");
passwordInputs.forEach(function(myInput) {
var inputType = myInput.getAttribute("type");
if (isPassword) {
myInput.setAttribute("type", "text");
} else {
myInput.setAttribute("type", "password");
}
});
isPassword = !isPassword;
}
</script>
#44 - Show Element If Attribute Matches Member ID
Conditionally show elements if they have an attribute matching the members' ID.
<!-- 💙 MEMBERSCRIPT #44 v0.1 💙 SHOW ELEMENT IF ATTRIBUTE MATCHES MEMBER ID -->
<script>
document.addEventListener("DOMContentLoaded", function() {
if (localStorage.getItem("_ms-mem")) {
const memberData = JSON.parse(localStorage.getItem("_ms-mem"));
const memberId = memberData.id;
const elements = document.querySelectorAll("[ms-code-member-id='" + memberId + "']");
elements.forEach(element => {
element.style.display = "block";
});
}
});
</script>
#43 - Block Scrolling When Modal Is Open
Stop the page from scrolling when someone opens a modal.
<!-- 💙 MEMBERSCRIPT #43 v0.1 💙 BLOCK SCROLLING WHEN MODAL IS OPEN -->
<style>
.no-scroll {
overflow: hidden;
}
</style>
<script>
function isDesktopViewport() {
return window.innerWidth >= 900; // Adjust the breakpoint width as needed
}
const codeBlocks = document.querySelectorAll('[ms-code-block-scroll]');
function handleScrollBlock(event) {
if (isDesktopViewport()) {
document.body.classList.add('no-scroll');
}
}
function handleScrollUnblock(event) {
if (isDesktopViewport()) {
document.body.classList.remove('no-scroll');
}
}
codeBlocks.forEach(codeBlock => {
codeBlock.addEventListener('mouseenter', handleScrollBlock);
codeBlock.addEventListener('mouseleave', handleScrollUnblock);
});
</script>
#42 - Image Editor Form Field
Allow people to upload & edit photos, then send them to Google Drive!
Head Code
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #42 HEAD CODE v0.2 💙 FILE EDITOR FEATURE -->
<link rel="stylesheet" href="https://unpkg.com/filepond@^4/dist/filepond.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" />
Body Code
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #42 BODY CODE v0.2 💙 FILE EDITOR FEATURE -->
<script> src="https://unpkg.com/filepond-plugin-file-encode/dist/filepond-plugin-file-encode.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.js"> </script>
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script> src="https://scaleflex.cloudimg.io/v7/plugins/filerobot-image-editor/latest/filerobot-image-editor.min.js"> </script>
<style>
.dXhZSB {
background-color: #2962ff;
}
.FIE_root * {
font-family: inherit !important;
}
.SfxModal-Wrapper * {
font-family: inherit !important;
}
.jpHEiD {
font-family: inherit !important;
}
#editor_container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Register the plugins
FilePond.registerPlugin(FilePondPluginImagePreview);
FilePond.registerPlugin(FilePondPluginImageEdit);
const inputElement = document.querySelector('input[type="file"]');
const pond = FilePond.create(inputElement, {
credits: false,
name: 'fileToUpload',
storeAsFile: true,
imageEditEditor: {
open: (file, instructions) => {
console.log('Open editor', file, instructions);
openFilerobotImageEditor(file, instructions);
},
onconfirm: (output) => {
console.log('Confirm editor', output);
handleImageEditConfirm(output);
},
oncancel: () => {
console.log('Cancel editor');
handleImageEditCancel();
},
onclose: () => {
console.log('Close editor');
handleImageEditClose();
}
}
});
function openFilerobotImageEditor(file, instructions) {
const imageURL = URL.createObjectURL(file);
const config = {
source: imageURL,
onSave: (updatedImage) => {
confirmCallback(updatedImage);
},
annotationsCommon: {
fill: '#ff0000'
},
Text: {
text: 'Add your text here',
font: 'inherit'
}, // Set font to inherit from the page body
Rotate: {
angle: instructions.rotation,
componentType: 'slider'
},
tabsIds: [
'Adjust',
'Annotate',
'Watermark'
],
defaultTabId: 'Annotate',
defaultToolId: 'Text'
};
const editorContainer = document.createElement('div');
editorContainer.id = 'editor_container';
document.body.appendChild(editorContainer);
const filerobotImageEditor = new window.FilerobotImageEditor(editorContainer, config);
const confirmCallback = (output) => {
console.log('Confirmed:', output);
const dataURL = output.imageBase64;
const file = dataURLToFile(dataURL, output.name);
// Add the file to FilePond
pond.addFiles([file]);
document.body.removeChild(editorContainer); // Remove the editor container
};
function dataURLToFile(dataURL, fileName) {
const arr = dataURL.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const fileExtension = mime.split('/')[1];
const updatedFileName = fileName + '.' + fileExtension;
const bstr = atob(arr[1]);
const n = bstr.length;
const u8arr = new Uint8Array(n);
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i);
}
return new File([u8arr], updatedFileName, { type: mime });
}
const cancelCallback = () => {
console.log('Canceled');
document.body.removeChild(editorContainer); // Remove the editor container
};
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.addEventListener('click', () => {
filerobotImageEditor.onClose();
});
const buttonContainer = document.createElement('div');
buttonContainer.appendChild(closeButton);
editorContainer.appendChild(buttonContainer);
filerobotImageEditor.render({
onClose: (closingReason) => {
console.log('Closing reason', closingReason);
filerobotImageEditor.terminate();
},
});
}
function handleImageEditConfirm(output) {
console.log('Image edit confirmed:', output);
// Handle the confirmed output here
}
function handleImageEditCancel() {
console.log('Image edit canceled');
// Handle the canceled edit here
}
function handleImageEditClose() {
console.log('Image editor closed');
// Handle the editor close here
}
});
</script>
#41 - Perfect Phone Number Inputs
International phone number inputs, the way they should be.
With IP Lookup
Use this if you want the users' IP country to automatically be prefilled. IMPORTANT: Do not use this with profile forms or it will behave erratically.
<!-- 💙 MEMBERSCRIPT #41 v0.2 💙 PERFECT PHONE NUMBER INPUTS (WITH IP LOOKUP) -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"> </script>
<script>
$(document).ready(function() {
$('input[ms-code-phone-number]').each(function() {
var input = this;
var preferredCountries = $(input).attr('ms-code-phone-number').split(',');
var iti = window.intlTelInput(input, {
preferredCountries: preferredCountries,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"
});
$.get("https://ipinfo.io", function(response) {
var countryCode = response.country;
iti.setCountry(countryCode);
}, "jsonp");
input.addEventListener('change', formatPhoneNumber);
input.addEventListener('keyup', formatPhoneNumber);
function formatPhoneNumber() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
}
var form = $(input).closest('form');
form.submit(function() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
});
});
});
</script>
Without IP Lookup
Use this on profile forms and/or if you do not want to automatically prefill based on user IP.
<!-- 💙 MEMBERSCRIPT #41 v0.2 💙 PERFECT PHONE NUMBER INPUTS (WITHOUT IP LOOKUP) -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"> </script>
<script>
$(document).ready(function() {
$('input[ms-code-phone-number]').each(function() {
var input = this;
var preferredCountries = $(input).attr('ms-code-phone-number').split(',');
var iti = window.intlTelInput(input, {
preferredCountries: preferredCountries,
utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"
});
input.addEventListener('change', formatPhoneNumber);
input.addEventListener('keyup', formatPhoneNumber);
function formatPhoneNumber() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
}
var form = $(input).closest('form');
form.submit(function() {
var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
input.value = formattedNumber;
});
});
});
</script>
#40 - Drag And Drop File Uploader
Easily add a drag n' drop file upload feature to your Webflow site!
Important
If you are using MemberScript #38, make sure you put this script AFTER!
<!-- 💙 MEMBERSCRIPT #40 v0.1 💙 DRAG AND DROP FILE UPLOADER -->
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const inputElement = document.querySelector('input[type="file"]');
const pond = FilePond.create(inputElement, {
credits: false,
name: 'fileToUpload',
storeAsFile: true
// for more property options, go to https://pqina.nl/filepond/docs/api/instance/properties/
});
});
</script>
#39 - Better Select Fields
Add searches and a better UI to select & multi-select fields!
Head Code
Put this in the <head> section of your page.
<!-- 💙 MEMBERSCRIPT #39 v0.1 HEAD CODE 💙 BETTER SELECT FIELDS -->
<script> src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"> </script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" />
Body Code
Put this in the </body> section of your page.
<!-- 💙 MEMBERSCRIPT #39 v0.1 BODY CODE 💙 BETTER SELECT FIELDS -->
<script> src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"> </script>
<script>
$(document).ready(function() {
$('[ms-code-custom-select="select-with-search"]').select2();
});
</script>
#38 - File Upload Field
Add a file uploader to any site and send the submission to Google Drive, email, or anywhere you want.
<!-- 💙 MEMBERSCRIPT #38 v0.1 💙 FORM FILE UPLOADER -->
<script>
const forms = document.querySelectorAll('form[ms-code-file-upload="form"]');
forms.forEach((form) => {
form.setAttribute('enctype', 'multipart/form-data');
const uploadInputs = form.querySelectorAll('[ms-code-file-upload-input]');
uploadInputs.forEach((uploadInput) => {
const inputName = uploadInput.getAttribute('ms-code-file-upload-input');
const fileInput = document.createElement('input');
fileInput.setAttribute('type', 'file');
fileInput.setAttribute('name', inputName);
fileInput.setAttribute('id', inputName);
fileInput.required = true; // delete this line to make the input optional
uploadInput.appendChild(fileInput);
});
});
</script>
#37 - Automatically Remove Free Plan
Automatically remove a free plan after a set amount of time!
<!-- 💙 MEMBERSCRIPT #37 v0.1 💙 MAKE FREE TRIAL EXPIRE AFTER SET TIME -->
<script>
let memberPlanId = "your_plan_ID"; // replace with your actual FREE plan ID
document.addEventListener("DOMContentLoaded", async function() {
const memberstack = window.$memberstackDom;
// Fetch the member's data
const member = await memberstack.getMemberJSON();
// Fetch the member's planConnections from local storage
const memberDataFromLocalStorage = JSON.parse(localStorage.getItem('_ms-mem'));
const planConnections = memberDataFromLocalStorage.planConnections;
// Check if the member has x plan
let hasPlan = false;
if (planConnections) {
hasPlan = planConnections.some(planConnection => planConnection.planId === memberPlanId);
}
if (hasPlan) {
// Check the members one-time-date
let currentDate = new Date();
let oneTimeDate = new Date(member.data['one-time-date']);
if (currentDate > oneTimeDate) {
// If the members' one time date has passed, remove x plan
memberstack.removePlan({
planId: memberPlanId
}).then(() => {
// Redirect to /free-trial-expired
window.location.href = "/free-trial-expired";
}).catch(error => {
// Handle error
});
}
}
});
</script>
#36 - Password Validation
Use this simple method to confirm your members have entered a strong password.
<!-- 💙 MEMBERSCRIPT #36 v0.1 💙 PASSWORD VALIDATION -->
<script>
window.addEventListener('load', function() {
const passwordInput = document.querySelector('input[data-ms-member="password"]');
const submitButton = document.querySelector('[ms-code-submit-button]');
if (!passwordInput || !submitButton) return; // Return if essential elements are not found
function checkAllValid() {
const validationPoints = document.querySelectorAll('[ms-code-pw-validation]');
return Array.from(validationPoints).every(validationPoint => {
const validIcon = validationPoint.querySelector('[ms-code-pw-validation-icon="true"]');
return validIcon && validIcon.style.display === 'flex'; // Check for validIcon existence before accessing style
});
}
passwordInput.addEventListener('keyup', function() {
const password = passwordInput.value;
const validationPoints = document.querySelectorAll('[ms-code-pw-validation]');
validationPoints.forEach(function(validationPoint) {
const rule = validationPoint.getAttribute('ms-code-pw-validation');
let isValid = false;
// MINIMUM LENGTH VALIDATION POINT
if (rule.startsWith('minlength-')) {
const minLength = parseInt(rule.split('-')[1]);
isValid = password.length >= minLength;
}
// SPECIAL CHARACTER VALIDATION POINT
else if (rule === 'special-character') {
isValid = /[!@#$%^&*(),.?":{}|<>]/g.test(password);
}
// UPPER AND LOWER CASE VALIDATION POINT
else if (rule === 'upper-lower-case') {
isValid = /[a-z]/.test(password) && /[A-Z]/.test(password);
}
// NUMBER VALIDATION POINT
else if (rule === 'number') {
isValid = /\d/.test(password);
}
const validIcon = validationPoint.querySelector('[ms-code-pw-validation-icon="true"]');
const invalidIcon = validationPoint.querySelector('[ms-code-pw-validation-icon="false"]');
if (validIcon && invalidIcon) { // Check for existence before accessing style
if (isValid) {
validIcon.style.display = 'flex';
invalidIcon.style.display = 'none';
} else {
validIcon.style.display = 'none';
invalidIcon.style.display = 'flex';
}
}
});
if (checkAllValid()) {
submitButton.classList.remove('disabled');
} else {
submitButton.classList.add('disabled');
}
});
// Trigger keyup event after adding event listener
var event = new Event('keyup');
passwordInput.dispatchEvent(event);
});
</script>
#35 - Easily Add FAQ Schema/Rich Snippets
Add one script and 2 attributes to enable constantly updating rich snippets on your page.
<!-- 💙 MEMBERSCRIPT #35 v0.1 💙 FAQ RICH SNIPPETS -->
<script>
let faqArray = [];
let questionElements = document.querySelectorAll('[ms-code-snippet-q]');
let answerElements = document.querySelectorAll('[ms-code-snippet-a]');
for (let i = 0; i < questionElements.length; i++) {
let question = questionElements[i].innerText;
let answer = '';
for (let j = 0; j < answerElements.length; j++) {
if (questionElements[i].getAttribute('ms-code-snippet-q') === answerElements[j].getAttribute('ms-code-snippet-a')) {
answer = answerElements[j].innerText;
break;
}
}
faqArray.push({
"@type": "Question",
"name": question,
"acceptedAnswer": {
"@type": "Answer",
"text": answer
}
});
}
let faqSchema = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faqArray
}
let script = document.createElement('script');
script.type = "application/ld+json";
script.innerHTML = JSON.stringify(faqSchema);
document.getElementsByTagName('head')[0].appendChild(script);
</script>
#34 - Require Business Email For Form Submission
Block people from submitting a form if their email uses a personal email such as gmail.
<!-- 💙 MEMBERSCRIPT #34 v0.1 💙 REQUIRE BUSINESS EMAILS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/parsley.js/2.9.2/parsley.min.js"> </script>
<script>
function isPersonalEmail(email) {
var personalDomains = [
"gmail.com",
"yahoo.com",
"hotmail.com",
"aol.com",
"msn.com",
"comcast.net",
"live.com",
"outlook.com",
"ymail.com",
"icloud.com"
];
var emailDomain = email.split('@')[1];
return personalDomains.includes(emailDomain);
}
window.Parsley.addValidator('businessEmail', {
validateString: function(value) {
return !isPersonalEmail(value);
},
messages: {
en: 'Please enter a business email.'
}
});
$(document).ready(function() {
$('form[ms-code-validate-form]').attr('data-parsley-validate', '');
$('input[ms-code-business-email]').attr('data-parsley-business-email', '');
$('form').parsley();
});
$('form').parsley().on('form:error', function() {
$('.parsley-errors-list').addClass('ms-code-validation-error');
});
</script>
#33 - Automatically Format Form Inputs
Force form inputs to follow a set format, such as DD/MM/YYYY.
<!-- 💙 MEMBERSCRIPT #33 v0.2 💙 AUTOMATICALLY FORMAT FORM INPUTS -->
<script src="https://cdn.jsdelivr.net/npm/cleave.js@1.6.0"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cleave.js/1.6.0/addons/cleave-phone.us.js"> </script>
<script>
document.addEventListener('DOMContentLoaded', function(){
// SELECT ALL ELEMENTS WITH THE ATTRIBUTE "ms-code-autoformat" OR "ms-code-autoformat-prefix"
const elements = document.querySelectorAll('[ms-code-autoformat], [ms-code-autoformat-prefix]');
for (let element of elements) {
const formatType = element.getAttribute('ms-code-autoformat');
const prefix = element.getAttribute('ms-code-autoformat-prefix');
// SET PREFIX
let cleaveOptions = {
prefix: prefix || '',
blocks: [Infinity]
};
// BASED ON THE VALUE OF "ms-code-autoformat", FORMAT THE INPUT
if (formatType) {
switch (formatType) {
// FORMAT PHONE NUMBERS
case 'phone-number':
cleaveOptions.phone = true;
cleaveOptions.phoneRegionCode = 'US';
break;
// FORMAT DATES IN 'YYYY-MM-DD' FORMAT
case 'date-yyyy-mm-dd':
cleaveOptions.date = true;
cleaveOptions.datePattern = ['Y', 'm', 'd'];
break;
// FORMAT DATES IN 'MM-DD-YYYY' FORMAT
case 'date-mm-dd-yyyy':
cleaveOptions.date = true;
cleaveOptions.datePattern = ['m', 'd', 'Y'];
break;
// FORMAT DATES IN 'DD-MM-YYYY' FORMAT
case 'date-dd-mm-yyyy':
cleaveOptions.date = true;
cleaveOptions.datePattern = ['d', 'm', 'Y'];
break;
// FORMAT TIMES IN 'HH-MM-SS' FORMAT
case 'time-hh-mm-ss':
cleaveOptions.time = true;
cleaveOptions.timePattern = ['h', 'm', 's'];
break;
// FORMAT TIMES IN 'HH-MM' FORMAT
case 'time-hh-mm':
cleaveOptions.time = true;
cleaveOptions.timePattern = ['h', 'm'];
break;
// FORMAT NUMBERS WITH THOUSANDS SEPARATORS
case 'number-thousand':
cleaveOptions.numeral = true;
cleaveOptions.numeralThousandsGroupStyle = 'thousand';
break;
}
}
new Cleave(element, cleaveOptions);
}
});
</script>
#32 - Set Input to Required if Visible
Create conditional forms by showing and hiding required inputs.
<!-- 💙 MEMBERSCRIPT #32 v0.1 💙 REQUIRE INPUT IF VISIBLE -->
<script>
document.addEventListener("DOMContentLoaded", function() {
// Function to check if an element is visible
function isElementVisible(element) {
return element.offsetParent !== null;
}
// Every time the user clicks on the document
document.addEventListener('click', function() {
// Get all inputs with the ms-code attribute
const inputs = document.querySelectorAll('[ms-code="required-if-visible"]');
// Loop through each input
inputs.forEach(function(input) {
// Check if the input or its parent is visible
if (isElementVisible(input)) {
// If the input is visible, add the required attribute
input.required = true;
} else {
// If the input is not visible, remove the required attribute
input.required = false;
}
});
});
});
</script>
#31 - Open a Webflow Tab with a Link
This script automatically generates links to your Webflow tabs.
<!-- 💙 MEMBERSCRIPT #31 v0.2 💙 OPEN WEBFLOW TAB w/ LINK -->
<!-- You can link to tabs like this 👉 www.yoursite.com#tab-name-lowercase -->
<!-- And sub tabs like this 👉 www.yoursite.com#tab-name/sub-tab-name -->
<script>
var Webflow = Webflow || [];
Webflow.push(() => {
function changeTab(shouldScroll = false) {
const hashSegments = window.location.hash.substring(1).split('/');
const offset = 90; // change this to match your fixed header height if you have one
let lastTabTarget;
for (const segment of hashSegments) {
const tabTarget = document.querySelector(`[data-w-tab="${segment}"]`);
if (tabTarget) {
tabTarget.click();
lastTabTarget = tabTarget;
}
}
if (shouldScroll && lastTabTarget) {
window.scrollTo({
top: $(lastTabTarget).offset().top - offset, behavior: 'smooth'
});
}
}
const tabs = document.querySelectorAll('[data-w-tab]');
tabs.forEach(tab => {
const dataWTabValue = tab.dataset.wTab;
const parsedDataTab = dataWTabValue.replace(/\s+/g,"-").toLowerCase();
tab.dataset.wTab = parsedDataTab;
tab.addEventListener('click', () => {
history.pushState({}, '', `#${parsedDataTab}`);
});
});
if (window.location.hash) {
requestAnimationFrame(() => { changeTab(true); });
}
window.addEventListener('hashchange', () => { changeTab() });
});
</script>
#30 - Count Items On Page And Update Number
Check how many items with a set attribute are on the page and apply that number to some text.
<!-- 💙 MEMBERSCRIPT #30 v0.1 💙 COUNT ITEMS AND DISPLAY COUNT -->
<script>
document.addEventListener("DOMContentLoaded", function() {
setTimeout(function() {
const rollupItems = document.querySelectorAll('[ms-code-rollup-item]');
const rollupNumbers = document.querySelectorAll('[ms-code-rollup-number]');
const updateRollupNumbers = function() {
const rollupCountMap = new Map();
rollupItems.forEach(item => {
const rollupKey = item.getAttribute('ms-code-rollup-item');
const count = rollupCountMap.get(rollupKey) || 0;
rollupCountMap.set(rollupKey, count + 1);
});
rollupNumbers.forEach(number => {
const rollupKey = number.getAttribute('ms-code-rollup-number');
const count = rollupCountMap.get(rollupKey) || 0;
number.textContent = count;
});
};
updateRollupNumbers(); // Initial update
// Polling function to periodically update rollup numbers
const pollRollupNumbers = function() {
updateRollupNumbers();
setTimeout(pollRollupNumbers, 1000); // Adjust the polling interval as needed (in milliseconds)
};
pollRollupNumbers(); // Start polling
}, 2000);
});
</script>
#29 - Temporarily Fix Element Height On Load
Force an element to be a set height for a certain duration on page load.
<!-- 💙 MEMBERSCRIPT #29 v0.1 💙 TEMPORARILY FIX ELEMENT HEIGHT -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const elements = document.querySelectorAll('[ms-code-temp-height]');
elements.forEach(element => {
const attributeValue = element.getAttribute('ms-code-temp-height');
if (attributeValue) {
const [time, height] = attributeValue.split(':');
if (!isNaN(time) && !isNaN(height)) {
const defaultHeight = element.style.height;
setTimeout(() => {
element.style.height = defaultHeight;
}, parseInt(time));
element.style.height = height + 'px';
}
}
});
});
</script>
#28 - Display Element Based On JSON Date Passing
Check the one time date from #27 and show/hide an element based on that.
<!-- 💙 MEMBERSCRIPT #28 v0.1 💙 CHECK ONE-TIME DATE AND UPDATE ELEMENT DISPLAY -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
const memberstack = window.$memberstackDom;
const updateElementVisibility = async function() {
const member = await memberstack.getMemberJSON();
if (!member.data || !member.data['one-time-date']) {
// Member data or expiration date not available, do nothing
return;
}
const expirationDate = new Date(member.data['one-time-date']);
const currentDate = new Date();
if (currentDate < expirationDate) {
// Expiration date has not passed, update element visibility
const elements = document.querySelectorAll('[ms-code-element-temporary]');
elements.forEach(element => {
const displayValue = element.getAttribute('ms-code-element-temporary');
// Update element visibility based on the attribute value
element.style.display = displayValue;
});
}
};
updateElementVisibility();
});
</script>
#27 - Set One Time Date On Signup
Apply a date to your member JSON after signup which can be used for anything.
JSON Only
If you don't need to add the date to a custom field too, use this.
<!-- 💙 MEMBERSCRIPT #27 v0.1 💙 SET ONE TIME DATE -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
const memberstack = window.$memberstackDom;
const updateDate = async function() {
const member = await memberstack.getMemberJSON();
if (!member.data) {
member.data = {};
}
if (!member.data['one-time-date']) {
const currentTime = new Date();
const expirationTime = new Date(currentTime.getTime() + (3 * 60 * 60 * 1000)); // Set the expiration time to 3 hours (adjust as needed)
member.data['one-time-date'] = expirationTime.toISOString();
// Update member JSON
await memberstack.updateMemberJSON({
json: member.data
});
}
};
updateDate();
});
</script>
JSON + Custom Field
Use this if you need to add the date to a custom field (usually for use with automations).
<!-- 💙💙 MEMBERSCRIPT #27 v0.1.1 (CUSTOM FIELD) 💙 SET ONE TIME DATE -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
const msMem = JSON.parse(localStorage.getItem('_ms-mem'));
const member = await memberstack.getMemberJSON();
if (!member.data) {
member.data = {};
}
// Check if the user has the 'one-time-date' custom field in Memberstack
if (!msMem.customFields || !msMem.customFields['one-time-date']) {
const currentTime = new Date();
const expirationTime = new Date(currentTime.getTime() + (3 * 60 * 60 * 1000)); // Set the expiration time to 3 hours (adjust as needed)
const updatedCustomFields = {
...msMem.customFields,
'one-time-date': expirationTime.toISOString()
};
member.data['one-time-date'] = expirationTime.toISOString();
await memberstack.updateMemberJSON({
json: member.data
});
await memberstack.updateMember({
customFields: updatedCustomFields
});
}
});
</script>
#26 - Gate Content With Custom Modals
Use custom modals to urge your visitors to get a paid account!
<!-- 💙 MEMBERSCRIPT #26 v0.1 💙 GATE CONTENT WITH MODALS -->
<script>
$memberstackDom.getCurrentMember().then(({ data }) => {
if (!data) {
// Member is not logged in
const triggers = document.querySelectorAll('[ms-code-gate-modal-trigger]');
const boxes = document.querySelectorAll('[ms-code-gate-modal-box]');
triggers.forEach(trigger => {
trigger.addEventListener('click', () => {
const targetId = trigger.getAttribute('ms-code-gate-modal-trigger');
const box = document.querySelector(`[ms-code-gate-modal-box="${targetId}"]`);
if (box) {
box.style.display = 'flex';
}
});
// Remove links and attributes from trigger
// Uncomment the lines below to enable this functionality
// trigger.removeAttribute('href');
// trigger.removeAttribute('target');
// trigger.removeAttribute('rel');
// trigger.removeAttribute('onclick');
});
}
});
</script>
#25 - Hide Element Based On Other Element's Children
Remove an element from the page if another defined element has no child elements.
<!-- 💙 MEMBERSCRIPT #25 v0.1 💙 HIDE ELEMENT BASED ON OTHER ELEMENT CHILDREN -->
<script>
window.addEventListener('DOMContentLoaded', function() {
const subjectAttribute = 'ms-code-visibility-subject';
const targetAttribute = 'ms-code-visibility-target';
const subjectElement = document.querySelector(`[${subjectAttribute}]`);
const targetElement = document.querySelector(`[${targetAttribute}]`);
if (!subjectElement || !targetElement) {
console.error('Subject or target element not found');
return;
}
function checkVisibility() {
const children = subjectElement.children;
let allHidden = true;
for (let i = 0; i < children.length; i++) {
const child = children[i];
const computedStyle = window.getComputedStyle(child);
if (computedStyle.display !== 'none') {
allHidden = false;
break;
}
}
if (children.length === 0 || allHidden) {
targetElement.style.display = 'none';
} else {
targetElement.style.display = '';
}
}
// Check visibility initially
checkVisibility();
// Check visibility whenever the subject element or its children change
const observer = new MutationObserver(checkVisibility);
observer.observe(subjectElement, { childList: true, subtree: true });
});
</script>
#24 - Filter Lists Based On Element
Filter any kind of list based on the presence of an element within it's children.
Standard option
This works in most use cases.
<!-- 💙 MEMBERSCRIPT #24 v0.1 💙 FILTER ITEMS WITHIN LIST BASED ON ELEMENT -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const filterListItems = function(list, filterAttribute) {
const items = list.querySelectorAll(`[ms-code-filter-item="${filterAttribute}"]`);
items.forEach(item => {
const target = item.querySelector(`[ms-code-filter-target="${filterAttribute}"]`);
if (!target || window.getComputedStyle(target).display === 'none') {
item.style.display = 'none';
} else {
item.style.display = '';
}
});
};
const filterLists = document.querySelectorAll('[ms-code-filter-list]');
const updateFiltering = function() {
filterLists.forEach(list => {
const filterAttribute = list.getAttribute('ms-code-filter-list');
filterListItems(list, filterAttribute);
});
};
const observeListChanges = function() {
const observer = new MutationObserver(updateFiltering);
filterLists.forEach(list => observer.observe(list, { childList: true, subtree: true }));
};
updateFiltering();
observeListChanges();
});
</script>
Polling option
If standard does not work, try this.
<!-- 💙 MEMBERSCRIPT #24 v0.1.1 💙 FILTER ITEMS WITHIN LIST BASED ON ELEMENT (POLLING) -->
<script>
window.addEventListener("DOMContentLoaded", function() {
const filterListItems = function(list, filterAttribute) {
const items = list.querySelectorAll(`[ms-code-filter-item="${filterAttribute}"]`);
items.forEach(item => {
const target = item.querySelector(`[ms-code-filter-target="${filterAttribute}"]`);
if (!target || window.getComputedStyle(target).display === 'none') {
item.style.display = 'none';
} else {
item.style.display = '';
}
});
};
const filterLists = document.querySelectorAll('[ms-code-filter-list]');
const updateFiltering = function() {
filterLists.forEach(list => {
const filterAttribute = list.getAttribute('ms-code-filter-list');
filterListItems(list, filterAttribute);
});
};
const pollPage = function() {
updateFiltering();
setTimeout(pollPage, 1000); // Poll every 1 second
};
pollPage();
});
</script>
#23 - Skeleton Screens / Content Loaders
Easily add these industry-standard loading states to your site in just a few seconds.
Light mode
Use this on a white background
<!-- 💙 MEMBERSCRIPT #23 v0.1 💙 SKELETON SCREENS/CONTENT LOADERS -->
<script>
window.addEventListener("DOMContentLoaded", (event) => {
const skeletonElements = document.querySelectorAll('[ms-code-skeleton]');
skeletonElements.forEach(element => {
// Create a skeleton div
const skeletonDiv = document.createElement('div');
skeletonDiv.classList.add('skeleton-loader');
// Add the skeleton div to the current element
element.style.position = 'relative';
element.appendChild(skeletonDiv);
// Get delay from the attribute
let delay = element.getAttribute('ms-code-skeleton');
// If attribute value is not a number, set default delay as 2000ms
if (isNaN(delay)) {
delay = 2000;
}
setTimeout(() => {
// Remove the skeleton loader div after delay
const skeletonDiv = element.querySelector('.skeleton-loader');
element.removeChild(skeletonDiv);
}, delay);
});
});
</script>
<style>
.skeleton-loader {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-radius: inherit; /* Inherit the border-radius of the parent element */
background: linear-gradient(to right, #f6f7f8 25%, #e0e0e0 50%, #f6f7f8 75%);
background-size: 200% 100%; /* Increase the size of the background image */
z-index: 1; /* Make sure the skeleton loader is on top of the content */
animation: skeleton 1s infinite linear;
}
@keyframes skeleton {
0% { background-position: -100% 0; }
100% { background-position: 100% 0; }
}
[ms-code-skeleton] {
background-clip: padding-box;
}
</style>
Dark mode
Use this on a black background
<!-- 💙 MEMBERSCRIPT #23 v0.1 💙 SKELETON SCREENS/CONTENT LOADERS -->
<script>
window.addEventListener("DOMContentLoaded", (event) => {
const skeletonElements = document.querySelectorAll('[ms-code-skeleton]');
skeletonElements.forEach(element => {
// Create a skeleton div
const skeletonDiv = document.createElement('div');
skeletonDiv.classList.add('skeleton-loader');
// Add the skeleton div to the current element
element.style.position = 'relative';
element.appendChild(skeletonDiv);
// Get delay from the attribute
let delay = element.getAttribute('ms-code-skeleton');
// If attribute value is not a number, set default delay as 2000ms
if (isNaN(delay)) {
delay = 2000;
}
setTimeout(() => {
// Remove the skeleton loader div after delay
const skeletonDiv = element.querySelector('.skeleton-loader');
element.removeChild(skeletonDiv);
}, delay);
});
});
</script>
<style>
.skeleton-loader {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-radius: inherit;
background: linear-gradient(to right, #222222 25%, #333333 50%, #222222 75%); /* Updated background colors */
background-size: 200% 100%;
z-index: 1;
animation: skeleton 1s infinite linear;
}
@keyframes skeleton {
0% { background-position: -100% 0; }
100% { background-position: 100% 0; }
}
[ms-code-skeleton] {
background-clip: padding-box;
}
</style>
#22 - Disable Submit Button Until Required Fields Are Done
Gray out your submit button until all required values have something in them.
<!-- 💙 MEMBERSCRIPT #22 v0.1 💙 DISABLE SUBMIT BUTTON UNTIL REQUIRED FIELDS ARE COMPLETE -->
<script>
window.onload = function() {
const forms = document.querySelectorAll('form[ms-code-submit-form]');
forms.forEach(form => {
const submitButton = form.querySelector('input[type="submit"]');
const requiredFields = form.querySelectorAll('input[required]');
form.addEventListener('input', function() {
const allFilled = Array.from(requiredFields).every(field => field.value.trim() !== '');
if (allFilled) {
submitButton.classList.add('submit-enabled');
} else {
submitButton.classList.remove('submit-enabled');
}
});
});
};
</script>
#21 - Custom Toast Notifications
Display custom toast boxes on element click!
<!-- 💙 MEMBERSCRIPT #21 v0.1 💙 CUSTOM TOAST BOXES -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const toastTriggers = document.querySelectorAll("[ms-code-toast-trigger]");
toastTriggers.forEach(trigger => {
trigger.addEventListener("click", function() {
const triggerId = trigger.getAttribute("ms-code-toast-trigger");
const toastBox = document.querySelector(`[ms-code-toast-box="${triggerId}"]`);
if (toastBox) {
const fadeInDuration = 200;
const fadeOutDuration = 200;
const staticDuration = 2000;
const totalDuration = fadeInDuration + staticDuration + fadeOutDuration;
toastBox.style.opacity = "0";
toastBox.style.display = "block";
let currentTime = 0;
const fade = function() {
currentTime += 10;
const opacity = currentTime < fadeInDuration
? currentTime / fadeInDuration
: currentTime < fadeInDuration + staticDuration
? 1
: 1 - (currentTime - fadeInDuration - staticDuration) / fadeOutDuration;
toastBox.style.opacity = opacity;
if (currentTime < totalDuration) {
setTimeout(fade, 10);
} else {
toastBox.style.display = "none";
}
};
fade();
}
});
});
});
</script>
#19 - Add URL From Custom Field To IFrame SRC
Create a member-specific embed functionality with this custom field iframe solution!
<!-- 💙 MEMBERSCRIPT #19 v0.1 💙 ADD CUSTOM FIELD AS AN IFRAME SRC -->
<script>
document.addEventListener("DOMContentLoaded", function() {
// Parse member data from local storage
const memberData = JSON.parse(localStorage.getItem('_ms-mem') || '{}');
// Check if the user is logged in
if(memberData && memberData.id) {
// Get custom fields
const customFields = memberData.customFields;
// Select all elements with 'ms-code-field-link' attribute
const elements = document.querySelectorAll('[ms-code-field-link]');
// Iterate over all selected elements
elements.forEach(element => {
// Get custom field key from 'ms-code-field-link' attribute
const fieldKey = element.getAttribute('ms-code-field-link');
// If key exists in custom fields, set element src to the corresponding value
if(customFields.hasOwnProperty(fieldKey)) {
element.setAttribute('src', customFields[fieldKey]);
}
});
}
});
</script>
#18 - Easily Truncate Text
Add one attribute and a simple script to programatically truncate text!
<!-- 💙 MEMBERSCRIPT #18 v0.2 💙 - EASILY TRUNCATE TEXT -->
<script>
const elements = document.querySelectorAll('[ms-code-truncate]');
elements.forEach((element) => {
const charLimit = parseInt(element.getAttribute('ms-code-truncate'));
// Create a helper function that will recursively traverse the DOM tree
const traverseNodes = (node, count) => {
for (let child of node.childNodes) {
// If the node is a text node, truncate if necessary
if (child.nodeType === Node.TEXT_NODE) {
if (count + child.textContent.length > charLimit) {
child.textContent = child.textContent.slice(0, charLimit - count) + '...';
return count + child.textContent.length;
}
count += child.textContent.length;
}
// If the node is an element, recurse through its children
else if (child.nodeType === Node.ELEMENT_NODE) {
count = traverseNodes(child, count);
}
}
return count;
}
// Create a deep clone of the element to work on. This is so that we don't modify the original element
// until we have completely finished processing.
const clone = element.cloneNode(true);
// Traverse and truncate the cloned node
traverseNodes(clone, 0);
// Replace the original element with our modified clone
element.parentNode.replaceChild(clone, element);
});
</script>
#17 - Populate Link From Custom Field
Use custom fields to populate link targets with one attribute!
<!-- 💙 MEMBERSCRIPT #17 v0.2 💙 ADD CUSTOM FIELD AS A LINK -->
<script>
document.addEventListener("DOMContentLoaded", function() {
// Parse member data from local storage
const memberData = JSON.parse(localStorage.getItem('_ms-mem') || '{}');
// Check if the user is logged in
if (memberData && memberData.id) {
// Get custom fields
const customFields = memberData.customFields;
// Select all elements with 'ms-code-field-link' attribute
const elements = document.querySelectorAll('[ms-code-field-link]');
// Iterate over all selected elements
elements.forEach(element => {
// Get custom field key from 'ms-code-field-link' attribute
const fieldKey = element.getAttribute('ms-code-field-link');
// If key exists in custom fields
if (customFields.hasOwnProperty(fieldKey)) {
// Get the custom field value
const fieldValue = customFields[fieldKey];
// Check if the custom field value is empty
if (fieldValue.trim() === '') {
// Set the element to display none
element.style.display = 'none';
} else {
// Check if the link has a protocol, if not, add http://
let link = fieldValue;
if (!/^https?:\/\//i.test(link)) {
link = 'http://' + link;
}
// Set the element href to the corresponding value
element.setAttribute('href', link);
}
} else {
// Set the element to display none if the custom field key doesn't exist
element.style.display = 'none';
}
});
}
});
</script>
#16 - End Session After X Minutes of Inactivity
This script redirects inactive users to a "Session Expired" page after a certain period of inactivity. A countdown time is displayed on the page and resets with page activity.
Add this code before the closing </body> tag any page which needs a countdown timer.
<!-- 💙 MEMBERSCRIPT #16 v0.2 💙 LOGOUT AFTER X MINUTES OF INACTIVITY -->
<script>
let logoutTimer;
let countdownInterval;
let initialTime;
function startLogoutTimer() {
// Get the logout time from the HTML element
const timeElement = document.querySelector('[ms-code-logout-timer]');
const timeParts = timeElement.textContent.split(':');
const minutes = parseInt(timeParts[0], 10);
const seconds = parseInt(timeParts[1], 10);
const LOGOUT_TIME = (minutes * 60 + seconds) * 1000; // Convert to milliseconds
// Store the initial time value
if (!initialTime) {
initialTime = LOGOUT_TIME;
}
// Clear the previous timer, if any
clearTimeout(logoutTimer);
clearInterval(countdownInterval);
let startTime = Date.now();
// Start a new timer to redirect the user after the specified time
logoutTimer = setTimeout(() => {
window.location.href = "/expired";
startLogoutTimer(); // Start the logout timer again
}, LOGOUT_TIME);
// Start a countdown timer
countdownInterval = setInterval(() => {
let elapsed = Date.now() - startTime;
let remaining = LOGOUT_TIME - elapsed;
updateCountdownDisplay(remaining);
}, 1000); // update every second
}
function updateCountdownDisplay(remainingTimeInMs) {
// convert ms to MM:SS format
let minutes = Math.floor(remainingTimeInMs / 1000 / 60);
let seconds = Math.floor((remainingTimeInMs / 1000) % 60);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
const timeElement = document.querySelector('[ms-code-logout-timer]');
timeElement.textContent = `${minutes}:${seconds}`;
}
// Call this function whenever the user interacts with the page
function resetLogoutTimer() {
const timeElement = document.querySelector('[ms-code-logout-timer]');
timeElement.textContent = formatTime(initialTime); // Reset to the initial time
startLogoutTimer();
}
function formatTime(timeInMs) {
let minutes = Math.floor(timeInMs / 1000 / 60);
let seconds = Math.floor((timeInMs / 1000) % 60);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
return `${minutes}:${seconds}`;
}
// Call this function when the user logs in
function cancelLogoutTimer() {
clearTimeout(logoutTimer);
clearInterval(countdownInterval);
}
// Start the timer when the page loads
startLogoutTimer();
// Add event listeners to reset timer on any page activity
document.addEventListener('mousemove', resetLogoutTimer);
document.addEventListener('keypress', resetLogoutTimer);
document.addEventListener('touchstart', resetLogoutTimer);
</script>
Add this code to your /expired page before the closing </body> tag.
<!-- 💙 MEMBERSCRIPT #16 v0.2 💙 LOGOUT AFTER X MINUTES OF INACTIVITY -->
<script>
window.addEventListener('load', () => {
window.$memberstackDom.getCurrentMember().then(async ({ data: member }) => {
if (member) {
try {
await window.$memberstackDom.logout();
console.log('Logged out successfully');
setTimeout(() => {
location.reload();
}, 1000); // Refresh the page 1 second after logout
} catch (error) {
console.error(error);
}
}
});
});
</script>
#15 - Refresh Page After Set Duration On Click
Refresh the page after a set amount of time when an element is clicked.
<!-- 💙 MEMBERSCRIPT #15 v0.1 💙 TRIGGER REFRESH ON CLICK -->
<script>
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("click", function(event) {
const clickedElement = event.target.closest('[ms-code-refresh]');
if (clickedElement) {
const refreshDelay = parseInt(clickedElement.getAttribute('ms-code-refresh'));
if (!isNaN(refreshDelay)) {
setTimeout(function() {
location.reload();
}, refreshDelay);
}
}
});
});
</script>
#14 - Create Loading State On Click
Simulate a custom loading state when an element is clicked.
<!-- 💙 MEMBERSCRIPT #14 v0.1 💙 CREATE LOADING STATE ON CLICK -->
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous" ></script>
<script>
$(document).ready(function() {
// Bind click event to elements with ms-code-loading-trigger attribute
$(document).on('click', '[ms-code-loading-trigger]', function() {
// Get the value of ms-code-loading-trigger attribute
var triggerValue = $(this).attr("ms-code-loading-trigger");
// Find the matching loading element
var loadingElement = $("[ms-code-loading-element='" + triggerValue + "']");
// Find the matching subject element
var subjectElement = $("[ms-code-loading-subject='" + triggerValue + "']");
// Show the loading element (with flex display), append it to the subject element
loadingElement.css('display', 'flex').appendTo(subjectElement);
// After 5 seconds (5000 milliseconds), hide the loading element
setTimeout(function() {
loadingElement.hide();
}, 5000);
});
});
</script>
Try Memberstack for free
Product
Full Feature ListUser AccountsGated ContentSecure PaymentsAPI & IntegrationsCreate a new account2.0 Log in1.0 Log inPricingLanguage
Something not working as expected?
We would really appreciate if you could let us know what the issue is! If you want us to get back to you about it, you can leave your email too.