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.

#138 - Desplazamiento del enlace de anclaje
Solucionar el problema con los enlaces de anclaje y las barras de navegación pegajosas/fijas en Webflow.
<!-- 💙 MEMBERSCRIPT #138 v0.1 💙 - ANCHOR LINK SCROLL OFFSET -->
<script>
// Disable Webflow's built-in smooth scrolling
var Webflow = Webflow || [];
Webflow.push(function() {
$(function() {
$(document).off('click.wf-scroll');
});
});
// Smooth scroll implementation with customizable settings
(function() {
// Customizable settings
const SCROLL_SETTINGS = {
duration: 1000, // in milliseconds
easing: 'easeInOutCubic' // 'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 'easeOutCubic', 'easeInOutCubic'
};
const EASING_FUNCTIONS = {
linear: t => t,
easeInQuad: t => t * t,
easeOutQuad: t => t * (2 - t),
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeInCubic: t => t * t * t,
easeOutCubic: t => (--t) * t * t + 1,
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
};
function getOffset() {
const navbar = document.querySelector('[ms-code-scroll-offset]');
if (!navbar) return 0;
const navbarHeight = navbar.offsetHeight;
const customOffset = parseInt(navbar.getAttribute('ms-code-scroll-offset') || '0', 10);
return navbarHeight + customOffset;
}
function smoothScroll(target) {
const startPosition = window.pageYOffset;
const offset = getOffset();
const targetPosition = target.getBoundingClientRect().top + startPosition - offset;
const distance = targetPosition - startPosition;
let startTime = null;
function animation(currentTime) {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const progress = Math.min(timeElapsed / SCROLL_SETTINGS.duration, 1);
const easeProgress = EASING_FUNCTIONS[SCROLL_SETTINGS.easing](progress);
window.scrollTo(0, startPosition + distance * easeProgress);
if (timeElapsed < SCROLL_SETTINGS.duration) requestAnimationFrame(animation);
}
requestAnimationFrame(animation);
}
function handleClick(e) {
const href = e.currentTarget.getAttribute('href');
if (href.startsWith('#')) {
e.preventDefault();
const target = document.getElementById(href.slice(1));
if (target) smoothScroll(target);
}
}
function handleHashChange() {
if (window.location.hash) {
const target = document.getElementById(window.location.hash.slice(1));
if (target) {
setTimeout(() => smoothScroll(target), 0);
}
}
}
function init() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', handleClick);
});
window.addEventListener('hashchange', handleHashChange);
handleHashChange(); // Handle initial hash on page load
}
document.addEventListener('DOMContentLoaded', init);
window.Webflow && window.Webflow.push(init);
})();
</script>
<!-- 💙 MEMBERSCRIPT #138 v0.1 💙 - ANCHOR LINK SCROLL OFFSET -->
<script>
// Disable Webflow's built-in smooth scrolling
var Webflow = Webflow || [];
Webflow.push(function() {
$(function() {
$(document).off('click.wf-scroll');
});
});
// Smooth scroll implementation with customizable settings
(function() {
// Customizable settings
const SCROLL_SETTINGS = {
duration: 1000, // in milliseconds
easing: 'easeInOutCubic' // 'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 'easeOutCubic', 'easeInOutCubic'
};
const EASING_FUNCTIONS = {
linear: t => t,
easeInQuad: t => t * t,
easeOutQuad: t => t * (2 - t),
easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeInCubic: t => t * t * t,
easeOutCubic: t => (--t) * t * t + 1,
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
};
function getOffset() {
const navbar = document.querySelector('[ms-code-scroll-offset]');
if (!navbar) return 0;
const navbarHeight = navbar.offsetHeight;
const customOffset = parseInt(navbar.getAttribute('ms-code-scroll-offset') || '0', 10);
return navbarHeight + customOffset;
}
function smoothScroll(target) {
const startPosition = window.pageYOffset;
const offset = getOffset();
const targetPosition = target.getBoundingClientRect().top + startPosition - offset;
const distance = targetPosition - startPosition;
let startTime = null;
function animation(currentTime) {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const progress = Math.min(timeElapsed / SCROLL_SETTINGS.duration, 1);
const easeProgress = EASING_FUNCTIONS[SCROLL_SETTINGS.easing](progress);
window.scrollTo(0, startPosition + distance * easeProgress);
if (timeElapsed < SCROLL_SETTINGS.duration) requestAnimationFrame(animation);
}
requestAnimationFrame(animation);
}
function handleClick(e) {
const href = e.currentTarget.getAttribute('href');
if (href.startsWith('#')) {
e.preventDefault();
const target = document.getElementById(href.slice(1));
if (target) smoothScroll(target);
}
}
function handleHashChange() {
if (window.location.hash) {
const target = document.getElementById(window.location.hash.slice(1));
if (target) {
setTimeout(() => smoothScroll(target), 0);
}
}
}
function init() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', handleClick);
});
window.addEventListener('hashchange', handleHashChange);
handleHashChange(); // Handle initial hash on page load
}
document.addEventListener('DOMContentLoaded', init);
window.Webflow && window.Webflow.push(init);
})();
</script>
.avif)
#137 - Mostrar el nombre del país del visitante
Sustituye el texto por el país en el que se encuentra un usuario en función de su dirección IP.
<!-- 💙 MEMBERSCRIPT #137 v0.1 💙 - DISPLAY COUNTRY NAME -->
<script>
document.addEventListener('DOMContentLoaded', function() {
fetch('https://ipapi.co/json/')
.then(response => response.json())
.then(data => {
if (data.country_name) {
const countryElements = document.querySelectorAll('[ms-code-display-country]');
countryElements.forEach(element => {
element.textContent = data.country_name;
});
}
})
.catch(error => {
console.error('Error fetching country:', error);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #137 v0.1 💙 - DISPLAY COUNTRY NAME -->
<script>
document.addEventListener('DOMContentLoaded', function() {
fetch('https://ipapi.co/json/')
.then(response => response.json())
.then(data => {
if (data.country_name) {
const countryElements = document.querySelectorAll('[ms-code-display-country]');
countryElements.forEach(element => {
element.textContent = data.country_name;
});
}
})
.catch(error => {
console.error('Error fetching country:', error);
});
});
</script>

#136 - Eliminar la ruta de la sección de la URL
Cuando se navegue a una sección, se eliminará la ruta del enlace de anclaje.
<!-- 💙 MEMBERSCRIPT #136 💙 REMOVE SECTION PATH FROM URL -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Check if there's a hash in the URL
if (window.location.hash) {
// Get the target element
const targetId = window.location.hash.substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Scroll to the target element
targetElement.scrollIntoView({behavior: 'smooth'});
// Remove the hash after a short delay (to allow scrolling to complete)
setTimeout(function() {
history.pushState("", document.title, window.location.pathname + window.location.search);
}, 100);
}
}
// Add click event listeners to all internal links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({behavior: 'smooth'});
// Remove the hash after a short delay (to allow scrolling to complete)
setTimeout(function() {
history.pushState("", document.title, window.location.pathname + window.location.search);
}, 100);
}
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #136 💙 REMOVE SECTION PATH FROM URL -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Check if there's a hash in the URL
if (window.location.hash) {
// Get the target element
const targetId = window.location.hash.substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Scroll to the target element
targetElement.scrollIntoView({behavior: 'smooth'});
// Remove the hash after a short delay (to allow scrolling to complete)
setTimeout(function() {
history.pushState("", document.title, window.location.pathname + window.location.search);
}, 100);
}
}
// Add click event listeners to all internal links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({behavior: 'smooth'});
// Remove the hash after a short delay (to allow scrolling to complete)
setTimeout(function() {
history.pushState("", document.title, window.location.pathname + window.location.search);
}, 100);
}
});
});
});
</script>

#135 - Redirección basada en el valor seleccionado
Establezca dinámicamente la redirección del formulario en función de la selección de los usuarios.
<!-- 💙 MEMBERSCRIPT #135 v0.2 💙 - REDIRECT FORM FROM SELECT VALUE -->
<script>
(function() {
'use strict';
function initDropdownRedirect() {
const dropdown = document.querySelector('select[ms-code-dropdown-redirect]');
if (!dropdown) return;
const form = dropdown.closest('form');
if (!form) return;
function updateRedirect() {
const selectedValue = dropdown.value;
form.setAttribute('redirect', selectedValue);
form.setAttribute('data-redirect', selectedValue);
}
function handleSubmit(event) {
event.preventDefault();
const redirectUrl = form.getAttribute('data-redirect') || form.getAttribute('redirect');
if (redirectUrl) {
setTimeout(() => {
window.location.href = redirectUrl;
}, 500); // Delay redirect by 500 milliseconds
} else {
form.submit(); // Fall back to normal form submission if no redirect is set
}
}
dropdown.addEventListener('change', updateRedirect);
form.addEventListener('submit', handleSubmit);
// Initialize redirect on page load
updateRedirect();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDropdownRedirect);
} else {
initDropdownRedirect();
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #135 v0.2 💙 - REDIRECT FORM FROM SELECT VALUE -->
<script>
(function() {
'use strict';
function initDropdownRedirect() {
const dropdown = document.querySelector('select[ms-code-dropdown-redirect]');
if (!dropdown) return;
const form = dropdown.closest('form');
if (!form) return;
function updateRedirect() {
const selectedValue = dropdown.value;
form.setAttribute('redirect', selectedValue);
form.setAttribute('data-redirect', selectedValue);
}
function handleSubmit(event) {
event.preventDefault();
const redirectUrl = form.getAttribute('data-redirect') || form.getAttribute('redirect');
if (redirectUrl) {
setTimeout(() => {
window.location.href = redirectUrl;
}, 500); // Delay redirect by 500 milliseconds
} else {
form.submit(); // Fall back to normal form submission if no redirect is set
}
}
dropdown.addEventListener('change', updateRedirect);
form.addEventListener('submit', handleSubmit);
// Initialize redirect on page load
updateRedirect();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDropdownRedirect);
} else {
initDropdownRedirect();
}
})();
</script>

#134 - Desplazarse al principio al cambiar de pestaña
Al cambiar de pestaña, la página se desplazará a la parte superior de la sección de pestañas.
<!-- 💙 MEMBERSCRIPT #134 💙 - SCROLL TO TOP OF TABS ON CHANGE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Select all tab containers with the ms-code-tab-scroll-top attribute
const tabContainers = document.querySelectorAll('.w-tabs[ms-code-tab-scroll-top]');
tabContainers.forEach(container => {
const tabLinks = container.querySelectorAll('.w-tab-link');
const scrollTopValue = parseInt(container.getAttribute('ms-code-tab-scroll-top') || '0');
tabLinks.forEach(link => {
link.addEventListener('click', function(e) {
// Small delay to ensure the new tab content is rendered
setTimeout(() => {
// Find the active tab pane within this container
const activePane = container.querySelector('.w-tab-pane.w--tab-active');
if (activePane) {
// Calculate the new scroll position
const newScrollPosition = container.getBoundingClientRect().top + window.pageYOffset + scrollTopValue;
// Scroll to the new position
window.scrollTo({
top: newScrollPosition,
behavior: 'smooth'
});
}
}, 50); // 50ms delay, adjust if needed
});
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #134 💙 - SCROLL TO TOP OF TABS ON CHANGE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Select all tab containers with the ms-code-tab-scroll-top attribute
const tabContainers = document.querySelectorAll('.w-tabs[ms-code-tab-scroll-top]');
tabContainers.forEach(container => {
const tabLinks = container.querySelectorAll('.w-tab-link');
const scrollTopValue = parseInt(container.getAttribute('ms-code-tab-scroll-top') || '0');
tabLinks.forEach(link => {
link.addEventListener('click', function(e) {
// Small delay to ensure the new tab content is rendered
setTimeout(() => {
// Find the active tab pane within this container
const activePane = container.querySelector('.w-tab-pane.w--tab-active');
if (activePane) {
// Calculate the new scroll position
const newScrollPosition = container.getBoundingClientRect().top + window.pageYOffset + scrollTopValue;
// Scroll to the new position
window.scrollTo({
top: newScrollPosition,
behavior: 'smooth'
});
}
}, 50); // 50ms delay, adjust if needed
});
});
});
});
</script>

#133 - Imágenes con marca de agua automática
Añada fácilmente una marca de agua a las imágenes de su sitio Webflow.
<!-- 💙 MEMBERSCRIPT #133 v0.1 💙 - AUTO IMAGE WATERMARK -->
<script>
function addWatermarkToImages() {
const images = document.querySelectorAll('img[ms-code-watermark]');
images.forEach(img => {
img.crossOrigin = "Anonymous"; // This allows us to work with images from other domains
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas size to match the image
canvas.width = img.width;
canvas.height = img.height;
// Draw the original image onto the canvas
ctx.drawImage(img, 0, 0, img.width, img.height);
// Get watermark text from attribute
const watermarkText = img.getAttribute('ms-code-watermark') || 'Watermark';
// Add watermark
ctx.font = `${img.width / 20}px Arial`; // Adjust font size based on image width
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Rotate and draw the watermark text
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(-Math.PI / 4); // Rotate 45 degrees
ctx.fillText(watermarkText, 0, 0);
ctx.restore();
// Preserve the original image's classes and other attributes
canvas.className = img.className;
for (let i = 0; i < img.attributes.length; i++) {
const attr = img.attributes[i];
if (attr.name !== 'src' && attr.name !== 'ms-code-watermark') {
canvas.setAttribute(attr.name, attr.value);
}
}
// Replace the original image with the watermarked canvas
img.parentNode.replaceChild(canvas, img);
};
// Trigger onload event (in case the image is already loaded)
if (img.complete) {
img.onload();
}
});
}
// Run the function when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', addWatermarkToImages);
</script>
<!-- 💙 MEMBERSCRIPT #133 v0.1 💙 - AUTO IMAGE WATERMARK -->
<script>
function addWatermarkToImages() {
const images = document.querySelectorAll('img[ms-code-watermark]');
images.forEach(img => {
img.crossOrigin = "Anonymous"; // This allows us to work with images from other domains
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas size to match the image
canvas.width = img.width;
canvas.height = img.height;
// Draw the original image onto the canvas
ctx.drawImage(img, 0, 0, img.width, img.height);
// Get watermark text from attribute
const watermarkText = img.getAttribute('ms-code-watermark') || 'Watermark';
// Add watermark
ctx.font = `${img.width / 20}px Arial`; // Adjust font size based on image width
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Rotate and draw the watermark text
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(-Math.PI / 4); // Rotate 45 degrees
ctx.fillText(watermarkText, 0, 0);
ctx.restore();
// Preserve the original image's classes and other attributes
canvas.className = img.className;
for (let i = 0; i < img.attributes.length; i++) {
const attr = img.attributes[i];
if (attr.name !== 'src' && attr.name !== 'ms-code-watermark') {
canvas.setAttribute(attr.name, attr.value);
}
}
// Replace the original image with the watermarked canvas
img.parentNode.replaceChild(canvas, img);
};
// Trigger onload event (in case the image is already loaded)
if (img.complete) {
img.onload();
}
});
}
// Run the function when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', addWatermarkToImages);
</script>

#132 - Ocultar elementos con la tecla Escape
Añade un atributo y cuando se haga clic en la tecla esc, el elemento se establecerá en mostrar ninguno.
<!-- 💙 MEMBERSCRIPT 💙 - HIDE ELEMENTS WITH ESC KEY -->
<script>
document.addEventListener('keydown', function(event) {
// Check if the pressed key is ESC (key code 27)
if (event.key === 'Escape' || event.keyCode === 27) {
// Find all elements with the attribute ms-code-close-esc
const elements = document.querySelectorAll('[ms-code-close-esc]');
// Loop through the elements and set their display to 'none'
elements.forEach(function(element) {
element.style.display = 'none';
});
}
});
</script>
<!-- 💙 MEMBERSCRIPT 💙 - HIDE ELEMENTS WITH ESC KEY -->
<script>
document.addEventListener('keydown', function(event) {
// Check if the pressed key is ESC (key code 27)
if (event.key === 'Escape' || event.keyCode === 27) {
// Find all elements with the attribute ms-code-close-esc
const elements = document.querySelectorAll('[ms-code-close-esc]');
// Loop through the elements and set their display to 'none'
elements.forEach(function(element) {
element.style.display = 'none';
});
}
});
</script>

#131 - Sumar entradas de números
Toma el valor de las entradas numéricas y lo muestra en un valor de entrada, o en un span de texto.
<!-- 💙 MEMBERSCRIPT #131 v0.1 💙 - CALCULATE NUMBER INPUTS -->
<script>
// Function to initialize the counter functionality
function initializeCounter() {
const counters = {};
// Find all elements with ms-code-show-number attribute
document.querySelectorAll('[ms-code-show-number]').forEach(el => {
const counterName = el.getAttribute('ms-code-show-number');
if (!counters[counterName]) {
counters[counterName] = { total: 0, displays: [] };
}
counters[counterName].displays.push(el);
});
// Find all input elements with ms-code-add-number attribute
document.querySelectorAll('input[ms-code-add-number]').forEach(input => {
const counterName = input.getAttribute('ms-code-add-number');
if (counters[counterName]) {
input.addEventListener('input', updateCounter);
}
});
// Function to update counter when input changes
function updateCounter(event) {
const input = event.target;
const counterName = input.getAttribute('ms-code-add-number');
const counter = counters[counterName];
if (counter) {
counter.total = 0;
document.querySelectorAll(`input[ms-code-add-number="${counterName}"]`).forEach(input => {
counter.total += parseInt(input.value) || 0;
});
counter.displays.forEach(display => {
if (display.tagName === 'INPUT') {
display.value = counter.total;
} else {
display.textContent = counter.total;
}
});
}
}
// Initial update for all counters
Object.keys(counters).forEach(counterName => {
const input = document.querySelector(`input[ms-code-add-number="${counterName}"]`);
if (input) {
input.dispatchEvent(new Event('input'));
}
});
}
// Run the initialization when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initializeCounter);
</script>
<!-- 💙 MEMBERSCRIPT #131 v0.1 💙 - CALCULATE NUMBER INPUTS -->
<script>
// Function to initialize the counter functionality
function initializeCounter() {
const counters = {};
// Find all elements with ms-code-show-number attribute
document.querySelectorAll('[ms-code-show-number]').forEach(el => {
const counterName = el.getAttribute('ms-code-show-number');
if (!counters[counterName]) {
counters[counterName] = { total: 0, displays: [] };
}
counters[counterName].displays.push(el);
});
// Find all input elements with ms-code-add-number attribute
document.querySelectorAll('input[ms-code-add-number]').forEach(input => {
const counterName = input.getAttribute('ms-code-add-number');
if (counters[counterName]) {
input.addEventListener('input', updateCounter);
}
});
// Function to update counter when input changes
function updateCounter(event) {
const input = event.target;
const counterName = input.getAttribute('ms-code-add-number');
const counter = counters[counterName];
if (counter) {
counter.total = 0;
document.querySelectorAll(`input[ms-code-add-number="${counterName}"]`).forEach(input => {
counter.total += parseInt(input.value) || 0;
});
counter.displays.forEach(display => {
if (display.tagName === 'INPUT') {
display.value = counter.total;
} else {
display.textContent = counter.total;
}
});
}
}
// Initial update for all counters
Object.keys(counters).forEach(counterName => {
const input = document.querySelector(`input[ms-code-add-number="${counterName}"]`);
if (input) {
input.dispatchEvent(new Event('input'));
}
});
}
// Run the initialization when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initializeCounter);
</script>

#130 - Enviar formulario automáticamente cuando cambian todas las entradas
Omita la necesidad de un botón de envío y envíe el formulario cuando cambien todas las entradas.
<!-- 💙 MEMBERSCRIPT #130 v0.1 💙 - AUTO SUBMIT FORMS FROM INPUT CHANGE -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const forms = document.querySelectorAll('form[ms-code-auto-submit]');
forms.forEach(form => {
const fields = form.querySelectorAll('input:not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea');
const fieldStates = new Map(Array.from(fields).map(field => [field, false]));
function updateFieldState(field, checkImmediately = false) {
switch (field.type) {
case 'checkbox':
fieldStates.set(field, true); // Considered interacted with once changed
break;
case 'radio':
const radioGroup = form.querySelectorAll(`input[type="radio"][name="${field.name}"]`);
radioGroup.forEach(radio => fieldStates.set(radio, true));
break;
case 'select-one':
case 'select-multiple':
fieldStates.set(field, field.value !== '');
break;
case 'file':
fieldStates.set(field, field.files.length > 0);
break;
case 'hidden':
fieldStates.set(field, true); // Always consider hidden fields as filled
break;
default:
// For text inputs, only update on blur or if checkImmediately is true
if (field.type === 'text' || field.type === 'password' || field.type === 'email' || field.type === 'tel' || field.type === 'url' || field.tagName === 'TEXTAREA') {
if (checkImmediately || !field.dataset.blurred) {
fieldStates.set(field, field.value.trim() !== '');
}
} else {
fieldStates.set(field, field.value.trim() !== '');
}
}
if (checkImmediately) {
checkAndSubmit();
}
}
function checkAndSubmit() {
if (Array.from(fieldStates.values()).every(state => state)) {
// Create and dispatch a submit event
const submitEvent = new Event('submit', {
bubbles: true,
cancelable: true
});
const submitted = form.dispatchEvent(submitEvent);
// If the event wasn't prevented, manually submit the form
if (submitted) {
form.submit();
}
}
}
fields.forEach(field => {
// Use 'change' event for checkboxes, radios, file inputs, and selects
if (['checkbox', 'radio', 'file', 'select-one', 'select-multiple'].includes(field.type) || field.tagName === 'SELECT') {
field.addEventListener('change', () => updateFieldState(field, true));
}
// For text-like inputs, use 'blur' event
if (field.type === 'text' || field.type === 'password' || field.type === 'email' || field.type === 'tel' || field.type === 'url' || field.tagName === 'TEXTAREA') {
field.addEventListener('blur', () => {
field.dataset.blurred = 'true';
updateFieldState(field, true);
});
// Also check on input, but don't submit immediately
field.addEventListener('input', () => updateFieldState(field, false));
}
});
// Initial check for pre-filled fields (e.g., browser autofill)
fields.forEach(field => updateFieldState(field, false));
checkAndSubmit();
});
});
</script>
<!-- 💙 MEMBERSCRIPT #130 v0.1 💙 - AUTO SUBMIT FORMS FROM INPUT CHANGE -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const forms = document.querySelectorAll('form[ms-code-auto-submit]');
forms.forEach(form => {
const fields = form.querySelectorAll('input:not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea');
const fieldStates = new Map(Array.from(fields).map(field => [field, false]));
function updateFieldState(field, checkImmediately = false) {
switch (field.type) {
case 'checkbox':
fieldStates.set(field, true); // Considered interacted with once changed
break;
case 'radio':
const radioGroup = form.querySelectorAll(`input[type="radio"][name="${field.name}"]`);
radioGroup.forEach(radio => fieldStates.set(radio, true));
break;
case 'select-one':
case 'select-multiple':
fieldStates.set(field, field.value !== '');
break;
case 'file':
fieldStates.set(field, field.files.length > 0);
break;
case 'hidden':
fieldStates.set(field, true); // Always consider hidden fields as filled
break;
default:
// For text inputs, only update on blur or if checkImmediately is true
if (field.type === 'text' || field.type === 'password' || field.type === 'email' || field.type === 'tel' || field.type === 'url' || field.tagName === 'TEXTAREA') {
if (checkImmediately || !field.dataset.blurred) {
fieldStates.set(field, field.value.trim() !== '');
}
} else {
fieldStates.set(field, field.value.trim() !== '');
}
}
if (checkImmediately) {
checkAndSubmit();
}
}
function checkAndSubmit() {
if (Array.from(fieldStates.values()).every(state => state)) {
// Create and dispatch a submit event
const submitEvent = new Event('submit', {
bubbles: true,
cancelable: true
});
const submitted = form.dispatchEvent(submitEvent);
// If the event wasn't prevented, manually submit the form
if (submitted) {
form.submit();
}
}
}
fields.forEach(field => {
// Use 'change' event for checkboxes, radios, file inputs, and selects
if (['checkbox', 'radio', 'file', 'select-one', 'select-multiple'].includes(field.type) || field.tagName === 'SELECT') {
field.addEventListener('change', () => updateFieldState(field, true));
}
// For text-like inputs, use 'blur' event
if (field.type === 'text' || field.type === 'password' || field.type === 'email' || field.type === 'tel' || field.type === 'url' || field.tagName === 'TEXTAREA') {
field.addEventListener('blur', () => {
field.dataset.blurred = 'true';
updateFieldState(field, true);
});
// Also check on input, but don't submit immediately
field.addEventListener('input', () => updateFieldState(field, false));
}
});
// Initial check for pre-filled fields (e.g., browser autofill)
fields.forEach(field => updateFieldState(field, false));
checkAndSubmit();
});
});
</script>

