MemberScripts
Una solución basada en atributos para añadir funciones a su sitio Webflow.
Simplemente copie algo de código, añada algunos atributos y listo.
Todos los clientes de Memberstack pueden solicitar asistencia en el Slack 2.0. Tenga en cuenta que no se trata de funciones oficiales y que no se puede garantizar la asistencia.

#38 - Campo de carga de archivos
Añade un cargador de archivos a cualquier sitio y envía el envío a Google Drive, correo electrónico o donde quieras.
<!-- 💙 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>
<!-- 💙 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 - Eliminar automáticamente el plan gratuito
Elimine automáticamente un plan gratuito después de un tiempo determinado.
<!-- 💙 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>
<!-- 💙 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 - Validación de contraseñas
Utilice este sencillo método para confirmar que sus usuarios han introducido una contraseña segura.
<!-- 💙 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>
<!-- 💙 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 - Añadir fácilmente FAQ Schema/Rich Snippets
Añade un script y 2 atributos para habilitar la actualización constante de rich snippets en tu página.
<!-- 💙 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>
<!-- 💙 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 - Requerir correo electrónico profesional para el envío de formularios
Bloquea a las personas que envíen un formulario si su correo electrónico utiliza un correo personal como 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>
<!-- 💙 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 - Formatear automáticamente las entradas del formulario
Forzar que las entradas del formulario sigan un formato establecido, como DD/MM/AAAA.
<!-- 💙 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>
<!-- 💙 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 - Establecer Entrada como Requerida si es Visible
Cree formularios condicionales mostrando y ocultando las entradas requeridas.
<!-- 💙 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>
<!-- 💙 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 - Abrir una pestaña Webflow con un enlace
Este script genera automáticamente enlaces a sus pestañas Webflow.
<!-- 💙 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>
<!-- 💙 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 - Contar elementos en la página y actualizar el número
Comprueba cuántos elementos con un atributo establecido hay en la página y aplica ese número a algún texto.
<!-- 💙 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>
<!-- 💙 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 - Fijar temporalmente la altura del elemento en carga
Fuerza a un elemento a tener una altura determinada durante un tiempo determinado al cargar la página.
<!-- 💙 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>
<!-- 💙 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 - Mostrar elemento basado en el paso de fecha JSON
Comprueba la fecha única de #27 y muestra/oculta un elemento basado en ella.
<!-- 💙 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>
<!-- 💙 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 - Establecer una fecha única en el registro
Aplicar una fecha a su miembro JSON después de la inscripción que se puede utilizar para cualquier cosa.
Sólo JSON
Si no necesita añadir también la fecha a un campo personalizado, utilice esto.
<!-- 💙 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 + Campo personalizado
Utilícelo si necesita añadir la fecha a un campo personalizado (normalmente para utilizarlo con automatizaciones).
<!-- 💙💙 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>
Sólo JSON
Si no necesita añadir también la fecha a un campo personalizado, utilice esto.
<!-- 💙 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 + Campo personalizado
Utilícelo si necesita añadir la fecha a un campo personalizado (normalmente para utilizarlo con automatizaciones).
<!-- 💙💙 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 - Contenido de la puerta con módulos personalizados
Utilice modales personalizados para instar a sus visitantes a obtener una cuenta de pago.
<!-- 💙 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>
<!-- 💙 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 - Ocultar Elemento Basado en los Hijos de Otro Elemento
Elimina un elemento de la página si otro elemento definido no tiene elementos hijos.
<!-- 💙 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>
<!-- 💙 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 - Filtrar listas por elemento
Filtra cualquier tipo de lista basándose en la presencia de un elemento dentro de sus hijos.
Opción estándar
Esto funciona en la mayoría de los casos.
<!-- 💙 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>
Opción de sondeo
Si la norma no funciona, prueba esto.
<!-- 💙 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>
Opción estándar
Esto funciona en la mayoría de los casos.
<!-- 💙 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>
Opción de sondeo
Si la norma no funciona, prueba esto.
<!-- 💙 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 - Pantallas esqueleto / Cargadores de contenido
Añada fácilmente a su sitio web estos estados de carga estándar del sector en sólo unos segundos.
Modo luz
Utilícelo sobre fondo blanco
<!-- 💙 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>
Modo oscuro
Utilícelo sobre fondo negro
<!-- 💙 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>
Modo luz
Utilícelo sobre fondo blanco
<!-- 💙 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>
Modo oscuro
Utilícelo sobre fondo negro
<!-- 💙 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 - Desactivar el botón Enviar hasta que se completen los campos obligatorios
Desengrase el botón de envío hasta que todos los valores requeridos contengan algo.
<!-- 💙 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>
<!-- 💙 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 - Notificaciones Toast personalizadas
Muestre cajas de tostadas personalizadas al hacer clic en un elemento
<!-- 💙 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>
<!-- 💙 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 - Añadir URL de campo personalizado a IFrame SRC
Cree una funcionalidad de incrustación específica para los miembros con esta solución de iframe de campo personalizado.
<!-- 💙 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>
<!-- 💙 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 - Truncar texto fácilmente
Añade un atributo y un sencillo script para truncar texto de forma programada.
<!-- 💙 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>
<!-- 💙 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>
MemberScripts
Instantly add custom features to your Webflow site.
Just paste a script, set attributes, and go live.
Join the Memberstack 2.0 Slack for tips, answers, and community scripts. Please note that these are not official features and support cannot be guaranteed.

#158 - Emoji Feedback Widget for Memberstack
This widget lets your logged-in members quickly share how they feel using simple emoji buttons.
<!-- 💙 MEMBERSCRIPT #158 v1.0 💙 - EMOJI FEEDBACK WIDGET -->
<!--
Collect emoji-based feedback from logged-in members.
Saves submission state in localStorage and sends data to Make.com.
-->
<script>
(function() {
const msDom = window.$memberstackDom;
if (!msDom) {
console.error('Memberstack DOM not found.');
return;
}
// Elements
const widget = document.querySelector('[data-ms-code="emoji-feedback-widget"]');
const closeBtn = widget.querySelector('[data-ms-code="emoji-feedback-close"]');
const buttons = widget.querySelectorAll('[data-ms-code="emoji-feedback-btn"]');
const thanks = widget.querySelector('[data-ms-code="emoji-feedback-thanks"]');
// Exit early if feedback is done or dismissed
if (
localStorage.getItem('emojiFeedbackDone') === 'true' ||
localStorage.getItem('emojiFeedbackClosed') === 'true'
) {
widget.style.display = 'none';
return;
}
// Handle close (×) click
closeBtn.addEventListener('click', e => {
e.preventDefault();
localStorage.setItem('emojiFeedbackClosed', 'true');
widget.style.display = 'none';
});
// Fetch member data
msDom.getCurrentMember()
.then(({ data: member }) => {
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const score = btn.getAttribute('data-value');
// Payload for Make.com
const payload = {
memberId: member.id,
name: member.customFields["first-name"] || '',
email: member.auth.email || '',
pageUrl: window.location.href,
feedback: score,
timestamp: new Date().toISOString()
};
// Send feedback to Make
fetch('https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => {
if (!res.ok) throw new Error(res.statusText);
// Success state: hide emojis, show thank you
localStorage.setItem('emojiFeedbackDone', 'true');
widget.querySelector('[data-ms-code="emoji-feedback-buttons"]').style.display = 'none';
thanks.style.display = 'block';
})
.catch(err => {
console.error('Emoji feedback error:', err);
// Optionally add error handling UI here
});
});
});
})
.catch(err => console.error('Couldn’t get member:', err));
})();
</script>
<!-- 💙 MEMBERSCRIPT #158 v1.0 💙 - EMOJI FEEDBACK WIDGET -->
<!--
Collect emoji-based feedback from logged-in members.
Saves submission state in localStorage and sends data to Make.com.
-->
<script>
(function() {
const msDom = window.$memberstackDom;
if (!msDom) {
console.error('Memberstack DOM not found.');
return;
}
// Elements
const widget = document.querySelector('[data-ms-code="emoji-feedback-widget"]');
const closeBtn = widget.querySelector('[data-ms-code="emoji-feedback-close"]');
const buttons = widget.querySelectorAll('[data-ms-code="emoji-feedback-btn"]');
const thanks = widget.querySelector('[data-ms-code="emoji-feedback-thanks"]');
// Exit early if feedback is done or dismissed
if (
localStorage.getItem('emojiFeedbackDone') === 'true' ||
localStorage.getItem('emojiFeedbackClosed') === 'true'
) {
widget.style.display = 'none';
return;
}
// Handle close (×) click
closeBtn.addEventListener('click', e => {
e.preventDefault();
localStorage.setItem('emojiFeedbackClosed', 'true');
widget.style.display = 'none';
});
// Fetch member data
msDom.getCurrentMember()
.then(({ data: member }) => {
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const score = btn.getAttribute('data-value');
// Payload for Make.com
const payload = {
memberId: member.id,
name: member.customFields["first-name"] || '',
email: member.auth.email || '',
pageUrl: window.location.href,
feedback: score,
timestamp: new Date().toISOString()
};
// Send feedback to Make
fetch('https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => {
if (!res.ok) throw new Error(res.statusText);
// Success state: hide emojis, show thank you
localStorage.setItem('emojiFeedbackDone', 'true');
widget.querySelector('[data-ms-code="emoji-feedback-buttons"]').style.display = 'none';
thanks.style.display = 'block';
})
.catch(err => {
console.error('Emoji feedback error:', err);
// Optionally add error handling UI here
});
});
});
})
.catch(err => console.error('Couldn’t get member:', err));
})();
</script>

