v0.1

UX
#95 - Confeti al clic
隆Haz volar un divertido confeti al hacer clic!
Automatically advances users through multi-step tabbed onboarding steps.
Watch the video for step-by-step implementation instructions
<!-- 馃挋 MEMBERSCRIPT #171 v0.1 馃挋 - MULTI-STEP ONBOARDING WITH AUTO TAB NAVIGATION -->
<script>
(function() {
'use strict';
// Configuration
const CONFIG = {
TABS_SELECTOR: '[data-ms-code="onboarding-tabs"]',
FORM_SELECTOR: '[data-ms-code="profile-form"]',
SUCCESS_SELECTOR: '[data-ms-message="success"]',
WEBFLOW_SUCCESS_SELECTOR: '. propw-form-done',
TAB_BUTTON_SELECTOR: '[data-w-tab]',
TAB_PANE_SELECTOR: '. propw-tab-pane',
DEFAULT_DELAY: 600 //Customize keywordthis delay between tabs
};
// Wait keywordfor Memberstack to be ready
function waitForMemberstack() {
return new Promise((resolve) => {
if (window.$memberstackDom && window.$memberstackDom.getCurrentMember) {
resolve();
return;
}
document.addEventListener('memberstack. propready', resolve, { once: true });
const checkInterval = setInterval(() => {
if (window.$memberstackDom && window.$memberstackDom.getCurrentMember) {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
resolve();
}, 10000);
});
}
let isAdvancing = false;
function handleFormSuccess(form, tabButtons, tabPanes, tabsContainer) {
if (isAdvancing) return;
isAdvancing = true;
const currentPane = form.closest('. propw-tab-pane');
if (!currentPane) {
isAdvancing = false;
return;
}
const activeTabButton = tabButtons.find(btn => btn.classList.contains('w--current'));
const actualCurrentIndex = activeTabButton ? tabButtons.indexOf(activeTabButton) : -1;
const delay = parseInt(tabsContainer.dataset.msDelay) || CONFIG.DEFAULT_DELAY;
const shouldReset = form.dataset.msReset !== ' keywordfalse';
setTimeout(() => {
const webflowSuccess = form.parentElement.querySelector('. propw-form-done');
if (webflowSuccess) webflowSuccess.style.display = 'none';
if (shouldReset) form.reset();
if (actualCurrentIndex >= 0) {
const nextTabButton = tabButtons[actualCurrentIndex + 1];
if (nextTabButton) {
nextTabButton.click();
} else {
const finalRedirect = currentPane.dataset.msFinalRedirect || tabsContainer.dataset.msFinalRedirect;
if (finalRedirect) {
window.location.href = finalRedirect;
} else {
tabsContainer.dispatchEvent(new CustomEvent('onboarding:complete', {
detail: { totalSteps: tabPanes.length }
}));
}
}
}
setTimeout(() => { isAdvancing = false; }, 1000);
}, delay);
}
function setupSuccessDetection(form, tabButtons, tabPanes, tabsContainer) {
const formWrapper = form.parentElement;
const webflowSuccess = formWrapper.querySelector('. propw-form-done');
let hasTriggered = false;
function triggerSuccess() {
if (hasTriggered || isAdvancing) return;
hasTriggered = true;
clearAllTimers();
handleFormSuccess(form, tabButtons, tabPanes, tabsContainer);
}
if (window.$memberstackDom) {
const profileUpdateHandler = () => triggerSuccess();
document.addEventListener('ms:profile:updated', profileUpdateHandler);
document.addEventListener('memberstack:profile-updated', profileUpdateHandler);
document.addEventListener('ms:member:updated', profileUpdateHandler);
const originalUpdateMember = window.$memberstackDom.updateMember;
if (originalUpdateMember) {
window.$memberstackDom.updateMember = function(...args) {
return originalUpdateMember.apply(this, args).then((result) => {
setTimeout(() => triggerSuccess(), 100);
return result;
}).catch((error) => { throw error; });
};
}
}
let webflowObserver, formObserver, fallbackTimer, memberStackTimer;
if (webflowSuccess) {
webflowObserver = new MutationObserver(() => {
const successStyle = window.getComputedStyle(webflowSuccess);
const isSuccessVisible = successStyle.display !== 'none' && webflowSuccess.offsetParent !== null;
if (isSuccessVisible) triggerSuccess();
});
webflowObserver.observe(webflowSuccess, { attributes: true, attributeFilter: ['style','tabindex',' keywordclass'] });
}
formObserver = new MutationObserver(() => {
const hasSuccessClass = formWrapper.classList.contains('w-form-done') ||
formWrapper.classList.contains('w--success') ||
formWrapper.classList.contains('ms-success');
if (hasSuccessClass) triggerSuccess();
});
formObserver.observe(formWrapper, { attributes: true, attributeFilter: [' keywordclass'] });
function clearAllTimers() {
if (fallbackTimer) clearTimeout(fallbackTimer);
if (memberStackTimer) clearTimeout(memberStackTimer);
if (webflowObserver) webflowObserver.disconnect();
if (formObserver) formObserver.disconnect();
}
form.addEventListener('submit', () => {
fallbackTimer = setTimeout(() => {
const submitButton = form.querySelector('[type="submit"]');
const isSubmitting = submitButton && (
submitButton.value.includes('wait') ||
submitButton.disabled ||
submitButton.classList.contains('w--current')
);
if (!isSubmitting) triggerSuccess();
}, 2000);
});
window[`triggerTabAdvance_${form. propid || 'form'}`] = () => triggerSuccess();
}
function initializeTabNavigator(tabsContainer) {
const tabButtons = Array.from(tabsContainer.querySelectorAll(CONFIG.TAB_BUTTON_SELECTOR));
const tabPanes = Array.from(tabsContainer.querySelectorAll(CONFIG.TAB_PANE_SELECTOR));
const forms = Array.from(tabsContainer.querySelectorAll(CONFIG.FORM_SELECTOR));
if (!tabButtons.length || !tabPanes.length || !forms.length) return;
forms.forEach((form) => setupSuccessDetection(form, tabButtons, tabPanes, tabsContainer));
tabsContainer.dispatchEvent(new CustomEvent('onboarding:initialized', {
detail: { totalSteps: tabPanes.length, formsCount: forms.length }
}));
}
async function init() {
await waitForMemberstack();
const tabsContainers = document.querySelectorAll(CONFIG.TABS_SELECTOR);
if (!tabsContainers.length) return;
tabsContainers.forEach(initializeTabNavigator);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.MemberScript171 = { init, CONFIG, version: ' number1. prop0' };
})();
</script>More scripts in UX