#129 - El paso del país
Impida que los visitantes vean su sitio si se encuentran en uno de los países no permitidos.
<!-- 💙 MEMBERSCRIPT #129 v0.1 💙 - COUNTRY GATING -->
<script>
// Configuration
const ACCESS_DENIED_PAGE = '/access-denied';
// List of disallowed countries using ISO 3166-1 alpha-2 country codes
const DISALLOWED_COUNTRIES = [
// "US", // United States
// "CN", // China
// "RU", // Russia
// "IN", // India
// "JP", // Japan
// "DE", // Germany
// "GB", // United Kingdom
// "FR", // France
// "BR", // Brazil
// "IT", // Italy
// Add more countries as needed
];
// Function to get visitor's country and check access
function checkCountryAccess() {
// Check if we're already on the access denied page
if (window.location.pathname === ACCESS_DENIED_PAGE) {
return; // Don't redirect if already on the access denied page
}
fetch('https://ipapi.co/json/')
.then(response => response.json())
.then(data => {
const visitorCountry = data.country_code; // This returns the ISO 3166-1 alpha-2 country code
if (DISALLOWED_COUNTRIES.includes(visitorCountry)) {
window.location.href = ACCESS_DENIED_PAGE;
}
})
.catch(error => {
console.error('Error fetching IP data:', error);
});
}
// Run the check when the page loads
document.addEventListener('DOMContentLoaded', checkCountryAccess);
</script>
<!-- 💙 MEMBERSCRIPT #129 v0.1 💙 - COUNTRY GATING -->
<script>
// Configuration
const ACCESS_DENIED_PAGE = '/access-denied';
// List of disallowed countries using ISO 3166-1 alpha-2 country codes
const DISALLOWED_COUNTRIES = [
// "US", // United States
// "CN", // China
// "RU", // Russia
// "IN", // India
// "JP", // Japan
// "DE", // Germany
// "GB", // United Kingdom
// "FR", // France
// "BR", // Brazil
// "IT", // Italy
// Add more countries as needed
];
// Function to get visitor's country and check access
function checkCountryAccess() {
// Check if we're already on the access denied page
if (window.location.pathname === ACCESS_DENIED_PAGE) {
return; // Don't redirect if already on the access denied page
}
fetch('https://ipapi.co/json/')
.then(response => response.json())
.then(data => {
const visitorCountry = data.country_code; // This returns the ISO 3166-1 alpha-2 country code
if (DISALLOWED_COUNTRIES.includes(visitorCountry)) {
window.location.href = ACCESS_DENIED_PAGE;
}
})
.catch(error => {
console.error('Error fetching IP data:', error);
});
}
// Run the check when the page loads
document.addEventListener('DOMContentLoaded', checkCountryAccess);
</script>

#128 - Ocultar elementos una vez vistos
Añada un atributo, y sus visitantes sólo verán ese elemento una vez. Después de actualizar, desaparecerá.
<!-- 💙 MEMBERSCRIPT #128 💙 - ONLY SHOW ELEMENT ONCE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Find all elements with the ms-code-show-once attribute
const elements = document.querySelectorAll('[ms-code-show-once]');
elements.forEach(element => {
const identifier = element.getAttribute('ms-code-show-once');
const storageKey = `ms-code-shown-${identifier}`;
// Check if the element has been seen before
if (localStorage.getItem(storageKey) !== 'true') {
// If not seen, show the element
element.style.display = 'block';
// Mark it as seen in localStorage
localStorage.setItem(storageKey, 'true');
} else {
// If already seen, hide the element
element.style.display = 'none';
}
});
});
</script>
<!-- 💙 MEMBERSCRIPT #128 💙 - ONLY SHOW ELEMENT ONCE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Find all elements with the ms-code-show-once attribute
const elements = document.querySelectorAll('[ms-code-show-once]');
elements.forEach(element => {
const identifier = element.getAttribute('ms-code-show-once');
const storageKey = `ms-code-shown-${identifier}`;
// Check if the element has been seen before
if (localStorage.getItem(storageKey) !== 'true') {
// If not seen, show the element
element.style.display = 'block';
// Mark it as seen in localStorage
localStorage.setItem(storageKey, 'true');
} else {
// If already seen, hide the element
element.style.display = 'none';
}
});
});
</script>

#127 - Validar entradas de texto
Valide las entradas de texto con cualquier lista de cadenas, incluidos los comodines.
<!-- 💙 MEMBERSCRIPT #127 v0.1 💙 - TEXT INPUT VALIDATION -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Debounce function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Find all fields with ms-code-require attribute
const fields = document.querySelectorAll('[ms-code-require]');
fields.forEach(field => {
// Get the error element for this field
const errorElement = document.querySelector(`[ms-code-require-error="${field.getAttribute('ms-code-require')}"]`);
// Hide error message initially
if (errorElement) {
errorElement.style.display = 'none';
}
// Get the form containing the field
const form = field.closest('form');
// Get the submit button
const submitButton = form ? form.querySelector(`[ms-code-submit-button="${field.getAttribute('ms-code-require')}"]`) : null;
// Get the require-list attribute value
const requireList = field.getAttribute('ms-code-require-list');
if (requireList) {
// Convert the require-list to an array of regex patterns
const patterns = requireList.split(',').map(pattern => {
return pattern.replace(/\{([^}]+)\}/g, (match, p1) => {
return p1.split('').map(char => {
switch(char) {
case '0': return '\\d';
case 'A': return '[A-Z]';
case 'a': return '[a-z]';
default: return char;
}
}).join('');
});
});
// Validate function
function validateField() {
const value = field.value;
const isValid = patterns.some(pattern => new RegExp(`^${pattern}$`).test(value));
if (errorElement) {
errorElement.style.display = isValid ? 'none' : 'block';
}
if (submitButton) {
submitButton.style.opacity = isValid ? '1' : '0.5';
submitButton.style.pointerEvents = isValid ? 'auto' : 'none';
}
return isValid;
}
// Debounced validate function
const debouncedValidate = debounce(validateField, 500);
// Add blur event listener
field.addEventListener('blur', validateField);
// Add input event listener for debounced validation
field.addEventListener('input', debouncedValidate);
// Handle form submission
if (form) {
form.addEventListener('submit', function(event) {
if (!validateField() && submitButton) {
event.preventDefault();
field.focus();
}
});
}
}
});
});
</script>
<!-- 💙 MEMBERSCRIPT #127 v0.1 💙 - TEXT INPUT VALIDATION -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Debounce function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Find all fields with ms-code-require attribute
const fields = document.querySelectorAll('[ms-code-require]');
fields.forEach(field => {
// Get the error element for this field
const errorElement = document.querySelector(`[ms-code-require-error="${field.getAttribute('ms-code-require')}"]`);
// Hide error message initially
if (errorElement) {
errorElement.style.display = 'none';
}
// Get the form containing the field
const form = field.closest('form');
// Get the submit button
const submitButton = form ? form.querySelector(`[ms-code-submit-button="${field.getAttribute('ms-code-require')}"]`) : null;
// Get the require-list attribute value
const requireList = field.getAttribute('ms-code-require-list');
if (requireList) {
// Convert the require-list to an array of regex patterns
const patterns = requireList.split(',').map(pattern => {
return pattern.replace(/\{([^}]+)\}/g, (match, p1) => {
return p1.split('').map(char => {
switch(char) {
case '0': return '\\d';
case 'A': return '[A-Z]';
case 'a': return '[a-z]';
default: return char;
}
}).join('');
});
});
// Validate function
function validateField() {
const value = field.value;
const isValid = patterns.some(pattern => new RegExp(`^${pattern}$`).test(value));
if (errorElement) {
errorElement.style.display = isValid ? 'none' : 'block';
}
if (submitButton) {
submitButton.style.opacity = isValid ? '1' : '0.5';
submitButton.style.pointerEvents = isValid ? 'auto' : 'none';
}
return isValid;
}
// Debounced validate function
const debouncedValidate = debounce(validateField, 500);
// Add blur event listener
field.addEventListener('blur', validateField);
// Add input event listener for debounced validation
field.addEventListener('input', debouncedValidate);
// Handle form submission
if (form) {
form.addEventListener('submit', function(event) {
if (!validateField() && submitButton) {
event.preventDefault();
field.focus();
}
});
}
}
});
});
</script>

#126 - Enviar formulario a Webhook sin redireccionar
Enviar datos a un webhook y mantener el comportamiento predeterminado del formulario Webflow.
<!-- 💙 MEMBERSCRIPT #126 v0.1 💙 - POST FORM DATA TO WEBHOOK WITHOUT REDIRECTING -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Select all forms with the ms-code-form-no-redirect attribute
const forms = document.querySelectorAll('form[ms-code-form-no-redirect]');
forms.forEach(form => {
// Select the success and error message elements for this form
const formWrapper = form.closest('.w-form');
const successMessage = formWrapper.querySelector('.w-form-done');
const errorMessage = formWrapper.querySelector('.w-form-fail');
// Add submit event listener to the form
form.addEventListener('submit', function(event) {
// Prevent the default form submission
event.preventDefault();
// Get the form data
const formData = new FormData(form);
// Get the submit button and set its text to the waiting message
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
const originalButtonText = submitButton.value || submitButton.textContent;
const waitingText = submitButton.getAttribute('data-wait') || 'Please wait...';
if (submitButton.tagName === 'INPUT') {
submitButton.value = waitingText;
} else {
submitButton.textContent = waitingText;
}
// Disable the submit button
submitButton.disabled = true;
// Send the form data to the form's action URL using fetch
fetch(form.action, {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
// If the submission was successful, show the success message
form.style.display = 'none';
successMessage.style.display = 'block';
errorMessage.style.display = 'none';
} else {
// If there was an error, show the error message
throw new Error('Form submission failed');
}
})
.catch(error => {
// If there was a network error or the submission failed, show the error message
console.error('Error:', error);
errorMessage.style.display = 'block';
successMessage.style.display = 'none';
})
.finally(() => {
// Reset the submit button text and re-enable it
if (submitButton.tagName === 'INPUT') {
submitButton.value = originalButtonText;
} else {
submitButton.textContent = originalButtonText;
}
submitButton.disabled = false;
});
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #126 v0.1 💙 - POST FORM DATA TO WEBHOOK WITHOUT REDIRECTING -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Select all forms with the ms-code-form-no-redirect attribute
const forms = document.querySelectorAll('form[ms-code-form-no-redirect]');
forms.forEach(form => {
// Select the success and error message elements for this form
const formWrapper = form.closest('.w-form');
const successMessage = formWrapper.querySelector('.w-form-done');
const errorMessage = formWrapper.querySelector('.w-form-fail');
// Add submit event listener to the form
form.addEventListener('submit', function(event) {
// Prevent the default form submission
event.preventDefault();
// Get the form data
const formData = new FormData(form);
// Get the submit button and set its text to the waiting message
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
const originalButtonText = submitButton.value || submitButton.textContent;
const waitingText = submitButton.getAttribute('data-wait') || 'Please wait...';
if (submitButton.tagName === 'INPUT') {
submitButton.value = waitingText;
} else {
submitButton.textContent = waitingText;
}
// Disable the submit button
submitButton.disabled = true;
// Send the form data to the form's action URL using fetch
fetch(form.action, {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
// If the submission was successful, show the success message
form.style.display = 'none';
successMessage.style.display = 'block';
errorMessage.style.display = 'none';
} else {
// If there was an error, show the error message
throw new Error('Form submission failed');
}
})
.catch(error => {
// If there was a network error or the submission failed, show the error message
console.error('Error:', error);
errorMessage.style.display = 'block';
successMessage.style.display = 'none';
})
.finally(() => {
// Reset the submit button text and re-enable it
if (submitButton.tagName === 'INPUT') {
submitButton.value = originalButtonText;
} else {
submitButton.textContent = originalButtonText;
}
submitButton.disabled = false;
});
});
});
});
</script>

#125 - Controlar Estado Requerido Con Checkbox
Establecer campos de formulario como obligatorios/opcionales en función del estado de una casilla de verificación.
<!-- 💙 MEMBERSCRIPT #125 v0.1 💙 - CHECKBOX TOGGLE REQUIRED ON FORM FIELDS -->
<script>
// Function to initialize the form field requirements
function initFormFieldRequirements() {
// Handle checkbox changes
document.querySelectorAll('[ms-code-req-if-checked], [ms-code-req-if-unchecked]').forEach(checkbox => {
checkbox.addEventListener('change', updateFieldRequirements);
// Initial update
updateFieldRequirements.call(checkbox);
});
}
// Function to update field requirements based on checkbox state
function updateFieldRequirements() {
const isChecked = this.checked;
const group = this.getAttribute('ms-code-req-if-checked') || this.getAttribute('ms-code-req-if-unchecked');
const ifChecked = this.hasAttribute('ms-code-req-if-checked');
const shouldBeRequired = ifChecked ? isChecked : !isChecked;
const shouldDisableIfNotReq = this.hasAttribute('ms-code-disable-if-not-req');
// Update associated input fields
document.querySelectorAll(`[ms-code-req-input="${group}"]`).forEach(input => {
input.required = shouldBeRequired;
updateInputStyle(input, shouldBeRequired, shouldDisableIfNotReq);
});
// Update associated labels
document.querySelectorAll(`[ms-code-req-label="${group}"]`).forEach(label => {
label.style.display = shouldBeRequired ? '' : 'none';
});
}
// Function to update input style based on required state and disable setting
function updateInputStyle(input, isRequired, shouldDisableIfNotReq) {
if (shouldDisableIfNotReq) {
input.style.opacity = isRequired ? '1' : '0.4';
input.style.pointerEvents = isRequired ? '' : 'none';
} else {
input.style.opacity = '';
input.style.pointerEvents = '';
}
}
// Initialize when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initFormFieldRequirements);
</script>
<!-- 💙 MEMBERSCRIPT #125 v0.1 💙 - CHECKBOX TOGGLE REQUIRED ON FORM FIELDS -->
<script>
// Function to initialize the form field requirements
function initFormFieldRequirements() {
// Handle checkbox changes
document.querySelectorAll('[ms-code-req-if-checked], [ms-code-req-if-unchecked]').forEach(checkbox => {
checkbox.addEventListener('change', updateFieldRequirements);
// Initial update
updateFieldRequirements.call(checkbox);
});
}
// Function to update field requirements based on checkbox state
function updateFieldRequirements() {
const isChecked = this.checked;
const group = this.getAttribute('ms-code-req-if-checked') || this.getAttribute('ms-code-req-if-unchecked');
const ifChecked = this.hasAttribute('ms-code-req-if-checked');
const shouldBeRequired = ifChecked ? isChecked : !isChecked;
const shouldDisableIfNotReq = this.hasAttribute('ms-code-disable-if-not-req');
// Update associated input fields
document.querySelectorAll(`[ms-code-req-input="${group}"]`).forEach(input => {
input.required = shouldBeRequired;
updateInputStyle(input, shouldBeRequired, shouldDisableIfNotReq);
});
// Update associated labels
document.querySelectorAll(`[ms-code-req-label="${group}"]`).forEach(label => {
label.style.display = shouldBeRequired ? '' : 'none';
});
}
// Function to update input style based on required state and disable setting
function updateInputStyle(input, isRequired, shouldDisableIfNotReq) {
if (shouldDisableIfNotReq) {
input.style.opacity = isRequired ? '1' : '0.4';
input.style.pointerEvents = isRequired ? '' : 'none';
} else {
input.style.opacity = '';
input.style.pointerEvents = '';
}
}
// Initialize when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initFormFieldRequirements);
</script>

