Files
openDIAW.be/packages/app/studio/src/ui/components/TimeCodeInput.tsx
T
kxkm 049547c822 feat: rebrand openDAW → openDIAW.be
Rename all user-visible strings: HTML title, meta tags, about dialogs,
sample origin enum, README, serve-studio log prefix.

Internal package imports (@opendaw/*) preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 21:40:21 +01:00

146 lines
5.7 KiB
TypeScript

import css from "./TimeCodeInput.sass?inline"
import {
checkIndex,
int,
isDefined,
isInstanceOf,
Lifecycle,
MutableObservableValue,
safeRead,
tryCatch
} from "@opendaw/lib-std"
import {createElement} from "@opendaw/lib-jsx"
import {ppqn, PPQN} from "@opendaw/lib-dsp"
import {Events, Html} from "@opendaw/lib-dom"
const defaultClassName = Html.adoptStyleSheet(css, "TimeCodeInput")
type Construct = {
lifecycle: Lifecycle
model: MutableObservableValue<ppqn>
className?: string
negativeWarning?: boolean
signature?: [int, int]
oneBased?: boolean
}
export const TimeCodeInput = ({lifecycle, model, className, negativeWarning, signature, oneBased}: Construct) => {
const upper = signature?.at(0) ?? 4
const lower = signature?.at(1) ?? 4
const units = [
{amount: PPQN.Bar, maxChars: 3},
{amount: PPQN.Quarter, maxChars: 1},
{amount: PPQN.SemiQuaver, maxChars: 1},
{amount: 1, maxChars: 3}
]
const unitOffset = oneBased === true ? 1 : 0
const inputs: ReadonlyArray<HTMLElement> = units.map(({maxChars}) => (
<div contentEditable="true" style={{width: `calc(0.5em + ${maxChars * 6 + 1}px)`}}/>
))
const element: HTMLElement = (
<div className={Html.buildClassList(defaultClassName, className)}>
{inputs}
</div>
)
const updateDigits = () => {
const value = model.getValue()
const negative = value < 0
element.classList.toggle("negative", negativeWarning === true && negative)
const {bars, beats, semiquavers, ticks} = PPQN.toParts(value, upper, lower)
inputs[0].textContent = negative ? String(bars) : String(bars + unitOffset).padStart(3, "0")
inputs[1].textContent = String(beats + unitOffset)
inputs[2].textContent = String(semiquavers + unitOffset)
inputs[3].textContent = String(ticks).padStart(3, "0")
}
lifecycle.ownAll(
model.subscribe(updateDigits),
Events.subscribe(element, "focusin", (event: Event) => {
if (!isInstanceOf(event.target, HTMLElement)) {return}
Html.selectContent(event.target)
}),
Events.subscribe(element, "focusout", (event: Event) => {
if (!isInstanceOf(event.target, HTMLElement)) {return}
Html.unselectContent(event.target)
}),
Events.subscribe(element, "copy", (event: ClipboardEvent) => {
event.preventDefault()
event.clipboardData?.setData("application/json", JSON.stringify({
app: "openDIAW.be",
content: "timecode",
value: model.getValue()
}))
}),
Events.subscribe(element, "paste", (event: ClipboardEvent) => {
const data = event.clipboardData?.getData("application/json")
if (isDefined(data)) {
const {status, value: json} = tryCatch(() => JSON.parse(data))
if (status === "failure") {return}
if (safeRead(json, "app") === "openDIAW.be" && safeRead(json, "content") === "timecode") {
event.preventDefault()
model.setValue(json.value ?? 0)
}
}
}),
Events.subscribe(element, "keydown", (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {return}
const target = event.target
if (!isInstanceOf(target, HTMLElement)) {return}
const index = checkIndex(inputs.indexOf(target), inputs)
switch (event.code) {
case "ArrowUp": {
event.preventDefault()
model.setValue(model.getValue() + units[index].amount)
Html.selectContent(target)
break
}
case "ArrowDown": {
event.preventDefault()
model.setValue(model.getValue() - units[index].amount)
Html.selectContent(target)
break
}
case "Enter": {
event.preventDefault()
const unit = parseInt(target.textContent ?? "") | 0
const prevValue = model.getValue()
const {bars, beats, semiquavers, ticks} = PPQN.toParts(prevValue, upper, lower)
const nextValue: int =
units[0].amount * (index === 0 ? prevValue >= 0 ? unit - unitOffset : unit : bars)
+ units[1].amount * (index === 1 ? unit - unitOffset : beats)
+ units[2].amount * (index === 2 ? unit - unitOffset : semiquavers)
+ units[3].amount * (index === 3 ? unit : ticks)
if (prevValue === nextValue) {
updateDigits()
} else {
model.setValue(nextValue)
}
Html.selectContent(target)
break
}
case "Digit1":
case "Digit2":
case "Digit3":
case "Digit4":
case "Digit5":
case "Digit6":
case "Digit7":
case "Digit8":
case "Digit9":
case "Digit0":
case "Tab":
case "ArrowLeft":
case "ArrowRight":
case "Minus":
case "Backspace": {
break // Allow
}
default: {
console.debug("ignore", event.code)
event.preventDefault()
}
}
})
)
updateDigits()
return element
}