← Guides EVOLution
Loaded

Exam 1 · Mechanisms / Pop Gen Study Guide

BIOL 4230 · Evolution · Exam 1 · Mechanisms / Pop Gen — Final exam Mon May 4, 2026 · 5–7 PM · Dr. Travis Robbins
Lectures: L01 · L02 · L03 · L04 · L05

This guide is fully editable — type anywhere, format with the toolbar, and your edits autosave to this browser. Reset restores the seeded content. Print to PDF (8.5×11 portrait) anytime.

L01 · What Is Evolution? Course Intro (Ch 1)

First lecture frames evolution as the unifying theory of biology — a process of heritable change in populations over generations — and previews the four mechanisms (selection, drift, gene flow, mutation) the rest of the course will dissect.

§A — Defining evolution

Evolution is descent with modification — change in the heritable traits of populations across generations. It is a population-level, multi-generational phenomenon, not something that happens to an individual.

Key points

Key terms

Exam traps
  • An individual cannot 'evolve' — even if they change during their lifetime, evolution requires change across generations in a population.
  • If trait differences are NOT heritable (e.g., purely environmental), selection on those traits produces no evolutionary change.

§B — Why evolution unifies biology

Dobzhansky's 1973 essay — 'Nothing in biology makes sense except in the light of evolution' — captures the role evolution plays as the explanatory glue across every biological subdiscipline.

Key points

Key terms

Exam traps
  • 'Adaptation' has two meanings — a trait (the noun) AND the process that produces it (the verb). Context matters.

§C — The four evolutionary mechanisms (preview)

Four forces change allele frequencies. The course spends the next several lectures dissecting each one in turn.

Key points

Key terms

Exam traps
  • Mutation is RANDOM with respect to fitness (it doesn't 'know' what's useful), but selection acting on the resulting variation is NON-random.
  • Drift and selection both change allele frequencies — but only selection systematically increases fitness.

L02 · Evolutionary Thinking — From Pre-Darwinian Worldviews to Natural Selection (Ch 2)

How biologists came to think about life as a tree, not a ladder. This lecture traces the conceptual road from the Great Chain of Being to Darwin and Wallace, and frames the scientific method (hypothesis vs. theory) that lets evolutionary claims be tested.

§A — The Great Chain of Being and pre-Darwinian thinking

Before Darwin, the dominant Western view of life was the Scala Naturae — a fixed hierarchical ladder with humans at the top and 'lower' organisms at the bottom. Species were considered immutable; extinction was not yet recognized as a real phenomenon.

Key points

Key terms

Exam traps
  • The Great Chain of Being is NOT an evolutionary tree — it is a fixed ladder with no branching, no common ancestry, and no extinction.
  • Don't confuse the Chain of Being's 'progression toward humans' with evolution. Evolution does not progress toward any goal.

§B — Extinction recognized — William Smith and stratigraphy

The realization that whole groups of organisms had disappeared came from geology. William Smith showed that fossil sequences in rock layers had a consistent pattern across geography — and that some forms simply stopped appearing in younger rock.

Key points

Key terms

Exam traps
  • The study guide explicitly flags William Smith's role in establishing extinction — be ready to attribute him.
  • Faunal succession is descriptive (which fossils where) — it doesn't on its own explain WHY species disappeared. Extinction as a concept comes from observing the pattern.

§C — Lamarck, Lyell, and the road to Darwin

Several thinkers helped clear the conceptual ground before Darwin. Lamarck proposed an evolutionary mechanism (wrong about the details). Lyell argued for deep time and gradual geological change. Both shaped how Darwin framed natural selection.

Key points

Key terms

Exam traps
  • Lamarck is famous as a wrong example, but the study guide calls for matching him to 'inheritance of acquired traits.' Don't dismiss him without naming his contribution.
  • Lyell was a geologist, not an evolutionary biologist. His contribution to evolution was indirect — providing deep time as a backdrop.

§D — Darwin, Wallace, and natural selection

Darwin's voyage on the Beagle (1831–1836) and decades of subsequent work led him to natural selection. Alfred Russel Wallace independently arrived at the same idea — and his 1858 letter is what finally pushed Darwin to publish.

Key points

Key terms

Exam traps
  • The study guide explicitly tests: 'identifying which naturalist spurred Darwin to make his work public' — that's WALLACE, via his 1858 letter.
  • Natural selection requires variation, heritability, AND differential reproductive success. All three. Missing any one and the population doesn't evolve.

