mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
fix: Link, Pages, Topic, Hook and Others (#178)
* chore: remove useKeyDownListener * chore: remove react-use, update jazz version and add query string * chore: update jazz version * chore: use simple mac or win utils code * feat(util): add isTextInput * feat(hooks): all needed hooks * fix: link bunch stuff * fix: page bunch stuff * chore: bunch update for custom component * chore: use throttle from internal hook * chore: topic bunch stuff * chore: update layout * fix: truncate content header of topic detail
This commit is contained in:
@@ -11,11 +11,11 @@ export function useActiveItemScroll<T extends HTMLElement>(options: ActiveItemSc
|
||||
const { activeIndex } = options
|
||||
const elementRefs = useRef<ElementRefs<T>>([])
|
||||
|
||||
const scrollActiveElementIntoView = useCallback((index: number) => {
|
||||
const scrollActiveElementIntoView = (index: number) => {
|
||||
const activeElement = elementRefs.current[index]
|
||||
activeElement?.focus()
|
||||
// activeElement?.scrollIntoView({ block: "nearest" })
|
||||
}, [])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex !== null) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as React from "react"
|
||||
|
||||
type EventMap = WindowEventMap & HTMLElementEventMap & VisualViewportEventMap
|
||||
|
||||
export function useEventListener<
|
||||
K extends keyof EventMap,
|
||||
T extends Window | HTMLElement | VisualViewport | null = Window
|
||||
>(
|
||||
eventName: K,
|
||||
handler: (event: EventMap[K]) => void,
|
||||
element: T = window as unknown as T, // Cast to `unknown` first, then `T`
|
||||
options: AddEventListenerOptions = {}
|
||||
) {
|
||||
const savedHandler = React.useRef<(event: EventMap[K]) => void>()
|
||||
const { capture, passive, once } = options
|
||||
|
||||
React.useEffect(() => {
|
||||
savedHandler.current = handler
|
||||
}, [handler])
|
||||
|
||||
React.useEffect(() => {
|
||||
const isSupported = element && element.addEventListener
|
||||
if (!isSupported) return
|
||||
|
||||
const eventListener = (event: EventMap[K]) => savedHandler.current?.(event)
|
||||
|
||||
const opts = { capture, passive, once }
|
||||
element.addEventListener(eventName, eventListener as EventListener, opts)
|
||||
return () => {
|
||||
element.removeEventListener(eventName, eventListener as EventListener, opts)
|
||||
}
|
||||
}, [eventName, element, capture, passive, once])
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
/**
|
||||
* Hook to check if component is still mounted
|
||||
*
|
||||
* @returns {boolean} true if the component is mounted, false otherwise
|
||||
*/
|
||||
export function useIsMounted() {
|
||||
const isMounted = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
isMounted.current = true
|
||||
return () => {
|
||||
isMounted.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return React.useCallback(() => isMounted.current, [])
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { isModKey, isTextInput } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
|
||||
type Callback = (event: KeyboardEvent) => void
|
||||
|
||||
export type KeyFilter = ((event: KeyboardEvent) => boolean) | string
|
||||
|
||||
export type Options = {
|
||||
allowInInput?: boolean
|
||||
}
|
||||
|
||||
type RegisteredCallback = {
|
||||
callback: Callback
|
||||
options?: Options
|
||||
}
|
||||
|
||||
// Registered keyboard event callbacks
|
||||
let callbacks: RegisteredCallback[] = []
|
||||
|
||||
// Track if IME input suggestions are open so we can ignore keydown shortcuts
|
||||
// in this case, they should never be triggered from mobile keyboards.
|
||||
let imeOpen = false
|
||||
|
||||
// Based on implementation in react-use
|
||||
// https://github.com/streamich/react-use/blob/master/src/useKey.ts#L15-L22
|
||||
const createKeyPredicate = (keyFilter: KeyFilter) =>
|
||||
typeof keyFilter === "function"
|
||||
? keyFilter
|
||||
: typeof keyFilter === "string"
|
||||
? (event: KeyboardEvent) => event.key === keyFilter
|
||||
: keyFilter
|
||||
? (_event: KeyboardEvent) => true
|
||||
: (_event: KeyboardEvent) => false
|
||||
|
||||
export function useKeyDown(key: KeyFilter, fn: Callback, options?: Options): void {
|
||||
const predicate = createKeyPredicate(key)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (predicate(event)) {
|
||||
fn(event)
|
||||
}
|
||||
}
|
||||
|
||||
callbacks.push({
|
||||
callback: handler,
|
||||
options
|
||||
})
|
||||
|
||||
return () => {
|
||||
callbacks = callbacks.filter(cb => cb.callback !== handler)
|
||||
}
|
||||
}, [fn, predicate, options])
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", event => {
|
||||
if (imeOpen) {
|
||||
return
|
||||
}
|
||||
|
||||
// reverse so that the last registered callbacks get executed first
|
||||
for (const registered of callbacks.reverse()) {
|
||||
if (event.defaultPrevented === true) {
|
||||
break
|
||||
}
|
||||
|
||||
if (!isTextInput(event.target as HTMLElement) || registered.options?.allowInInput || isModKey(event)) {
|
||||
registered.callback(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener("compositionstart", () => {
|
||||
imeOpen = true
|
||||
})
|
||||
|
||||
window.addEventListener("compositionend", () => {
|
||||
imeOpen = false
|
||||
})
|
||||
@@ -2,7 +2,18 @@ import { useAtom } from "jotai"
|
||||
import { useEffect, useCallback } from "react"
|
||||
import { keyboardDisableSourcesAtom } from "@/store/keydown-manager"
|
||||
|
||||
const allowedKeys = ["Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Enter"]
|
||||
const allowedKeys = [
|
||||
"Escape",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"Enter",
|
||||
"Tab",
|
||||
"Backspace",
|
||||
"Home",
|
||||
"End"
|
||||
]
|
||||
|
||||
export function useKeyboardManager(sourceId: string) {
|
||||
const [disableSources, setDisableSources] = useAtom(keyboardDisableSourcesAtom)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useAtomValue } from "jotai"
|
||||
import { useEffect, useCallback } from "react"
|
||||
import { keyboardDisableSourcesAtom } from "@/store/keydown-manager"
|
||||
|
||||
export function useKeydownListener(callback: (event: KeyboardEvent) => void) {
|
||||
const disableSources = useAtomValue(keyboardDisableSourcesAtom)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (disableSources.size === 0) {
|
||||
callback(event)
|
||||
}
|
||||
},
|
||||
[disableSources, callback]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [handleKeyDown])
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
export function useMedia(query: string): boolean {
|
||||
const [matches, setMatches] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia) {
|
||||
const media = window.matchMedia(query)
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches)
|
||||
}
|
||||
const listener = () => {
|
||||
setMatches(media.matches)
|
||||
}
|
||||
media.addListener(listener)
|
||||
return () => media.removeListener(listener)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}, [matches, query])
|
||||
|
||||
return matches
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import { useEventListener } from "./use-event-listener"
|
||||
|
||||
/**
|
||||
* Hook to detect clicks outside of a specified element.
|
||||
*
|
||||
* @param ref The React ref to the element.
|
||||
* @param callback The handler to call when a click outside the element is detected.
|
||||
*/
|
||||
export function useOnClickOutside(
|
||||
ref: React.RefObject<HTMLElement | null>,
|
||||
callback?: (event: MouseEvent | TouchEvent) => void,
|
||||
options: AddEventListenerOptions = {}
|
||||
) {
|
||||
const listener = React.useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
// Do nothing if clicking ref's element or descendent elements
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) {
|
||||
return
|
||||
}
|
||||
callback?.(event)
|
||||
},
|
||||
[ref, callback]
|
||||
)
|
||||
|
||||
useEventListener("mousedown", listener, window, options)
|
||||
useEventListener("touchstart", listener, window, options)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useRef, useCallback } from "react"
|
||||
|
||||
export function useThrottle<T extends (...args: any[]) => void>(
|
||||
callback: T,
|
||||
delay: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
const lastRan = useRef(Date.now())
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
return useCallback(
|
||||
(...args: Parameters<T>) => {
|
||||
const handler = () => {
|
||||
if (Date.now() - lastRan.current >= delay) {
|
||||
callback(...args)
|
||||
lastRan.current = Date.now()
|
||||
} else {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
timeoutRef.current = setTimeout(
|
||||
() => {
|
||||
callback(...args)
|
||||
lastRan.current = Date.now()
|
||||
},
|
||||
delay - (Date.now() - lastRan.current)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
handler()
|
||||
},
|
||||
[callback, delay]
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
const SSR = typeof window === "undefined"
|
||||
|
||||
export function useTouchSensor() {
|
||||
const [isTouchDevice, setIsTouchDevice] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const detectTouch = () => {
|
||||
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0)
|
||||
setIsTouchDevice(
|
||||
!SSR &&
|
||||
(window.matchMedia?.("(hover: none) and (pointer: coarse)")?.matches ||
|
||||
"ontouchstart" in window ||
|
||||
navigator.maxTouchPoints > 0)
|
||||
)
|
||||
}
|
||||
|
||||
detectTouch()
|
||||
|
||||
Reference in New Issue
Block a user