From 14a54dfa8bf4a76088f1918238471ab85511b1a5 Mon Sep 17 00:00:00 2001 From: Ross Date: Tue, 11 Nov 2025 12:49:13 +0000 Subject: [PATCH] Refactor answer click handling to prevent duplicate event listeners on the new mark2 page; streamline state management for answer marking. --- anatomy/static/js/anatomy.js | 31 ++++--- anatomy/templates/anatomy/mark2.html | 91 +++++++++++++------ .../partials/mark2_marked_list_fragment.html | 42 +++++---- static/js/anatomy.js | 30 +++--- 4 files changed, 121 insertions(+), 73 deletions(-) diff --git a/anatomy/static/js/anatomy.js b/anatomy/static/js/anatomy.js index 056cb021..fd9203af 100644 --- a/anatomy/static/js/anatomy.js +++ b/anatomy/static/js/anatomy.js @@ -44,24 +44,27 @@ $(document).ready(function () { // $(element).append($("G")); //}); - $(".answer-list span.answer").each(function (index, element) { - console.log(element); + // Legacy per-element click handler: skip on the new mark2 page which + // manages clicks via a delegated handler (it uses #answer-list). This + // prevents duplicate/colliding handlers when mark2 is rendered. + if (!document.getElementById('answer-list')) { + $(".answer-list span.answer").each(function (index, element) { + console.log(element); - $(element).click(function (e) { + $(element).click(function (e) { + + var classes = ['answer correct', 'answer half-correct', 'answer incorrect']; + $(element).each(function () { + let mark = classes[($.inArray(this.className, classes) + 1) % classes.length]; + this.className = mark + this.dataset.newmark = mark.split(" ")[1] + }); + + prepAnswerData(); - var classes = ['answer correct', 'answer half-correct', 'answer incorrect']; - $(element).each(function () { - let mark = classes[($.inArray(this.className, classes) + 1) % classes.length]; - this.className = mark - this.dataset.newmark = mark.split(" ")[1] }); - - prepAnswerData(); - - - }); - }); + } prepAnswerData(); if ($(".post-form").length > 0) { diff --git a/anatomy/templates/anatomy/mark2.html b/anatomy/templates/anatomy/mark2.html index d342838f..3ebaf839 100644 --- a/anatomy/templates/anatomy/mark2.html +++ b/anatomy/templates/anatomy/mark2.html @@ -114,6 +114,8 @@ .answer-actions .action-icon { margin-left: 0.5rem; color: #6c757d; text-decoration: none; border: none; background: none; } .answer-actions .action-icon:hover { color: #000; text-decoration: none; } .answer-actions .copy-to-clipboard { cursor: pointer; } + /* Active state for numeric mark buttons */ + .mark-btn.active { background-color: #0d6efd; color: #fff; border-color: #0d6efd; } {% endblock %} @@ -139,42 +141,41 @@ // Delegated click handler for answers — only updates local state. document.addEventListener('click', function(e){ - const el = e.target.closest('span.answer'); - if (!el) return; + // Ignore clicks on action icons or mark controls + if (e.target.closest('.answer-actions') || e.target.closest('.mark-controls') || e.target.closest('.copy-to-clipboard')) return; - // cycle states: not-marked -> correct -> half-correct -> incorrect -> not-marked + // Prefer a clicked span.answer if present + let spanEl = e.target.closest('span.answer'); + // Otherwise fall back to the list-item's span + if (!spanEl) { + const li = e.target.closest('#answer-list li'); + if (!li) return; + spanEl = li.querySelector('span.answer'); + if (!spanEl) return; + } + + // Ensure span has an explicit state class + if (!spanEl.classList.contains('correct') && !spanEl.classList.contains('half-correct') && !spanEl.classList.contains('incorrect') && !spanEl.classList.contains('not-marked')) { + spanEl.classList.add('not-marked'); + } + + // cycle states: not-marked -> correct -> half-correct -> incorrect -> not-marked const states = ['not-marked', 'correct', 'half-correct', 'incorrect']; - // current may be 'answer correct' or just 'answer' - let parts = el.className.split(/\s+/).filter(Boolean); - // find current semantic state let currentState = 'not-marked'; - for (let p of parts) { + for (let p of spanEl.classList) { if (states.includes(p)) { currentState = p; break; } } let idx = (states.indexOf(currentState) + 1) % states.length; const newState = states[idx]; - // set class preserving 'answer' base - el.className = 'answer ' + newState; + // map to numeric for setMarkForSpan + let numeric = null; + if (newState === 'correct') numeric = 2; + else if (newState === 'half-correct') numeric = 1; + else if (newState === 'incorrect') numeric = 0; - // mark/unmark list item visually (no moving between lists in single-column mode) - const li = el.closest('li'); - if (li) { - // clear any previous per-state classes - li.classList.remove('marked-correct','marked-half-correct','marked-incorrect','marked-item'); - if (newState === 'not-marked') { - // nothing - } else if (newState === 'correct') { - li.classList.add('marked-item','marked-correct'); - } else if (newState === 'half-correct') { - li.classList.add('marked-item','marked-half-correct'); - } else if (newState === 'incorrect') { - li.classList.add('marked-item','marked-incorrect'); - } - } - - // update hidden form field so change is saved only on explicit Save/Next/Previous - updateMarkedAnswersField(); + // delegate to setMarkForSpan to keep behaviour consistent (updates classes, active buttons, hidden field) + setMarkForSpan(spanEl, numeric); }, false); @@ -213,6 +214,10 @@ if (li.querySelector('.mark-controls')) return; // already attached const span = li.querySelector('span.answer'); if (!span) return; + // Ensure span has an explicit default state class so logic is consistent + if (!span.classList.contains('correct') && !span.classList.contains('half-correct') && !span.classList.contains('incorrect') && !span.classList.contains('not-marked')) { + span.classList.add('not-marked'); + } const controls = document.createElement('div'); controls.className = 'mark-controls mt-2'; const b2 = makeButton('2', 'mark-2'); @@ -232,6 +237,20 @@ b1.addEventListener('click', function(){ setMarkForSpan(span, 1); }); b0.addEventListener('click', function(){ setMarkForSpan(span, 0); }); bClear.addEventListener('click', function(){ setMarkForSpan(span, null); }); + + // set initial active button based on existing state + (function(){ + let state = null; + if (span.classList.contains('correct')) state = 2; + else if (span.classList.contains('half-correct')) state = 1; + else if (span.classList.contains('incorrect')) state = 0; + // clear any pre-existing active flags then set the correct one + [b2, b1, b0, bClear].forEach(b => b.classList.remove('active')); + if (state === 2) b2.classList.add('active'); + else if (state === 1) b1.classList.add('active'); + else if (state === 0) b0.classList.add('active'); + else bClear.classList.add('active'); + })(); }); } @@ -256,6 +275,24 @@ li.classList.add('marked-item','marked-incorrect'); } } + // update active state of numeric controls for this list item + try { + const li = el.closest('li'); + if (li) { + const btn2 = li.querySelector('.mark-2'); + const btn1 = li.querySelector('.mark-1'); + const btn0 = li.querySelector('.mark-0'); + const btnClear = li.querySelector('.mark-clear'); + [btn2, btn1, btn0, btnClear].forEach(b => { if (b) { b.classList.remove('active'); } }); + if (numeric === 2 && btn2) btn2.classList.add('active'); + else if (numeric === 1 && btn1) btn1.classList.add('active'); + else if (numeric === 0 && btn0) btn0.classList.add('active'); + else if (numeric === null && btnClear) btnClear.classList.add('active'); + } + } catch (err) { + // ignore + } + updateMarkedAnswersField(); } diff --git a/anatomy/templates/anatomy/partials/mark2_marked_list_fragment.html b/anatomy/templates/anatomy/partials/mark2_marked_list_fragment.html index 97ed6b9d..6662e592 100644 --- a/anatomy/templates/anatomy/partials/mark2_marked_list_fragment.html +++ b/anatomy/templates/anatomy/partials/mark2_marked_list_fragment.html @@ -1,36 +1,42 @@ {% comment %}Renders only the list items for already-marked answers (used as a partial){% endcomment %} {% for answer in correct_answers %} {% with answer_url=answer|urlencode:'' %} -
  • -
    {{ answer }}
    -
    - 🔍 - - 🔗 +
  • +
    +
    {{ answer }}
    +
    + 🔍 + + 🔗 +
  • {% endwith %} {% endfor %} {% for answer in half_mark_answers %} {% with answer_url=answer|urlencode:'' %} -
  • -
    {{ answer }}
    -
    - 🔍 - - 🔗 +
  • +
    +
    {{ answer }}
    +
    + 🔍 + + 🔗 +
  • {% endwith %} {% endfor %} {% for answer in incorrect_answers %} {% with answer_url=answer|urlencode:'' %} -
  • -
    {{ answer }}
    -
    - 🔍 - - 🔗 +
  • +
    +
    {{ answer }}
    +
    + 🔍 + + 🔗 +
  • {% endwith %} diff --git a/static/js/anatomy.js b/static/js/anatomy.js index 0493c7c2..857b2598 100644 --- a/static/js/anatomy.js +++ b/static/js/anatomy.js @@ -20,25 +20,27 @@ $(document).ready(function () { //$(".answer-list li").each(function (index, element) { // $(element).append($("G")); //}); + // Legacy per-element click handler: skip on the new mark2 page which + // manages clicks via a delegated handler (it uses #answer-list). This + // prevents duplicate/colliding handlers when mark2 is rendered. + if (!document.getElementById('answer-list')) { + $(".answer-list span.answer").each(function (index, element) { + console.log(element); - $(".answer-list span.answer").each(function (index, element) { - console.log(element); + $(element).click(function (e) { - $(element).click(function (e) { + var classes = ['answer correct', 'answer half-correct', 'answer incorrect']; + $(element).each(function () { + let mark = classes[($.inArray(this.className, classes) + 1) % classes.length]; + this.className = mark + this.dataset.newmark = mark.split(" ")[1] + }); + + prepAnswerData(); - var classes = ['answer correct', 'answer half-correct', 'answer incorrect']; - $(element).each(function () { - let mark = classes[($.inArray(this.className, classes) + 1) % classes.length]; - this.className = mark - this.dataset.newmark = mark.split(" ")[1] }); - - prepAnswerData(); - - - }); - }); + } prepAnswerData(); if ($(".post-form").length > 0) {