#124 - Alternar la visibilidad de los elementos
Utilice botones personalizados para ocultar y mostrar elementos, con los estados guardados en el almacenamiento local.
<!-- 💙 MEMBERSCRIPT #124 v0.1 💙 - TOGGLE ITEM VISIBILITY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const STORAGE_KEY = 'ms-code-vis-states';
// Load saved states from local storage
let savedStates = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
// Find all unique group identifiers
const groups = new Set([...document.querySelectorAll('[ms-code-vis-item]')].map(el => el.getAttribute('ms-code-vis-item')));
groups.forEach(group => {
const item = document.querySelector(`[ms-code-vis-item="${group}"]`);
const showButton = document.querySelector(`[ms-code-vis-show="${group}"]`);
const hideButton = document.querySelector(`[ms-code-vis-hide="${group}"]`);
if (!item || !showButton || !hideButton) return;
// Function to update visibility
function updateVisibility(isVisible) {
if (isVisible) {
item.style.removeProperty('display');
showButton.style.display = 'none';
hideButton.style.removeProperty('display');
} else {
item.style.display = 'none';
showButton.style.removeProperty('display');
hideButton.style.display = 'none';
}
// Save state to local storage
savedStates[group] = isVisible;
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedStates));
}
// Set initial visibility
const defaultState = item.getAttribute('ms-code-vis-default');
const savedState = savedStates[group];
if (savedState !== undefined) {
updateVisibility(savedState);
} else if (defaultState === 'hide') {
updateVisibility(false);
} else {
updateVisibility(true);
}
// Add click event listeners
showButton.addEventListener('click', function(e) {
e.preventDefault();
updateVisibility(true);
});
hideButton.addEventListener('click', function(e) {
e.preventDefault();
updateVisibility(false);
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #124 v0.1 💙 - TOGGLE ITEM VISIBILITY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const STORAGE_KEY = 'ms-code-vis-states';
// Load saved states from local storage
let savedStates = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
// Find all unique group identifiers
const groups = new Set([...document.querySelectorAll('[ms-code-vis-item]')].map(el => el.getAttribute('ms-code-vis-item')));
groups.forEach(group => {
const item = document.querySelector(`[ms-code-vis-item="${group}"]`);
const showButton = document.querySelector(`[ms-code-vis-show="${group}"]`);
const hideButton = document.querySelector(`[ms-code-vis-hide="${group}"]`);
if (!item || !showButton || !hideButton) return;
// Function to update visibility
function updateVisibility(isVisible) {
if (isVisible) {
item.style.removeProperty('display');
showButton.style.display = 'none';
hideButton.style.removeProperty('display');
} else {
item.style.display = 'none';
showButton.style.removeProperty('display');
hideButton.style.display = 'none';
}
// Save state to local storage
savedStates[group] = isVisible;
localStorage.setItem(STORAGE_KEY, JSON.stringify(savedStates));
}
// Set initial visibility
const defaultState = item.getAttribute('ms-code-vis-default');
const savedState = savedStates[group];
if (savedState !== undefined) {
updateVisibility(savedState);
} else if (defaultState === 'hide') {
updateVisibility(false);
} else {
updateVisibility(true);
}
// Add click event listeners
showButton.addEventListener('click', function(e) {
e.preventDefault();
updateVisibility(true);
});
hideButton.addEventListener('click', function(e) {
e.preventDefault();
updateVisibility(false);
});
});
});
</script>

#123 - Una reproducción de audio cada vez
Ponga en pausa automáticamente todos los demás reproductores de audio cuando un usuario haga clic para reproducir uno.
<!-- 💙 MEMBERSCRIPT #123 💙 - ONE AUDIO AT A TIME -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const pauseOthers = current =>
document.querySelectorAll('audio, video').forEach(el => el !== current && !el.paused && el.pause());
const addPlayListener = el => el.addEventListener('play', e => pauseOthers(e.target));
new MutationObserver(mutations =>
mutations.forEach(m => m.addedNodes.forEach(n =>
(n.nodeName === 'AUDIO' || n.nodeName === 'VIDEO') && addPlayListener(n)
))
).observe(document.body, { childList: true, subtree: true });
document.querySelectorAll('audio, video').forEach(addPlayListener);
});
</script>
<!-- 💙 MEMBERSCRIPT #123 💙 - ONE AUDIO AT A TIME -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const pauseOthers = current =>
document.querySelectorAll('audio, video').forEach(el => el !== current && !el.paused && el.pause());
const addPlayListener = el => el.addEventListener('play', e => pauseOthers(e.target));
new MutationObserver(mutations =>
mutations.forEach(m => m.addedNodes.forEach(n =>
(n.nodeName === 'AUDIO' || n.nodeName === 'VIDEO') && addPlayListener(n)
))
).observe(document.body, { childList: true, subtree: true });
document.querySelectorAll('audio, video').forEach(addPlayListener);
});
</script>

#122 - Abrir enlaces externos en una nueva pestaña
Haga que los enlaces externos se abran automáticamente en una nueva pestaña y añada atributos nofollow y noreferrer.
<!-- 💙 MEMBERSCRIPT #122 💙 - OPEN EXTERNAL LINKS IN NEW TAB -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const thisDomain = location.hostname;
const externalSelector = `a:not([href*="${thisDomain}"]):not([href^="/"]):not([href^="#"]):not([ms-code-ext-link="ignore"])`;
document.querySelectorAll(externalSelector).forEach(link => {
link.target = '_blank'; // Open in new tab (comment out to disable)
link.rel = (link.rel ? link.rel + ' ' : '') + 'noreferrer'; // Add noreferrer (comment out to disable)
link.rel = (link.rel ? link.rel + ' ' : '') + 'nofollow'; // Add nofollow (comment out to disable)
});
});
</script>
<!-- 💙 MEMBERSCRIPT #122 💙 - OPEN EXTERNAL LINKS IN NEW TAB -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const thisDomain = location.hostname;
const externalSelector = `a:not([href*="${thisDomain}"]):not([href^="/"]):not([href^="#"]):not([ms-code-ext-link="ignore"])`;
document.querySelectorAll(externalSelector).forEach(link => {
link.target = '_blank'; // Open in new tab (comment out to disable)
link.rel = (link.rel ? link.rel + ' ' : '') + 'noreferrer'; // Add noreferrer (comment out to disable)
link.rel = (link.rel ? link.rel + ' ' : '') + 'nofollow'; // Add nofollow (comment out to disable)
});
});
</script>

#121 - Renderizar Matriz desde Campo Personalizado
Muestre cualquier lista separada por comas a sus miembros en Webflow.
<!-- 💙 MEMBERSCRIPT #121 v0.1 💙 - RENDER ARRAY FROM CUSTOM FIELD -->
<script>
// Function to render arrays from Memberstack custom fields
function renderMemberstackArrays() {
// Get all elements with the ms-code-render-array attribute
const arrayElements = document.querySelectorAll('[ms-code-render-array]');
arrayElements.forEach(element => {
const customFieldKey = element.getAttribute('ms-code-render-array');
// Get Memberstack data from localStorage
const memberData = JSON.parse(localStorage.getItem('_ms-mem'));
if (!memberData || !memberData.customFields || !memberData.customFields[customFieldKey]) {
// If no data found, remove the element
element.remove();
return;
}
const arrayString = memberData.customFields[customFieldKey];
// Convert string to array, trim whitespace
const arrayItems = arrayString.split(',').map(item => item.trim()).filter(item => item !== '');
if (arrayItems.length === 0) {
// If array is empty, remove the element
element.remove();
return;
}
// Store the parent element
const parentElement = element.parentNode;
// Clone the template
const templateItem = element.cloneNode(true);
// Remove the ms-code-render-array attribute from the template
templateItem.removeAttribute('ms-code-render-array');
// Create a document fragment to hold the new items
const fragment = document.createDocumentFragment();
// Render array items
arrayItems.forEach(item => {
const newItem = templateItem.cloneNode(true);
// Replace the content of the new item
replaceContent(newItem, item);
fragment.appendChild(newItem);
});
// Replace the original element with the new items
parentElement.replaceChild(fragment, element);
});
}
// Helper function to replace content in the element
function replaceContent(element, newContent) {
if (element.childNodes.length > 0) {
element.childNodes.forEach(child => {
if (child.nodeType === Node.ELEMENT_NODE) {
replaceContent(child, newContent);
} else if (child.nodeType === Node.TEXT_NODE) {
child.textContent = newContent;
}
});
} else {
element.textContent = newContent;
}
}
// Run the function when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', renderMemberstackArrays);
</script>
<!-- 💙 MEMBERSCRIPT #121 v0.1 💙 - RENDER ARRAY FROM CUSTOM FIELD -->
<script>
// Function to render arrays from Memberstack custom fields
function renderMemberstackArrays() {
// Get all elements with the ms-code-render-array attribute
const arrayElements = document.querySelectorAll('[ms-code-render-array]');
arrayElements.forEach(element => {
const customFieldKey = element.getAttribute('ms-code-render-array');
// Get Memberstack data from localStorage
const memberData = JSON.parse(localStorage.getItem('_ms-mem'));
if (!memberData || !memberData.customFields || !memberData.customFields[customFieldKey]) {
// If no data found, remove the element
element.remove();
return;
}
const arrayString = memberData.customFields[customFieldKey];
// Convert string to array, trim whitespace
const arrayItems = arrayString.split(',').map(item => item.trim()).filter(item => item !== '');
if (arrayItems.length === 0) {
// If array is empty, remove the element
element.remove();
return;
}
// Store the parent element
const parentElement = element.parentNode;
// Clone the template
const templateItem = element.cloneNode(true);
// Remove the ms-code-render-array attribute from the template
templateItem.removeAttribute('ms-code-render-array');
// Create a document fragment to hold the new items
const fragment = document.createDocumentFragment();
// Render array items
arrayItems.forEach(item => {
const newItem = templateItem.cloneNode(true);
// Replace the content of the new item
replaceContent(newItem, item);
fragment.appendChild(newItem);
});
// Replace the original element with the new items
parentElement.replaceChild(fragment, element);
});
}
// Helper function to replace content in the element
function replaceContent(element, newContent) {
if (element.childNodes.length > 0) {
element.childNodes.forEach(child => {
if (child.nodeType === Node.ELEMENT_NODE) {
replaceContent(child, newContent);
} else if (child.nodeType === Node.TEXT_NODE) {
child.textContent = newContent;
}
});
} else {
element.textContent = newContent;
}
}
// Run the function when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', renderMemberstackArrays);
</script>

#120 - Mostrar/Ocultar Elemento Basado en Dispositivo, OS o Navegador
Visibilidad condicional en función del dispositivo, sistema operativo o navegador que utilice el visitante.
<!-- 💙 MEMBERSCRIPT #120 v0.1 💙 - DEVICE/OS/BROWSER CONDITIONAL VISIBILITY -->
<script>
// Define device types, operating systems, and browsers with their detection methods
const detectionTypes = {
// Device types
mobile: () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
tablet: () => /iPad|Android|Silk/i.test(navigator.userAgent) && !/Mobile/i.test(navigator.userAgent),
desktop: () => !(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)),
touchdevice: () => 'ontouchstart' in window || navigator.maxTouchPoints > 0,
landscape: () => window.matchMedia("(orientation: landscape)").matches,
portrait: () => window.matchMedia("(orientation: portrait)").matches,
// Operating Systems
ios: () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream,
android: () => /Android/.test(navigator.userAgent),
macos: () => /Mac OS X/.test(navigator.userAgent) && !(/iPad|iPhone|iPod/.test(navigator.userAgent)),
windows: () => /Win/.test(navigator.platform),
linux: () => /Linux/.test(navigator.platform),
// Browsers
chrome: () => /Chrome/.test(navigator.userAgent) && !/Chromium/.test(navigator.userAgent),
firefox: () => /Firefox/.test(navigator.userAgent),
safari: () => /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent),
edge: () => /Edg/.test(navigator.userAgent),
opera: () => /OPR/.test(navigator.userAgent) || /Opera/.test(navigator.userAgent),
ie: () => /Trident/.test(navigator.userAgent)
};
function detectTypes() {
const detected = [];
for (const [type, detector] of Object.entries(detectionTypes)) {
if (detector()) {
detected.push(type);
}
}
return detected;
}
function evaluateCondition(condition, currentTypes) {
const parts = condition.split('&').map(part => part.trim());
return parts.every(part => {
const orParts = part.split('|').map(p => p.trim());
return orParts.some(p => {
if (p.startsWith('!')) {
return !currentTypes.includes(p.slice(1));
}
return currentTypes.includes(p);
});
});
}
function updateElementVisibility() {
const currentTypes = detectTypes();
const elements = document.querySelectorAll('[ms-code-device-show]');
elements.forEach(element => {
const showAttribute = element.getAttribute('ms-code-device-show').toLowerCase();
const shouldShow = evaluateCondition(showAttribute, currentTypes);
element.style.display = shouldShow ? '' : 'none';
});
}
// Run on page load and window resize
window.addEventListener('load', updateElementVisibility);
window.addEventListener('resize', updateElementVisibility);
</script>
<!-- 💙 MEMBERSCRIPT #120 v0.1 💙 - DEVICE/OS/BROWSER CONDITIONAL VISIBILITY -->
<script>
// Define device types, operating systems, and browsers with their detection methods
const detectionTypes = {
// Device types
mobile: () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
tablet: () => /iPad|Android|Silk/i.test(navigator.userAgent) && !/Mobile/i.test(navigator.userAgent),
desktop: () => !(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)),
touchdevice: () => 'ontouchstart' in window || navigator.maxTouchPoints > 0,
landscape: () => window.matchMedia("(orientation: landscape)").matches,
portrait: () => window.matchMedia("(orientation: portrait)").matches,
// Operating Systems
ios: () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream,
android: () => /Android/.test(navigator.userAgent),
macos: () => /Mac OS X/.test(navigator.userAgent) && !(/iPad|iPhone|iPod/.test(navigator.userAgent)),
windows: () => /Win/.test(navigator.platform),
linux: () => /Linux/.test(navigator.platform),
// Browsers
chrome: () => /Chrome/.test(navigator.userAgent) && !/Chromium/.test(navigator.userAgent),
firefox: () => /Firefox/.test(navigator.userAgent),
safari: () => /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent),
edge: () => /Edg/.test(navigator.userAgent),
opera: () => /OPR/.test(navigator.userAgent) || /Opera/.test(navigator.userAgent),
ie: () => /Trident/.test(navigator.userAgent)
};
function detectTypes() {
const detected = [];
for (const [type, detector] of Object.entries(detectionTypes)) {
if (detector()) {
detected.push(type);
}
}
return detected;
}
function evaluateCondition(condition, currentTypes) {
const parts = condition.split('&').map(part => part.trim());
return parts.every(part => {
const orParts = part.split('|').map(p => p.trim());
return orParts.some(p => {
if (p.startsWith('!')) {
return !currentTypes.includes(p.slice(1));
}
return currentTypes.includes(p);
});
});
}
function updateElementVisibility() {
const currentTypes = detectTypes();
const elements = document.querySelectorAll('[ms-code-device-show]');
elements.forEach(element => {
const showAttribute = element.getAttribute('ms-code-device-show').toLowerCase();
const shouldShow = evaluateCondition(showAttribute, currentTypes);
element.style.display = shouldShow ? '' : 'none';
});
}
// Run on page load and window resize
window.addEventListener('load', updateElementVisibility);
window.addEventListener('resize', updateElementVisibility);
</script>

#119 - Calculadora de edad a partir de una fecha
Calcula el total de años transcurridos y rellena previamente una entrada con el número.
<!-- 💙 MEMBERSCRIPT #119 v0.1 💙 - AGE CALCULATOR FROM DATE INPUT -->
<script>
// Function to calculate age
function calculateAge(birthDate) {
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
}
// Function to update age input
function updateAgeInput(birthdayInput) {
const attrValue = birthdayInput.getAttribute('ms-code-bday-input');
const ageInput = document.querySelector(`[ms-code-age-input="${attrValue}"]`);
if (birthdayInput.value) {
const age = calculateAge(birthdayInput.value);
ageInput.value = age;
} else {
ageInput.value = '';
}
}
// Function to set up event listeners for all birthday inputs
function setupAgeCalculators() {
const birthdayInputs = document.querySelectorAll('[ms-code-bday-input]');
birthdayInputs.forEach(input => {
input.addEventListener('input', () => updateAgeInput(input));
// Initial call to set age if birthday is pre-filled
updateAgeInput(input);
});
}
// Run setup when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', setupAgeCalculators);
</script>
<!-- 💙 MEMBERSCRIPT #119 v0.1 💙 - AGE CALCULATOR FROM DATE INPUT -->
<script>
// Function to calculate age
function calculateAge(birthDate) {
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--;
}
return age;
}
// Function to update age input
function updateAgeInput(birthdayInput) {
const attrValue = birthdayInput.getAttribute('ms-code-bday-input');
const ageInput = document.querySelector(`[ms-code-age-input="${attrValue}"]`);
if (birthdayInput.value) {
const age = calculateAge(birthdayInput.value);
ageInput.value = age;
} else {
ageInput.value = '';
}
}
// Function to set up event listeners for all birthday inputs
function setupAgeCalculators() {
const birthdayInputs = document.querySelectorAll('[ms-code-bday-input]');
birthdayInputs.forEach(input => {
input.addEventListener('input', () => updateAgeInput(input));
// Initial call to set age if birthday is pre-filled
updateAgeInput(input);
});
}
// Run setup when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', setupAgeCalculators);
</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.

#118 - Guardar la última dirección IP del miembro
Actualice un campo personalizado con la dirección IP más reciente desde la que se conectan sus usuarios.
<!-- 💙 MEMBERSCRIPT #118 v0.1 💙 - SAVE LAST IP ADDRESS -->
<script>
const memberstack = window.$memberstackDom;
memberstack.getCurrentMember().then(async (response) => {
if (response && response.data) {
const member = response.data;
try {
// Fetch the current IP address from the ipify API
const ipResponse = await fetch('https://api.ipify.org?format=json');
const ipData = await ipResponse.json();
const currentIpAddress = ipData.ip;
// Retrieve the stored IP address from Memberstack's custom fields
const storedIpAddress = member.customFields["last-ip"];
// Check if the IP address has changed and update if necessary
if (currentIpAddress !== storedIpAddress) {
await memberstack.updateMember({
customFields: {
"last-ip": currentIpAddress
}
});
// Optional: Uncomment the line below to log a message when the IP is updated
// console.log('IP address updated');
} else {
// Optional: Uncomment the line below to log when the IP remains unchanged
// console.log('IP address unchanged, no update needed');
}
} catch (error) {
// Log any errors encountered during the fetch or update process
console.error('Error checking or updating member IP:', error);
}
} else {
// Optional: Uncomment the line below to log when no member is logged in
// console.log('No member is currently logged in');
}
});
</script>
<!-- 💙 MEMBERSCRIPT #118 v0.1 💙 - SAVE LAST IP ADDRESS -->
<script>
const memberstack = window.$memberstackDom;
memberstack.getCurrentMember().then(async (response) => {
if (response && response.data) {
const member = response.data;
try {
// Fetch the current IP address from the ipify API
const ipResponse = await fetch('https://api.ipify.org?format=json');
const ipData = await ipResponse.json();
const currentIpAddress = ipData.ip;
// Retrieve the stored IP address from Memberstack's custom fields
const storedIpAddress = member.customFields["last-ip"];
// Check if the IP address has changed and update if necessary
if (currentIpAddress !== storedIpAddress) {
await memberstack.updateMember({
customFields: {
"last-ip": currentIpAddress
}
});
// Optional: Uncomment the line below to log a message when the IP is updated
// console.log('IP address updated');
} else {
// Optional: Uncomment the line below to log when the IP remains unchanged
// console.log('IP address unchanged, no update needed');
}
} catch (error) {
// Log any errors encountered during the fetch or update process
console.error('Error checking or updating member IP:', error);
}
} else {
// Optional: Uncomment the line below to log when no member is logged in
// console.log('No member is currently logged in');
}
});
</script>

#117 - Barra de progreso de desplazamiento de página
Un indicador de desplazamiento de página flexible y personalizado para mostrar el progreso del desplazamiento de página.
<!-- 💙 MEMBERSCRIPT #117 v0.1 💙 - PAGE SCROLL PROGRESS BAR -->
<script>
// Function to update the progress bar
function updateProgressBar() {
const container = document.querySelector('[ms-code-ps="container"]');
const bar = document.querySelector('[ms-code-ps="bar"]');
const startElement = document.querySelector('[ms-code-ps="start"]');
const endElement = document.querySelector('[ms-code-ps="end"]');
if (!container || !bar) return;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
let startPosition = 0;
let endPosition = documentHeight - windowHeight;
if (startElement) {
const startRect = startElement.getBoundingClientRect();
startPosition = scrollTop + startRect.top - windowHeight;
}
if (endElement) {
const endRect = endElement.getBoundingClientRect();
endPosition = scrollTop + endRect.top - windowHeight;
}
const scrollRange = endPosition - startPosition;
const scrollProgress = scrollTop - startPosition;
const scrollPercentage = Math.max(0, Math.min(100, (scrollProgress / scrollRange) * 100));
// Use requestAnimationFrame for smooth animation
requestAnimationFrame(() => {
bar.style.width = `${scrollPercentage}%`;
bar.style.transition = 'width 0.1s linear';
});
}
// Throttle function to limit how often updateProgressBar is called
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Add scroll event listener with throttling
window.addEventListener('scroll', throttle(updateProgressBar, 10));
// Initial call to set the correct width on page load
updateProgressBar();
</script>
<!-- 💙 MEMBERSCRIPT #117 v0.1 💙 - PAGE SCROLL PROGRESS BAR -->
<script>
// Function to update the progress bar
function updateProgressBar() {
const container = document.querySelector('[ms-code-ps="container"]');
const bar = document.querySelector('[ms-code-ps="bar"]');
const startElement = document.querySelector('[ms-code-ps="start"]');
const endElement = document.querySelector('[ms-code-ps="end"]');
if (!container || !bar) return;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
let startPosition = 0;
let endPosition = documentHeight - windowHeight;
if (startElement) {
const startRect = startElement.getBoundingClientRect();
startPosition = scrollTop + startRect.top - windowHeight;
}
if (endElement) {
const endRect = endElement.getBoundingClientRect();
endPosition = scrollTop + endRect.top - windowHeight;
}
const scrollRange = endPosition - startPosition;
const scrollProgress = scrollTop - startPosition;
const scrollPercentage = Math.max(0, Math.min(100, (scrollProgress / scrollRange) * 100));
// Use requestAnimationFrame for smooth animation
requestAnimationFrame(() => {
bar.style.width = `${scrollPercentage}%`;
bar.style.transition = 'width 0.1s linear';
});
}
// Throttle function to limit how often updateProgressBar is called
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Add scroll event listener with throttling
window.addEventListener('scroll', throttle(updateProgressBar, 10));
// Initial call to set the correct width on page load
updateProgressBar();
</script>

#116 - Compartir enlaces de texto resaltados
Permita a los usuarios resaltar texto y compartir el enlace con los demás.
<!-- 💙 MEMBERSCRIPT #116 v0.1 💙 - SHARE HIGHLIGHTED TEXT LINKS -->
<script>
// Function to encode text and position for URL
function encodeSelection(text, nodeIndex, textOffset) {
return btoa(encodeURIComponent(JSON.stringify({ text, nodeIndex, textOffset })));
}
// Function to decode selection from URL
function decodeSelection(encoded) {
try {
return JSON.parse(decodeURIComponent(atob(encoded)));
} catch (e) {
// If parsing fails, assume it's just the text in the old format
return { text: decodeURIComponent(atob(encoded)) };
}
}
// Function to remove existing highlight
function removeExistingHighlight() {
const existingHighlight = document.querySelector('.ms-highlight');
if (existingHighlight) {
const parent = existingHighlight.parentNode;
parent.replaceChild(document.createTextNode(existingHighlight.textContent), existingHighlight);
parent.normalize(); // Merge adjacent text nodes
}
}
// Function to handle text selection
function handleSelection() {
const selection = window.getSelection();
if (selection.toString().length > 0) {
removeExistingHighlight();
const range = selection.getRangeAt(0);
const selectedText = selection.toString();
const textNodes = getAllTextNodes(document.body);
const nodeIndex = textNodes.indexOf(range.startContainer);
const textOffset = range.startOffset;
// Create a unique identifier for the selection
const selectionId = encodeSelection(selectedText, nodeIndex, textOffset);
// Update URL with the selection parameter
const url = new URL(window.location);
url.searchParams.set('highlight', selectionId);
window.history.pushState({}, '', url);
// Highlight the selected text
highlightText(selectionId, range);
}
}
// Function to highlight text
function highlightText(selectionId, range) {
const span = document.createElement('span');
span.className = 'ms-highlight';
span.id = selectionId;
range.surroundContents(span);
}
// Function to highlight and scroll to text based on URL parameter
function highlightFromURL() {
removeExistingHighlight();
const url = new URL(window.location);
const highlightId = url.searchParams.get('highlight');
if (highlightId) {
const { text, nodeIndex, textOffset } = decodeSelection(highlightId);
const textNodes = getAllTextNodes(document.body);
if (nodeIndex !== undefined && textOffset !== undefined) {
// Use precise location if available
if (nodeIndex < textNodes.length) {
const node = textNodes[nodeIndex];
if (node.textContent.substr(textOffset, text.length) === text) {
const range = document.createRange();
range.setStart(node, textOffset);
range.setEnd(node, textOffset + text.length);
highlightText(highlightId, range);
}
}
} else {
// Fall back to searching for the first occurrence of the text
for (let node of textNodes) {
const index = node.textContent.indexOf(text);
if (index !== -1) {
const range = document.createRange();
range.setStart(node, index);
range.setEnd(node, index + text.length);
highlightText(highlightId, range);
break;
}
}
}
const highlightedSpan = document.getElementById(highlightId);
if (highlightedSpan) {
highlightedSpan.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
// Helper function to get all text nodes
function getAllTextNodes(element) {
const textNodes = [];
const walk = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
let node;
while (node = walk.nextNode()) {
textNodes.push(node);
}
return textNodes;
}
// Add event listener for text selection
document.addEventListener('mouseup', handleSelection);
// Call highlightFromURL when the page loads
window.addEventListener('load', highlightFromURL);
</script>
<!-- 💙 MEMBERSCRIPT #116 v0.1 💙 - SHARE HIGHLIGHTED TEXT LINKS -->
<script>
// Function to encode text and position for URL
function encodeSelection(text, nodeIndex, textOffset) {
return btoa(encodeURIComponent(JSON.stringify({ text, nodeIndex, textOffset })));
}
// Function to decode selection from URL
function decodeSelection(encoded) {
try {
return JSON.parse(decodeURIComponent(atob(encoded)));
} catch (e) {
// If parsing fails, assume it's just the text in the old format
return { text: decodeURIComponent(atob(encoded)) };
}
}
// Function to remove existing highlight
function removeExistingHighlight() {
const existingHighlight = document.querySelector('.ms-highlight');
if (existingHighlight) {
const parent = existingHighlight.parentNode;
parent.replaceChild(document.createTextNode(existingHighlight.textContent), existingHighlight);
parent.normalize(); // Merge adjacent text nodes
}
}
// Function to handle text selection
function handleSelection() {
const selection = window.getSelection();
if (selection.toString().length > 0) {
removeExistingHighlight();
const range = selection.getRangeAt(0);
const selectedText = selection.toString();
const textNodes = getAllTextNodes(document.body);
const nodeIndex = textNodes.indexOf(range.startContainer);
const textOffset = range.startOffset;
// Create a unique identifier for the selection
const selectionId = encodeSelection(selectedText, nodeIndex, textOffset);
// Update URL with the selection parameter
const url = new URL(window.location);
url.searchParams.set('highlight', selectionId);
window.history.pushState({}, '', url);
// Highlight the selected text
highlightText(selectionId, range);
}
}
// Function to highlight text
function highlightText(selectionId, range) {
const span = document.createElement('span');
span.className = 'ms-highlight';
span.id = selectionId;
range.surroundContents(span);
}
// Function to highlight and scroll to text based on URL parameter
function highlightFromURL() {
removeExistingHighlight();
const url = new URL(window.location);
const highlightId = url.searchParams.get('highlight');
if (highlightId) {
const { text, nodeIndex, textOffset } = decodeSelection(highlightId);
const textNodes = getAllTextNodes(document.body);
if (nodeIndex !== undefined && textOffset !== undefined) {
// Use precise location if available
if (nodeIndex < textNodes.length) {
const node = textNodes[nodeIndex];
if (node.textContent.substr(textOffset, text.length) === text) {
const range = document.createRange();
range.setStart(node, textOffset);
range.setEnd(node, textOffset + text.length);
highlightText(highlightId, range);
}
}
} else {
// Fall back to searching for the first occurrence of the text
for (let node of textNodes) {
const index = node.textContent.indexOf(text);
if (index !== -1) {
const range = document.createRange();
range.setStart(node, index);
range.setEnd(node, index + text.length);
highlightText(highlightId, range);
break;
}
}
}
const highlightedSpan = document.getElementById(highlightId);
if (highlightedSpan) {
highlightedSpan.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
// Helper function to get all text nodes
function getAllTextNodes(element) {
const textNodes = [];
const walk = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
let node;
while (node = walk.nextNode()) {
textNodes.push(node);
}
return textNodes;
}
// Add event listener for text selection
document.addEventListener('mouseup', handleSelection);
// Call highlightFromURL when the page loads
window.addEventListener('load', highlightFromURL);
</script>

#115 - Generar una contraseña aleatoria
Registro sin fricciones. Exigir o permitir que los miembros establezcan una contraseña en el futuro.
<!-- 💙 MEMBERSCRIPT #115 v0.1 💙 - GENERATE PASSWORD-->
<script>
document.addEventListener('DOMContentLoaded', function() {
var passwordInput = document.querySelector('[data-ms-member="password"]');
if (passwordInput) {
// Function to generate random password
function generatePassword() {
var timestamp = Date.now().toString(36);
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}[]|:;<>,.?/~';
var randomChars = '';
for (var i = 0; i < 16; i++) {
randomChars += characters.charAt(Math.floor(Math.random() * characters.length));
}
return (timestamp + randomChars).slice(0, 32);
}
// Generate and set password
passwordInput.value = generatePassword();
// Block password managers and prevent editing
passwordInput.setAttribute('autocomplete', 'off');
passwordInput.setAttribute('readonly', 'readonly');
// Prevent copy and paste
passwordInput.addEventListener('copy', function(e) {
e.preventDefault();
});
passwordInput.addEventListener('paste', function(e) {
e.preventDefault();
});
// Prevent dragging
passwordInput.addEventListener('dragstart', function(e) {
e.preventDefault();
});
// Prevent context menu
passwordInput.addEventListener('contextmenu', function(e) {
e.preventDefault();
});
}
});
</script>
<!-- 💙 MEMBERSCRIPT #115 v0.1 💙 - GENERATE PASSWORD-->
<script>
document.addEventListener('DOMContentLoaded', function() {
var passwordInput = document.querySelector('[data-ms-member="password"]');
if (passwordInput) {
// Function to generate random password
function generatePassword() {
var timestamp = Date.now().toString(36);
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}[]|:;<>,.?/~';
var randomChars = '';
for (var i = 0; i < 16; i++) {
randomChars += characters.charAt(Math.floor(Math.random() * characters.length));
}
return (timestamp + randomChars).slice(0, 32);
}
// Generate and set password
passwordInput.value = generatePassword();
// Block password managers and prevent editing
passwordInput.setAttribute('autocomplete', 'off');
passwordInput.setAttribute('readonly', 'readonly');
// Prevent copy and paste
passwordInput.addEventListener('copy', function(e) {
e.preventDefault();
});
passwordInput.addEventListener('paste', function(e) {
e.preventDefault();
});
// Prevent dragging
passwordInput.addEventListener('dragstart', function(e) {
e.preventDefault();
});
// Prevent context menu
passwordInput.addEventListener('contextmenu', function(e) {
e.preventDefault();
});
}
});
</script>

#114 - Botón de desplazamiento al principio
Añade un botón que se desplazará a la parte superior de la página cuando se haga clic en él,
<!-- 💙 MEMBERSCRIPT #114 v0.1 💙 - SCROLL TO TOP BUTTON -->
<script>
document.addEventListener('DOMContentLoaded', function() {
var scrollTopButton = document.querySelector('[ms-code-scroll-top="button"]');
if (scrollTopButton) {
// Set initial styles
scrollTopButton.style.opacity = '0';
scrollTopButton.style.visibility = 'hidden';
scrollTopButton.style.transition = 'opacity 0.3s, visibility 0.3s';
// Function to check scroll position and toggle button visibility
function toggleButtonVisibility() {
if (window.pageYOffset > 300) {
scrollTopButton.style.opacity = '1';
scrollTopButton.style.visibility = 'visible';
} else {
scrollTopButton.style.opacity = '0';
scrollTopButton.style.visibility = 'hidden';
}
}
// Initial check on page load
toggleButtonVisibility();
// Check on scroll
window.addEventListener('scroll', toggleButtonVisibility);
// Scroll to top when button is clicked
scrollTopButton.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
});
</script>
<!-- 💙 MEMBERSCRIPT #114 v0.1 💙 - SCROLL TO TOP BUTTON -->
<script>
document.addEventListener('DOMContentLoaded', function() {
var scrollTopButton = document.querySelector('[ms-code-scroll-top="button"]');
if (scrollTopButton) {
// Set initial styles
scrollTopButton.style.opacity = '0';
scrollTopButton.style.visibility = 'hidden';
scrollTopButton.style.transition = 'opacity 0.3s, visibility 0.3s';
// Function to check scroll position and toggle button visibility
function toggleButtonVisibility() {
if (window.pageYOffset > 300) {
scrollTopButton.style.opacity = '1';
scrollTopButton.style.visibility = 'visible';
} else {
scrollTopButton.style.opacity = '0';
scrollTopButton.style.visibility = 'hidden';
}
}
// Initial check on page load
toggleButtonVisibility();
// Check on scroll
window.addEventListener('scroll', toggleButtonVisibility);
// Scroll to top when button is clicked
scrollTopButton.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
});
</script>

#113 - Canales RSS
Utilice una interfaz de usuario Webflow para mostrar un canal RSS directamente en su sitio web.
<!-- 💙 MEMBERSCRIPT #113 v0.2 💙 - RSS FEEDS IN WEBFLOW -->
<script>
(function() {
// console.log('RSS Feed Script starting...');
const CORS_PROXIES = [
'https://corsproxy.io/?',
'https://api.allorigins.win/raw?url=',
'https://cors-anywhere.herokuapp.com/',
'https://thingproxy.freeboard.io/fetch/',
'https://yacdn.org/proxy/'
];
function loadScript(src, onLoad, onError) {
const script = document.createElement('script');
script.src = src;
script.onload = onLoad;
script.onerror = onError;
document.head.appendChild(script);
}
async function fetchWithFallback(url) {
for (const proxy of CORS_PROXIES) {
try {
const response = await fetch(proxy + encodeURIComponent(url));
if (response.ok) {
return await response.text();
}
} catch (error) {
console.warn(`Failed to fetch with proxy ${proxy}:`, error);
}
}
throw new Error('All CORS proxies failed');
}
function initRSSFeed() {
if (typeof RSSParser === 'undefined') {
console.error('RSSParser is not defined.');
return;
}
const parser = new RSSParser({
customFields: {
item: [
['media:content', 'mediaContent', {keepArray: true}],
['media:thumbnail', 'mediaThumbnail', {keepArray: true}],
['enclosure', 'enclosure', {keepArray: true}],
]
}
});
document.querySelectorAll('[ms-code-rss-feed]').forEach(element => {
const url = element.getAttribute('ms-code-rss-url');
const limit = parseInt(element.getAttribute('ms-code-rss-limit')) || 5;
fetchWithFallback(url)
.then(str => parser.parseString(str))
.then(feed => {
renderRSSItems(element, feed.items.slice(0, limit), {
showImage: element.getAttribute('ms-code-rss-show-image') !== 'false',
showDate: element.getAttribute('ms-code-rss-show-date') !== 'false',
dateFormat: element.getAttribute('ms-code-rss-date-format') || 'short',
target: element.getAttribute('ms-code-rss-target') || '_self'
});
})
.catch(err => {
console.error('Error fetching or parsing RSS feed:', err);
element.textContent = `Failed to load RSS feed from ${url}. Error: ${err.message}`;
});
});
}
function renderRSSItems(element, items, options) {
const templateItem = element.querySelector('[ms-code-rss-item]');
if (!templateItem) return;
element.innerHTML = ''; // Clear existing items
items.forEach(item => {
const itemElement = templateItem.cloneNode(true);
const title = itemElement.querySelector('[ms-code-rss-title]');
if (title) {
const titleLength = parseInt(title.getAttribute('ms-code-rss-title-length')) || Infinity;
title.textContent = truncate(item.title, titleLength);
}
const description = itemElement.querySelector('[ms-code-rss-description]');
if (description) {
const descriptionLength = parseInt(description.getAttribute('ms-code-rss-description-length')) || Infinity;
description.textContent = truncate(stripHtml(item.content || item.description), descriptionLength);
}
const date = itemElement.querySelector('[ms-code-rss-date]');
if (date && options.showDate && item.pubDate) {
date.textContent = formatDate(new Date(item.pubDate), options.dateFormat);
}
const img = itemElement.querySelector('[ms-code-rss-image]');
if (img && options.showImage) {
const imgUrl = getImageUrl(item);
if (imgUrl) {
img.src = imgUrl;
img.alt = item.title;
img.removeAttribute('srcset');
}
}
const linkElement = itemElement.querySelector('[ms-code-rss-link]');
if (linkElement) {
linkElement.setAttribute('href', item.link);
linkElement.setAttribute('target', options.target);
}
element.appendChild(itemElement);
});
}
function getImageUrl(item) {
const sources = ['mediaContent', 'mediaThumbnail', 'enclosure'];
for (let source of sources) {
if (item[source] && item[source][0]) {
return item[source][0].$ ? item[source][0].$.url : item[source][0].url;
}
}
return null;
}
function truncate(str, length) {
if (!str) return '';
if (length === Infinity) return str;
return str.length > length ? str.slice(0, length) + '...' : str;
}
function stripHtml(html) {
const tmp = document.createElement('DIV');
tmp.innerHTML = html || '';
return tmp.textContent || tmp.innerText || '';
}
function formatDate(date, format) {
if (!(date instanceof Date) || isNaN(date)) return '';
const options = format === 'long' ?
{ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' } :
undefined;
return format === 'relative' ? getRelativeTimeString(date) : date.toLocaleDateString(undefined, options);
}
function getRelativeTimeString(date, lang = navigator.language) {
const timeMs = date.getTime();
const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);
const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];
const units = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'];
const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));
const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });
return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
}
loadScript('https://cdn.jsdelivr.net/npm/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
console.error('Error loading RSS Parser script');
loadScript('https://unpkg.com/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
console.error('Error loading RSS Parser script from backup CDN');
});
});
})();
</script>
<!-- 💙 MEMBERSCRIPT #113 v0.2 💙 - RSS FEEDS IN WEBFLOW -->
<script>
(function() {
// console.log('RSS Feed Script starting...');
const CORS_PROXIES = [
'https://corsproxy.io/?',
'https://api.allorigins.win/raw?url=',
'https://cors-anywhere.herokuapp.com/',
'https://thingproxy.freeboard.io/fetch/',
'https://yacdn.org/proxy/'
];
function loadScript(src, onLoad, onError) {
const script = document.createElement('script');
script.src = src;
script.onload = onLoad;
script.onerror = onError;
document.head.appendChild(script);
}
async function fetchWithFallback(url) {
for (const proxy of CORS_PROXIES) {
try {
const response = await fetch(proxy + encodeURIComponent(url));
if (response.ok) {
return await response.text();
}
} catch (error) {
console.warn(`Failed to fetch with proxy ${proxy}:`, error);
}
}
throw new Error('All CORS proxies failed');
}
function initRSSFeed() {
if (typeof RSSParser === 'undefined') {
console.error('RSSParser is not defined.');
return;
}
const parser = new RSSParser({
customFields: {
item: [
['media:content', 'mediaContent', {keepArray: true}],
['media:thumbnail', 'mediaThumbnail', {keepArray: true}],
['enclosure', 'enclosure', {keepArray: true}],
]
}
});
document.querySelectorAll('[ms-code-rss-feed]').forEach(element => {
const url = element.getAttribute('ms-code-rss-url');
const limit = parseInt(element.getAttribute('ms-code-rss-limit')) || 5;
fetchWithFallback(url)
.then(str => parser.parseString(str))
.then(feed => {
renderRSSItems(element, feed.items.slice(0, limit), {
showImage: element.getAttribute('ms-code-rss-show-image') !== 'false',
showDate: element.getAttribute('ms-code-rss-show-date') !== 'false',
dateFormat: element.getAttribute('ms-code-rss-date-format') || 'short',
target: element.getAttribute('ms-code-rss-target') || '_self'
});
})
.catch(err => {
console.error('Error fetching or parsing RSS feed:', err);
element.textContent = `Failed to load RSS feed from ${url}. Error: ${err.message}`;
});
});
}
function renderRSSItems(element, items, options) {
const templateItem = element.querySelector('[ms-code-rss-item]');
if (!templateItem) return;
element.innerHTML = ''; // Clear existing items
items.forEach(item => {
const itemElement = templateItem.cloneNode(true);
const title = itemElement.querySelector('[ms-code-rss-title]');
if (title) {
const titleLength = parseInt(title.getAttribute('ms-code-rss-title-length')) || Infinity;
title.textContent = truncate(item.title, titleLength);
}
const description = itemElement.querySelector('[ms-code-rss-description]');
if (description) {
const descriptionLength = parseInt(description.getAttribute('ms-code-rss-description-length')) || Infinity;
description.textContent = truncate(stripHtml(item.content || item.description), descriptionLength);
}
const date = itemElement.querySelector('[ms-code-rss-date]');
if (date && options.showDate && item.pubDate) {
date.textContent = formatDate(new Date(item.pubDate), options.dateFormat);
}
const img = itemElement.querySelector('[ms-code-rss-image]');
if (img && options.showImage) {
const imgUrl = getImageUrl(item);
if (imgUrl) {
img.src = imgUrl;
img.alt = item.title;
img.removeAttribute('srcset');
}
}
const linkElement = itemElement.querySelector('[ms-code-rss-link]');
if (linkElement) {
linkElement.setAttribute('href', item.link);
linkElement.setAttribute('target', options.target);
}
element.appendChild(itemElement);
});
}
function getImageUrl(item) {
const sources = ['mediaContent', 'mediaThumbnail', 'enclosure'];
for (let source of sources) {
if (item[source] && item[source][0]) {
return item[source][0].$ ? item[source][0].$.url : item[source][0].url;
}
}
return null;
}
function truncate(str, length) {
if (!str) return '';
if (length === Infinity) return str;
return str.length > length ? str.slice(0, length) + '...' : str;
}
function stripHtml(html) {
const tmp = document.createElement('DIV');
tmp.innerHTML = html || '';
return tmp.textContent || tmp.innerText || '';
}
function formatDate(date, format) {
if (!(date instanceof Date) || isNaN(date)) return '';
const options = format === 'long' ?
{ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' } :
undefined;
return format === 'relative' ? getRelativeTimeString(date) : date.toLocaleDateString(undefined, options);
}
function getRelativeTimeString(date, lang = navigator.language) {
const timeMs = date.getTime();
const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);
const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];
const units = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'];
const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));
const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });
return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
}
loadScript('https://cdn.jsdelivr.net/npm/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
console.error('Error loading RSS Parser script');
loadScript('https://unpkg.com/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
console.error('Error loading RSS Parser script from backup CDN');
});
});
})();
</script>