#157 - Range Slider Feedback Widget
A simple and friendly slider widget that lets logged-in members give quick feedback (0–10).
<!-- 💙 MEMBERSCRIPT #157 v1.0 💙 - RANGE SLIDER FEEDBACK WIDGET -->
<!--
A lightweight feedback widget that uses a range slider.
Prevents duplicate submissions with localStorage, and sends feedback to Make.com via webhook.
-->
<script>
Webflow.push(function() {
// Silently disable all form submissions
$('form').submit(function(e) {
e.preventDefault();
return false;
});
});
(function () {
const msDom = window.$memberstackDom;
if (!msDom) {
console.error("Memberstack DOM not found. Did you include data-memberstack-app?");
return;
}
const widget = document.querySelector('[data-ms-code="feedback-widget"]');
const dialog = widget?.querySelector('[data-ms-code="feedback-dialog"]');
const toggle = widget?.querySelector('[data-ms-code="feedback-toggle"]');
const slider = widget?.querySelector('[data-ms-code="feedback-range"]');
const submit = widget?.querySelector('[data-ms-code="feedback-next"]');
const form = widget?.closest("form");
const done = localStorage.getItem("feedbackDone") === "true";
const closed = localStorage.getItem("feedbackClosed") === "true";
if (!widget || !dialog || !toggle || !slider || !submit || done || closed) {
if (widget) widget.style.display = "none";
return;
}
// Manual close button logic
toggle.addEventListener("click", (e) => {
e.preventDefault();
localStorage.setItem("feedbackClosed", "true");
widget.style.display = "none";
});
// Fetch logged-in member
msDom.getCurrentMember()
.then(({ data: member }) => {
slider.addEventListener("input", () => {
submit.disabled = false;
});
submit.addEventListener("click", () => {
const payload = {
memberId: member.id,
name: member.customFields["first-name"] || "",
email: member.auth.email || "",
pageUrl: window.location.href,
feedback: slider.value,
timestamp: new Date().toISOString()
};
fetch("https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
localStorage.setItem("feedbackDone", "true");
const msg = document.createElement("p");
msg.textContent = "Thanks for your feedback!";
msg.style.padding = "1em";
msg.style.textAlign = "center";
dialog.innerHTML = "";
dialog.appendChild(msg);
})
.catch((err) => {
console.error("Feedback error:", err);
dialog.insertAdjacentHTML(
"beforeend",
'<p style="color:red; text-align:center;">Oops! Could not send. Try again?</p>'
);
});
});
})
.catch((err) => console.error("Couldn’t get member:", err));
})();
</script>
<!-- 💙 MEMBERSCRIPT #157 v1.0 💙 - RANGE SLIDER FEEDBACK WIDGET -->
<!--
A lightweight feedback widget that uses a range slider.
Prevents duplicate submissions with localStorage, and sends feedback to Make.com via webhook.
-->
<script>
Webflow.push(function() {
// Silently disable all form submissions
$('form').submit(function(e) {
e.preventDefault();
return false;
});
});
(function () {
const msDom = window.$memberstackDom;
if (!msDom) {
console.error("Memberstack DOM not found. Did you include data-memberstack-app?");
return;
}
const widget = document.querySelector('[data-ms-code="feedback-widget"]');
const dialog = widget?.querySelector('[data-ms-code="feedback-dialog"]');
const toggle = widget?.querySelector('[data-ms-code="feedback-toggle"]');
const slider = widget?.querySelector('[data-ms-code="feedback-range"]');
const submit = widget?.querySelector('[data-ms-code="feedback-next"]');
const form = widget?.closest("form");
const done = localStorage.getItem("feedbackDone") === "true";
const closed = localStorage.getItem("feedbackClosed") === "true";
if (!widget || !dialog || !toggle || !slider || !submit || done || closed) {
if (widget) widget.style.display = "none";
return;
}
// Manual close button logic
toggle.addEventListener("click", (e) => {
e.preventDefault();
localStorage.setItem("feedbackClosed", "true");
widget.style.display = "none";
});
// Fetch logged-in member
msDom.getCurrentMember()
.then(({ data: member }) => {
slider.addEventListener("input", () => {
submit.disabled = false;
});
submit.addEventListener("click", () => {
const payload = {
memberId: member.id,
name: member.customFields["first-name"] || "",
email: member.auth.email || "",
pageUrl: window.location.href,
feedback: slider.value,
timestamp: new Date().toISOString()
};
fetch("https://hook.eu2.make.com/8wm1j323te1sybyweux6x33mh77vswvm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
localStorage.setItem("feedbackDone", "true");
const msg = document.createElement("p");
msg.textContent = "Thanks for your feedback!";
msg.style.padding = "1em";
msg.style.textAlign = "center";
dialog.innerHTML = "";
dialog.appendChild(msg);
})
.catch((err) => {
console.error("Feedback error:", err);
dialog.insertAdjacentHTML(
"beforeend",
'<p style="color:red; text-align:center;">Oops! Could not send. Try again?</p>'
);
});
});
})
.catch((err) => console.error("Couldn’t get member:", err));
})();
</script>

#156 - Encrypt Sensitive Data Before Sending to Memberstack
This script protects sensitive user data by encrypting it in the browser before it’s sent to Memberstack.
<!-- 💙 MEMBERSCRIPT #156 v1.0 💙 - ENCRYPT SENSITIVE DATA BEFORE SENDING TO MEMBERSTACK -->
<!--
This script encrypts input fields before they're submitted to Memberstack,
using AES-GCM with a passphrase-based modal.
-->
<script>
document.addEventListener('DOMContentLoaded', function () {
(function () {
const enc = new TextEncoder();
const dec = new TextDecoder();
// Show the passphrase modal
function showModal() {
return new Promise(resolve => {
const modal = document.querySelector('[data-ms-code="encrypt-modal"]');
if (!modal) return alert('Encryption modal missing from the page.');
const input = modal.querySelector('[data-ms-code="pass-input"]');
const remember = modal.querySelector('[data-ms-code="remember-pass"]');
const submit = modal.querySelector('[data-ms-code="submit-pass"]');
const closeButtons = modal.querySelectorAll(
'[data-ms-code="close-encrypt-modal"], [data-ms-code="close-encrypt-icon"]'
);
modal.style.display = 'flex';
input.value = '';
input.focus();
const cleanup = () => {
modal.style.display = 'none';
};
if (submit) {
submit.onclick = () => {
const pass = input.value;
const keep = remember.checked;
cleanup();
resolve({ pass, remember: keep });
};
}
closeButtons.forEach(btn => {
btn.onclick = () => {
cleanup();
resolve({ pass: null });
};
});
});
}
// Derive AES key using PBKDF2
async function deriveKey(pass, salt) {
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(pass),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Encrypt a string
async function encryptText(text, pass) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(pass, salt);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
enc.encode(text)
);
return [
btoa(String.fromCharCode(...salt)),
btoa(String.fromCharCode(...iv)),
btoa(String.fromCharCode(...new Uint8Array(encrypted)))
].join(':');
}
// Decrypt a string
async function decryptText(encrypted, pass) {
const [saltB64, ivB64, dataB64] = encrypted.split(':');
if (!saltB64 || !ivB64 || !dataB64) throw new Error('Invalid format');
const salt = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));
const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
const data = Uint8Array.from(atob(dataB64), c => c.charCodeAt(0));
const key = await deriveKey(pass, salt);
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data);
return dec.decode(decrypted);
}
// Encrypt and submit form
document.querySelectorAll('[data-ms-code-encrypt]').forEach(btn => {
if (!btn.hasAttribute('data-ms-encrypt-attached')) {
btn.addEventListener('click', async e => {
e.preventDefault();
let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
if (!passphrase) {
const { pass, remember } = await showModal();
if (!pass) return;
passphrase = pass;
if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
}
const fields = document.querySelectorAll('[data-ms-code-id]');
for (let field of fields) {
const value = field.value.trim();
if (!value) continue;
try {
const encrypted = await encryptText(value, passphrase);
field.value = encrypted;
} catch (err) {
console.error('Encryption error:', err);
alert('Encryption failed.');
return;
}
}
const form = btn.closest('form');
if (form) form.requestSubmit();
});
btn.setAttribute('data-ms-encrypt-attached', 'true');
}
});
// Add decrypt button logic
function attachDecryptButton() {
const decryptBtn = document.querySelector('[data-ms-code="decrypt-all"]');
if (!decryptBtn || decryptBtn.hasAttribute('data-ms-decrypt-attached')) return;
decryptBtn.addEventListener('click', async e => {
e.preventDefault();
const encryptedFields = document.querySelectorAll('[data-ms-code-id]');
if (encryptedFields.length === 0) return alert('No fields to decrypt.');
let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
if (!passphrase) {
const { pass, remember } = await showModal();
if (!pass) return;
passphrase = pass;
if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
}
for (let field of encryptedFields) {
const encrypted = field.value.trim();
if (!encrypted) continue;
try {
const decrypted = await decryptText(encrypted, passphrase);
field.value = decrypted;
} catch (err) {
console.error('Decryption error:', err);
alert('One or more fields failed to decrypt.');
return;
}
}
});
decryptBtn.setAttribute('data-ms-decrypt-attached', 'true');
}
attachDecryptButton();
})();
});
</script>
<!-- 💙 MEMBERSCRIPT #156 v1.0 💙 - ENCRYPT SENSITIVE DATA BEFORE SENDING TO MEMBERSTACK -->
<!--
This script encrypts input fields before they're submitted to Memberstack,
using AES-GCM with a passphrase-based modal.
-->
<script>
document.addEventListener('DOMContentLoaded', function () {
(function () {
const enc = new TextEncoder();
const dec = new TextDecoder();
// Show the passphrase modal
function showModal() {
return new Promise(resolve => {
const modal = document.querySelector('[data-ms-code="encrypt-modal"]');
if (!modal) return alert('Encryption modal missing from the page.');
const input = modal.querySelector('[data-ms-code="pass-input"]');
const remember = modal.querySelector('[data-ms-code="remember-pass"]');
const submit = modal.querySelector('[data-ms-code="submit-pass"]');
const closeButtons = modal.querySelectorAll(
'[data-ms-code="close-encrypt-modal"], [data-ms-code="close-encrypt-icon"]'
);
modal.style.display = 'flex';
input.value = '';
input.focus();
const cleanup = () => {
modal.style.display = 'none';
};
if (submit) {
submit.onclick = () => {
const pass = input.value;
const keep = remember.checked;
cleanup();
resolve({ pass, remember: keep });
};
}
closeButtons.forEach(btn => {
btn.onclick = () => {
cleanup();
resolve({ pass: null });
};
});
});
}
// Derive AES key using PBKDF2
async function deriveKey(pass, salt) {
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(pass),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Encrypt a string
async function encryptText(text, pass) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(pass, salt);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
enc.encode(text)
);
return [
btoa(String.fromCharCode(...salt)),
btoa(String.fromCharCode(...iv)),
btoa(String.fromCharCode(...new Uint8Array(encrypted)))
].join(':');
}
// Decrypt a string
async function decryptText(encrypted, pass) {
const [saltB64, ivB64, dataB64] = encrypted.split(':');
if (!saltB64 || !ivB64 || !dataB64) throw new Error('Invalid format');
const salt = Uint8Array.from(atob(saltB64), c => c.charCodeAt(0));
const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0));
const data = Uint8Array.from(atob(dataB64), c => c.charCodeAt(0));
const key = await deriveKey(pass, salt);
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data);
return dec.decode(decrypted);
}
// Encrypt and submit form
document.querySelectorAll('[data-ms-code-encrypt]').forEach(btn => {
if (!btn.hasAttribute('data-ms-encrypt-attached')) {
btn.addEventListener('click', async e => {
e.preventDefault();
let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
if (!passphrase) {
const { pass, remember } = await showModal();
if (!pass) return;
passphrase = pass;
if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
}
const fields = document.querySelectorAll('[data-ms-code-id]');
for (let field of fields) {
const value = field.value.trim();
if (!value) continue;
try {
const encrypted = await encryptText(value, passphrase);
field.value = encrypted;
} catch (err) {
console.error('Encryption error:', err);
alert('Encryption failed.');
return;
}
}
const form = btn.closest('form');
if (form) form.requestSubmit();
});
btn.setAttribute('data-ms-encrypt-attached', 'true');
}
});
// Add decrypt button logic
function attachDecryptButton() {
const decryptBtn = document.querySelector('[data-ms-code="decrypt-all"]');
if (!decryptBtn || decryptBtn.hasAttribute('data-ms-decrypt-attached')) return;
decryptBtn.addEventListener('click', async e => {
e.preventDefault();
const encryptedFields = document.querySelectorAll('[data-ms-code-id]');
if (encryptedFields.length === 0) return alert('No fields to decrypt.');
let passphrase = sessionStorage.getItem('ms-encrypt-passphrase');
if (!passphrase) {
const { pass, remember } = await showModal();
if (!pass) return;
passphrase = pass;
if (remember) sessionStorage.setItem('ms-encrypt-passphrase', passphrase);
}
for (let field of encryptedFields) {
const encrypted = field.value.trim();
if (!encrypted) continue;
try {
const decrypted = await decryptText(encrypted, passphrase);
field.value = decrypted;
} catch (err) {
console.error('Decryption error:', err);
alert('One or more fields failed to decrypt.');
return;
}
}
});
decryptBtn.setAttribute('data-ms-decrypt-attached', 'true');
}
attachDecryptButton();
})();
});
</script>

#155 - Bulk Update Customer Subscriptions via Stripe & Make
Bulk update existing members to a new pricing plan with Stripe and Make

