diff --git a/__TEST__/hyperaudio-lite.test.js b/__TEST__/hyperaudio-lite.test.js index 5b9ff0d..5caf057 100644 --- a/__TEST__/hyperaudio-lite.test.js +++ b/__TEST__/hyperaudio-lite.test.js @@ -281,6 +281,19 @@ test("getSelectionRange with valid selection", () => { expect(ht.getSelectionRange()).toBe("0.88,5.21"); }); +test("getSelectionRange ignores leading whitespace from the previous word", () => { + const firstSpanText = document.querySelector('span[data-m="880"]').firstChild; + const lastSpanText = document.querySelector('span[data-m="4750"]').firstChild; + const range = document.createRange(); + range.setStart(firstSpanText, firstSpanText.length - 1); + range.setEnd(lastSpanText, 3); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + + expect(range.toString().startsWith(" ")).toBe(true); + expect(ht.getSelectionRange()).toBe("2.56,5.21"); +}); + test("clearActiveClasses removes all active classes", () => { const spans = document.querySelectorAll('span'); spans.forEach(span => span.classList.add('active')); diff --git a/js/hyperaudio-lite.js b/js/hyperaudio-lite.js index c86b2a3..c819c7d 100644 --- a/js/hyperaudio-lite.js +++ b/js/hyperaudio-lite.js @@ -658,14 +658,27 @@ class HyperaudioLite { // Get all relevant spans const allSpans = Array.from(this.transcript.querySelectorAll('[data-m]')); - // Find the first and last span that contain selected text + // Find the first and last spans that contribute non-whitespace text. + // Range#intersectsNode also returns true when only a word's trailing + // whitespace is selected, so inspect the actual intersection. let startSpan = null; let endSpan = null; - let selectedText = range.toString(); - let trimmedSelectedText = selectedText.trim(); for (let span of allSpans) { - if (range.intersectsNode(span) && span.textContent.trim() !== '') { + if (!range.intersectsNode(span)) continue; + + const spanRange = document.createRange(); + spanRange.selectNodeContents(span); + const intersection = range.cloneRange(); + + if (intersection.compareBoundaryPoints(Range.START_TO_START, spanRange) < 0) { + intersection.setStart(spanRange.startContainer, spanRange.startOffset); + } + if (intersection.compareBoundaryPoints(Range.END_TO_END, spanRange) > 0) { + intersection.setEnd(spanRange.endContainer, spanRange.endOffset); + } + + if (!intersection.collapsed && intersection.toString().trim() !== '') { if (!startSpan) startSpan = span; endSpan = span; } @@ -673,14 +686,6 @@ class HyperaudioLite { if (!startSpan || !endSpan) return null; - // Adjust start span if selection starts with a space - let startIndex = allSpans.indexOf(startSpan); - while (selectedText.startsWith(' ') && startIndex < allSpans.length - 1) { - startIndex++; - startSpan = allSpans[startIndex]; - selectedText = selectedText.slice(1); - } - // Calculate start time let startTime = parseInt(startSpan.dataset.m) / 1000; @@ -700,8 +705,7 @@ class HyperaudioLite { let startTimeFormatted = (Math.round(startTime * 100) / 100).toFixed(2); let endTimeFormatted = (Math.round(endTime * 100) / 100).toFixed(2); - // Only return a range if there's actually selected text (excluding only spaces) - return trimmedSelectedText ? `${startTimeFormatted},${endTimeFormatted}` : null; + return `${startTimeFormatted},${endTimeFormatted}`; } getSelectionMediaFragment = () => { diff --git a/js/hyperaudio-lite.mjs b/js/hyperaudio-lite.mjs index 82f9f7c..6fc9717 100644 --- a/js/hyperaudio-lite.mjs +++ b/js/hyperaudio-lite.mjs @@ -658,14 +658,27 @@ class HyperaudioLite { // Get all relevant spans const allSpans = Array.from(this.transcript.querySelectorAll('[data-m]')); - // Find the first and last span that contain selected text + // Find the first and last spans that contribute non-whitespace text. + // Range#intersectsNode also returns true when only a word's trailing + // whitespace is selected, so inspect the actual intersection. let startSpan = null; let endSpan = null; - let selectedText = range.toString(); - let trimmedSelectedText = selectedText.trim(); for (let span of allSpans) { - if (range.intersectsNode(span) && span.textContent.trim() !== '') { + if (!range.intersectsNode(span)) continue; + + const spanRange = document.createRange(); + spanRange.selectNodeContents(span); + const intersection = range.cloneRange(); + + if (intersection.compareBoundaryPoints(Range.START_TO_START, spanRange) < 0) { + intersection.setStart(spanRange.startContainer, spanRange.startOffset); + } + if (intersection.compareBoundaryPoints(Range.END_TO_END, spanRange) > 0) { + intersection.setEnd(spanRange.endContainer, spanRange.endOffset); + } + + if (!intersection.collapsed && intersection.toString().trim() !== '') { if (!startSpan) startSpan = span; endSpan = span; } @@ -673,14 +686,6 @@ class HyperaudioLite { if (!startSpan || !endSpan) return null; - // Adjust start span if selection starts with a space - let startIndex = allSpans.indexOf(startSpan); - while (selectedText.startsWith(' ') && startIndex < allSpans.length - 1) { - startIndex++; - startSpan = allSpans[startIndex]; - selectedText = selectedText.slice(1); - } - // Calculate start time let startTime = parseInt(startSpan.dataset.m) / 1000; @@ -700,8 +705,7 @@ class HyperaudioLite { let startTimeFormatted = (Math.round(startTime * 100) / 100).toFixed(2); let endTimeFormatted = (Math.round(endTime * 100) / 100).toFixed(2); - // Only return a range if there's actually selected text (excluding only spaces) - return trimmedSelectedText ? `${startTimeFormatted},${endTimeFormatted}` : null; + return `${startTimeFormatted},${endTimeFormatted}`; } getSelectionMediaFragment = () => {