Componentes
Plantillas
Atributos
Integraciones
Comprobador del sitio
Código personalizado

MemberScripts

Una solución basada en atributos para añadir funciones a su sitio Webflow.
Simplemente copie algo de código, añada algunos atributos y listo.

Muchas gracias. Hemos recibido su envío.
¡Uy! Algo ha ido mal al enviar el formulario.
¿Necesita ayuda con MemberScripts?

Todos los clientes de Memberstack pueden solicitar asistencia en el Slack 2.0. Tenga en cuenta que no se trata de funciones oficiales y que no se puede garantizar la asistencia.

UX

#111 - Pestañas con rotación automática

La forma más sencilla de hacer que tus pestañas roten automáticamente con un temporizador.


<!-- 💙 MEMBERSCRIPT #111 v0.1 💙 - AUTO-ROTATING TABS -->
<script>
  // Function to rotate tabs
  function initializeTabRotator() {
    // Find all tab containers with the ms-code-rotate-tabs attribute
    const tabContainers = document.querySelectorAll('[ms-code-rotate-tabs]');

    tabContainers.forEach(container => {
      const interval = parseInt(container.getAttribute('ms-code-rotate-tabs'), 10);
      const tabLinks = container.querySelectorAll('.w-tab-link');
      const tabContent = container.closest('.w-tabs').querySelector('.w-tab-content');
      const tabPanes = tabContent.querySelectorAll('.w-tab-pane');
      let currentIndex = Array.from(tabLinks).findIndex(link => link.classList.contains('w--current'));
      let rotationTimer;

      // ANIMATION CONFIGURATION
      // Modify these values to adjust the animation behavior
      const FADE_OUT_DURATION = 300; // Duration for fading out the current tab (in milliseconds)
      const FADE_IN_DURATION = 100;  // Duration for fading in the new tab (in milliseconds)
      const EASING_FUNCTION = 'ease'; // Choose from: 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'
      // or use a cubic-bezier function like 'cubic-bezier(0.1, 0.7, 1.0, 0.1)'
      // Additional easing options (uncomment to use):
      // const EASING_FUNCTION = 'ease-in-quad';
      // const EASING_FUNCTION = 'ease-out-quad';
      // const EASING_FUNCTION = 'ease-in-out-quad';
      // const EASING_FUNCTION = 'ease-in-cubic';
      // const EASING_FUNCTION = 'ease-out-cubic';
      // const EASING_FUNCTION = 'ease-in-out-cubic';
      // const EASING_FUNCTION = 'ease-in-quart';
      // const EASING_FUNCTION = 'ease-out-quart';
      // const EASING_FUNCTION = 'ease-in-out-quart';
      // const EASING_FUNCTION = 'ease-in-quint';
      // const EASING_FUNCTION = 'ease-out-quint';
      // const EASING_FUNCTION = 'ease-in-out-quint';
      // const EASING_FUNCTION = 'ease-in-sine';
      // const EASING_FUNCTION = 'ease-out-sine';
      // const EASING_FUNCTION = 'ease-in-out-sine';
      // const EASING_FUNCTION = 'ease-in-expo';
      // const EASING_FUNCTION = 'ease-out-expo';
      // const EASING_FUNCTION = 'ease-in-out-expo';
      // const EASING_FUNCTION = 'ease-in-circ';
      // const EASING_FUNCTION = 'ease-out-circ';
      // const EASING_FUNCTION = 'ease-in-out-circ';
      // const EASING_FUNCTION = 'ease-in-back';
      // const EASING_FUNCTION = 'ease-out-back';
      // const EASING_FUNCTION = 'ease-in-out-back';
      // END OF ANIMATION CONFIGURATION

      function switchToTab(index) {
        // Fade out current tab
        tabPanes[currentIndex].style.transition = `opacity ${FADE_OUT_DURATION}ms ${EASING_FUNCTION}`;
        tabPanes[currentIndex].style.opacity = '0';

        setTimeout(() => {
          // Remove active classes and update ARIA attributes for current tab and pane
          tabLinks[currentIndex].classList.remove('w--current');
          tabLinks[currentIndex].setAttribute('aria-selected', 'false');
          tabLinks[currentIndex].setAttribute('tabindex', '-1');
          tabPanes[currentIndex].classList.remove('w--tab-active');

          // Update current index
          currentIndex = index;

          // Add active classes and update ARIA attributes for new current tab and pane
          tabLinks[currentIndex].classList.add('w--current');
          tabLinks[currentIndex].setAttribute('aria-selected', 'true');
          tabLinks[currentIndex].setAttribute('tabindex', '0');
          tabPanes[currentIndex].classList.add('w--tab-active');

          // Fade in new tab
          tabPanes[currentIndex].style.transition = `opacity ${FADE_IN_DURATION}ms ${EASING_FUNCTION}`;
          tabPanes[currentIndex].style.opacity = '1';

          // Update the data-current attribute on the parent w-tabs element
          const wTabsElement = container.closest('.w-tabs');
          if (wTabsElement) {
            wTabsElement.setAttribute('data-current', tabLinks[currentIndex].getAttribute('data-w-tab'));
          }
        }, FADE_OUT_DURATION);
      }

      function rotateToNextTab() {
        const nextIndex = (currentIndex + 1) % tabLinks.length;
        switchToTab(nextIndex);
      }

      function startRotation() {
        clearInterval(rotationTimer);
        rotationTimer = setInterval(rotateToNextTab, interval);
      }

      // Add click event listeners to tab links
      tabLinks.forEach((link, index) => {
        link.addEventListener('click', (e) => {
          e.preventDefault();
          switchToTab(index);
          startRotation(); // Restart rotation from this tab
        });
      });

      // Start the initial rotation
      startRotation();
    });
  }

  // Run the function when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', initializeTabRotator);
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
UX