#154 - Two-Factor Authentication (2FA) for Memberstack Logins
Add an extra layer of security to your Memberstack logins by enabling Two-Factor Authentication (2FA).
<!--
MEMBERSCRIPT #154
---------------------------------
LOGIN PAGE SCRIPT
-->
<script>
(async function() {
const delay = ms => new Promise(r => setTimeout(r, ms));
async function routeLogin() {
try {
// Check if Memberstack is loaded
if (!window.$memberstackDom) {
console.log("Memberstack not loaded yet");
return;
}
// Get current member
const { data: member } = await window.$memberstackDom.getCurrentMember();
if (!member) return; // Exit if not logged in
// Get member JSON data
const jsonResponse = await window.$memberstackDom.getMemberJSON();
const memberData = jsonResponse.data || {};
// Check if 2FA is enabled
const needs2FA = memberData["2fa_enabled"] === true ||
jsonResponse["2fa_enabled"] === true;
// Check session storage for verification status
const verified = sessionStorage.getItem("2fa_verified") === "true";
console.log("2FA Status:", {
enabled: needs2FA,
verified: verified,
currentPath: window.location.pathname
});
// Handle 2FA redirect
if (needs2FA && !verified) {
if (!window.location.pathname.includes("/2fa-verify")) {
console.log("Redirecting to /2fa-verify");
window.location.href = "/2fa-verify";
}
return; // Stop further execution
}
// Handle success redirect
if (!window.location.pathname.includes("/success")) {
console.log("Redirecting to /success");
// Remove Memberstack's auto-redirect if login form exists
const loginForm = document.querySelector('[data-ms-form="login"]');
if (loginForm) {
loginForm.removeAttribute('data-ms-redirect');
}
window.location.href = "/success";
}
} catch (err) {
console.error("2FA routing error:", err);
}
}
// Wait for Memberstack to initialize
await delay(300);
routeLogin();
// Poll with cleanup
const pollInterval = setInterval(routeLogin, 500);
setTimeout(() => clearInterval(pollInterval), 10000);
})();
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
SETTINGS PAGE SCRIPT
-->
<!-- Load otplib preset-browser -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const { data: member } = await ms.getCurrentMember();
if (!member) return;
const checkbox = document.querySelector('[data-ms-code="enable-2fa"]');
const qrContainer = document.querySelector('[data-ms-code="2fa-qr-container"]');
const qrImage = document.querySelector('[data-ms-code="2fa-qr-image"]');
// Hide QR container by default
qrContainer.style.display = "none";
// Load member JSON and ensure data object exists
const jsonObj = await ms.getMemberJSON(); // { data: ... } or { data: null }
if (!jsonObj.data) jsonObj.data = {};
const inner = jsonObj.data;
// Set checkbox initial state
const enabled = inner["2fa_enabled"] === true;
checkbox.checked = enabled;
checkbox.addEventListener("change", async (e) => {
const isChecked = e.target.checked;
// Reload member and JSON
const { data: member } = await ms.getCurrentMember();
const jsonObj2 = await ms.getMemberJSON();
if (!jsonObj2.data) jsonObj2.data = {};
const inner2 = jsonObj2.data;
if (isChecked) {
// Enable 2FA: generate secret and QR
const secret = window.otplib.authenticator.generateSecret();
const uri = window.otplib.authenticator.keyuri(member.email, "Memberscript #154", secret);
qrImage.src = "https://api.qrserver.com/v1/create-qr-code/?data=" + encodeURIComponent(uri);
qrContainer.style.display = "flex";
inner2["2fa_enabled"] = true;
inner2["2fa_secret"] = secret;
await ms.updateMember({
customFields: { "2fa-enabled": "true" }
});
} else {
// Disable 2FA: remove secret and hide QR
qrContainer.style.display = "none";
inner2["2fa_enabled"] = false;
delete inner2["2fa_secret"];
await ms.updateMember({
customFields: { "2fa-enabled": "false" }
});
}
// Persist only nested JSON
await ms.updateMemberJSON({ json: inner2 });
// ✅ Debugging: check result
const check = await ms.getMemberJSON();
console.log(check);
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
SUCCESS / DASHBOARD PAGE SCRIPT
-->
<script>
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
if (!member) return; // Not logged in, no redirect
window.$memberstackDom.getMemberJSON().then(json => {
const enabled = json["2fa_enabled"];
const verified = sessionStorage.getItem("2fa_verified") === "true";
if (enabled && !verified && window.location.pathname !== "/2fa-verify") {
window.location.href = "/2fa-verify";
}
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
TWO FACTOR VERIFICATION PAGE SCRIPT
-->
<!-- Include the Buffer polyfill -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<!-- Include the otplib library -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const memberstack = window.$memberstackDom;
const { data: member } = await memberstack.getCurrentMember();
if (!member) return;
const form = document.querySelector('[data-ms-form="2fa-verification"]');
if (!form) return;
const codeInput = form.querySelector('[data-ms-code="2fa-code"]');
const errorContainer = form.querySelector('[data-ms-error="2fa-code"]');
function showError(message) {
if (errorContainer) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
} else {
alert(message);
}
}
function clearError() {
if (errorContainer) {
errorContainer.textContent = '';
errorContainer.style.display = 'none';
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
e.stopImmediatePropagation(); // important to stop other listeners
clearError();
const code = codeInput.value.trim();
const { data: json } = await memberstack.getMemberJSON();
const secret = json?.["2fa_secret"];
if (!secret || !code) {
const msg = form.getAttribute('data-ms-error-msg-missing') || 'Please enter your 2FA code';
showError(msg);
return;
}
if (otplib.authenticator.check(code, secret)) {
sessionStorage.setItem('2fa_verified', 'true');
window.location.href = '/success';
} else {
const msg = form.getAttribute('data-ms-error-msg-invalid') || 'Oops, the 2FA code is incorrect. Try again.';
showError(msg);
}
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
LOGIN PAGE SCRIPT
-->
<script>
(async function() {
const delay = ms => new Promise(r => setTimeout(r, ms));
async function routeLogin() {
try {
// Check if Memberstack is loaded
if (!window.$memberstackDom) {
console.log("Memberstack not loaded yet");
return;
}
// Get current member
const { data: member } = await window.$memberstackDom.getCurrentMember();
if (!member) return; // Exit if not logged in
// Get member JSON data
const jsonResponse = await window.$memberstackDom.getMemberJSON();
const memberData = jsonResponse.data || {};
// Check if 2FA is enabled
const needs2FA = memberData["2fa_enabled"] === true ||
jsonResponse["2fa_enabled"] === true;
// Check session storage for verification status
const verified = sessionStorage.getItem("2fa_verified") === "true";
console.log("2FA Status:", {
enabled: needs2FA,
verified: verified,
currentPath: window.location.pathname
});
// Handle 2FA redirect
if (needs2FA && !verified) {
if (!window.location.pathname.includes("/2fa-verify")) {
console.log("Redirecting to /2fa-verify");
window.location.href = "/2fa-verify";
}
return; // Stop further execution
}
// Handle success redirect
if (!window.location.pathname.includes("/success")) {
console.log("Redirecting to /success");
// Remove Memberstack's auto-redirect if login form exists
const loginForm = document.querySelector('[data-ms-form="login"]');
if (loginForm) {
loginForm.removeAttribute('data-ms-redirect');
}
window.location.href = "/success";
}
} catch (err) {
console.error("2FA routing error:", err);
}
}
// Wait for Memberstack to initialize
await delay(300);
routeLogin();
// Poll with cleanup
const pollInterval = setInterval(routeLogin, 500);
setTimeout(() => clearInterval(pollInterval), 10000);
})();
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
SETTINGS PAGE SCRIPT
-->
<!-- Load otplib preset-browser -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const { data: member } = await ms.getCurrentMember();
if (!member) return;
const checkbox = document.querySelector('[data-ms-code="enable-2fa"]');
const qrContainer = document.querySelector('[data-ms-code="2fa-qr-container"]');
const qrImage = document.querySelector('[data-ms-code="2fa-qr-image"]');
// Hide QR container by default
qrContainer.style.display = "none";
// Load member JSON and ensure data object exists
const jsonObj = await ms.getMemberJSON(); // { data: ... } or { data: null }
if (!jsonObj.data) jsonObj.data = {};
const inner = jsonObj.data;
// Set checkbox initial state
const enabled = inner["2fa_enabled"] === true;
checkbox.checked = enabled;
checkbox.addEventListener("change", async (e) => {
const isChecked = e.target.checked;
// Reload member and JSON
const { data: member } = await ms.getCurrentMember();
const jsonObj2 = await ms.getMemberJSON();
if (!jsonObj2.data) jsonObj2.data = {};
const inner2 = jsonObj2.data;
if (isChecked) {
// Enable 2FA: generate secret and QR
const secret = window.otplib.authenticator.generateSecret();
const uri = window.otplib.authenticator.keyuri(member.email, "Memberscript #154", secret);
qrImage.src = "https://api.qrserver.com/v1/create-qr-code/?data=" + encodeURIComponent(uri);
qrContainer.style.display = "flex";
inner2["2fa_enabled"] = true;
inner2["2fa_secret"] = secret;
await ms.updateMember({
customFields: { "2fa-enabled": "true" }
});
} else {
// Disable 2FA: remove secret and hide QR
qrContainer.style.display = "none";
inner2["2fa_enabled"] = false;
delete inner2["2fa_secret"];
await ms.updateMember({
customFields: { "2fa-enabled": "false" }
});
}
// Persist only nested JSON
await ms.updateMemberJSON({ json: inner2 });
// ✅ Debugging: check result
const check = await ms.getMemberJSON();
console.log(check);
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
SUCCESS / DASHBOARD PAGE SCRIPT
-->
<script>
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
if (!member) return; // Not logged in, no redirect
window.$memberstackDom.getMemberJSON().then(json => {
const enabled = json["2fa_enabled"];
const verified = sessionStorage.getItem("2fa_verified") === "true";
if (enabled && !verified && window.location.pathname !== "/2fa-verify") {
window.location.href = "/2fa-verify";
}
});
});
</script>
<!--
MEMBERSCRIPT #154
---------------------------------
TWO FACTOR VERIFICATION PAGE SCRIPT
-->
<!-- Include the Buffer polyfill -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/buffer.js"></script>
<!-- Include the otplib library -->
<script src="https://unpkg.com/@otplib/preset-browser@^12.0.0/index.js"></script>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const memberstack = window.$memberstackDom;
const { data: member } = await memberstack.getCurrentMember();
if (!member) return;
const form = document.querySelector('[data-ms-form="2fa-verification"]');
if (!form) return;
const codeInput = form.querySelector('[data-ms-code="2fa-code"]');
const errorContainer = form.querySelector('[data-ms-error="2fa-code"]');
function showError(message) {
if (errorContainer) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
} else {
alert(message);
}
}
function clearError() {
if (errorContainer) {
errorContainer.textContent = '';
errorContainer.style.display = 'none';
}
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
e.stopImmediatePropagation(); // important to stop other listeners
clearError();
const code = codeInput.value.trim();
const { data: json } = await memberstack.getMemberJSON();
const secret = json?.["2fa_secret"];
if (!secret || !code) {
const msg = form.getAttribute('data-ms-error-msg-missing') || 'Please enter your 2FA code';
showError(msg);
return;
}
if (otplib.authenticator.check(code, secret)) {
sessionStorage.setItem('2fa_verified', 'true');
window.location.href = '/success';
} else {
const msg = form.getAttribute('data-ms-error-msg-invalid') || 'Oops, the 2FA code is incorrect. Try again.';
showError(msg);
}
});
});
</script>

#153 - Instant Multilingual Site with Google Translate
Make your Webflow site multilingual in minutes with Google Translate and Memberstack
<!-- 💙 MEMBERSCRIPT #153 v1.0 💙 - FREE MULTILINGUAL SITE WITH GOOGLE TRANSLATE -->
<script>
// 1. Inject Google Translate script dynamically
const gtScript = document.createElement('script');
gtScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
document.head.appendChild(gtScript);
// 2. Inject CSS to hide Google Translate UI
const style = document.createElement('style');
style.innerHTML = `
body { top: 0px !important; position: static !important; }
.goog-te-banner-frame, .skiptranslate,
#goog-gt-tt, .goog-te-balloon-frame,
.goog-text-highlight {
display: none !important;
background: none !important;
box-shadow: none !important;
}
`;
document.head.appendChild(style);
// 3. Google Translate init
window.googleTranslateElementInit = function () {
new google.translate.TranslateElement({
pageLanguage: 'en',
layout: google.translate.TranslateElement.FloatPosition.TOP_LEFT
}, 'google_translate_element');
};
// 4. Helper to get cookies
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [key, value] = cookie.trim().split('=');
if (key === name) return decodeURIComponent(value);
}
return null;
}
// 5. Language map
const languageMap = new Map([
["af","Afrikaans"], ["sq","Albanian"], ["ar","Arabic"], ["hy","Armenian"],
["az","Azerbaijani"], ["eu","Basque"], ["be","Belarusian"], ["bg","Bulgarian"],
["ca","Catalan"], ["zh-CN","ChineseSimplified"], ["zh-TW","ChineseTraditional"],
["hr","Croatian"], ["cs","Czech"], ["da","Danish"], ["nl","Dutch"], ["de","German"],
["en","English"], ["et","Estonian"], ["tl","Filipino"], ["fi","Finnish"],
["fr","French"], ["gl","Galician"], ["ka","Georgian"], ["el","Greek"],
["ht","Haitian"], ["iw","Hebrew"], ["hi","Hindi"], ["hu","Hungarian"],
["is","Icelandic"], ["id","Indonesian"], ["ga","Irish"], ["it","Italian"],
["ja","Japanese"], ["ko","Korean"], ["lv","Latvian"], ["lt","Lithuanian"],
["mk","Macedonian"], ["ms","Malay"], ["mt","Maltese"], ["no","Norwegian"],
["fa","Persian"], ["pl","Polish"], ["pt","Portuguese"], ["ro","Romanian"],
["ru","Russian"], ["sr","Serbian"], ["sk","Slovak"], ["sl","Slovenian"],
["es","Spanish"], ["sw","Swahili"], ["sv","Swedish"], ["th","Thai"],
["tr","Turkish"], ["uk","Ukrainian"], ["ur","Urdu"], ["vi","Vietnamese"],
["cy","Welsh"], ["yi","Yiddish"]
]);
// 6. Detect current language
let currentLang = getCookie("googtrans")?.split("/").pop() || "en";
// 7. Show language-specific content & set up language switch
document.addEventListener("DOMContentLoaded", function () {
const readableLang = languageMap.get(currentLang);
const langClass = `.languagespecific.${readableLang?.toLowerCase()}specific`;
const fallbackClass = `.languagespecific.englishspecific`;
if (document.querySelector(langClass)) {
document.querySelectorAll(langClass).forEach(el => el.style.display = 'block');
} else {
document.querySelectorAll(fallbackClass).forEach(el => el.style.display = 'block');
}
document.querySelectorAll('[data-ms-code-lang-select]').forEach(el => {
el.addEventListener('click', function (e) {
e.preventDefault();
const selectedLang = this.getAttribute('data-ms-code-lang');
if (selectedLang === 'en') {
document.cookie = "googtrans=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
document.cookie = "googtrans=;domain=.webflow.io;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
window.location.hash = "";
setTimeout(() => location.reload(true), 100);
} else {
const combo = document.querySelector('.goog-te-combo');
if (combo) combo.value = selectedLang;
window.location.hash = "#googtrans(en|" + selectedLang + ")";
location.reload();
}
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #153 v1.0 💙 - FREE MULTILINGUAL SITE WITH GOOGLE TRANSLATE -->
<script>
// 1. Inject Google Translate script dynamically
const gtScript = document.createElement('script');
gtScript.src = "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
document.head.appendChild(gtScript);
// 2. Inject CSS to hide Google Translate UI
const style = document.createElement('style');
style.innerHTML = `
body { top: 0px !important; position: static !important; }
.goog-te-banner-frame, .skiptranslate,
#goog-gt-tt, .goog-te-balloon-frame,
.goog-text-highlight {
display: none !important;
background: none !important;
box-shadow: none !important;
}
`;
document.head.appendChild(style);
// 3. Google Translate init
window.googleTranslateElementInit = function () {
new google.translate.TranslateElement({
pageLanguage: 'en',
layout: google.translate.TranslateElement.FloatPosition.TOP_LEFT
}, 'google_translate_element');
};
// 4. Helper to get cookies
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [key, value] = cookie.trim().split('=');
if (key === name) return decodeURIComponent(value);
}
return null;
}
// 5. Language map
const languageMap = new Map([
["af","Afrikaans"], ["sq","Albanian"], ["ar","Arabic"], ["hy","Armenian"],
["az","Azerbaijani"], ["eu","Basque"], ["be","Belarusian"], ["bg","Bulgarian"],
["ca","Catalan"], ["zh-CN","ChineseSimplified"], ["zh-TW","ChineseTraditional"],
["hr","Croatian"], ["cs","Czech"], ["da","Danish"], ["nl","Dutch"], ["de","German"],
["en","English"], ["et","Estonian"], ["tl","Filipino"], ["fi","Finnish"],
["fr","French"], ["gl","Galician"], ["ka","Georgian"], ["el","Greek"],
["ht","Haitian"], ["iw","Hebrew"], ["hi","Hindi"], ["hu","Hungarian"],
["is","Icelandic"], ["id","Indonesian"], ["ga","Irish"], ["it","Italian"],
["ja","Japanese"], ["ko","Korean"], ["lv","Latvian"], ["lt","Lithuanian"],
["mk","Macedonian"], ["ms","Malay"], ["mt","Maltese"], ["no","Norwegian"],
["fa","Persian"], ["pl","Polish"], ["pt","Portuguese"], ["ro","Romanian"],
["ru","Russian"], ["sr","Serbian"], ["sk","Slovak"], ["sl","Slovenian"],
["es","Spanish"], ["sw","Swahili"], ["sv","Swedish"], ["th","Thai"],
["tr","Turkish"], ["uk","Ukrainian"], ["ur","Urdu"], ["vi","Vietnamese"],
["cy","Welsh"], ["yi","Yiddish"]
]);
// 6. Detect current language
let currentLang = getCookie("googtrans")?.split("/").pop() || "en";
// 7. Show language-specific content & set up language switch
document.addEventListener("DOMContentLoaded", function () {
const readableLang = languageMap.get(currentLang);
const langClass = `.languagespecific.${readableLang?.toLowerCase()}specific`;
const fallbackClass = `.languagespecific.englishspecific`;
if (document.querySelector(langClass)) {
document.querySelectorAll(langClass).forEach(el => el.style.display = 'block');
} else {
document.querySelectorAll(fallbackClass).forEach(el => el.style.display = 'block');
}
document.querySelectorAll('[data-ms-code-lang-select]').forEach(el => {
el.addEventListener('click', function (e) {
e.preventDefault();
const selectedLang = this.getAttribute('data-ms-code-lang');
if (selectedLang === 'en') {
document.cookie = "googtrans=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
document.cookie = "googtrans=;domain=.webflow.io;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;";
window.location.hash = "";
setTimeout(() => location.reload(true), 100);
} else {
const combo = document.querySelector('.goog-te-combo');
if (combo) combo.value = selectedLang;
window.location.hash = "#googtrans(en|" + selectedLang + ")";
location.reload();
}
});
});
});
</script>

#152 - OTP Verification via WhatsApp in Webflow
Verify phone numbers via WhatsApp before allowing form submissions in Webflow.
<!-- 💙 MEMBERSCRIPT #152 v1.0 💙 - OTP VERIFICATION VIA WHATSAPP IN WEBFLOW -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const phoneInput = document.querySelector('[data-ms-code="phone-number"]');
const form = phoneInput?.closest("form");
const submitBtn = form?.querySelector('[type="submit"]');
let isVerified = false;
let originalMsFormValue = "signup"; // update this if your form uses another Memberstack action
if (phoneInput && form && submitBtn && window.$WhatsAuthForm) {
// Temporarily disable Memberstack
form.removeAttribute("data-ms-form");
// Create error message
const errorMsg = document.createElement('div');
errorMsg.style.color = 'red';
errorMsg.style.marginTop = '8px';
errorMsg.style.display = 'none';
errorMsg.textContent = '⚠️ Please verify your phone number via WhatsApp.';
phoneInput.parentNode.appendChild(errorMsg);
// Initially disable submit
submitBtn.disabled = true;
submitBtn.style.opacity = 0.6;
submitBtn.style.cursor = 'not-allowed';
// Init WhatsAuth
window.$WhatsAuthForm.init({
inputSelector: '[data-ms-code="phone-number"]',
apiKey: "k07Zj8EwdAIzHzLcLPQh-5jCuREbSKXG", //REPLACE WITH YOUR API KEY
placeholder: phoneInput.getAttribute("data-ms-placeholder") || "",
primaryColor: phoneInput.getAttribute("data-ms-primary-color") || "",
secondaryColor: phoneInput.getAttribute("data-ms-secondary-color") || "",
btnText: phoneInput.getAttribute("data-ms-btn-text") || ""
});
// Watch for success class from WhatsAuth
const observer = new MutationObserver(() => {
if (phoneInput.classList.contains("whatsauth-success") && !isVerified) {
isVerified = true;
// Enable submit
submitBtn.disabled = false;
submitBtn.style.opacity = 1;
submitBtn.style.cursor = 'pointer';
// Hide error
errorMsg.style.display = 'none';
// Re-enable Memberstack
form.setAttribute("data-ms-form", originalMsFormValue);
}
});
observer.observe(phoneInput, { attributes: true, attributeFilter: ["class"] });
// Final form safeguard
form.addEventListener("submit", function (e) {
if (!isVerified) {
e.preventDefault();
errorMsg.style.display = 'block';
}
});
submitBtn.addEventListener("click", function (e) {
if (!isVerified) {
e.preventDefault();
errorMsg.style.display = 'block';
}
});
}
});
</script>
<!-- 💙 MEMBERSCRIPT #152 v1.0 💙 - OTP VERIFICATION VIA WHATSAPP IN WEBFLOW -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const phoneInput = document.querySelector('[data-ms-code="phone-number"]');
const form = phoneInput?.closest("form");
const submitBtn = form?.querySelector('[type="submit"]');
let isVerified = false;
let originalMsFormValue = "signup"; // update this if your form uses another Memberstack action
if (phoneInput && form && submitBtn && window.$WhatsAuthForm) {
// Temporarily disable Memberstack
form.removeAttribute("data-ms-form");
// Create error message
const errorMsg = document.createElement('div');
errorMsg.style.color = 'red';
errorMsg.style.marginTop = '8px';
errorMsg.style.display = 'none';
errorMsg.textContent = '⚠️ Please verify your phone number via WhatsApp.';
phoneInput.parentNode.appendChild(errorMsg);
// Initially disable submit
submitBtn.disabled = true;
submitBtn.style.opacity = 0.6;
submitBtn.style.cursor = 'not-allowed';
// Init WhatsAuth
window.$WhatsAuthForm.init({
inputSelector: '[data-ms-code="phone-number"]',
apiKey: "k07Zj8EwdAIzHzLcLPQh-5jCuREbSKXG", //REPLACE WITH YOUR API KEY
placeholder: phoneInput.getAttribute("data-ms-placeholder") || "",
primaryColor: phoneInput.getAttribute("data-ms-primary-color") || "",
secondaryColor: phoneInput.getAttribute("data-ms-secondary-color") || "",
btnText: phoneInput.getAttribute("data-ms-btn-text") || ""
});
// Watch for success class from WhatsAuth
const observer = new MutationObserver(() => {
if (phoneInput.classList.contains("whatsauth-success") && !isVerified) {
isVerified = true;
// Enable submit
submitBtn.disabled = false;
submitBtn.style.opacity = 1;
submitBtn.style.cursor = 'pointer';
// Hide error
errorMsg.style.display = 'none';
// Re-enable Memberstack
form.setAttribute("data-ms-form", originalMsFormValue);
}
});
observer.observe(phoneInput, { attributes: true, attributeFilter: ["class"] });
// Final form safeguard
form.addEventListener("submit", function (e) {
if (!isVerified) {
e.preventDefault();
errorMsg.style.display = 'block';
}
});
submitBtn.addEventListener("click", function (e) {
if (!isVerified) {
e.preventDefault();
errorMsg.style.display = 'block';
}
});
}
});
</script>

