Forum Discussion
Crossword in Rise
I vibe coded a crossword puzzle interaction in the new Articulate Rise custom html block to support our Accounts Review training.
It took about an hour of back-and-forth with Copilot to get this working.
Check it out here
Full HTML code is below the preview, feel free to adapt it and repurpose for own projects.
19 Replies
This is so fun to see! Thanks for sharing. Love the idea.
FYI we've also added a "Copy code" button to code snippets (Block Library ->Multimedia -> Code Snippet) if that makes it easier for you to share examples with others!I'm so glad to see you shared this here danielbenton! Just a heads-up: we’ll be featuring this in an upcoming ELH Weekly newsletter. Be sure you’re subscribed if you want to get it in your inbox. 🎉
- EmilyFrisbyCommunity Member
I love this idea! Thanks for sharing Daniel 😀
- LisaAnderson-57Community Member
This is fantastic! Thank you for sharing.
Have you, or anyone for that matter, tested this for accessibility accommodations - ADA?
Thank you!
- danielbentonCommunity Member
Hi LisaAnderson-57
I have made a new version with improvements for accessibility accommodations - if you open it again and scroll down you can find the new version and full code.
- JanessaBaddeleyCommunity Member
This is a good idea! The code isn't working when I copy it into my course though.
- danielbentonCommunity Member
Thanks JanessaBaddeley - I think it was an error on my part when I pasted the code, I've redone it now and tested - it should work now if you give it a refresh?
- AG001Community Member
It works now Thanks.
- DavidAtkinson-dCommunity Member
This is very good, we are starting to see the potential of the new custom block / code feature in rise. As above comment the copied code fails - I am thinking there is a source image or similar linked
- danielbentonCommunity Member
Thanks DavidAtkinson-d - I think it was an error on my part when I pasted the code, I've redone it now and tested - it should work now if you give it a refresh?
- CandiceMCCommunity Member
This is awesome! It worked for me, spent a bit more time making minor adjustments to the code. And once you have it set up to your liking, it is pretty simple to update with for a weekly/monthly crossword challenge!
- EmiliaPietrz889Community Member
Hey, CandiceMC did you edit the code with AI or yourself? I really like how it worked out for you, but coding is totally not my thing - YET. Is it ok for me to ask if you would be able to catch up and briefly explain? Or give me some advice in the comment if it's ok.
- CandiceMCCommunity Member
EmiliaPietrz229 Used copilot. I gave it the initial code in the example and took multiple attempts to fix it the way I wanted. Once the basic coding was done, I was able to review it and figure out how to make my own edits to identify columns and rows to make edits for different version of the crossword puzzle. I am open for a quick chat to explain my process. cybersafemindset@gmail.com
- samlnolanCommunity Member
Such a cool concept! Can anyone help troubleshoot why the code is not working? I havent been able to find a solution
- danielbentonCommunity Member
Thanks samlnolan - I think it was an error on my part when I pasted the code, I've redone it now and tested - it should work now if you give it a refresh?
- samlnolanCommunity Member
It works now! Thank you so much
- CDawesCommunity Member
danielbenton Thank you for the idea and original code. I've been meaning to play around with coding with AI and this gave me the nudge to do it. The attached PDF shows all of the features and functionality in this new version. Just change the file extension from .txt to .html
- CDawesCommunity Member
I'm not sure why the txt file won't stay attached.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Crossword</title>
<style>
:root {
--gap: 2px;
--ok: #c8f7c5;
--bad: #f7c5c5;
--blank: #eee;
--brand: #0079bf;
--cell-size: 40px;
--highlight: #fff9b3;
--focus-ring: #0079bf;
}
body { font-family: Segoe UI, Arial, sans-serif; margin: 0; padding: 20px; }
.cw-wrap { display:flex; align-items:flex-start; gap:20px; }
.controls { display:flex; gap:10px; margin-bottom:15px; flex-wrap:wrap; }
.controls button { padding:10px 16px; background:var(--brand); color:#fff; border:none; border-radius:6px; cursor:pointer; font-size:14px; font-weight:500; min-height:44px; transition:background .2s; }
.controls button.secondary { background:#666; }
.controls button:hover { background:#006aa8; }
.controls button.secondary:hover { background:#555; }
.controls button:focus { outline:3px solid var(--focus-ring); outline-offset:2px; }
.grid { display:grid; gap:var(--gap); justify-content:center; grid-template-columns: repeat(var(--cols), var(--cell-size)); }
.cell { width:var(--cell-size); height:var(--cell-size); background:#fff; border:1px solid #c9c9c9; display:flex; align-items:center; justify-content:center; position:relative; }
.blank { background:var(--blank); border-color:#d9d9d9; }
.cell-number { position:absolute; top:2px; left:3px; font-size:10px; font-weight:bold; color:#333; pointer-events:none; }
.cell input { font-size:calc(var(--cell-size)*0.6); text-transform:uppercase; text-align:center; width:100%; height:100%; border:none; outline:none; background:transparent; -moz-appearance:textfield; }
.cell input:focus { outline:3px solid var(--focus-ring); outline-offset:-3px; z-index:10; }
.ok::after { content:"✔"; color:green; font-size:.8em; position:absolute; bottom:2px; right:2px; }
.bad::after { content:"✖"; color:red; font-size:.8em; position:absolute; bottom:2px; right:2px; }
.highlight { background-color:var(--highlight); }
.highlight-active { background-color:#ffd700; }
.clues { max-width:380px; }
.clue-section { margin-bottom:20px; }
.clue-heading { padding:8px; background:var(--brand); color:#fff; border-radius:6px; font-size:1.1rem; margin-bottom:8px; font-weight:bold; }
.clue-section p { padding:8px 10px; margin:6px 0; cursor:pointer; border-radius:4px; transition:background .2s; min-height:44px; display:flex; align-items:center; }
.clue-section p.active-clue { background:var(--highlight); border-left:4px solid var(--brand); }
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }.modal { display:none; position:fixed; z-index:1000; inset:0; background:rgba(0,0,0,.5); }
.modal.show { display:flex; align-items:center; justify-content:center; }
.modal-content { background:#fff; padding:30px; border-radius:12px; max-width:420px; text-align:center; box-shadow:0 4px 20px rgba(0,0,0,.3); }MeDia (max-width:800px) {
.cw-wrap { flex-direction:column; align-items:center; }
.board, .clues { width:100%; max-width:95vw; }
.controls button { flex:1 1 calc(50% - 5px); min-width:120px; }
}body.high-contrast { --ok:#90ee90; --bad:#ffb3b3; --blank:#ddd; --highlight:#ffff00; --focus-ring:#000; }
body.high-contrast .cell { border:2px solid #000; }
body.high-contrast .cell input { color:#000; font-weight:bold; }
</style>
</head>
<body>
<div class="cw-wrap">
<div class="board">
<div class="controls">
<button id="checkBtn" aria-label="Check all answers">Check Answers</button>
<button id="clearWordBtn" class="secondary" aria-label="Clear current word">Clear Word</button>
<button id="clearAllBtn" class="secondary" aria-label="Clear all answers">Clear All</button>
<button id="revealLetterBtn" aria-label="Reveal current letter">Reveal Letter</button>
<button id="revealWordBtn" aria-label="Reveal current word">Reveal Word</button>
<button id="contrastBtn" class="secondary" aria-label="Toggle high contrast mode">High Contrast</button>
</div>
<div id="grid" class="grid" aria-label="Crossword grid" role="grid"></div>
</div><div class="clues">
<div class="clue-section">
<div class="clue-heading">Across</div>
<div id="acrossClues"></div>
</div>
<div class="clue-section">
<div class="clue-heading">Down</div>
<div id="downClues"></div>
</div>
</div>
</div><div id="completionModal" class="modal" role="dialog" aria-labelledby="modalTitle" aria-modal="true">
<div class="modal-content">
<h2 id="modalTitle">🎉 Congratulations!</h2>
<p>You've completed the crossword correctly!</p>
<button onclick="closeModal()">Close</button>
</div>
</div><div id="announcements" class="sr-only" aria-live="polite" aria-atomic="true"></div>
<script>
/* ===== Editable Word List ===== */
const wordList = [
{word:"APPLE", clue:"A type of fruit that is usually red or green"},
{word:"TRIGGER", clue:"What action occurs? When does it happen?"},
{word:"MATTER", clue:"Subject ________ Expert"},
{word:"LEARNERS", clue:"Use materials"},
{word:"SCHEDULE", clue:"What we need to follow"},
{word:"NOVEMBER", clue:"Election month"},
{word:"TUESDAY", clue:"Election day"},
{word:"RECHARGE", clue:"Weekends"},
{word:"STORYLINE", clue:"Can be extended with Javascript"},
{word:"ONLINE", clue:"__________ Learning"}
];/* ===== Generator ===== */
function generateCrossword(list){
const words = list.map(w=>({...w, word:w.word.toUpperCase()})).sort((a,b)=>b.word.length-a.word.length);
const placed = [];
const grid = {};
const first = words[0]; const sr=10, sc=10;
placed.push({word:first.word, clue:first.clue, r:sr, c:sc, dir:"across"});
for(let i=0;i<first.word.length;i++){ grid[`${sr},${sc+i}`] = {letter:first.word[i], words:[0]}; }for(let w=1; w<words.length; w++){
const word = words[w].word, clue = words[w].clue;
let best=null, maxI=0;
for(let p=0;p<placed.length;p++){
const pw=placed[p];
for(const dir of ["across","down"]){
if(dir===pw.dir) continue;
for(let i=0;i<word.length;i++){
for(let j=0;j<pw.word.length;j++){
if(word[i]!==pw.word[j]) continue;
let r,c;
if(dir==="across"){ r = pw.dir==="across"? pw.r : pw.r + j; c = pw.dir==="across"? pw.c + j - i : pw.c - i; }
else { r = pw.dir==="across"? pw.r - i : pw.r + j - i; c = pw.dir==="across"? pw.c + j : pw.c; }
if(isValid(word,r,c,dir,grid)){
const inter = countInter(word,r,c,dir,grid);
if(inter>maxI){ maxI=inter; best={r,c,dir}; }
}
}
}
}
}
if(best){
placed.push({word, clue, r:best.r, c:best.c, dir:best.dir});
for(let i=0;i<word.length;i++){
const rr = best.dir==="across" ? best.r : best.r + i;
const cc = best.dir==="across" ? best.c + i : best.c;
const key = `${rr},${cc}`;
if(!grid[key]) grid[key] = {letter:word[i], words:[]};
grid[key].words.push(placed.length-1);
}
}
}// Normalize to start at (1,1)
let minR=Infinity, minC=Infinity;
for(const key in grid){ const [r,c]=key.split(',').map(Number); minR=Math.min(minR,r); minC=Math.min(minC,c); }
placed.forEach(w=>{ w.r = w.r - minR + 1; w.c = w.c - minC + 1; });// Assign clue numbers by start cell order
const starts = {};
placed.forEach((w,idx)=>{ const k=`${w.r},${w.c}`; if(!starts[k]) starts[k]=[]; starts[k].push({idx,dir:w.dir}); });
const startKeys = Object.keys(starts).sort((a,b)=>{
const [r1,c1]=a.split(',').map(Number); const [r2,c2]=b.split(',').map(Number);
return r1===r2 ? c1-c2 : r1-r2;
});
let num=1;
startKeys.forEach(k=>{ starts[k].forEach(({idx})=> placed[idx].num=num); num++; });return placed;
}
function isValid(word,r,c,dir,g){
for(let i=0;i<word.length;i++){
const rr = dir==="across"? r : r + i;
const cc = dir==="across"? c + i : c;
const key = `${rr},${cc}`;
if(g[key] && g[key].letter !== word[i]) return false;
if(!g[key]){
const checks = dir==="across" ? [[rr-1,cc],[rr+1,cc]] : [[rr,cc-1],[rr,cc+1]];
for(const [cr,cc2] of checks){ if(g[`${cr},${cc2}`]) return false; }
}
}
const before = dir==="across"? `${r},${c-1}` : `${r-1},${c}`;
const after = dir==="across"? `${r},${c+word.length}` : `${r+word.length},${c}`;
if(g[before] || g[after]) return false;
return true;
}
function countInter(word,r,c,dir,g){
let n=0;
for(let i=0;i<word.length;i++){
const rr = dir==="across"? r : r + i;
const cc = dir==="across"? c + i : c;
const key = `${rr},${cc}`;
if(g[key] && g[key].letter===word[i]) n++;
}
return n;
}/* ===== Render & Layout ===== */
const words = generateCrossword(wordList).sort((a,b)=>a.num-b.num);
let rows=0, cols=0;
words.forEach(({r,c,dir,word})=>{
const endR = dir==="across" ? r : r + word.length - 1;
const endC = dir==="across" ? c + word.length - 1 : c;
rows = Math.max(rows, endR);
cols = Math.max(cols, endC);
});
const BLANK = "#";
const SOL = Array.from({length: rows*cols}, ()=>BLANK);
const idx = (r,c)=>(r-1)*cols+(c-1);
function place(r,c,dir,word){
word = word.toUpperCase();
for(let i=0;i<word.length;i++){
const rr = dir==="across" ? r : r + i;
const cc = dir==="across" ? c + i : c;
SOL[idx(rr,cc)] = word[i];
}
}
words.forEach(w=>place(w.r,w.c,w.dir,w.word));// Auto-bumped storage version based on layout + list
function computeVersion(){
try{
const data = JSON.stringify({rows,cols,sol:SOL.join(''), list:wordList.map(w=>w.word).join('|')});
let h=0; for(let i=0;i<data.length;i++){ h=((h<<5)-h)+data.charCodeAt(i); h|=0; }
return 'v'+Math.abs(h);
}catch(e){ return 'v0'; }
}
const APP_VERSION = computeVersion();
const STORAGE_KEYS = { progress:`crossword-progress-${APP_VERSION}`, contrast:'highContrast' };const gridEl = document.getElementById('grid');
document.documentElement.style.setProperty('--cols', cols);
if(gridEl) gridEl.style.setProperty('--cols', cols);let currentDirection = "across";
let currentWord = null;
let autoCheckEnabled = false;
/* ----- Responsive sizing (mobile-friendly) ----- */
function updateCellSize() {
// Leave some room for controls/clues
const verticalPadding = 260; // header/controls/modal margin
const horizontalPadding = 60; // padding/margins
const maxCell = 56; // cap desktop
const minCell = 24; // cap mobile
const availH = Math.max(200, (window.innerHeight || document.documentElement.clientHeight) - verticalPadding);
const availW = Math.max(200, (window.innerWidth || document.documentElement.clientWidth) - horizontalPadding);
const hSize = Math.floor(availH / rows);
const wSize = Math.floor(availW / cols);
const size = Math.max(minCell, Math.min(maxCell, Math.min(hSize, wSize)));
document.documentElement.style.setProperty('--cell-size', size + 'px');
}
updateCellSize();
window.addEventListener('resize', updateCellSize, {passive:true});
window.addEventListener('orientationchange', updateCellSize);/* ===== Clues ===== */
function getCellNumbers(){
const nums={};
words.forEach(w=>{ const k=`${w.r},${w.c}`; if(!nums[k] || nums[k] > w.num) nums[k]=w.num; });
return nums;
}
function renderClues(){
const acrossEl = document.getElementById('acrossClues');
const downEl = document.getElementById('downClues');
acrossEl.innerHTML = ''; downEl.innerHTML = '';
const across = words.filter(w=>w.dir==='across').sort((a,b)=>a.num-b.num);
const down = words.filter(w=>w.dir==='down').sort((a,b)=>a.num-b.num);
for(const w of across){
const p=document.createElement('p'); p.id=`clue-${w.dir}-${w.num}`;
p.innerHTML = `<b>${w.num}</b> – ${w.clue}`; p.tabIndex=0;
p.addEventListener('click',()=>focusWord(w));
p.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); focusWord(w); } });
acrossEl.appendChild(p);
}
for(const w of down){
const p=document.createElement('p'); p.id=`clue-${w.dir}-${w.num}`;
p.innerHTML = `<b>${w.num}</b> – ${w.clue}`; p.tabIndex=0;
p.addEventListener('click',()=>focusWord(w));
p.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); focusWord(w); } });
downEl.appendChild(p);
}
}/* ===== Grid Render ===== */
function render(){
gridEl.innerHTML='';
const cellNumbers = getCellNumbers();
for(let r=1;r<=rows;r++){
for(let c=1;c<=cols;c++){
const val = SOL[idx(r,c)];
const cell = document.createElement('div');
cell.className='cell'; cell.dataset.row=r; cell.dataset.col=c;
if(val===BLANK){
cell.classList.add('blank');
}else{
const key=`${r},${c}`;
if(cellNumbers[key]){
const span=document.createElement('span'); span.className='cell-number'; span.textContent=cellNumbers[key];
cell.appendChild(span);
}
const input=document.createElement('input'); input.type='text'; input.maxLength=1;
input.setAttribute('aria-label',`Row ${r}, Column ${c}`);
input.addEventListener('input', e=>handleInput(e,cell,r,c));
input.addEventListener('keydown', e=>handleKeyDown(e,cell,r,c));
input.addEventListener('focus', ()=>highlightWord(input));
cell.dataset.answer = val;
cell.appendChild(input);
}
gridEl.appendChild(cell);
}
}
loadProgress();
}/* ===== Helpers ===== */
function getWordCells(w){
const arr=[];
for(let i=0;i<w.word.length;i++){
const r = w.dir==="across" ? w.r : w.r + i;
const c = w.dir==="across" ? w.c + i : w.c;
arr.push({row:r,col:c});
}
return arr;
}
function getFirstEmptyInputInWord(w){
for(const pos of getWordCells(w)){
const el = gridEl.children[idx(pos.row,pos.col)];
const i = el && el.querySelector('input');
if(i && !i.value) return i;
}
return null;
}
function getStrictOrder(){ // Across then Down by number
const across = words.filter(w=>w.dir==='across').sort((a,b)=>a.num-b.num);
const down = words.filter(w=>w.dir==='down').sort((a,b)=>a.num-b.num);
return [...across, ...down];
}/* ===== Highlight & Focus ===== */
function highlightWord(input){
document.querySelectorAll('.cell').forEach(c=>c.classList.remove('highlight','highlight-active'));
document.querySelectorAll('.clue-section p').forEach(p=>p.classList.remove('active-clue'));
if(!input) return;
const cell = input.closest('.cell'); if(!cell) return;
const row = +cell.dataset.row, col = +cell.dataset.col;
const w = words.find(w=> (w.dir===currentDirection &&
((w.dir==='across' && w.r===row && col>=w.c && col<w.c+w.word.length) ||
(w.dir==='down' && w.c===col && row>=w.r && row<w.r+w.word.length)))) ||
words.find(w=> ((w.dir==='across' && w.r===row && col>=w.c && col<w.c+w.word.length) ||
(w.dir==='down' && w.c===col && row>=w.r && row<w.r+w.word.length)));
if(w){
currentWord=w; currentDirection=w.dir;
for(const pos of getWordCells(w)){
const el = gridEl.children[idx(pos.row,pos.col)];
el.classList.add('highlight');
if(pos.row===row && pos.col===col) el.classList.add('highlight-active');
}
const clueEl = document.getElementById(`clue-${w.dir}-${w.num}`);
if(clueEl) clueEl.classList.add('active-clue');
announce(`${w.dir==='across'?'Across':'Down'} ${w.num}: ${w.clue}`);
}
}
function focusWord(w){
currentWord=w; currentDirection=w.dir;
const first = gridEl.children[idx(w.r,w.c)]; const input = first && first.querySelector('input');
if(input){ input.focus(); highlightWord(input); }
}/* ===== Input & Navigation (2ms autoskip, intelligent next-word, stop at end) ===== */
let saveTimer=null;
function throttledSaveProgress(){ clearTimeout(saveTimer); saveTimer=setTimeout(()=>{ saveProgress(); saveTimer=null; },500); }function handleInput(e,cell,r,c){
const t=e.target.value.toUpperCase(); e.target.value=t;
if(autoCheckEnabled) paint(cell,t);
if(t){
const next = findNextEmptyCellInCurrentWord(r,c);
if(next){ setTimeout(()=>next.focus(),2); }
else{
// Move to first empty cell of the next word in strict Across→Down order; stop at end
if(currentWord){
const order = getStrictOrder();
const curIdx = order.indexOf(currentWord);
for(let k=curIdx+1; k<order.length; k++){
const i = getFirstEmptyInputInWord(order[k]);
if(i){ currentWord=order[k]; currentDirection=order[k].dir; setTimeout(()=>i.focus(),2); break; }
}
}
}
}
throttledSaveProgress();
}
function findNextEmptyCellInCurrentWord(r,c){
if(!currentWord) return null;
const cells = getWordCells(currentWord);
const i = cells.findIndex(p=>p.row===r && p.col===c);
if(i>=0){
for(let k=i+1;k<cells.length;k++){
const el = gridEl.children[idx(cells[k].row,cells[k].col)];
const inp = el && el.querySelector('input');
if(inp && !inp.value) return inp;
}
}
return null;
}function handleKeyDown(e,cell,r,c){
const inputs = Array.from(document.querySelectorAll('.cell input'));
const currentIndex = inputs.indexOf(e.target);
let nextIndex=null;
switch(e.key){
case "ArrowRight": nextIndex=currentIndex+1; currentDirection="across"; break;
case "ArrowLeft": nextIndex=currentIndex-1; currentDirection="across"; break;
case "ArrowDown": nextIndex=currentIndex+cols; currentDirection="down"; break;
case "ArrowUp": nextIndex=currentIndex-cols; currentDirection="down"; break;
case "Tab": return;
case " ":
e.preventDefault(); toggleDirection(); highlightWord(e.target); return;
case "Backspace":
if(!e.target.value && currentWord){
e.preventDefault();
const cells=getWordCells(currentWord);
const i=cells.findIndex(p=>p.row===r && p.col===c);
if(i>0){
const prev = gridEl.children[idx(cells[i-1].row,cells[i-1].col)].querySelector('input');
if(prev){ prev.value=''; prev.focus(); if(autoCheckEnabled) paint(prev.closest('.cell'),''); throttledSaveProgress(); }
}
}else{ setTimeout(()=>throttledSaveProgress(),0); }
return;
case "Home":
e.preventDefault(); if(currentWord){ const first=gridEl.children[idx(currentWord.r,currentWord.c)].querySelector('input'); if(first) first.focus(); } return;
case "End":
e.preventDefault(); if(currentWord){ const lr=currentWord.dir==="across"? currentWord.r : currentWord.r+currentWord.word.length-1; const lc=currentWord.dir==="across"? currentWord.c+currentWord.word.length-1 : currentWord.c; const last=gridEl.children[idx(lr,lc)].querySelector('input'); if(last) last.focus(); } return;
}
if(nextIndex!==null && inputs[nextIndex]){ e.preventDefault(); inputs[nextIndex].focus(); }
}
function toggleDirection(){ currentDirection = currentDirection==="across" ? "down" : "across"; announce(`Direction: ${currentDirection}`); }/* ===== Checking & Modal ===== */
function paint(cell,val){
cell.classList.remove('ok','bad');
if(!val) return;
if(val===cell.dataset.answer){ cell.classList.add('ok'); announce('Correct'); }
else{ cell.classList.add('bad'); announce('Incorrect'); }
}
function checkAllAnswers(){
autoCheckEnabled = true;
const cells = gridEl.querySelectorAll('.cell:not(.blank)');
let allCorrect = true;
cells.forEach(cell=>{
const ans = cell.dataset.answer;
const input = cell.querySelector('input');
const val = (input && input.value ? input.value.toUpperCase() : "");
cell.classList.remove('ok','bad');
if(val === ans){
cell.classList.add('ok');
} else {
cell.classList.add('bad');
allCorrect = false;
}
});
if(allCorrect) showCompletionModal(); else announce("Some answers are incorrect. Incorrect cells are marked.");
}
function showCompletionModal(){ document.getElementById('completionModal').classList.add('show'); }
function closeModal(){ document.getElementById('completionModal').classList.remove('show'); }/* ===== Clear / Reveal ===== */
function clearCurrentWord(){
if(!currentWord){ announce("No word selected"); return; }
for(const pos of getWordCells(currentWord)){
const cell = gridEl.children[idx(pos.row,pos.col)];
const i = cell.querySelector('input');
if(i){ i.value=''; cell.classList.remove('ok','bad'); }
}
announce(`Cleared ${currentWord.dir} ${currentWord.num}`);
throttledSaveProgress();
}
function clearAll(){
// Clear everything without a confirm to avoid mobile popup issues
const inputs = gridEl.querySelectorAll('.cell input');
inputs.forEach(i=>{ i.value=''; i.closest('.cell').classList.remove('ok','bad'); });
announce("All answers cleared");
try{ localStorage.removeItem(STORAGE_KEYS.progress); }catch(e){}
}
function revealLetter(){
let target=document.activeElement;
if(!target || target.tagName!=='INPUT'){
const active=document.querySelector('.cell.highlight-active input');
if(active) target=active;
}
if(target){
const cell=target.closest('.cell');
if(cell && cell.dataset.answer){
target.value=cell.dataset.answer;
cell.classList.remove('bad'); cell.classList.add('ok');
announce(`Revealed letter: ${cell.dataset.answer}`);
throttledSaveProgress();
const r=+cell.dataset.row, c=+cell.dataset.col;
const next=findNextEmptyCellInCurrentWord(r,c);
if(next){ setTimeout(()=>next.focus(),2); }
else if(currentWord){
const order=getStrictOrder(); const curIdx=order.indexOf(currentWord);
for(let k=curIdx+1;k<order.length;k++){
const i=getFirstEmptyInputInWord(order[k]);
if(i){ currentWord=order[k]; currentDirection=order[k].dir; setTimeout(()=>i.focus(),2); break; }
}
}
}
} else { announce("Focus on a cell first"); }
}
function revealWord(){
if(!currentWord){ announce("No word selected"); return; }
for(const pos of getWordCells(currentWord)){
const cell = gridEl.children[idx(pos.row,pos.col)];
const i = cell.querySelector('input');
if(i){ i.value=cell.dataset.answer; cell.classList.remove('bad'); cell.classList.add('ok'); }
}
announce(`Revealed ${currentWord.dir} ${currentWord.num}: ${currentWord.word}`);
throttledSaveProgress();
// Move to first empty of next word (strict order); stop at end
const order=getStrictOrder(); const curIdx=order.indexOf(currentWord);
for(let k=curIdx+1;k<order.length;k++){
const i=getFirstEmptyInputInWord(order[k]);
if(i){ currentWord=order[k]; currentDirection=order[k].dir; setTimeout(()=>i.focus(),2); break; }
}
}/* ===== Persistence ===== */
function saveProgress(){
try{
const progress={};
const inputs=gridEl.querySelectorAll('.cell input');
inputs.forEach((i,k)=>{ if(i.value) progress[k]=i.value; });
localStorage.setItem(STORAGE_KEYS.progress, JSON.stringify(progress));
}catch(e){ console.log('Could not save progress', e); }
}
function loadProgress(){
try{
const data = localStorage.getItem(STORAGE_KEYS.progress);
if(data){
const progress=JSON.parse(data);
const inputs=gridEl.querySelectorAll('.cell input');
inputs.forEach((i,k)=>{ if(progress && progress[k]) i.value=progress[k]; });
}
if(localStorage.getItem('highContrast')==='true') document.body.classList.add('high-contrast');
}catch(e){ console.log('Could not load progress', e); }
}/* ===== High Contrast ===== */
function toggleHighContrast(){
document.body.classList.toggle('high-contrast');
try{ localStorage.setItem('highContrast', document.body.classList.contains('high-contrast') ? 'true' : 'false'); }catch(e){}
}/* ===== Announce ===== */
function announce(m){ document.getElementById('announcements').textContent = m; }/* ===== Wire up ===== */
document.getElementById('checkBtn').onclick = checkAllAnswers;
document.getElementById('clearWordBtn').onclick = clearCurrentWord;
document.getElementById('clearAllBtn').onclick = clearAll;
document.getElementById('revealLetterBtn').onclick = revealLetter;
document.getElementById('revealWordBtn').onclick = revealWord;
document.getElementById('contrastBtn').onclick = toggleHighContrast;
window.onclick = e => { const m=document.getElementById('completionModal'); if(e.target===m) closeModal(); };/* ===== Init ===== */
renderClues();
render();
announce('Crossword ready — '+APP_VERSION);
</script>
</body>
</html>
- TaraPCommunity Member
I absolutely love this! Thank you!
Related Content
- 4 months ago
- 2 months ago
- 10 months ago
- 4 months ago