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

2 Likes

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:

2 Likes

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:

1 Like

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

1 Like

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

A post was split to a new topic: Dynamic field caption

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: