experience.js (4622B)
1 import { isEditableTarget } from "./dom.js"; 2 3 const experienceHandlers = { 4 keydown: null, 5 popstate: null, 6 }; 7 8 export function initExperience() { 9 const section = document.getElementById("experience"); 10 if (!section) return; 11 12 const cards = Array.from(section.querySelectorAll(".exp-card")); 13 const timelines = Array.from(section.querySelectorAll(".exp-timeline")); 14 const rows = Array.from(section.querySelectorAll(".exp-row")); 15 if (cards.length === 0) return; 16 17 let selectedIndex = 0; 18 19 function experienceIDAt(index) { 20 return cards[index]?.dataset.id || rows[index]?.dataset.id || ""; 21 } 22 23 function indexFromPath() { 24 const id = decodeURIComponent( 25 window.location.pathname.match(/^\/experience\/([^/]+)$/)?.[1] || "", 26 ); 27 if (!id) return -1; 28 return cards.findIndex((card) => card.dataset.id === id); 29 } 30 31 function updateExperienceURL(index) { 32 const id = experienceIDAt(index); 33 if (!id) return; 34 35 const path = `/experience/${encodeURIComponent(id)}`; 36 if (window.location.pathname !== path) { 37 history.replaceState({ tab: "experience" }, "", path); 38 } 39 } 40 41 function scrollSelectionIntoView(index) { 42 const target = rows[index] || cards[index]; 43 if (!target) return; 44 45 const scroller = section.querySelector(".experience-detail"); 46 if (!scroller || !scroller.contains(target)) { 47 target.scrollIntoView({ 48 behavior: "smooth", 49 block: "center", 50 inline: "nearest", 51 }); 52 return; 53 } 54 55 const scrollerRect = scroller.getBoundingClientRect(); 56 const targetRect = target.getBoundingClientRect(); 57 const isAbove = targetRect.top < scrollerRect.top + 12; 58 const isBelow = targetRect.bottom > scrollerRect.bottom - 12; 59 if (!isAbove && !isBelow) return; 60 61 const targetTop = 62 targetRect.top - scrollerRect.top + scroller.scrollTop - 8; 63 scroller.scrollTo({ top: Math.max(targetTop, 0), behavior: "smooth" }); 64 } 65 66 function select(index, updateURL = true, scrollIntoView = false) { 67 selectedIndex = index; 68 cards.forEach((card, i) => card.classList.toggle("selected", i === index)); 69 timelines.forEach((timeline, i) => 70 timeline.classList.toggle("active", i === index), 71 ); 72 73 if (scrollIntoView) { 74 scrollSelectionIntoView(index); 75 } 76 77 if (updateURL) { 78 updateExperienceURL(index); 79 } 80 } 81 82 cards.forEach((card, i) => { 83 if (card.dataset.boundHover !== "1") { 84 card.dataset.boundHover = "1"; 85 card.addEventListener("mouseenter", () => select(i)); 86 } 87 }); 88 89 timelines.forEach((timeline, i) => { 90 if (timeline.dataset.boundHover !== "1") { 91 timeline.dataset.boundHover = "1"; 92 timeline.addEventListener("mouseenter", () => select(i)); 93 } 94 }); 95 96 if (experienceHandlers.keydown) { 97 document.removeEventListener("keydown", experienceHandlers.keydown); 98 } 99 100 experienceHandlers.keydown = (e) => { 101 if (!section.isConnected || window.isLightboxOpen?.()) return; 102 if (isEditableTarget(e.target)) return; 103 104 switch (e.key) { 105 case "ArrowUp": 106 e.preventDefault(); 107 if (selectedIndex > 0) select(selectedIndex - 1, true, true); 108 break; 109 case "ArrowDown": 110 e.preventDefault(); 111 if (selectedIndex < cards.length - 1) 112 select(selectedIndex + 1, true, true); 113 break; 114 default: 115 break; 116 } 117 }; 118 119 document.addEventListener("keydown", experienceHandlers.keydown); 120 121 if (experienceHandlers.popstate) { 122 window.removeEventListener("popstate", experienceHandlers.popstate); 123 } 124 125 experienceHandlers.popstate = () => { 126 if (!section.isConnected) return; 127 const index = indexFromPath(); 128 if (index >= 0) { 129 select(index, false); 130 } 131 }; 132 133 window.addEventListener("popstate", experienceHandlers.popstate); 134 135 section 136 .querySelectorAll(".exp-image-hero, .exp-image-thumb") 137 .forEach((img) => { 138 if (img.dataset.boundClick === "1") return; 139 img.dataset.boundClick = "1"; 140 141 img.addEventListener("click", () => { 142 const container = img.closest(".exp-card-images"); 143 const allImages = container 144 ? Array.from(container.querySelectorAll("img")).map((i) => i.src) 145 : []; 146 const startIndex = allImages.indexOf(img.src); 147 window.openLightbox?.(allImages, startIndex); 148 }); 149 }); 150 151 const selectedCard = section.querySelector(".exp-card.selected") || cards[0]; 152 const initialIndex = Math.max(cards.indexOf(selectedCard), 0); 153 const pathIndex = indexFromPath(); 154 if (pathIndex >= 0) { 155 select(pathIndex, false); 156 } else { 157 select(initialIndex); 158 } 159 }