§E — Hypotheses vs. theories — the scientific method

Lay usage of 'theory' often means 'guess,' but in science it means something quite different. Distinguishing hypothesis from theory is a small but reliably testable item.

Key points

Key terms

Exam traps
  • 'Theory' in everyday language ≠ 'theory' in science. Saying evolution is 'just a theory' confuses the two senses.
  • A good hypothesis says what observations would falsify it. 'Evolution happens' is too broad to be a hypothesis; 'antibiotic resistance increases when antibiotics are widely used' is testable.

L03 · Genes and Heritable Variation (Ch 5)

Evolution requires heritable variation. This lecture is the genetics-and-genomics foundation: what genes ARE, how genomes are built, how alleles differ in their phenotypic effects, and how gene expression is regulated at multiple levels.

§A — Genome architecture — coding vs. noncoding DNA

Eukaryotic genomes are mostly NOT made of protein-coding genes. The bulk is noncoding DNA — pseudogenes, mobile elements, regulatory sequences, and stretches with no known function. This matters because evolution acts on genome content as a whole.

Key points

Key terms

Exam traps
  • Genome size ≠ gene count ≠ complexity. The study guide flags this directly: 'genome size does not necessarily correlate with organismal complexity.'
  • Pseudogenes ARE part of the genome — they are real DNA, just not functional. Don't exclude them when listing genome contents.

§B — Levels of gene expression regulation

Cells regulate gene expression at multiple stages — pre-transcriptional, transcriptional, post-transcriptional, and post-translational. Knowing which mechanism acts at which level is a core test item.

Key points

Key terms

Exam traps
  • DNA methylation = PRE-transcriptional (controls access). MicroRNA = POST-transcriptional (degrades mRNAs after they're made). Don't mix them up.
  • Phosphorylation can occur during transcription factor activation (transcriptional) or as a post-translational modification of any protein. Context tells you which level.

§C — Alleles — dominant, recessive, additive

Alleles are alternative versions of a gene at the same locus. How they interact with one another (dominance) — and how the homozygote/heterozygote phenotypes relate — sets how quickly selection can change allele frequencies.

Key points

Key terms

Exam traps
  • Beneficial RECESSIVE alleles increase slowly when rare (most are in Aa heterozygotes, hidden from selection). Beneficial DOMINANT alleles increase quickly when rare. Selection efficiency depends on dominance.
  • Don't confuse 'additive' (linear dose-response across copies) with 'codominant' (both alleles' phenotypes are visible in the heterozygote, but not necessarily linearly intermediate).

§D — Mutation as the source of new variation

Mutation is the ULTIMATE source of new genetic variation. Selection cannot create new alleles — it can only sort among existing variants. Mutation is random with respect to fitness.

Key points

Key terms

Exam traps
  • Mutation is random WITH RESPECT TO FITNESS. Mutation rates can vary across the genome (hotspots) and respond to environmental stressors — but the randomness referred to is about fitness-direction, not uniform per-base probability.
  • Selection requires variation; mutation generates it. Without mutation, selection eventually exhausts genetic variation.

L04 · Genetic Evolution in Populations — Hardy-Weinberg (Ch 6)

Hardy-Weinberg equilibrium is the null model of population genetics. It predicts genotype frequencies from allele frequencies in an idealized non-evolving population, so any deviation tells you something is going on (selection, drift, mutation, gene flow, or non-random mating).

§A — Allele frequencies vs genotype frequencies

An allele frequency is the proportion of a given allele at a locus across the whole population's gene pool. Genotype frequencies are the proportions of homozygotes and heterozygotes.

Key points

Key terms

Exam traps
  • Don't confuse allele counts with individual counts — each diploid individual contributes TWO allele copies.
  • Heterozygotes contribute ONE A and ONE a to the allele tally, not two of one.

§B — The Hardy-Weinberg equation

Under five idealizing assumptions, expected genotype frequencies are p², 2pq, q². The cross-multiplication comes from random union of gametes — Punnett-square logic at the population level.

Key points

Key terms

Exam traps
  • Hardy-Weinberg gives EXPECTED genotype frequencies under the null. If observed frequencies differ, that's evidence the population is NOT in HWE — i.e., one or more forces are acting.
  • HWE is achieved in ONE generation of random mating from any starting genotype frequencies (allele freq p stays the same; genotype freq snaps to p²/2pq/q²).

