Scripts des membres
Une solution basée sur les attributs pour ajouter des fonctionnalités à votre site Webflow.
Il suffit de copier un peu de code, d'ajouter quelques attributs et le tour est joué.
Tous les clients de Memberstack peuvent demander de l'aide dans le Slack 2.0. Veuillez noter qu'il ne s'agit pas de fonctionnalités officielles et que le support ne peut être garanti.
#141 - Démarrer l'intégration de YouTube à un moment précis
Activez les liens partageables et lancez la lecture des vidéos à une heure donnée.
<!-- 💙 MEMBERSCRIPT #141 v0.1 💙 - START YOUTUBE VIDEO AT SPECIFIC TIME -->
<script>
(function() {
// Function to get URL parameters
function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^]*)');
var results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
// Function to update YouTube embed src within Embedly iframe
function updateYouTubeEmbed(embedly_iframe, startTime) {
var embedly_src = embedly_iframe.src;
var youtube_src_match = embedly_src.match(/src=([^&]+)/);
if (youtube_src_match) {
var youtube_src = decodeURIComponent(youtube_src_match[1]);
var new_youtube_src = youtube_src.replace(/(\?|&)start=\d+/, '');
new_youtube_src += (new_youtube_src.includes('?') ? '&' : '?') + 'start=' + startTime;
var new_embedly_src = embedly_src.replace(/src=([^&]+)/, 'src=' + encodeURIComponent(new_youtube_src));
embedly_iframe.src = new_embedly_src;
}
}
// Get all elements with ms-code-yt-start attribute
var elements = document.querySelectorAll('[ms-code-yt-start]');
elements.forEach(function(element) {
var paramName = element.getAttribute('ms-code-yt-start');
var startTime = getUrlParameter(paramName);
var defaultStartTime = element.getAttribute('ms-code-yt-start-default');
// If no URL parameter, use the default start time (if specified)
if (!startTime && defaultStartTime) {
startTime = defaultStartTime;
}
// If we have a start time (either from URL or default), update the embed
if (startTime) {
var iframe = element.querySelector('iframe.embedly-embed');
if (iframe) {
updateYouTubeEmbed(iframe, startTime);
}
}
});
})();
</script>
#140 - Confirmer la concordance des entrées
Vérifier une entrée avant d'autoriser la soumission - idéal pour éviter les informations erronées !
<!-- 💙 MEMBERSCRIPT #140 v0.1 💙 - CONFIRM MATCHING INPUTS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
const inputPairs = form.querySelectorAll('[ms-code-conf-input]');
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
if (!submitButton) {
console.error('Submit button not found in the form');
return;
}
function validateForm() {
let fieldsMatch = true;
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
const errorElement = form.querySelector(`[ms-code-conf-error="${confType}"]`);
if (confirmInput && errorElement) {
if (input.value && confirmInput.value) {
if (input.value !== confirmInput.value) {
errorElement.style.removeProperty('display');
fieldsMatch = false;
} else {
errorElement.style.display = 'none';
}
} else {
errorElement.style.display = 'none';
}
}
});
if (fieldsMatch) {
submitButton.style.removeProperty('pointer-events');
submitButton.disabled = false;
} else {
submitButton.style.pointerEvents = 'none';
submitButton.disabled = true;
}
}
inputPairs.forEach(input => {
const confType = input.getAttribute('ms-code-conf-input');
const confirmInput = form.querySelector(`[ms-code-conf="${confType}"]`);
if (confirmInput) {
input.addEventListener('input', validateForm);
confirmInput.addEventListener('input', validateForm);
}
});
// Initial validation
validateForm();
// Extra precaution: prevent form submission if fields don't match
form.addEventListener('submit', function(event) {
if (submitButton.disabled) {
event.preventDefault();
console.log('Form submission blocked: Fields do not match');
}
});
});
});
</script>
#139 - Réinitialiser le formulaire après l'avoir soumis
Créer un bouton dans l'état de réussite du formulaire qui permet de le soumettre à nouveau.
<!-- 💙 MEMBERSCRIPT #139 v0.1 💙 - RESET FORM BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
// Find all "Add another" buttons
const resetButtons = document.querySelectorAll('[ms-code-reset-form]');
// Add click event listener to each button
resetButtons.forEach(function(resetButton) {
resetButton.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default link behavior
// Find the closest form and success message elements
const formWrapper = this.closest('.w-form');
const form = formWrapper.querySelector('form');
const successMessage = formWrapper.querySelector('.w-form-done');
// Reset the form
form.reset();
// Hide the success message
successMessage.style.display = 'none';
// Show the form
form.style.display = 'block';
});
});
});
</script>
#138 - Décalage du défilement du lien d'ancrage
Correction du problème avec les liens d'ancrage et les barres de navigation collantes/fixées dans 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>
#137 - Afficher le nom du pays des visiteurs
Remplacer le texte par le pays dans lequel se trouve l'utilisateur en fonction de son adresse 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>
#136 - Supprimer le chemin de section de l'URL
Lorsque l'on navigue vers une section, le chemin du lien d'ancrage est supprimé.
<!-- 💙 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 - Redirection basée sur une valeur sélectionnée
Définir dynamiquement la redirection du formulaire en fonction de la sélection de l'utilisateur.
<!-- 💙 MEMBERSCRIPT #135 v0.1 💙 - 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) {
window.location.href = redirectUrl;
} 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 - Défilement vers le haut lors d'un changement d'onglet
Lorsque l'onglet est modifié, la page défile jusqu'en haut de la section de l'onglet.
<!-- 💙 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 - Filigrane automatique des images
Ajoutez facilement un filigrane aux images de votre site 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>
#132 - Masquer des éléments avec la touche Escape
Ajoutez un attribut et lorsque vous cliquez sur la touche esc, l'élément sera affiché en mode "none".
<!-- 💙 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 - Additionner les entrées numériques
Prendre la valeur des entrées numériques et l'afficher dans une valeur d'entrée ou dans une zone de texte.
<!-- 💙 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 - Soumettre automatiquement le formulaire lorsque toutes les entrées sont modifiées
Il n'est pas nécessaire d'avoir un bouton d'envoi et le formulaire est envoyé lorsque toutes les entrées sont modifiées.
<!-- 💙 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 - La mise en place d'une barrière de sécurité dans les pays
Empêcher les visiteurs de consulter votre site s'ils se trouvent dans l'un des pays interdits.
<!-- 💙 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 - Masquer les éléments après qu'ils aient été vus
Ajoutez un attribut et vos visiteurs ne verront cet élément qu'une seule fois. Après actualisation, il aura disparu.
<!-- 💙 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 - Valider les entrées de texte
Valider les entrées de texte par rapport à n'importe quelle liste de chaînes de caractères, y compris les caractères génériques !
<!-- 💙 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 - Poster un formulaire vers un Webhook sans redirection
Envoyer des données à un webhook et conserver le comportement par défaut du formulaire 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>
#125 - Contrôler l'état requis avec une case à cocher
Définir les champs du formulaire comme obligatoires/facultatifs en fonction de l'état d'une case à cocher.
<!-- 💙 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 - Visibilité de l'article
Utilisez des boutons personnalisés pour masquer et afficher des éléments, les états étant enregistrés dans la mémoire locale.
<!-- 💙 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 - Une lecture audio à la fois
Mettre automatiquement en pause tous les autres audioplayers lorsqu'un utilisateur clique sur l'un d'entre eux.
<!-- 💙 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 - Ouvrir les liens externes dans un nouvel onglet
Les liens externes s'ouvrent automatiquement dans un nouvel onglet et les attributs nofollow et noreferrer sont ajoutés.
<!-- 💙 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 - Rendre un tableau à partir d'un champ personnalisé
Affichez n'importe quelle liste séparée par des virgules à vos membres dans 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>
#120 - Afficher/masquer un élément en fonction de l'appareil, du système d'exploitation ou du navigateur
Visibilité conditionnelle en fonction de l'appareil, du système d'exploitation ou du navigateur utilisé par le visiteur.
<!-- 💙 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 - Calculateur d'âge à partir de la date d'entrée
Calculer le nombre total d'années écoulées et pré-remplir une entrée avec ce nombre.
<!-- 💙 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>
#118 - Enregistrer la dernière adresse IP du membre
Mettez à jour un champ personnalisé avec l'adresse IP la plus récente à partir de laquelle vos membres se sont connectés.
<!-- 💙 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 - Barre de progression du défilement des pages
Un indicateur de défilement de page flexible et personnalisé pour afficher la progression du défilement de la page.
<!-- 💙 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 - Partager des liens de texte surlignés
Permettre aux utilisateurs de surligner un texte et de partager le lien avec d'autres personnes !
<!-- 💙 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 - Générer un mot de passe aléatoire
Inscription sans friction. Exiger ou permettre aux membres de définir un mot de passe à l'avenir.
<!-- 💙 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 - Bouton de défilement vers le haut
Ajoutez un bouton qui fera défiler la page vers le haut lorsqu'il sera cliqué,
<!-- 💙 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>
#N°113 - Flux RSS
Utilisez une interface Webflow pour afficher un flux RSS directement sur votre site 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>
#112 - Sliders avant et après
Ajoutez facilement un slider de photos avant/après à votre site 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>
#111 - Onglets à rotation automatique
Le moyen le plus simple de faire tourner vos onglets automatiquement sur une minuterie.
<!-- 💙 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 - Info-bulles pour Webflow
Ajoutez facilement des infobulles Tippy.js à votre site Webflow avec des attributs.
<!-- 💙 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 - Multi-sélections personnalisées
Multi-sélections personnalisées avec recherche, sélection au clavier, etc.
<!-- 💙 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 - Boutons de soumission de formulaire personnalisés
Créez n'importe quel élément dans Webflow et utilisez-le pour soumettre n'importe quel type de formulaire.
<!-- 💙 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 - Plan de sélection avec radios
Ajouter un sélecteur de régime dans les formulaires d'inscription et de mise à jour de régime.
<!-- 💙 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 - Aimer et conserver les articles de la CMS
Permettez à vos membres d'enregistrer des éléments de la CMS dans leur profil.
<!-- 💙 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 - Vérification après la connexion
Lancer automatiquement la caisse si un membre sélectionne un prix avant de se connecter.
<!-- 💙 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 - Indicateur en ligne
Montrez aux visiteurs de votre site votre statut en ligne en fonction des fuseaux horaires.
<!-- 💙 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 - Formulaire de réservation personnalisé
Ajoutez un formulaire de réservation personnalisé à votre site web qui crée un événement dans le calendrier 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>
#102 - Redimensionner automatiquement la hauteur du Textarea
Augmenter ou diminuer la hauteur d'un textarea en fonction de son contenu.
<!-- 💙 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 - Redimensionner automatiquement la largeur d'entrée
Augmenter ou réduire la largeur d'une entrée en fonction de son contenu.
<!-- 💙 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 - Compression automatique des téléchargements d'images
Compression des images téléchargées, y compris les images de profil.
<!-- 💙 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 - Fichiers d'entrée personnalisés
Transformez n'importe quoi en fichier d'entrée !
<!-- 💙 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>
#N° 98 - Limitation de l'âge
Les utilisateurs doivent confirmer leur âge avant de continuer.
<!-- 💙 MEMBERSCRIPT #98 v0.1 💙 AGE GATE -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const form = document.querySelector('form[ms-code-age-gate]');
const dayInput = document.querySelector('input[ms-code-age-gate="day"]');
const monthInput = document.querySelector('input[ms-code-age-gate="month"]');
const yearInput = document.querySelector('input[ms-code-age-gate="year"]');
const backButton = document.querySelector('a[ms-code-age-gate="back"]');
const wrapper = document.querySelector('[ms-code-age-gate="wrapper"]');
const errorDiv = document.querySelector('div[ms-code-age-gate="error"]');
if (localStorage.getItem('ageVerified') === 'true') {
wrapper.remove();
return;
}
backButton.addEventListener('click', (e) => {
e.preventDefault();
window.history.back();
});
const inputs = [dayInput, monthInput, yearInput];
inputs.forEach((input, index) => {
input.addEventListener('keyup', (e) => {
const maxChars = input === yearInput ? 4 : 2;
let value = e.target.value;
if (input === dayInput && value.length === maxChars) {
value = value > 31 ? '31' : value.padStart(2, '0');
} else if (input === monthInput && value.length === maxChars) {
value = value > 12 ? '12' : value.padStart(2, '0');
}
e.target.value = value;
if (value.length === maxChars) {
const nextInput = inputs[index + 1];
if (nextInput) {
nextInput.focus();
}
}
});
});
form.addEventListener('submit', (e) => {
e.preventDefault();
const enteredDate = new Date(yearInput.value, monthInput.value - 1, dayInput.value);
const currentDate = new Date();
const ageDifference = new Date(currentDate - enteredDate);
const age = Math.abs(ageDifference.getUTCFullYear() - 1970);
const ageLimit = parseInt(form.getAttribute('ms-code-age-gate').split('-')[1], 10);
if (age >= ageLimit) {
console.log('Age verified.');
errorDiv.style.display = 'none';
localStorage.setItem('ageVerified', 'true');
wrapper.remove();
} else {
console.log('Age verification failed, user is under the age limit.');
errorDiv.style.display = 'block';
}
});
});
</script>
#97 - Uploader des fichiers vers le S3 Bucket
Autoriser les téléchargements vers un godet S3 à partir d'un formulaire Webflow.
<!-- 💙 MEMBERSCRIPT #97 v0.1 💙 S3 FILE UPLOADS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
document.querySelectorAll('input[ms-code-s3-uploader]').forEach(input => {
input.addEventListener('change', function() {
if (this.files.length > 0) {
const file = this.files[0];
const uuid = generateUUID();
const extension = file.name.split('.').pop();
const newFileName = `${uuid}.${extension}`;
const wrapper = this.closest('div[ms-code-s3-wrapper]');
const s3FileInput = wrapper.querySelector('input[ms-code-s3-file]');
s3FileInput.value = s3FileInput.getAttribute('ms-code-s3-file') + encodeURIComponent(newFileName);
const apiGatewayUrl = wrapper.getAttribute('ms-code-s3-wrapper').replace('${encodeURIComponent(fileName)}', encodeURIComponent(newFileName));
fetch(apiGatewayUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type }
})
.then(response => {
if (response.status !== 200) {
throw new Error(`Upload failed with status: ${response.status}`);
}
console.log('File uploaded successfully:', newFileName);
})
.catch(error => {
console.error('Upload error:', error);
alert('Upload failed.');
});
}
});
});
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', function(event) {
const s3Inputs = Array.from(form.querySelectorAll('input[ms-code-s3-file]'));
const allUrlsSet = s3Inputs.every(input => input.value);
if (!allUrlsSet) {
event.preventDefault();
alert('Please wait for all files to finish uploading before submitting.');
}
});
});
});
</script>
#96 - Enregistrer un décompte
Créez et mettez à jour un décompte qui s'enregistre dans un champ personnalisé !
<!-- 💙 MEMBERSCRIPT #96 v0.1 💙 KEEPING A TALLY -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
const addButtons = document.querySelectorAll("[ms-code-add-tally]");
addButtons.forEach(button => {
button.addEventListener("click", async () => {
const tallyKey = button.getAttribute("ms-code-add-tally");
const tallyText = document.querySelector(`[ms-code-tally="${tallyKey}"]`);
if(tallyText){
let currentCount = parseInt(tallyText.textContent, 10);
currentCount += 1;
tallyText.textContent = currentCount;
// Store the new tally count to Memberstack
let newFields = {};
newFields[tallyKey] = currentCount;
await memberstack.updateMember({customFields: newFields});
}
});
});
});
</script>
#95 - Confetti On Click
Faites voler des confettis amusants en cliquant !
<!-- 💙 MEMBERSCRIPT #95 v0.1 💙 CONFETTI -->
<script src="https://cdn.jsdelivr.net/npm/tsparticles-confetti@2.12.0/tsparticles.confetti.bundle.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const confettiElems = document.querySelectorAll("[ms-code-confetti]");
confettiElems.forEach(item => {
item.addEventListener("click", () => {
const effect = item.getAttribute("ms-code-confetti");
switch (effect) {
case "falling":
const makeFall = () => {
confetti({
particleCount: 100,
startVelocity: 30,
spread: 360,
origin: { x: Math.random(), y: 0 },
colors: ['#ffffff','#ff0000','#00ff00','#0000ff']
});
}
setInterval(makeFall, 2000);
break;
case "single":
confetti({
particleCount: 1,
startVelocity: 30,
spread: 360,
origin: { x: Math.random(), y: Math.random() }
});
break;
case "sides":
confetti({
particleCount: 100,
startVelocity: 30,
spread: 360,
origin: { x: Math.random(), y: 0.5 }
});
break;
case "explosions":
confetti({
particleCount: 100,
startVelocity: 50,
spread: 360
});
break;
case "bottom":
confetti({
particleCount: 100,
startVelocity: 30,
spread: 360,
origin: { x: 0.5, y: 1 }
});
break;
default:
console.log("Unknown confetti effect");
}
});
});
});
</script>
#94 - Définir l'attribut href
Définir dynamiquement un lien à l'aide du CMS Webflow (ou autre)
<!-- 💙 MEMBERSCRIPT #94 v0.1 💙 SET HREF ATTRIBUTE -->
<script>
window.onload = function(){
var elements = document.querySelectorAll('[ms-code-href]');
elements.forEach(function(element) {
var url = element.getAttribute('ms-code-href');
element.setAttribute('href', url);
});
};
</script>
#93 - Forcer les URL valides dans les formulaires de saisie
Convertit automatiquement tout ce qui est saisi en URL valide.
<!-- 💙 MEMBERSCRIPT #93 v0.1 💙 FORCE INPUT TO BE A VALID URL -->
<script>
// Get all form fields with attribute ms-code-convert="link"
const formFields = document.querySelectorAll('input[ms-code-convert="link"], textarea[ms-code-convert="link"]');
// Add event listener to each form field
formFields.forEach((field) => {
field.addEventListener('input', convertToLink);
});
// Function to convert input to a link
function convertToLink(event) {
const input = event.target;
// Get user input
const userInput = input.value.trim();
// Check if input starts with http:// or https://
if (userInput.startsWith('http://') || userInput.startsWith('https://')) {
input.value = userInput; // No conversion needed for valid links
} else {
input.value = `http://${userInput}`; // Prepend http:// for simplicity
}
}
</script>
#92 - Changer de page en cliquant
Modifier l'URL de la page en cours lorsque l'on clique sur un élément.
<!-- 💙 MEMBERSCRIPT #92 v0.1 💙 TURN ANYTHING INTO A LINK -->
<script>
document.addEventListener('click', function(event) {
let target = event.target;
// Traverse up the DOM tree to find an element with the ms-code-navigate attribute
while (target && !target.getAttribute('ms-code-navigate')) {
target = target.parentElement;
}
// If we found an element with the ms-code-navigate attribute
if (target) {
const navigateUrl = target.getAttribute('ms-code-navigate');
if (navigateUrl) {
event.preventDefault();
// Always open in a new tab
window.open(navigateUrl, '_blank');
}
}
});
</script>
#91 - Masquer la fenêtre popup pour une durée déterminée
Masquer une fenêtre contextuelle pendant une durée X lorsqu'un bouton est cliqué.
<!-- 💙 MEMBERSCRIPT #91 v0.1 💙 HIDE POPUP FOR SET DURATION -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
// Look for elements with 'ms-code-hide-popup' attribute
var items = $('[ms-code-hide-popup]');
var button;
var timeElement;
// Determine which element is the button and which is the timer
items.each(function(index, item) {
var value = $(item).attr('ms-code-hide-popup');
if (value === "button") {
button = $(item);
} else {
timeElement = $(item);
}
});
// Calculate the target date
var calculateTargetDate = function(timeStr) {
var splitTime = timeStr.split(':');
var now = new Date();
now.setDate(now.getDate() + parseInt(splitTime[0])); // add days
now.setHours(now.getHours() + parseInt(splitTime[1])); // add hours
now.setMinutes(now.getMinutes() + parseInt(splitTime[2])); // add minutes
now.setSeconds(now.getSeconds() + parseInt(splitTime[3])); // add seconds
return now;
};
// Check if element should be removed from DOM
var checkTimeAndRemoveElement = function() {
var targetDate = localStorage.getItem('targetDate');
if (targetDate && new Date() < new Date(targetDate)) {
timeElement.remove();
} else {
localStorage.removeItem('targetDate');
}
};
// Action on button click
button.on('click', function() {
var time = timeElement.attr('ms-code-hide-popup');
localStorage.setItem('targetDate', calculateTargetDate(time));
checkTimeAndRemoveElement();
});
// Initial check
checkTimeAndRemove AndRemoveElement();
});
</script>
#90 - Afficher les éléments lors d'un changement d'entrée
Afficher 1 ou plusieurs éléments lorsque l'utilisateur modifie la valeur d'entrée.
<!-- 💙 MEMBERSCRIPT #90 v0.1 💙 SHOW ELEMENTS ON INPUT CHANGE -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
// Initially hide all elements
$('[ms-code-show-item]').css('display', 'none');
setTimeout(function() {
$('[ms-code-show-field]').change(function() {
var field = $(this).attr('ms-code-show-field');
$('[ms-code-show-item=' + field + ']').css('display', 'block');
});
}, 500); // Wait 500ms before starting, you can change this time based on your needs
});
</script>
#89 - Menus contextuels personnalisés
Afficher un menu contextuel personnalisé et intégré à Webflow lorsque l'on clique avec le bouton droit de la souris sur votre élément.
<!-- 💙 MEMBERSCRIPT #89 v0.1 💙 CUSTOM CONTEXT MENUS -->
<script>
// Cache elements
const items = document.querySelectorAll("[ms-code-context-item]");
const menus = document.querySelectorAll("[ms-code-context-menu]");
// Disable default context menu on item right click and show custom context menu
items.forEach(element => {
element.addEventListener('contextmenu', event => {
event.preventDefault(); // Prevents showing the default context menu
hideAllMenus(); // Make sure other menus are hidden
// fetch the related menu, make it visible
const menuItemId = element.getAttribute("ms-code-context-item");
const menu = document.querySelector(`[ms-code-context-menu="${menuItemId}"]`);
if (menu) {
menu.classList.remove('hidden');
menu.classList.add('visible');
}
});
});
// Add click event on custom menus to stop event propagation
menus.forEach(menu => {
menu.addEventListener('click', event => {
event.stopPropagation();
});
});
// Close custom context menu on outside click
document.body.addEventListener('click', hideAllMenus);
// Helper function to hide all custom context menus
function hideAllMenus() {
menus.forEach(menu => {
menu.classList.remove('visible');
menu.classList.add('hidden');
});
}
</script>
#88 - Afficher l'état actuel du CMS, liens vers les dossiers
Affichez l'état "actuel" du Webflow sur vos pages imbriquées et les éléments du CMS.
<!-- 💙 MEMBERSCRIPT #88 v0.1 💙 SHOW CURRENT STATE FOR NESTED URLS -->
<script>
window.onload = function() {
var currentUrl = window.location.href;
var elements = document.querySelectorAll('[ms-code-nested-link]'); // get all elements with ms-code-nested-link attribute
elements.forEach(function (element) {
var linkAttrValue = element.getAttribute('ms-code-nested-link'); // get the ms-code-nested-link value
if (currentUrl.includes(linkAttrValue)) { // check if current url matches the attribute value
element.classList.add('w--current'); // apply the class
}
});
};
</script>
#87 - Supprimer un plan après le compte à rebours
Créez des contenus sécurisés sensibles au facteur temps !
<!-- 💙 MEMBERSCRIPT #87 v0.1 💙 REMOVE PLAN AFTER COUNTDOWN -->
<script>
const memberstack = window.$memberstackDom;
const countdown = new Date(localStorage.getItem('countdownDateTime'));
// Check if date has passed
const checkDate = async () => {
const now = new Date();
if (now > countdown) {
// Remove member's free plan
await memberstack.removePlan({
planId: "pln_10-minutes-of-gif-access-rw1fh0ktg"
});
console.log("Plan removed");
// Reload the page
location.reload();
}
}
// Execute checkDate every 10s
const intervalId = setInterval(checkDate, 10000);
</script>
#86 - Text-To-Speech simple et gratuit
Ajoutez un bouton qui permet aux visiteurs d'écouter votre article.
<!-- 💙 MEMBERSCRIPT #86 v0.1 💙 VOICE TO TEXT BUTTON -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const textDiv = document.querySelector('[ms-code-text-to-speech="text"]');
const speakButton = document.querySelector('[ms-code-text-to-speech="button"]');
let utterance = new SpeechSynthesisUtterance();
speakButton.addEventListener('click', () => {
if(speechSynthesis.speaking || speechSynthesis.paused) {
speechSynthesis.cancel(); // stops current speech
} else {
utterance.text = textDiv.innerText;
speechSynthesis.speak(utterance); // starts speaking
}
});
});
</script>
#85 - Entrées du formulaire "Ajouter une ligne
Permet aux membres d'ajouter et de supprimer des lignes à partir d'un formulaire.
<!-- 💙 MEMBERSCRIPT #85 v0.1 💙 ADD A ROW FORM INPUTS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
// Hide all rows except the original row
$('[ms-code-row-input="new"]').hide();
// Add row button click event
$('[ms-code-row-input="add-row"]').click(function(e) {
e.preventDefault();
var clonedRow = $('[ms-code-row-input="new"]').first().clone();
clonedRow.find('input').val('');
clonedRow.show().appendTo('[ms-code-row-input="row-container"]');
updateHolderValue();
});
// Delete row button click event
$(document).on('click', '[ms-code-row-input="delete"]', function(e) {
e.preventDefault();
$(this).closest('[ms-code-row-input="new"]').remove();
updateHolderValue();
});
// Event for all inputs
$(document).on('input', '[ms-code-row-input="original"], [ms-code-row-input="new-input"], [ms-code-row-input="holder"]', function() {
if ($(this).is('[ms-code-row-input="holder"]')) {
updateRowsFromHolder();
} else {
updateHolderValue();
}
});
// Function to update the holder input value
function updateHolderValue() {
var values = [];
$('[ms-code-row-input="original"], [ms-code-row-input="new-input"]').each(function() {
var value = $(this).val().trim();
if (value) {
values.push(value);
}
});
$('[ms-code-row-input="holder"]').val(values.join(','));
}
// Function to update rows from the holder field
function updateRowsFromHolder() {
var holderValue = $('[ms-code-row-input="holder"]').val();
var values = holderValue.split(',');
$('[ms-code-row-input="new"]').not(':first').remove();
// For each holder value, create a new row
values.forEach(function(val, idx) {
if (idx === 0) {
$('[ms-code-row-input="original"]').val(val);
} else {
var newRow = $('[ms-code-row-input="new"]').first().clone().appendTo('[ms-code-row-input="row-container"]');
newRow.find('input').val(val);
newRow.show();
}
});
}
// Initial update of the holder input value
updateHolderValue();
// Adding MutationObserver to call updateRowsFromHolder on changes to the holder field
var targetNode = $('[ms-code-row-input="holder"]')[0];
var config = { attributes: true, childList: true, subtree: true };
var callback = function(mutationsList, observer) {
for(let mutation of mutationsList) {
if (mutation.type === 'childList')
{
updateRowsFromHolder();
}
}
};
var observer = new MutationObserver(callback);
observer.observe(targetNode, config);
});
</script>
#84 - Effacer les entrées en charge
Ajoutez ce script à n'importe quelle page pour effacer la valeur d'un champ personnalisé au chargement de la page.
<!-- 💙 MEMBERSCRIPT #84 v0.1 💙 CLEAR INPUT VALUES ONLOAD -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
const fieldsToClear = ["phone", "last-name"]; // Specify the fields to clear
// Clear inputs and Memberstack fields on page load
memberstack.getCurrentMember().then(async ({ data: member }) => {
if (member) {
const customFieldsToUpdate = {};
fieldsToClear.forEach(fieldName => {
customFieldsToUpdate[fieldName] = '';
});
try {
await memberstack.updateMember({
customFields: customFieldsToUpdate
});
console.log("Fields cleared on page load.");
} catch (error) {
console.error('Error clearing fields on page load:', error);
}
}
// Clear input values on page load for specified fields
fieldsToClear.forEach(fieldName => {
const inputField = document.querySelector(`[data-ms-member="${fieldName}"]`);
if (inputField) {
inputField.value = '';
}
});
});
});
</script>
#83 - Préférences de cookies inter-appareils
Permettre aux membres d'enregistrer leurs préférences en matière de cookies dans leur compte.
<!-- 💙 MEMBERSCRIPT #83 v0.1 💙 CROSS-DEVICE COOKIE PREFERENCES -->
<script>
// Function to retrieve a cookie value by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift());
}
async function updateMemberConsentPreferences(fsCcCookieValue) {
try {
const memberstack = window.$memberstackDom;
const userData = await memberstack.getCurrentMember();
if (userData && userData.data.customFields) {
if (!userData.data.customFields['cookie-consent']) {
const decodedFsCcCookieValue = decodeURIComponent(fsCcCookieValue);
await memberstack.updateMember({
customFields: {
'cookie-consent': decodedFsCcCookieValue
}
});
} else {
document.cookie = `fs-cc=${encodeURIComponent(userData.data.customFields['cookie-consent'])}`;
}
}
} catch (error) {}
}
async function initialize() {
const fsCcCookieValue = getCookie('fs-cc');
if (fsCcCookieValue) {
await updateMemberConsentPreferences(fsCcCookieValue);
const checkboxes = document.querySelectorAll('[fs-cc-checkbox]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', async () => {
const memberstack = window.$memberstackDom;
const userData = await memberstack.getCurrentMember();
if (userData && userData.data.customFields) {
const customFieldKey = 'cookie-consent';
const checkboxName = checkbox.getAttribute('fs-cc-checkbox');
if (userData.data.customFields[customFieldKey]) {
const consentData = JSON.parse(userData.data.customFields[customFieldKey]);
consentData.consents[checkboxName] = checkbox.checked;
const updatedCustomField = JSON.stringify(consentData);
await memberstack.updateMember({
customFields: {
[customFieldKey]: updatedCustomField
}
});
document.cookie = `fs-cc=${encodeURIComponent(updatedCustomField)}`;
}
}
});
});
}
}
// Initialize the script
initialize();
</script>
#82 - Clés de licence
Sécurisez vos contenus téléchargeables à l'aide de clés de licence.
<!-- 💙 MEMBERSCRIPT #82 v0.1 💙 LICENSE KEYS -->
<script>
const memberstack = window.$memberstackDom;
// Initialize MutationObserver
const observer = new MutationObserver(async (mutations) => {
const downloadBtn = document.getElementById("download");
if (downloadBtn) {
// Element exists, so add event listener
downloadBtn.addEventListener("click", async () => {
await memberstack.removePlan({
planId: "pln_activate-license-key-952c0d8u"
});
console.log("Plan removed");
});
// Stop observing since we found the element
observer.disconnect();
}
});
// Observe the whole document
observer.observe(document.body, { childList: true, subtree: true });
</script>
#81 - Valeurs personnalisées des cases à cocher
Transmettre une valeur unique selon que la case est cochée ou non.
<!-- 💙 MEMBERSCRIPT #81 v0.1 💙 CUSTOM CHECKBOX VALUES -->
<script>
document.addEventListener('submit', function(e) {
var checkboxes = document.querySelectorAll('[ms-code-custom-checkbox]');
checkboxes.forEach(function(checkbox) {
var values = checkbox.getAttribute('ms-code-custom-checkbox').split(',');
var valueToSubmit = checkbox.checked ? values[0] : values[1];
var hiddenInput = document.createElement('input');
// Copy all attributes except type and ms-code-custom-checkbox
for (var i = 0; i < checkbox.attributes.length; i++) {
var attr = checkbox.attributes[i];
if (attr.name !== 'type' && attr.name !== 'ms-code-custom-checkbox') {
hiddenInput.setAttribute(attr.name, attr.value);
}
}
hiddenInput.type = 'hidden';
hiddenInput.value = valueToSubmit;
checkbox.form.appendChild(hiddenInput);
checkbox.remove(); // Remove the original checkbox so it doesn't interfere with submission
});
});
</script>
#80 - Notification d'annulation du plan
Déclencher une notification Slack lorsqu'un membre annule son plan.
#79 - Déclencher le clic au survol
Déclencher un événement de clic onHover.
<!-- 💙 MEMBERSCRIPT #79 v0.1 💙 HOVER BASED TABS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const hoverTabElements = document.querySelectorAll('[ms-code-onhover="click"]');
hoverTabElements.forEach(hoverTabElement => {
hoverTabElement.addEventListener('mouseenter', function() {
hoverTabElement.click(); // Click on the element when hovering
});
});
});
</script>
#78 - Effacer les entrées sur le clic
Créez un bouton qui peut effacer les valeurs d'une ou plusieurs entrées.
<!-- 💙 MEMBERSCRIPT #78 v0.1 💙 CLEAR INPUT VALUES ONCLICK -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const clearBtns = document.querySelectorAll('[ms-code-clear-value]');
clearBtns.forEach(btn => {
btn.addEventListener('click', () => {
const fieldIds = btn.getAttribute('ms-code-clear-value').split(',');
fieldIds.forEach(fieldId => {
const input = document.querySelector(`[data-ms-member="${fieldId}"]`);
if (input) {
input.value = '';
}
});
});
});
});
</script>
#77 - Emojis universels
Faites en sorte que vos emojis sur site soient les mêmes sur tous les appareils/OS.
<!-- 💙 MEMBERSCRIPT #77 v0.1 💙 UNIVERSAL EMOJIS -->
<script>
document.querySelectorAll('[ms-code-emoji]').forEach(element => {
var imageUrl = element.getAttribute('ms-code-emoji');
var img = document.createElement('img');
img.src = imageUrl;
var textStyle = window.getComputedStyle(element);
var adjustedHeight = parseFloat(textStyle.fontSize) * 1.0;
img.style.height = adjustedHeight + 'px';
img.style.width = 'auto';
img.style.verticalAlign = 'text-top';
element.innerHTML = ''; // Clears the text content inside the span
element.appendChild(img);
});
</script>
#76 - Visibilité dans le temps
Afficher différents éléments en fonction de l'heure de la journée.
<!-- 💙 MEMBERSCRIPT #76 v0.1 💙 TIME-BASED VISIBILITY -->
<script>
function hideElements() {
const elements = document.querySelectorAll('[ms-code-time]');
elements.forEach(element => {
element.style.display = 'none';
});
}
function displayBasedOnTime() {
const elements = document.querySelectorAll('[ms-code-time]');
const currentTime = new Date();
elements.forEach(element => {
const timeRange = element.getAttribute('ms-code-time');
const [start, end] = timeRange.split(' - ');
const [startHour, startMinute] = start.split(':').map(Number);
const [endHour, endMinute] = end.split(':').map(Number);
let startTime = new Date(currentTime);
startTime.setHours(startHour, startMinute, 0, 0);
let endTime = new Date(currentTime);
endTime.setHours(endHour, endMinute, 0, 0);
// If the end time is earlier than the start time, add a day to the end time
if (endTime < startTime) {
endTime.setDate(endTime.getDate() + 1);
}
if (currentTime >= startTime && currentTime <= endTime) {
element.style.display = 'flex';
}
});
}
// Call the functions
hideElements();
displayBasedOnTime();
</script>
#75 - Entrées de caractères non autorisées
Afficher un message d'erreur personnalisé si un utilisateur saisit un élément que vous avez défini dans une entrée.
<!-- 💙 MEMBERSCRIPT #75 v0.1 💙 DISALOWED CHARACTER INPUTS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const inputFields = document.querySelectorAll('[ms-code-disallow]');
inputFields.forEach(inputField => {
const errorBlock = inputField.nextElementSibling;
errorBlock.innerHTML = ''; // Use innerHTML to interpret <br> tags
inputField.addEventListener('input', function() {
const rules = inputField.getAttribute('ms-code-disallow').split(')');
let errorMessage = '';
rules.forEach(rule => {
const parts = rule.trim().split('=');
const ruleType = parts[0].substring(1); // Remove the opening parenthesis
const disallowedValue = parts[1];
if (ruleType.startsWith('custom')) {
const disallowedChar = ruleType.split('-')[1]; // Extract the character after the '-'
if (inputField.value.includes(disallowedChar)) {
errorMessage += disallowedValue + '<br>'; // Add line break
}
} else if (ruleType === 'space' && inputField.value.includes(' ')) {
errorMessage += disallowedValue + '<br>'; // Add line break
} else if (ruleType === 'number' && /\d/.test(inputField.value)) {
errorMessage += disallowedValue + '<br>'; // Add line break
} else if (ruleType === 'special' && /[^a-zA-Z0-9\s]/.test(inputField.value)) { // Notice the \s here
errorMessage += disallowedValue + '<br>'; // Add line break
}
});
errorBlock.innerHTML = errorMessage || ''; // Use innerHTML to interpret <br> tags
});
});
});
</script>
#74 - Styliser avec des paramètres de lien
Mise à jour du style de la page en fonction d'un paramètre de lien. Ex. ?ms-code-target=CLASSNAME&ms-code-style=display:block
<!-- 💙 MEMBERSCRIPT #74 v0.1 💙 UPDATE STYLING WITH LINK PARAMS -->
<script>
// Function to parse URL parameters
function getURLParameter(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
}
// Function to apply styles
function applyStylesFromURL() {
const targetClass = getURLParameter('ms-code-target');
const rawStyles = getURLParameter('ms-code-style');
if (targetClass && rawStyles) {
const elements = document.querySelectorAll(`.${targetClass}`);
const styles = rawStyles.split(';').filter(style => style.trim() !== ''); // filter out any empty strings
styles.forEach(style => {
const [property, value] = style.split(':');
elements.forEach(element => {
element.style[property] = value;
});
});
}
}
// Call the function once the DOM is loaded
window.addEventListener('DOMContentLoaded', (event) => {
applyStylesFromURL();
});
</script>
#73 - Affichage de la date et de l'heure
Affiche l'heure actuelle, l'heure du jour, le jour, le mois ou l'année pour un utilisateur. Fonctionne si l'utilisateur est connecté ou déconnecté.
<!-- 💙 MEMBERSCRIPT #73 v0.1 💙 DATES AND TIMES -->
function getCurrentDateInfo(attribute) {
const now = new Date();
const options = { hour12: true };
switch(attribute) {
case "day":
return now.toLocaleDateString('en-US', { weekday: 'long' });
case "time":
return now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase();
case "month":
return now.toLocaleDateString('en-US', { month: 'long' });
case "year":
return now.getFullYear().toString();
case "time-of-day":
const hour = now.getHours();
if (5 <= hour && hour < 12) return "morning";
if (12 <= hour && hour < 17) return "afternoon";
if (17 <= hour && hour < 21) return "evening";
return "night";
default:
return "Invalid attribute";
}
}
function updateDateInfoOnPage() {
const spanTags = document.querySelectorAll('span[ms-code-date]');
spanTags.forEach(tag => {
const attributeValue = tag.getAttribute('ms-code-date');
const dateInfo = getCurrentDateInfo(attributeValue);
tag.textContent = dateInfo;
});
}
// Call the function to update the content on the page
updateDateInfoOnPage();
</script>
#72 - Valider les valeurs originales
N'autoriser l'envoi d'un formulaire que si la valeur d'entrée est originale (c'est-à-dire les noms d'utilisateur).
<!-- 💙 MEMBERSCRIPT #72 v0.1 💙 VALIDATE ORIGINAL VALUES -->
<style>
[ms-code-available="true"],
[ms-code-available="false"],
[ms-code-available="invalid"]{
display: none;
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
let input = document.querySelector('[ms-code-available="input"]');
let trueElement = document.querySelector('[ms-code-available="true"]');
let falseElement = document.querySelector('[ms-code-available="false"]');
let invalidElement = document.querySelector('[ms-code-available="invalid"]');
let listElements = Array.from(document.querySelectorAll('[ms-code-available="list"]'));
let submitButton = document.querySelector('[ms-code-available="submit"]');
function checkUsername() {
// Check if the input matches any of the list items
let isTaken = listElements.some(elem => elem.textContent.trim() === input.value.trim());
if (isTaken) {
trueElement.style.display = 'none';
falseElement.style.display = 'flex';
submitButton.classList.add('disabled'); // disable the button if username is taken
} else {
trueElement.style.display = 'flex';
falseElement.style.display = 'none';
submitButton.classList.remove('disabled');
}
}
input.addEventListener('input', function() {
// Display the invalid element if input length is between 1 and 3
if (input.value.length >= 1 && input.value.length <= 3) {
invalidElement.style.display = 'flex';
} else {
invalidElement.style.display = 'none';
}
// Add the .disabled class to the submit button if input is empty or less than 3 characters
if (input.value.length <= 3) {
submitButton.classList.add('disabled');
trueElement.style.display = 'none';
falseElement.style.display = 'none';
} else {
checkUsername();
}
});
});
</script>
#71 - Redirection si certains champs sont vides
Rediriger un membre vers une page d'accueil si certains champs personnalisés sont vides.
<!-- 💙 MEMBERSCRIPT #71 v0.1 💙 REDIRECT IF FIELDS ARE EMPTY -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
const onboardingPageUrl = '/onboarding'; // replace
const customFieldKeys = 'custom-field-1,custom-field-2'; // replace
// No need to edit past this line
const member = await memberstack.getCurrentMember();
if (!member) {
return;
}
// If current page slug matches the redirect slug, exit the script
const currentPageSlug = window.location.pathname;
if (currentPageSlug === onboardingPageUrl) {
return;
}
async function checkOnboardingStatus() {
try {
const memberData = await memberstack.updateMember({});
const customFields = customFieldKeys.split(',');
for (let field of customFields) {
if (!memberData.data.customFields[field.trim()]) {
// Redirect to onboarding page if the custom field is empty
window.location.href = onboardingPageUrl;
return;
}
}
} catch (error) {
console.error(`Error in checkOnboardingStatus function: ${error}`);
}
}
// Check onboarding status and potentially redirect
checkOnboardingStatus().catch(error => {
console.error(`Error in MemberScript #71 initial functions: ${error}`);
});
});
</script>
#70 - Masquer les éléments anciens/vus du CMS
N'affichez que les éléments du CMS qui sont nouveaux pour un membre particulier. S'il l'a déjà vu, cachez-le.
<!-- 💙 MEMBERSCRIPT #70 v0.1 💙 HIDE OLD CMS ITEMS -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
// Only proceed if a member is found
const member = await memberstack.getCurrentMember();
if (!member) {
console.log('No member found in MemberScript #70, exiting script');
return;
}
async function getCmsItemsFromJson() {
try {
const memberData = await memberstack.getMemberJSON();
return memberData?.data?.cmsItems || [];
} catch (error) {
console.error(`Error in getCmsItemsFromJson function: ${error}`);
}
}
async function updateCmsItemsInJson(newCmsItems) {
try {
const memberData = await memberstack.getMemberJSON();
memberData.data = memberData.data || {};
memberData.data.cmsItems = newCmsItems;
console.log(`CMS items in JSON after update: ${JSON.stringify(newCmsItems)}`);
await memberstack.updateMemberJSON({ json: memberData.data });
} catch (error) {
console.error(`Error in updateCmsItemsInJson function: ${error}`);
}
}
async function hideSeenCmsItems() {
try {
const cmsItemsElements = document.querySelectorAll('[ms-code-cms-item]');
const cmsItemsFromJson = await getCmsItemsFromJson();
cmsItemsElements.forEach(element => {
const cmsValue = element.getAttribute('ms-code-cms-item');
if (cmsItemsFromJson.includes(cmsValue)) {
element.style.display = 'none';
} else {
cmsItemsFromJson.push(cmsValue);
}
});
// Update the CMS items in JSON after the checks
await updateCmsItemsInJson(cmsItemsFromJson);
} catch (error) {
console.error(`Error in hideSeenCmsItems function: ${error}`);
}
}
// Hide seen CMS items when the page loads
hideSeenCmsItems().catch(error => {
console.error(`Error in MemberScript #70 initial functions: ${error}`);
});
});
</script>
#69 - Notifier les membres des nouveaux éléments de la CMS
Afficher un élément lorsqu'il y a de nouveaux éléments CMS.
<!-- 💙 MEMBERSCRIPT #69 v0.1 💙 DISPLAY ELEMENT IF NEW CMS ITEMS -->
<script>
document.addEventListener('DOMContentLoaded', async function() {
const memberstack = window.$memberstackDom;
// Set this variable to 'YES' or 'NO' depending on whether you want the UI to be displayed for new users
const displayForNewUsers = 'YES';
// Only proceed if a member is found
const member = await memberstack.getCurrentMember();
if (!member) {
console.log('No member found, exiting script');
return;
}
async function getUpdatesIDFromJson() {
try {
const memberData = await memberstack.getMemberJSON();
console.log(`Member data: ${JSON.stringify(memberData)}`);
return memberData?.data?.updatesID || '';
} catch (error) {
console.error(`Error in getUpdatesIDFromJson function: ${error}`);
}
}
async function updateUpdatesIDInJson(newUpdatesID) {
try {
const memberData = await memberstack.getMemberJSON();
memberData.data = memberData.data || {};
memberData.data.updatesID = newUpdatesID;
console.log(`Updates ID in JSON after update: ${newUpdatesID}`);
await memberstack.updateMemberJSON({ json: memberData.data });
} catch (error) {
console.error(`Error in updateUpdatesIDInJson function: ${error}`);
}
}
async function checkAndUpdateUI() {
try {
const element = document.querySelector('[ms-code-update-item]');
const cmsItem = element.textContent;
console.log(`CMS item: ${cmsItem}`);
// Get the current updates ID from JSON
const updatesIDFromJson = await getUpdatesIDFromJson();
console.log(`Updates ID from JSON: ${updatesIDFromJson}`);
// Check displayForNewUsers variable to decide behavior
if (displayForNewUsers === 'NO' && !updatesIDFromJson) {
console.log('Updates ID from JSON is undefined, null, or empty, not changing UI');
return;
}
if (cmsItem !== updatesIDFromJson) {
const uiElements = document.querySelectorAll('[ms-code-update-ui]');
uiElements.forEach(uiElement => {
uiElement.style.display = 'block';
uiElement.style.opacity = '1';
});
}
// Update the updates ID in JSON after the UI has been updated
await updateUpdatesIDInJson(cmsItem);
} catch (error) {
console.error(`Error in checkAndUpdateUI function: ${error}`);
}
}
// Check and update UI when the page loads
checkAndUpdateUI().catch(error => {
console.error(`Error in initial functions: ${error}`);
});
});
</script>
#68 - Offrir une adhésion
Permettre aux membres d'acheter des cadeaux pour leurs amis et leur famille.
#67 - Pré-remplir le formulaire en fonction des paramètres de l'URL
Remplir facilement les entrées à l'aide de paramètres URL.
<!-- 💙 MEMBERSCRIPT #67 v0.1 💙 PREFILL INPUTS WITH URL PARAMETERS -->
<script>
// Function to get URL parameters
function getURLParams() {
const urlParams = new URLSearchParams(window.location.search);
return Object.fromEntries(urlParams.entries());
}
// Function to prefill inputs based on URL parameters
function prefillInputs() {
const urlParams = getURLParams();
const inputElements = document.querySelectorAll('[ms-code-prefill-param]');
inputElements.forEach((inputElement) => {
const paramKey = inputElement.getAttribute('ms-code-prefill-param');
if (paramKey && urlParams[paramKey]) {
inputElement.value = urlParams[paramKey];
}
});
}
// Call the function to prefill inputs when the page loads
prefillInputs();
</script>
#66 - Liens d'invitation pour l'identification des membres
Créer des liens d'invitation/de référence personnalisés et uniques.
<!-- 💙 MEMBERSCRIPT #66 v0.1 💙 MEMBER ID INVITE LINKS -->
<script>
// Function to get the member ID from local storage
function getMemberIDFromLocalStorage() {
// Assuming "_ms-mem" is the key that holds the member object in local storage
const memberObject = JSON.parse(localStorage.getItem("_ms-mem"));
if (memberObject && memberObject.id) {
return memberObject.id;
}
return null;
}
// Function to update the invite link with the member ID as a URL parameter
function updateInviteLink() {
const inviteLinkElement = document.querySelector('[ms-code-invite-link]');
if (inviteLinkElement) {
const inviteLinkBase = inviteLinkElement.getAttribute('ms-code-invite-link');
const memberID = getMemberIDFromLocalStorage();
if (memberID) {
const inviteLinkWithID = `${inviteLinkBase}?inviteCode=${memberID}`;
inviteLinkElement.textContent = inviteLinkWithID;
inviteLinkElement.href = inviteLinkWithID; // If it's an anchor link
}
}
}
// Call the function to update the invite link when the page loads
updateInviteLink();
</script>
#65 - Fenêtre contextuelle d'intention de sortie
Montrer aux visiteurs une fenêtre contextuelle lorsque leur souris quitte le haut de la page.
<!-- 💙 MEMBERSCRIPT #65 v0.1 💙 EXIT INTENT POPUP -->
<script>
const CookieService = {
setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = days ? '; expires=' + date.toUTCString() : '';
document.cookie = name + '=' + (value || '') + expires + ';';
},
getCookie(name) {
const cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith(name))
?.split('=')[1];
return cookieValue || null;
}
};
const exitPopup = document.querySelector('[ms-code-popup="exit-intent"]');
const mouseEvent = e => {
const shouldShowExitIntent =
!e.toElement &&
!e.relatedTarget &&
e.clientY < 10;
if (shouldShowExitIntent) {
document.removeEventListener('mouseout', mouseEvent);
exitPopup.style.display = 'flex';
CookieService.setCookie('exitIntentShown', true, 30);
}
};
if (!CookieService.getCookie('exitIntentShown')) {
document.addEventListener('mouseout', mouseEvent);
document.addEventListener('keydown', exit);
exitPopup.addEventListener('click', exit);
}
</script>
#64 - Logique du formulaire radio
Affiche les éléments du jeu en fonction de la radio sélectionnée.
<!-- 💙 MEMBERSCRIPT #64 v0.1 💙 RADIO FORM LOGIC -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
// initially hide all divs with 'ms-code-more-info' attribute
$("div[ms-code-more-info]").hide();
// listen for change events on all radios with 'ms-code-radio-option' attribute
$("input[ms-code-radio-option]").change(function() {
// hide all divs again
$("div[ms-code-more-info]").hide();
// get the value of the selected radio button
var selectedValue = $(this).attr("ms-code-radio-option");
// find the div with the 'ms-code-more-info' attribute that matches the selected value and show it
$("div[ms-code-more-info=" + selectedValue + "]").show();
});
});
</script>
#63 - Sélecteur d'intervalle de dates
Créer une plage de dates dans Webflow !
<!-- 💙 MEMBERSCRIPT #62 v0.1 💙 DATE RANGE PICKER -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
<style>
.daterangepicker td.active {
background-color: #006cfa !important ;
}
</style>
<script type="text/javascript">
$(function() {
$('input[ms-code-input="date-range"]').daterangepicker({
"opens": "center",
"locale": {
"format": "MM/DD/YYYY",
"separator": " - ",
"applyLabel": "Apply",
"cancelLabel": "Cancel",
"fromLabel": "From",
"toLabel": "To",
"customRangeLabel": "Custom",
"weekLabel": "W",
"daysOfWeek": [
"Su",
"Mo",
"Tu",
"We",
"Th",
"Fr",
"Sa"
],
"monthNames": [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
},
});
$('input[name="datefilter"]').on('apply.daterangepicker', function(ev, picker) {
$(this).val(picker.startDate.format('MM/DD/YYYY') + ' - ' + picker.endDate.format('MM/DD/YYYY'));
});
$('input[name="datefilter"]').on('cancel.daterangepicker', function(ev, picker) {
$(this).val('');
});
});
</script>
#62 - Bouton "Upvote
Ajouter la fonctionnalité "upvote" au CMS Webflow.
<!-- 💙 MEMBERSCRIPT #62 v0.2 💙 UPVOTE FORM -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const memberstack = window.$memberstackDom;
const upvoteButtons = document.querySelectorAll('[ms-code="upvote-button"]');
const upvoteForms = document.querySelectorAll('[ms-code="upvote-form"]');
const upvotedValues = document.querySelectorAll('[ms-code="upvoted-value"]');
const upvoteCounts = document.querySelectorAll('[ms-code="upvote-count"]');
let clickTimeout; // Variable to store the timer
let lastClickedButton = null; // Variable to store the last clicked button
// Function to handle upvote button click
function handleUpvoteButtonClick(event) {
event.preventDefault();
const button = event.currentTarget;
// Clear the timer if the same button is clicked
if (button === lastClickedButton) {
clearTimeout(clickTimeout);
}
lastClickedButton = button; // Store the reference to the currently clicked button
// Set a new timer
clickTimeout = setTimeout(function() {
const form = button.closest('form');
const cmsId = button.getAttribute('data-cms-id');
const upvotedValue = form.querySelector('[ms-code="upvoted-value"]');
const upvoteCount = form.querySelector('[ms-code="upvote-count"]');
if (button.classList.contains('is-true')) {
// Remove upvote
button.classList.remove('is-true');
upvotedValue.value = 'false';
upvoteCount.textContent = parseInt(upvoteCount.textContent) - 1;
memberstack.getMemberJSON()
.then(function(memberData) {
if (memberData.data && memberData.data.upvotes) {
const upvotes = memberData.data.upvotes;
const index = upvotes.indexOf(cmsId);
if (index !== -1) {
upvotes.splice(index, 1);
memberstack.updateMemberJSON({ json: memberData.data });
}
}
})
.catch(function(error) {
console.error('Error retrieving/updating member data:', error);
});
} else {
// Add upvote
button.classList.add('is-true');
upvotedValue.value = 'true';
upvoteCount.textContent = parseInt(upvoteCount.textContent) + 1;
memberstack.getMemberJSON()
.then(function(memberData) {
memberData.data = memberData.data || {};
memberData.data.upvotes = memberData.data.upvotes || [];
memberData.data.upvotes.push(cmsId);
memberstack.updateMemberJSON({ json: memberData.data });
})
.catch(function(error) {
console.error('Error retrieving/updating member data:', error);
});
}
// Make the API call
fetch(form.action, {
method: form.method,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(new FormData(form))
})
.then(function(response) {
if (response.ok) {
// Handle successful API response
return response.json();
} else {
// Handle API error
throw new Error('API Error');
}
})
.then(function(data) {
// Handle API response to update vote count
upvoteCount.textContent = data.upvoteCount; // Replace with the actual property holding the updated vote count
})
.catch(function(error) {
console.error('API Error:', error);
});
}, 200); // 0.2 seconds
}
// Attach event listeners to upvote buttons
upvoteButtons.forEach(function(button) {
button.addEventListener('click', handleUpvoteButtonClick);
});
// Check if member has upvotes on page load
memberstack.getMemberJSON()
.then(function(memberData) {
if (memberData.data && memberData.data.upvotes) {
const upvotes = memberData.data.upvotes;
upvoteButtons.forEach(function(button) {
const cmsId = button.getAttribute('data-cms-id');
if (upvotes.includes(cmsId)) {
button.classList.add('is-true');
const form = button.closest('form');
const upvotedValue = form.querySelector('[ms-code="upvoted-value"]');
upvotedValue.value = 'true';
}
});
}
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
});
</script>
#61 - Affichage de l'élément si la case est cochée
Créer une visibilité conditionnelle basée sur un champ de type case à cocher.
<!-- 💙 MEMBERSCRIPT #61 v0.1 💙 SHOW ELEMENT IF CHECKBOX IS CHECKED -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script>
$(document).ready(function() {
// Initially hide all elements with the 'ms-code-checkbox-display' attribute
$("[ms-code-checkbox-display]").hide();
// When a checkbox with 'ms-code-checkbox-input' attribute is clicked, perform the following
$("[ms-code-checkbox-input]").click(function() {
// Get the value of the 'ms-code-checkbox-input' attribute
var checkboxVal = $(this).attr('ms-code-checkbox-input');
// Find the corresponding element with the 'ms-code-checkbox-display' attribute and same value
var displayElement = $("[ms-code-checkbox-display=" + checkboxVal + "]");
// If this checkbox is checked, show the corresponding element
if ($(this).is(":checked")) {
displayElement.show();
} else {
// If this checkbox is unchecked, hide the corresponding element
displayElement.hide();
}
});
});
</script>
#60 - Augmentation/diminution de la valeur de sélection
Créer des boutons précédent et suivant pour un champ de sélection.
<!-- 💙 MEMBERSCRIPT #60 v0.1 💙 INCREASE/DECREASE SELECT VALUE -->
<script>
var select = document.querySelector('[ms-code-select="input"]');
var prev = document.querySelector('[ms-code-select="prev"]');
var next = document.querySelector('[ms-code-select="next"]');
function updateButtons() {
prev.style.opacity = select.selectedIndex === 0 ? '0.5' : '1';
next.style.opacity = select.selectedIndex === select.options.length - 1 ? '0.5' : '1';
}
prev.addEventListener('click', function() {
if (select.selectedIndex > 0) {
select.selectedIndex--;
}
updateButtons();
});
next.addEventListener('click', function() {
if (select.selectedIndex < select.options.length - 1) {
select.selectedIndex++;
}
updateButtons();
});
updateButtons();
</script>
#59 - Redémarrer le GIF au survol de la page
Commencer un GIF depuis le début au survol de la page.
<!-- 💙 MEMBERSCRIPT #59 v0.1 💙 RESTART GIF -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const hoverElements = document.querySelectorAll('[data-gif-hover]');
hoverElements.forEach((element) => {
element.addEventListener('mouseover', function() {
const gifNum = this.getAttribute('data-gif-hover');
const gifElement = document.querySelector(`[data-gif="${gifNum}"]`);
if (gifElement) {
const gifSrc = gifElement.getAttribute('src');
gifElement.setAttribute('src', '');
gifElement.setAttribute('src', gifSrc);
}
});
});
});
</script>
#58 - Entrées du curseur de gamme de prix
Créez une entrée de fourchette de prix avec des entrées individuelles pour le minimum et le maximum.
<!-- 💙 MEMBERSCRIPT #58 v0.1 💙 RANGE SLIDER INPUT -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/css/ion.rangeSlider.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/js/ion.rangeSlider.min.js"></script>
<style>
.irs {
font-family: inherit;
}
</style>
<script>
$(document).ready(function() {
var rangeSlider = $('[ms-code-input="range-slider"]');
var priceFromInput = $('[ms-code-input="price-from"]');
var priceToInput = $('[ms-code-input="price-to"]');
// Set the default range values
var defaultFrom = 20000;
var defaultTo = 50000;
rangeSlider.ionRangeSlider({
skin: "round", // You can also try "flat", "big", "modern", "sharp", or "square". Default styles can be overridden with CSS.
type: "double",
grid: true,
min: 0,
max: 100000,
from: defaultFrom,
to: defaultTo,
prefix: "$",
onStart: function(data) {
priceFromInput.val(data.from);
priceToInput.val(data.to);
},
onChange: function(data) {
priceFromInput.val(data.from);
priceToInput.val(data.to);
}
});
// Get the initial range values and update the inputs
var initialRange = rangeSlider.data("ionRangeSlider");
var initialData = initialRange.result;
priceFromInput.val(initialData.from);
priceToInput.val(initialData.to);
// Update the range slider and inputs when the inputs lose focus
priceFromInput.on("blur", function() {
var value = $(this).val();
var toValue = priceToInput.val();
// Perform validation
if (
value < initialRange.options.min ||
value > initialRange.options.max ||
value >= toValue ||
value > initialData.to // Check if fromValue is higher than the current toValue
) {
value = defaultFrom;
}
rangeSlider.data("ionRangeSlider").update({
from: value
});
priceFromInput.val(value);
});
priceToInput.on("blur", function() {
var value = $(this).val();
var fromValue = priceFromInput.val();
// Perform validation
if (
value < initialRange.options.min ||
value > initialRange.options.max ||
value <= fromValue ||
value < initialData.from // Check if toValue is lower than the current fromValue
) {
value = defaultTo;
}
rangeSlider.data("ionRangeSlider").update({
to: value
});
priceToInput.val(value);
});
});
</script>
#57 - Saisie de l'heure
Ajoutez un sélecteur de temps à votre formulaire et pré-remplissez l'heure dans un champ.
<!-- 💙 MEMBERSCRIPT #57 v0.1 💙 TIME PICKER -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.css">
<script src="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.js"> </script>
<script>
$(document).ready(function() {
var tpSpinbox = new tui.TimePicker(document.querySelector('[ms-code-timepicker="box"]'), {
inputType: 'spinbox',
showMeridiem: true // If you don't use AM/PM remove this line
});
// Setup an event handler for when the time is selected
tpSpinbox.on('change', function() {
// Get the selected time
var hour = tpSpinbox.getHour();
var minute = tpSpinbox.getMinute();
var selectedTime = hour + ':' + (minute < 10 ? '0' : '') + minute;
// Update the value of the input element
document.querySelector('[ms-code-timepicker="input"]').value = selectedTime;
});
});
</script>
#56 - Paires d'options d'entrée
Combinez les valeurs de plusieurs entrées dans un seul champ.
<!-- 💙 MEMBERSCRIPT #56 v0.1 💙 INPUT OPTION PAIRS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script>
$(document).ready(function() {
var groups = {};
// Get all inputs with the attribute ms-code-combine-inputs
var inputs = $('input[ms-code-combine-inputs], select[ms-code-combine-inputs]');
// For each input
inputs.each(function() {
// Split the attribute value at the dash
var parts = $(this).attr('ms-code-combine-inputs').split('-');
// If the group doesn't exist yet, create it
if (!groups[parts[0]]) {
groups[parts[0]] = {
targets: [],
values: [],
};
}
// If it's a target, add it to the targets
if (parts[1] == 'target') {
groups[parts[0]].targets.push($(this));
} else {
// It's an input, add it to the values and attach a listener
groups[parts[0]].values.push($(this));
$(this).on('input change', function() {
// On input or change, combine all values with a space in between
// and set the targets' value
var combinedValue = '';
$.each(groups[parts[0]].values, function(index, value) {
combinedValue += $(this).val();
if (index < groups[parts[0]].values.length - 1) {
combinedValue += ' '; // Add a space between values
}
});
$.each(groups[parts[0]].targets, function() {
$(this).val(combinedValue);
});
});
}
});
});
</script>
#55 - Modifier le style des cases à cocher
Modifier les styles d'un autre élément lorsqu'une case est cochée.
<!-- 💙 MEMBERSCRIPT #55 v0.1 💙 UPDATE CHECKBOX PARENT STYLES -->
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
// Get all the checkbox elements
var checkboxes = document.querySelectorAll('[ms-code-checkbox="check"]');
// Iterate over each checkbox element
checkboxes.forEach(function(checkbox) {
// Get the boolean wrap element associated with the current checkbox
var booleanWrap = checkbox.closest('[ms-code-checkbox="wrap"]');
// Function to update the boolean wrap class based on checkbox state
function updateBooleanWrapClass() {
if (checkbox.checked) {
booleanWrap.classList.add('checked');
} else {
booleanWrap.classList.remove('checked');
}
}
// Check the initial value of the checkbox
updateBooleanWrapClass();
// Add an event listener to the checkbox to handle changes
checkbox.addEventListener('change', function() {
updateBooleanWrapClass();
});
});
});
</script>
#54 - Logique des champs de formulaire à cases à cocher
Bloquer d'autres champs/éléments si une case n'est pas cochée.
<!-- 💙 MEMBERSCRIPT #54 v0.1 💙 CHECKBOX FIELD FORM LOGIC -->
<style>
.disabled {
pointer-events: none;
opacity: 0.5;
}
</style>
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
// Get all the trigger checkboxes
var triggerCheckboxes = document.querySelectorAll('[ms-code-field-logic-trigger]');
// Iterate over each trigger checkbox
triggerCheckboxes.forEach(function(checkbox) {
// Get the value of the trigger checkbox's attribute
var triggerValue = checkbox.getAttribute('ms-code-field-logic-trigger');
// Function to update the target elements' class based on checkbox state
function updateTargetElementsClass() {
// Find the associated target elements based on the attribute value
var targetElements = document.querySelectorAll('[ms-code-field-logic-target="' + triggerValue + '"]');
// Check the new value of the trigger checkbox
if (!checkbox.checked) {
// Add the "disabled" class to each target element
targetElements.forEach(function(targetElement) {
targetElement.classList.add('disabled');
});
} else {
// Remove the "disabled" class from each target element
targetElements.forEach(function(targetElement) {
targetElement.classList.remove('disabled');
});
}
}
// Check the initial value of the trigger checkbox
updateTargetElementsClass();
// Add an event listener to the trigger checkbox to handle changes
checkbox.addEventListener('change', function() {
updateTargetElementsClass();
});
});
});
</script>
#53 - Mettre à jour des éléments JSON avec un formulaire
Permettez à vos membres de modifier les détails de leurs éléments JSON.
<!-- 💙 MEMBERSCRIPT #53 v0.1 💙 UPDATE JSON ITEMS WITH A FORM -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const memberstack = window.$memberstackDom;
// Add click event listener to the document
document.addEventListener("click", async function(event) {
const target = event.target;
// Check if the clicked element has ms-code-edit-item attribute
const editItem = target.closest('[ms-code-edit-item="prompt"]');
if (editItem) {
// Get the item key from the closest ancestor element with ms-code-item-key attribute
const key = editItem.closest('[ms-code-item-key]').getAttribute('ms-code-item-key');
// Retrieve the current member JSON data
const member = await memberstack.getMemberJSON();
// SET THE TARGET - EDIT ME
let targetObject = member.data.projects; // Update this line with the desired target location
if (member.data && targetObject && targetObject[key]) {
// Get the form element with the ms-code-edit-item="form" attribute
const form = document.querySelector('form[ms-code-edit-item="form"]');
if (form) {
// Loop through the form fields
for (const field of form.elements) {
const jsonName = field.getAttribute('ms-code-json-name');
if (jsonName && targetObject[key].hasOwnProperty(jsonName)) {
// Pre-fill the form field with the corresponding value from the JSON item
field.value = targetObject[key][jsonName];
}
}
// Get the modal element with the ms-code-edit-item="modal" attribute
const modal = document.querySelector('[ms-code-edit-item="modal"]');
if (modal) {
// Set the display property of the modal to flex
modal.style.display = 'flex';
}
// Add submit event listener to the form
form.addEventListener("submit", async function(event) {
event.preventDefault(); // Prevent the form from submitting normally
// Create an object to hold the updated values
const updatedValues = {};
// Loop through the form fields
for (const field of form.elements) {
const jsonName = field.getAttribute('ms-code-json-name');
if (jsonName) {
// Update the corresponding value in the updatedValues object
updatedValues[jsonName] = field.value;
}
}
// Update the target object with the new values
targetObject[key] = { ...targetObject[key], ...updatedValues };
// Update the member JSON using the Memberstack SDK
await memberstack.updateMemberJSON({
json: member.data
});
// Optional: Display a success message or perform any other desired action
console.log('Member JSON updated successfully');
});
} else {
console.error('Form element not found');
}
} else {
console.error(`Could not find item with key: ${key}`);
}
}
});
});
</script>
#52 - Redirection de page retardée
Rediriger les membres vers une nouvelle page avec un délai facultatif.
<!-- 💙 MEMBERSCRIPT #52 v0.1 💙 DELAYED REDIRECT TO NEW PAGE -->
<script>
const redirectToNewPage = function() {
setTimeout(function() {
window.location.href = "/your-page";
}, 1000); // Delay in milliseconds
};
redirectToNewPage();
</script>
#51 - Afficher les métadonnées des membres
Affichez les métadonnées des membres de manière dynamique sur votre site web.
<!-- 💙 MEMBERSCRIPT #51 v0.2 💙 DISPLAY MEMBER METADATA -->
<script>
function replaceTextWithMetadata(metadata) {
var els = Array.from(document.querySelectorAll('[ms-code-member-meta]'));
els.forEach((el) => {
const key = el.getAttribute('ms-code-member-meta');
const value = metadata[key];
if (value !== undefined) {
el.innerHTML = value;
el.value = value;
el.src = value;
}
});
}
const memberstack = window.$memberstackDom;
memberstack.getCurrentMember()
.then(({ data: member }) => {
if (member && member.metaData) {
replaceTextWithMetadata(member.metaData);
}
})
.catch((error) => {
console.error('Error retrieving member data:', error);
});
</script>
#50 - Mode sombre inter-appareils
Option de mode sombre persistant qui continue à fonctionner sur les différents appareils de vos membres.
Code de la tête
Put this in the <head> section of your site.
<!-- 💙 MEMBERSCRIPT #50 HEAD CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const themePreference = localStorage.getItem('themePreference');
if (themePreference === 'dark') {
document.body.classList.add('dark');
}
});
</script>
Code du corps
Put this in the </body> section of your site.
<!-- 💙 MEMBERSCRIPT #50 BODY CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const darkModeToggle = document.querySelector('[ms-code-dark-mode="toggle"]');
const bodyElement = document.querySelector('.body');
// Function to check if theme preference is saved in member JSON
function checkThemePreference() {
const memberstack = window.$memberstackDom;
memberstack.getMemberJSON()
.then(function(memberData) {
const themePreference = memberData.data?.themePreference;
if (themePreference === 'dark') {
enableDarkMode();
} else {
disableDarkMode();
}
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
}
// Function to enable dark mode
function enableDarkMode() {
darkModeToggle.classList.add('dark');
bodyElement.classList.add('dark');
updateThemePreference('dark');
}
// Function to disable dark mode
function disableDarkMode() {
darkModeToggle.classList.remove('dark');
bodyElement.classList.remove('dark');
updateThemePreference('light');
}
// Function to update theme preference in member JSON
function updateThemePreference(themePreference) {
const memberstack = window.$memberstackDom;
memberstack.getMemberJSON()
.then(function(memberData) {
memberData.data = memberData.data || {};
memberData.data.themePreference = themePreference;
memberstack.updateMemberJSON({ json: memberData.data })
.then(function() {
localStorage.setItem('themePreference', themePreference);
})
.catch(function(error) {
console.error('Error updating member data:', error);
});
})
.catch(function(error) {
console.error('Error retrieving member data:', error);
});
}
// Event listener for dark mode toggle
darkModeToggle.addEventListener('click', function() {
if (darkModeToggle.classList.contains('dark')) {
disableDarkMode();
} else {
enableDarkMode();
}
});
// Apply transition duration and timing to all elements
const transitionDuration = '0.0s';
const transitionTiming = 'ease';
const elementsToTransition = [darkModeToggle, bodyElement];
elementsToTransition.forEach(function(element) {
element.style.transitionDuration = transitionDuration;
element.style.transitionTimingFunction = transitionTiming;
});
// Check theme preference on page load
const savedThemePreference = localStorage.getItem('themePreference');
if (savedThemePreference === 'dark') {
enableDarkMode();
} else {
disableDarkMode();
}
checkThemePreference();
});
</script>
#49 - Désactiver la première option d'une entrée de sélection
Empêchez les utilisateurs de sélectionner l'option de remplacement dans vos entrées de sélection.
<!-- 💙 MEMBERSCRIPT #49 v0.1 💙 DISABLE FIRST OPTION IN SELECT INPUT -->
<script>
let selects = document.querySelectorAll("select[ms-code=hide-first-option]");
selects.forEach((select) => {
let options = select.getElementsByTagName("option");
options[0].hidden = true;
});
</script>
#48 - Autocomplétion des entrées d'adresses
Pré-remplir toutes les entrées d'adresse à l'aide de l'API Google Places !
Code de la tête
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #48 HEAD CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR-API-KEY&libraries=places&callback=initAutocomplete" async defer> </script>
<style>
.pac-logo::after {
display: none;
}
.pac-container {
border-radius: 5px;
border: 1px solid #ccc;
}
.pac-item {
padding: 0 10px;
}
</style>
Code du corps
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #48 BODY CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script>
let autocomplete;
function initAutocomplete() {
autocomplete = new google.maps.places.Autocomplete(
document.querySelector('input[ms-code-input="address"]'),
{
componentRestrictions: { country: ['US'] },
fields: ['address_components'],
types: ['address']
}
);
autocomplete.addListener('place_changed', function() {
const place = autocomplete.getPlace();
if (place) {
const addressInput = document.querySelector('input[ms-code-input="address"]');
const cityInput = document.querySelector('input[ms-code-input="city"]');
const regionInput = document.querySelector('input[ms-code-input="region"]');
const countryInput = document.querySelector('input[ms-code-input="country"]');
const postalCodeInput = document.querySelector('input[ms-code-input="postal-code"]');
addressInput.value = extractAddress(place);
cityInput.value = extractCity(place);
regionInput.value = extractRegion(place);
countryInput.value = extractCountry(place);
postalCodeInput.value = extractPostalCode(place);
}
});
}
function extractAddress(place) {
let address = '';
const streetNumber = extractComponent(place, 'street_number');
const route = extractComponent(place, 'route');
if (streetNumber) {
address += streetNumber + ' ';
}
if (route) {
address += route;
}
return address.trim();
}
function extractComponent(place, componentType) {
for (const component of place.address_components) {
if (component.types.includes(componentType)) {
return component.long_name;
}
}
return '';
}
function extractCity(place) {
for (const component of place.address_components) {
if (component.types.includes('locality')) {
return component.long_name;
}
}
return '';
}
function extractRegion(place) {
for (const component of place.address_components) {
if (component.types.includes('administrative_area_level_1')) {
return component.long_name;
}
}
return '';
}
function extractCountry(place) {
for (const component of place.address_components) {
if (component.types.includes('country')) {
return component.long_name;
}
}
return '';
}
function extractPostalCode(place) {
for (const component of place.address_components) {
if (component.types.includes('postal_code')) {
return component.long_name;
}
}
return '';
}
</script>
#47 - Afficher la date du membre JSON
Indiquez aux membres une date - par exemple, la date d'expiration de leur plan !
<!-- 💙 MEMBERSCRIPT #47 v0.1 💙 DISPLAY ONE TIME DATE -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
const memberstack = window.$memberstackDom;
const formatDate = function(date) {
const options = { month: 'long', day: 'numeric', year: 'numeric' };
return new Date(date).toLocaleDateString('en-US', options);
// Replace 'en-US' with one of these depending on your locale: en-US, en-GB, en-CA, en-AU, fr-FR, de-DE, es-ES, it-IT, ja-JP, ko-KR, pt-BR, ru-RU, zn-CH, ar-SA
};
const updateTextSpans = async function() {
const member = await memberstack.getMemberJSON();
if (!member.data || !member.data['one-time-date']) {
// Member data or one-time date not available, do nothing
return;
}
const oneTimeDate = formatDate(member.data['one-time-date']);
const textSpans = document.querySelectorAll('[ms-code-display-text="one-time-date"]');
textSpans.forEach(span => {
span.textContent = oneTimeDate;
});
};
updateTextSpans();
});
</script>
#46 - Confirmer le mot de passe
Ajoutez une entrée de confirmation de mot de passe à vos formulaires d'inscription et de réinitialisation de mot de passe.
<!-- 💙 MEMBERSCRIPT #46 v0.1 💙 CONFIRM PASSWORD INPUT -->
<script>
var password = document.querySelector('[data-ms-member=password]')
, confirm_password = document.querySelector('[ms-code-password=confirm]')
function validatePassword(){
if(password.value != confirm_password.value) {
confirm_password.setCustomValidity("Passwords Don't Match");
confirm_password.classList.add("invalid")
confirm_password.classList.remove("valid")
} else {
confirm_password.setCustomValidity('');
confirm_password.classList.remove("invalid")
confirm_password.classList.add("valid")
}
}
password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
</script>
#45 - Afficher/masquer le mot de passe
Ajoutez un bouton "afficher/masquer le mot de passe" à tout formulaire comportant une entrée de mot de passe.
<!-- 💙 MEMBERSCRIPT #45 v0.2 💙 SHOW AND HIDE PASSWORD -->
<script>
document.querySelectorAll("[ms-code-password='transform']").forEach(function(button) {
button.addEventListener("click", transform);
});
var isPassword = true;
function transform() {
var passwordInputs = document.querySelectorAll("[data-ms-member='password'], [data-ms-member='new-password'], [data-ms-member='current-password']");
passwordInputs.forEach(function(myInput) {
var inputType = myInput.getAttribute("type");
if (isPassword) {
myInput.setAttribute("type", "text");
} else {
myInput.setAttribute("type", "password");
}
});
isPassword = !isPassword;
}
</script>
#44 - Afficher l'élément si l'attribut correspond à l'identifiant du membre
Affiche conditionnellement les éléments qui ont un attribut correspondant à l'ID des membres.
<!-- 💙 MEMBERSCRIPT #44 v0.1 💙 SHOW ELEMENT IF ATTRIBUTE MATCHES MEMBER ID -->
<script>
document.addEventListener("DOMContentLoaded", function() {
if (localStorage.getItem("_ms-mem")) {
const memberData = JSON.parse(localStorage.getItem("_ms-mem"));
const memberId = memberData.id;
const elements = document.querySelectorAll("[ms-code-member-id='" + memberId + "']");
elements.forEach(element => {
element.style.display = "block";
});
}
});
</script>
#43 - Bloquer le défilement lorsque la fenêtre modale est ouverte
Empêcher le défilement de la page lorsque quelqu'un ouvre une fenêtre modale.
<!-- 💙 MEMBERSCRIPT #43 v0.1 💙 BLOCK SCROLLING WHEN MODAL IS OPEN -->
<style>
.no-scroll {
overflow: hidden;
}
</style>
<script>
function isDesktopViewport() {
return window.innerWidth >= 900; // Adjust the breakpoint width as needed
}
const codeBlocks = document.querySelectorAll('[ms-code-block-scroll]');
function handleScrollBlock(event) {
if (isDesktopViewport()) {
document.body.classList.add('no-scroll');
}
}
function handleScrollUnblock(event) {
if (isDesktopViewport()) {
document.body.classList.remove('no-scroll');
}
}
codeBlocks.forEach(codeBlock => {
codeBlock.addEventListener('mouseenter', handleScrollBlock);
codeBlock.addEventListener('mouseleave', handleScrollUnblock);
});
</script>
#42 - Champ de formulaire de l'éditeur d'images
Permettez aux gens de télécharger et de modifier des photos, puis de les envoyer sur Google Drive !
Code de la tête
Place this in your page <head>
<!-- 💙 MEMBERSCRIPT #42 HEAD CODE v0.2 💙 FILE EDITOR FEATURE -->
<link rel="stylesheet" href="https://unpkg.com/filepond@^4/dist/filepond.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" />
Code du corps
Place this in your page </body>
<!-- 💙 MEMBERSCRIPT #42 BODY CODE v0.2 💙 FILE EDITOR FEATURE -->
<script> src="https://unpkg.com/filepond-plugin-file-encode/dist/filepond-plugin-file-encode.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.js"> </script>
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script> src="https://scaleflex.cloudimg.io/v7/plugins/filerobot-image-editor/latest/filerobot-image-editor.min.js"> </script>
<style>
.dXhZSB {
background-color: #2962ff;
}
.FIE_root * {
font-family: inherit !important;
}
.SfxModal-Wrapper * {
font-family: inherit !important;
}
.jpHEiD {
font-family: inherit !important;
}
#editor_container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Register the plugins
FilePond.registerPlugin(FilePondPluginImagePreview);
FilePond.registerPlugin(FilePondPluginImageEdit);
const inputElement = document.querySelector('input[type="file"]');
const pond = FilePond.create(inputElement, {
credits: false,
name: 'fileToUpload',
storeAsFile: true,
imageEditEditor: {
open: (file, instructions) => {
console.log('Open editor', file, instructions);
openFilerobotImageEditor(file, instructions);
},
onconfirm: (output) => {
console.log('Confirm editor', output);
handleImageEditConfirm(output);
},
oncancel: () => {
console.log('Cancel editor');
handleImageEditCancel();
},
onclose: () => {
console.log('Close editor');
handleImageEditClose();
}
}
});
function openFilerobotImageEditor(file, instructions) {
const imageURL = URL.createObjectURL(file);
const config = {
source: imageURL,
onSave: (updatedImage) => {
confirmCallback(updatedImage);
},
annotationsCommon: {
fill: '#ff0000'
},
Text: {
text: 'Add your text here',
font: 'inherit'
}, // Set font to inherit from the page body
Rotate: {
angle: instructions.rotation,
componentType: 'slider'
},
tabsIds: [
'Adjust',
'Annotate',
'Watermark'
],
defaultTabId: 'Annotate',
defaultToolId: 'Text'
};
const editorContainer = document.createElement('div');
editorContainer.id = 'editor_container';
document.body.appendChild(editorContainer);
const filerobotImageEditor = new window.FilerobotImageEditor(editorContainer, config);
const confirmCallback = (output) => {
console.log('Confirmed:', output);
const dataURL = output.imageBase64;
const file = dataURLToFile(dataURL, output.name);
// Add the file to FilePond
pond.addFiles([file]);
document.body.removeChild(editorContainer); // Remove the editor container
};
function dataURLToFile(dataURL, fileName) {
const arr = dataURL.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const fileExtension = mime.split('/')[1];
const updatedFileName = fileName + '.' + fileExtension;
const bstr = atob(arr[1]);
const n = bstr.length;
const u8arr = new Uint8Array(n);
for (let i = 0; i < n; i++) {
u8arr[i] = bstr.charCodeAt(i);
}
return new File([u8arr], updatedFileName, { type: mime });
}
const cancelCallback = () => {
console.log('Canceled');
document.body.removeChild(editorContainer); // Remove the editor container
};
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.addEventListener('click', () => {
filerobotImageEditor.onClose();
});
const buttonContainer = document.createElement('div');
buttonContainer.appendChild(closeButton);
editorContainer.appendChild(buttonContainer);
filerobotImageEditor.render({
onClose: (closingReason) => {
console.log('Closing reason', closingReason);
filerobotImageEditor.terminate();
},
});
}
function handleImageEditConfirm(output) {
console.log('Image edit confirmed:', output);
// Handle the confirmed output here
}
function handleImageEditCancel() {
console.log('Image edit canceled');
// Handle the canceled edit here
}
function handleImageEditClose() {
console.log('Image editor closed');
// Handle the editor close here
}
});
</script>
Besoin d'aide avec MemberScripts ? Rejoignez notre communauté Slack de plus de 5 500 membres ! 🙌
Les MemberScripts sont une ressource communautaire de Memberstack - si vous avez besoin d'aide pour les faire fonctionner avec votre projet, rejoignez le Slack de Memberstack 2.0 et demandez de l'aide !
Rejoignez notre SlackDécouvrez les entreprises qui ont réussi avec Memberstack
Ne vous contentez pas de nous croire sur parole, consultez les entreprises de toutes tailles qui font confiance à Memberstack pour leur authentification et leurs paiements.
Commencez à construire vos rêves
Memberstack est 100% gratuit jusqu'à ce que vous soyez prêt à vous lancer - alors, qu'attendez-vous ? Créez votre première application et commencez à construire dès aujourd'hui.