The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore(cleanup): misc refactoring for cleanup, fix some search bugs

+102 -173
+4 -2
quartz/components/scripts/clipboard.inline.ts
··· 14 14 button.type = "button" 15 15 button.innerHTML = svgCopy 16 16 button.ariaLabel = "Copy source" 17 - button.addEventListener("click", () => { 17 + function onClick() { 18 18 navigator.clipboard.writeText(source).then( 19 19 () => { 20 20 button.blur() ··· 26 26 }, 27 27 (error) => console.error(error), 28 28 ) 29 - }) 29 + } 30 + button.addEventListener("click", onClick) 31 + window.addCleanup(() => button.removeEventListener("click", onClick)) 30 32 els[i].prepend(button) 31 33 } 32 34 }
+12 -9
quartz/components/scripts/darkmode.inline.ts
··· 10 10 } 11 11 12 12 document.addEventListener("nav", () => { 13 - const switchTheme = (e: any) => { 14 - const newTheme = e.target.checked ? "dark" : "light" 13 + const switchTheme = (e: Event) => { 14 + const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light" 15 15 document.documentElement.setAttribute("saved-theme", newTheme) 16 16 localStorage.setItem("theme", newTheme) 17 17 emitThemeChangeEvent(newTheme) 18 18 } 19 19 20 + const themeChange = (e: MediaQueryListEvent) => { 21 + const newTheme = e.matches ? "dark" : "light" 22 + document.documentElement.setAttribute("saved-theme", newTheme) 23 + localStorage.setItem("theme", newTheme) 24 + toggleSwitch.checked = e.matches 25 + emitThemeChangeEvent(newTheme) 26 + } 27 + 20 28 // Darkmode toggle 21 29 const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement 22 30 toggleSwitch.addEventListener("change", switchTheme) ··· 27 35 28 36 // Listen for changes in prefers-color-scheme 29 37 const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)") 30 - colorSchemeMediaQuery.addEventListener("change", (e) => { 31 - const newTheme = e.matches ? "dark" : "light" 32 - document.documentElement.setAttribute("saved-theme", newTheme) 33 - localStorage.setItem("theme", newTheme) 34 - toggleSwitch.checked = e.matches 35 - emitThemeChangeEvent(newTheme) 36 - }) 38 + colorSchemeMediaQuery.addEventListener("change", themeChange) 39 + window.addCleanup(() => colorSchemeMediaQuery.removeEventListener("change", themeChange)) 37 40 })
+68 -133
quartz/components/scripts/search.inline.ts
··· 26 26 27 27 const tokenizeTerm = (term: string) => { 28 28 const tokens = term.split(/\s+/).filter((t) => t.trim() !== "") 29 - 30 29 const tokenLen = tokens.length 31 30 if (tokenLen > 1) { 32 31 for (let i = 1; i < tokenLen; i++) { ··· 77 76 }) 78 77 .join(" ") 79 78 80 - return `${startIndex === 0 ? "" : "..."}${slice}${ 81 - endIndex === tokenizedText.length - 1 ? "" : "..." 82 - }` 79 + return `${startIndex === 0 ? "" : "..."}${slice}${endIndex === tokenizedText.length - 1 ? "" : "..." 80 + }` 83 81 } 84 82 85 - function highlightHTML(searchTerm: string, el: HTMLElement) { 83 + function highlightHTML(searchTerm: string, innerHTML: string) { 86 84 const p = new DOMParser() 87 85 const tokenizedTerms = tokenizeTerm(searchTerm) 88 - const html = p.parseFromString(el.innerHTML, "text/html") 86 + const html = p.parseFromString(innerHTML, "text/html") 89 87 90 88 const createHighlightSpan = (text: string) => { 91 89 const span = document.createElement("span") ··· 125 123 126 124 document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { 127 125 const currentSlug = e.detail.url 128 - 129 126 const data = await fetchData 130 127 const container = document.getElementById("search-container") 131 - const searchSpace = document.getElementById("search-space") 132 128 const sidebar = container?.closest(".sidebar") as HTMLElement 133 129 const searchIcon = document.getElementById("search-icon") 134 130 const searchBar = document.getElementById("search-bar") as HTMLInputElement | null ··· 193 189 e.preventDefault() 194 190 const searchBarOpen = container?.classList.contains("active") 195 191 searchBarOpen ? hideSearch() : showSearch("basic") 192 + return 196 193 } else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { 197 194 // Hotkey to open tag search 198 195 e.preventDefault() ··· 201 198 202 199 // add "#" prefix for tag search 203 200 if (searchBar) searchBar.value = "#" 201 + return 204 202 } 205 203 206 204 if (currentHover) { ··· 262 260 } 263 261 } 264 262 265 - function trimContent(content: string) { 266 - // works without escaping html like in `description.ts` 267 - const sentences = content.replace(/\s+/g, " ").split(".") 268 - let finalDesc = "" 269 - let sentenceIdx = 0 270 - 271 - // Roughly estimate characters by (words * 5). Matches description length in `description.ts`. 272 - const len = contextWindowWords * 5 273 - while (finalDesc.length < len) { 274 - const sentence = sentences[sentenceIdx] 275 - if (!sentence) break 276 - finalDesc += sentence + "." 277 - sentenceIdx++ 278 - } 279 - 280 - // If more content would be available, indicate it by finishing with "..." 281 - if (finalDesc.length < content.length) { 282 - finalDesc += ".." 283 - } 284 - 285 - return finalDesc 286 - } 287 - 288 263 const formatForDisplay = (term: string, id: number) => { 289 264 const slug = idDataMap[id] 290 265 return { 291 266 id, 292 267 slug, 293 268 title: searchType === "tags" ? data[slug].title : highlight(term, data[slug].title ?? ""), 294 - // if searchType is tag, display context from start of file and trim, otherwise use regular highlight 295 - content: 296 - searchType === "tags" 297 - ? trimContent(data[slug].content) 298 - : highlight(term, data[slug].content ?? "", true), 299 - tags: highlightTags(term, data[slug].tags), 269 + content: highlight(term, data[slug].content ?? "", true), 270 + tags: highlightTags(term.substring(1), data[slug].tags), 300 271 } 301 272 } 302 273 303 274 function highlightTags(term: string, tags: string[]) { 304 - if (tags && searchType === "tags") { 305 - // Find matching tags 306 - const termLower = term.toLowerCase() 307 - let matching = tags.filter((str) => str.includes(termLower)) 275 + if (!tags || searchType !== "tags") { 276 + return [] 277 + } 308 278 309 - // Subtract matching from original tags, then push difference 310 - if (matching.length > 0) { 311 - let difference = tags.filter((x) => !matching.includes(x)) 312 - 313 - // Convert to html (cant be done later as matches/term dont get passed to `resultToHTML`) 314 - matching = matching.map((tag) => `<li><p class="match-tag">#${tag}</p></li>`) 315 - difference = difference.map((tag) => `<li><p>#${tag}</p></li>`) 316 - matching.push(...difference) 317 - } 318 - 319 - // Only allow max of `numTagResults` in preview 320 - if (tags.length > numTagResults) { 321 - matching.splice(numTagResults) 279 + return tags.map(tag => { 280 + if (tag.toLowerCase().includes(term.toLowerCase())) { 281 + return `<li><p class="match-tag">#${tag}</p></li>` 282 + } else { 283 + return `<li><p>#${tag}</p></li>` 322 284 } 323 - 324 - return matching 325 - } else { 326 - return [] 327 - } 285 + }).slice(0, numTagResults) 328 286 } 329 287 330 288 function resolveUrl(slug: FullSlug): URL { ··· 332 290 } 333 291 334 292 const resultToHTML = ({ slug, title, content, tags }: Item) => { 335 - const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : `` 336 - const resultContent = enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>` 337 - 293 + const htmlTags = tags.length > 0 ? `<ul class="tags">${tags.join("")}</ul>` : `` 338 294 const itemTile = document.createElement("a") 339 295 itemTile.classList.add("result-card") 340 - Object.assign(itemTile, { 341 - id: slug, 342 - href: resolveUrl(slug).toString(), 343 - innerHTML: `<h3>${title}</h3>${htmlTags}${resultContent}`, 344 - }) 296 + itemTile.id = slug 297 + itemTile.href = resolveUrl(slug).toString() 298 + itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p class="preview">${content}</p>` 345 299 346 300 async function onMouseEnter(ev: MouseEvent) { 347 - // Actually when we hover, we need to clean all highlights within the result childs 348 301 if (!ev.target) return 349 - for (const el of document.getElementsByClassName( 350 - "result-card", 351 - ) as HTMLCollectionOf<HTMLElement>) { 352 - el.classList.remove("focus") 353 - el.blur() 354 - } 302 + currentHover?.classList.remove('focus') 303 + currentHover?.blur() 355 304 const target = ev.target as HTMLInputElement 356 305 await displayPreview(target) 357 306 currentHover = target 358 - currentHover.classList.remove("focus") 307 + currentHover.classList.add("focus") 359 308 } 360 309 361 310 async function onMouseLeave(ev: MouseEvent) { 362 - const target = ev.target as HTMLAnchorElement 311 + if (!ev.target) return 312 + const target = ev.target as HTMLElement 363 313 target.classList.remove("focus") 364 314 } 365 315 ··· 373 323 hideSearch() 374 324 }, 375 325 ], 376 - ] as [keyof HTMLElementEventMap, (this: HTMLElement) => void][] 326 + ] as const 377 327 378 - events.forEach(([event, handler]) => itemTile.addEventListener(event, handler)) 328 + events.forEach(([event, handler]) => { 329 + itemTile.addEventListener(event, handler) 330 + window.addCleanup(() => itemTile.removeEventListener(event, handler)) 331 + }) 379 332 380 333 return itemTile 381 334 } ··· 386 339 removeAllChildren(results) 387 340 if (finalResults.length === 0) { 388 341 results.innerHTML = `<a class="result-card no-match"> 389 - <h3>No results.</h3> 390 - <p>Try another search term?</p> 391 - </a>` 342 + <h3>No results.</h3> 343 + <p>Try another search term?</p> 344 + </a>` 392 345 } else { 393 346 results.append(...finalResults.map(resultToHTML)) 394 347 } 395 - // focus on first result, then also dispatch preview immediately 396 - if (results?.firstElementChild) { 348 + 349 + if (finalResults.length === 0 && preview) { 350 + // no results, clear previous preview 351 + removeAllChildren(preview) 352 + } else { 353 + // focus on first result, then also dispatch preview immediately 397 354 const firstChild = results.firstElementChild as HTMLElement 398 - if (firstChild.classList.contains("no-match")) { 399 - removeAllChildren(preview as HTMLElement) 400 - } else { 401 - firstChild.classList.add("focus") 402 - currentHover = firstChild as HTMLInputElement 403 - await displayPreview(firstChild) 404 - } 355 + firstChild.classList.add("focus") 356 + currentHover = firstChild as HTMLInputElement 357 + await displayPreview(firstChild) 405 358 } 406 359 } 407 360 ··· 427 380 } 428 381 429 382 async function displayPreview(el: HTMLElement | null) { 430 - if (!searchLayout || !enablePreview || !el) return 431 - 383 + if (!searchLayout || !enablePreview || !el || !preview) return 432 384 const slug = el.id as FullSlug 433 385 el.classList.add("focus") 434 - 435 - removeAllChildren(preview as HTMLElement) 436 - 437 386 previewInner = document.createElement("div") 438 387 previewInner.classList.add("preview-inner") 439 - preview?.appendChild(previewInner) 440 - 441 388 const innerDiv = await fetchContent(slug).then((contents) => 442 - contents.map((el) => highlightHTML(currentSearchTerm, el as HTMLElement)), 389 + contents.map((el) => highlightHTML(currentSearchTerm, el.innerHTML)), 443 390 ) 444 391 previewInner.append(...innerDiv) 392 + preview.replaceChildren(previewInner) 393 + 394 + // scroll to longest 395 + const highlights = [...preview.querySelectorAll(".highlight")].sort((a, b) => b.innerHTML.length - a.innerHTML.length) 396 + highlights[0]?.scrollIntoView() 445 397 } 446 398 447 399 async function onType(e: HTMLElementEventMap["input"]) { 448 - let term = (e.target as HTMLInputElement).value 449 - let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] 400 + if (!searchLayout || !index) return 450 401 currentSearchTerm = (e.target as HTMLInputElement).value 402 + searchLayout.style.visibility = currentSearchTerm === "" ? "hidden" : "visible" 403 + searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic" 451 404 452 - if (searchLayout) { 453 - searchLayout.style.visibility = "visible" 454 - } 455 - 456 - if (term === "" && searchLayout) { 457 - searchLayout.style.visibility = "hidden" 458 - } 459 - 460 - if (term.toLowerCase().startsWith("#")) { 461 - searchType = "tags" 462 - } else { 463 - searchType = "basic" 464 - } 465 - 466 - switch (searchType) { 467 - case "tags": { 468 - term = term.substring(1) 469 - searchResults = 470 - (await index?.searchAsync({ query: term, limit: numSearchResults, index: ["tags"] })) ?? 471 - [] 472 - break 473 - } 474 - case "basic": 475 - default: { 476 - searchResults = 477 - (await index?.searchAsync({ 478 - query: term, 479 - limit: numSearchResults, 480 - index: ["title", "content"], 481 - })) ?? [] 482 - } 405 + let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[] 406 + if (searchType === "tags") { 407 + searchResults = await index.searchAsync({ 408 + query: currentSearchTerm.substring(1), 409 + limit: numSearchResults, 410 + index: ["tags"], 411 + }) 412 + } else if (searchType === "basic") { 413 + searchResults = await index.searchAsync({ 414 + query: currentSearchTerm, 415 + limit: numSearchResults, 416 + index: ["title", "content"], 417 + }) 483 418 } 484 419 485 420 const getByField = (field: string): number[] => { ··· 493 428 ...getByField("content"), 494 429 ...getByField("tags"), 495 430 ]) 496 - const finalResults = [...allIds].map((id) => formatForDisplay(term, id)) 431 + const finalResults = [...allIds].map((id) => formatForDisplay(currentSearchTerm, id)) 497 432 await displayResults(finalResults) 498 433 } 499 434 ··· 505 440 window.addCleanup(() => searchBar?.removeEventListener("input", onType)) 506 441 507 442 index ??= await fillDocument(data) 508 - registerEscapeHandler(searchSpace, hideSearch) 443 + registerEscapeHandler(container, hideSearch) 509 444 }) 510 445 511 446 /**
+18 -29
quartz/components/styles/search.scss
··· 88 88 visibility: hidden; 89 89 border: 1px solid var(--lightgray); 90 90 91 + @media all and (min-width: $tabletBreakpoint) { 92 + &[data-preview] { 93 + & .result-card > p.preview { 94 + display: none; 95 + } 96 + } 97 + } 98 + 91 99 & > div { 92 100 // vh - #search-space.margin-top 93 101 height: calc(75vh - 12vh); ··· 174 182 outline: none; 175 183 font-weight: inherit; 176 184 177 - &:hover, 178 185 &:focus, 179 186 &.focus { 180 187 background: var(--lightgray); ··· 184 191 margin: 0; 185 192 } 186 193 187 - & > ul > li { 188 - margin: 0; 189 - display: inline-block; 190 - white-space: nowrap; 191 - margin: 0; 192 - overflow-wrap: normal; 193 - } 194 - 195 - & > ul { 196 - list-style: none; 197 - display: flex; 198 - padding-left: 0; 199 - gap: 0.4rem; 200 - margin: 0; 194 + & > ul.tags { 201 195 margin-top: 0.45rem; 202 - box-sizing: border-box; 203 - overflow: hidden; 204 - background-clip: border-box; 196 + margin-bottom: 0; 205 197 } 206 198 207 199 & > ul > li > p { 208 200 border-radius: 8px; 209 201 background-color: var(--highlight); 210 - overflow: hidden; 211 - background-clip: border-box; 212 - padding: 0.03rem 0.4rem; 213 - margin: 0; 202 + padding: 0.2rem 0.4rem; 203 + margin: 0 0.1rem; 204 + line-height: 1.4rem; 205 + font-weight: bold; 214 206 color: var(--secondary); 215 - opacity: 0.85; 216 - } 217 - 218 - & > ul > li > .match-tag { 219 - color: var(--tertiary); 220 - font-weight: bold; 221 - opacity: 1; 207 + 208 + &.match-tag { 209 + color: var(--tertiary); 210 + } 222 211 } 223 212 224 213 & > p {