Display calculation caption on button hover

There is a Window called “caption” in calculation & fiel where you can add some information. That info is written below the main topic or calculation. You allready tried to minimize that by offering a much smaller text. But it would be way better if people like me could use the “caption” field and the option to hide the text behind a small “info” button like that :information_source:. Position of that should be behind the main topic/calculation in the right top corner … like m². By tapping or mouse over effect on the info, the text will appear and the whole calc is way easier to read. Looking forward to see it. Dimitri

Hi there, @user23793 :waving_hand:

Interesting idea, thanks for sharing! If more users support this request, we’ll try to think it over in the future.

As for now, our devs will be happy to create a custom solution for you. I’ve forwarded your request to them and let you know once it’s done :slightly_smiling_face:

Hi there, @user23793 :waving_hand:

This code should be added to the Custom JS field on the Settings tab of your widget’s settings:

const INFO_LIST = ['Eigenkapital', 'Monatliche Kosten ⁉️'];

function listener(selector, callback) {
  const target = document.querySelector(selector);
  if (target) return callback(target);

  const observer = new MutationObserver(() => {
    const node = document.querySelector(selector);
    if (node) {
      observer.disconnect();
      callback(node);
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
}

function normalizedString(str) {
  return str.toLowerCase().trim();
}

function createElement(tag, options = {}) {
  return Object.assign(document.createElement(tag), options);
}

function createInfo(size) {
  const container = createElement('div', {
    className: 'result-info-container',
    style: `
      display: flex;
      align-items: center;
      justify-content: center;
      opacity: 0.7;
      font-size: ${Math.floor((size * 2) / 3)}px;
      width: ${size}px;
      height: ${size}px;
      border: 1px solid rgb(17, 17, 17);
      border-radius: 50%;
    `
  });

  const block = createElement('div', {
    className: 'result-info',
    textContent: 'i'
  });

  container.appendChild(block);
  return container;
}

function mapResultBlocks(container) {
  const resultList = container.querySelectorAll('[class^="result"]');
  const map = new Map();

  for (const block of resultList) {
    const isSecondary = block.className.includes('secondary');
    const labelWrapper = (() => {
      if (isSecondary) {
        return block.querySelector(
          '[class*="result-secondary__SecondaryLabel-sc"]'
        );
      }
      return block;
    })();

    if (!labelWrapper) continue;

    const label = labelWrapper.firstElementChild;
    const caption = labelWrapper.lastElementChild;
    const labelText = label?.textContent;

    if (!labelText) continue;

    const value = (() => {
      if (isSecondary) {
        return block.querySelector('[class*="result-secondary__Value-sc"]');
      }
      return block.querySelector('[class*="result-primary__Value-sc"]');
    })();

    map.set(normalizedString(labelText), {
      label,
      container: block,
      caption: label === caption ? null : caption,
      value,
      isSecondary
    });
  }

  return map;
}

function attachHoverBehavior(value, infoSize) {
  let isHovering = false;
  let isTouched = false;

  const { value: valueNode, caption, container, isSecondary } = value;
  if (!caption || !valueNode) return;

  const info = createInfo(infoSize);
  valueNode.appendChild(info);

  valueNode.style.display = 'flex';
  valueNode.style.alignItems = 'center';

  if (isSecondary) {
    const animatedValue = valueNode.querySelector(
      '[class*="animated-number__Content-sc"]'
    );
    const hiddenValue = valueNode.querySelector(
      '[class*="result-secondary__HiddenValue-sc"]'
    );
    if (animatedValue) animatedValue.style.position = 'relative';
    if (hiddenValue) hiddenValue.style.display = 'none';
    valueNode.style.minWidth =
      parseInt(window.getComputedStyle(valueNode).minWidth || '0') +
      infoSize +
      'px';
    valueNode.style.justifyContent = 'end';
  }

  container.style.position = 'relative';
  container.style.overflow = 'unset';

  caption.style.cssText = `
    margin-top: 0;
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    background-color: #fff;
    z-index: 2;
    opacity: 1;
    padding: 8px 12px;
    border-radius: 4px;
    box-shadow: 0px 2px 8px 0px #00000040;
  `;

  function show() {
    isHovering = true;
    caption.style.display = 'block';
  }

  function hide() {
    isHovering = false;
    setTimeout(() => {
      if (!isHovering) caption.style.display = 'none';
    }, 100);
  }

  valueNode.addEventListener('mouseenter', show);
  valueNode.addEventListener('mouseleave', hide);
  caption.addEventListener('mouseenter', show);
  caption.addEventListener('mouseleave', hide);

  valueNode.addEventListener('touchstart', (e) => {
    e.stopPropagation();
    isTouched = !isTouched;

    if (isTouched) {
      show();
    } else {
      hide();
    }
  });

  document.addEventListener('touchstart', (e) => {
    if (!valueNode.contains(e.target) && !caption.contains(e.target)) {
      isTouched = false;
      hide();
    }
  });
}

listener('[class*="results__Container-sc"]', (container) => {
  const mappedBlocks = mapResultBlocks(container);

  INFO_LIST.forEach((title) => {
    const value = mappedBlocks.get(normalizedString(title));
    if (!value || !value.value) return;

    const fontSize = parseInt(
      window.getComputedStyle(value.value).fontSize || '14'
    );
    const infoSize = Math.floor(fontSize / 2);

    attachHoverBehavior(value, infoSize);
  });
});

In the 1st line, you should add the calculation names where the captions should appear on hover:


Please try it out and let me know if you like the result :wink:

Hi,

it works almost perfectly. Small things to adjust…

  1. the hover Info should be connected to the title, not to the result.
  2. if using the view of „secondary result“, the i hover button is way to small.
  3. works only in the „calculations“ not in „ fields“
    But in general it’s a cool calc!!!

Dimitri Geizenräder

Otto-Brenner-Strasse 86
D-45549 Sprockhövel
phone 49 (0)176 22 36 11 83

info@dimitri-geizenraeder.de

von unterwegs gesendet

Hi there, @user23793 :waving_hand:

Thank you for the feedback!

I’ll discuss with the devs if it’s possible to adjust the first 3 points you’ve mentioned

Hi there, @user23793 :waving_hand:

Here is a final solution with the requested adjustments:

const WIDGET_SELECTOR = '.elfsight-app-8fc0c5c7-3987-491f-a48b-1703f4ba3566';
const INFO_LIST = [
  'Zu versteuerndes Einkommen',
  'a) Zinsen + Tilgung p.a.',
  'Eigenkapital'
];

function listener(selector, callback) {
  const firstTarget = document.querySelector(selector);
  if (firstTarget) {
    return callback(firstTarget);
  }

  const observer = new MutationObserver((_, observer) => {
    const targetNode = document.querySelector(selector);
    if (targetNode) {
      observer.disconnect();
      callback(targetNode);
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
}

function createElement(tag, options) {
  return Object.assign(document.createElement(tag), options);
}

function normalizedString(string) {
  return string.replace(/\s+/g, '').toLowerCase();
}

function installStyles() {
  return document.head.appendChild(
    createElement('style', {
      innerHTML: `
        .result-info-container + .tooltip-wrapper {
          display: none;
        }

        .result-info-container:hover + .tooltip-wrapper,
        .result-info-container:focus + .tooltip-wrapper,
        .result-info-container:active + .tooltip-wrapper {
          display: block;
        }

        .result-info-container + .tooltip-wrapper:hover,
        .result-info-container + .tooltip-wrapper:focus,
        .result-info-container + .tooltip-wrapper:active {
          display: block;
        }
      `
    })
  );
}

function createInfo(size) {
  const container = createElement('button', {
    tabindex: '0',
    className: 'result-info-container',
    style: `
      display: flex;
      align-items: center;
      justify-content: center;
      opacity: 0.7;
      font-size: ${Math.floor((size * 2) / 3)}px;
      width: ${size}px;
      height: ${size}px;
      border: 1px solid rgb(17, 17, 17);
      border-radius: 50%;
      margin-left: 8px;
      background: transparent;
    `
  });

  const block = createElement('div', {
    className: 'result-info',
    textContent: 'i'
  });

  container.appendChild(block);
  return container;
}

function getDescriptionSelector(type) {
  return {
    primary: '[class*="result-primary__PrimaryContainer-sc"] > :nth-child(3)',
    secondary: '[class*="result-secondary__SecondaryLabel-sc"] > :nth-child(2)',
    field: '[class*="FormFieldLayout__Hint-sc"]'
  }[type];
}

function getDescription({ type, node }) {
  const selector = getDescriptionSelector(type);

  if (!selector) {
    return;
  }

  if (type === 'field') {
    return node
      .closest('[class*="FormFieldLayout__Element-sc"]')
      ?.querySelector(selector);
  }

  return node.parentNode.querySelector(selector);
}

const selector = `${WIDGET_SELECTOR} [class*="widget__Layout-sc"]`;
listener(selector, (container) => {
  const formLabels = Array.from(
    container.querySelectorAll(
      '[class*="form__Container-sc"] [class*="Label-sc"]'
    )
  ).map((node) => ({ type: 'field', node }));
  const primaryLabel = Array.from(
    container.querySelectorAll(
      '[class*="result-primary__PrimaryContainer-sc"] > div'
    )
  ).map((node) => ({ type: 'primary', node }));
  const secondaryLabel = Array.from(
    container.querySelectorAll(
      '[class*="result-secondary__SecondaryLabel-sc"] > div'
    )
  ).map((node) => ({ type: 'secondary', node }));

  const targetBlocks = [
    ...formLabels,
    ...secondaryLabel,
    ...primaryLabel
  ].filter(({ node }) =>
    INFO_LIST.some(
      (label) => normalizedString(label) === normalizedString(node.textContent)
    )
  );

  installStyles();

  targetBlocks.forEach(({ type, node }) => {
    const description = getDescription({ type, node });
    console.log(description);
    if (!description) {
      return;
    }

    const size = parseInt(window.getComputedStyle(node).fontSize);
    const infoBlock = createInfo(size);
    node.appendChild(infoBlock);
    node.style.display = 'inline-flex';
    node.style.alignItems = 'center';

    description.style.cssText = `
      opacity: 1;
      padding: 8px 16px;
      margin-top: ${(size + 8) * (type === 'field' ? 2 : 1)}px;
      background-color: #fff;
      box-shadow: 0px 2px 8px 0px #00000040;
      border-radius: 8px;
    `;

    const tooltipWrapper = createElement('div', {
      className: 'tooltip-wrapper',
      style: `
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        z-index: 2;
      `
    });
    tooltipWrapper.appendChild(description);
    node.appendChild(tooltipWrapper);

    const container = node.parentNode.closest('[class*="Container-sc"]');
    if (!container) {
      return;
    }

    container.style.position = 'relative';
    container.style.overflow = 'unset';
  });
});

Please try it out and let me know how it worked :wink:

Hello max,

sorry for late answering…holidays. I tried and so far ist allmost perfect, check yourself … comlicated calc reduced to only 4 sliders … great tool, thank you! Dein U | VILLAGEU

Great, thank you so much for the feedback!

You said it’s almost perfect — is there anything specific in the widget’s design you’d like to tweak?

If so, just let us know the details, and we’ll do our best to make it happen :slightly_smiling_face: