File size: 3,051 Bytes
d0d6e7b
 
92cb20a
d0d6e7b
 
 
 
 
92cb20a
d0d6e7b
 
 
2c2de0b
 
 
 
 
 
 
 
 
 
 
 
d0d6e7b
 
 
 
 
2c2de0b
 
d0d6e7b
 
 
 
 
 
 
 
 
2c2de0b
d0d6e7b
9f55f84
 
92cb20a
 
 
 
9f55f84
92cb20a
9f55f84
 
 
 
 
 
 
 
 
92cb20a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Delegate to the component root so navigation still works after HTML updates.
function activate(event) {
  const target = event.target.closest('[data-path]');
  if (!target || !element.contains(target)) return;
  if (event.type === 'keydown' && !['Enter', ' '].includes(event.key)) return;
  // Native buttons already synthesize a click on keyboard activation.
  if (event.type === 'keydown' && target.tagName.toLowerCase() === 'button') return;
  event.preventDefault();
  trigger('click', {route: target.dataset.path});
}
element.addEventListener('click', activate);
element.addEventListener('keydown', activate);
// A group and the flux arriving at it share a data-node id, so hovering either
// one lights both. The ribbons are painted beneath every group, so no CSS
// sibling rule can reach them from the group being hovered.
let lit = '';
function light(uid) {
  if (uid === lit) return;
  lit = uid;
  element.querySelectorAll('.is-lit').forEach((el) => el.classList.remove('is-lit'));
  if (uid) element.querySelectorAll(`[data-node="${CSS.escape(uid)}"]`).forEach((el) => el.classList.add('is-lit'));
}
element.addEventListener('focusin', (event) => light(event.target.closest('[data-node]')?.dataset.node || ''));
element.addEventListener('focusout', () => light(''));
element.addEventListener('pointermove', (event) => {
  const node = event.target.closest('[data-detail]');
  const tooltip = element.querySelector('.atlas-tooltip');
  const atlas = element.querySelector('.atlas');
  if (!tooltip || !atlas) return;
  if (!node || event.pointerType === 'touch') { tooltip.hidden = true; light(''); return; }
  light(node.dataset.node || '');
  tooltip.textContent = node.dataset.detail;
  tooltip.hidden = false;
  const bounds = atlas.getBoundingClientRect();
  tooltip.style.left = Math.max(8, Math.min(event.clientX - bounds.left + 15, bounds.width - 305)) + 'px';
  tooltip.style.top = Math.max(8, Math.min(event.clientY - bounds.top + 18, bounds.height - tooltip.offsetHeight - 8)) + 'px';
});
element.addEventListener('pointerleave', () => {
  const tooltip = element.querySelector('.atlas-tooltip');
  if (tooltip) tooltip.hidden = true;
  light('');
});
// Expanding adds a level below. The page scrolls, not the widget, so only nudge
// it when the new bottom of the tree has fallen past the viewport.
let shownRoute = element.querySelector('.life-tree')?.dataset.route;
new MutationObserver(() => {
  const route = element.querySelector('.life-tree')?.dataset.route;
  if (!route || route === shownRoute) return;
  const grew = route.length > shownRoute.length;
  shownRoute = route;
  if (!grew) return;
  requestAnimationFrame(() => {
    const tree = element.querySelector('.life-tree');
    if (!tree) return;
    const bottom = tree.getBoundingClientRect().bottom;
    if (bottom > window.innerHeight) {
      window.scrollBy({top: Math.min(bottom - window.innerHeight + 24, 400), behavior: 'smooth'});
    }
  });
}).observe(element, {childList: true, subtree: true, attributes: true, attributeFilter: ['data-route']});