#151 - Onboarding Tour For New Members
Launch a step-by-step product tour the first time a member logs in. Uses Memberstack’s JS API + Intro.js
<!-- 💙 MEMBERSCRIPT #151 v0.1 💙 - ONBOARDING TOUR FOR NEW MEMBERS -->
<script>
// 1. Wait for Memberstack v2 DOM
function ready(fn) {
if (window.$memberstackReady) return fn();
document.addEventListener("memberstack.ready", fn);
}
// 2. Collect all steps from the DOM
function collectSteps() {
// all elements with ms-code-step, in a NodeList
var els = document.querySelectorAll("[ms-code-step]");
// build an array of { order, element, intro }
var steps = Array.prototype.map.call(els, function(el) {
return {
order: parseInt(el.getAttribute("ms-code-step"), 10),
element: el,
intro: el.getAttribute("ms-code-intro") || ""
};
});
// sort by order ascending
return steps.sort(function(a, b) {
return a.order - b.order;
}).map(function(s) {
return { element: s.element, intro: s.intro };
});
}
// 3. Kick off the tour for first-time members
function launchTour(member) {
if (!member || !member.id) return; // only for logged-in
if (localStorage.getItem("ms-code-tour-shown")) return;
// Build steps dynamically
var options = {
steps: collectSteps(),
showProgress: true,
exitOnOverlayClick: false
};
introJs().setOptions(options).start();
localStorage.setItem("ms-code-tour-shown", "true");
}
// 4. Glue it together
ready(function() {
window.$memberstackDom
.getCurrentMember()
.then(function(res) {
launchTour(res.data);
})
.catch(function(err) {
console.error("MS-Code-Tour error:", err);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #151 v0.1 💙 - ONBOARDING TOUR FOR NEW MEMBERS -->
<script>
// 1. Wait for Memberstack v2 DOM
function ready(fn) {
if (window.$memberstackReady) return fn();
document.addEventListener("memberstack.ready", fn);
}
// 2. Collect all steps from the DOM
function collectSteps() {
// all elements with ms-code-step, in a NodeList
var els = document.querySelectorAll("[ms-code-step]");
// build an array of { order, element, intro }
var steps = Array.prototype.map.call(els, function(el) {
return {
order: parseInt(el.getAttribute("ms-code-step"), 10),
element: el,
intro: el.getAttribute("ms-code-intro") || ""
};
});
// sort by order ascending
return steps.sort(function(a, b) {
return a.order - b.order;
}).map(function(s) {
return { element: s.element, intro: s.intro };
});
}
// 3. Kick off the tour for first-time members
function launchTour(member) {
if (!member || !member.id) return; // only for logged-in
if (localStorage.getItem("ms-code-tour-shown")) return;
// Build steps dynamically
var options = {
steps: collectSteps(),
showProgress: true,
exitOnOverlayClick: false
};
introJs().setOptions(options).start();
localStorage.setItem("ms-code-tour-shown", "true");
}
// 4. Glue it together
ready(function() {
window.$memberstackDom
.getCurrentMember()
.then(function(res) {
launchTour(res.data);
})
.catch(function(err) {
console.error("MS-Code-Tour error:", err);
});
});
</script>

#150 - Save and Unsave Items to Your Collection (Pinterest-style)
A simple save/unsave system that lets members bookmark items into personal collections.
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 1 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const member = await ms.getCurrentMember();
const isLoggedIn = !!member;
let savedItems = {};
const fetchSavedItems = async () => {
try {
const { data } = await ms.getMemberJSON();
savedItems = data.savedItems || {};
} catch {
savedItems = {};
}
};
const persistSavedItems = async () => {
try {
await ms.updateMemberJSON({ json: { savedItems } });
} catch (err) {
console.error("Error saving items:", err);
}
};
const updateButtons = () => {
document.querySelectorAll('[ms-code-add-button]').forEach(btn => {
const id = btn.getAttribute('ms-code-save');
const category = btn.getAttribute('ms-code-category');
const exists = savedItems[category]?.some(i => i.id === id);
btn.style.display = exists ? 'none' : 'inline-block';
});
document.querySelectorAll('[ms-code-unsave-button]').forEach(btn => {
const id = btn.getAttribute('ms-code-unsave');
const category = btn.getAttribute('ms-code-category');
const exists = savedItems[category]?.some(i => i.id === id);
btn.style.display = exists ? 'inline-block' : 'none';
});
};
const onAddClick = async (e) => {
e.preventDefault();
if (!isLoggedIn) return;
const btn = e.currentTarget;
const container = btn.closest('[ms-code-save-item]');
const id = btn.getAttribute('ms-code-save');
const category = btn.getAttribute('ms-code-category');
const img = container?.querySelector('[ms-code-image]');
const url = img?.src;
if (!savedItems[category]) savedItems[category] = [];
if (!savedItems[category].some(i => i.id === id)) {
savedItems[category].push({ id, url });
updateButtons();
await persistSavedItems();
}
};
const onUnsaveClick = async (e) => {
e.preventDefault();
if (!isLoggedIn) return;
const btn = e.currentTarget;
const id = btn.getAttribute('ms-code-unsave');
const category = btn.getAttribute('ms-code-category');
if (savedItems[category]) {
savedItems[category] = savedItems[category].filter(i => i.id !== id);
if (savedItems[category].length === 0) delete savedItems[category];
updateButtons();
await persistSavedItems();
}
};
const onDownloadClick = (e) => {
e.preventDefault();
const btn = e.currentTarget;
const container = btn.closest('[ms-code-save-item]');
const img = container?.querySelector('[ms-code-image]');
const url = img?.src;
if (url) {
const a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
a.click();
a.remove();
}
};
const attachListeners = () => {
document.querySelectorAll('[ms-code-add-button]').forEach(b => b.addEventListener('click', onAddClick));
document.querySelectorAll('[ms-code-unsave-button]').forEach(b => b.addEventListener('click', onUnsaveClick));
document.querySelectorAll('[ms-code-download-button]').forEach(b => b.addEventListener('click', onDownloadClick));
};
await fetchSavedItems();
updateButtons();
attachListeners();
});
</script>
<!-- GENERATE PINTEREST GRID STYLE -->
<script>
$(document).ready(function () {
setTimeout(function() {
function resizeGridItem(item) {
grid = document.getElementsByClassName("grid")[0];
rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap'));
rowSpan = Math.ceil((item.querySelector('.content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));
item.style.gridRowEnd = "span " + rowSpan;
}
function resizeAllGridItems() {
allItems = document.getElementsByClassName("item");
for (x = 0; x < allItems.length; x++) {
resizeGridItem(allItems[x]);
}
}
function resizeInstance(instance) {
item = instance.elements[0];
resizeGridItem(item);
}
window.onload = resizeAllGridItems();
window.addEventListener("resize", resizeAllGridItems);
allItems = document.getElementsByClassName("item");
for (x = 0; x < allItems.length; x++) {
imagesLoaded(allItems[x], resizeInstance);
}
setTimeout(function() { resizeInstance() }, 100);
}, 800);
})
</script>
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 2 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const wrapper = document.querySelector('[ms-code-collections-wrapper]');
const template = document.querySelector('[ms-code-folder-template]') || document.querySelector('[ms-code-folder]');
const emptyState = document.querySelector('[ms-code-empty]');
if (!wrapper || !template) return;
let member;
try {
member = await ms.getCurrentMember();
} catch {
wrapper.textContent = "Please log in to view your collections.";
return;
}
let savedItems = {};
try {
const { data } = await ms.getMemberJSON();
savedItems = data?.savedItems || {};
} catch {
wrapper.textContent = "Could not load your collections.";
return;
}
if (Object.keys(savedItems).length === 0) {
wrapper.innerHTML = '';
if (emptyState) emptyState.style.display = 'block';
return;
}
if (emptyState) emptyState.style.display = 'none';
wrapper.innerHTML = '';
const persistSavedItems = async () => {
try {
await ms.updateMemberJSON({ json: { savedItems } });
} catch (err) {
console.error("Failed to save", err);
}
};
const updateButtons = (modal, id, category) => {
const addBtn = modal.querySelector('[ms-code-add-button]');
const unsaveBtn = modal.querySelector('[ms-code-unsave-button]');
const exists = savedItems[category]?.some(item => item.id === id);
addBtn.style.display = exists ? 'none' : 'inline-block';
unsaveBtn.style.display = exists ? 'inline-block' : 'none';
};
Object.entries(savedItems).forEach(([category, items]) => {
const folderClone = template.cloneNode(true);
const titleEl = folderClone.querySelector('[ms-code-folder-title]');
if (titleEl) titleEl.textContent = `${category} (${items.length})`;
const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
if (imageTemplate) imageTemplate.style.display = 'none';
const modal = folderClone.querySelector('[ms-code-modal]');
const modalImg = folderClone.querySelector('[ms-code-modal-img]');
const modalClose = folderClone.querySelector('[ms-code-modal-close]');
const addButton = folderClone.querySelector('[ms-code-add-button]');
const unsaveButton = folderClone.querySelector('[ms-code-unsave-button]');
const downloadButton = folderClone.querySelector('[ms-code-download-button]');
const hiddenImage = folderClone.querySelector('[ms-code-image]');
items.forEach(item => {
const imgClone = imageTemplate.cloneNode(true);
imgClone.src = item.url;
imgClone.alt = category;
imgClone.style.display = 'block';
imgClone.style.objectFit = 'cover';
imgClone.style.width = '100%';
imgClone.style.height = 'auto';
imgClone.style.maxWidth = '100%';
imgClone.addEventListener('click', () => {
if (modal && modalImg) {
modalImg.src = item.url;
if (hiddenImage) hiddenImage.src = item.url;
const id = item.id;
addButton.onclick = async (e) => {
e.preventDefault();
savedItems[category] = savedItems[category] || [];
if (!savedItems[category].some(i => i.id === id)) {
savedItems[category].push({ id, url: item.url });
await persistSavedItems();
updateButtons(modal, id, category);
}
};
unsaveButton.onclick = async (e) => {
e.preventDefault();
savedItems[category] = savedItems[category].filter(i => i.id !== id);
if (savedItems[category].length === 0) delete savedItems[category];
await persistSavedItems();
modal.style.display = 'none';
location.reload();
};
downloadButton.onclick = (e) => {
e.preventDefault();
const a = document.createElement('a');
a.href = item.url;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
updateButtons(modal, id, category);
}
modal.style.display = 'flex';
});
imageContainer.appendChild(imgClone);
});
if (modal && modalClose) {
modalClose.addEventListener('click', () => {
modal.style.display = 'none';
if (modalImg) modalImg.src = '';
});
}
wrapper.appendChild(folderClone);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 1 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const member = await ms.getCurrentMember();
const isLoggedIn = !!member;
let savedItems = {};
const fetchSavedItems = async () => {
try {
const { data } = await ms.getMemberJSON();
savedItems = data.savedItems || {};
} catch {
savedItems = {};
}
};
const persistSavedItems = async () => {
try {
await ms.updateMemberJSON({ json: { savedItems } });
} catch (err) {
console.error("Error saving items:", err);
}
};
const updateButtons = () => {
document.querySelectorAll('[ms-code-add-button]').forEach(btn => {
const id = btn.getAttribute('ms-code-save');
const category = btn.getAttribute('ms-code-category');
const exists = savedItems[category]?.some(i => i.id === id);
btn.style.display = exists ? 'none' : 'inline-block';
});
document.querySelectorAll('[ms-code-unsave-button]').forEach(btn => {
const id = btn.getAttribute('ms-code-unsave');
const category = btn.getAttribute('ms-code-category');
const exists = savedItems[category]?.some(i => i.id === id);
btn.style.display = exists ? 'inline-block' : 'none';
});
};
const onAddClick = async (e) => {
e.preventDefault();
if (!isLoggedIn) return;
const btn = e.currentTarget;
const container = btn.closest('[ms-code-save-item]');
const id = btn.getAttribute('ms-code-save');
const category = btn.getAttribute('ms-code-category');
const img = container?.querySelector('[ms-code-image]');
const url = img?.src;
if (!savedItems[category]) savedItems[category] = [];
if (!savedItems[category].some(i => i.id === id)) {
savedItems[category].push({ id, url });
updateButtons();
await persistSavedItems();
}
};
const onUnsaveClick = async (e) => {
e.preventDefault();
if (!isLoggedIn) return;
const btn = e.currentTarget;
const id = btn.getAttribute('ms-code-unsave');
const category = btn.getAttribute('ms-code-category');
if (savedItems[category]) {
savedItems[category] = savedItems[category].filter(i => i.id !== id);
if (savedItems[category].length === 0) delete savedItems[category];
updateButtons();
await persistSavedItems();
}
};
const onDownloadClick = (e) => {
e.preventDefault();
const btn = e.currentTarget;
const container = btn.closest('[ms-code-save-item]');
const img = container?.querySelector('[ms-code-image]');
const url = img?.src;
if (url) {
const a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a);
a.click();
a.remove();
}
};
const attachListeners = () => {
document.querySelectorAll('[ms-code-add-button]').forEach(b => b.addEventListener('click', onAddClick));
document.querySelectorAll('[ms-code-unsave-button]').forEach(b => b.addEventListener('click', onUnsaveClick));
document.querySelectorAll('[ms-code-download-button]').forEach(b => b.addEventListener('click', onDownloadClick));
};
await fetchSavedItems();
updateButtons();
attachListeners();
});
</script>
<!-- GENERATE PINTEREST GRID STYLE -->
<script>
$(document).ready(function () {
setTimeout(function() {
function resizeGridItem(item) {
grid = document.getElementsByClassName("grid")[0];
rowHeight = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-auto-rows'));
rowGap = parseInt(window.getComputedStyle(grid).getPropertyValue('grid-row-gap'));
rowSpan = Math.ceil((item.querySelector('.content').getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));
item.style.gridRowEnd = "span " + rowSpan;
}
function resizeAllGridItems() {
allItems = document.getElementsByClassName("item");
for (x = 0; x < allItems.length; x++) {
resizeGridItem(allItems[x]);
}
}
function resizeInstance(instance) {
item = instance.elements[0];
resizeGridItem(item);
}
window.onload = resizeAllGridItems();
window.addEventListener("resize", resizeAllGridItems);
allItems = document.getElementsByClassName("item");
for (x = 0; x < allItems.length; x++) {
imagesLoaded(allItems[x], resizeInstance);
}
setTimeout(function() { resizeInstance() }, 100);
}, 800);
})
</script>
<!-- 💙 MEMBERSCRIPT #150 v0.1 💙 - SAVE AND UNSAVE ITEMS TO YOUR COLLECTION PART 2 -->
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ms = window.$memberstackDom;
const wrapper = document.querySelector('[ms-code-collections-wrapper]');
const template = document.querySelector('[ms-code-folder-template]') || document.querySelector('[ms-code-folder]');
const emptyState = document.querySelector('[ms-code-empty]');
if (!wrapper || !template) return;
let member;
try {
member = await ms.getCurrentMember();
} catch {
wrapper.textContent = "Please log in to view your collections.";
return;
}
let savedItems = {};
try {
const { data } = await ms.getMemberJSON();
savedItems = data?.savedItems || {};
} catch {
wrapper.textContent = "Could not load your collections.";
return;
}
if (Object.keys(savedItems).length === 0) {
wrapper.innerHTML = '';
if (emptyState) emptyState.style.display = 'block';
return;
}
if (emptyState) emptyState.style.display = 'none';
wrapper.innerHTML = '';
const persistSavedItems = async () => {
try {
await ms.updateMemberJSON({ json: { savedItems } });
} catch (err) {
console.error("Failed to save", err);
}
};
const updateButtons = (modal, id, category) => {
const addBtn = modal.querySelector('[ms-code-add-button]');
const unsaveBtn = modal.querySelector('[ms-code-unsave-button]');
const exists = savedItems[category]?.some(item => item.id === id);
addBtn.style.display = exists ? 'none' : 'inline-block';
unsaveBtn.style.display = exists ? 'inline-block' : 'none';
};
Object.entries(savedItems).forEach(([category, items]) => {
const folderClone = template.cloneNode(true);
const titleEl = folderClone.querySelector('[ms-code-folder-title]');
if (titleEl) titleEl.textContent = `${category} (${items.length})`;
const imageContainer = folderClone.querySelector('[ms-code-folder-items]');
const imageTemplate = folderClone.querySelector('[ms-code-folder-image]');
if (imageTemplate) imageTemplate.style.display = 'none';
const modal = folderClone.querySelector('[ms-code-modal]');
const modalImg = folderClone.querySelector('[ms-code-modal-img]');
const modalClose = folderClone.querySelector('[ms-code-modal-close]');
const addButton = folderClone.querySelector('[ms-code-add-button]');
const unsaveButton = folderClone.querySelector('[ms-code-unsave-button]');
const downloadButton = folderClone.querySelector('[ms-code-download-button]');
const hiddenImage = folderClone.querySelector('[ms-code-image]');
items.forEach(item => {
const imgClone = imageTemplate.cloneNode(true);
imgClone.src = item.url;
imgClone.alt = category;
imgClone.style.display = 'block';
imgClone.style.objectFit = 'cover';
imgClone.style.width = '100%';
imgClone.style.height = 'auto';
imgClone.style.maxWidth = '100%';
imgClone.addEventListener('click', () => {
if (modal && modalImg) {
modalImg.src = item.url;
if (hiddenImage) hiddenImage.src = item.url;
const id = item.id;
addButton.onclick = async (e) => {
e.preventDefault();
savedItems[category] = savedItems[category] || [];
if (!savedItems[category].some(i => i.id === id)) {
savedItems[category].push({ id, url: item.url });
await persistSavedItems();
updateButtons(modal, id, category);
}
};
unsaveButton.onclick = async (e) => {
e.preventDefault();
savedItems[category] = savedItems[category].filter(i => i.id !== id);
if (savedItems[category].length === 0) delete savedItems[category];
await persistSavedItems();
modal.style.display = 'none';
location.reload();
};
downloadButton.onclick = (e) => {
e.preventDefault();
const a = document.createElement('a');
a.href = item.url;
a.download = '';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
updateButtons(modal, id, category);
}
modal.style.display = 'flex';
});
imageContainer.appendChild(imgClone);
});
if (modal && modalClose) {
modalClose.addEventListener('click', () => {
modal.style.display = 'none';
if (modalImg) modalImg.src = '';
});
}
wrapper.appendChild(folderClone);
});
});
</script>

#149 - Favicon for Dark/Light Mode
Use this script to update your website's favicon based on the user's system color scheme preference.
<!-- 💙 MEMBERSCRIPT #149 v0.1 💙 - FAVICON FOR DARK/LIGHT MODE -->
<script>
// Helper: Retrieve or create a favicon element
function getFaviconElement() {
let favicon = document.querySelector('link[rel="icon"]') ||
document.querySelector('link[rel="shortcut icon"]');
if (!favicon) {
favicon = document.createElement('link');
favicon.rel = 'icon';
document.head.appendChild(favicon);
}
return favicon;
}
// Function to update the favicon based on dark mode
function updateFavicon(e) {
const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
const favicon = getFaviconElement();
// Update these paths to your favicon assets in Webflow’s Asset Manager or a CDN
favicon.href = darkModeOn
? 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f85b2a9f281a373ca_Dark%20Mode%20Logo.png'
: 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f1c2fa3cebee1b150_Light%20Mode%20Logo.png';
}
// Initialize the favicon update on page load
updateFavicon();
// Listen for changes in the dark mode media query
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (typeof darkModeMediaQuery.addEventListener === 'function') {
darkModeMediaQuery.addEventListener('change', updateFavicon);
} else if (typeof darkModeMediaQuery.addListener === 'function') {
darkModeMediaQuery.addListener(updateFavicon);
}
</script>
<!-- 💙 MEMBERSCRIPT #149 v0.1 💙 - FAVICON FOR DARK/LIGHT MODE -->
<script>
// Helper: Retrieve or create a favicon element
function getFaviconElement() {
let favicon = document.querySelector('link[rel="icon"]') ||
document.querySelector('link[rel="shortcut icon"]');
if (!favicon) {
favicon = document.createElement('link');
favicon.rel = 'icon';
document.head.appendChild(favicon);
}
return favicon;
}
// Function to update the favicon based on dark mode
function updateFavicon(e) {
const darkModeOn = e ? e.matches : window.matchMedia('(prefers-color-scheme: dark)').matches;
const favicon = getFaviconElement();
// Update these paths to your favicon assets in Webflow’s Asset Manager or a CDN
favicon.href = darkModeOn
? 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f85b2a9f281a373ca_Dark%20Mode%20Logo.png'
: 'https://cdn.prod.website-files.com/67fcff014042c2f5945437c0/67fd000f1c2fa3cebee1b150_Light%20Mode%20Logo.png';
}
// Initialize the favicon update on page load
updateFavicon();
// Listen for changes in the dark mode media query
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (typeof darkModeMediaQuery.addEventListener === 'function') {
darkModeMediaQuery.addEventListener('change', updateFavicon);
} else if (typeof darkModeMediaQuery.addListener === 'function') {
darkModeMediaQuery.addListener(updateFavicon);
}
</script>

#148 - Disable Webflow Form Success Window
Use this script to override Webflow's default form submission behavior by hiding the success message.
<!-- 💙 MEMBERSCRIPT #148 v0.1 💙 - DISABLE WEBFLOW FORM SUCCESS WINDOW -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector('[ms-code-form="form"]');
const successEl = document.querySelector('[ms-code-success="true"]');
if (!form || !successEl) return;
const observer = new MutationObserver(() => {
const isVisible = window.getComputedStyle(successEl).display !== 'none';
if (isVisible) {
successEl.style.display = 'none';
form.style.display = 'block';
}
});
observer.observe(successEl, { attributes: true, attributeFilter: ['style'] });
// Cleanup observer when done (optional, for performance)
form.addEventListener('w-form-success', () => {
setTimeout(() => {
observer.disconnect();
}, 100);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #148 v0.1 💙 - DISABLE WEBFLOW FORM SUCCESS WINDOW -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector('[ms-code-form="form"]');
const successEl = document.querySelector('[ms-code-success="true"]');
if (!form || !successEl) return;
const observer = new MutationObserver(() => {
const isVisible = window.getComputedStyle(successEl).display !== 'none';
if (isVisible) {
successEl.style.display = 'none';
form.style.display = 'block';
}
});
observer.observe(successEl, { attributes: true, attributeFilter: ['style'] });
// Cleanup observer when done (optional, for performance)
form.addEventListener('w-form-success', () => {
setTimeout(() => {
observer.disconnect();
}, 100);
});
});
</script>

#147 - Age Verification Popup with Cookies in Webflow
Use this script to add an age verification popup to your Webflow site.
<!-- 💙 MEMBERSCRIPT #147 v0.1 💙 - AGE VERIFICATION POPUP WITH COOKIES -->
<script>
// Simple cookie helper functions
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
}
function getCookie(name) {
const cname = name + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const cookies = decodedCookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let c = cookies[i].trim();
if (c.indexOf(cname) === 0) {
return c.substring(cname.length, c.length);
}
}
return "";
}
document.addEventListener('DOMContentLoaded', function() {
// Select the age gate element via ms-code attribute
const ageGateEl = document.querySelector('[ms-code-agegate]');
// On page load: if the cookie exists, hide the age gate
if (getCookie('ms-code-ageVerified') === 'true') {
if (ageGateEl) ageGateEl.style.display = 'none';
}
// Listen for clicks on elements with ms-code-click
document.addEventListener('click', function(event) {
if (event.target.closest('[ms-code-click="confirmAge"]')) {
setCookie('ms-code-ageVerified', 'true', 30);
if (ageGateEl) ageGateEl.style.display = 'none';
} else if (event.target.closest('[ms-code-click="denyAge"]')) {
window.location.href = 'https://age-verification-popup-with-cookies.webflow.io/access-denied'; // Change URL as needed
}
});
});
</script>
<!-- 💙 MEMBERSCRIPT #147 v0.1 💙 - AGE VERIFICATION POPUP WITH COOKIES -->
<script>
// Simple cookie helper functions
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
}
function getCookie(name) {
const cname = name + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const cookies = decodedCookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let c = cookies[i].trim();
if (c.indexOf(cname) === 0) {
return c.substring(cname.length, c.length);
}
}
return "";
}
document.addEventListener('DOMContentLoaded', function() {
// Select the age gate element via ms-code attribute
const ageGateEl = document.querySelector('[ms-code-agegate]');
// On page load: if the cookie exists, hide the age gate
if (getCookie('ms-code-ageVerified') === 'true') {
if (ageGateEl) ageGateEl.style.display = 'none';
}
// Listen for clicks on elements with ms-code-click
document.addEventListener('click', function(event) {
if (event.target.closest('[ms-code-click="confirmAge"]')) {
setCookie('ms-code-ageVerified', 'true', 30);
if (ageGateEl) ageGateEl.style.display = 'none';
} else if (event.target.closest('[ms-code-click="denyAge"]')) {
window.location.href = 'https://age-verification-popup-with-cookies.webflow.io/access-denied'; // Change URL as needed
}
});
});
</script>

#146 - Stop Videos from Playing When A Modal Closes
Automatically stop video playback when closing modals in Webflow.
<!-- 💙 MEMBERSCRIPT #146 v0.1 💙 - STOP VIDEOS FROM PLAYING WHEN A MODAL CLOSES -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Select all modals on the page
var modals = document.querySelectorAll('[data-ms-modal="modal"]');
function initializeModal(modal) {
var iframe = modal.querySelector('iframe');
if (!iframe) return; // If no iframe found, do nothing
var originalSrc = iframe.dataset.src || iframe.src;
// Function to stop video playback
function stopVideo() {
iframe.src = "";
setTimeout(() => {
iframe.src = originalSrc;
}, 100);
}
// Attach event listeners to all close buttons inside the modal
var closeBtns = modal.querySelectorAll('[data-ms-modal="close"]');
closeBtns.forEach(function(closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
stopVideo();
});
});
// Also close the modal when clicking outside the content
modal.addEventListener('click', function(e) {
if (e.target === modal) {
stopVideo();
}
});
}
// Initialize all modals
modals.forEach(initializeModal);
});
</script>
<!-- 💙 MEMBERSCRIPT #146 v0.1 💙 - STOP VIDEOS FROM PLAYING WHEN A MODAL CLOSES -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Select all modals on the page
var modals = document.querySelectorAll('[data-ms-modal="modal"]');
function initializeModal(modal) {
var iframe = modal.querySelector('iframe');
if (!iframe) return; // If no iframe found, do nothing
var originalSrc = iframe.dataset.src || iframe.src;
// Function to stop video playback
function stopVideo() {
iframe.src = "";
setTimeout(() => {
iframe.src = originalSrc;
}, 100);
}
// Attach event listeners to all close buttons inside the modal
var closeBtns = modal.querySelectorAll('[data-ms-modal="close"]');
closeBtns.forEach(function(closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
stopVideo();
});
});
// Also close the modal when clicking outside the content
modal.addEventListener('click', function(e) {
if (e.target === modal) {
stopVideo();
}
});
}
// Initialize all modals
modals.forEach(initializeModal);
});
</script>