#110 - Tooltips para Webflow

Añada fácilmente tooltips Tippy.js a su sitio Webflow con atributos.


<!-- 💙 MEMBERSCRIPT #110 v0.1 💙 - TOOLTIPS FOR WEBFLOW -->
<script>
  // Function to load Tippy.js, its CSS, and additional theme/animation CSS
  function loadTippy(callback) {
    // Load Tippy.js script
    const script = document.createElement('script');
    script.src = 'https://unpkg.com/@popperjs/core@2';
    script.onload = function() {
      const tippyScript = document.createElement('script');
      tippyScript.src = 'https://unpkg.com/tippy.js@6';
      tippyScript.onload = function() {
        // Load Tippy.js CSS
        const cssFiles = [
          'https://unpkg.com/tippy.js@6/dist/tippy.css',
          'https://unpkg.com/tippy.js@6/themes/light.css',
          'https://unpkg.com/tippy.js@6/themes/light-border.css',
          'https://unpkg.com/tippy.js@6/animations/shift-away.css',
          'https://unpkg.com/tippy.js@6/animations/shift-toward.css',
          'https://unpkg.com/tippy.js@6/animations/scale.css',
          'https://unpkg.com/tippy.js@6/animations/perspective.css'
        ];
        
        let loadedCount = 0;
        cssFiles.forEach(file => {
          const link = document.createElement('link');
          link.href = file;
          link.rel = 'stylesheet';
          link.onload = function() {
            loadedCount++;
            if (loadedCount === cssFiles.length) {
              // Call the callback function when everything is loaded
              callback();
            }
          };
          document.head.appendChild(link);
        });
      };
      document.head.appendChild(tippyScript);
    };
    document.head.appendChild(script);
  }

  // Function to initialize Tippy tooltips
  function initializeTippyTooltips() {
    // Select all elements with any ms-code-tooltip-* attribute
    const elements = document.querySelectorAll('[ms-code-tooltip-top], [ms-code-tooltip-bottom], [ms-code-tooltip-left], [ms-code-tooltip-right], [ms-code-tooltip-content]');

    elements.forEach(element => {
      const tippyOptions = {};

      // Content and Placement
      if (element.hasAttribute('ms-code-tooltip-top')) {
        tippyOptions.content = element.getAttribute('ms-code-tooltip-top');
        tippyOptions.placement = 'top';
      } else if (element.hasAttribute('ms-code-tooltip-bottom')) {
        tippyOptions.content = element.getAttribute('ms-code-tooltip-bottom');
        tippyOptions.placement = 'bottom';
      } else if (element.hasAttribute('ms-code-tooltip-left')) {
        tippyOptions.content = element.getAttribute('ms-code-tooltip-left');
        tippyOptions.placement = 'left';
      } else if (element.hasAttribute('ms-code-tooltip-right')) {
        tippyOptions.content = element.getAttribute('ms-code-tooltip-right');
        tippyOptions.placement = 'right';
      } else if (element.hasAttribute('ms-code-tooltip-content')) {
        tippyOptions.content = element.getAttribute('ms-code-tooltip-content');
      }

      if (element.hasAttribute('ms-code-tooltip-placement')) {
        tippyOptions.placement = element.getAttribute('ms-code-tooltip-placement');
      }

      // Theme
      if (element.hasAttribute('ms-code-tooltip-theme')) {
        tippyOptions.theme = element.getAttribute('ms-code-tooltip-theme');
      }

      // Animation
      if (element.hasAttribute('ms-code-tooltip-animation')) {
        tippyOptions.animation = element.getAttribute('ms-code-tooltip-animation');
      }

      // Max Width
      if (element.hasAttribute('ms-code-tooltip-maxwidth')) {
        tippyOptions.maxWidth = parseInt(element.getAttribute('ms-code-tooltip-maxwidth'));
      }

      // Delay
      if (element.hasAttribute('ms-code-tooltip-delay')) {
        tippyOptions.delay = JSON.parse(element.getAttribute('ms-code-tooltip-delay'));
      }

      // Duration
      if (element.hasAttribute('ms-code-tooltip-duration')) {
        tippyOptions.duration = JSON.parse(element.getAttribute('ms-code-tooltip-duration'));
      }

      // Interactive
      if (element.hasAttribute('ms-code-tooltip-interactive')) {
        tippyOptions.interactive = element.getAttribute('ms-code-tooltip-interactive') === 'true';
      }

      // Arrow
      if (element.hasAttribute('ms-code-tooltip-arrow')) {
        tippyOptions.arrow = element.getAttribute('ms-code-tooltip-arrow') === 'true';
      }

      // Trigger
      if (element.hasAttribute('ms-code-tooltip-trigger')) {
        tippyOptions.trigger = element.getAttribute('ms-code-tooltip-trigger');
      }

      // Hide On Click
      if (element.hasAttribute('ms-code-tooltip-hideOnClick')) {
        tippyOptions.hideOnClick = element.getAttribute('ms-code-tooltip-hideOnClick') === 'true';
      }

      // Follow Cursor
      if (element.hasAttribute('ms-code-tooltip-followCursor')) {
        tippyOptions.followCursor = element.getAttribute('ms-code-tooltip-followCursor');
      }

      // Offset
      if (element.hasAttribute('ms-code-tooltip-offset')) {
        tippyOptions.offset = JSON.parse(element.getAttribute('ms-code-tooltip-offset'));
      }

      // Z-Index
      if (element.hasAttribute('ms-code-tooltip-zIndex')) {
        tippyOptions.zIndex = parseInt(element.getAttribute('ms-code-tooltip-zIndex'));
      }

      // Allow HTML
      if (element.hasAttribute('ms-code-tooltip-allowHTML')) {
        tippyOptions.allowHTML = element.getAttribute('ms-code-tooltip-allowHTML') === 'true';
      }

      // Touch
      if (element.hasAttribute('ms-code-tooltip-touch')) {
        const touchValue = element.getAttribute('ms-code-tooltip-touch');
        tippyOptions.touch = touchValue === 'true' || touchValue === 'false' ? (touchValue === 'true') : JSON.parse(touchValue);
      }

      // Initialize Tippy instance
      tippy(element, tippyOptions);
    });
  }

  // Wait for the DOM to be fully loaded, then load Tippy and initialize tooltips
  document.addEventListener('DOMContentLoaded', function() {
    loadTippy(initializeTippyTooltips);
  });
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
Campos personalizados
UX

#109 - Selección múltiple personalizada

Multiselección personalizada con búsqueda, selección por teclado y mucho más.


<!-- 💙 MEMBERSCRIPT #109 v0.1 💙 - CUSTOM MULTI SELECT -->
<script>
  $(document).ready(function() {
    $('[ms-code-select-wrapper]').each(function() {
      const $wrapper = $(this);
      const isMulti = $wrapper.attr('ms-code-select-wrapper') === 'multi';
      const $input = $wrapper.find('[ms-code-select="input"]');
      const $list = $wrapper.find('[ms-code-select="list"]');
      const $selectedWrapper = $wrapper.find('[ms-code-select="selected-wrapper"]');
      const $emptyState = $wrapper.find('[ms-code-select="empty-state"]');
      const options = $input.attr('ms-code-select-options').split(',').map(opt => opt.trim());

      let selectedOptions = [];
      let highlightedIndex = -1;

      const $templateSelectedTag = $selectedWrapper.find('[ms-code-select="tag"]');
      const templateSelectedTagHTML = $templateSelectedTag.prop('outerHTML');
      $templateSelectedTag.remove();

      const $templateNewTag = $list.find('[ms-code-select="tag-name-new"]');
      const templateNewTagHTML = $templateNewTag.prop('outerHTML');
      $templateNewTag.remove();

      function createSelectedTag(value) {
        const $newTag = $(templateSelectedTagHTML);
        $newTag.find('[ms-code-select="tag-name-selected"]').text(value);
        $newTag.find('[ms-code-select="tag-close"]').on('click', function(e) {
          e.stopPropagation();
          removeTag(value);
        });
        return $newTag;
      }

      function addTag(value) {
        if (!selectedOptions.includes(value) && options.includes(value)) {
          selectedOptions.push(value);
          $selectedWrapper.append(createSelectedTag(value));
          updateInput();
          filterOptions();
        }
      }

      function removeTag(value) {
        selectedOptions = selectedOptions.filter(option => option !== value);
        $selectedWrapper.find(`[ms-code-select="tag-name-selected"]:contains("${value}")`).closest('[ms-code-select="tag"]').remove();
        updateInput();
        if (isMulti && selectedOptions.length > 0) {
          $input.val($input.val() + ', ');
        }
        filterOptions();
      }

      function updateInput() {
        $input.val(selectedOptions.join(', '));
      }

      function toggleList(show) {
        $list.toggle(show);
      }

      function createOptionElement(value) {
        const $option = $(templateNewTagHTML);
        $option.text(value);
        $option.on('click', function() {
          selectOption(value);
        });
        return $option;
      }

      function selectOption(value) {
        if (isMulti) {
          addTag(value);
          $input.val(selectedOptions.join(', ') + (selectedOptions.length > 0 ? ', ' : ''));
          $input.focus();
        } else {
          selectedOptions = [value];
          $selectedWrapper.empty().append(createSelectedTag(value));
          updateInput();
          toggleList(false);
        }
        filterOptions();
      }

      function filterOptions() {
        const inputValue = $input.val();
        const searchTerm = isMulti ? inputValue.split(',').pop().trim() : inputValue.trim();
        let visibleOptionsCount = 0;

        $list.find('[ms-code-select="tag-name-new"]').each(function() {
          const $option = $(this);
          const optionText = $option.text().toLowerCase();
          const matches = optionText.includes(searchTerm.toLowerCase());
          const isSelected = selectedOptions.includes($option.text());
          $option.toggle(matches && !isSelected);
          if (matches && !isSelected) visibleOptionsCount++;
        });

        $emptyState.toggle(visibleOptionsCount === 0 && searchTerm !== '');
        highlightedIndex = -1;
        updateHighlight();
      }

      function cleanInput() {
        const inputValues = $input.val().split(',').map(v => v.trim()).filter(v => v);
        const validValues = inputValues.filter(v => options.includes(v));
        selectedOptions = validValues;
        $selectedWrapper.empty();
        selectedOptions.forEach(value => $selectedWrapper.append(createSelectedTag(value)));
        updateInput();
        filterOptions();
      }

      function handleInputChange() {
        const inputValue = $input.val();
        const inputValues = inputValue.split(',').map(v => v.trim());
        const lastValue = inputValues[inputValues.length - 1];

        if (inputValue.endsWith(',') || inputValue.endsWith(', ')) {
          inputValues.pop();
          const newValidValues = inputValues.filter(v => options.includes(v) && !selectedOptions.includes(v));
          newValidValues.forEach(addTag);
          $input.val(selectedOptions.join(', ') + (selectedOptions.length > 0 ? ', ' : ''));
        } else if (options.includes(lastValue) && !selectedOptions.includes(lastValue)) {
          addTag(lastValue);
          $input.val(selectedOptions.join(', ') + ', ');
        }

        filterOptions();
      }

      function initializeWithValue() {
        const initialValue = $input.val();
        if (initialValue) {
          const initialValues = initialValue.split(',').map(v => v.trim());
          initialValues.forEach(value => {
            if (options.includes(value)) {
              addTag(value);
            }
          });
          updateInput();
          filterOptions();
        }
      }

      function updateHighlight() {
        $list.find('[ms-code-select="tag-name-new"]').removeClass('highlighted').css('background-color', '');
        if (highlightedIndex >= 0) {
          $list.find('[ms-code-select="tag-name-new"]:visible').eq(highlightedIndex)
            .addClass('highlighted')
            .css('background-color', '#e0e0e0');
        }
      }

      function handleKeyDown(e) {
        const visibleOptions = $list.find('[ms-code-select="tag-name-new"]:visible');
        const optionCount = visibleOptions.length;

        switch (e.key) {
          case 'ArrowDown':
            e.preventDefault();
            highlightedIndex = (highlightedIndex + 1) % optionCount;
            updateHighlight();
            break;
          case 'ArrowUp':
            e.preventDefault();
            highlightedIndex = (highlightedIndex - 1 + optionCount) % optionCount;
            updateHighlight();
            break;
          case 'Enter':
            e.preventDefault();
            if (highlightedIndex >= 0) {
              const selectedValue = visibleOptions.eq(highlightedIndex).text();
              selectOption(selectedValue);
            }
            break;
        }
      }

      $.each(options, function(i, option) {
        $list.append(createOptionElement(option));
      });

      $input.on('focus', function() {
        toggleList(true);
        if (isMulti) {
          const currentVal = $input.val().trim();
          if (currentVal !== '' && !currentVal.endsWith(',')) {
            $input.val(currentVal + ', ');
          }
          this.selectionStart = this.selectionEnd = this.value.length;
        }
        filterOptions();
      });

      $input.on('click', function(e) {
        e.preventDefault();
        this.selectionStart = this.selectionEnd = this.value.length;
      });

      $input.on('blur', function() {
        setTimeout(function() {
          if (!$list.is(':hover')) {
            toggleList(false);
            cleanInput();
          }
        }, 100);
      });

      $input.on('input', handleInputChange);
      $input.on('keydown', handleKeyDown);

      $list.on('mouseenter', '[ms-code-select="tag-name-new"]', function() {
        $(this).css('background-color', '#e0e0e0');
      });

      $list.on('mouseleave', '[ms-code-select="tag-name-new"]', function() {
        if (!$(this).hasClass('highlighted')) {
          $(this).css('background-color', '');
        }
      });

      initializeWithValue();
      toggleList(false);
    });
  });
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
UX

#108 - Botones de envío de formularios personalizados

Cree cualquier elemento en Webflow y utilícelo para enviar cualquier tipo de formulario.


<!-- 💙 MEMBERSCRIPT #108 v0.1 💙 CUSTOM FORM SUBMIT BUTTON -->
<script>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', function() {
    // Find all elements with the ms-code-submit-new attribute
    const newSubmitButtons = document.querySelectorAll('[ms-code-submit-new]');

    // Add click event listeners to each new submit button
    newSubmitButtons.forEach(button => {
        button.addEventListener('click', function(e) {
            e.preventDefault(); // Prevent default action if it's a link

            // Get the value of the ms-code-submit-new attribute
            const submitId = this.getAttribute('ms-code-submit-new');

            // Find the corresponding old submit button
            const oldSubmitButton = document.querySelector(`[ms-code-submit-old="${submitId}"]`);

            // If found, trigger a click on the old submit button
            if (oldSubmitButton) {
                oldSubmitButton.click();
            } else {
                console.error(`No matching old submit button found for ID: ${submitId}`);
            }
        });
    });
});
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
UX

#107 - Seleccionar plan con radios

Añadir un radio selector de planes a los formularios de inscripción y actualización de planes.


<!-- 💙 MEMBERSCRIPT #107 v0.1 💙 SELECT PLAN WITH RADIO BUTTONS -->
<script>
  (function() {
    const PRICE_ATTRIBUTES = [
      'data-ms-plan:add',
      'data-ms-plan:update',
      'data-ms-price:add',
      'data-ms-price:update'
    ];

    function findElementWithAttribute(form) {
      // First, check if the form itself has one of the attributes
      for (let attr of PRICE_ATTRIBUTES) {
        if (form.hasAttribute(attr)) {
          return { element: form, attribute: attr };
        }
      }

      // If not found on form, search child elements
      for (let attr of PRICE_ATTRIBUTES) {
        let element = Array.from(form.querySelectorAll('*')).find(el => el.hasAttribute(attr));
        if (element) {
          return { element, attribute: attr };
        }
      }
      return null;
    }

    function updateAttribute(radio) {
      const form = radio.closest('form');
      if (!form) return;

      const result = findElementWithAttribute(form);
      if (result) {
        result.element.setAttribute(result.attribute, radio.value);
      }
    }

    function handleRadioChange(e) {
      updateAttribute(e.target);
    }

    function initializeRadioButtons() {
      const forms = document.querySelectorAll('[ms-code-radio-plan="form"]');
      forms.forEach(form => {
        const radios = form.querySelectorAll('input[type="radio"]');
        radios.forEach(radio => {
          radio.addEventListener('change', handleRadioChange);
          if (radio.checked) {
            updateAttribute(radio);
          }
        });
      });
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', initializeRadioButtons);
    } else {
      initializeRadioButtons();
    }
  })();

  // Only keep this section if you have an update plan form on the page
  (function() {
    function updateRadioButtonState(radio) {
      radio.checked = true;
      radio.dispatchEvent(new Event('change'));

      // Update custom radio button UI if present
      const customRadio = radio.parentElement.querySelector('.w-radio-input');
      if (customRadio) {
        customRadio.classList.add('w--redirected-checked');
      }
    }

    function checkAndSelectPlan() {
      const msMemData = localStorage.getItem('_ms-mem');
      if (!msMemData) return;

      try {
        const memberData = JSON.parse(msMemData);
        const activePlanConnections = memberData.planConnections?.filter(conn => conn.active) || [];

        if (activePlanConnections.length === 0) return;

        const forms = document.querySelectorAll('[ms-code-radio-plan="form"]');
        forms.forEach(form => {
          const radios = form.querySelectorAll('input[type="radio"]');
          radios.forEach(radio => {
            const matchingPlan = activePlanConnections.find(conn => conn.payment.priceId === radio.value);
            if (matchingPlan) {
              updateRadioButtonState(radio);
            }
          });
        });
      } catch (error) {
        console.error('Error processing _ms-mem data:', error);
      }
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', checkAndSelectPlan);
    } else {
      checkAndSelectPlan();
    }
  })();
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
JSON

