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.