#112 - Deslizadores Antes y Después
Añada fácilmente un deslizador de fotos de antes/después a su sitio Webflow.
<!-- 💙 MEMBERSCRIPT #112 v0.1 💙 - BEFORE & AFTER SLIDERS -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const wraps = document.querySelectorAll('[ms-code-ba-wrap]');
wraps.forEach(wrap => {
const before = wrap.querySelector('[ms-code-ba-before]');
const after = wrap.querySelector('[ms-code-ba-after]');
// Create slider element
const slider = document.createElement('div');
slider.setAttribute('ms-code-ba-slider', wrap.getAttribute('ms-code-ba-wrap'));
wrap.appendChild(slider);
let isDown = false;
// Ensure proper positioning
wrap.style.position = 'relative';
wrap.style.overflow = 'hidden';
before.style.width = '100%';
before.style.display = 'block';
after.style.position = 'absolute';
after.style.top = '0';
after.style.left = '0';
after.style.width = '100%';
after.style.height = '100%';
slider.style.position = 'absolute';
slider.style.top = '0';
slider.style.bottom = '0';
slider.style.width = '4px';
slider.style.background = 'white';
slider.style.cursor = 'ew-resize';
slider.style.zIndex = '3';
const setPosition = (position) => {
const clampedPosition = Math.max(0, Math.min(1, position));
slider.style.left = `${clampedPosition * 100}%`;
after.style.clipPath = `inset(0 0 0 ${clampedPosition * 100}%)`;
};
const move = (e) => {
if (!isDown && e.type !== 'mousemove') return;
e.preventDefault();
const x = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
const rect = wrap.getBoundingClientRect();
const position = (x - rect.left) / rect.width;
setPosition(position);
};
const easeBack = () => {
setPosition(0.5); // Move back to center
};
wrap.addEventListener('mousedown', () => isDown = true);
wrap.addEventListener('mouseup', () => isDown = false);
wrap.addEventListener('mouseleave', () => {
isDown = false;
easeBack();
});
wrap.addEventListener('mousemove', move);
wrap.addEventListener('touchstart', (e) => {
isDown = true;
move(e);
});
wrap.addEventListener('touchmove', move);
wrap.addEventListener('touchend', () => {
isDown = false;
easeBack();
});
// Initialize position
setPosition(0.5);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #112 v0.1 💙 - BEFORE & AFTER SLIDERS -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const wraps = document.querySelectorAll('[ms-code-ba-wrap]');
wraps.forEach(wrap => {
const before = wrap.querySelector('[ms-code-ba-before]');
const after = wrap.querySelector('[ms-code-ba-after]');
// Create slider element
const slider = document.createElement('div');
slider.setAttribute('ms-code-ba-slider', wrap.getAttribute('ms-code-ba-wrap'));
wrap.appendChild(slider);
let isDown = false;
// Ensure proper positioning
wrap.style.position = 'relative';
wrap.style.overflow = 'hidden';
before.style.width = '100%';
before.style.display = 'block';
after.style.position = 'absolute';
after.style.top = '0';
after.style.left = '0';
after.style.width = '100%';
after.style.height = '100%';
slider.style.position = 'absolute';
slider.style.top = '0';
slider.style.bottom = '0';
slider.style.width = '4px';
slider.style.background = 'white';
slider.style.cursor = 'ew-resize';
slider.style.zIndex = '3';
const setPosition = (position) => {
const clampedPosition = Math.max(0, Math.min(1, position));
slider.style.left = `${clampedPosition * 100}%`;
after.style.clipPath = `inset(0 0 0 ${clampedPosition * 100}%)`;
};
const move = (e) => {
if (!isDown && e.type !== 'mousemove') return;
e.preventDefault();
const x = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
const rect = wrap.getBoundingClientRect();
const position = (x - rect.left) / rect.width;
setPosition(position);
};
const easeBack = () => {
setPosition(0.5); // Move back to center
};
wrap.addEventListener('mousedown', () => isDown = true);
wrap.addEventListener('mouseup', () => isDown = false);
wrap.addEventListener('mouseleave', () => {
isDown = false;
easeBack();
});
wrap.addEventListener('mousemove', move);
wrap.addEventListener('touchstart', (e) => {
isDown = true;
move(e);
});
wrap.addEventListener('touchmove', move);
wrap.addEventListener('touchend', () => {
isDown = false;
easeBack();
});
// Initialize position
setPosition(0.5);
});
});
</script>

#111 - Pestañas con rotación automática
La forma más sencilla de hacer que tus pestañas roten automáticamente con un temporizador.
<!-- 💙 MEMBERSCRIPT #111 v0.1 💙 - AUTO-ROTATING TABS -->
<script>
// Function to rotate tabs
function initializeTabRotator() {
// Find all tab containers with the ms-code-rotate-tabs attribute
const tabContainers = document.querySelectorAll('[ms-code-rotate-tabs]');
tabContainers.forEach(container => {
const interval = parseInt(container.getAttribute('ms-code-rotate-tabs'), 10);
const tabLinks = container.querySelectorAll('.w-tab-link');
const tabContent = container.closest('.w-tabs').querySelector('.w-tab-content');
const tabPanes = tabContent.querySelectorAll('.w-tab-pane');
let currentIndex = Array.from(tabLinks).findIndex(link => link.classList.contains('w--current'));
let rotationTimer;
// ANIMATION CONFIGURATION
// Modify these values to adjust the animation behavior
const FADE_OUT_DURATION = 300; // Duration for fading out the current tab (in milliseconds)
const FADE_IN_DURATION = 100; // Duration for fading in the new tab (in milliseconds)
const EASING_FUNCTION = 'ease'; // Choose from: 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'
// or use a cubic-bezier function like 'cubic-bezier(0.1, 0.7, 1.0, 0.1)'
// Additional easing options (uncomment to use):
// const EASING_FUNCTION = 'ease-in-quad';
// const EASING_FUNCTION = 'ease-out-quad';
// const EASING_FUNCTION = 'ease-in-out-quad';
// const EASING_FUNCTION = 'ease-in-cubic';
// const EASING_FUNCTION = 'ease-out-cubic';
// const EASING_FUNCTION = 'ease-in-out-cubic';
// const EASING_FUNCTION = 'ease-in-quart';
// const EASING_FUNCTION = 'ease-out-quart';
// const EASING_FUNCTION = 'ease-in-out-quart';
// const EASING_FUNCTION = 'ease-in-quint';
// const EASING_FUNCTION = 'ease-out-quint';
// const EASING_FUNCTION = 'ease-in-out-quint';
// const EASING_FUNCTION = 'ease-in-sine';
// const EASING_FUNCTION = 'ease-out-sine';
// const EASING_FUNCTION = 'ease-in-out-sine';
// const EASING_FUNCTION = 'ease-in-expo';
// const EASING_FUNCTION = 'ease-out-expo';
// const EASING_FUNCTION = 'ease-in-out-expo';
// const EASING_FUNCTION = 'ease-in-circ';
// const EASING_FUNCTION = 'ease-out-circ';
// const EASING_FUNCTION = 'ease-in-out-circ';
// const EASING_FUNCTION = 'ease-in-back';
// const EASING_FUNCTION = 'ease-out-back';
// const EASING_FUNCTION = 'ease-in-out-back';
// END OF ANIMATION CONFIGURATION
function switchToTab(index) {
// Fade out current tab
tabPanes[currentIndex].style.transition = `opacity ${FADE_OUT_DURATION}ms ${EASING_FUNCTION}`;
tabPanes[currentIndex].style.opacity = '0';
setTimeout(() => {
// Remove active classes and update ARIA attributes for current tab and pane
tabLinks[currentIndex].classList.remove('w--current');
tabLinks[currentIndex].setAttribute('aria-selected', 'false');
tabLinks[currentIndex].setAttribute('tabindex', '-1');
tabPanes[currentIndex].classList.remove('w--tab-active');
// Update current index
currentIndex = index;
// Add active classes and update ARIA attributes for new current tab and pane
tabLinks[currentIndex].classList.add('w--current');
tabLinks[currentIndex].setAttribute('aria-selected', 'true');
tabLinks[currentIndex].setAttribute('tabindex', '0');
tabPanes[currentIndex].classList.add('w--tab-active');
// Fade in new tab
tabPanes[currentIndex].style.transition = `opacity ${FADE_IN_DURATION}ms ${EASING_FUNCTION}`;
tabPanes[currentIndex].style.opacity = '1';
// Update the data-current attribute on the parent w-tabs element
const wTabsElement = container.closest('.w-tabs');
if (wTabsElement) {
wTabsElement.setAttribute('data-current', tabLinks[currentIndex].getAttribute('data-w-tab'));
}
}, FADE_OUT_DURATION);
}
function rotateToNextTab() {
const nextIndex = (currentIndex + 1) % tabLinks.length;
switchToTab(nextIndex);
}
function startRotation() {
clearInterval(rotationTimer);
rotationTimer = setInterval(rotateToNextTab, interval);
}
// Add click event listeners to tab links
tabLinks.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
switchToTab(index);
startRotation(); // Restart rotation from this tab
});
});
// Start the initial rotation
startRotation();
});
}
// Run the function when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initializeTabRotator);
</script>
<!-- 💙 MEMBERSCRIPT #111 v0.1 💙 - AUTO-ROTATING TABS -->
<script>
// Function to rotate tabs
function initializeTabRotator() {
// Find all tab containers with the ms-code-rotate-tabs attribute
const tabContainers = document.querySelectorAll('[ms-code-rotate-tabs]');
tabContainers.forEach(container => {
const interval = parseInt(container.getAttribute('ms-code-rotate-tabs'), 10);
const tabLinks = container.querySelectorAll('.w-tab-link');
const tabContent = container.closest('.w-tabs').querySelector('.w-tab-content');
const tabPanes = tabContent.querySelectorAll('.w-tab-pane');
let currentIndex = Array.from(tabLinks).findIndex(link => link.classList.contains('w--current'));
let rotationTimer;
// ANIMATION CONFIGURATION
// Modify these values to adjust the animation behavior
const FADE_OUT_DURATION = 300; // Duration for fading out the current tab (in milliseconds)
const FADE_IN_DURATION = 100; // Duration for fading in the new tab (in milliseconds)
const EASING_FUNCTION = 'ease'; // Choose from: 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'
// or use a cubic-bezier function like 'cubic-bezier(0.1, 0.7, 1.0, 0.1)'
// Additional easing options (uncomment to use):
// const EASING_FUNCTION = 'ease-in-quad';
// const EASING_FUNCTION = 'ease-out-quad';
// const EASING_FUNCTION = 'ease-in-out-quad';
// const EASING_FUNCTION = 'ease-in-cubic';
// const EASING_FUNCTION = 'ease-out-cubic';
// const EASING_FUNCTION = 'ease-in-out-cubic';
// const EASING_FUNCTION = 'ease-in-quart';
// const EASING_FUNCTION = 'ease-out-quart';
// const EASING_FUNCTION = 'ease-in-out-quart';
// const EASING_FUNCTION = 'ease-in-quint';
// const EASING_FUNCTION = 'ease-out-quint';
// const EASING_FUNCTION = 'ease-in-out-quint';
// const EASING_FUNCTION = 'ease-in-sine';
// const EASING_FUNCTION = 'ease-out-sine';
// const EASING_FUNCTION = 'ease-in-out-sine';
// const EASING_FUNCTION = 'ease-in-expo';
// const EASING_FUNCTION = 'ease-out-expo';
// const EASING_FUNCTION = 'ease-in-out-expo';
// const EASING_FUNCTION = 'ease-in-circ';
// const EASING_FUNCTION = 'ease-out-circ';
// const EASING_FUNCTION = 'ease-in-out-circ';
// const EASING_FUNCTION = 'ease-in-back';
// const EASING_FUNCTION = 'ease-out-back';
// const EASING_FUNCTION = 'ease-in-out-back';
// END OF ANIMATION CONFIGURATION
function switchToTab(index) {
// Fade out current tab
tabPanes[currentIndex].style.transition = `opacity ${FADE_OUT_DURATION}ms ${EASING_FUNCTION}`;
tabPanes[currentIndex].style.opacity = '0';
setTimeout(() => {
// Remove active classes and update ARIA attributes for current tab and pane
tabLinks[currentIndex].classList.remove('w--current');
tabLinks[currentIndex].setAttribute('aria-selected', 'false');
tabLinks[currentIndex].setAttribute('tabindex', '-1');
tabPanes[currentIndex].classList.remove('w--tab-active');
// Update current index
currentIndex = index;
// Add active classes and update ARIA attributes for new current tab and pane
tabLinks[currentIndex].classList.add('w--current');
tabLinks[currentIndex].setAttribute('aria-selected', 'true');
tabLinks[currentIndex].setAttribute('tabindex', '0');
tabPanes[currentIndex].classList.add('w--tab-active');
// Fade in new tab
tabPanes[currentIndex].style.transition = `opacity ${FADE_IN_DURATION}ms ${EASING_FUNCTION}`;
tabPanes[currentIndex].style.opacity = '1';
// Update the data-current attribute on the parent w-tabs element
const wTabsElement = container.closest('.w-tabs');
if (wTabsElement) {
wTabsElement.setAttribute('data-current', tabLinks[currentIndex].getAttribute('data-w-tab'));
}
}, FADE_OUT_DURATION);
}
function rotateToNextTab() {
const nextIndex = (currentIndex + 1) % tabLinks.length;
switchToTab(nextIndex);
}
function startRotation() {
clearInterval(rotationTimer);
rotationTimer = setInterval(rotateToNextTab, interval);
}
// Add click event listeners to tab links
tabLinks.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
switchToTab(index);
startRotation(); // Restart rotation from this tab
});
});
// Start the initial rotation
startRotation();
});
}
// Run the function when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initializeTabRotator);
</script>