#106 - Gustar y guardar artículos CMS

Permita a sus miembros guardar elementos CMS en su perfil.


<!-- 💙 MEMBERSCRIPT #106 v0.2 💙 SAVING & UNSAVING CMS ITEMS -->
<style>
  [ms-code-save], [ms-code-unsave] {
    display: none;
  }
  [ms-code-save-item] {
    display: none;
  }
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
  const memberstack = window.$memberstackDom;
  let isLoggedIn = false;
  let savedItems = [];

  async function checkMemberLogin() {
    try {
      const member = await memberstack.getCurrentMember();
      return !!member;
    } catch (error) {
      return false;
    }
  }

  function getSavedItems(memberData) {
    return memberData.savedItems || [];
  }

  function updateButtonVisibility() {
    const saveButtons = document.querySelectorAll('[ms-code-save]');
    const unsaveButtons = document.querySelectorAll('[ms-code-unsave]');

    saveButtons.forEach(button => {
      const itemId = button.getAttribute('ms-code-save');
      button.style.display = !savedItems.includes(itemId) ? 'block' : 'none';
    });

    unsaveButtons.forEach(button => {
      const itemId = button.getAttribute('ms-code-unsave');
      button.style.display = savedItems.includes(itemId) ? 'block' : 'none';
    });
  }

  function updateItemVisibility() {
    const saveLists = document.querySelectorAll('[ms-code-save-list]');
    saveLists.forEach(list => {
      const filter = list.getAttribute('ms-code-save-list');
      const items = list.querySelectorAll('[ms-code-save-item]');
      items.forEach(item => {
        const saveButton = item.querySelector('[ms-code-save]');
        if (!saveButton) {
          item.style.display = 'block';
          return;
        }
        const itemId = saveButton.getAttribute('ms-code-save');
        
        if (!isLoggedIn || filter === 'all') {
          item.style.display = 'block';
        } else if (filter === 'saved' & savedItems.includes(itemId)) {
          item.style.display = 'block';
        } else if (filter === 'unsaved' & !savedItems.includes(itemId)) {
          item.style.display = 'block';
        } else {
          item.style.display = 'none';
        }
      });
    });
  }

  async function handleButtonClick(event) {
    if (!isLoggedIn) return;

    const button = event.currentTarget;
    const action = button.getAttribute('ms-code-save') ? 'save' : 'unsave';
    const itemId = button.getAttribute(action === 'save' ? 'ms-code-save' : 'ms-code-unsave');
    
    if (action === 'save' && !savedItems.includes(itemId)) {
      savedItems.push(itemId);
    } else if (action === 'unsave') {
      savedItems = savedItems.filter(id => id !== itemId);
    }

    try {
      await memberstack.updateMemberJSON({ json: { savedItems } });
    } catch (error) {
      // Silently handle the error
    }

    updateButtonVisibility();
    updateItemVisibility();
  }

  function addClickListeners() {
    const saveButtons = document.querySelectorAll('[ms-code-save]');
    const unsaveButtons = document.querySelectorAll('[ms-code-unsave]');
    saveButtons.forEach(button => button.addEventListener('click', handleButtonClick));
    unsaveButtons.forEach(button => button.addEventListener('click', handleButtonClick));
  }

  async function initializeScript() {
    isLoggedIn = await checkMemberLogin();

    if (isLoggedIn) {
      try {
        const result = await memberstack.getMemberJSON();
        const memberData = result.data || {};
        savedItems = getSavedItems(memberData);
      } catch (error) {
        // Silently handle the error
      }
    }

    updateButtonVisibility();
    updateItemVisibility();
    addClickListeners();

    // Set up a MutationObserver to watch for changes in the DOM
    const observer = new MutationObserver((mutations) => {
      let shouldUpdate = false;
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
          shouldUpdate = true;
        }
      });
      if (shouldUpdate) {
        updateButtonVisibility();
        updateItemVisibility();
        addClickListeners();
      }
    });

    // Start observing the document with the configured parameters
    observer.observe(document.body, { childList: true, subtree: true });
  }

  initializeScript();
});
</script>
v0.2

