-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.ts
148 lines (127 loc) · 4.25 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import {interpret} from "xstate"
import {
calcMachine,
type Operator as NormalOperator,
isOperator as isNormalOperator,
isUnsignedDigit as isDigit,
isNumeric,
} from "./machine"
import {initThemeSwitch} from "./theme-switch"
import {initRovingTabIndex} from "./toolbar"
initThemeSwitch()
initRovingTabIndex()
const calcService = interpret(calcMachine).start()
// Handle button clicks.
const buttons = document.querySelectorAll<HTMLButtonElement>(".Button")
buttons.forEach((button) => {
button.addEventListener("click", () => {
const text = button.textContent!.trim().toUpperCase()
handleCalcInput(text)
})
})
// Handle keyboard presses (including shortcuts).
// Listen for 'keydown' not 'keyup', so that a user can press
// and hold a key to type it repeatedly.
document.body.addEventListener("keydown", (e) => {
const target = e.target as HTMLElement
if (target.matches("input[type=radio]")) return
// Don't handle the Enter key when it's pressed on a button,
// to avoid interfering with the default button-Enter behaviour,
// which is to activate the button. I must admit that, because
// of this, the UX feels a little weird.
if (!target.matches("button") || (target.matches("button") && e.key !== "Enter")) {
handleCalcInput(e.key)
}
})
// Sync the display with the machine.
const display = document.querySelector(".Display")!
calcService.onTransition((state) => {
if (!state.changed) return
display.textContent = state.context.tokens
// format numbers and operators
.map((token) => {
if (isNumeric(token)) return formatNumStr(token)
if (isOperator(token)) return formatOperator(token)
return token
})
.join(" ")
// Scroll the display as far right as possible, so that
// the newly added/removed characters are obvious.
// See https://stackoverflow.com/q/1962168/12695621
display.scrollLeft = display.scrollWidth
console.log(`State '${state.toStrings().at(-1)}'. Tokens ${JSON.stringify(state.context.tokens)}`)
})
/* HELPERS */
/** Handle a (button or keyboard) calculator input */
function handleCalcInput(input: string) {
if (isDigit(input)) {
calcService.send({type: "DIGIT", data: input})
} else if (isOperator(input)) {
calcService.send({type: "OPERATOR", data: normalizeOperator(input)})
} else if (input === ".") {
calcService.send({type: "DECIMAL_POINT"})
} else if (input === "=" || input === "Enter") {
calcService.send({type: "SOLVE"})
} else if (input === "DEL" || input === "Backspace") {
calcService.send({type: "DELETE"})
} else if (input === "RESET" || input === "Delete") {
calcService.send({type: "RESET"})
} else {
console.warn("Unhandled input", input)
}
}
// The 'fancy' operators are displayed on the UI,
// while the 'normal' ones are used by the machine.
type Operator = NormalOperator | FancyOperator
const FANCY_OPERATORS = ["×", "−"] as const
type FancyOperator = typeof FANCY_OPERATORS[number]
function isOperator(str: string): str is Operator {
// @ts-ignore
return isNormalOperator(str) || FANCY_OPERATORS.includes(str)
}
/**
* Convert a fancy operator to a normal one.
* But return a normal one as is.
*/
function normalizeOperator(op: Operator) {
if (op === "×") return "*"
if (op === "−") return "-"
return op
}
/**
* Format a normal operator into a fancy one.
* But return a fancy one as is.
*/
function formatOperator(op: Operator) {
if (op === "*") return "×"
if (op === "-") return "−"
return op
}
/**
* Format a numeric string into a comma-separated one.
* (This doesn't comma-separate the fraction part, if any)
*/
function formatNumStr(numStr: `${number}`) {
const sign = numStr.startsWith("-") ? "-" : ""
const numericPart = sign ? numStr.slice(1) : numStr
const [intPart, fractionPart] = numericPart.split(".")
let formatted = formatUnsignedIntStr(intPart!)
if (fractionPart !== undefined) {
formatted += "." + fractionPart
}
formatted = sign + formatted
return formatted
}
/**
* Format an unsigned integral string into a comma-separated one.
*/
function formatUnsignedIntStr(intStr: string) {
let formatted = ""
const len = intStr.length
for (let i = 1; i <= len; i++) {
const nextDigit = intStr[len - i]
// Add a comma if i is a multiple of 3 and there's more digits in front
formatted = (i % 3 === 0 && i < len ? "," : "") + nextDigit + formatted
}
return formatted
}