From 15fd66e3e4566b7269e608c01d37db615c2b8ba5 Mon Sep 17 00:00:00 2001 From: Guan Tong Date: Wed, 29 Jul 2026 20:42:26 +0800 Subject: [PATCH] Fix paused click active tracking --- __TEST__/hyperaudio-lite.test.js | 23 +++++++++++++++++++++++ js/hyperaudio-lite.js | 8 ++++++++ js/hyperaudio-lite.mjs | 8 ++++++++ 3 files changed, 39 insertions(+) diff --git a/__TEST__/hyperaudio-lite.test.js b/__TEST__/hyperaudio-lite.test.js index 5b9ff0d..b6fa1bd 100644 --- a/__TEST__/hyperaudio-lite.test.js +++ b/__TEST__/hyperaudio-lite.test.js @@ -414,6 +414,29 @@ test("setPlayHead updates currentTime and plays if playOnClick is true", () => { expect(ht.myPlayer.play).toHaveBeenCalled(); }); +test("setPlayHead tracks the clicked word when timestamps are duplicated (#269)", () => { + const spans = document.querySelectorAll("span[data-m]"); + const lastWordInFirstParagraph = spans[4]; + const firstWordInSecondParagraph = spans[5]; + const originalTimestamp = lastWordInFirstParagraph.dataset.m; + + lastWordInFirstParagraph.dataset.m = firstWordInSecondParagraph.dataset.m; + ht.wordArr = ht.createWordArray(spans); + ht.myPlayer = { setTime: jest.fn(), play: jest.fn(), paused: true }; + ht.playOnClick = false; + + ht.setPlayHead({ target: firstWordInSecondParagraph }); + + expect(lastWordInFirstParagraph.parentNode.classList.contains("active")).toBe(false); + expect(firstWordInSecondParagraph.parentNode.classList.contains("active")).toBe(true); + expect(ht.activeWordElement).toBe(firstWordInSecondParagraph); + expect(ht.activeParentElement).toBe(firstWordInSecondParagraph.parentNode); + + lastWordInFirstParagraph.dataset.m = originalTimestamp; + ht.wordArr = ht.createWordArray(spans); + ht.clearActiveClasses(); +}); + test("preparePlayHead sets paused to false and calls checkPlayHead", () => { ht.checkPlayHead = jest.fn(); ht.preparePlayHead(); diff --git a/js/hyperaudio-lite.js b/js/hyperaudio-lite.js index c86b2a3..22e4714 100644 --- a/js/hyperaudio-lite.js +++ b/js/hyperaudio-lite.js @@ -732,8 +732,16 @@ class HyperaudioLite { // the playback loop owns the active word; when paused, we explicitly mark // the clicked word so the user sees what they clicked. if (this.myPlayer.paused && target.dataset.m) { + if (this.activeWordElement && this.activeWordElement !== target) { + this.activeWordElement.classList.remove('active'); + } + if (this.activeParentElement && this.activeParentElement !== target.parentNode) { + this.activeParentElement.classList.remove('active'); + } target.classList.add('active'); target.parentNode.classList.add('active'); + this.activeWordElement = target; + this.activeParentElement = target.parentNode; } if (!isNaN(timeSecs)) { diff --git a/js/hyperaudio-lite.mjs b/js/hyperaudio-lite.mjs index 82f9f7c..3e56790 100644 --- a/js/hyperaudio-lite.mjs +++ b/js/hyperaudio-lite.mjs @@ -732,8 +732,16 @@ class HyperaudioLite { // the playback loop owns the active word; when paused, we explicitly mark // the clicked word so the user sees what they clicked. if (this.myPlayer.paused && target.dataset.m) { + if (this.activeWordElement && this.activeWordElement !== target) { + this.activeWordElement.classList.remove('active'); + } + if (this.activeParentElement && this.activeParentElement !== target.parentNode) { + this.activeParentElement.classList.remove('active'); + } target.classList.add('active'); target.parentNode.classList.add('active'); + this.activeWordElement = target; + this.activeParentElement = target.parentNode; } if (!isNaN(timeSecs)) {