<!-- 💙 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>
Ver Memberscript
UX
Flujos personalizados

#105 - Pago después de iniciar sesión

Iniciar automáticamente el proceso de pago si un usuario selecciona un precio antes de iniciar sesión.


<!-- 💙 MEMBERSCRIPT #105 v0.1 💙 CHECKOUT AFTER LOGIN -->
<script>
  /* Checks if the current URL matches the configured redirect URL, or if no specific URL is required */
  function isCorrectPage() {
    return redirectOnLoginURL === '' || window.location.pathname === redirectOnLoginURL;
  }

  /* Checks if Memberstack is fully loaded before running any Memberstack-specific code.*/
  function memberstackReady(callback) {
    function checkAndExecute() {
      if (window.$memberstackDom) {
        callback(); // Memberstack is ready, run the callback function.
      } else {
        setTimeout(checkAndExecute, 100); // Wait for 100ms and check again.
      }
    }

    checkAndExecute(); // Start checking if Memberstack is ready.
  }

  /* Initiates the Stripe checkout process with a specified price ID.*/
  async function initiateCheckout(priceId) {
    try {
      // Set a flag in session storage to indicate that the checkout page was accessed.
      sessionStorage.setItem('ms_checkout_viewed', 'true');
      await window.$memberstackDom.purchasePlansWithCheckout({
        priceId, // The price ID for the product being purchased.
        returnUrl: window.location.href, // Redirect the user back here after completing the checkout.
      });
    } catch (error) {
      console.error('Failed to initiate payment:', error); // Provide error details in the console.
    }
  }

  /* Main execution flow that starts once Memberstack is confirmed to be ready */
  memberstackReady(() => {
    window.$memberstackDom.getCurrentMember().then(({ data: member }) => {
      if (member && sessionStorage.getItem('ms_price') && !sessionStorage.getItem('ms_checkout_viewed')) {
        initiateCheckout(sessionStorage.getItem('ms_price')); // Start the checkout process if conditions are met.
      }
    }).catch(error => {
      console.error('Failed to retrieve user data:', error); // Log an error if fetching member data fails.
    });
  });
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
UX

#104 - Indicador en línea

Muestre a los visitantes de su sitio web su estado en línea en función de las zonas horarias.


<!-- 💙 MEMBERSCRIPT #104 v0.1 💙 ONLINE INDICATOR -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    const businessHours = {
        start: 9, // Business hours start at 9 AM
        end: 17, // Business hours end at 5 PM
        days: [1, 2, 3, 4, 5] // Monday to Friday
    };
    const colors = {
        businessHours: '#34b426',
        outsideBusinessHours: '#F25022'
    };

    const wrappers = document.querySelectorAll('[ms-code-online-wrapper]');
    wrappers.forEach(wrapper => {
        const timeZone = wrapper.getAttribute('ms-code-online-wrapper');
        const dot = wrapper.querySelector('[ms-code-online="dot"]');
        const timeSpan = wrapper.querySelector('[ms-code-online="time"]');

        const now = new Date();
        const formatter = new Intl.DateTimeFormat('en-US', {
            hour: 'numeric',
            minute: '2-digit',
            timeZone: timeZone
        });
        const formattedTime = formatter.format(now);

        if (timeSpan) timeSpan.textContent = formattedTime;

        const currentDay = now.getDay();
        const currentHour = new Date().toLocaleTimeString('en-US', {
            hour: '2-digit',
            hour12: false,
            timeZone: timeZone
        });

        const isBusinessDay = businessHours.days.includes(currentDay);
        const isBusinessHour = currentHour >= businessHours.start && currentHour < businessHours.end;

        if (dot) {
            dot.style.backgroundColor = (isBusinessDay && isBusinessHour) ? colors.businessHours : colors.outsideBusinessHours;
        }
    });
});
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
Integración