#145 - Automatically Save & Prefill Forms
Automatically save and prefill forms in a browsers localStorage upon form submission.
<!-- 💙 MEMBERSCRIPT #145 v0.1 💙 - HOW TO PRE-FILL FORM INPUT FIELDS AT PAGE LOAD -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to store form data in localStorage
function storeFormData() {
const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
fields.forEach(function(field) {
const fieldId = field.getAttribute('ms-code-field-id');
const value = field.value.trim();
if (value) {
localStorage.setItem(fieldId, value);
}
});
}
// Function to pre-fill form fields with stored data
function preFillForm() {
const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
fields.forEach(function(field) {
const fieldId = field.getAttribute('ms-code-field-id');
const storedValue = localStorage.getItem(fieldId);
if (storedValue) {
field.value = storedValue;
}
});
}
// Handle form submission
const form = document.querySelector('#my-form, form[ms-code-form-id]');
if (form) {
form.addEventListener('submit', function(event) {
event.preventDefault(); // Prevent default form submission
storeFormData();
// Refresh the page after storing data
setTimeout(function() {
location.reload();
}, 500); // Short delay to simulate form submission
});
}
// Pre-fill form fields when the page loads
preFillForm();
});
</script>
<!-- 💙 MEMBERSCRIPT #145 v0.1 💙 - HOW TO PRE-FILL FORM INPUT FIELDS AT PAGE LOAD -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to store form data in localStorage
function storeFormData() {
const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
fields.forEach(function(field) {
const fieldId = field.getAttribute('ms-code-field-id');
const value = field.value.trim();
if (value) {
localStorage.setItem(fieldId, value);
}
});
}
// Function to pre-fill form fields with stored data
function preFillForm() {
const fields = document.querySelectorAll('input[ms-code-field-id], textarea[ms-code-field-id], select[ms-code-field-id]');
fields.forEach(function(field) {
const fieldId = field.getAttribute('ms-code-field-id');
const storedValue = localStorage.getItem(fieldId);
if (storedValue) {
field.value = storedValue;
}
});
}
// Handle form submission
const form = document.querySelector('#my-form, form[ms-code-form-id]');
if (form) {
form.addEventListener('submit', function(event) {
event.preventDefault(); // Prevent default form submission
storeFormData();
// Refresh the page after storing data
setTimeout(function() {
location.reload();
}, 500); // Short delay to simulate form submission
});
}
// Pre-fill form fields when the page loads
preFillForm();
});
</script>

