fix(tracker): simplify input module logic

This commit is contained in:
Alex Kaminskii 2023-02-12 18:34:22 +01:00 committed by nick-delirium
parent 2cd74dcad1
commit 72785e7ede
2 changed files with 50 additions and 82 deletions

View file

@ -1,14 +1,14 @@
import type App from '../app/index.js'
import { normSpaces, IN_BROWSER, getLabelAttribute, debounce } from '../utils.js'
import { normSpaces, IN_BROWSER, getLabelAttribute, now } from '../utils.js'
import { hasTag } from '../app/guards.js'
import { InputChange, SetInputValue, SetInputChecked } from '../app/messages.gen.js'
const INPUT_TYPES = ['text', 'password', 'email', 'search', 'number', 'range', 'date', 'tel']
// TODO: take into consideration "contenteditable" attribute
type TextEditableElement = HTMLInputElement | HTMLTextAreaElement
type TextFeildElement = HTMLInputElement | HTMLTextAreaElement
function isTextEditable(node: any): node is TextEditableElement {
function isTextFeildElement(node: Node): node is TextFeildElement {
if (hasTag(node, 'textarea')) {
return true
}
@ -19,7 +19,7 @@ function isTextEditable(node: any): node is TextEditableElement {
return INPUT_TYPES.includes(node.type)
}
function isCheckbox(node: any): node is HTMLInputElement {
function isCheckbox(node: Node): node is HTMLInputElement & { type: 'checkbox' | 'radio' } {
if (!hasTag(node, 'input')) {
return false
}
@ -27,7 +27,7 @@ function isCheckbox(node: any): node is HTMLInputElement {
return type === 'checkbox' || type === 'radio'
}
const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | undefined =
const labelElementFor: (element: TextFeildElement) => HTMLLabelElement | undefined =
IN_BROWSER && 'labels' in HTMLInputElement.prototype
? (node) => {
let p: Node | null = node
@ -57,7 +57,7 @@ const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | unde
}
}
export function getInputLabel(node: TextEditableElement): string {
export function getInputLabel(node: TextFeildElement): string {
let label = getLabelAttribute(node)
if (label === null) {
const labelElement = labelElementFor(node)
@ -86,7 +86,6 @@ export interface Options {
}
export default function (app: App, opts: Partial<Options>): void {
const inputHesitationMap: Map<number, { hesitation: number; focusEv: number }> = new Map()
const options: Options = Object.assign(
{
obscureInputNumbers: true,
@ -97,16 +96,7 @@ export default function (app: App, opts: Partial<Options>): void {
opts,
)
function sendInputChange(id: number, node: TextEditableElement): void {
const label = getInputLabel(node)
// @ts-ignore maybe if hesitationTime > 150 ?
const { hesitation } = inputHesitationMap.get(id)
if (label !== '') {
app.send(InputChange(id, label, hesitation))
}
}
function sendInputValue(id: number, node: TextEditableElement | HTMLSelectElement): void {
function sendInputValue(id: number, node: TextFeildElement | HTMLSelectElement): void {
let value = node.value
let inputMode: InputMode = options.defaultInputMode
@ -144,92 +134,81 @@ export default function (app: App, opts: Partial<Options>): void {
checkboxValues.clear()
})
const debouncedUpdate = debounce((id: number, node: TextEditableElement) => {
sendInputChange(id, node)
function trackInputValue(id: number, node: TextFeildElement) {
if (inputValues.get(id) === node.value) {
return
}
inputValues.set(id, node.value)
sendInputValue(id, node)
}, 125)
}
const debouncedTyping = debounce((id: number, node: TextEditableElement) => {
sendInputValue(id, node)
}, 60)
function trackCheckboxValue(id: number, value: boolean) {
if (checkboxValues.get(id) === value) {
return
}
checkboxValues.set(id, value)
app.send(SetInputChecked(id, value))
}
// The only way (to our knowladge) to track all kinds of input changes, including those made by JS
app.ticker.attach(() => {
inputValues.forEach((value, id) => {
const node = app.nodes.getNode(id) as HTMLInputElement
if (!node) return inputValues.delete(id)
if (value !== node.value) {
inputValues.set(id, node.value)
sendInputValue(id, node)
}
trackInputValue(id, node)
})
checkboxValues.forEach((checked, id) => {
const node = app.nodes.getNode(id) as HTMLInputElement
if (!node) return checkboxValues.delete(id)
if (checked !== node.checked) {
checkboxValues.set(id, node.checked)
app.send(SetInputChecked(id, node.checked))
}
trackCheckboxValue(id, node.checked)
})
}, 5)
function sendInputChange(id: number, node: TextFeildElement, hesitationTime: number) {
trackInputValue(id, node)
const label = getInputLabel(node)
app.send(InputChange(id, label, hesitationTime))
}
app.nodes.attachNodeCallback(
app.safe((node: Node): void => {
const id = app.nodes.getID(node)
if (id === undefined) {
return
}
// TODO: support multiple select (?): use selectedOptions; Need send target?
// TODO: support multiple select (?): use selectedOptions;
if (hasTag(node, 'select')) {
sendInputValue(id, node)
const handler = () => {
sendInputValue(id, node)
}
app.nodes.attachNodeListener(node, 'change', handler)
app.nodes.attachNodeListener(node, 'change', () => sendInputValue(id, node))
}
if (isTextEditable(node)) {
inputValues.set(id, node.value)
inputHesitationMap.set(id, { hesitation: 0, focusEv: 0 })
sendInputValue(id, node)
const setFocus = () => {
inputHesitationMap.set(id, { hesitation: 0, focusEv: +new Date() })
if (isTextFeildElement(node)) {
trackInputValue(id, node)
let nodeFocusTime = 0
let nodeHesitationTime = 0
const onFocus = () => {
nodeFocusTime = now()
}
const inputEvent = (e: InputEvent) => {
const value = (e.target as HTMLInputElement).value
if (inputValues.get(id) === '' && value !== '') {
const inputTime = +new Date()
const { focusEv } = inputHesitationMap.get(id)!
// @ts-ignore
const hesitationTime = inputTime - focusEv
inputHesitationMap.set(id, { hesitation: hesitationTime, focusEv })
const onInput = () => {
const value = node.value
if (nodeHesitationTime === 0) {
nodeHesitationTime = nodeFocusTime - now()
}
inputValues.set(id, value)
debouncedTyping(id, node)
}
const changeEvent = (e: InputEvent) => {
const value = (e.target as HTMLInputElement).value
if (inputValues.get(id) !== value) {
inputValues.set(id, value)
debouncedUpdate(id, node)
}
inputHesitationMap.set(id, { hesitation: 0, focusEv: 0 })
const onChange = () => {
sendInputChange(id, node, nodeHesitationTime)
nodeHesitationTime = 0
}
app.nodes.attachNodeListener(node, 'focus', setFocus)
app.nodes.attachNodeListener(node, 'input', inputEvent)
app.nodes.attachNodeListener(node, 'change', changeEvent)
app.nodes.attachNodeListener(node, 'focus', onFocus)
app.nodes.attachNodeListener(node, 'input', onInput)
app.nodes.attachNodeListener(node, 'change', onChange)
return
}
if (isCheckbox(node)) {
checkboxValues.set(id, node.checked)
app.send(SetInputChecked(id, node.checked))
const checkboxChange = (e: InputEvent) => {
const value = (e.target as HTMLInputElement).checked
checkboxValues.set(id, value)
app.send(SetInputChecked(id, value))
}
app.nodes.attachNodeListener(node, 'change', checkboxChange)
trackCheckboxValue(id, node.checked)
app.nodes.attachNodeListener(node, 'change', (e) => trackCheckboxValue(id, node.checked))
return
}
}),

View file

@ -81,14 +81,3 @@ export function hasOpenreplayAttribute(e: Element, attr: string): boolean {
return false
}
export function debounce(func: (...args: any[]) => void, timeout = 125) {
let timer: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timer)
timer = setTimeout(() => {
// @ts-ignore
func.apply(this, args)
}, timeout)
}
}