Forum Discussion

danielbenton's avatar
danielbenton
Community Member
2 months ago

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

https://rise.articulate.com/share/OHzJApuSIhFcNe4GLwmto58-5dg_-j-C#/lessons/3cT6ydJmoggnBlDSVsXmKaxp11ASrlKp

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. 🎉

  • This is fantastic! Thank you for sharing.

    Have you, or anyone for that matter, tested this for accessibility accommodations - ADA?

    Thank you!

    • danielbenton's avatar
      danielbenton
      Community 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. 



  • This is a good idea! The code isn't working when I copy it into my course though. 

    • danielbenton's avatar
      danielbenton
      Community 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? 

  • 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 

    • danielbenton's avatar
      danielbenton
      Community 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? 

  • CandiceMC's avatar
    CandiceMC
    Community 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!  

     

    • EmiliaPietrz889's avatar
      EmiliaPietrz889
      Community 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.

      • CandiceMC's avatar
        CandiceMC
        Community 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

  • samlnolan's avatar
    samlnolan
    Community Member

    Such a cool concept! Can anyone help troubleshoot why the code is not working? I havent been able to find a solution

     

    • danielbenton's avatar
      danielbenton
      Community 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? 

  • CDawes's avatar
    CDawes
    Community 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

    • CDawes's avatar
      CDawes
      Community 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>

  • TaraP's avatar
    TaraP
    Community Member

    I absolutely love this! Thank you!