#110 - Tooltips para Webflow
Añada fácilmente tooltips Tippy.js a su sitio Webflow con atributos.
<!-- 💙 MEMBERSCRIPT #110 v0.1 💙 - TOOLTIPS FOR WEBFLOW -->
<script>
// Function to load Tippy.js, its CSS, and additional theme/animation CSS
function loadTippy(callback) {
// Load Tippy.js script
const script = document.createElement('script');
script.src = 'https://unpkg.com/@popperjs/core@2';
script.onload = function() {
const tippyScript = document.createElement('script');
tippyScript.src = 'https://unpkg.com/tippy.js@6';
tippyScript.onload = function() {
// Load Tippy.js CSS
const cssFiles = [
'https://unpkg.com/tippy.js@6/dist/tippy.css',
'https://unpkg.com/tippy.js@6/themes/light.css',
'https://unpkg.com/tippy.js@6/themes/light-border.css',
'https://unpkg.com/tippy.js@6/animations/shift-away.css',
'https://unpkg.com/tippy.js@6/animations/shift-toward.css',
'https://unpkg.com/tippy.js@6/animations/scale.css',
'https://unpkg.com/tippy.js@6/animations/perspective.css'
];
let loadedCount = 0;
cssFiles.forEach(file => {
const link = document.createElement('link');
link.href = file;
link.rel = 'stylesheet';
link.onload = function() {
loadedCount++;
if (loadedCount === cssFiles.length) {
// Call the callback function when everything is loaded
callback();
}
};
document.head.appendChild(link);
});
};
document.head.appendChild(tippyScript);
};
document.head.appendChild(script);
}
// Function to initialize Tippy tooltips
function initializeTippyTooltips() {
// Select all elements with any ms-code-tooltip-* attribute
const elements = document.querySelectorAll('[ms-code-tooltip-top], [ms-code-tooltip-bottom], [ms-code-tooltip-left], [ms-code-tooltip-right], [ms-code-tooltip-content]');
elements.forEach(element => {
const tippyOptions = {};
// Content and Placement
if (element.hasAttribute('ms-code-tooltip-top')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-top');
tippyOptions.placement = 'top';
} else if (element.hasAttribute('ms-code-tooltip-bottom')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-bottom');
tippyOptions.placement = 'bottom';
} else if (element.hasAttribute('ms-code-tooltip-left')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-left');
tippyOptions.placement = 'left';
} else if (element.hasAttribute('ms-code-tooltip-right')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-right');
tippyOptions.placement = 'right';
} else if (element.hasAttribute('ms-code-tooltip-content')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-content');
}
if (element.hasAttribute('ms-code-tooltip-placement')) {
tippyOptions.placement = element.getAttribute('ms-code-tooltip-placement');
}
// Theme
if (element.hasAttribute('ms-code-tooltip-theme')) {
tippyOptions.theme = element.getAttribute('ms-code-tooltip-theme');
}
// Animation
if (element.hasAttribute('ms-code-tooltip-animation')) {
tippyOptions.animation = element.getAttribute('ms-code-tooltip-animation');
}
// Max Width
if (element.hasAttribute('ms-code-tooltip-maxwidth')) {
tippyOptions.maxWidth = parseInt(element.getAttribute('ms-code-tooltip-maxwidth'));
}
// Delay
if (element.hasAttribute('ms-code-tooltip-delay')) {
tippyOptions.delay = JSON.parse(element.getAttribute('ms-code-tooltip-delay'));
}
// Duration
if (element.hasAttribute('ms-code-tooltip-duration')) {
tippyOptions.duration = JSON.parse(element.getAttribute('ms-code-tooltip-duration'));
}
// Interactive
if (element.hasAttribute('ms-code-tooltip-interactive')) {
tippyOptions.interactive = element.getAttribute('ms-code-tooltip-interactive') === 'true';
}
// Arrow
if (element.hasAttribute('ms-code-tooltip-arrow')) {
tippyOptions.arrow = element.getAttribute('ms-code-tooltip-arrow') === 'true';
}
// Trigger
if (element.hasAttribute('ms-code-tooltip-trigger')) {
tippyOptions.trigger = element.getAttribute('ms-code-tooltip-trigger');
}
// Hide On Click
if (element.hasAttribute('ms-code-tooltip-hideOnClick')) {
tippyOptions.hideOnClick = element.getAttribute('ms-code-tooltip-hideOnClick') === 'true';
}
// Follow Cursor
if (element.hasAttribute('ms-code-tooltip-followCursor')) {
tippyOptions.followCursor = element.getAttribute('ms-code-tooltip-followCursor');
}
// Offset
if (element.hasAttribute('ms-code-tooltip-offset')) {
tippyOptions.offset = JSON.parse(element.getAttribute('ms-code-tooltip-offset'));
}
// Z-Index
if (element.hasAttribute('ms-code-tooltip-zIndex')) {
tippyOptions.zIndex = parseInt(element.getAttribute('ms-code-tooltip-zIndex'));
}
// Allow HTML
if (element.hasAttribute('ms-code-tooltip-allowHTML')) {
tippyOptions.allowHTML = element.getAttribute('ms-code-tooltip-allowHTML') === 'true';
}
// Touch
if (element.hasAttribute('ms-code-tooltip-touch')) {
const touchValue = element.getAttribute('ms-code-tooltip-touch');
tippyOptions.touch = touchValue === 'true' || touchValue === 'false' ? (touchValue === 'true') : JSON.parse(touchValue);
}
// Initialize Tippy instance
tippy(element, tippyOptions);
});
}
// Wait for the DOM to be fully loaded, then load Tippy and initialize tooltips
document.addEventListener('DOMContentLoaded', function() {
loadTippy(initializeTippyTooltips);
});
</script>
<!-- 💙 MEMBERSCRIPT #110 v0.1 💙 - TOOLTIPS FOR WEBFLOW -->
<script>
// Function to load Tippy.js, its CSS, and additional theme/animation CSS
function loadTippy(callback) {
// Load Tippy.js script
const script = document.createElement('script');
script.src = 'https://unpkg.com/@popperjs/core@2';
script.onload = function() {
const tippyScript = document.createElement('script');
tippyScript.src = 'https://unpkg.com/tippy.js@6';
tippyScript.onload = function() {
// Load Tippy.js CSS
const cssFiles = [
'https://unpkg.com/tippy.js@6/dist/tippy.css',
'https://unpkg.com/tippy.js@6/themes/light.css',
'https://unpkg.com/tippy.js@6/themes/light-border.css',
'https://unpkg.com/tippy.js@6/animations/shift-away.css',
'https://unpkg.com/tippy.js@6/animations/shift-toward.css',
'https://unpkg.com/tippy.js@6/animations/scale.css',
'https://unpkg.com/tippy.js@6/animations/perspective.css'
];
let loadedCount = 0;
cssFiles.forEach(file => {
const link = document.createElement('link');
link.href = file;
link.rel = 'stylesheet';
link.onload = function() {
loadedCount++;
if (loadedCount === cssFiles.length) {
// Call the callback function when everything is loaded
callback();
}
};
document.head.appendChild(link);
});
};
document.head.appendChild(tippyScript);
};
document.head.appendChild(script);
}
// Function to initialize Tippy tooltips
function initializeTippyTooltips() {
// Select all elements with any ms-code-tooltip-* attribute
const elements = document.querySelectorAll('[ms-code-tooltip-top], [ms-code-tooltip-bottom], [ms-code-tooltip-left], [ms-code-tooltip-right], [ms-code-tooltip-content]');
elements.forEach(element => {
const tippyOptions = {};
// Content and Placement
if (element.hasAttribute('ms-code-tooltip-top')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-top');
tippyOptions.placement = 'top';
} else if (element.hasAttribute('ms-code-tooltip-bottom')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-bottom');
tippyOptions.placement = 'bottom';
} else if (element.hasAttribute('ms-code-tooltip-left')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-left');
tippyOptions.placement = 'left';
} else if (element.hasAttribute('ms-code-tooltip-right')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-right');
tippyOptions.placement = 'right';
} else if (element.hasAttribute('ms-code-tooltip-content')) {
tippyOptions.content = element.getAttribute('ms-code-tooltip-content');
}
if (element.hasAttribute('ms-code-tooltip-placement')) {
tippyOptions.placement = element.getAttribute('ms-code-tooltip-placement');
}
// Theme
if (element.hasAttribute('ms-code-tooltip-theme')) {
tippyOptions.theme = element.getAttribute('ms-code-tooltip-theme');
}
// Animation
if (element.hasAttribute('ms-code-tooltip-animation')) {
tippyOptions.animation = element.getAttribute('ms-code-tooltip-animation');
}
// Max Width
if (element.hasAttribute('ms-code-tooltip-maxwidth')) {
tippyOptions.maxWidth = parseInt(element.getAttribute('ms-code-tooltip-maxwidth'));
}
// Delay
if (element.hasAttribute('ms-code-tooltip-delay')) {
tippyOptions.delay = JSON.parse(element.getAttribute('ms-code-tooltip-delay'));
}
// Duration
if (element.hasAttribute('ms-code-tooltip-duration')) {
tippyOptions.duration = JSON.parse(element.getAttribute('ms-code-tooltip-duration'));
}
// Interactive
if (element.hasAttribute('ms-code-tooltip-interactive')) {
tippyOptions.interactive = element.getAttribute('ms-code-tooltip-interactive') === 'true';
}
// Arrow
if (element.hasAttribute('ms-code-tooltip-arrow')) {
tippyOptions.arrow = element.getAttribute('ms-code-tooltip-arrow') === 'true';
}
// Trigger
if (element.hasAttribute('ms-code-tooltip-trigger')) {
tippyOptions.trigger = element.getAttribute('ms-code-tooltip-trigger');
}
// Hide On Click
if (element.hasAttribute('ms-code-tooltip-hideOnClick')) {
tippyOptions.hideOnClick = element.getAttribute('ms-code-tooltip-hideOnClick') === 'true';
}
// Follow Cursor
if (element.hasAttribute('ms-code-tooltip-followCursor')) {
tippyOptions.followCursor = element.getAttribute('ms-code-tooltip-followCursor');
}
// Offset
if (element.hasAttribute('ms-code-tooltip-offset')) {
tippyOptions.offset = JSON.parse(element.getAttribute('ms-code-tooltip-offset'));
}
// Z-Index
if (element.hasAttribute('ms-code-tooltip-zIndex')) {
tippyOptions.zIndex = parseInt(element.getAttribute('ms-code-tooltip-zIndex'));
}
// Allow HTML
if (element.hasAttribute('ms-code-tooltip-allowHTML')) {
tippyOptions.allowHTML = element.getAttribute('ms-code-tooltip-allowHTML') === 'true';
}
// Touch
if (element.hasAttribute('ms-code-tooltip-touch')) {
const touchValue = element.getAttribute('ms-code-tooltip-touch');
tippyOptions.touch = touchValue === 'true' || touchValue === 'false' ? (touchValue === 'true') : JSON.parse(touchValue);
}
// Initialize Tippy instance
tippy(element, tippyOptions);
});
}
// Wait for the DOM to be fully loaded, then load Tippy and initialize tooltips
document.addEventListener('DOMContentLoaded', function() {
loadTippy(initializeTippyTooltips);
});
</script>

#109 - Selección múltiple personalizada
Multiselección personalizada con búsqueda, selección por teclado y mucho más.
<!-- 💙 MEMBERSCRIPT #109 v0.1 💙 - CUSTOM MULTI SELECT -->
<script>
$(document).ready(function() {
$('[ms-code-select-wrapper]').each(function() {
const $wrapper = $(this);
const isMulti = $wrapper.attr('ms-code-select-wrapper') === 'multi';
const $input = $wrapper.find('[ms-code-select="input"]');
const $list = $wrapper.find('[ms-code-select="list"]');
const $selectedWrapper = $wrapper.find('[ms-code-select="selected-wrapper"]');
const $emptyState = $wrapper.find('[ms-code-select="empty-state"]');
const options = $input.attr('ms-code-select-options').split(',').map(opt => opt.trim());
let selectedOptions = [];
let highlightedIndex = -1;
const $templateSelectedTag = $selectedWrapper.find('[ms-code-select="tag"]');
const templateSelectedTagHTML = $templateSelectedTag.prop('outerHTML');
$templateSelectedTag.remove();
const $templateNewTag = $list.find('[ms-code-select="tag-name-new"]');
const templateNewTagHTML = $templateNewTag.prop('outerHTML');
$templateNewTag.remove();
function createSelectedTag(value) {
const $newTag = $(templateSelectedTagHTML);
$newTag.find('[ms-code-select="tag-name-selected"]').text(value);
$newTag.find('[ms-code-select="tag-close"]').on('click', function(e) {
e.stopPropagation();
removeTag(value);
});
return $newTag;
}
function addTag(value) {
if (!selectedOptions.includes(value) && options.includes(value)) {
selectedOptions.push(value);
$selectedWrapper.append(createSelectedTag(value));
updateInput();
filterOptions();
}
}
function removeTag(value) {
selectedOptions = selectedOptions.filter(option => option !== value);
$selectedWrapper.find(`[ms-code-select="tag-name-selected"]:contains("${value}")`).closest('[ms-code-select="tag"]').remove();
updateInput();
if (isMulti && selectedOptions.length > 0) {
$input.val($input.val() + ', ');
}
filterOptions();
}
function updateInput() {
$input.val(selectedOptions.join(', '));
}
function toggleList(show) {
$list.toggle(show);
}
function createOptionElement(value) {
const $option = $(templateNewTagHTML);
$option.text(value);
$option.on('click', function() {
selectOption(value);
});
return $option;
}
function selectOption(value) {
if (isMulti) {
addTag(value);
$input.val(selectedOptions.join(', ') + (selectedOptions.length > 0 ? ', ' : ''));
$input.focus();
} else {
selectedOptions = [value];
$selectedWrapper.empty().append(createSelectedTag(value));
updateInput();
toggleList(false);
}
filterOptions();
}
function filterOptions() {
const inputValue = $input.val();
const searchTerm = isMulti ? inputValue.split(',').pop().trim() : inputValue.trim();
let visibleOptionsCount = 0;
$list.find('[ms-code-select="tag-name-new"]').each(function() {
const $option = $(this);
const optionText = $option.text().toLowerCase();
const matches = optionText.includes(searchTerm.toLowerCase());
const isSelected = selectedOptions.includes($option.text());
$option.toggle(matches && !isSelected);
if (matches && !isSelected) visibleOptionsCount++;
});
$emptyState.toggle(visibleOptionsCount === 0 && searchTerm !== '');
highlightedIndex = -1;
updateHighlight();
}
function cleanInput() {
const inputValues = $input.val().split(',').map(v => v.trim()).filter(v => v);
const validValues = inputValues.filter(v => options.includes(v));
selectedOptions = validValues;
$selectedWrapper.empty();
selectedOptions.forEach(value => $selectedWrapper.append(createSelectedTag(value)));
updateInput();
filterOptions();
}
function handleInputChange() {
const inputValue = $input.val();
const inputValues = inputValue.split(',').map(v => v.trim());
const lastValue = inputValues[inputValues.length - 1];
if (inputValue.endsWith(',') || inputValue.endsWith(', ')) {
inputValues.pop();
const newValidValues = inputValues.filter(v => options.includes(v) && !selectedOptions.includes(v));
newValidValues.forEach(addTag);
$input.val(selectedOptions.join(', ') + (selectedOptions.length > 0 ? ', ' : ''));
} else if (options.includes(lastValue) && !selectedOptions.includes(lastValue)) {
addTag(lastValue);
$input.val(selectedOptions.join(', ') + ', ');
}
filterOptions();
}
function initializeWithValue() {
const initialValue = $input.val();
if (initialValue) {
const initialValues = initialValue.split(',').map(v => v.trim());
initialValues.forEach(value => {
if (options.includes(value)) {
addTag(value);
}
});
updateInput();
filterOptions();
}
}
function updateHighlight() {
$list.find('[ms-code-select="tag-name-new"]').removeClass('highlighted').css('background-color', '');
if (highlightedIndex >= 0) {
$list.find('[ms-code-select="tag-name-new"]:visible').eq(highlightedIndex)
.addClass('highlighted')
.css('background-color', '#e0e0e0');
}
}
function handleKeyDown(e) {
const visibleOptions = $list.find('[ms-code-select="tag-name-new"]:visible');
const optionCount = visibleOptions.length;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
highlightedIndex = (highlightedIndex + 1) % optionCount;
updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
highlightedIndex = (highlightedIndex - 1 + optionCount) % optionCount;
updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (highlightedIndex >= 0) {
const selectedValue = visibleOptions.eq(highlightedIndex).text();
selectOption(selectedValue);
}
break;
}
}
$.each(options, function(i, option) {
$list.append(createOptionElement(option));
});
$input.on('focus', function() {
toggleList(true);
if (isMulti) {
const currentVal = $input.val().trim();
if (currentVal !== '' && !currentVal.endsWith(',')) {
$input.val(currentVal + ', ');
}
this.selectionStart = this.selectionEnd = this.value.length;
}
filterOptions();
});
$input.on('click', function(e) {
e.preventDefault();
this.selectionStart = this.selectionEnd = this.value.length;
});
$input.on('blur', function() {
setTimeout(function() {
if (!$list.is(':hover')) {
toggleList(false);
cleanInput();
}
}, 100);
});
$input.on('input', handleInputChange);
$input.on('keydown', handleKeyDown);
$list.on('mouseenter', '[ms-code-select="tag-name-new"]', function() {
$(this).css('background-color', '#e0e0e0');
});
$list.on('mouseleave', '[ms-code-select="tag-name-new"]', function() {
if (!$(this).hasClass('highlighted')) {
$(this).css('background-color', '');
}
});
initializeWithValue();
toggleList(false);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #109 v0.1 💙 - CUSTOM MULTI SELECT -->
<script>
$(document).ready(function() {
$('[ms-code-select-wrapper]').each(function() {
const $wrapper = $(this);
const isMulti = $wrapper.attr('ms-code-select-wrapper') === 'multi';
const $input = $wrapper.find('[ms-code-select="input"]');
const $list = $wrapper.find('[ms-code-select="list"]');
const $selectedWrapper = $wrapper.find('[ms-code-select="selected-wrapper"]');
const $emptyState = $wrapper.find('[ms-code-select="empty-state"]');
const options = $input.attr('ms-code-select-options').split(',').map(opt => opt.trim());
let selectedOptions = [];
let highlightedIndex = -1;
const $templateSelectedTag = $selectedWrapper.find('[ms-code-select="tag"]');
const templateSelectedTagHTML = $templateSelectedTag.prop('outerHTML');
$templateSelectedTag.remove();
const $templateNewTag = $list.find('[ms-code-select="tag-name-new"]');
const templateNewTagHTML = $templateNewTag.prop('outerHTML');
$templateNewTag.remove();
function createSelectedTag(value) {
const $newTag = $(templateSelectedTagHTML);
$newTag.find('[ms-code-select="tag-name-selected"]').text(value);
$newTag.find('[ms-code-select="tag-close"]').on('click', function(e) {
e.stopPropagation();
removeTag(value);
});
return $newTag;
}
function addTag(value) {
if (!selectedOptions.includes(value) && options.includes(value)) {
selectedOptions.push(value);
$selectedWrapper.append(createSelectedTag(value));
updateInput();
filterOptions();
}
}
function removeTag(value) {
selectedOptions = selectedOptions.filter(option => option !== value);
$selectedWrapper.find(`[ms-code-select="tag-name-selected"]:contains("${value}")`).closest('[ms-code-select="tag"]').remove();
updateInput();
if (isMulti && selectedOptions.length > 0) {
$input.val($input.val() + ', ');
}
filterOptions();
}
function updateInput() {
$input.val(selectedOptions.join(', '));
}
function toggleList(show) {
$list.toggle(show);
}
function createOptionElement(value) {
const $option = $(templateNewTagHTML);
$option.text(value);
$option.on('click', function() {
selectOption(value);
});
return $option;
}
function selectOption(value) {
if (isMulti) {
addTag(value);
$input.val(selectedOptions.join(', ') + (selectedOptions.length > 0 ? ', ' : ''));
$input.focus();
} else {
selectedOptions = [value];
$selectedWrapper.empty().append(createSelectedTag(value));
updateInput();
toggleList(false);
}
filterOptions();
}
function filterOptions() {
const inputValue = $input.val();
const searchTerm = isMulti ? inputValue.split(',').pop().trim() : inputValue.trim();
let visibleOptionsCount = 0;
$list.find('[ms-code-select="tag-name-new"]').each(function() {
const $option = $(this);
const optionText = $option.text().toLowerCase();
const matches = optionText.includes(searchTerm.toLowerCase());
const isSelected = selectedOptions.includes($option.text());
$option.toggle(matches && !isSelected);
if (matches && !isSelected) visibleOptionsCount++;
});
$emptyState.toggle(visibleOptionsCount === 0 && searchTerm !== '');
highlightedIndex = -1;
updateHighlight();
}
function cleanInput() {
const inputValues = $input.val().split(',').map(v => v.trim()).filter(v => v);
const validValues = inputValues.filter(v => options.includes(v));
selectedOptions = validValues;
$selectedWrapper.empty();
selectedOptions.forEach(value => $selectedWrapper.append(createSelectedTag(value)));
updateInput();
filterOptions();
}
function handleInputChange() {
const inputValue = $input.val();
const inputValues = inputValue.split(',').map(v => v.trim());
const lastValue = inputValues[inputValues.length - 1];
if (inputValue.endsWith(',') || inputValue.endsWith(', ')) {
inputValues.pop();
const newValidValues = inputValues.filter(v => options.includes(v) && !selectedOptions.includes(v));
newValidValues.forEach(addTag);
$input.val(selectedOptions.join(', ') + (selectedOptions.length > 0 ? ', ' : ''));
} else if (options.includes(lastValue) && !selectedOptions.includes(lastValue)) {
addTag(lastValue);
$input.val(selectedOptions.join(', ') + ', ');
}
filterOptions();
}
function initializeWithValue() {
const initialValue = $input.val();
if (initialValue) {
const initialValues = initialValue.split(',').map(v => v.trim());
initialValues.forEach(value => {
if (options.includes(value)) {
addTag(value);
}
});
updateInput();
filterOptions();
}
}
function updateHighlight() {
$list.find('[ms-code-select="tag-name-new"]').removeClass('highlighted').css('background-color', '');
if (highlightedIndex >= 0) {
$list.find('[ms-code-select="tag-name-new"]:visible').eq(highlightedIndex)
.addClass('highlighted')
.css('background-color', '#e0e0e0');
}
}
function handleKeyDown(e) {
const visibleOptions = $list.find('[ms-code-select="tag-name-new"]:visible');
const optionCount = visibleOptions.length;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
highlightedIndex = (highlightedIndex + 1) % optionCount;
updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
highlightedIndex = (highlightedIndex - 1 + optionCount) % optionCount;
updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (highlightedIndex >= 0) {
const selectedValue = visibleOptions.eq(highlightedIndex).text();
selectOption(selectedValue);
}
break;
}
}
$.each(options, function(i, option) {
$list.append(createOptionElement(option));
});
$input.on('focus', function() {
toggleList(true);
if (isMulti) {
const currentVal = $input.val().trim();
if (currentVal !== '' && !currentVal.endsWith(',')) {
$input.val(currentVal + ', ');
}
this.selectionStart = this.selectionEnd = this.value.length;
}
filterOptions();
});
$input.on('click', function(e) {
e.preventDefault();
this.selectionStart = this.selectionEnd = this.value.length;
});
$input.on('blur', function() {
setTimeout(function() {
if (!$list.is(':hover')) {
toggleList(false);
cleanInput();
}
}, 100);
});
$input.on('input', handleInputChange);
$input.on('keydown', handleKeyDown);
$list.on('mouseenter', '[ms-code-select="tag-name-new"]', function() {
$(this).css('background-color', '#e0e0e0');
});
$list.on('mouseleave', '[ms-code-select="tag-name-new"]', function() {
if (!$(this).hasClass('highlighted')) {
$(this).css('background-color', '');
}
});
initializeWithValue();
toggleList(false);
});
});
</script>

#108 - Botones de envío de formularios personalizados
Cree cualquier elemento en Webflow y utilícelo para enviar cualquier tipo de formulario.
<!-- 💙 MEMBERSCRIPT #108 v0.1 💙 CUSTOM FORM SUBMIT BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Find all elements with the ms-code-submit-new attribute
const newSubmitButtons = document.querySelectorAll('[ms-code-submit-new]');
// Add click event listeners to each new submit button
newSubmitButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default action if it's a link
// Get the value of the ms-code-submit-new attribute
const submitId = this.getAttribute('ms-code-submit-new');
// Find the corresponding old submit button
const oldSubmitButton = document.querySelector(`[ms-code-submit-old="${submitId}"]`);
// If found, trigger a click on the old submit button
if (oldSubmitButton) {
oldSubmitButton.click();
} else {
console.error(`No matching old submit button found for ID: ${submitId}`);
}
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #108 v0.1 💙 CUSTOM FORM SUBMIT BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Find all elements with the ms-code-submit-new attribute
const newSubmitButtons = document.querySelectorAll('[ms-code-submit-new]');
// Add click event listeners to each new submit button
newSubmitButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default action if it's a link
// Get the value of the ms-code-submit-new attribute
const submitId = this.getAttribute('ms-code-submit-new');
// Find the corresponding old submit button
const oldSubmitButton = document.querySelector(`[ms-code-submit-old="${submitId}"]`);
// If found, trigger a click on the old submit button
if (oldSubmitButton) {
oldSubmitButton.click();
} else {
console.error(`No matching old submit button found for ID: ${submitId}`);
}
});
});
});
</script>

