mirror of
https://github.com/kennethreitz/rhymepad.org.git
synced 2026-06-11 17:08:33 +00:00
Sidebar rebuilt as a word page; phrase lookups
One lookup renders the whole entry — phonetic readout as a plain header (card chrome and dismiss gone), describes, rhymes by syllable (+near), synonyms always fully expanded — with collapsed sections behind "more…", a clickable history trail, and lookups firing as you type (Go button and mode seg removed). Multi-word lookups work: phrases read straight through for the phonetic readout, rhyme on their final word (labeled), and hit WordNet compounds for synonyms (new york -> Empire State). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1209,9 +1209,9 @@ def synonyms_for(w: str, limit: int) -> list[dict]:
|
||||
broader terms, and related words. Input is lemmatized first so
|
||||
'keys' and 'feeling' resolve to 'key' and 'feel'."""
|
||||
wn = get_wordnet()
|
||||
base = w
|
||||
base = w.replace(" ", "_")
|
||||
for pos in ("n", "v", "a", "r"):
|
||||
m = wn.morphy(w, pos)
|
||||
m = wn.morphy(base, pos)
|
||||
if m:
|
||||
base = m
|
||||
break
|
||||
@@ -1298,9 +1298,13 @@ def get_homophones(w: str, phones: str) -> list[str]:
|
||||
|
||||
@app.get("/api/word")
|
||||
def word_info(word: str):
|
||||
"""Phonetic anatomy of a word: phones, syllables, stress, rime."""
|
||||
w = word.strip().lower()[:64]
|
||||
phones = phones_for(w)
|
||||
"""Phonetic anatomy of a word — or a phrase, read straight through."""
|
||||
w = " ".join(word.strip().lower().split()[:4])[:64]
|
||||
if " " in w:
|
||||
parts = [phones_for(p) for p in w.split()]
|
||||
phones = " ".join(p for p in parts if p) if all(parts) else None
|
||||
else:
|
||||
phones = phones_for(w)
|
||||
if not phones:
|
||||
return {"word": w, "known": False}
|
||||
pl = phones.split()
|
||||
@@ -1310,9 +1314,9 @@ def word_info(word: str):
|
||||
senses = None
|
||||
try:
|
||||
wn = get_wordnet()
|
||||
base = w
|
||||
base = w.replace(" ", "_")
|
||||
for pos in ("n", "v", "a", "r"):
|
||||
m = wn.morphy(w, pos)
|
||||
m = wn.morphy(base, pos)
|
||||
if m:
|
||||
base = m
|
||||
break
|
||||
@@ -1322,7 +1326,7 @@ def word_info(word: str):
|
||||
return {"word": w, "known": True,
|
||||
"phones": DIGITS.sub("", phones), "syl": len(stress),
|
||||
"stress": stress, "rime": rime, "senses": senses,
|
||||
"homophones": get_homophones(w, phones),
|
||||
"homophones": [] if " " in w else get_homophones(w, phones),
|
||||
"zipf": round(zipf_frequency(w, "en"), 1)}
|
||||
|
||||
|
||||
@@ -1330,6 +1334,10 @@ def word_info(word: str):
|
||||
def lookup(word: str, mode: str = "rhyme", limit: int = 60):
|
||||
w = word.strip().lower()[:64]
|
||||
limit = min(limit, 200)
|
||||
rhyme_on = None
|
||||
if " " in w and mode != "syn":
|
||||
rhyme_on = w.split()[-1] # a phrase rhymes on its final word
|
||||
w = rhyme_on
|
||||
if mode == "syn":
|
||||
sections = synonyms_for(w, limit)
|
||||
return {"word": w, "mode": mode, "known": bool(sections),
|
||||
@@ -1346,7 +1354,8 @@ def lookup(word: str, mode: str = "rhyme", limit: int = 60):
|
||||
# rhyme mode carries both: perfect in "words", slant in "near"
|
||||
words = _ranked(perfect, {w}, limit)
|
||||
near = _ranked(near_cands, {w}, limit // 2)
|
||||
return {"word": w, "mode": mode, "known": True, "words": words, "near": near}
|
||||
return {"word": w, "mode": mode, "known": True, "words": words,
|
||||
"near": near, "rhyme_on": rhyme_on}
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory=Path(__file__).parent / "static",
|
||||
|
||||
+120
-163
@@ -187,6 +187,11 @@
|
||||
.tab:hover { color: var(--ink); }
|
||||
.panel-body { padding: 16px; overflow-y: auto; flex: 1; }
|
||||
.lookup-row { display: flex; gap: 8px; }
|
||||
#histBar { color: var(--ink-dim); font-size: 12px; margin: 8px 2px 0; min-height: 0; }
|
||||
#histBar .hist { cursor: pointer; }
|
||||
#histBar .hist:hover { color: var(--accent); }
|
||||
.more { color: var(--accent); cursor: pointer; font-size: 11px; letter-spacing: 0; text-transform: lowercase; }
|
||||
.res-label.sub { color: #6a5f52; margin: 8px 0 2px; }
|
||||
.lookup-row input {
|
||||
flex: 1; font-family: inherit; font-size: 14px;
|
||||
background: var(--panel-2); border: 1px solid var(--line);
|
||||
@@ -201,14 +206,8 @@
|
||||
}
|
||||
.seg button.active { color: var(--accent); border-color: var(--accent); }
|
||||
.results { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 6px; }
|
||||
.defcard {
|
||||
background: var(--panel-2); border: 1px solid var(--line);
|
||||
border-radius: 10px; padding: 12px 14px; margin: 10px 0 4px;
|
||||
}
|
||||
.defhead { display: flex; align-items: baseline; gap: 8px; font-size: 14px; }
|
||||
.defx { margin-left: auto; cursor: pointer; color: var(--ink-dim); font-size: 15px; }
|
||||
.defx:hover { color: var(--r6); }
|
||||
.defphon { font-size: 12px; margin: 8px 0 10px; line-height: 1.6; }
|
||||
.defhead { margin-top: 14px; font-size: 15px; }
|
||||
.defphon { font-size: 12px; margin: 4px 0 2px; line-height: 1.6; }
|
||||
.defphon i { color: var(--accent-2); font-style: normal; }
|
||||
.chip {
|
||||
font-size: 13px; background: var(--panel-2); border: 1px solid var(--line);
|
||||
@@ -304,17 +303,12 @@ Double-click any word to look it up on the right."></textarea>
|
||||
|
||||
<div class="panel-body" id="tab-lookup">
|
||||
<div class="lookup-row">
|
||||
<input id="lookupInput" placeholder="a word…" autocomplete="off">
|
||||
<button class="btn" id="lookupBtn">Go</button>
|
||||
</div>
|
||||
<div class="seg" id="modeSeg">
|
||||
<button class="active" data-mode="desc" title="Adjectives that describe it">Describes</button>
|
||||
<button data-mode="rhyme">Rhymes</button>
|
||||
<button data-mode="syn">Synonyms</button>
|
||||
<input id="lookupInput" placeholder="look up a word…" autocomplete="off">
|
||||
</div>
|
||||
<div id="histBar"></div>
|
||||
<div id="defBox"></div>
|
||||
<div id="lookupResults">
|
||||
<p class="muted">Type a word and hit Go, or double-click a word in your draft. A phonetic readout pins on top; the buttons switch rhymes, synonyms, and describing words. Tip: end a word with “?” to jump to synonyms.</p>
|
||||
<p class="muted">Type a word — or double-click one in your draft — and its whole entry appears: how it sounds, what describes it, what rhymes, what could replace it.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -697,9 +691,8 @@ function lookupSelection(){
|
||||
const sel = editor.value.slice(editor.selectionStart, editor.selectionEnd)
|
||||
.trim().replace(/[^A-Za-z']/g,'');
|
||||
if(sel && !sel.includes(' ')){
|
||||
document.getElementById('lookupInput').value = sel;
|
||||
tab('lookup');
|
||||
setMode('desc');
|
||||
lookupFor(sel);
|
||||
}
|
||||
}
|
||||
editor.addEventListener('dblclick', lookupSelection);
|
||||
@@ -713,97 +706,79 @@ if(window.matchMedia('(pointer: coarse)').matches){
|
||||
|
||||
/* ---------- insert at cursor ---------- */
|
||||
/* ============================================================
|
||||
LOOKUP — rhymes & near rhymes come from our backend (CMU dict,
|
||||
frequency-ranked). Synonyms come from the free Datamuse API.
|
||||
WORD ENTRIES — one lookup renders the whole entry: phonetic
|
||||
readout, describes, rhymes (+near), synonyms. Rhymes & sounds
|
||||
come from our engine; describes comes from Datamuse.
|
||||
============================================================ */
|
||||
const modeSeg = document.getElementById('modeSeg');
|
||||
let mode = 'desc';
|
||||
function setMode(m){
|
||||
mode = m;
|
||||
[...modeSeg.children].forEach(c=>c.classList.toggle('active', c.dataset.mode === m));
|
||||
const lookupInput = document.getElementById('lookupInput');
|
||||
const resultsBox = document.getElementById('lookupResults');
|
||||
const histBar = document.getElementById('histBar');
|
||||
const defBox = document.getElementById('defBox');
|
||||
let curWord = null, lookupSeq = 0, entry = null;
|
||||
const wordHistory = [];
|
||||
|
||||
lookupInput.addEventListener('focus', ()=> lookupInput.select());
|
||||
lookupInput.addEventListener('keydown', e=>{ if(e.key === 'Enter') doLookup(); });
|
||||
lookupInput.addEventListener('input', debounce(()=>{
|
||||
const w = lookupInput.value.trim();
|
||||
if(w.length >= 2 && /^[A-Za-z' -]+\??$/.test(w)) doLookup();
|
||||
}, 400));
|
||||
|
||||
function lookupFor(word){
|
||||
lookupInput.value = word;
|
||||
doLookup();
|
||||
}
|
||||
modeSeg.addEventListener('click', e=>{
|
||||
const b = e.target.closest('button'); if(!b) return;
|
||||
setMode(b.dataset.mode);
|
||||
});
|
||||
document.getElementById('lookupBtn').addEventListener('click', doLookup);
|
||||
document.getElementById('lookupInput').addEventListener('keydown', e=>{ if(e.key==='Enter') doLookup(); });
|
||||
|
||||
const lookupInput = document.getElementById('lookupInput');
|
||||
lookupInput.addEventListener('focus', ()=> lookupInput.select());
|
||||
const resultsBox = document.getElementById('lookupResults');
|
||||
function renderHistory(){
|
||||
const past = wordHistory.filter(w=>w !== curWord).slice(-5).reverse();
|
||||
histBar.innerHTML = past.length
|
||||
? '↩ ' + past.map(w=>`<span class="hist" data-w="${esc(w)}">${esc(w)}</span>`).join(' · ')
|
||||
: '';
|
||||
histBar.querySelectorAll('.hist').forEach(el=>
|
||||
el.addEventListener('click', ()=> lookupFor(el.dataset.w)));
|
||||
}
|
||||
|
||||
async function doLookup(){
|
||||
let word = document.getElementById('lookupInput').value.trim();
|
||||
// RhymeZone-style: a trailing ? jumps to synonyms
|
||||
if(word.endsWith('?')){
|
||||
word = word.slice(0, -1).trim();
|
||||
document.getElementById('lookupInput').value = word;
|
||||
if(word) setMode('syn');
|
||||
return;
|
||||
}
|
||||
if(!word) return;
|
||||
const word = lookupInput.value.trim().replace(/\?$/, '').toLowerCase();
|
||||
if(!word || word === curWord) return;
|
||||
document.getElementById('tab-lookup').scrollTop = 0;
|
||||
// the card belongs to the word, not the submode: rebuild only when
|
||||
// the word changes, and respect a dismissal until then
|
||||
if(word.toLowerCase() !== defWord){
|
||||
defWord = word.toLowerCase();
|
||||
defDismissed = false;
|
||||
showDefinition(word);
|
||||
if(curWord){
|
||||
const i = wordHistory.indexOf(curWord);
|
||||
if(i >= 0) wordHistory.splice(i, 1);
|
||||
wordHistory.push(curWord);
|
||||
if(wordHistory.length > 8) wordHistory.shift();
|
||||
}
|
||||
resultsBox.innerHTML = '<p class="muted">Searching…</p>';
|
||||
try{
|
||||
const r = await fetch(`/api/lookup?word=${encodeURIComponent(word)}&mode=${mode}`);
|
||||
const data = await r.json();
|
||||
if(mode === 'desc'){
|
||||
renderDescribes(word);
|
||||
return;
|
||||
}
|
||||
if(mode === 'syn'){
|
||||
if(!data.known){
|
||||
resultsBox.innerHTML = `<p class="muted">No synonyms found for “${esc(word)}”.</p>`;
|
||||
return;
|
||||
}
|
||||
renderSections(word, data.sections);
|
||||
curWord = word;
|
||||
renderHistory();
|
||||
const seq = ++lookupSeq;
|
||||
defBox.innerHTML = `<div class="defhead"><b>${esc(word)}</b></div>` +
|
||||
`<div class="muted defphon" id="defPhon">…</div>`;
|
||||
resultsBox.innerHTML = '<p class="muted">gathering…</p>';
|
||||
const [info, rhyme, syn, desc] = await Promise.all([
|
||||
fetch(`/api/word?word=${encodeURIComponent(word)}`).then(r=>r.json()).catch(()=>null),
|
||||
fetch(`/api/lookup?word=${encodeURIComponent(word)}&mode=rhyme`).then(r=>r.json()).catch(()=>null),
|
||||
fetch(`/api/lookup?word=${encodeURIComponent(word)}&mode=syn`).then(r=>r.json()).catch(()=>null),
|
||||
fetch(`https://api.datamuse.com/words?rel_jjb=${encodeURIComponent(word)}&max=40`).then(r=>r.json()).catch(()=>null),
|
||||
]);
|
||||
if(seq !== lookupSeq) return;
|
||||
const el = defBox.querySelector('#defPhon');
|
||||
if(el){
|
||||
if(info && info.known){
|
||||
const dots = [...info.stress].map(s=>s==='1' ? '●' : '○').join('');
|
||||
const mates = draftMates(word);
|
||||
el.innerHTML = `/${esc(info.phones.toLowerCase())}/ · ${info.syl} syl ${dots}` +
|
||||
` · rhymes on <i>${esc(info.rime.toLowerCase())}</i>` +
|
||||
(info.senses >= 3 ? ` · ${info.senses} senses` : '') +
|
||||
((info.homophones || []).length ? `<br>sounds like: ${info.homophones.map(esc).join(', ')}` : '') +
|
||||
(mates.length ? `<br>in your draft: ${mates.map(esc).join(', ')}` : '');
|
||||
}else{
|
||||
if(!data.known){
|
||||
resultsBox.innerHTML = `<p class="muted">“${esc(word)}” isn't in the pronunciation dictionary.</p>`;
|
||||
return;
|
||||
}
|
||||
renderRhymes(word, data);
|
||||
el.textContent = 'not in the pronunciation dictionary';
|
||||
}
|
||||
}catch(err){
|
||||
resultsBox.innerHTML = `<p class="muted">Lookup failed — is the backend running? (${esc(String(err))})</p>`;
|
||||
}
|
||||
entry = {word, rhyme, syn, desc, expanded: {}};
|
||||
paintSections();
|
||||
}
|
||||
|
||||
function rarity(z){
|
||||
if(z == null) return '';
|
||||
return z >= 4.6 ? ' common' : (z < 3.4 ? ' rare' : '');
|
||||
}
|
||||
function chipHtml(items, cls){
|
||||
// items: strings or {word, z}; words already in the draft get a tick
|
||||
const inDraft = new Set((editor.value.toLowerCase().match(/[a-z']+/g)) || []);
|
||||
return '<div class="results">' +
|
||||
items.map(d=>{
|
||||
const w = d.word || d, r = rarity(d.z);
|
||||
const used = inDraft.has(w.toLowerCase()) ? ' indraft' : '';
|
||||
return `<span class="chip${cls ? ' ' + cls : ''}${r}${used}" data-w="${esc(w)}">${esc(w)}</span>`;
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
function wireChips(){
|
||||
// clicking a result navigates to that word's dictionary entry
|
||||
resultsBox.querySelectorAll('.chip').forEach(c=>{
|
||||
c.addEventListener('click', ()=>{
|
||||
document.getElementById('lookupInput').value = c.dataset.w;
|
||||
doLookup();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- word card: the engine's own read on a word ---------- */
|
||||
const defBox = document.getElementById('defBox');
|
||||
let defWord = null, defDismissed = false;
|
||||
function draftMates(word){
|
||||
if(!analysis) return [];
|
||||
const lw = word.toLowerCase();
|
||||
@@ -820,80 +795,62 @@ function draftMates(word){
|
||||
return [...mates].slice(0, 8);
|
||||
}
|
||||
|
||||
async function showDefinition(word){
|
||||
defBox.innerHTML = `<div class="defcard"><div class="defhead"><b>${esc(word)}</b>` +
|
||||
`<span class="defx" title="Dismiss">×</span></div>` +
|
||||
`<div class="muted defphon" id="defPhon"></div></div>`;
|
||||
defBox.querySelector('.defx').addEventListener('click', ()=>{ defBox.innerHTML=''; defDismissed = true; });
|
||||
try{
|
||||
const info = await (await fetch(`/api/word?word=${encodeURIComponent(word)}`)).json();
|
||||
const el = defBox.querySelector('#defPhon');
|
||||
if(!el) return;
|
||||
if(!info.known){ el.textContent = "not in the pronunciation dictionary"; return; }
|
||||
const dots = [...info.stress].map(s=>s==='1' ? '●' : '○').join('');
|
||||
const mates = draftMates(word);
|
||||
el.innerHTML = `/${esc(info.phones.toLowerCase())}/ · ${info.syl} syl ${dots}` +
|
||||
` · rhymes on <i>${esc(info.rime.toLowerCase())}</i>` +
|
||||
(info.senses >= 3 ? ` · ${info.senses} senses` : '') +
|
||||
((info.homophones || []).length ? `<br>sounds like: ${info.homophones.map(esc).join(', ')}` : '') +
|
||||
(mates.length ? `<br>in your draft: ${mates.map(esc).join(', ')}` : '');
|
||||
}catch(e){}
|
||||
function rarity(z){
|
||||
if(z == null) return '';
|
||||
return z >= 4.6 ? ' common' : (z < 3.4 ? ' rare' : '');
|
||||
}
|
||||
async function renderDescribes(word){
|
||||
resultsBox.innerHTML = '<p class="muted">Searching…</p>';
|
||||
try{
|
||||
const r = await fetch(`https://api.datamuse.com/words?rel_jjb=${encodeURIComponent(word)}&max=60`);
|
||||
const data = await r.json();
|
||||
const words = data.map(d=>d.word);
|
||||
if(!words.length){
|
||||
resultsBox.innerHTML = `<p class="muted">No describing words for “${esc(word)}”.</p>`;
|
||||
return;
|
||||
}
|
||||
resultsBox.innerHTML = `<div class="res-label">words that describe “${esc(word)}”</div>` + chipHtml(words);
|
||||
wireChips();
|
||||
}catch(e){
|
||||
resultsBox.innerHTML = '<p class="muted">Couldn\'t reach the describing-words service.</p>';
|
||||
}
|
||||
function chipHtml(items, cls){
|
||||
// items: strings or {word, z}; words already in the draft get a tick
|
||||
const inDraft = new Set((editor.value.toLowerCase().match(/[a-z']+/g)) || []);
|
||||
return '<div class="results">' +
|
||||
items.map(d=>{
|
||||
const w = d.word || d, r = rarity(d.z);
|
||||
const used = inDraft.has(w.toLowerCase()) ? ' indraft' : '';
|
||||
return `<span class="chip${cls ? ' ' + cls : ''}${r}${used}" data-w="${esc(w)}">${esc(w)}</span>`;
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
function renderChips(label, words){
|
||||
if(!words.length){ resultsBox.innerHTML = '<p class="muted">No results.</p>'; return; }
|
||||
resultsBox.innerHTML = `<div class="res-label">${label}</div>` + chipHtml(words.slice(0,50));
|
||||
wireChips();
|
||||
}
|
||||
function renderSections(word, sections){
|
||||
if(!sections.length){ resultsBox.innerHTML = `<p class="muted">No synonyms for “${esc(word)}”.</p>`; return; }
|
||||
let h = '';
|
||||
sections.forEach(s=>{
|
||||
h += `<div class="res-label">${esc(s.label)}</div>` +
|
||||
chipHtml(s.words.map(d=>d.word), s.label === 'synonyms' ? '' : 'near');
|
||||
});
|
||||
resultsBox.innerHTML = h;
|
||||
wireChips();
|
||||
function secLabel(id, label, truncated){
|
||||
return `<div class="res-label">${label}` +
|
||||
(truncated && !entry.expanded[id] ? ` <span class="more" data-sec="${id}">more…</span>` : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function syllableSections(items, cls){
|
||||
const bySyl = {};
|
||||
items.forEach(d=>{ (bySyl[d.syl || 0] ||= []).push(d); });
|
||||
return Object.keys(bySyl).sort((a,b)=>a-b).map(k=>
|
||||
`<div class="res-label">${k == 0 ? '?' : k} syllable${k == 1 ? '' : 's'}</div>` + chipHtml(bySyl[k], cls)
|
||||
).join('');
|
||||
}
|
||||
function renderRhymes(word, data){
|
||||
const near = data.near || [];
|
||||
if(!data.words.length && !near.length){
|
||||
resultsBox.innerHTML = `<p class="muted">No rhymes for “${esc(word)}”.</p>`;
|
||||
return;
|
||||
}
|
||||
function paintSections(){
|
||||
const e = entry;
|
||||
if(!e) return;
|
||||
let h = '';
|
||||
if(data.words.length){
|
||||
h += `<div class="res-label">Rhymes for “${esc(word)}”</div>` + syllableSections(data.words);
|
||||
if(e.desc && e.desc.length){
|
||||
const items = e.desc.map(d=>d.word);
|
||||
const lim = e.expanded.desc ? items.length : 12;
|
||||
h += secLabel('desc', 'describes', items.length > 12) + chipHtml(items.slice(0, lim));
|
||||
}
|
||||
if(near.length){
|
||||
h += `<div class="res-label">Near rhymes</div>` + chipHtml(near.map(d=>d.word), 'near');
|
||||
if(e.rhyme && e.rhyme.known && (e.rhyme.words.length || (e.rhyme.near || []).length)){
|
||||
const lim = e.expanded.rhyme ? 999 : 8;
|
||||
const bySyl = {};
|
||||
e.rhyme.words.forEach(d=>{ (bySyl[d.syl || 0] ||= []).push(d); });
|
||||
const truncated = e.rhyme.words.length > 16 || (e.rhyme.near || []).length > 8;
|
||||
const onWord = e.rhyme.rhyme_on ? ` — on “${esc(e.rhyme.rhyme_on)}”` : '';
|
||||
h += secLabel('rhyme', 'rhymes' + onWord, truncated);
|
||||
Object.keys(bySyl).sort((a,b)=>a-b).forEach(k=>{
|
||||
h += `<div class="res-label sub">${k == 0 ? '?' : k} syl</div>` + chipHtml(bySyl[k].slice(0, lim));
|
||||
});
|
||||
const near = (e.rhyme.near || []).slice(0, e.expanded.rhyme ? 999 : 8);
|
||||
if(near.length) h += `<div class="res-label sub">near</div>` + chipHtml(near, 'near');
|
||||
}
|
||||
resultsBox.innerHTML = h;
|
||||
wireChips();
|
||||
if(e.syn && e.syn.known && e.syn.sections.length){
|
||||
// bottom section: always fully expanded — nothing below to crowd
|
||||
h += secLabel('syn', 'synonyms', false);
|
||||
e.syn.sections.forEach(s=>{
|
||||
if(s.label !== 'synonyms') h += `<div class="res-label sub">${esc(s.label)}</div>`;
|
||||
h += chipHtml(s.words.map(d=>d.word), s.label === 'synonyms' ? '' : 'near');
|
||||
});
|
||||
}
|
||||
resultsBox.innerHTML = h || `<p class="muted">nothing found for “${esc(e.word)}”.</p>`;
|
||||
resultsBox.querySelectorAll('.chip').forEach(c=>
|
||||
c.addEventListener('click', ()=> lookupFor(c.dataset.w)));
|
||||
resultsBox.querySelectorAll('.more').forEach(m=>
|
||||
m.addEventListener('click', ()=>{ entry.expanded[m.dataset.sec] = true; paintSections(); }));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
Reference in New Issue
Block a user