From 016a71c8e1b101e6996ef3e1798d2df88d19acbb Mon Sep 17 00:00:00 2001 From: fanshi1028 <8843502+fanshi1028@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:25:55 +0800 Subject: [PATCH 1/6] strict ts --- client/src/events.ts | 122 +++++++++++++++++++++++++++++----------- client/src/hyperview.ts | 15 +++++ client/src/index.ts | 100 ++++++++++++++++---------------- client/src/message.ts | 4 +- client/src/response.ts | 6 +- client/src/sockets.ts | 27 ++++++--- client/tsconfig.json | 3 +- 7 files changed, 178 insertions(+), 99 deletions(-) create mode 100644 client/src/hyperview.ts diff --git a/client/src/events.ts b/client/src/events.ts index 9290c3bb..a09f3f82 100644 --- a/client/src/events.ts +++ b/client/src/events.ts @@ -1,54 +1,78 @@ import * as debounce from 'debounce' import { encodedParam } from './action' +import { HyperView, isHyperView } from './hyperview' export type UrlFragment = string -export function listenKeydown(cb: (target: HTMLElement, action: string) => void): void { +export function listenKeydown(cb: (target: HyperView, action: string) => void): void { listenKeyEvent("keydown", cb) } -export function listenKeyup(cb: (target: HTMLElement, action: string) => void): void { +export function listenKeyup(cb: (target: HyperView, action: string) => void): void { listenKeyEvent("keyup", cb) } -export function listenKeyEvent(event: string, cb: (target: HTMLElement, action: string) => void): void { - document.addEventListener(event.toLowerCase(), function(e: KeyboardEvent) { - let source = e.target as HTMLInputElement +export function listenKeyEvent(event: "keyup" | "keydown", cb: (target: HyperView, action: string) => void): void { + + document.addEventListener(event, function(e: KeyboardEvent) { + if (!(e.target instanceof HTMLInputElement)) { + console.error("listenKeyEvent event target is not HTMLInputElement: ", e.target) + return + } + let source = e.target let datasetKey = "on" + event + e.key let action = source.dataset[datasetKey] if (!action) return e.preventDefault() - cb(nearestTarget(source), action) + const target = nearestTarget(source) + if (!target) { + console.error("Missing target: ", source) + return + } + cb(target, action) }) } -export function listenBubblingEvent(event: string, cb: (target: HTMLElement, action: string) => void): void { +export function listenBubblingEvent(event: string, cb: (target: HyperView, action: string) => void): void { document.addEventListener(event, function(e) { - let el = e.target as HTMLInputElement + if (!(e.target instanceof HTMLInputElement)) { + console.error("listenBubblingEvent event target is not HTMLInputElement: ", e.target) + return + } + let el = e.target // clicks can fire on internal elements. Find the parent with a click handler - let source = el.closest("[data-on" + event + "]") as HTMLElement + let source = el.closest("[data-on" + event + "]") if (!source) return e.preventDefault() let target = nearestTarget(source) - cb(target, source.dataset["on" + event]) + if (!target) { + console.error("Missing target: ", source) + return + } + const action = source.dataset["on" + event] + if (action === undefined) { + console.error("Missing action: ", source, event) + return + } + cb(target, action) }) } -export function listenClick(cb: (target: HTMLElement, action: string) => void): void { +export function listenClick(cb: (target: HyperView, action: string) => void): void { listenBubblingEvent("click", cb) } -export function listenDblClick(cb: (target: HTMLElement, action: string) => void): void { +export function listenDblClick(cb: (target: HyperView, action: string) => void): void { listenBubblingEvent("dblclick", cb) } -export function listenTopLevel(cb: (target: HTMLElement, action: string) => void): void { +export function listenTopLevel(cb: (target: HyperView, action: string) => void): void { document.addEventListener("hyp-load", function(e: CustomEvent) { let action = e.detail.onLoad let target = e.detail.target @@ -72,8 +96,8 @@ export function listenTopLevel(cb: (target: HTMLElement, action: string) => void export function listenLoad(node: HTMLElement): void { // it doesn't really matter WHO runs this except that it should have target - node.querySelectorAll("[data-onload]").forEach((load: HTMLElement) => { - let delay = parseInt(load.dataset.delay) || 0 + node.querySelectorAll("[data-onload]").forEach((load) => { + let delay = parseInt(load.dataset.delay || "") || 0 let onLoad = load.dataset.onload // console.log("load start", load.dataset.onLoad) @@ -95,7 +119,7 @@ export function listenLoad(node: HTMLElement): void { } export function listenMouseEnter(node: HTMLElement): void { - node.querySelectorAll("[data-onmouseenter]").forEach((node: HTMLElement) => { + node.querySelectorAll("[data-onmouseenter]").forEach((node) => { let onMouseEnter = node.dataset.onmouseenter let target = nearestTarget(node) @@ -108,7 +132,7 @@ export function listenMouseEnter(node: HTMLElement): void { } export function listenMouseLeave(node: HTMLElement): void { - node.querySelectorAll("[data-onmouseleave]").forEach((node: HTMLElement) => { + node.querySelectorAll("[data-onmouseleave]").forEach((node) => { let onMouseLeave = node.dataset.onmouseleave let target = nearestTarget(node) @@ -121,21 +145,33 @@ export function listenMouseLeave(node: HTMLElement): void { } -export function listenChange(cb: (target: HTMLElement, action: string) => void): void { +export function listenChange(cb: (target: HyperView, action: string) => void): void { document.addEventListener("change", function(e) { - let el = e.target as HTMLElement + if (!(e.target instanceof HTMLElement)) { + console.error("listenChange event target is not HTMLElement: ", e.target) + return + } + let el = e.target - let source = el.closest("[data-onchange]") as HTMLInputElement + let source = el.closest("[data-onchange]") if (!source) return e.preventDefault() - if (source.value == null) { + if (source.value === null) { console.error("Missing input value:", source) return } let target = nearestTarget(source) + if (!target) { + console.error("Missing target: listenChange") + return + } + if (source.dataset.onchange === undefined) { + console.error("source.dataset.onchange is undefined") + return + } let action = encodedParam(source.dataset.onchange, source.value) cb(target, action) }) @@ -145,14 +181,18 @@ interface LiveInputElement extends HTMLInputElement { debouncedCallback?: Function; } -export function listenInput(startedTyping: (target: HTMLElement) => void, cb: (target: HTMLElement, action: string) => void): void { +export function listenInput(startedTyping: (target: HyperView) => void, cb: (target: HyperView, action: string) => void): void { document.addEventListener("input", function(e) { - let el = e.target as HTMLElement - let source = el.closest("[data-oninput]") as LiveInputElement + if (!(e.target instanceof HTMLElement)) { + console.error("listenInput event target is not HTMLElement: ", e.target) + return + } + let el = e.target + let source = el.closest("[data-oninput]") if (!source) return - let delay = parseInt(source.dataset.delay) || 250 + let delay = parseInt(source.dataset.delay || "") || 250 if (delay < 250) { console.warn("Input delay < 250 can result in poor performance.") } @@ -165,13 +205,17 @@ export function listenInput(startedTyping: (target: HTMLElement) => void, cb: (t e.preventDefault() let target = nearestTarget(source) + if (!target) { + console.error("Missing target: ", source) + return + } // I want to CANCEL the active request as soon as we start typing startedTyping(target) if (!source.debouncedCallback) { + const action = encodedParam(source.dataset.oninput, source.value) source.debouncedCallback = debounce(() => { - let action = encodedParam(source.dataset.oninput, source.value) cb(target, action) }, delay) } @@ -182,11 +226,16 @@ export function listenInput(startedTyping: (target: HTMLElement) => void, cb: (t -export function listenFormSubmit(cb: (target: HTMLElement, action: string, form: FormData) => void): void { +export function listenFormSubmit(cb: (target: HyperView, action: string, form: FormData) => void): void { document.addEventListener("submit", function(e) { - let form = e.target as HTMLFormElement + if (!(e.target instanceof HTMLFormElement)) { + console.error("listenFormSubmit event target is not HTMLFormElement: ", e.target) + return + } + let form = e.target + - if (!form?.dataset.onsubmit) { + if (!form.dataset.onsubmit) { console.error("Missing onSubmit: ", form) return } @@ -195,23 +244,32 @@ export function listenFormSubmit(cb: (target: HTMLElement, action: string, form: let target = nearestTarget(form) const formData = new FormData(form) + if (!target) { + console.error("Missing target: ", form) + return + } cb(target, form.dataset.onsubmit, formData) }) } function nearestTargetId(node: HTMLElement): string | undefined { - let targetData = node.closest("[data-target]") as HTMLElement | undefined + let targetData = node.closest("[data-target]") return targetData?.dataset.target || node.closest("[id]")?.id } -function nearestTarget(node: HTMLElement): HTMLElement { +function nearestTarget(node: HTMLElement): HyperView | undefined { let targetId = nearestTargetId(node) - let target = document.getElementById(targetId) + let target = targetId && document.getElementById(targetId) if (!target) { console.error("Cannot find target: ", targetId, node) return } + if (!isHyperView(target)) { + console.error("Non HyperView target: ", target) + return + } + return target } diff --git a/client/src/hyperview.ts b/client/src/hyperview.ts new file mode 100644 index 00000000..9b2fe9a1 --- /dev/null +++ b/client/src/hyperview.ts @@ -0,0 +1,15 @@ +import { type Request } from "./action"; + +export interface HyperView extends HTMLElement { + runAction(action: string): Promise; + activeRequest?: Request; + cancelActiveRequest(): void; + concurrency: ConcurrencyMode; + _timeout?: number; +} + +export const isHyperView = (ele: any): ele is HyperView => { + return ele?.runAction !== undefined; +}; + +export type ConcurrencyMode = string; diff --git a/client/src/index.ts b/client/src/index.ts index d69c6833..ae764a1c 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -5,6 +5,7 @@ import { actionMessage, ActionMessage, Request, newRequest } from './action' import { ViewId, Metadata, parseMetadata, ViewState } from './message' import { setQuery } from "./browser" import { parseResponse, Response, LiveUpdate } from './response' +import { ConcurrencyMode, HyperView, isHyperView } from "./hyperview" let PACKAGE = require('../package.json'); @@ -63,7 +64,7 @@ function handleRedirect(red: Redirect) { console.log("REDIRECT", red) // the other metdata doesn't apply, they are all specific to the page - applyCookies(red.meta.cookies) + applyCookies(red.meta.cookies ?? []) window.location.href = red.url } @@ -72,6 +73,7 @@ function handleRedirect(red: Redirect) { function handleResponse(res: Update) { // console.log("Handle Response", res) let target = handleUpdate(res) + if (!target) return // clean up the request delete target.activeRequest @@ -79,19 +81,18 @@ function handleResponse(res: Update) { target.classList.remove("hyp-loading") } -function handleUpdate(res: Update): HyperView { +function handleUpdate(res: Update): HyperView | undefined { // console.log("|UPDATE|", res) let targetViewId = res.targetViewId || res.viewId - let target = document.getElementById(targetViewId) as HyperView + let target = document.getElementById(targetViewId) - - if (!target) { - console.error("Missing Update Target: ", targetViewId, res) - return target + if (!isHyperView(target)) { + console.error("Missing Update HyperView Target: ", targetViewId, res) + return } - if (res.requestId < target.activeRequest?.requestId) { + if (target.activeRequest?.requestId && res.requestId < target.activeRequest.requestId) { // this should only happen on Replace, since other requests should be dropped // but it's safe to assume we never want to apply an old requestId console.warn("Ignore Stale Action (" + res.requestId + ") vs (" + target.activeRequest.requestId + "): " + res.action) @@ -117,14 +118,14 @@ function handleUpdate(res: Update): HyperView { // Patch the node const old: VNode = create(target) let next: VNode = create(update.content) - let atts = next.attributes as any + let atts = next.attributes - if (atts["id"] != target.id) { + if (atts["id"] !== target.id) { console.error("Mismatched ViewId in update - ", atts["id"], " target:", target.id) return } - let state = (next.attributes as any)["data-state"] + let state = atts["data-state"] next.attributes = old.attributes @@ -133,22 +134,23 @@ function handleUpdate(res: Update): HyperView { // Emit relevant events let newTarget = document.getElementById(target.id) - dispatchContent(newTarget) if (!newTarget) { console.warn("Target Missing: ", target.id) return target } + dispatchContent(newTarget) + // re-add state attribute - if (state == undefined || state == "()") + if (state === undefined || state == "()") delete newTarget.dataset.state else newTarget.dataset.state = state // execute the metadata, anything that doesn't interrupt the dom update runMetadata(res.meta, newTarget) - applyCookies(res.meta.cookies) + applyCookies(res.meta.cookies ?? []) // now way for these to bubble) listenLoad(newTarget) @@ -183,7 +185,7 @@ function runMetadata(meta: Metadata, target?: HTMLElement) { document.title = meta.pageTitle } - meta.events.forEach((remoteEvent) => { + meta.events?.forEach((remoteEvent) => { setTimeout(() => { let event = new CustomEvent(remoteEvent.name, { bubbles: true, detail: remoteEvent.detail }) let eventTarget = target || document @@ -191,9 +193,9 @@ function runMetadata(meta: Metadata, target?: HTMLElement) { }, 10) }) - meta.actions.forEach(([viewId, action]) => { + meta.actions?.forEach(([viewId, action]) => { setTimeout(() => { - let view = window.Hyperbole.hyperView(viewId) + let view = window.Hyperbole?.hyperView(viewId) if (view) { runAction(view, action) } @@ -203,19 +205,19 @@ function runMetadata(meta: Metadata, target?: HTMLElement) { function fixInputs(target: HTMLElement) { - let focused = target.querySelector("[autofocus]") as HTMLInputElement + let focused = target.querySelector("[autofocus]") if (focused?.focus) { focused.focus() } - target.querySelectorAll("input[value]").forEach((input: HTMLInputElement) => { + target.querySelectorAll("input[value]").forEach((input) => { let val = input.getAttribute("value") - if (val !== undefined) { + if (val !== null) { input.value = val } }) - target.querySelectorAll("input[type=checkbox]").forEach((checkbox: HTMLInputElement) => { + target.querySelectorAll("input[type=checkbox]").forEach((checkbox) => { let checked = checkbox.dataset.checked == "True" checkbox.checked = checked }) @@ -223,9 +225,11 @@ function fixInputs(target: HTMLElement) { function addCSS(src: HTMLStyleElement | null) { if (!src) return; - const rules: any = src.sheet.cssRules - for (const rule of rules) { - if (addedRulesIndex.has(rule.cssText) == false) { + const rules = src.sheet?.cssRules + if (!rules) return; + for (let i = 0; i < rules.length; i++) { + const rule = rules.item(i) + if (rule && addedRulesIndex.has(rule.cssText) == false && rootStyles.sheet) { rootStyles.sheet.insertRule(rule.cssText); addedRulesIndex.add(rule.cssText); } @@ -237,13 +241,15 @@ function addCSS(src: HTMLStyleElement | null) { function init() { // metadata attached to initial page loads need to be executed - let meta = parseMetadata(document.getElementById("hyp.metadata").innerText) + let meta = parseMetadata(document.getElementById("hyp.metadata")?.innerText ?? "") // runMetadataImmediate(meta) runMetadata(meta) - rootStyles = document.body.querySelector('style') + const style = document.body.querySelector('style') - if (!rootStyles) { + if (style !== null) { + rootStyles = style + } else { console.warn("rootStyles missing from page, creating...") rootStyles = document.createElement("style") rootStyles.type = "text/css" @@ -304,10 +310,10 @@ function init() { function enrichHyperViews(node: HTMLElement): void { // enrich all the hyperviews - node.querySelectorAll("[id]").forEach((element: HyperView) => { + node.querySelectorAll("[id]").forEach((element) => { element.runAction = function(action: string) { - runAction(this, action) - }.bind(element) + return runAction(element, action) + } element.concurrency = element.dataset.concurrency || "Drop" @@ -331,10 +337,8 @@ document.addEventListener("DOMContentLoaded", init) -// Should we connect to the socket or not? const sock = new SocketConnection() -sock.connect() -sock.addEventListener("update", (ev: CustomEvent) => handleUpdate(ev.detail)) +sock.addEventListener("update", (ev: CustomEvent) => { handleUpdate(ev.detail) }) sock.addEventListener("response", (ev: CustomEvent) => handleResponse(ev.detail)) sock.addEventListener("redirect", (ev: CustomEvent) => handleRedirect(ev.detail)) @@ -351,7 +355,7 @@ type VNode = { // An object whose key/value pairs are the attribute // name and value, respectively - attributes: [string: string] + attributes: { [key: string]:string | undefined } // Is set to `true` if a node is an `svg`, which tells // Omdomdom to treat it, and its children, as such @@ -375,6 +379,11 @@ declare global { interface Window { Hyperbole?: HyperboleAPI; } + interface DocumentEventMap { + "hyp-load": CustomEvent; + "hyp-mouseenter": CustomEvent; + "hyp-mouseleave": CustomEvent; + } } export interface HyperboleAPI { @@ -385,33 +394,18 @@ export interface HyperboleAPI { socket: SocketConnection } - - - -export interface HyperView extends HTMLElement { - runAction(target: HTMLElement, action: string, form?: FormData): Promise - activeRequest?: Request - cancelActiveRequest(): void - concurrency: ConcurrencyMode - _timeout?: any -} - -type ConcurrencyMode = string - - window.Hyperbole = { runAction: runAction, parseMetadata: parseMetadata, action: function(con, ...params: any[]) { - let ps = params.reduce((str, param) => str + " " + JSON.stringify(param), "") - return con + ps + return params.reduce((str, param) => str + " " + JSON.stringify(param), con); }, hyperView: function(viewId) { - let element = document.getElementById(viewId) as any - if (!element?.runAction) { + let element = document.getElementById(viewId) + if (!isHyperView(element)) { console.error("Element id=" + viewId + " was not a HyperView") - return undefined + return } return element }, diff --git a/client/src/message.ts b/client/src/message.ts index e1a2633a..f49e30be 100644 --- a/client/src/message.ts +++ b/client/src/message.ts @@ -9,7 +9,7 @@ export type RequestId = number export type EncodedAction = string export type ViewState = string -type RemoteEvent = { name: string, detail: any } +type RemoteEvent = { name: string, detail: unknown } export function renderMetas(meta: Meta[]): string { @@ -99,7 +99,7 @@ export function parseAction(input: string): [ViewId, string] { return [viewId, action] } -function breakNextSegment(input: string): [string, string] | undefined { +function breakNextSegment(input: string): [string, string] { let ix = input.indexOf('|') if (ix === -1) { let err = new Error("Bad Encoding, Expected Segment") diff --git a/client/src/response.ts b/client/src/response.ts index d07909a5..44e441e3 100644 --- a/client/src/response.ts +++ b/client/src/response.ts @@ -13,8 +13,8 @@ export type ResponseBody = string export function parseResponse(res: ResponseBody): LiveUpdate { const parser = new DOMParser() const doc = parser.parseFromString(res, 'text/html') - const css = doc.querySelector("style") as HTMLStyleElement - const content = doc.querySelector("div") as HTMLElement + const css = doc.querySelector("style") + const content = doc.querySelector("div") return { content: content, @@ -23,7 +23,7 @@ export function parseResponse(res: ResponseBody): LiveUpdate { } export type LiveUpdate = { - content: HTMLElement + content: HTMLElement | null css: HTMLStyleElement | null } diff --git a/client/src/sockets.ts b/client/src/sockets.ts index 1b93324e..df84a372 100644 --- a/client/src/sockets.ts +++ b/client/src/sockets.ts @@ -6,7 +6,11 @@ import { ViewId, RequestId, EncodedAction, metaValue, Metadata } from "./message const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const defaultAddress = `${protocol}//${window.location.host}${window.location.pathname}` - +interface SocketConnectionEventMap { + "update": CustomEvent; + "response": CustomEvent; + "redirect": CustomEvent; +} export class SocketConnection { socket: WebSocket @@ -17,12 +21,16 @@ export class SocketConnection { queue: ActionMessage[] = [] events: EventTarget - constructor() { + constructor(addr = defaultAddress) { this.events = new EventTarget() + const sock = new WebSocket(addr) + this.socket = sock + // Should we connect to the socket or not? + this.connect(addr, false) } - connect(addr = defaultAddress) { - const sock = new WebSocket(addr) + connect(addr = defaultAddress, createSocket = true) { + const sock = createSocket ? new WebSocket(addr) : this.socket this.socket = sock function onConnectError(ev: Event) { @@ -88,7 +96,7 @@ export class SocketConnection { private runQueue() { // send all messages queued while disconnected - let next: ActionMessage | null = this.queue.pop() + let next: ActionMessage | undefined = this.queue.pop() if (next) { console.log("runQueue: ", next) this.sendAction(next) @@ -197,11 +205,14 @@ export class SocketConnection { // }) // } - addEventListener(e: string, cb: EventListenerOrEventListenerObject) { - this.events.addEventListener(e, cb) + addEventListener(e: K, cb: (ev: SocketConnectionEventMap[K]) => void) { + this.events.addEventListener(e, + // @ts-ignore: HACK + cb + ) } - dispatchEvent(e: Event) { + dispatchEvent(e: SocketConnectionEventMap[K]) { this.events.dispatchEvent(e) } diff --git a/client/tsconfig.json b/client/tsconfig.json index d1bd7be1..8ce28f4a 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -8,7 +8,8 @@ "lib": ["ES2020","DOM"], "allowJs": true, "moduleResolution": "node", - "declaration": true + "declaration": true, + "strict": true // "skipLibCheck": true /*"declarationMap": true*/ }, From 1a26bac6723a50d520e4b7a992138612700ec8fb Mon Sep 17 00:00:00 2001 From: fanshi1028 <8843502+fanshi1028@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:27:17 +0800 Subject: [PATCH 2/6] fixup! strict ts --- client/src/events.ts | 2 +- client/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/events.ts b/client/src/events.ts index a09f3f82..fcea9f50 100644 --- a/client/src/events.ts +++ b/client/src/events.ts @@ -204,7 +204,7 @@ export function listenInput(startedTyping: (target: HyperView) => void, cb: (tar e.preventDefault() - let target = nearestTarget(source) + const target = nearestTarget(source) if (!target) { console.error("Missing target: ", source) return diff --git a/client/src/index.ts b/client/src/index.ts index ae764a1c..512a213e 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -41,7 +41,7 @@ async function runAction(target: HyperView, action: string, form?: FormData) { } } - target._timeout = setTimeout(() => { + target._timeout = window.setTimeout(() => { // add loading after 100ms, not right away // if it runs shorter than that we probably don't want to show the user any loading feedback target.classList.add("hyp-loading") From 89cf0769846881ab2d5f93a52d1d920d3abbd346 Mon Sep 17 00:00:00 2001 From: fanshi1028 <8843502+fanshi1028@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:19:57 +0800 Subject: [PATCH 3/6] fixup! strict ts --- client/src/events.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/events.ts b/client/src/events.ts index fcea9f50..30af181c 100644 --- a/client/src/events.ts +++ b/client/src/events.ts @@ -36,10 +36,9 @@ export function listenKeyEvent(event: "keyup" | "keydown", cb: (target: HyperVie }) } -export function listenBubblingEvent(event: string, cb: (target: HyperView, action: string) => void): void { +export function listenBubblingEvent(event: string, cb: (_target: HyperView, action: string) => void): void { document.addEventListener(event, function(e) { - if (!(e.target instanceof HTMLInputElement)) { - console.error("listenBubblingEvent event target is not HTMLInputElement: ", e.target) + if (!(e.target instanceof HTMLElement)) { return } let el = e.target From fdfd8e327930ad278b499b2a31386c4172f7fc84 Mon Sep 17 00:00:00 2001 From: fanshi1028 <8843502+fanshi1028@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:06:10 +0800 Subject: [PATCH 4/6] fix: listenKeyEvent no need to target HTMLInputElement --- client/src/events.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/events.ts b/client/src/events.ts index 30af181c..d038fa42 100644 --- a/client/src/events.ts +++ b/client/src/events.ts @@ -16,8 +16,8 @@ export function listenKeyup(cb: (target: HyperView, action: string) => void): vo export function listenKeyEvent(event: "keyup" | "keydown", cb: (target: HyperView, action: string) => void): void { document.addEventListener(event, function(e: KeyboardEvent) { - if (!(e.target instanceof HTMLInputElement)) { - console.error("listenKeyEvent event target is not HTMLInputElement: ", e.target) + if (!(e.target instanceof HTMLElement)) { + console.error("listenKeyEvent event target is not HTMLElement: ", e.target) return } let source = e.target From 9013e290bc93bd60f2747d48e9c0297824c79bcd Mon Sep 17 00:00:00 2001 From: fanshi1028 <8843502+fanshi1028@users.noreply.github.com> Date: Tue, 10 Feb 2026 11:08:33 +0800 Subject: [PATCH 5/6] fix: listenMouseEnter/Leave eventTarget could be non HyperView --- client/src/events.ts | 20 +++++++++++++------- client/src/index.ts | 9 --------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/client/src/events.ts b/client/src/events.ts index d038fa42..f48de804 100644 --- a/client/src/events.ts +++ b/client/src/events.ts @@ -121,7 +121,7 @@ export function listenMouseEnter(node: HTMLElement): void { node.querySelectorAll("[data-onmouseenter]").forEach((node) => { let onMouseEnter = node.dataset.onmouseenter - let target = nearestTarget(node) + let target = nearestNonHyperViewTarget(node) node.onmouseenter = () => { const event = new CustomEvent("hyp-mouseenter", { bubbles: true, detail: { target, onMouseEnter } }) @@ -134,7 +134,7 @@ export function listenMouseLeave(node: HTMLElement): void { node.querySelectorAll("[data-onmouseleave]").forEach((node) => { let onMouseLeave = node.dataset.onmouseleave - let target = nearestTarget(node) + let target = nearestNonHyperViewTarget(node) node.onmouseleave = () => { const event = new CustomEvent("hyp-mouseleave", { bubbles: true, detail: { target, onMouseLeave } }) @@ -257,6 +257,17 @@ function nearestTargetId(node: HTMLElement): string | undefined { } function nearestTarget(node: HTMLElement): HyperView | undefined { + const target = nearestNonHyperViewTarget(node) + + if (!isHyperView(target)) { + console.error("Non HyperView target: ", target) + return + } + + return target +} + +function nearestNonHyperViewTarget(node: HTMLElement): HTMLElement | undefined { let targetId = nearestTargetId(node) let target = targetId && document.getElementById(targetId) @@ -265,10 +276,5 @@ function nearestTarget(node: HTMLElement): HyperView | undefined { return } - if (!isHyperView(target)) { - console.error("Non HyperView target: ", target) - return - } - return target } diff --git a/client/src/index.ts b/client/src/index.ts index 512a213e..e94b4690 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -23,15 +23,6 @@ let addedRulesIndex = new Set(); // Run an action in a given HyperView async function runAction(target: HyperView, action: string, form?: FormData) { - if (target === undefined) { - console.error("Undefined HyperView!", action) - return - } - - if (action === undefined) { - console.error("Undefined Action!", target.id) - return - } if (target.activeRequest && !target.activeRequest?.isCancelled) { // Active Request! From 4f6241f63fc79b2354723e21485562e35390b94a Mon Sep 17 00:00:00 2001 From: fanshi1028 <8843502+fanshi1028@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:02:04 +0800 Subject: [PATCH 6/6] fix listenInput --- client/src/events.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/client/src/events.ts b/client/src/events.ts index f48de804..07a7f08a 100644 --- a/client/src/events.ts +++ b/client/src/events.ts @@ -187,7 +187,7 @@ export function listenInput(startedTyping: (target: HyperView) => void, cb: (tar return } let el = e.target - let source = el.closest("[data-oninput]") + const source = el.closest("[data-oninput]") if (!source) return @@ -196,11 +196,6 @@ export function listenInput(startedTyping: (target: HyperView) => void, cb: (tar console.warn("Input delay < 250 can result in poor performance.") } - if (!source?.dataset.oninput) { - console.error("Missing onInput: ", source) - return - } - e.preventDefault() const target = nearestTarget(source) @@ -213,8 +208,12 @@ export function listenInput(startedTyping: (target: HyperView) => void, cb: (tar startedTyping(target) if (!source.debouncedCallback) { - const action = encodedParam(source.dataset.oninput, source.value) source.debouncedCallback = debounce(() => { + if (!source.dataset.oninput) { + console.error("Missing onInput: ", source) + return + } + const action = encodedParam(source.dataset.oninput, source.value) cb(target, action) }, delay) }