#107 - Seleccionar plan con radios
Añadir un radio selector de planes a los formularios de inscripción y actualización de planes.
<!-- 💙 MEMBERSCRIPT #107 v0.1 💙 SELECT PLAN WITH RADIO BUTTONS -->
<script>
(function() {
const PRICE_ATTRIBUTES = [
'data-ms-plan:add',
'data-ms-plan:update',
'data-ms-price:add',
'data-ms-price:update'
];
function findElementWithAttribute(form) {
// First, check if the form itself has one of the attributes
for (let attr of PRICE_ATTRIBUTES) {
if (form.hasAttribute(attr)) {
return { element: form, attribute: attr };
}
}
// If not found on form, search child elements
for (let attr of PRICE_ATTRIBUTES) {
let element = Array.from(form.querySelectorAll('*')).find(el => el.hasAttribute(attr));
if (element) {
return { element, attribute: attr };
}
}
return null;
}
function updateAttribute(radio) {
const form = radio.closest('form');
if (!form) return;
const result = findElementWithAttribute(form);
if (result) {
result.element.setAttribute(result.attribute, radio.value);
}
}
function handleRadioChange(e) {
updateAttribute(e.target);
}
function initializeRadioButtons() {
const forms = document.querySelectorAll('[ms-code-radio-plan="form"]');
forms.forEach(form => {
const radios = form.querySelectorAll('input[type="radio"]');
radios.forEach(radio => {
radio.addEventListener('change', handleRadioChange);
if (radio.checked) {
updateAttribute(radio);
}
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeRadioButtons);
} else {
initializeRadioButtons();
}
})();
// Only keep this section if you have an update plan form on the page
(function() {
function updateRadioButtonState(radio) {
radio.checked = true;
radio.dispatchEvent(new Event('change'));
// Update custom radio button UI if present
const customRadio = radio.parentElement.querySelector('.w-radio-input');
if (customRadio) {
customRadio.classList.add('w--redirected-checked');
}
}
function checkAndSelectPlan() {
const msMemData = localStorage.getItem('_ms-mem');
if (!msMemData) return;
try {
const memberData = JSON.parse(msMemData);
const activePlanConnections = memberData.planConnections?.filter(conn => conn.active) || [];
if (activePlanConnections.length === 0) return;
const forms = document.querySelectorAll('[ms-code-radio-plan="form"]');
forms.forEach(form => {
const radios = form.querySelectorAll('input[type="radio"]');
radios.forEach(radio => {
const matchingPlan = activePlanConnections.find(conn => conn.payment.priceId === radio.value);
if (matchingPlan) {
updateRadioButtonState(radio);
}
});
});
} catch (error) {
console.error('Error processing _ms-mem data:', error);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAndSelectPlan);
} else {
checkAndSelectPlan();
}
})();
</script>
<!-- 💙 MEMBERSCRIPT #107 v0.1 💙 SELECT PLAN WITH RADIO BUTTONS -->
<script>
(function() {
const PRICE_ATTRIBUTES = [
'data-ms-plan:add',
'data-ms-plan:update',
'data-ms-price:add',
'data-ms-price:update'
];
function findElementWithAttribute(form) {
// First, check if the form itself has one of the attributes
for (let attr of PRICE_ATTRIBUTES) {
if (form.hasAttribute(attr)) {
return { element: form, attribute: attr };
}
}
// If not found on form, search child elements
for (let attr of PRICE_ATTRIBUTES) {
let element = Array.from(form.querySelectorAll('*')).find(el => el.hasAttribute(attr));
if (element) {
return { element, attribute: attr };
}
}
return null;
}
function updateAttribute(radio) {
const form = radio.closest('form');
if (!form) return;
const result = findElementWithAttribute(form);
if (result) {
result.element.setAttribute(result.attribute, radio.value);
}
}
function handleRadioChange(e) {
updateAttribute(e.target);
}
function initializeRadioButtons() {
const forms = document.querySelectorAll('[ms-code-radio-plan="form"]');
forms.forEach(form => {
const radios = form.querySelectorAll('input[type="radio"]');
radios.forEach(radio => {
radio.addEventListener('change', handleRadioChange);
if (radio.checked) {
updateAttribute(radio);
}
});
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeRadioButtons);
} else {
initializeRadioButtons();
}
})();
// Only keep this section if you have an update plan form on the page
(function() {
function updateRadioButtonState(radio) {
radio.checked = true;
radio.dispatchEvent(new Event('change'));
// Update custom radio button UI if present
const customRadio = radio.parentElement.querySelector('.w-radio-input');
if (customRadio) {
customRadio.classList.add('w--redirected-checked');
}
}
function checkAndSelectPlan() {
const msMemData = localStorage.getItem('_ms-mem');
if (!msMemData) return;
try {
const memberData = JSON.parse(msMemData);
const activePlanConnections = memberData.planConnections?.filter(conn => conn.active) || [];
if (activePlanConnections.length === 0) return;
const forms = document.querySelectorAll('[ms-code-radio-plan="form"]');
forms.forEach(form => {
const radios = form.querySelectorAll('input[type="radio"]');
radios.forEach(radio => {
const matchingPlan = activePlanConnections.find(conn => conn.payment.priceId === radio.value);
if (matchingPlan) {
updateRadioButtonState(radio);
}
});
});
} catch (error) {
console.error('Error processing _ms-mem data:', error);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', checkAndSelectPlan);
} else {
checkAndSelectPlan();
}
})();
</script>

#106 - Gustar y guardar artículos CMS
Permita a sus miembros guardar elementos CMS en su perfil.
<!-- 💙 MEMBERSCRIPT #106 v0.2 💙 SAVING & UNSAVING CMS ITEMS -->
<style>
[ms-code-save], [ms-code-unsave] {
display: none;
}
[ms-code-save-item] {
display: none;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
let isLoggedIn = false;
let savedItems = [];
async function checkMemberLogin() {
try {
const member = await memberstack.getCurrentMember();
return !!member;
} catch (error) {
return false;
}
}
function getSavedItems(memberData) {
return memberData.savedItems || [];
}
function updateButtonVisibility() {
const saveButtons = document.querySelectorAll('[ms-code-save]');
const unsaveButtons = document.querySelectorAll('[ms-code-unsave]');
saveButtons.forEach(button => {
const itemId = button.getAttribute('ms-code-save');
button.style.display = !savedItems.includes(itemId) ? 'block' : 'none';
});
unsaveButtons.forEach(button => {
const itemId = button.getAttribute('ms-code-unsave');
button.style.display = savedItems.includes(itemId) ? 'block' : 'none';
});
}
function updateItemVisibility() {
const saveLists = document.querySelectorAll('[ms-code-save-list]');
saveLists.forEach(list => {
const filter = list.getAttribute('ms-code-save-list');
const items = list.querySelectorAll('[ms-code-save-item]');
items.forEach(item => {
const saveButton = item.querySelector('[ms-code-save]');
if (!saveButton) {
item.style.display = 'block';
return;
}
const itemId = saveButton.getAttribute('ms-code-save');
if (!isLoggedIn || filter === 'all') {
item.style.display = 'block';
} else if (filter === 'saved' & savedItems.includes(itemId)) {
item.style.display = 'block';
} else if (filter === 'unsaved' & !savedItems.includes(itemId)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
}
async function handleButtonClick(event) {
if (!isLoggedIn) return;
const button = event.currentTarget;
const action = button.getAttribute('ms-code-save') ? 'save' : 'unsave';
const itemId = button.getAttribute(action === 'save' ? 'ms-code-save' : 'ms-code-unsave');
if (action === 'save' && !savedItems.includes(itemId)) {
savedItems.push(itemId);
} else if (action === 'unsave') {
savedItems = savedItems.filter(id => id !== itemId);
}
try {
await memberstack.updateMemberJSON({ json: { savedItems } });
} catch (error) {
// Silently handle the error
}
updateButtonVisibility();
updateItemVisibility();
}
function addClickListeners() {
const saveButtons = document.querySelectorAll('[ms-code-save]');
const unsaveButtons = document.querySelectorAll('[ms-code-unsave]');
saveButtons.forEach(button => button.addEventListener('click', handleButtonClick));
unsaveButtons.forEach(button => button.addEventListener('click', handleButtonClick));
}
async function initializeScript() {
isLoggedIn = await checkMemberLogin();
if (isLoggedIn) {
try {
const result = await memberstack.getMemberJSON();
const memberData = result.data || {};
savedItems = getSavedItems(memberData);
} catch (error) {
// Silently handle the error
}
}
updateButtonVisibility();
updateItemVisibility();
addClickListeners();
// Set up a MutationObserver to watch for changes in the DOM
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
shouldUpdate = true;
}
});
if (shouldUpdate) {
updateButtonVisibility();
updateItemVisibility();
addClickListeners();
}
});
// Start observing the document with the configured parameters
observer.observe(document.body, { childList: true, subtree: true });
}
initializeScript();
});
</script>
<!-- 💙 MEMBERSCRIPT #106 v0.2 💙 SAVING & UNSAVING CMS ITEMS -->
<style>
[ms-code-save], [ms-code-unsave] {
display: none;
}
[ms-code-save-item] {
display: none;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
let isLoggedIn = false;
let savedItems = [];
async function checkMemberLogin() {
try {
const member = await memberstack.getCurrentMember();
return !!member;
} catch (error) {
return false;
}
}
function getSavedItems(memberData) {
return memberData.savedItems || [];
}
function updateButtonVisibility() {
const saveButtons = document.querySelectorAll('[ms-code-save]');
const unsaveButtons = document.querySelectorAll('[ms-code-unsave]');
saveButtons.forEach(button => {
const itemId = button.getAttribute('ms-code-save');
button.style.display = !savedItems.includes(itemId) ? 'block' : 'none';
});
unsaveButtons.forEach(button => {
const itemId = button.getAttribute('ms-code-unsave');
button.style.display = savedItems.includes(itemId) ? 'block' : 'none';
});
}
function updateItemVisibility() {
const saveLists = document.querySelectorAll('[ms-code-save-list]');
saveLists.forEach(list => {
const filter = list.getAttribute('ms-code-save-list');
const items = list.querySelectorAll('[ms-code-save-item]');
items.forEach(item => {
const saveButton = item.querySelector('[ms-code-save]');
if (!saveButton) {
item.style.display = 'block';
return;
}
const itemId = saveButton.getAttribute('ms-code-save');
if (!isLoggedIn || filter === 'all') {
item.style.display = 'block';
} else if (filter === 'saved' & savedItems.includes(itemId)) {
item.style.display = 'block';
} else if (filter === 'unsaved' & !savedItems.includes(itemId)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
}
async function handleButtonClick(event) {
if (!isLoggedIn) return;
const button = event.currentTarget;
const action = button.getAttribute('ms-code-save') ? 'save' : 'unsave';
const itemId = button.getAttribute(action === 'save' ? 'ms-code-save' : 'ms-code-unsave');
if (action === 'save' && !savedItems.includes(itemId)) {
savedItems.push(itemId);
} else if (action === 'unsave') {
savedItems = savedItems.filter(id => id !== itemId);
}
try {
await memberstack.updateMemberJSON({ json: { savedItems } });
} catch (error) {
// Silently handle the error
}
updateButtonVisibility();
updateItemVisibility();
}
function addClickListeners() {
const saveButtons = document.querySelectorAll('[ms-code-save]');
const unsaveButtons = document.querySelectorAll('[ms-code-unsave]');
saveButtons.forEach(button => button.addEventListener('click', handleButtonClick));
unsaveButtons.forEach(button => button.addEventListener('click', handleButtonClick));
}
async function initializeScript() {
isLoggedIn = await checkMemberLogin();
if (isLoggedIn) {
try {
const result = await memberstack.getMemberJSON();
const memberData = result.data || {};
savedItems = getSavedItems(memberData);
} catch (error) {
// Silently handle the error
}
}
updateButtonVisibility();
updateItemVisibility();
addClickListeners();
// Set up a MutationObserver to watch for changes in the DOM
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
shouldUpdate = true;
}
});
if (shouldUpdate) {
updateButtonVisibility();
updateItemVisibility();
addClickListeners();
}
});
// Start observing the document with the configured parameters
observer.observe(document.body, { childList: true, subtree: true });
}
initializeScript();
});
</script>

#105 - Pago después de iniciar sesión
Iniciar automáticamente el proceso de pago si un usuario selecciona un precio antes de iniciar sesión.
<!-- 💙 MEMBERSCRIPT #105 v0.1 💙 CHECKOUT AFTER LOGIN -->
<script>
/* Checks if the current URL matches the configured redirect URL, or if no specific URL is required */
function isCorrectPage() {
return redirectOnLoginURL === '' || window.location.pathname === redirectOnLoginURL;
}
/* Checks if Memberstack is fully loaded before running any Memberstack-specific code.*/
function memberstackReady(callback) {
function checkAndExecute() {
if (window.$memberstackDom) {
callback(); // Memberstack is ready, run the callback function.
} else {
setTimeout(checkAndExecute, 100); // Wait for 100ms and check again.
}
}
checkAndExecute(); // Start checking if Memberstack is ready.
}
/* Initiates the Stripe checkout process with a specified price ID.*/
async function initiateCheckout(priceId) {
try {
// Set a flag in session storage to indicate that the checkout page was accessed.
sessionStorage.setItem('ms_checkout_viewed', 'true');
await window.$memberstackDom.purchasePlansWithCheckout({
priceId, // The price ID for the product being purchased.
returnUrl: window.location.href, // Redirect the user back here after completing the checkout.
});
} catch (error) {
console.error('Failed to initiate payment:', error); // Provide error details in the console.
}
}
/* Main execution flow that starts once Memberstack is confirmed to be ready */
memberstackReady(() => {
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
if (member && sessionStorage.getItem('ms_price') && !sessionStorage.getItem('ms_checkout_viewed')) {
initiateCheckout(sessionStorage.getItem('ms_price')); // Start the checkout process if conditions are met.
}
}).catch(error => {
console.error('Failed to retrieve user data:', error); // Log an error if fetching member data fails.
});
});
</script>
<!-- 💙 MEMBERSCRIPT #105 v0.1 💙 CHECKOUT AFTER LOGIN -->
<script>
/* Checks if the current URL matches the configured redirect URL, or if no specific URL is required */
function isCorrectPage() {
return redirectOnLoginURL === '' || window.location.pathname === redirectOnLoginURL;
}
/* Checks if Memberstack is fully loaded before running any Memberstack-specific code.*/
function memberstackReady(callback) {
function checkAndExecute() {
if (window.$memberstackDom) {
callback(); // Memberstack is ready, run the callback function.
} else {
setTimeout(checkAndExecute, 100); // Wait for 100ms and check again.
}
}
checkAndExecute(); // Start checking if Memberstack is ready.
}
/* Initiates the Stripe checkout process with a specified price ID.*/
async function initiateCheckout(priceId) {
try {
// Set a flag in session storage to indicate that the checkout page was accessed.
sessionStorage.setItem('ms_checkout_viewed', 'true');
await window.$memberstackDom.purchasePlansWithCheckout({
priceId, // The price ID for the product being purchased.
returnUrl: window.location.href, // Redirect the user back here after completing the checkout.
});
} catch (error) {
console.error('Failed to initiate payment:', error); // Provide error details in the console.
}
}
/* Main execution flow that starts once Memberstack is confirmed to be ready */
memberstackReady(() => {
window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
if (member && sessionStorage.getItem('ms_price') && !sessionStorage.getItem('ms_checkout_viewed')) {
initiateCheckout(sessionStorage.getItem('ms_price')); // Start the checkout process if conditions are met.
}
}).catch(error => {
console.error('Failed to retrieve user data:', error); // Log an error if fetching member data fails.
});
});
</script>