#103 - Formulario de reserva personalizado

Añade un formulario de reserva personalizado a tu sitio web que cree un evento en el calendario de Google.


<!-- 💙 MEMBERSCRIPT #103 v0.1 💙 CUSTOM BOOKING FORM -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.33/moment-timezone-with-data.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
  function getNextBusinessDays() {
    let businessDays = [];
    let currentDate = moment();
    currentDate.add(1, 'days');
    
    while (businessDays.length < 14) {
      if (currentDate.day() !== 0 && currentDate.day() !== 6) {
        let formattedDay = currentDate.format('dddd, MMMM D');
        let rawDay = currentDate.format('YYYY-MM-DD');
        businessDays.push({formattedDay, rawDay});
      }
      currentDate.add(1, 'days');
    }
    return businessDays;
  }

  function generateTimeSlots() {
    let slots = [];
    let startHour = 9;
    let endHour = 16.5;
    let currentTime = moment().startOf('day').add(startHour, 'hours');
    
    while (currentTime.hour() + (currentTime.minute() / 60) <= endHour) {
      let formattedTime = currentTime.format('h:mm A');
      let timeValue = currentTime.format('HH:mm');
      slots.push({formattedTime, timeValue});
      currentTime.add(30, 'minutes');
    }
    return slots;
  }

  function updateTimestamp(day, time, timezone) {
    let timestampInput = document.getElementById('timestamp');
    if (!timestampInput) {
        timestampInput = document.createElement('input');
        timestampInput.type = 'hidden';
        timestampInput.id = 'timestamp';
        timestampInput.name = 'timestamp';
        document.querySelector('form').appendChild(timestampInput);
    }

    let datetime = moment.tz(`${day} ${time}`, "YYYY-MM-DD HH:mm", timezone);
    timestampInput.value = datetime.valueOf();
  }

  function populateFields() {
    const days = getNextBusinessDays();
    const times = generateTimeSlots();
    const daySelect = document.querySelector('[ms-code-booking="day"]');
    const timeSelect = document.querySelector('[ms-code-booking="time"]');
    const form = daySelect.closest('form');
    const timezone = form.getAttribute('ms-code-booking-timezone') || moment.tz.guess();
    
    days.forEach(({formattedDay, rawDay}) => {
      let option = new Option(formattedDay, rawDay);
      daySelect.appendChild(option);
    });
    
    times.forEach(({formattedTime, timeValue}) => {
      let option = new Option(formattedTime, timeValue);
      timeSelect.appendChild(option);
    });

    function handleSelectChange() {
      if (daySelect.value && timeSelect.value) {
        updateTimestamp(daySelect.value, timeSelect.value, timezone);
      }
    }

    daySelect.addEventListener('change', handleSelectChange);
    timeSelect.addEventListener('change', handleSelectChange);
  }

  populateFields();
});
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
Campos personalizados
UX

#102 - Redimensionar automáticamente la altura del área de texto

Aumenta o disminuye la altura de un textarea en función de su contenido.


<!-- 💙 MEMBERSCRIPT #102 v0.1 💙 RESIZE TEXTAREA VERTICALLY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    const elements = document.querySelectorAll('[data-ms-post="content"], [ms-code-resize-input="height"]');

    elements.forEach(element => {
        if (element.tagName.toLowerCase() === 'textarea') {
            element.addEventListener('input', function() {
                autoResize(this);
            }, false);
        }
    });

    function autoResize(element) {
        const maxHeight = parseInt(getComputedStyle(element).maxHeight, 10);
        element.style.height = 'auto';
        element.style.overflow = 'hidden'; // Prevents scrollbar appearance during height adjustment

        if (element.scrollHeight > maxHeight) {
            element.style.height = `${maxHeight}px`;
            element.style.overflow = 'auto'; // Adds scrollbar when content exceeds max height
        } else {
            element.style.height = `${element.scrollHeight}px`;
        }
    }
});
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
Campos personalizados
UX

#101 - Redimensionar automáticamente la anchura de entrada

Aumentar o disminuir el ancho de una entrada en función del contenido.


<!-- 💙 MEMBERSCRIPT #101 v0.1 💙 RESIZE INPUT HORIZONTALLY -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const elements = document.querySelectorAll('[ms-code-resize-input="width"]');

    // Store the initial widths
    const initialWidths = new Map();
    elements.forEach(element => {
        initialWidths.set(element, element.offsetWidth);
    });

    elements.forEach(element => {
        element.addEventListener('input', function() {
            autoResizeWidth(this);
        });
    });

    function autoResizeWidth(element) {
        // Find the nearest hidden measure element
        const measurer = element.nextElementSibling.getAttribute('ms-code-resize-input') === 'hidden-measure' 
                         ? element.nextElementSibling 
                         : null;
        if (!measurer) return; // Exit if no measurer is found

        measurer.textContent = element.value;

        const maxWidth = parseInt(getComputedStyle(element).maxWidth, 10);
        const minWidth = initialWidths.get(element);
        const contentWidth = measurer.offsetWidth;

        if (contentWidth > minWidth && contentWidth < maxWidth) {
            element.style.width = `${contentWidth}px`;
        } else if (contentWidth >= maxWidth) {
            element.style.width = `${maxWidth}px`;
        } else {
            element.style.width = `${minWidth}px`;
        }
    }
  });
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
Campos personalizados
Integración