§C — Detecting deviations and what they mean

Excess homozygotes vs. excess heterozygotes are diagnostic of different evolutionary processes. Spotting and interpreting deviations is a core exam skill.

Key points

Key terms

Exam traps
  • Selection AGAINST the recessive homozygote (aa) makes a slow change because most a alleles hide in heterozygotes — selection on rare recessives is inefficient.
  • Inbreeding does NOT change allele frequencies on its own — it only changes genotype frequencies (more homozygotes, fewer heterozygotes).

§D — Worked computation pattern

Exam questions often hand you genotype counts and ask you to compute allele frequencies, then expected genotype frequencies under HWE, then compare to observed.

Key points

Exam traps
  • Common arithmetic slip: forgetting to multiply expected genotype FREQUENCIES by N (population size) when comparing to observed COUNTS.
  • If only allele frequencies are given, you cannot detect HWE deviations — you need genotype counts.

L05 · Quantitative Genetics, Selection, and Plasticity (Ch 7)

How traits with continuous variation (height, milk yield, beak depth) evolve. This lecture covers phenotypic variance partitioning, heritability, the breeder's equation, and phenotypic plasticity — the ways environment shapes phenotype without changing genotype.

§A — Quantitative traits and partitioning phenotypic variance

Quantitative traits are continuously varying (height, mass, fitness components) — many genes plus environment. The total phenotypic variance V_P can be decomposed into components, each with a different evolutionary meaning.

Key points

Key terms

Exam traps
  • Only V_A responds predictably to selection. V_D and V_I get reshuffled by recombination each generation, so selection on them produces inconsistent gains.
  • The study guide explicitly asks for the V_P expression — be ready to write it: V_P = V_A + V_D + V_I + V_E.

§B — Heritability — broad-sense vs. narrow-sense

Heritability is the fraction of phenotypic variance attributable to genetic causes. The narrow-sense version (h²) — using only V_A — is what predicts response to selection.

Key points

Key terms

Exam traps
  • h² (lowercase, narrow-sense) ≠ H² (uppercase, broad-sense). The study guide tests h² because it's the one that predicts evolution.
  • Heritability is population-and-environment specific. A trait with h² = 0.6 in one population can have h² = 0.2 in another. Don't quote heritability values as universal facts.

§C — The breeder's equation — predicting response to selection

The breeder's equation R = h² · S links heritability to selection differential to predict the per-generation response to selection. It is the central equation of quantitative genetics.

Key points

Key terms

Exam traps
  • If you select strongly (large S) but h² is near zero, the population doesn't evolve. Heritability is the gating factor.
  • The breeder's equation works for ONE generation at a time. Iterating across generations requires knowing how V_A and h² change as allele frequencies shift (they don't stay constant).

§D — Phenotypic plasticity and reaction norms

A single genotype can produce different phenotypes in different environments. Phenotypic plasticity is the rule, not the exception, and the patterns reveal whether plasticity itself can evolve.

Key points

Key terms

Exam traps
  • Sloped reaction norms = plasticity. Non-parallel reaction norms = G×E. The study guide asks about interpreting these graphs.
  • Plasticity is itself heritable and can evolve. A 'plastic' trait isn't the same as a 'non-genetic' trait — the capacity to be plastic is genetic.

§E — Selection vs. evolution — a recurring distinction

Selection ACTING on a trait does not guarantee EVOLUTION. The link is heritability. This is the integrative concept the study guide returns to repeatedly.

Key points

Exam traps
  • Common exam scenario: selection clearly happens (some individuals reproduce more than others) but the trait is environmentally determined → NO evolutionary response. R = h² · S = 0.
  • Selection is a within-generation process. Evolution is a between-generation outcome that requires heritability.