#144 - Track Users Login History & Active Users
Automatically track a members login history, keep a login streak and total visits
<!-- 💙 MEMBERSCRIPT #144 v0.1 💙 - TRACK A USERS LOGIN HISTORY & ACTIVE USERS -->
<script>
(function(){
const memberstack = window.$memberstackDom;
// Helper: Execute callback when Memberstack is ready
function onMemberstackReady(cb) {
if (window.$memberstackReady) {
cb();
} else {
document.addEventListener("memberstack.ready", cb);
}
}
async function initTracking() {
// Check if a member is logged in (via localStorage)
const currentUser = JSON.parse(localStorage.getItem("_ms-mem") || "null");
if (!currentUser) {
console.warn("No logged-in member found. Tracking not applied.");
return;
}
// Retrieve member metadata
const memberJson = await memberstack.getMemberJSON();
let metadata = memberJson.data || {};
// Ensure userVisits exists as an array
metadata.userVisits = Array.isArray(metadata.userVisits) ? metadata.userVisits : [];
// Use ISO date (YYYY-MM-DD) to record one visit per day
const today = new Date().toISOString().split("T")[0];
if (!metadata.userVisits.includes(today)) {
metadata.userVisits.push(today);
}
// Helper: Compute consecutive login streak from userVisits
function computeStreak(visits) {
if (!visits.length) return 0;
// Ensure dates are unique and sorted ascending
const uniqueVisits = [...new Set(visits)].sort();
let streak = 1;
let currentDate = new Date(uniqueVisits[uniqueVisits.length - 1]);
for (let i = uniqueVisits.length - 2; i >= 0; i--) {
const prevDate = new Date(uniqueVisits[i]);
const diffDays = Math.floor((currentDate - prevDate) / (1000 * 60 * 60 * 24));
if (diffDays === 1) {
streak++;
currentDate = prevDate;
} else {
break;
}
}
return streak;
}
// Calculate the login streak and total visits
metadata.loginStreak = computeStreak(metadata.userVisits);
metadata.totalVisits = metadata.userVisits.length;
// Update Memberstack metadata
await memberstack.updateMemberJSON({ json: metadata });
console.log("User visits:", metadata.userVisits);
console.log("Login streak:", metadata.loginStreak);
console.log("Total visits:", metadata.totalVisits);
}
onMemberstackReady(initTracking);
})();
</script>
<!-- 💙 MEMBERSCRIPT #144 v0.1 💙 - TRACK A USERS LOGIN HISTORY & ACTIVE USERS -->
<script>
(function(){
const memberstack = window.$memberstackDom;
// Helper: Execute callback when Memberstack is ready
function onMemberstackReady(cb) {
if (window.$memberstackReady) {
cb();
} else {
document.addEventListener("memberstack.ready", cb);
}
}
async function initTracking() {
// Check if a member is logged in (via localStorage)
const currentUser = JSON.parse(localStorage.getItem("_ms-mem") || "null");
if (!currentUser) {
console.warn("No logged-in member found. Tracking not applied.");
return;
}
// Retrieve member metadata
const memberJson = await memberstack.getMemberJSON();
let metadata = memberJson.data || {};
// Ensure userVisits exists as an array
metadata.userVisits = Array.isArray(metadata.userVisits) ? metadata.userVisits : [];
// Use ISO date (YYYY-MM-DD) to record one visit per day
const today = new Date().toISOString().split("T")[0];
if (!metadata.userVisits.includes(today)) {
metadata.userVisits.push(today);
}
// Helper: Compute consecutive login streak from userVisits
function computeStreak(visits) {
if (!visits.length) return 0;
// Ensure dates are unique and sorted ascending
const uniqueVisits = [...new Set(visits)].sort();
let streak = 1;
let currentDate = new Date(uniqueVisits[uniqueVisits.length - 1]);
for (let i = uniqueVisits.length - 2; i >= 0; i--) {
const prevDate = new Date(uniqueVisits[i]);
const diffDays = Math.floor((currentDate - prevDate) / (1000 * 60 * 60 * 24));
if (diffDays === 1) {
streak++;
currentDate = prevDate;
} else {
break;
}
}
return streak;
}
// Calculate the login streak and total visits
metadata.loginStreak = computeStreak(metadata.userVisits);
metadata.totalVisits = metadata.userVisits.length;
// Update Memberstack metadata
await memberstack.updateMemberJSON({ json: metadata });
console.log("User visits:", metadata.userVisits);
console.log("Login streak:", metadata.loginStreak);
console.log("Total visits:", metadata.totalVisits);
}
onMemberstackReady(initTracking);
})();
</script>