#100 - Autocompresión de imágenes

Comprime las subidas de imágenes, incluidas las imágenes de perfil.


<!-- 💙 MEMBERSCRIPT #100 v0.1 💙 AUTO-COMPRESSED IMAGE UPLOADS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/compressorjs/1.2.1/compressor.min.js" integrity="sha512-MgYeYFj8R3S6rvZHiJ1xA9cM/VDGcT4eRRFQwGA7qDP7NHbnWKNmAm28z0LVjOuUqjD0T9JxpDMdVqsZOSHaSA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
  const compressibleInputs = document.querySelectorAll('input[type="file"][ms-code-file_compress]');

  compressibleInputs.forEach(fileInput => {
    let isCompressing = false;

    fileInput.addEventListener('change', function (event) {
      if (isCompressing) {
        isCompressing = false;
        return;
      }

      if (fileInput.files.length === 0) {
        return;
      }

      const originalFile = fileInput.files[0];
      const compressionLevel = parseFloat(fileInput.getAttribute('ms-code-file_compress'));

      new Compressor(originalFile, {
        quality: compressionLevel,
        maxWidth: 2000,
        maxHeight: 2000,
        success(compressedResult) {
          const compressedFile = new File([compressedResult], originalFile.name, {
            type: compressedResult.type,
            lastModified: Date.now(),
          });

          const dataTransfer = new DataTransfer();
          dataTransfer.items.add(compressedFile);
          fileInput.files = dataTransfer.files;

          isCompressing = true;
          fileInput.dispatchEvent(new Event('change', { bubbles: true }));
        },
        error(err) {
          console.error('Compression Error: ', err.message);
        },
      });

      event.stopPropagation();
    }, true);
  });
});
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
Campos personalizados

#99 - Entradas de archivos personalizadas

Convierta cualquier cosa en un archivo de entrada


<!-- 💙 MEMBERSCRIPT #99 v0.1 💙 CUSTOM FILE UPLOAD INPUT -->
<script>
document.addEventListener('DOMContentLoaded', function () {
  const uploadButtons = document.querySelectorAll('[ms-code-file_uploader]');

  uploadButtons.forEach(button => {
    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.style.display = 'none';
    fileInput.name = button.getAttribute('ms-code-file_uploader');
    fileInput.accept = button.getAttribute('ms-code-file_types');

    document.body.appendChild(fileInput);

    button.addEventListener('click', function (e) {
      e.preventDefault();
      fileInput.click();
    });

    fileInput.addEventListener('change', function () {
      const fileName = fileInput.files[0].name;
      button.querySelector('div').textContent = fileName;
    });
  });
});
</script>
v0.1

<!-- 💙 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>
Ver Memberscript
Visibilidad condicional

#98 - Age Gating

Haga que los usuarios confirmen su edad antes de continuar.


<!-- 💙 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>
v0.1

<!-- 💙 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>
Ver Memberscript
Integración

#97 - Subir archivos a S3 Bucket

Permitir subidas a un bucket S3 desde un formulario 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>
v0.1

<!-- 💙 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>
Ver Memberscript
Campos personalizados

#96 - Guardar un recuento

Cree y actualice un recuento que se guarde en un campo personalizado.


<!-- 💙 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>
v0.1

<!-- 💙 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>
Ver Memberscript
UX

#95 - Confeti al clic

¡Haz volar un divertido confeti al hacer clic!


<!-- 💙 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>
v0.1

<!-- 💙 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>
Ver Memberscript
UX

#94 - Establecer atributo href

Establecer dinámicamente un enlace utilizando el Webflow CMS (o cualquier otra cosa)


<!-- 💙 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>
v0.1

<!-- 💙 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>
Ver Memberscript
Campos personalizados
UX

#93 - Forzar URLs válidas en los formularios

Convierte automáticamente cualquier entrada en una URL válida.


<!-- 💙 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>
v0.1

<!-- 💙 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>
Ver Memberscript
UX

#92 - Cambiar de página al hacer clic

Cambia la URL de la página actual al hacer clic en cualquier elemento.


<!-- 💙 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>
v0.1

<!-- 💙 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>
Ver Memberscript
No hemos podido encontrar ningún script para esa búsqueda... por favor, inténtalo de nuevo.
Slack

¿Necesitas ayuda con MemberScripts? ¡Únete a nuestra comunidad Slack de más de 5.500 miembros! 🙌

Los MemberScripts son un recurso comunitario de Memberstack - si necesitas ayuda para que funcionen con tu proyecto, ¡únete al Slack de Memberstack 2.0 y pide ayuda!

Únete a nuestro Slack
Escaparate

Explore empresas reales que han tenido éxito con Memberstack

No se fíe sólo de nuestra palabra: eche un vistazo a las empresas de todos los tamaños que confían en Memberstack para su autenticación y sus pagos.

Ver todas las historias de éxito
Incluso Webflow utiliza Memberstack.
Empezar a construir

Empieza a construir tus sueños

Memberstack es 100% gratis hasta que estés listo para lanzarla - así que, ¿a qué estás esperando? Crea tu primera aplicación y empieza a construir hoy mismo.