PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/services/typewriterEgg.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:25:56 UTC 3,019 bytes, 97 lines download raw bytes

1// The typewriter (#754): type the site's name outside a form field and the
2// page wears a typewriter face for a while, then puts it back.
3//
4// The treatment is a font change, not an animation, so a visitor who asks for
5// reduced motion sees the same thing everyone else does with nothing moving.
6//
7// Putting the font back reflows the page. A reflow that a visitor did not ask
8// for counts against the page's layout-shift score, while one that follows a
9// key or a click does not. So the timer here does not revert anything. It only
10// marks the treatment as spent, and the next key or pointer press takes it
11// off. Escape ends it at once from anywhere, including a form field, so nobody
12// can be stuck in it. The trigger logic lives in utils/easterEggs.js and is
13// unit tested there; this module only wires it to the document.
14
15import {
16  TYPEWRITER_DURATION_MS,
17  TYPEWRITER_SEQUENCE,
18  createSequenceMatcher,
19  isTypingTarget,
20} from '../utils/easterEggs.js?v=3.8.36';
21
22export const TYPEWRITER_CLASS = 'archive-typewriter-egg';
23
24/**
25 * Listen for the sequence and apply the treatment when it lands.
26 * Returns a teardown function that removes the listeners and the class.
27 */
28export function installTypewriterEgg({
29  doc = typeof document === 'undefined' ? null : document,
30  sequence = TYPEWRITER_SEQUENCE,
31  durationMs = TYPEWRITER_DURATION_MS,
32} = {}) {
33  if (!doc || typeof doc.addEventListener !== 'function') return () => {};
34
35  const matcher = createSequenceMatcher(sequence);
36  let timer = null;
37  // True once the treatment has had its time and is waiting for the visitor to
38  // press something before it comes off.
39  let spent = false;
40
41  const clearTimer = () => {
42    if (timer !== null) {
43      clearTimeout(timer);
44      timer = null;
45    }
46  };
47
48  const stop = () => {
49    clearTimer();
50    spent = false;
51    doc.body?.classList.remove(TYPEWRITER_CLASS);
52  };
53
54  const start = () => {
55    if (!doc.body) return;
56    clearTimer();
57    spent = false;
58    doc.body.classList.add(TYPEWRITER_CLASS);
59    timer = setTimeout(() => {
60      timer = null;
61      spent = true;
62    }, durationMs);
63  };
64
65  const endIfSpent = () => {
66    if (spent) stop();
67  };
68
69  const handleKeydown = (event) => {
70    // Escape comes first, so it still works with the caret in the search box.
71    if (event.key === 'Escape') {
72      matcher.reset();
73      stop();
74      return;
75    }
76    endIfSpent();
77    if (event.metaKey || event.ctrlKey || event.altKey) return;
78    if (isTypingTarget(event.target)) return;
79    if (typeof event.key !== 'string' || event.key.length !== 1) return;
80    if (matcher.push(event.key)) start();
81  };
82
83  const handlePointerdown = () => {
84    endIfSpent();
85  };
86
87  doc.addEventListener('keydown', handleKeydown);
88  doc.addEventListener('pointerdown', handlePointerdown);
89
90  return () => {
91    doc.removeEventListener('keydown', handleKeydown);
92    doc.removeEventListener('pointerdown', handlePointerdown);
93    stop();
94  };
95}
96
97export default { installTypewriterEgg, TYPEWRITER_CLASS };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.