#143 - Initial Based Profile Avatar
Generate a custom avatar with initials when a member has no profile picture.
<!-- 💙 MEMBERSCRIPT #143 v0.1 💙 - GENERATE INITIALS BASED AVATAR -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const checkMemberstack = setInterval(() => {
if (window.$memberstackDom) {
clearInterval(checkMemberstack);
window.$memberstackDom.getCurrentMember().then(({ data }) => {
if (!data) return console.log("No member data (logged out)");
const profileImage = document.querySelector('[data-ms-member="profile-image"]');
const avatarWrapper = document.querySelector('[data-ms-code="avatar"]');
const initialsDiv = avatarWrapper?.querySelector('.ms-avatar-initial');
if (data.profileImage) {
profileImage?.style.setProperty("display", "block");
avatarWrapper?.style.setProperty("display", "none");
} else {
profileImage?.style.setProperty("display", "none");
avatarWrapper?.style.setProperty("display", "flex");
// Get initials from available fields
const first = data.customFields["first-name"]?.trim().charAt(0).toUpperCase() || "";
const last = data.customFields["last-name"]?.trim().charAt(0).toUpperCase() || "";
let initials = first + last;
if (!initials) {
const fullName = data.customFields["name"]?.trim().split(" ") || [];
initials = fullName.length > 1
? (fullName[0].charAt(0) + fullName[1].charAt(0)).toUpperCase()
: fullName[0]?.charAt(0).toUpperCase() || "?";
}
if (initialsDiv) {
initialsDiv.textContent = initials;
} else {
avatarWrapper.innerHTML = `${initials}`;
}
}
}).catch(console.error);
}
}, 100);
});
</script>
<!-- 💙 MEMBERSCRIPT #143 v0.1 💙 - GENERATE INITIALS BASED AVATAR -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const checkMemberstack = setInterval(() => {
if (window.$memberstackDom) {
clearInterval(checkMemberstack);
window.$memberstackDom.getCurrentMember().then(({ data }) => {
if (!data) return console.log("No member data (logged out)");
const profileImage = document.querySelector('[data-ms-member="profile-image"]');
const avatarWrapper = document.querySelector('[data-ms-code="avatar"]');
const initialsDiv = avatarWrapper?.querySelector('.ms-avatar-initial');
if (data.profileImage) {
profileImage?.style.setProperty("display", "block");
avatarWrapper?.style.setProperty("display", "none");
} else {
profileImage?.style.setProperty("display", "none");
avatarWrapper?.style.setProperty("display", "flex");
// Get initials from available fields
const first = data.customFields["first-name"]?.trim().charAt(0).toUpperCase() || "";
const last = data.customFields["last-name"]?.trim().charAt(0).toUpperCase() || "";
let initials = first + last;
if (!initials) {
const fullName = data.customFields["name"]?.trim().split(" ") || [];
initials = fullName.length > 1
? (fullName[0].charAt(0) + fullName[1].charAt(0)).toUpperCase()
: fullName[0]?.charAt(0).toUpperCase() || "?";
}
if (initialsDiv) {
initialsDiv.textContent = initials;
} else {
avatarWrapper.innerHTML = `${initials}`;
}
}
}).catch(console.error);
}
}, 100);
});
</script>

#142 - Embed PDFs For Webflow
Easily embed a PDF on your Webflow site - for free, without any custom code.
<!-- 💙 MEMBERSCRIPT #142 v0.1 💙 - EMBED PDFS IN WEBFLOW -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const pdfElements = document.querySelectorAll('[ms-code-pdf-src]');
pdfElements.forEach(function (element) {
const src = element.getAttribute('ms-code-pdf-src');
const height = element.getAttribute('ms-code-pdf-height') || '500px';
const iframe = document.createElement('iframe');
iframe.src = src;
iframe.style.width = '100%';
iframe.style.height = height;
iframe.style.border = 'none';
// Set the iframe to block to remove any inline element gaps
iframe.style.display = 'block';
iframe.setAttribute('scrolling', 'auto');
element.innerHTML = '';
element.appendChild(iframe);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #142 v0.1 💙 - EMBED PDFS IN WEBFLOW -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const pdfElements = document.querySelectorAll('[ms-code-pdf-src]');
pdfElements.forEach(function (element) {
const src = element.getAttribute('ms-code-pdf-src');
const height = element.getAttribute('ms-code-pdf-height') || '500px';
const iframe = document.createElement('iframe');
iframe.src = src;
iframe.style.width = '100%';
iframe.style.height = height;
iframe.style.border = 'none';
// Set the iframe to block to remove any inline element gaps
iframe.style.display = 'block';
iframe.setAttribute('scrolling', 'auto');
element.innerHTML = '';
element.appendChild(iframe);
});
});
</script>

#141 - Iniciar la inserción en YouTube a una hora específica
Habilita enlaces compartibles y comienza a reproducir vídeos a una hora determinada.
<!-- 💙 MEMBERSCRIPT #141 v0.1 💙 - START YOUTUBE VIDEO AT SPECIFIC TIME -->
<script>
(function() {
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^]*)');
var results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
// Function to update YouTube embed src within Embedly iframe
function updateYouTubeEmbed(embedly_iframe, startTime) {
var embedly_src = embedly_iframe.src;
var youtube_src_match = embedly_src.match(/src=([^&]+)/);
if (youtube_src_match) {
var youtube_src = decodeURIComponent(youtube_src_match[1]);
var new_youtube_src = youtube_src.replace(/(\?|&)start=\d+/, '');
new_youtube_src += (new_youtube_src.includes('?') ? '&' : '?') + 'start=' + startTime;
var new_embedly_src = embedly_src.replace(/src=([^&]+)/, 'src=' + encodeURIComponent(new_youtube_src));
embedly_iframe.src = new_embedly_src;
}
}
// Get all elements with ms-code-yt-start attribute
var elements = document.querySelectorAll('[ms-code-yt-start]');
elements.forEach(function(element) {
var paramName = element.getAttribute('ms-code-yt-start');
var startTime = getUrlParameter(paramName);
var defaultStartTime = element.getAttribute('ms-code-yt-start-default');
// If no URL parameter, use the default start time (if specified)
if (!startTime && defaultStartTime) {
startTime = defaultStartTime;
}
// If we have a start time (either from URL or default), update the embed
if (startTime) {
var iframe = element.querySelector('iframe.embedly-embed');
if (iframe) {
updateYouTubeEmbed(iframe, startTime);
}
}
});
})();
</script>
<!-- 💙 MEMBERSCRIPT #141 v0.1 💙 - START YOUTUBE VIDEO AT SPECIFIC TIME -->
<script>
(function() {
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^]*)');
var results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
// Function to update YouTube embed src within Embedly iframe
function updateYouTubeEmbed(embedly_iframe, startTime) {
var embedly_src = embedly_iframe.src;
var youtube_src_match = embedly_src.match(/src=([^&]+)/);
if (youtube_src_match) {
var youtube_src = decodeURIComponent(youtube_src_match[1]);
var new_youtube_src = youtube_src.replace(/(\?|&)start=\d+/, '');
new_youtube_src += (new_youtube_src.includes('?') ? '&' : '?') + 'start=' + startTime;
var new_embedly_src = embedly_src.replace(/src=([^&]+)/, 'src=' + encodeURIComponent(new_youtube_src));
embedly_iframe.src = new_embedly_src;
}
}
// Get all elements with ms-code-yt-start attribute
var elements = document.querySelectorAll('[ms-code-yt-start]');
elements.forEach(function(element) {
var paramName = element.getAttribute('ms-code-yt-start');
var startTime = getUrlParameter(paramName);
var defaultStartTime = element.getAttribute('ms-code-yt-start-default');
// If no URL parameter, use the default start time (if specified)
if (!startTime && defaultStartTime) {
startTime = defaultStartTime;
}
// If we have a start time (either from URL or default), update the embed
if (startTime) {
var iframe = element.querySelector('iframe.embedly-embed');
if (iframe) {
updateYouTubeEmbed(iframe, startTime);
}
}
});
})();
</script>

#140 - Confirmar la coincidencia de entradas
Verificar una entrada antes de permitir su envío: ¡genial para evitar información incorrecta!
<!-- 💙 MEMBERSCRIPT #140 v0.1 💙 - CONFIRM MATCHING INPUTS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
const inputPairs = form.querySelectorAll('[ms-code-conf-input]');
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
if (!submitButton) {
console.error('Submit button not found in the form');
return;
}
function validateForm() {
let fieldsMatch = true;
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
const errorElement = form.querySelector(`[ms-code-conf-error="${confType}"]`);
if (confirmInput && errorElement) {
if (input.value && confirmInput.value) {
if (input.value !== confirmInput.value) {
errorElement.style.removeProperty('display');
fieldsMatch = false;
} else {
errorElement.style.display = 'none';
}
} else {
errorElement.style.display = 'none';
}
}
});
if (fieldsMatch) {
submitButton.style.removeProperty('pointer-events');
submitButton.disabled = false;
} else {
submitButton.style.pointerEvents = 'none';
submitButton.disabled = true;
}
}
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
if (confirmInput) {
input.addEventListener('input', validateForm);
confirmInput.addEventListener('input', validateForm);
}
});
// Initial validation
validateForm();
// Extra precaution: prevent form submission if fields don't match
form.addEventListener('submit', function(event) {
if (submitButton.disabled) {
event.preventDefault();
console.log('Form submission blocked: Fields do not match');
}
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #140 v0.1 💙 - CONFIRM MATCHING INPUTS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
const inputPairs = form.querySelectorAll('[ms-code-conf-input]');
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
if (!submitButton) {
console.error('Submit button not found in the form');
return;
}
function validateForm() {
let fieldsMatch = true;
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
const errorElement = form.querySelector(`[ms-code-conf-error="${confType}"]`);
if (confirmInput && errorElement) {
if (input.value && confirmInput.value) {
if (input.value !== confirmInput.value) {
errorElement.style.removeProperty('display');
fieldsMatch = false;
} else {
errorElement.style.display = 'none';
}
} else {
errorElement.style.display = 'none';
}
}
});
if (fieldsMatch) {
submitButton.style.removeProperty('pointer-events');
submitButton.disabled = false;
} else {
submitButton.style.pointerEvents = 'none';
submitButton.disabled = true;
}
}
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
if (confirmInput) {
input.addEventListener('input', validateForm);
confirmInput.addEventListener('input', validateForm);
}
});
// Initial validation
validateForm();
// Extra precaution: prevent form submission if fields don't match
form.addEventListener('submit', function(event) {
if (submitButton.disabled) {
event.preventDefault();
console.log('Form submission blocked: Fields do not match');
}
});
});
});
</script>

#139 - Restablecer formulario tras envío
Cree un botón en el estado de éxito del formulario que permita enviarlo de nuevo.
<!-- 💙 MEMBERSCRIPT #139 v0.1 💙 - RESET FORM BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Find all "Add another" buttons
const resetButtons = document.querySelectorAll('[ms-code-reset-form]');
// Add click event listener to each button
resetButtons.forEach(function(resetButton) {
resetButton.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default link behavior
// Find the closest form and success message elements
const formWrapper = this.closest('.w-form');
const form = formWrapper.querySelector('form');
const successMessage = formWrapper.querySelector('.w-form-done');
// Reset the form
form.reset();
// Hide the success message
successMessage.style.display = 'none';
// Show the form
form.style.display = 'block';
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #139 v0.1 💙 - RESET FORM BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Find all "Add another" buttons
const resetButtons = document.querySelectorAll('[ms-code-reset-form]');
// Add click event listener to each button
resetButtons.forEach(function(resetButton) {
resetButton.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default link behavior
// Find the closest form and success message elements
const formWrapper = this.closest('.w-form');
const form = formWrapper.querySelector('form');
const successMessage = formWrapper.querySelector('.w-form-done');
// Reset the form
form.reset();
// Hide the success message
successMessage.style.display = 'none';
// Show the form
form.style.display = 'block';
});
});
});
</script>
¿Necesitas ayuda con MemberScripts? ¡Únete a nuestra comunidad Slack de más de 5.500 miembros! 🙌
Los MemberScripts son un recurso comunitario de Memberstack - si necesitas ayuda para que funcionen con tu proyecto, ¡únete al Slack de Memberstack 2.0 y pide ayuda!
Únete a nuestro SlackExplore empresas reales que han tenido éxito con Memberstack
No se fíe sólo de nuestra palabra: eche un vistazo a las empresas de todos los tamaños que confían en Memberstack para su autenticación y sus pagos.
Empieza a construir tus sueños
Memberstack es 100% gratis hasta que estés listo para lanzarla - así que, ¿a qué estás esperando? Crea tu primera aplicación y empieza a construir hoy mismo.