#104 - Indicador en línea
Muestre a los visitantes de su sitio web su estado en línea en función de las zonas horarias.
<!-- 💙 MEMBERSCRIPT #104 v0.1 💙 ONLINE INDICATOR -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const businessHours = {
start: 9, // Business hours start at 9 AM
end: 17, // Business hours end at 5 PM
days: [1, 2, 3, 4, 5] // Monday to Friday
};
const colors = {
businessHours: '#34b426',
outsideBusinessHours: '#F25022'
};
const wrappers = document.querySelectorAll('[ms-code-online-wrapper]');
wrappers.forEach(wrapper => {
const timeZone = wrapper.getAttribute('ms-code-online-wrapper');
const dot = wrapper.querySelector('[ms-code-online="dot"]');
const timeSpan = wrapper.querySelector('[ms-code-online="time"]');
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
timeZone: timeZone
});
const formattedTime = formatter.format(now);
if (timeSpan) timeSpan.textContent = formattedTime;
const currentDay = now.getDay();
const currentHour = new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
hour12: false,
timeZone: timeZone
});
const isBusinessDay = businessHours.days.includes(currentDay);
const isBusinessHour = currentHour >= businessHours.start && currentHour < businessHours.end;
if (dot) {
dot.style.backgroundColor = (isBusinessDay && isBusinessHour) ? colors.businessHours : colors.outsideBusinessHours;
}
});
});
</script>
<!-- 💙 MEMBERSCRIPT #104 v0.1 💙 ONLINE INDICATOR -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const businessHours = {
start: 9, // Business hours start at 9 AM
end: 17, // Business hours end at 5 PM
days: [1, 2, 3, 4, 5] // Monday to Friday
};
const colors = {
businessHours: '#34b426',
outsideBusinessHours: '#F25022'
};
const wrappers = document.querySelectorAll('[ms-code-online-wrapper]');
wrappers.forEach(wrapper => {
const timeZone = wrapper.getAttribute('ms-code-online-wrapper');
const dot = wrapper.querySelector('[ms-code-online="dot"]');
const timeSpan = wrapper.querySelector('[ms-code-online="time"]');
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
timeZone: timeZone
});
const formattedTime = formatter.format(now);
if (timeSpan) timeSpan.textContent = formattedTime;
const currentDay = now.getDay();
const currentHour = new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
hour12: false,
timeZone: timeZone
});
const isBusinessDay = businessHours.days.includes(currentDay);
const isBusinessHour = currentHour >= businessHours.start && currentHour < businessHours.end;
if (dot) {
dot.style.backgroundColor = (isBusinessDay && isBusinessHour) ? colors.businessHours : colors.outsideBusinessHours;
}
});
});
</script>

#103 - Formulario de reserva personalizado
Añade un formulario de reserva personalizado a tu sitio web que cree un evento en el calendario de Google.
<!-- 💙 MEMBERSCRIPT #103 v0.1 💙 CUSTOM BOOKING FORM -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.33/moment-timezone-with-data.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
function getNextBusinessDays() {
let businessDays = [];
let currentDate = moment();
currentDate.add(1, 'days');
while (businessDays.length < 14) {
if (currentDate.day() !== 0 && currentDate.day() !== 6) {
let formattedDay = currentDate.format('dddd, MMMM D');
let rawDay = currentDate.format('YYYY-MM-DD');
businessDays.push({formattedDay, rawDay});
}
currentDate.add(1, 'days');
}
return businessDays;
}
function generateTimeSlots() {
let slots = [];
let startHour = 9;
let endHour = 16.5;
let currentTime = moment().startOf('day').add(startHour, 'hours');
while (currentTime.hour() + (currentTime.minute() / 60) <= endHour) {
let formattedTime = currentTime.format('h:mm A');
let timeValue = currentTime.format('HH:mm');
slots.push({formattedTime, timeValue});
currentTime.add(30, 'minutes');
}
return slots;
}
function updateTimestamp(day, time, timezone) {
let timestampInput = document.getElementById('timestamp');
if (!timestampInput) {
timestampInput = document.createElement('input');
timestampInput.type = 'hidden';
timestampInput.id = 'timestamp';
timestampInput.name = 'timestamp';
document.querySelector('form').appendChild(timestampInput);
}
let datetime = moment.tz(`${day} ${time}`, "YYYY-MM-DD HH:mm", timezone);
timestampInput.value = datetime.valueOf();
}
function populateFields() {
const days = getNextBusinessDays();
const times = generateTimeSlots();
const daySelect = document.querySelector('[ms-code-booking="day"]');
const timeSelect = document.querySelector('[ms-code-booking="time"]');
const form = daySelect.closest('form');
const timezone = form.getAttribute('ms-code-booking-timezone') || moment.tz.guess();
days.forEach(({formattedDay, rawDay}) => {
let option = new Option(formattedDay, rawDay);
daySelect.appendChild(option);
});
times.forEach(({formattedTime, timeValue}) => {
let option = new Option(formattedTime, timeValue);
timeSelect.appendChild(option);
});
function handleSelectChange() {
if (daySelect.value && timeSelect.value) {
updateTimestamp(daySelect.value, timeSelect.value, timezone);
}
}
daySelect.addEventListener('change', handleSelectChange);
timeSelect.addEventListener('change', handleSelectChange);
}
populateFields();
});
</script>
<!-- 💙 MEMBERSCRIPT #103 v0.1 💙 CUSTOM BOOKING FORM -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.33/moment-timezone-with-data.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
function getNextBusinessDays() {
let businessDays = [];
let currentDate = moment();
currentDate.add(1, 'days');
while (businessDays.length < 14) {
if (currentDate.day() !== 0 && currentDate.day() !== 6) {
let formattedDay = currentDate.format('dddd, MMMM D');
let rawDay = currentDate.format('YYYY-MM-DD');
businessDays.push({formattedDay, rawDay});
}
currentDate.add(1, 'days');
}
return businessDays;
}
function generateTimeSlots() {
let slots = [];
let startHour = 9;
let endHour = 16.5;
let currentTime = moment().startOf('day').add(startHour, 'hours');
while (currentTime.hour() + (currentTime.minute() / 60) <= endHour) {
let formattedTime = currentTime.format('h:mm A');
let timeValue = currentTime.format('HH:mm');
slots.push({formattedTime, timeValue});
currentTime.add(30, 'minutes');
}
return slots;
}
function updateTimestamp(day, time, timezone) {
let timestampInput = document.getElementById('timestamp');
if (!timestampInput) {
timestampInput = document.createElement('input');
timestampInput.type = 'hidden';
timestampInput.id = 'timestamp';
timestampInput.name = 'timestamp';
document.querySelector('form').appendChild(timestampInput);
}
let datetime = moment.tz(`${day} ${time}`, "YYYY-MM-DD HH:mm", timezone);
timestampInput.value = datetime.valueOf();
}
function populateFields() {
const days = getNextBusinessDays();
const times = generateTimeSlots();
const daySelect = document.querySelector('[ms-code-booking="day"]');
const timeSelect = document.querySelector('[ms-code-booking="time"]');
const form = daySelect.closest('form');
const timezone = form.getAttribute('ms-code-booking-timezone') || moment.tz.guess();
days.forEach(({formattedDay, rawDay}) => {
let option = new Option(formattedDay, rawDay);
daySelect.appendChild(option);
});
times.forEach(({formattedTime, timeValue}) => {
let option = new Option(formattedTime, timeValue);
timeSelect.appendChild(option);
});
function handleSelectChange() {
if (daySelect.value && timeSelect.value) {
updateTimestamp(daySelect.value, timeSelect.value, timezone);
}
}
daySelect.addEventListener('change', handleSelectChange);
timeSelect.addEventListener('change', handleSelectChange);
}
populateFields();
});
</script>

#102 - Redimensionar automáticamente la altura del área de texto
Aumenta o disminuye la altura de un textarea en función de su contenido.
<!-- 💙 MEMBERSCRIPT #102 v0.1 💙 RESIZE TEXTAREA VERTICALLY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const elements = document.querySelectorAll('[data-ms-post="content"], [ms-code-resize-input="height"]');
elements.forEach(element => {
if (element.tagName.toLowerCase() === 'textarea') {
element.addEventListener('input', function() {
autoResize(this);
}, false);
}
});
function autoResize(element) {
const maxHeight = parseInt(getComputedStyle(element).maxHeight, 10);
element.style.height = 'auto';
element.style.overflow = 'hidden'; // Prevents scrollbar appearance during height adjustment
if (element.scrollHeight > maxHeight) {
element.style.height = `${maxHeight}px`;
element.style.overflow = 'auto'; // Adds scrollbar when content exceeds max height
} else {
element.style.height = `${element.scrollHeight}px`;
}
}
});
</script>
<!-- 💙 MEMBERSCRIPT #102 v0.1 💙 RESIZE TEXTAREA VERTICALLY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const elements = document.querySelectorAll('[data-ms-post="content"], [ms-code-resize-input="height"]');
elements.forEach(element => {
if (element.tagName.toLowerCase() === 'textarea') {
element.addEventListener('input', function() {
autoResize(this);
}, false);
}
});
function autoResize(element) {
const maxHeight = parseInt(getComputedStyle(element).maxHeight, 10);
element.style.height = 'auto';
element.style.overflow = 'hidden'; // Prevents scrollbar appearance during height adjustment
if (element.scrollHeight > maxHeight) {
element.style.height = `${maxHeight}px`;
element.style.overflow = 'auto'; // Adds scrollbar when content exceeds max height
} else {
element.style.height = `${element.scrollHeight}px`;
}
}
});
</script>

#101 - Redimensionar automáticamente la anchura de entrada
Aumentar o disminuir el ancho de una entrada en función del contenido.
<!-- 💙 MEMBERSCRIPT #101 v0.1 💙 RESIZE INPUT HORIZONTALLY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const elements = document.querySelectorAll('[ms-code-resize-input="width"]');
// Store the initial widths
const initialWidths = new Map();
elements.forEach(element => {
initialWidths.set(element, element.offsetWidth);
});
elements.forEach(element => {
element.addEventListener('input', function() {
autoResizeWidth(this);
});
});
function autoResizeWidth(element) {
// Find the nearest hidden measure element
const measurer = element.nextElementSibling.getAttribute('ms-code-resize-input') === 'hidden-measure'
? element.nextElementSibling
: null;
if (!measurer) return; // Exit if no measurer is found
measurer.textContent = element.value;
const maxWidth = parseInt(getComputedStyle(element).maxWidth, 10);
const minWidth = initialWidths.get(element);
const contentWidth = measurer.offsetWidth;
if (contentWidth > minWidth && contentWidth < maxWidth) {
element.style.width = `${contentWidth}px`;
} else if (contentWidth >= maxWidth) {
element.style.width = `${maxWidth}px`;
} else {
element.style.width = `${minWidth}px`;
}
}
});
</script>
<!-- 💙 MEMBERSCRIPT #101 v0.1 💙 RESIZE INPUT HORIZONTALLY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const elements = document.querySelectorAll('[ms-code-resize-input="width"]');
// Store the initial widths
const initialWidths = new Map();
elements.forEach(element => {
initialWidths.set(element, element.offsetWidth);
});
elements.forEach(element => {
element.addEventListener('input', function() {
autoResizeWidth(this);
});
});
function autoResizeWidth(element) {
// Find the nearest hidden measure element
const measurer = element.nextElementSibling.getAttribute('ms-code-resize-input') === 'hidden-measure'
? element.nextElementSibling
: null;
if (!measurer) return; // Exit if no measurer is found
measurer.textContent = element.value;
const maxWidth = parseInt(getComputedStyle(element).maxWidth, 10);
const minWidth = initialWidths.get(element);
const contentWidth = measurer.offsetWidth;
if (contentWidth > minWidth && contentWidth < maxWidth) {
element.style.width = `${contentWidth}px`;
} else if (contentWidth >= maxWidth) {
element.style.width = `${maxWidth}px`;
} else {
element.style.width = `${minWidth}px`;
}
}
});
</script>

#100 - Autocompresión de imágenes
Comprime las subidas de imágenes, incluidas las imágenes de perfil.
<!-- 💙 MEMBERSCRIPT #100 v0.1 💙 AUTO-COMPRESSED IMAGE UPLOADS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/compressorjs/1.2.1/compressor.min.js" integrity="sha512-MgYeYFj8R3S6rvZHiJ1xA9cM/VDGcT4eRRFQwGA7qDP7NHbnWKNmAm28z0LVjOuUqjD0T9JxpDMdVqsZOSHaSA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const compressibleInputs = document.querySelectorAll('input[type="file"][ms-code-file_compress]');
compressibleInputs.forEach(fileInput => {
let isCompressing = false;
fileInput.addEventListener('change', function (event) {
if (isCompressing) {
isCompressing = false;
return;
}
if (fileInput.files.length === 0) {
return;
}
const originalFile = fileInput.files[0];
const compressionLevel = parseFloat(fileInput.getAttribute('ms-code-file_compress'));
new Compressor(originalFile, {
quality: compressionLevel,
maxWidth: 2000,
maxHeight: 2000,
success(compressedResult) {
const compressedFile = new File([compressedResult], originalFile.name, {
type: compressedResult.type,
lastModified: Date.now(),
});
const dataTransfer = new DataTransfer();
dataTransfer.items.add(compressedFile);
fileInput.files = dataTransfer.files;
isCompressing = true;
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
},
error(err) {
console.error('Compression Error: ', err.message);
},
});
event.stopPropagation();
}, true);
});
});
</script>
<!-- 💙 MEMBERSCRIPT #100 v0.1 💙 AUTO-COMPRESSED IMAGE UPLOADS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/compressorjs/1.2.1/compressor.min.js" integrity="sha512-MgYeYFj8R3S6rvZHiJ1xA9cM/VDGcT4eRRFQwGA7qDP7NHbnWKNmAm28z0LVjOuUqjD0T9JxpDMdVqsZOSHaSA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const compressibleInputs = document.querySelectorAll('input[type="file"][ms-code-file_compress]');
compressibleInputs.forEach(fileInput => {
let isCompressing = false;
fileInput.addEventListener('change', function (event) {
if (isCompressing) {
isCompressing = false;
return;
}
if (fileInput.files.length === 0) {
return;
}
const originalFile = fileInput.files[0];
const compressionLevel = parseFloat(fileInput.getAttribute('ms-code-file_compress'));
new Compressor(originalFile, {
quality: compressionLevel,
maxWidth: 2000,
maxHeight: 2000,
success(compressedResult) {
const compressedFile = new File([compressedResult], originalFile.name, {
type: compressedResult.type,
lastModified: Date.now(),
});
const dataTransfer = new DataTransfer();
dataTransfer.items.add(compressedFile);
fileInput.files = dataTransfer.files;
isCompressing = true;
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
},
error(err) {
console.error('Compression Error: ', err.message);
},
});
event.stopPropagation();
}, true);
});
});
</script>

#99 - Entradas de archivos personalizadas
Convierta cualquier cosa en un archivo de entrada
<!-- 💙 MEMBERSCRIPT #99 v0.1 💙 CUSTOM FILE UPLOAD INPUT -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const uploadButtons = document.querySelectorAll('[ms-code-file_uploader]');
uploadButtons.forEach(button => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
fileInput.name = button.getAttribute('ms-code-file_uploader');
fileInput.accept = button.getAttribute('ms-code-file_types');
document.body.appendChild(fileInput);
button.addEventListener('click', function (e) {
e.preventDefault();
fileInput.click();
});
fileInput.addEventListener('change', function () {
const fileName = fileInput.files[0].name;
button.querySelector('div').textContent = fileName;
});
});
});
</script>
<!-- 💙 MEMBERSCRIPT #99 v0.1 💙 CUSTOM FILE UPLOAD INPUT -->
<script>
document.addEventListener('DOMContentLoaded', function () {
const uploadButtons = document.querySelectorAll('[ms-code-file_uploader]');
uploadButtons.forEach(button => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.style.display = 'none';
fileInput.name = button.getAttribute('ms-code-file_uploader');
fileInput.accept = button.getAttribute('ms-code-file_types');
document.body.appendChild(fileInput);
button.addEventListener('click', function (e) {
e.preventDefault();
fileInput.click();
});
fileInput.addEventListener('change', function () {
const fileName = fileInput.files[0].name;
button.querySelector('div').textContent = fileName;
});
});
});
</script>
¿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.