cleanup event listeners with solid-primitives/event-listener (#20619)

This commit is contained in:
Brendan Allan
2026-04-02 17:40:03 +08:00
committed by GitHub
parent 327f62526a
commit 69d047ae7d
22 changed files with 102 additions and 176 deletions

View File

@@ -515,6 +515,7 @@
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@shikijs/transformers": "3.9.2", "@shikijs/transformers": "3.9.2",
"@solid-primitives/bounds": "0.1.3", "@solid-primitives/bounds": "0.1.3",
"@solid-primitives/event-listener": "2.4.5",
"@solid-primitives/media": "2.3.3", "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",

View File

@@ -1,6 +1,7 @@
import { useIsRouting, useLocation } from "@solidjs/router" import { useIsRouting, useLocation } from "@solidjs/router"
import { batch, createEffect, onCleanup, onMount } from "solid-js" import { batch, createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -349,13 +350,12 @@ export function DebugBar() {
syncHeap() syncHeap()
start() start()
document.addEventListener("visibilitychange", vis) makeEventListener(document, "visibilitychange", vis)
onCleanup(() => { onCleanup(() => {
if (one !== 0) cancelAnimationFrame(one) if (one !== 0) cancelAnimationFrame(one)
if (two !== 0) cancelAnimationFrame(two) if (two !== 0) cancelAnimationFrame(two)
stop() stop()
document.removeEventListener("visibilitychange", vis)
for (const ob of obs) ob.disconnect() for (const ob of obs) ob.disconnect()
}) })
}) })

View File

@@ -1,4 +1,5 @@
import { onCleanup, onMount } from "solid-js" import { onMount } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt" import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -181,15 +182,9 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
} }
onMount(() => { onMount(() => {
document.addEventListener("dragover", handleGlobalDragOver) makeEventListener(document, "dragover", handleGlobalDragOver)
document.addEventListener("dragleave", handleGlobalDragLeave) makeEventListener(document, "dragleave", handleGlobalDragLeave)
document.addEventListener("drop", handleGlobalDrop) makeEventListener(document, "drop", handleGlobalDrop)
})
onCleanup(() => {
document.removeEventListener("dragover", handleGlobalDragOver)
document.removeEventListener("dragleave", handleGlobalDragLeave)
document.removeEventListener("drop", handleGlobalDrop)
}) })
return { return {

View File

@@ -1,5 +1,6 @@
import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js" import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -250,8 +251,7 @@ function useKeyCapture(input: {
input.stop() input.stop()
} }
document.addEventListener("keydown", handle, true) makeEventListener(document, "keydown", handle, { capture: true })
onCleanup(() => document.removeEventListener("keydown", handle, true))
}) })
} }

View File

@@ -2,6 +2,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js" import { type Accessor, createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
import { dict as en } from "@/i18n/en" import { dict as en } from "@/i18n/en"
@@ -378,11 +379,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
} }
onMount(() => { onMount(() => {
document.addEventListener("keydown", handleKeyDown) makeEventListener(document, "keydown", handleKeyDown)
})
onCleanup(() => {
document.removeEventListener("keydown", handleKeyDown)
}) })
function register(cb: () => CommandOption[]): void function register(cb: () => CommandOption[]): void

View File

@@ -1,7 +1,8 @@
import type { Event } from "@opencode-ai/sdk/v2/client" import type { Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus" import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener"
import { batch, onCleanup, onMount } from "solid-js"
import z from "zod" import z from "zod"
import { createSdkForServer } from "@/utils/server" import { createSdkForServer } from "@/utils/server"
import { useLanguage } from "./language" import { useLanguage } from "./language"
@@ -206,21 +207,16 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
clearHeartbeat() clearHeartbeat()
} }
const onVisibility = () => { onMount(() => {
if (typeof document === "undefined") return makeEventListener(document, "visibilitychange", () => {
if (document.visibilityState !== "visible") return if (document.visibilityState !== "visible") return
if (!started) return if (!started) return
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
attempt?.abort() attempt?.abort()
} })
if (typeof document !== "undefined") { })
document.addEventListener("visibilitychange", onVisibility)
}
onCleanup(() => { onCleanup(() => {
if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", onVisibility)
}
stop() stop()
abort.abort() abort.abort()
flush() flush()

View File

@@ -1,6 +1,7 @@
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useGlobalSync } from "./global-sync" import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk" import { useGlobalSDK } from "./global-sdk"
import { useServer } from "./server" import { useServer } from "./server"
@@ -366,12 +367,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
flush() flush()
} }
window.addEventListener("pagehide", flush) makeEventListener(window, "pagehide", flush)
document.addEventListener("visibilitychange", handleVisibility) makeEventListener(document, "visibilitychange", handleVisibility)
onCleanup(() => { onCleanup(() => {
window.removeEventListener("pagehide", flush)
document.removeEventListener("visibilitychange", handleVisibility)
scroll.dispose() scroll.dispose()
}) })
}) })

View File

@@ -12,6 +12,7 @@ import {
untrack, untrack,
type Accessor, type Accessor,
} from "solid-js" } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout" import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
@@ -215,18 +216,11 @@ export default function Layout(props: ParentProps) {
if (document.visibilityState !== "hidden") return if (document.visibilityState !== "hidden") return
reset() reset()
} }
window.addEventListener("pointerup", stop) makeEventListener(window, "pointerup", stop)
window.addEventListener("pointercancel", stop) makeEventListener(window, "pointercancel", stop)
window.addEventListener("blur", stop) makeEventListener(window, "blur", stop)
window.addEventListener("blur", blur) makeEventListener(window, "blur", blur)
document.addEventListener("visibilitychange", hide) makeEventListener(document, "visibilitychange", hide)
onCleanup(() => {
window.removeEventListener("pointerup", stop)
window.removeEventListener("pointercancel", stop)
window.removeEventListener("blur", stop)
window.removeEventListener("blur", blur)
document.removeEventListener("visibilitychange", hide)
})
}) })
const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
@@ -1394,8 +1388,7 @@ export default function Layout(props: ParentProps) {
} }
handleDeepLinks(drainPendingDeepLinks(window)) handleDeepLinks(drainPendingDeepLinks(window))
window.addEventListener(deepLinkEvent, handler as EventListener) makeEventListener(window, deepLinkEvent, handler as EventListener)
onCleanup(() => window.removeEventListener(deepLinkEvent, handler as EventListener))
}) })
async function renameProject(project: LocalProject, next: string) { async function renameProject(project: LocalProject, next: string) {

View File

@@ -14,6 +14,7 @@ import {
onMount, onMount,
untrack, untrack,
} from "solid-js" } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createMediaQuery } from "@solid-primitives/media" import { createMediaQuery } from "@solid-primitives/media"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
@@ -1687,11 +1688,10 @@ export default function Page() {
) )
onMount(() => { onMount(() => {
document.addEventListener("keydown", handleKeyDown) makeEventListener(document, "keydown", handleKeyDown)
}) })
onCleanup(() => { onCleanup(() => {
document.removeEventListener("keydown", handleKeyDown)
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)

View File

@@ -1,5 +1,6 @@
import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
@@ -86,8 +87,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
pull() pull()
} }
window.addEventListener(composerEvent, onEvent) makeEventListener(window, composerEvent, onEvent)
onCleanup(() => window.removeEventListener(composerEvent, onEvent))
}) })
const todos = createMemo((): Todo[] => { const todos = createMemo((): Todo[] => {

View File

@@ -1,6 +1,7 @@
import { createEffect, createMemo, Match, on, onCleanup, Switch } from "solid-js" import { createEffect, createMemo, createSignal, Match, on, onCleanup, Switch } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { FileSearchHandle } from "@opencode-ai/ui/file" import type { FileSearchHandle } from "@opencode-ai/ui/file"
import { useFileComponent } from "@opencode-ai/ui/context/file" import { useFileComponent } from "@opencode-ai/ui/context/file"
import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
@@ -59,7 +60,7 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
let scrollFrame: number | undefined let scrollFrame: number | undefined
let restoreFrame: number | undefined let restoreFrame: number | undefined
let pending: ScrollPos | undefined let pending: ScrollPos | undefined
let code: HTMLElement[] = [] const [code, setCode] = createSignal<HTMLElement[]>([])
const getCode = () => { const getCode = () => {
const el = scroll const el = scroll
@@ -106,17 +107,9 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
const sync = () => { const sync = () => {
const next = getCode() const next = getCode()
if (next.length === code.length && next.every((el, i) => el === code[i])) return const current = code()
if (next.length === current.length && next.every((el, i) => el === current[i])) return
for (const item of code) { setCode(next)
item.removeEventListener("scroll", onCodeScroll)
}
code = next
for (const item of code) {
item.addEventListener("scroll", onCodeScroll)
}
} }
const restore = () => { const restore = () => {
@@ -128,14 +121,14 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
sync() sync()
if (code.length > 0) { if (code().length > 0) {
for (const item of code) { for (const item of code()) {
if (item.scrollLeft !== pos.x) item.scrollLeft = pos.x if (item.scrollLeft !== pos.x) item.scrollLeft = pos.x
} }
} }
if (el.scrollTop !== pos.y) el.scrollTop = pos.y if (el.scrollTop !== pos.y) el.scrollTop = pos.y
if (code.length > 0) return if (code().length > 0) return
if (el.scrollLeft !== pos.x) el.scrollLeft = pos.x if (el.scrollLeft !== pos.x) el.scrollLeft = pos.x
} }
@@ -149,24 +142,24 @@ function createScrollSync(input: { tab: () => string; view: ReturnType<typeof us
} }
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
if (code.length === 0) sync() if (code().length === 0) sync()
save({ save({
x: code[0]?.scrollLeft ?? event.currentTarget.scrollLeft, x: code()[0]?.scrollLeft ?? event.currentTarget.scrollLeft,
y: event.currentTarget.scrollTop, y: event.currentTarget.scrollTop,
}) })
} }
createEffect(() => {
for (const item of code()) makeEventListener(item, "scroll", onCodeScroll)
})
const setViewport = (el: HTMLDivElement) => { const setViewport = (el: HTMLDivElement) => {
scroll = el scroll = el
restore() restore()
} }
onCleanup(() => { onCleanup(() => {
for (const item of code) {
item.removeEventListener("scroll", onCodeScroll)
}
if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame) if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
}) })
@@ -358,8 +351,7 @@ export function FileTabContent(props: { tab: string }) {
find?.focus() find?.focus()
} }
window.addEventListener("keydown", onKeyDown, { capture: true }) makeEventListener(window, "keydown", onKeyDown, { capture: true })
onCleanup(() => window.removeEventListener("keydown", onKeyDown, { capture: true }))
}) })
createEffect( createEffect(

View File

@@ -1,5 +1,6 @@
import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { same } from "@/utils/same" import { same } from "@/utils/same"
const emptyTabs: string[] = [] const emptyTabs: string[] = []
@@ -171,14 +172,9 @@ export const createSizing = () => {
} }
onMount(() => { onMount(() => {
window.addEventListener("pointerup", stop) makeEventListener(window, "pointerup", stop)
window.addEventListener("pointercancel", stop) makeEventListener(window, "pointercancel", stop)
window.addEventListener("blur", stop) makeEventListener(window, "blur", stop)
onCleanup(() => {
window.removeEventListener("pointerup", stop)
window.removeEventListener("pointercancel", stop)
window.removeEventListener("blur", stop)
})
}) })
onCleanup(() => { onCleanup(() => {

View File

@@ -1,4 +1,5 @@
import { createEffect, onCleanup, type JSX } from "solid-js" import { createEffect, createSignal, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { FileDiff } from "@opencode-ai/sdk/v2" import type { FileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/ui/session-review" import { SessionReview } from "@opencode-ai/ui/session-review"
import type { import type {
@@ -123,13 +124,6 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
onCleanup(() => { onCleanup(() => {
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
if (scroll) {
scroll.removeEventListener("wheel", handleInteraction, { capture: true })
scroll.removeEventListener("mousewheel", handleInteraction, { capture: true })
scroll.removeEventListener("pointerdown", handleInteraction, { capture: true })
scroll.removeEventListener("touchstart", handleInteraction, { capture: true })
scroll.removeEventListener("keydown", handleInteraction, { capture: true })
}
}) })
return ( return (
@@ -138,11 +132,11 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
empty={props.empty} empty={props.empty}
scrollRef={(el) => { scrollRef={(el) => {
scroll = el scroll = el
el.addEventListener("wheel", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "wheel", handleInteraction, { passive: true, capture: true })
el.addEventListener("mousewheel", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "mousewheel", handleInteraction, { passive: true, capture: true })
el.addEventListener("pointerdown", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "pointerdown", handleInteraction, { passive: true, capture: true })
el.addEventListener("touchstart", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "touchstart", handleInteraction, { passive: true, capture: true })
el.addEventListener("keydown", handleInteraction, { passive: true, capture: true }) makeEventListener(el, "keydown", handleInteraction, { capture: true })
props.onScrollRef?.(el) props.onScrollRef?.(el)
queueRestore() queueRestore()
}} }}

View File

@@ -1,5 +1,6 @@
import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -50,12 +51,8 @@ export function TerminalPanel() {
const port = window.visualViewport const port = window.visualViewport
sync() sync()
window.addEventListener("resize", sync) makeEventListener(window, "resize", sync)
port?.addEventListener("resize", sync) if (port) makeEventListener(port, "resize", sync)
onCleanup(() => {
window.removeEventListener("resize", sync)
port?.removeEventListener("resize", sync)
})
}) })
createEffect(() => { createEffect(() => {

View File

@@ -48,6 +48,7 @@
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@shikijs/transformers": "3.9.2", "@shikijs/transformers": "3.9.2",
"@solid-primitives/bounds": "0.1.3", "@solid-primitives/bounds": "0.1.3",
"@solid-primitives/event-listener": "2.4.5",
"@solid-primitives/media": "2.3.3", "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",

View File

@@ -16,6 +16,7 @@ import {
} from "@pierre/diffs" } from "@pierre/diffs"
import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { type PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { createMediaQuery } from "@solid-primitives/media" import { createMediaQuery } from "@solid-primitives/media"
import { makeEventListener } from "@solid-primitives/event-listener"
import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js" import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js"
import { createDefaultOptions, styleVariables } from "../pierre" import { createDefaultOptions, styleVariables } from "../pierre"
import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines" import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines"
@@ -286,17 +287,10 @@ function useFileViewer(config: ViewerConfig) {
createEffect(() => { createEffect(() => {
if (!config.enableLineSelection()) return if (!config.enableLineSelection()) return
container.addEventListener("mousedown", handleMouseDown) makeEventListener(container, "mousedown", handleMouseDown)
container.addEventListener("mousemove", handleMouseMove) makeEventListener(container, "mousemove", handleMouseMove)
window.addEventListener("mouseup", handleMouseUp) makeEventListener(window, "mouseup", handleMouseUp)
document.addEventListener("selectionchange", handleSelectionChange) makeEventListener(document, "selectionchange", handleSelectionChange)
onCleanup(() => {
container.removeEventListener("mousedown", handleMouseDown)
container.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("mouseup", handleMouseUp)
document.removeEventListener("selectionchange", handleSelectionChange)
})
}) })
onCleanup(() => { onCleanup(() => {

View File

@@ -1,6 +1,7 @@
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks" import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, For, onCleanup, type JSX, on, Show } from "solid-js" import { createEffect, For, type JSX, on, Show } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
import { Icon, type IconProps } from "./icon" import { Icon, type IconProps } from "./icon"
import { IconButton } from "./icon-button" import { IconButton } from "./icon-button"
@@ -228,9 +229,8 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0) setState("stuck", rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
} }
scroll.addEventListener("scroll", handler, { passive: true }) makeEventListener(scroll, "scroll", handler, { passive: true })
handler() handler()
onCleanup(() => scroll.removeEventListener("scroll", handler))
}) })
return ( return (

View File

@@ -1,15 +1,7 @@
import { Popover as Kobalte } from "@kobalte/core/popover" import { Popover as Kobalte } from "@kobalte/core/popover"
import { import { ComponentProps, JSXElement, ParentProps, Show, createEffect, splitProps, ValidComponent } from "solid-js"
ComponentProps,
JSXElement,
ParentProps,
Show,
createEffect,
onCleanup,
splitProps,
ValidComponent,
} from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
import { IconButton } from "./icon-button" import { IconButton } from "./icon-button"
@@ -104,15 +96,9 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
close("outside") close("outside")
} }
window.addEventListener("keydown", onKeyDown, true) makeEventListener(window, "keydown", onKeyDown, { capture: true })
window.addEventListener("pointerdown", onPointerDown, true) makeEventListener(window, "pointerdown", onPointerDown, { capture: true })
window.addEventListener("focusin", onFocusIn, true) makeEventListener(window, "focusin", onFocusIn, { capture: true })
onCleanup(() => {
window.removeEventListener("keydown", onKeyDown, true)
window.removeEventListener("pointerdown", onPointerDown, true)
window.removeEventListener("focusin", onFocusIn, true)
})
}) })
const content = () => ( const content = () => (

View File

@@ -12,6 +12,7 @@ import {
type JSX, type JSX,
} from "solid-js" } from "solid-js"
import { Dialog as Kobalte } from "@kobalte/core/dialog" import { Dialog as Kobalte } from "@kobalte/core/dialog"
import { makeEventListener } from "@solid-primitives/event-listener"
type DialogElement = () => JSX.Element type DialogElement = () => JSX.Element
@@ -68,8 +69,7 @@ function init() {
event.stopPropagation() event.stopPropagation()
} }
window.addEventListener("keydown", onKeyDown, true) makeEventListener(window, "keydown", onKeyDown, { capture: true })
onCleanup(() => window.removeEventListener("keydown", onKeyDown, true))
}) })
const show = (element: DialogElement, owner: Owner, onClose?: () => void) => { const show = (element: DialogElement, owner: Owner, onClose?: () => void) => {

View File

@@ -1,5 +1,6 @@
import { createEffect, on, onCleanup } from "solid-js" import { createEffect, createSignal, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
export interface AutoScrollOptions { export interface AutoScrollOptions {
@@ -14,7 +15,6 @@ export function createAutoScroll(options: AutoScrollOptions) {
let settling = false let settling = false
let settleTimer: ReturnType<typeof setTimeout> | undefined let settleTimer: ReturnType<typeof setTimeout> | undefined
let autoTimer: ReturnType<typeof setTimeout> | undefined let autoTimer: ReturnType<typeof setTimeout> | undefined
let cleanup: (() => void) | undefined
let auto: { top: number; time: number } | undefined let auto: { top: number; time: number } | undefined
const threshold = () => options.bottomThreshold ?? 10 const threshold = () => options.bottomThreshold ?? 10
@@ -216,26 +216,14 @@ export function createAutoScroll(options: AutoScrollOptions) {
onCleanup(() => { onCleanup(() => {
if (settleTimer) clearTimeout(settleTimer) if (settleTimer) clearTimeout(settleTimer)
if (autoTimer) clearTimeout(autoTimer) if (autoTimer) clearTimeout(autoTimer)
if (cleanup) cleanup()
}) })
return { return {
scrollRef: (el: HTMLElement | undefined) => { scrollRef: (el: HTMLElement | undefined) => {
if (cleanup) {
cleanup()
cleanup = undefined
}
scroll = el
if (!el) return if (!el) return
updateOverflowAnchor(el) updateOverflowAnchor(el)
el.addEventListener("wheel", handleWheel, { passive: true }) makeEventListener(el, "wheel", handleWheel, { passive: true })
cleanup = () => {
el.removeEventListener("wheel", handleWheel)
}
}, },
contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el), contentRef: (el: HTMLElement | undefined) => setStore("contentRef", el),
handleScroll, handleScroll,

View File

@@ -1,4 +1,5 @@
import { createEffect, onCleanup, onMount } from "solid-js" import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
@@ -105,9 +106,9 @@ type CreateFileFindOptions = {
export function createFileFind(opts: CreateFileFindOptions) { export function createFileFind(opts: CreateFileFindOptions) {
let input: HTMLInputElement | undefined let input: HTMLInputElement | undefined
let overlayFrame: number | undefined let overlayFrame: number | undefined
let overlayScroll: HTMLElement[] = []
let mode: "highlights" | "overlay" = "overlay" let mode: "highlights" | "overlay" = "overlay"
let hits: Range[] = [] let hits: Range[] = []
const [overlayScroll, setOverlayScroll] = createSignal<HTMLElement[]>([])
const [state, setState] = createStore({ const [state, setState] = createStore({
open: false, open: false,
@@ -123,8 +124,7 @@ export function createFileFind(opts: CreateFileFindOptions) {
const pos = () => state.pos const pos = () => state.pos
const clearOverlayScroll = () => { const clearOverlayScroll = () => {
for (const el of overlayScroll) el.removeEventListener("scroll", scheduleOverlay) setOverlayScroll([])
overlayScroll = []
} }
const clearOverlay = () => { const clearOverlay = () => {
@@ -197,11 +197,11 @@ export function createFileFind(opts: CreateFileFindOptions) {
(node): node is HTMLElement => node instanceof HTMLElement, (node): node is HTMLElement => node instanceof HTMLElement,
) )
: [] : []
if (next.length === overlayScroll.length && next.every((el, i) => el === overlayScroll[i])) return const current = overlayScroll()
if (next.length === current.length && next.every((el, i) => el === current[i])) return
clearOverlayScroll() clearOverlayScroll()
overlayScroll = next setOverlayScroll(next)
for (const el of overlayScroll) el.addEventListener("scroll", scheduleOverlay, { passive: true })
} }
const clearFind = () => { const clearFind = () => {
@@ -404,6 +404,10 @@ export function createFileFind(opts: CreateFileFindOptions) {
close, close,
} }
createEffect(() => {
for (const el of overlayScroll()) makeEventListener(el, "scroll", scheduleOverlay, { passive: true })
})
onMount(() => { onMount(() => {
mode = supportsHighlights() ? "highlights" : "overlay" mode = supportsHighlights() ? "highlights" : "overlay"
installShortcuts() installShortcuts()
@@ -425,16 +429,12 @@ export function createFileFind(opts: CreateFileFindOptions) {
const update = () => positionBar() const update = () => positionBar()
requestAnimationFrame(update) requestAnimationFrame(update)
window.addEventListener("resize", update, { passive: true }) makeEventListener(window, "resize", update, { passive: true })
const wrapper = opts.wrapper() const wrapper = opts.wrapper()
if (!wrapper) return if (!wrapper) return
const root = scrollParent(wrapper) ?? wrapper const root = scrollParent(wrapper) ?? wrapper
createResizeObserver(root, update) createResizeObserver(root, update)
onCleanup(() => {
window.removeEventListener("resize", update)
})
}) })
onCleanup(() => { onCleanup(() => {

View File

@@ -1,5 +1,6 @@
import { createEffect, onCleanup, onMount } from "solid-js" import { createEffect, onMount } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createSimpleContext } from "../context/helper" import { createSimpleContext } from "../context/helper"
import oc2ThemeJson from "./themes/oc-2.json" import oc2ThemeJson from "./themes/oc-2.json"
import { resolveThemeVariant, themeToCss } from "./resolve" import { resolveThemeVariant, themeToCss } from "./resolve"
@@ -237,19 +238,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
} }
} }
if (typeof window === "object") {
window.addEventListener("storage", onStorage)
onCleanup(() => window.removeEventListener("storage", onStorage))
}
onMount(() => { onMount(() => {
makeEventListener(window, "storage", onStorage)
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const onMedia = () => { const onMedia = () => {
if (store.colorScheme !== "system") return if (store.colorScheme !== "system") return
setStore("mode", getSystemMode()) setStore("mode", getSystemMode())
} }
mediaQuery.addEventListener("change", onMedia) makeEventListener(mediaQuery, "change", onMedia)
onCleanup(() => mediaQuery.removeEventListener("change", onMedia))
const rawTheme = read(STORAGE_KEYS.THEME_ID) const rawTheme = read(STORAGE_KEYS.THEME_ID)
const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2" const savedTheme = normalize(rawTheme ?? props.defaultTheme) ?? "oc-2"