flotilla/src/lib/html.ts

163 lines
3.9 KiB
TypeScript
Raw Normal View History

2025-09-18 19:17:31 +00:00
import {sleep, last, randomId} from "@welshman/lib"
2025-02-04 03:28:29 +00:00
export {preventDefault, stopPropagation} from "svelte/legacy"
2024-09-11 18:33:50 +00:00
2024-08-08 23:32:46 +00:00
export const copyToClipboard = (text: string) => {
const {activeElement} = document
const input = document.createElement("textarea")
input.innerHTML = text
document.body.appendChild(input)
input.select()
const result = document.execCommand("copy")
document.body.removeChild(input)
;(activeElement as HTMLElement).focus()
return result
}
2024-09-11 18:33:50 +00:00
2024-11-12 22:32:36 +00:00
export type ScrollerOpts = {
2024-10-01 21:55:33 +00:00
onScroll: () => any
element: Element
2024-09-11 18:33:50 +00:00
threshold?: number
2025-02-06 00:26:22 +00:00
reverse?: boolean
2024-09-24 23:47:36 +00:00
delay?: number
2024-09-11 18:33:50 +00:00
}
2024-11-12 22:32:36 +00:00
export type Scroller = {
check: () => Promise<void>
stop: () => void
}
2024-09-11 18:33:50 +00:00
2024-10-01 21:55:33 +00:00
export const createScroller = ({
onScroll,
element,
delay = 1000,
threshold = 2000,
2025-02-06 00:26:22 +00:00
reverse = false,
2024-10-01 21:55:33 +00:00
}: ScrollerOpts) => {
2024-09-11 18:33:50 +00:00
let done = false
const container = element.classList.contains("scroll-container")
? element
: element.closest(".scroll-container")
2024-09-11 18:33:50 +00:00
const check = async () => {
2024-10-17 21:26:53 +00:00
if (container) {
// While we have empty space, fill it
const {scrollY, innerHeight} = window
const {scrollHeight, scrollTop} = container
const offset = Math.abs(scrollTop || scrollY)
const shouldLoad = offset + innerHeight + threshold > scrollHeight
// Only trigger loading the first time we reach the threshold
if (shouldLoad) {
await onScroll()
}
2024-09-11 18:33:50 +00:00
}
// No need to check all that often
await sleep(delay)
if (!done) {
requestAnimationFrame(check)
}
}
requestAnimationFrame(check)
return {
check,
stop: () => {
done = true
},
}
}
2024-10-24 22:48:45 +00:00
export const isMobile = "ontouchstart" in document.documentElement
2025-02-14 18:53:36 +00:00
export const downloadText = (filename: string, text: string) => {
const blob = new Blob([text], {type: "text/plain"})
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
2025-03-20 16:38:57 +00:00
export const isIntersecting = async (element: Element) =>
new Promise(resolve => {
const observer = new IntersectionObserver(xs => {
resolve(xs.some(x => x.isIntersecting))
observer.unobserve(element)
})
observer.observe(element)
})
2025-06-05 17:55:16 +00:00
export const scrollToEvent = async (id: string, attempts = 3): Promise<boolean> => {
2025-03-20 16:38:57 +00:00
const element = document.querySelector(`[data-event="${id}"]`) as any
2025-06-19 17:22:17 +00:00
const elements = Array.from(document.querySelectorAll("[data-event]"))
2025-03-20 16:38:57 +00:00
if (element) {
element.scrollIntoView({behavior: "smooth", block: "center"})
element.style = "filter: brightness(1.5); transition-property: all; transition-duration: 400ms;"
setTimeout(() => {
element.style = "transition-property: all; transition-duration: 300ms;"
}, 800)
setTimeout(() => {
element.style = ""
}, 800 + 400)
2025-06-05 17:55:16 +00:00
return true
2025-06-19 17:22:17 +00:00
} else if (elements.length > 0) {
const lastElement = last(elements)
2025-03-20 16:38:57 +00:00
if (lastElement && !isIntersecting(lastElement)) {
lastElement.scrollIntoView({behavior: "smooth", block: "center"})
}
await sleep(300)
2025-06-05 17:55:16 +00:00
if (attempts > 0) {
return scrollToEvent(id, attempts - 1)
} else {
return false
}
2025-03-20 16:38:57 +00:00
}
2025-06-19 17:22:17 +00:00
return false
2025-03-20 16:38:57 +00:00
}
2025-09-15 12:40:02 +00:00
2025-09-18 19:17:31 +00:00
export const compressFile = async (file: File | Blob): Promise<File> => {
2025-09-15 12:40:02 +00:00
const {default: Compressor} = await import("compressorjs")
2025-09-18 19:17:31 +00:00
return new Promise<File>((resolve, _reject) => {
2025-09-15 12:40:02 +00:00
new Compressor(file, {
2025-09-18 19:17:31 +00:00
maxWidth: 2048,
maxHeight: 2048,
2025-09-15 12:40:02 +00:00
convertSize: 10 * 1024 * 1024,
2025-09-18 19:17:31 +00:00
success: result => resolve(result as File),
2025-09-15 12:40:02 +00:00
error: e => {
2025-09-18 19:17:31 +00:00
// Non-images break compressor, return the original file
2025-09-15 12:40:02 +00:00
if (e.toString().includes("File or Blob")) {
2025-09-18 19:17:31 +00:00
if (file instanceof Blob) {
file = new File([file], `${randomId()}.${file.type}`, {type: file.type})
}
return resolve(file as File)
2025-09-15 12:40:02 +00:00
}
_reject(e)
},
})
})
}