-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathscript.js
419 lines (391 loc) · 14.2 KB
/
script.js
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
// -------------------------------- D A T A -------------------------------- //
const dollars = [200, 400, 600, 800, 1000]
const data = [
{
category: `Authors`,
questions: [
{
q: `Etiquette expert who wrote in 1928 "How to Behave Though a Debutante"`,
a: `Emily Post`,
},
{
q: `Tolstoy lived by his own commandments & was eventually excommunicated by this church`,
a: `(Russian) Orthodox`,
},
{
q: `Varina Davis, daughter of this famous man, wrote the 1895 novel "The Veiled Doctor"`,
a: `Jefferson Davis`,
},
{
q: `This Elizabethan courtier wrote the sonnet that's the preface to Spenser's "The Faerie Queene"`,
a: `Sir Walter Raleigh`,
},
{
q: `He was only 5 when the plague ravaged London; he wrote his "Journal of the Plague Year" 57 years later`,
a: `Daniel Defoe`,
},
],
},
{
category: "U.S. History",
questions: [
{
q: `A U.S. team that played this sport met with Chinese premier Chou-En-lai in 1971`,
a: `ping-pong`,
},
{
q: `When this Vice President was Grand Marshal of the Rose Parade in 1959, the theme was "Tall Tales and True"`,
a: `Nixon`,
},
{
q: `During WWI James Montgomery Flagg produced a series of about 45 posters for this purpose`,
a: `recruiting for the military`,
},
{
q: `It's estimated on May 11, 1934 the Great Plains lost 300 million tons of this`,
a: `topsoil`,
},
{
q: `Name given to the FDR administration's efforts to improve U.S.-Latin American relations`,
a: `Good Neighbor Policy`,
},
],
},
{
category: "Africa",
questions: [
{
q: `This ocean separates East Africa from Australia`,
a: `Indian`,
},
{
q: `This lake in East Central Africa is the largest source of the Nile River`,
a: `Lake Victoria`,
},
{
q: `Country in which you'd find the Booker T. Washington Institute`,
a: `Liberia`,
},
{
q: `Shaaban Robert of Tanzania was one of the best-known authors to write in this Bantu language`,
a: `Swahili`,
},
{
q: `Portuguese explorers who found gold in what's now this country dubbed it the Gold Coast`,
a: `Ghana`,
},
],
},
{
category: "Theatre",
questions: [
{
q: `A musical set during his final days was titled "Are You Lonesome Tonight?"`,
a: `"the King", Elvis`,
},
{
q: `In 1598 this playwright acted in Ben Jonson's 1st important play, "Every Man in His Humour"`,
a: `Shakespeare`,
},
{
q: `The leading characters in "the Lisbon Traviata" are obsessed with this Greek-American diva`,
a: `Maria Callas`,
},
{
q: `Jason Robards starred in the original 1960 production of her play "Toys in the Attic"`,
a: `Lillian Hellman`,
},
{
q: `The 2 characters in this Englishman's 1957 play "The Dumb Waiter" are hired killers`,
a: `Harold Pinter`,
},
],
},
{
category: "Physical Science",
questions: [
{
q: `While it makes up more of the earth's crust than iron, its ore, bauxite, is rarer than iron ore`,
a: `aluminum`,
},
{
q: `Surgeons now use them to "weld" a detached retina or to remove tattoos`,
a: `lasers`,
},
{
q: `Willard Libby won a Nobel Prize for showing you can date fossils by the amount of this isotope in them`,
a: `carbon (carbon 14)`,
},
{
q: `The sun produces its energy through nuclear-fusion, changing hydrogen to this gas`,
a: `helium`,
},
{
q: `Alberto Santos-Dumont in 1906 was the 1st to do this in Europe, 3 years after it was done in the U.S.`,
a: `undergo powered flight`,
},
],
},
{
category: "Mythological Words & Phrases",
questions: [
{
q: `"To cut" this "knot" means to solve a difficult problem in an easy, decisive way`,
a: `Gordian Knot`,
},
{
q: `The expression "hydra-headed" is derived from the many-headed Hydra fought by this hero`,
a: `Hercules`,
},
{
q: `If you stare at your own reflection constantly, this mythological word fits you perfectly`,
a: `Narcissus`,
},
{
q: `The name of this banquet hall has come to describe a final resting place for great men`,
a: `Valhalla`,
},
{
q: `The name of this rock on which a siren sat is now synonymous with "siren"`,
a: `Lorelai`,
},
],
},
]
// -----------------------I N I T I A L I Z A T I O N ---------------------- //
/**
* Calls the `init` function to set up the grid after the document is loaded.
* We need to wait for the document to load because `init` adds new HTML
* elements to a container defined in `jeopardy.html` (`<div id="grid">`).
* If we try to run it before the container is present in the DOM, it'll error.
*
* Note: here we call a function above where it has been defined in the file.
* This works in JS because functions defined with the `function` keyword are
* "hoisted", meaning the interpreter moves them to the top of the file before
* execution. This is _not_ the case for function expressions assigned to
* variables, eg `const init = () => { ... }`. Those are not hoisted, so they
* can only be used after they're defined.
*/
document.addEventListener("DOMContentLoaded", init)
// ----------------------------- H E L P E R S ----------------------------- //
/**
* A helper function to set a bunch of properties on an HTML element at the
* same time.
*/
function setProperties(element, properties) {
Object.entries(properties).forEach(([key, value]) => {
if (typeof value === "object") setProperties(element[key], value)
else element[key] = value
})
}
/**
* A helper function to create an HTML element and add properties to it at the
* same time.
*/
function createElement(type, properties) {
let element = document.createElement(type)
setProperties(element, properties)
return element
}
/**
* Helper for getting question data for a column and row.
* This function prints a detailed error message to the browser console if any
* of the data is missing (which will cause the cell to appear blank).
*/
function getData(column, row) {
const { category, questions } = data[column]
const question = questions?.[row]
let { q = "", a = "" } = question ?? {}
if (!question || !q || !a) {
let detail = " "
if (question) {
let missing = []
if (!q) missing.push('"q"')
if (!a) missing.push('"a"')
detail = ` ${missing.join(" and ")} for `
}
console.error(
`Missing` +
detail +
`question ${row + 1} ($${dollars[row]}) in category ${category}`
)
}
return { q, a }
}
// -------------- A C T I V E - Q U E S T I O N - S C R E E N -------------- //
/**
* Displays the "active" screen and sets it to show a particular question,
* identified by the column (category) and row (dollar value).
* This is triggered by clicking on a question cell.
*
* The active screen is a div element that overlays the grid. We make it
* invisible by setting all of its edges to the center of the screen, so that
* it has 0 width and height. This lets us use the CSS `transition` property
* to animate expanding it back to the screen size.
*/
function setActive(column, row) {
// get the question and answer for the cell we just clicked on
const { q, a } = getData(column, row)
// we created this empty element in the html document and gave it the id "active"
let active = document.getElementById("active")
// update the element's properties to make it visible.
// in style.css we have it set to animate style changes over 0.5 seconds,
// so it will appear to expand from the center while fading in.
setProperties(active, {
style: {
opacity: 1.0,
left: 0,
right: 0,
top: 0,
bottom: 0,
pointerEvents: "auto",
},
hidden: false,
})
let question = createElement("div", {
className: "question",
textContent: q,
})
let onClickQuestion = () => {
let answer = createElement("div", { className: "answer", textContent: a })
active.appendChild(answer)
// add a new click handler to return to the grid
active.addEventListener("click", clearActive, { once: true })
// update the cell on the board to its completed state
completeCell(column, row)
}
// wait 0.5 seconds for the animation to finish, and then add the question
// and the click handler to show the answer.
setTimeout(() => {
active.appendChild(question)
active.addEventListener("click", onClickQuestion, { once: true })
}, 500)
}
/**
* Hides the "active" screen and makes it non-clickable.
*/
function clearActive() {
// 1. hide the active question screen
let active = document.getElementById("active")
// On an element with `position: absolute` and 0 minimum size, setting all
// the positional offsets (`left`, `right`, `top`, `bottom`) to 50% will
// position it in the exact center of its parent. However, the "active"
// element does have a minimum size because of its class `screen`, which
// gives it the beveled border that mimics the jeopardy screen (0.7em wide)
// and 0.5em of padding. Thus, to actually center it, we need to subtract
// the border and padding size from our 50% positional offset.
let offset = "calc(50% - 0.7em - 0.5em)"
setProperties(active, {
style: {
opacity: 0,
left: offset,
right: offset,
top: offset,
bottom: offset,
pointerEvents: "none",
},
hidden: true,
})
// 2. clear the active question screen (remove the current question & answer)
active.replaceChildren()
}
// ------------------------------- C E L L S ------------------------------- //
/**
* Sets up the given question cell with the correct dollar value and an event
* handler that will activate the cell's question when it is clicked.
*
* This function accepts an optional reference to the cell's element because we
* first call it (in the `init` function) _before_ we add the element to the
* DOM, meaning we can't yet look up the element with `document.getElementById`
* and must pass it in directly instead.
*/
function initCell(column, row, element) {
let cell = element ?? document.getElementById(`${column}_${row}`)
// If any of the data is missing, make the cell empty and unclickable.
const { q, a } = getData(column, row)
if (!q || !a) {
cell.onclick = null
cell.replaceChildren()
return
}
// There are two ways to make things clickable in javascript: setting the
// `onclick` property, or adding an event listener. An element can have
// multiple event listeners, and it's possible to accidentally add the same
// event listener more than once, which would call the function multiple
// times. Since we only want to call `setActive` once, and nothing else,
// it's best to use `onclick`.
cell.onclick = () => setActive(column, row)
cell.style.cursor = "pointer"
cell.replaceChildren(
createElement("div", {
textContent: `$` + dollars[row],
className: "value",
})
)
}
/**
* "Complete" a cell by updating it to show the question and answer instead of
* the dollar amount. On real jeopardy they blank out the cell, but since this
* is meant as an educational tool, we want to keep the information visible.
*/
function completeCell(column, row) {
let cell = document.getElementById(`${column}_${row}`)
// remove the click event and change the cursor back to normal
cell.onclick = null
cell.style.cursor = "default"
let { q, a } = getData(column, row)
const [question, answer] = [
createElement("div", { textContent: q, className: "question" }),
createElement("div", { textContent: a, className: "answer" }),
]
cell.replaceChildren(question, answer)
}
// -------------------------------- G R I D -------------------------------- //
/**
* Initialize the jeopardy grid. This function creates the necessary rows and
* cells, adds category titles to each column, and validates that question data
* is formatted correctly.
*/
function init() {
clearActive()
// we created this empty table in the html document and gave it the id "grid"
let grid = document.getElementById("grid")
// clear the grid (in case someone is running this from console to reset the board)
grid.replaceChildren()
// figure out how many columns we need, and configure the grid with that amount
const columns = data.length
grid.style.gridTemplateColumns = `repeat(${columns}, 1fr)`
// We store questions by category then row (top-down then left-right), but CSS
// grids run left-right then top-down. To make sure we add cells in the right
// order, we need to add them to temporary "rows" as we go through categories,
// then dump all the rows into the grid afterward.
let questionRows = dollars.map(() => [])
data.forEach(({ category, questions }, column) => {
if (questions.length != dollars.length) {
console.warn(
`Invalid number of questions in category ${category}: expected ${dollars.length} but found ${questions.length}`
)
}
// create a header cell for the category title and add it to grid
let categoryTitle = createElement("div", {
className: "category screen",
// since categories won't change, we can just set the text now
textContent: category,
})
grid.appendChild(categoryTitle)
// Create as many rows for each column as there are items in the `dollars`
// array (extra questions will be ignored). `row` is the index; we don't
// need the dollar value, so we call its parameter `_`, which is the common
// way to indicate that a parameter is ignorable.
dollars.forEach((_, row) => {
let questionCell = createElement("div", {
// identify the cell by column and row so we can update its contents later
id: `${column}_${row}`,
className: "screen",
})
initCell(column, row, questionCell)
questionRows[row].push(questionCell)
})
})
questionRows.forEach((row) => grid.append(...row))
}