'; const blob = new Blob([html], {type:'text/html'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'Exam1_StudyGuide_BIOL4230.html'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); } function applyBlockStyle(){ const el = document.getElementById('styleSelect'); if(!el) return; const tag = el.value; if(!tag) return; try{ document.execCommand('formatBlock', false, '<' + tag + '>'); doc.focus(); markDirty(); }catch(e){ console.warn('formatBlock failed', e); } el.selectedIndex = 0; } function getSelectedBlocks(){ const sel = window.getSelection(); if(!sel || sel.rangeCount === 0) return []; const range = sel.getRangeAt(0); const blocks = new Set(); function climb(node){ while(node && node !== doc){ if(node.nodeType === 1){ const d = getComputedStyle(node).display; if(d === 'block' || d === 'list-item' || /^(P|H[1-6]|LI|BLOCKQUOTE|PRE|FIGURE|TABLE|TR|TD|TH|DIV)$/.test(node.tagName)){ blocks.add(node); return; } } node = node.parentNode; } } climb(range.startContainer); climb(range.endContainer); const it = document.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT, { acceptNode: n => range.intersectsNode(n) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP }); let n; while((n = it.nextNode())){ const d = getComputedStyle(n).display; if(d === 'block' || d === 'list-item') blocks.add(n); } return Array.from(blocks); } function changeCase(){ const choice = prompt('Change case to:\n 1) UPPERCASE\n 2) lowercase\n 3) Title Case\n 4) Sentence case\nEnter 1, 2, 3, or 4:'); const sel = window.getSelection(); if(!sel || sel.isCollapsed){ alert('Select some text first.'); return; } const text = sel.toString(); let out = text; if(choice === '1') out = text.toUpperCase(); else if(choice === '2') out = text.toLowerCase(); else if(choice === '3') out = text.toLowerCase().replace(/\b([a-z])/g, (m, c) => c.toUpperCase()); else if(choice === '4'){ out = text.toLowerCase().replace(/(^|[.!?]\s+)([a-z])/g, (m, p, c) => p + c.toUpperCase()); } else return; document.execCommand('insertText', false, out); markDirty(); } function applyLineSpacing(){ const el = document.getElementById('lineSpacing'); if(!el || !el.value) return; const v = el.value; getSelectedBlocks().forEach(b => { b.style.lineHeight = v; }); el.selectedIndex = 0; markDirty(); } function colorsEqual(a, b){ if(!a || !b) return false; const ma = ('' + a).match(/\d+/g); const mb = ('' + b).match(/\d+/g); if(!ma || !mb) return false; return ma.slice(0,3).join(',') === mb.slice(0,3).join(','); } function hexToRgb(hex){ const m = hex.replace('#',''); return 'rgb(' + parseInt(m.slice(0,2),16) + ', ' + parseInt(m.slice(2,4),16) + ', ' + parseInt(m.slice(4,6),16) + ')'; } function applyShade(){ const c = document.getElementById('paraShade').value; const target = hexToRgb(c); const blocks = getSelectedBlocks(); if(!blocks.length) return; const allHave = blocks.every(b => colorsEqual(b.style.backgroundColor, target)); blocks.forEach(b => { if(allHave){ b.style.backgroundColor = ''; if(!/pb-/.test(b.className)) b.style.padding = ''; } else { b.style.backgroundColor = c; if(!b.style.padding) b.style.padding = '4px 8px'; } }); markDirty(); } function applyBorder(){ const el = document.getElementById('borderSelect'); if(!el || !el.value) return; const which = el.value; const blocks = getSelectedBlocks(); el.selectedIndex = 0; if(!blocks.length) return; if(which === 'none'){ blocks.forEach(b => b.classList.remove('pb-all','pb-top','pb-bottom','pb-left','pb-right')); } else { const cls = 'pb-' + which; const allHave = blocks.every(b => b.classList.contains(cls)); blocks.forEach(b => { if(allHave){ b.classList.remove(cls); } else { b.classList.remove('pb-all','pb-top','pb-bottom','pb-left','pb-right'); b.classList.add(cls); } }); } markDirty(); } function applyListStyle(){ const el = document.getElementById('listStyleSelect'); if(!el || !el.value) return; const [tag, style] = el.value.split(':'); const cmdName = tag === 'ol' ? 'insertOrderedList' : 'insertUnorderedList'; document.execCommand(cmdName, false, null); const sel = window.getSelection(); if(sel && sel.rangeCount){ let n = sel.getRangeAt(0).startContainer; while(n && n !== doc){ if(n.nodeType === 1 && /^(UL|OL)$/.test(n.tagName)){ n.style.listStyleType = style; break; } n = n.parentNode; } } el.selectedIndex = 0; markDirty(); } /* FIND / REPLACE */ let findHits = []; let findIndex = -1; function clearFindHits(){ doc.querySelectorAll('.find-hit, .find-hit-current').forEach(el => { const parent = el.parentNode; while(el.firstChild) parent.insertBefore(el.firstChild, el); parent.removeChild(el); parent.normalize(); }); findHits = []; findIndex = -1; const cnt = document.getElementById('findCount'); if(cnt) cnt.textContent = ''; } function highlightAll(query){ clearFindHits(); if(!query) return; const re = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); const walker = document.createTreeWalker(doc, NodeFilter.SHOW_TEXT, { acceptNode: n => n.parentElement && n.parentElement.closest('.find-hit') ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT }); const targets = []; let n; while((n = walker.nextNode())){ if(re.test(n.nodeValue)){ re.lastIndex = 0; targets.push(n); } } targets.forEach(node => { const text = node.nodeValue; const frag = document.createDocumentFragment(); let last = 0; let m; const re2 = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); while((m = re2.exec(text))){ if(m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index))); const span = document.createElement('span'); span.className = 'find-hit'; span.textContent = m[0]; frag.appendChild(span); findHits.push(span); last = m.index + m[0].length; if(m[0].length === 0) re2.lastIndex++; } if(last < text.length) frag.appendChild(document.createTextNode(text.slice(last))); node.parentNode.replaceChild(frag, node); }); const cnt = document.getElementById('findCount'); if(cnt) cnt.textContent = findHits.length ? (findHits.length + ' match' + (findHits.length === 1 ? '' : 'es')) : 'No matches'; if(findHits.length){ findIndex = 0; markCurrent(); } } function markCurrent(){ findHits.forEach((h, i) => h.className = (i === findIndex ? 'find-hit-current' : 'find-hit')); if(findHits[findIndex]) findHits[findIndex].scrollIntoView({block:'center', behavior:'smooth'}); const cnt = document.getElementById('findCount'); if(cnt && findHits.length) cnt.textContent = (findIndex+1) + ' / ' + findHits.length; } function findNext(){ if(!findHits.length) return; findIndex = (findIndex+1) % findHits.length; markCurrent(); } function findPrev(){ if(!findHits.length) return; findIndex = (findIndex-1+findHits.length) % findHits.length; markCurrent(); } function replaceOne(){ const r = document.getElementById('replaceInput').value; if(findIndex < 0 || !findHits[findIndex]) return; const hit = findHits[findIndex]; const txt = document.createTextNode(r); hit.parentNode.replaceChild(txt, hit); findHits.splice(findIndex, 1); if(findIndex >= findHits.length) findIndex = 0; markCurrent(); markDirty(); } function replaceAll(){ const r = document.getElementById('replaceInput').value; if(!findHits.length) return; findHits.forEach(h => { h.parentNode.replaceChild(document.createTextNode(r), h); }); const n = findHits.length; findHits = []; findIndex = -1; const cnt = document.getElementById('findCount'); if(cnt) cnt.textContent = 'Replaced ' + n; markDirty(); } function openFind(){ document.getElementById('findbar').style.display = 'flex'; const i = document.getElementById('findInput'); i.focus(); i.select(); } function openReplace(){ openFind(); document.getElementById('replaceInput').focus(); } function closeFind(){ clearFindHits(); document.getElementById('findbar').style.display = 'none'; doc.focus(); } /* JUMP-TO TOC */ function buildJumpTo(){ const sel = document.getElementById('jumpTo'); if(!sel) return; sel.innerHTML = ''; const heads = doc.querySelectorAll('h1, h2, h3'); let i = 0; heads.forEach(h => { if(!h.id) h.id = 'sec-' + (i++); const indent = h.tagName === 'H1' ? '' : (h.tagName === 'H2' ? '— ' : ' · '); const o = document.createElement('option'); o.value = h.id; o.textContent = indent + (h.textContent || '').trim().slice(0, 60); sel.appendChild(o); }); } buildJumpTo(); let tocTimer = null; doc.addEventListener('input', () => { clearTimeout(tocTimer); tocTimer = setTimeout(buildJumpTo, 1500); }); /* TOOLBAR WIRING */ document.querySelectorAll('.tb button, .findbar button').forEach(btn => { btn.addEventListener('mousedown', e => e.preventDefault()); }); let savedRange = null; function saveSelection(){ try{ const sel = window.getSelection(); if(sel && sel.rangeCount){ const r = sel.getRangeAt(0); if(doc.contains(r.commonAncestorContainer)) savedRange = r.cloneRange(); } }catch(e){} } function restoreSelection(){ if(!savedRange) return; try{ const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(savedRange); }catch(e){} } doc.addEventListener('mouseup', saveSelection); doc.addEventListener('keyup', saveSelection); document.querySelectorAll('.tb select, .tb input[type=color]').forEach(el => { el.addEventListener('mousedown', saveSelection); el.addEventListener('focus', saveSelection); }); function wrapWithRestore(handler){ return function(e){ restoreSelection(); handler(e); }; } document.getElementById('styleSelect').addEventListener('change', wrapWithRestore(applyBlockStyle)); document.getElementById('lineSpacing').addEventListener('change', wrapWithRestore(applyLineSpacing)); document.getElementById('borderSelect').addEventListener('change', wrapWithRestore(applyBorder)); document.getElementById('listStyleSelect').addEventListener('change', wrapWithRestore(applyListStyle)); document.getElementById('jumpTo').addEventListener('change', e => { const id = e.target.value; if(!id) return; const target = document.getElementById(id); if(target){ target.scrollIntoView({behavior:'smooth', block:'start'}); } e.target.selectedIndex = 0; }); document.getElementById('findInput').addEventListener('input', e => highlightAll(e.target.value)); document.getElementById('findInput').addEventListener('keydown', e => { if(e.key === 'Enter'){ e.preventDefault(); e.shiftKey ? findPrev() : findNext(); } if(e.key === 'Escape'){ closeFind(); } }); document.getElementById('replaceInput').addEventListener('keydown', e => { if(e.key === 'Escape') closeFind(); }); /* HIGHLIGHT toggle */ function selectionHasHighlight(){ try{ const v = document.queryCommandValue('hiliteColor') || document.queryCommandValue('backColor'); if(!v) return false; const trimmed = ('' + v).trim(); if(!trimmed || trimmed === 'transparent' || /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/.test(trimmed)) return false; const m = trimmed.match(/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); if(m && +m[1] >= 250 && +m[2] >= 250 && +m[3] >= 250) return false; return true; }catch(e){ return false; } } function unhighlight(){ try{ document.execCommand('hiliteColor', false, 'transparent'); const sel = window.getSelection(); if(sel && sel.rangeCount){ const range = sel.getRangeAt(0); const it = document.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT, { acceptNode: n => range.intersectsNode(n) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP }); let n; while((n = it.nextNode())){ if(n.style && n.style.backgroundColor) n.style.backgroundColor = ''; } } doc.focus(); markDirty(); }catch(e){ console.warn('unhighlight failed', e); } } function toggleHighlight(){ if(selectionHasHighlight()){ unhighlight(); } else { const c = document.getElementById('bgColor').value; cmd('hiliteColor', c); } } /* CONFIGURABLE SHORTCUTS */ const ACTIONS = [ {id:'save', label:'Save now', def:'ctrl+s', run: () => saveNow()}, {id:'find', label:'Find', def:'ctrl+f', run: () => openFind()}, {id:'replace', label:'Find & Replace', def:'ctrl+h', run: () => openReplace()}, {id:'highlight', label:'Toggle highlight on selection', def:'ctrl+shift+h', run: () => toggleHighlight()}, {id:'unhighlight', label:'Remove highlight', def:'ctrl+shift+u', run: () => unhighlight()}, {id:'bold', label:'Bold', def:'ctrl+b', run: () => cmd('bold')}, {id:'italic', label:'Italic', def:'ctrl+i', run: () => cmd('italic')}, {id:'underline', label:'Underline', def:'ctrl+u', run: () => cmd('underline')}, {id:'strike', label:'Strikethrough', def:'', run: () => cmd('strikeThrough')}, {id:'super', label:'Superscript', def:'ctrl+shift+=', run: () => cmd('superscript')}, {id:'sub', label:'Subscript', def:'ctrl+=', run: () => cmd('subscript')}, {id:'selectAll', label:'Select all (in document)', def:'ctrl+a', run: () => { const range = document.createRange(); range.selectNodeContents(doc); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); }}, {id:'indent', label:'Increase indent', def:'tab', run: () => { document.execCommand('indent'); markDirty(); }}, {id:'outdent', label:'Decrease indent', def:'shift+tab', run: () => { document.execCommand('outdent'); markDirty(); }}, {id:'undo', label:'Undo', def:'ctrl+z', run: () => cmd('undo')}, {id:'redo', label:'Redo', def:'ctrl+y', run: () => cmd('redo')} ]; let bindings = {}; function loadShortcuts(){ try{ const saved = JSON.parse(localStorage.getItem(SHORTCUT_KEY) || '{}'); bindings = {}; ACTIONS.forEach(a => { bindings[a.id] = saved[a.id] != null ? saved[a.id] : a.def; }); }catch(e){ bindings = Object.fromEntries(ACTIONS.map(a => [a.id, a.def])); } } function saveShortcuts(){ try{ localStorage.setItem(SHORTCUT_KEY, JSON.stringify(bindings)); }catch(e){ console.warn('save shortcuts failed', e); } } function resetShortcuts(){ if(!confirm('Reset all shortcuts to defaults?')) return; bindings = {}; ACTIONS.forEach(a => { bindings[a.id] = a.def; }); saveShortcuts(); renderShortcuts(); } function comboFromEvent(e){ if(e.key === 'Tab'){ return (e.shiftKey ? 'shift+' : '') + 'tab'; } if(e.key === 'Escape' || e.key === 'Control' || e.key === 'Shift' || e.key === 'Alt' || e.key === 'Meta') return null; const parts = []; if(e.ctrlKey || e.metaKey) parts.push('ctrl'); if(e.altKey) parts.push('alt'); if(e.shiftKey) parts.push('shift'); parts.push(e.key.toLowerCase()); return parts.join('+'); } loadShortcuts(); function openShortcuts(){ document.getElementById('shortcutsModal').classList.add('show'); renderShortcuts(); } function closeShortcuts(){ document.getElementById('shortcutsModal').classList.remove('show'); } function renderShortcuts(){ const tbl = document.getElementById('shortcutsTable'); tbl.innerHTML = 'ActionShortcut'; ACTIONS.forEach(a => { const tr = document.createElement('tr'); tr.innerHTML = ''+a.label+'' + '' + ''; tbl.appendChild(tr); }); tbl.querySelectorAll('.kb-btn').forEach(btn => { btn.onclick = () => { btn.textContent = '… press a key combo …'; btn.style.background = '#fff4e2'; const handler = (ev) => { ev.preventDefault(); const combo = comboFromEvent(ev); if(combo == null) return; const conflict = Object.entries(bindings).find(([k,v]) => v === combo && k !== btn.dataset.act); if(conflict){ const ok = confirm('That combo is already bound to "'+ACTIONS.find(a=>a.id===conflict[0]).label+'". Reassign anyway?'); if(!ok){ btn.textContent = bindings[btn.dataset.act] || '— click to set —'; btn.style.background='#fff'; window.removeEventListener('keydown', handler, true); return; } bindings[conflict[0]] = ''; } bindings[btn.dataset.act] = combo; saveShortcuts(); window.removeEventListener('keydown', handler, true); renderShortcuts(); }; window.addEventListener('keydown', handler, true); }; }); tbl.querySelectorAll('button[data-clear]').forEach(btn => { btn.onclick = () => { bindings[btn.dataset.clear] = ''; saveShortcuts(); renderShortcuts(); }; }); } document.addEventListener('keydown', e => { if(document.getElementById('shortcutsModal').classList.contains('show')) return; const combo = comboFromEvent(e); if(!combo) return; for(const a of ACTIONS){ if(bindings[a.id] && bindings[a.id] === combo){ if((a.id === 'indent' || a.id === 'outdent') && !doc.contains(document.activeElement)) return; if(a.id === 'selectAll' && !doc.contains(document.activeElement)) return; e.preventDefault(); try{ a.run(); }catch(err){ console.warn('shortcut '+a.id+' failed:', err); } return; } } }); /* PUBLIC */ window.cmd = cmd; window.changeCase = changeCase; window.applyShade = applyShade; window.unhighlight = unhighlight; window.toggleHighlight = toggleHighlight; window.openShortcuts = openShortcuts; window.closeShortcuts = closeShortcuts; window.resetShortcuts = resetShortcuts; window.insertHorizontalRule = insertHorizontalRule; window.insertPageBreak = insertPageBreak; window.insertImageFromFile = insertImageFromFile; window.openFind = openFind; window.openReplace = openReplace; window.closeFind = closeFind; window.findNext = findNext; window.findPrev = findPrev; window.replaceOne = replaceOne; window.replaceAll = replaceAll; window.resetDoc = resetDoc; window.downloadHTML = downloadHTML; })();