Created
November 16, 2023 15:54
-
-
Save chilang/29677e3e48fb829997d038507a52652c to your computer and use it in GitHub Desktop.
hyperaudio-static
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! (C) The Hyperaudio Project. MIT @license: en.wikipedia.org/wiki/MIT_License. */ | |
/*! Version 2.1.4 */ | |
'use strict'; | |
var caption = function () { | |
var cap = {}; | |
function formatSeconds(seconds) { | |
if (typeof seconds == 'number') { | |
//console.log("seconds = "+seconds); | |
return new Date(seconds.toFixed(3) * 1000).toISOString().substring(11,23); | |
} else { | |
console.log('warning - attempting to format the non number: ' + seconds); | |
return null; | |
} | |
} | |
function convertTimecodeToSrt(timecode) { | |
//the same as VTT format but milliseconds separated by a comma | |
return timecode.substring(0,8) + "," + timecode.substring(9,12); | |
} | |
cap.init = function (transcriptId, playerId, maxLength, minLength, label, srclang) { | |
var transcript = document.getElementById(transcriptId); | |
var words = transcript.querySelectorAll('[data-m]'); | |
var data = {}; | |
data.segments = []; | |
function segmentMeta(speaker, start, duration, chars) { | |
this.speaker = speaker; | |
this.start = start; | |
this.duration = duration; | |
this.chars = chars; | |
this.words = []; | |
} | |
function wordMeta(start, duration, text) { | |
this.start = start; | |
this.duration = duration; | |
this.text = text; | |
} | |
var thisWordMeta; | |
var thisSegmentMeta = null; | |
// defaults | |
var maxLineLength = 37; | |
var minLineLength = 21; | |
var maxWordDuration = 2; //seconds | |
var captionsVtt = 'WEBVTT\n'; | |
var captionsSrt = ''; | |
var endSentenceDelimiter = /[\.。?؟!]/g; | |
var midSentenceDelimiter = /[,、–,،و:,…‥]/g; | |
if (!isNaN(maxLength) && maxLength != null) { | |
maxLineLength = maxLength; | |
} | |
if (!isNaN(minLength) && minLength != null) { | |
minLineLength = minLength; | |
} | |
words.forEach(function (word, i) { | |
if (thisSegmentMeta === null) { | |
// create segment meta object | |
thisSegmentMeta = new segmentMeta('', null, 0, 0, 0); | |
} | |
if (word.classList.contains('speaker')) { | |
// checking that this is not a new segment AND a new empty segment wasn't already created | |
if (thisSegmentMeta !== null && thisSegmentMeta.start !== null) { | |
data.segments.push(thisSegmentMeta); // push the previous segment because it's a new speaker | |
thisSegmentMeta = new segmentMeta('', null, 0, 0, 0); | |
} | |
thisSegmentMeta.speaker = word.innerText; | |
} else { | |
var thisStart = parseInt(word.getAttribute('data-m')) / 1000; | |
var thisDuration = parseInt(word.getAttribute('data-d')) / 1000; | |
if (isNaN(thisStart)) { | |
thisStart = 0; | |
} | |
// data-d (duration) is an optional attribute, if it doesn't exist | |
// use the start time of the next word (if it exists) or for the last word | |
// pick a sensible duration. | |
if (isNaN(thisDuration)) { | |
if (i < (words.length - 1)) { | |
thisDuration = (parseInt(words[i+1].getAttribute('data-m') - 1) / 1000) - thisStart; | |
if (thisDuration > maxWordDuration) { | |
thisDuration = maxWordDuration; | |
} | |
} else { | |
thisDuration = 5; // sensible default for the last word | |
} | |
} | |
var thisText = word.innerText; | |
thisWordMeta = new wordMeta(thisStart, thisDuration, thisText); | |
if (thisSegmentMeta.start === null) { | |
thisSegmentMeta.start = thisStart; | |
thisSegmentMeta.duration = 0; | |
thisSegmentMeta.chars = 0; | |
} | |
thisSegmentMeta.duration += thisDuration; | |
thisSegmentMeta.chars += thisText.length; | |
thisSegmentMeta.words.push(thisWordMeta); | |
// remove spaces first just in case | |
var lastChar = thisText.replace(/\s/g, '').slice(-1); | |
if (lastChar.match(endSentenceDelimiter)) { | |
data.segments.push(thisSegmentMeta); | |
thisSegmentMeta = null; | |
} | |
} | |
}); | |
function captionMeta(start, stop, text) { | |
this.start = start; | |
this.stop = stop; | |
this.text = text; | |
} | |
var captions = []; | |
var thisCaption = null; | |
data.segments.map(function (segment, i, arr) { | |
// If the entire segment fits on a line, add it to the captions. | |
if (segment.chars < maxLineLength) { | |
if (segment.duration === 0){ | |
if (i < arr.length) { | |
segment.duration = arr[i+1].start - segment.start; | |
} else { | |
segment.duration = 5 * 1000; | |
} | |
} | |
thisCaption = new captionMeta( | |
formatSeconds(segment.start), | |
formatSeconds(segment.start + segment.duration), | |
'', | |
); | |
segment.words.forEach(function (wordMeta) { | |
thisCaption.text += wordMeta.text; | |
}); | |
thisCaption.text += '\n'; | |
//console.log("0. pushing because the whole segment fits on a line!"); | |
//console.log(thisCaption); | |
captions.push(thisCaption); | |
thisCaption = null; | |
} else { | |
// The number of chars in this segment is longer than our single line maximum | |
var charCount = 0; | |
var lineText = ''; | |
var firstLine = true; | |
var lastOutTime; | |
var lastInTime = null; | |
segment.words.forEach(function (wordMeta, index) { | |
var lastChar = wordMeta.text.replace(/\s/g, '').slice(-1); | |
if (lastInTime === null) { | |
// if it doesn't exist yet set the caption start time to the word's start time. | |
lastInTime = wordMeta.start; | |
} | |
// Are we over the minimum length of a line and hitting a good place to split mid-sentence? | |
if (charCount + wordMeta.text.length > minLineLength && lastChar.match(midSentenceDelimiter)) { | |
if (firstLine === true) { | |
thisCaption = new captionMeta( | |
formatSeconds(lastInTime), | |
formatSeconds(wordMeta.start + wordMeta.duration), | |
'', | |
); | |
thisCaption.text += lineText + wordMeta.text + '\n'; | |
//check for last word in segment, if it is we can push a one line caption, if not – move on to second line | |
if (index + 1 >= segment.words.length) { | |
//console.log("1. pushing because we're at a good place to split, we're on the first line but it's the last word of the segment."); | |
//console.log(thisCaption); | |
captions.push(thisCaption); | |
thisCaption = null; | |
} else { | |
firstLine = false; | |
} | |
} else { | |
// We're on the second line ... we're over the minimum chars and in a good place to split – let's push the caption | |
thisCaption.stop = formatSeconds(wordMeta.start + wordMeta.duration); | |
thisCaption.text += lineText + wordMeta.text; | |
//console.log("2. pushing because we're on the second line and have a good place to split"); | |
//console.log(thisCaption); | |
captions.push(thisCaption); | |
thisCaption = null; | |
firstLine = true; | |
} | |
// whether first line or not we should reset ready for a new caption | |
charCount = 0; | |
lineText = ''; | |
lastInTime = null; | |
} else { | |
// we're not over the minimum length with a suitable splitting point | |
// If we add this word are we over the maximum? | |
if (charCount + wordMeta.text.length > maxLineLength) { | |
if (firstLine === true) { | |
if (lastOutTime === undefined) { | |
lastOutTime = wordMeta.start + wordMeta.duration; | |
} | |
thisCaption = new captionMeta(formatSeconds(lastInTime), formatSeconds(lastOutTime), ''); | |
thisCaption.text += lineText + '\n'; | |
// It's just the first line so we should only push a new caption if it's the very last word! | |
if (index >= segment.words.length) { | |
captions.push(thisCaption); | |
thisCaption = null; | |
} else { | |
firstLine = false; | |
} | |
} else { | |
// We're on the second line and since we're over the maximum with the next word we should push this caption! | |
thisCaption.stop = formatSeconds(lastOutTime); | |
thisCaption.text += lineText; | |
captions.push(thisCaption); | |
thisCaption = null; | |
firstLine = true; | |
} | |
// do the stuff we need to do to start a new line | |
charCount = wordMeta.text.length; | |
lineText = wordMeta.text; | |
lastInTime = wordMeta.start; | |
} else { | |
// We're not over the maximum with this word, update the line length and add the word to the text | |
charCount += wordMeta.text.length; | |
lineText += wordMeta.text; | |
} | |
} | |
// for every word update the lastOutTime | |
lastOutTime = wordMeta.start + wordMeta.duration; | |
}); | |
// we're out of words for this segment - decision time! | |
if (thisCaption !== null) { | |
// The caption had been started, time to add whatever text we have and add a stop point | |
thisCaption.stop = formatSeconds(lastOutTime); | |
thisCaption.text += lineText; | |
//console.log("3. pushing at end of segment when new caption HAS BEEN created"); | |
//console.log(thisCaption); | |
captions.push(thisCaption); | |
thisCaption = null; | |
} else { | |
// caption hadn't been started yet - create one! | |
if (lastInTime !== null) { | |
thisCaption = new captionMeta(formatSeconds(lastInTime), formatSeconds(lastOutTime), lineText); | |
//console.log("4. pushing at end of segment when new caption has yet to be created"); | |
//console.log(thisCaption); | |
captions.push(thisCaption); | |
thisCaption = null; | |
} | |
} | |
} | |
}); | |
captions.forEach(function (caption, i) { | |
captionsVtt += '\n' + caption.start + ' --> ' + caption.stop + '\n' + caption.text + '\n'; | |
//console.log(caption.start + ' --> ' + caption.stop + '\n' + caption.text); | |
captionsSrt += '\n' + (i + 1) + '\n' + convertTimecodeToSrt(caption.start) + ' --> ' + convertTimecodeToSrt(caption.stop) + '\n' + caption.text + '\n'; | |
}); | |
var video = document.getElementById(playerId); | |
if (video !== null) { | |
video.addEventListener("loadedmetadata", function listener() { | |
var track = document.getElementById(playerId+'-vtt'); | |
if (track !== null){ | |
track.kind = "captions"; | |
if (label !== undefined) { | |
//console.log("setting label as "+label); | |
track.label = label; | |
} | |
if (srclang !== undefined) { | |
//console.log("setting srclang as "+srclang); | |
track.srclang = srclang; | |
} | |
track.src = "data:text/vtt,"+encodeURIComponent(captionsVtt); | |
video.textTracks[0].mode = "showing"; | |
video.removeEventListener("loadedmetadata", listener, true); | |
} | |
}, true); | |
if (video.textTracks !== undefined && video.textTracks[0] !== undefined) { | |
video.textTracks[0].mode = "showing"; | |
} | |
} | |
function captionsObj(vtt, srt, data) { | |
// clean up – remove any double blank lines | |
// and blank line at the start of srt | |
if (srt.charAt(0) !== "1") { | |
srt = srt.slice(1); | |
} | |
this.vtt = vtt.replaceAll("\n\n\n","\n\n"); | |
this.srt = srt.replaceAll("\n\n\n","\n\n"); | |
this.data = captions; | |
} | |
return new captionsObj(captionsVtt, captionsSrt, captions); | |
}; | |
return cap; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! (C) The Hyperaudio Project. MIT @license: en.wikipedia.org/wiki/MIT_License. */ | |
/*! Version 2.1.1b */ | |
'use strict'; | |
// Example wrapper for hyperaudio-lite with search and playbackRate included | |
let searchForm = document.getElementById('searchForm'); | |
if (searchForm) { | |
searchForm.addEventListener('submit', function(event){ | |
searchPhrase(document.getElementById('search').value); | |
event.preventDefault(); | |
}, false); | |
} | |
let searchPhrase = function (phrase) { | |
let htmlWords = document.querySelectorAll('[data-m]'); | |
let htmlWordsLen = htmlWords.length; | |
let phraseWords = phrase.split(" "); | |
let phraseWordsLen = phraseWords.length; | |
let matchedTimes = []; | |
// clear matched times | |
let searchMatched = document.querySelectorAll('.search-match'); | |
searchMatched.forEach((match) => { | |
match.classList.remove('search-match'); | |
}); | |
for (let i = 0; i < htmlWordsLen; i++) { | |
let numWordsMatched = 0; | |
let potentiallyMatched = []; | |
for (let j = 0; j < phraseWordsLen; j++) { | |
let wordIndex = i + numWordsMatched; | |
if (wordIndex >= htmlWordsLen) { | |
break; | |
} | |
// regex removes punctuation - NB for htmlWords case we also remove the space | |
if (phraseWords[j].toLowerCase() == htmlWords[wordIndex].innerHTML.toLowerCase().replace(/[\.,-\/#!$%\^&\*;:{}=\-_`~()\? ]/g,"")) { | |
potentiallyMatched.push(htmlWords[wordIndex].getAttribute('data-m')); | |
numWordsMatched++; | |
} else { | |
break; | |
} | |
// if the num of words matched equal the search phrase we have a winner! | |
if (numWordsMatched >= phraseWordsLen) { | |
matchedTimes = matchedTimes.concat(potentiallyMatched); | |
} | |
} | |
} | |
// display | |
matchedTimes.forEach(matchedTime => { | |
document.querySelectorAll("[data-m='"+matchedTime+"']")[0].classList.add("search-match"); | |
}); | |
} | |
window.onload = function() { | |
const playbackRateCtrl = document.getElementById('pbr'); | |
const currentPlaybackRate = document.getElementById('currentPbr'); | |
if (playbackRateCtrl !== null) { | |
playbackRateCtrl.addEventListener('input', function(){ | |
currentPlaybackRate.innerHTML = playbackRateCtrl.value; | |
hyperplayer.playbackRate = playbackRateCtrl.value; | |
},false); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! (C) The Hyperaudio Project. MIT @license: en.wikipedia.org/wiki/MIT_License. */ | |
/* Updated for Version 2.1 */ | |
.hyperaudio-transcript .strikethrough { | |
text-decoration: line-through; | |
} | |
.hyperaudio-transcript .annotation, .hyperaudio-transcript .parannotation{ | |
opacity: 0.7; | |
} | |
.hyperaudio-transcript .highlight { | |
background-color: yellow; | |
} | |
.hyperaudio-transcript .highlight.active { | |
background-color: lightGreen; | |
} | |
.hyperaudio-transcript header { | |
font-size: 200%; | |
} | |
/*.hyperaudio-transcript a { | |
cursor: pointer; | |
color: #000; | |
}*/ | |
.hyperaudio-transcript a, a.link { | |
border: 0px; | |
} | |
.hyperaudio-transcript .read { | |
color: #000; | |
} | |
.hyperaudio-transcript .active > .active { | |
color: #000; | |
} | |
.hyperaudio-transcript .unread { | |
color: #777; | |
} | |
.hyperaudio-transcript .search-match { | |
background-color: pink; | |
} | |
.hyperaudio-transcript .share-match { | |
background-color: #66ffad; | |
} | |
.hyperaudio-transcript sub:before { | |
content: '\231C'; | |
} | |
.hyperaudio-transcript sub.highlight-duration:before { | |
content: '\231D'; | |
} | |
.hyperaudio-transcript h5 { | |
font-size: 130%; | |
} | |
[data-m] { | |
cursor: pointer; | |
} | |
body { | |
font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif; | |
line-height: 1.5; | |
} | |
.speaker { | |
font-weight: bold; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! (C) The Hyperaudio Project. MIT @license: en.wikipedia.org/wiki/MIT_License. */ | |
/*! Version 2.1.5 */ | |
'use strict'; | |
function nativePlayer(instance) { | |
this.player = instance.player; | |
this.player.addEventListener('pause', instance.pausePlayHead, false); | |
this.player.addEventListener('play', instance.preparePlayHead, false); | |
this.paused = true; | |
this.getTime = () => { | |
return new Promise((resolve) => { | |
resolve(this.player.currentTime); | |
}); | |
} | |
this.setTime = (seconds) => { | |
this.player.currentTime = seconds; | |
} | |
this.play = () => { | |
this.player.play(); | |
this.paused = false; | |
} | |
this.pause = () => { | |
this.player.pause(); | |
this.paused = true; | |
} | |
} | |
function soundcloudPlayer(instance) { | |
this.player = SC.Widget(instance.player.id); | |
this.player.bind(SC.Widget.Events.PAUSE, instance.pausePlayHead); | |
this.player.bind(SC.Widget.Events.PLAY, instance.preparePlayHead); | |
this.paused = true; | |
this.getTime = () => { | |
return new Promise((resolve) => { | |
this.player.getPosition(ms => { | |
resolve(ms / 1000); | |
}); | |
}); | |
} | |
this.setTime = (seconds) => { | |
this.player.seekTo(seconds * 1000); | |
} | |
this.play = () => { | |
this.player.play(); | |
this.paused = false; | |
} | |
this.pause = () => { | |
this.player.pause(); | |
this.paused = true; | |
} | |
} | |
function videojsPlayer(instance) { | |
this.player = videojs.getPlayer(instance.player.id); | |
this.player.addEventListener('pause', instance.pausePlayHead, false); | |
this.player.addEventListener('play', instance.preparePlayHead, false); | |
this.paused = true; | |
this.getTime = () => { | |
return new Promise((resolve) => { | |
resolve(this.player.currentTime()); | |
}); | |
} | |
this.setTime = (seconds) => { | |
this.player.currentTime(seconds); | |
} | |
this.play = () => { | |
this.player.play(); | |
this.paused = false; | |
} | |
this.pause = () => { | |
this.player.pause(); | |
this.paused = true; | |
} | |
} | |
function vimeoPlayer(instance) { | |
const iframe = document.querySelector('iframe'); | |
this.player = new Vimeo.Player(iframe); | |
this.player.setCurrentTime(0); | |
this.paused = true; | |
this.player.ready().then(instance.checkPlayHead); | |
this.player.on('play',instance.preparePlayHead); | |
this.player.on('pause',instance.pausePlayHead); | |
this.getTime = () => { | |
return new Promise((resolve) => { | |
resolve(this.player.getCurrentTime()); | |
}); | |
} | |
this.setTime = (seconds) => { | |
this.player.setCurrentTime(seconds); | |
} | |
this.play = () => { | |
this.player.play(); | |
this.paused = false; | |
} | |
this.pause = () => { | |
this.player.pause(); | |
this.paused = true; | |
} | |
} | |
function youtubePlayer(instance) { | |
const tag = document.createElement('script'); | |
tag.id = 'iframe-demo'; | |
tag.src = 'https://www.youtube.com/iframe_api'; | |
const firstScriptTag = document.getElementsByTagName('script')[0]; | |
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); | |
this.paused = true; | |
const previousYTEvent = window.onYouTubeIframeAPIReady; | |
window.onYouTubeIframeAPIReady = () => { | |
if (typeof previousYTEvent !== 'undefined') { // used for multiple YouTube players | |
previousYTEvent(); | |
} | |
this.player = new YT.Player(instance.player.id, { | |
events: { | |
onStateChange: onPlayerStateChange, | |
}, | |
}); | |
}; | |
let onPlayerStateChange = event => { | |
if (event.data === 1) { | |
// playing | |
instance.preparePlayHead(); | |
this.paused = false; | |
} else if (event.data === 2) { | |
// paused | |
instance.pausePlayHead(); | |
this.paused = true; | |
} | |
}; | |
this.getTime = () => { | |
return new Promise((resolve) => { | |
resolve(this.player.getCurrentTime()); | |
}); | |
} | |
this.setTime = (seconds) => { | |
this.player.seekTo(seconds, true); | |
} | |
this.play = () => { | |
this.player.playVideo(); | |
} | |
this.pause = () => { | |
this.player.pauseVideo(); | |
} | |
} | |
const hyperaudioPlayerOptions = { | |
"native": nativePlayer, | |
"soundcloud": soundcloudPlayer, | |
"youtube": youtubePlayer, | |
"videojs": videojsPlayer, | |
"vimeo": vimeoPlayer | |
} | |
function hyperaudioPlayer(playerType, instance) { | |
if (playerType !== null && playerType !== undefined) { | |
return new playerType(instance); | |
} else { | |
console.warn("HYPERAUDIO LITE WARNING: data-player-type attribute should be set on player if not native, eg SoundCloud, YouTube, Vimeo, VideoJS"); | |
} | |
} | |
class HyperaudioLite { | |
constructor(transcriptId, mediaElementId, minimizedMode, autoscroll, doubleClick, webMonetization, playOnClick) { | |
this.transcript = document.getElementById(transcriptId); | |
this.init(mediaElementId, minimizedMode, autoscroll, doubleClick, webMonetization, playOnClick); | |
} | |
init = (mediaElementId, minimizedMode, autoscroll, doubleClick, webMonetization, playOnClick) => { | |
const windowHash = window.location.hash; | |
const hashVar = windowHash.substring(1, windowHash.indexOf('=')); | |
if (hashVar === this.transcript.id) { | |
this.hashArray = windowHash.substring(this.transcript.id.length + 2).split(','); | |
} else { | |
this.hashArray = []; | |
} | |
document.addEventListener( | |
'selectionchange', | |
() => { | |
const mediaFragment = this.getSelectionMediaFragment(); | |
if (mediaFragment !== null) { | |
document.location.hash = mediaFragment; | |
} | |
}, | |
false, | |
); | |
this.minimizedMode = minimizedMode; | |
this.textShot = ''; | |
this.wordIndex = 0; | |
this.autoscroll = autoscroll; | |
this.scrollerContainer = this.transcript; | |
this.scrollerOffset = 0; | |
this.scrollerDuration = 800; | |
this.scrollerDelay = 0; | |
this.doubleClick = doubleClick; | |
this.webMonetization = webMonetization; | |
this.playOnClick = playOnClick; | |
this.highlightedText = false; | |
this.start = null; | |
this.myPlayer = null; | |
this.playerPaused = true; | |
if (this.autoscroll === true) { | |
this.scroller = window.Velocity || window.jQuery.Velocity; | |
} | |
//Create the array of timed elements (wordArr) | |
const words = this.transcript.querySelectorAll('[data-m]'); | |
this.wordArr = this.createWordArray(words); | |
this.parentTag = words[0].parentElement.tagName; | |
this.parentElements = this.transcript.getElementsByTagName(this.parentTag); | |
this.player = document.getElementById(mediaElementId); | |
// Grab the media source and type from the first section if it exists | |
// and add it to the media element. | |
const mediaSrc = this.transcript.querySelector('[data-media-src]'); | |
if (mediaSrc !== null && mediaSrc !== undefined) { | |
this.player.src = mediaSrc.getAttribute('data-media-src'); | |
} | |
if (this.player.tagName == 'VIDEO' || this.player.tagName == 'AUDIO') { | |
//native HTML media elements | |
this.playerType = 'native'; | |
} else { | |
//assume it is a SoundCloud or YouTube iframe | |
this.playerType = this.player.getAttribute('data-player-type'); | |
} | |
this.myPlayer = hyperaudioPlayer(hyperaudioPlayerOptions[this.playerType], this); | |
this.parentElementIndex = 0; | |
words[0].classList.add('active'); | |
//this.parentElements[0].classList.add('active'); | |
let playHeadEvent = 'click'; | |
if (this.doubleClick === true) { | |
playHeadEvent = 'dblclick'; | |
} | |
this.transcript.addEventListener(playHeadEvent, this.setPlayHead, false); | |
this.transcript.addEventListener(playHeadEvent, this.checkPlayHead, false); | |
this.start = this.hashArray[0]; | |
if (!isNaN(parseFloat(this.start))) { | |
this.highlightedText = true; | |
let indices = this.updateTranscriptVisualState(this.start); | |
let index = indices.currentWordIndex; | |
if (index > 0) { | |
this.scrollToParagraph(indices.currentParentElementIndex, index); | |
} | |
} | |
this.end = this.hashArray[1]; | |
//TODO convert to binary search for below for quicker startup | |
if (this.start && this.end) { | |
for (let i = 1; i < words.length; i++) { | |
const wordStart = parseInt(words[i].getAttribute('data-m')) / 1000; | |
if (wordStart > parseFloat(this.start) && parseFloat(this.end) > wordStart) { | |
words[i].classList.add('share-match'); | |
} | |
} | |
} | |
}; // end init | |
createWordArray = words => { | |
let wordArr = []; | |
words.forEach((word, i) => { | |
const m = parseInt(word.getAttribute('data-m')); | |
let p = word.parentNode; | |
while (p !== document) { | |
if ( | |
p.tagName.toLowerCase() === 'p' || | |
p.tagName.toLowerCase() === 'figure' || | |
p.tagName.toLowerCase() === 'ul' | |
) { | |
break; | |
} | |
p = p.parentNode; | |
} | |
wordArr[i] = { n: words[i], m: m, p: p }; | |
wordArr[i].n.classList.add('unread'); | |
}); | |
return wordArr; | |
}; | |
getSelectionMediaFragment = () => { | |
let fragment = null; | |
let selection = null; | |
if (window.getSelection) { | |
selection = window.getSelection(); | |
} else if (document.selection) { | |
selection = document.selection.createRange(); | |
} | |
// check to see if selection is actually inside the transcript | |
let insideTranscript = false; | |
let parentElement = selection.focusNode; | |
while (parentElement !== null) { | |
if (parentElement.id === this.transcript.id) { | |
insideTranscript = true; | |
break; | |
} | |
parentElement = parentElement.parentElement; | |
} | |
if (selection.toString() !== '' && insideTranscript === true && selection.focusNode !== null && selection.anchorNode !== null) { | |
let fNode = selection.focusNode.parentNode; | |
let aNode = selection.anchorNode.parentNode; | |
if (aNode.tagName === "P") { | |
aNode = selection.anchorNode.nextElementSibling; | |
} | |
if (fNode.tagName === "P") { | |
fNode = selection.focusNode.nextElementSibling; | |
} | |
if (aNode.getAttribute('data-m') === null || aNode.className === 'speaker') { | |
aNode = aNode.nextElementSibling; | |
} | |
if (fNode.getAttribute('data-m') === null || fNode.className === 'speaker') { | |
fNode = fNode.previousElementSibling; | |
} | |
// if the selection starts with a space we want the next element | |
if(selection.toString().charAt(0) == " " && aNode !== null) { | |
aNode = aNode.nextElementSibling; | |
} | |
if (aNode !== null) { | |
let aNodeTime = parseInt(aNode.getAttribute('data-m'), 10); | |
let aNodeDuration = parseInt(aNode.getAttribute('data-d'), 10); | |
let fNodeTime; | |
let fNodeDuration; | |
if (fNode !== null && fNode.getAttribute('data-m') !== null) { | |
// if the selection ends in a space we want the previous element if it exists | |
if(selection.toString().slice(-1) == " " && fNode.previousElementSibling !== null) { | |
fNode = fNode.previousElementSibling; | |
} | |
fNodeTime = parseInt(fNode.getAttribute('data-m'), 10); | |
fNodeDuration = parseInt(fNode.getAttribute('data-d'), 10); | |
// if the selection starts with a space we want the next element | |
} | |
// 1 decimal place will do | |
aNodeTime = Math.round(aNodeTime / 100) / 10; | |
aNodeDuration = Math.round(aNodeDuration / 100) / 10; | |
fNodeTime = Math.round(fNodeTime / 100) / 10; | |
fNodeDuration = Math.round(fNodeDuration / 100) / 10; | |
let nodeStart = aNodeTime; | |
let nodeDuration = Math.round((fNodeTime + fNodeDuration - aNodeTime) * 10) / 10; | |
if (aNodeTime >= fNodeTime) { | |
nodeStart = fNodeTime; | |
nodeDuration = Math.round((aNodeTime + aNodeDuration - fNodeTime) * 10) / 10; | |
} | |
if (nodeDuration === 0 || nodeDuration === null || isNaN(nodeDuration)) { | |
nodeDuration = 10; // arbitary for now | |
} | |
if (isNaN(parseFloat(nodeStart))) { | |
fragment = null; | |
} else { | |
fragment = this.transcript.id + '=' + nodeStart + ',' + Math.round((nodeStart + nodeDuration) * 10) / 10; | |
} | |
} | |
} | |
return fragment; | |
}; | |
setPlayHead = e => { | |
const target = e.target ? e.target : e.srcElement; | |
// cancel highlight playback | |
this.highlightedText = false; | |
// clear elements with class='active' | |
let activeElements = Array.from(this.transcript.getElementsByClassName('active')); | |
activeElements.forEach(e => { | |
e.classList.remove('active'); | |
}); | |
if (this.myPlayer.paused === true && target.getAttribute('data-m') !== null) { | |
target.classList.add('active'); | |
target.parentNode.classList.add('active'); | |
} | |
const timeSecs = parseInt(target.getAttribute('data-m')) / 1000; | |
this.updateTranscriptVisualState(timeSecs); | |
if (!isNaN(parseFloat(timeSecs))) { | |
this.end = null; | |
this.myPlayer.setTime(timeSecs); | |
if (this.playOnClick === true) { | |
this.myPlayer.play(); | |
} | |
} | |
}; | |
clearTimer = () => { | |
if (this.timer) clearTimeout(this.timer); | |
}; | |
preparePlayHead = () => { | |
this.myPlayer.paused = false; | |
this.checkPlayHead(); | |
} | |
pausePlayHead = () => { | |
this.clearTimer(); | |
this.myPlayer.paused = true; | |
} | |
checkPlayHead = () => { | |
this.clearTimer(); | |
(async (instance) => { | |
instance.currentTime = await instance.myPlayer.getTime(); | |
if (instance.highlightedText === true) { | |
instance.currentTime = instance.start; | |
instance.myPlayer.setTime(instance.currentTime); | |
instance.highlightedText = false; | |
} | |
// no need to check status if the currentTime hasn't changed | |
instance.checkStatus(); | |
})(this); | |
} | |
scrollToParagraph = (currentParentElementIndex, index) => { | |
let newPara = false; | |
let scrollNode = this.wordArr[index - 1].n.parentNode; | |
if (scrollNode !== null && scrollNode.tagName != 'P') { | |
// it's not inside a para so just use the element | |
scrollNode = this.wordArr[index - 1].n; | |
} | |
if (currentParentElementIndex != this.parentElementIndex) { | |
if (typeof this.scroller !== 'undefined' && this.autoscroll === true) { | |
if (scrollNode !== null) { | |
if (typeof this.scrollerContainer !== 'undefined' && this.scrollerContainer !== null) { | |
this.scroller(scrollNode, 'scroll', { | |
container: this.scrollerContainer, | |
duration: this.scrollerDuration, | |
delay: this.scrollerDelay, | |
offset: this.scrollerOffset, | |
}); | |
} else { | |
this.scroller(scrollNode, 'scroll', { | |
duration: this.scrollerDuration, | |
delay: this.scrollerDelay, | |
offset: this.scrollerOffset, | |
}); | |
} | |
} else { | |
// the wordlst needs refreshing | |
let words = this.transcript.querySelectorAll('[data-m]'); | |
this.wordArr = this.createWordArray(words); | |
this.parentElements = this.transcript.getElementsByTagName(this.parentTag); | |
} | |
} | |
newPara = true; | |
this.parentElementIndex = currentParentElementIndex; | |
} | |
return(newPara); | |
} | |
checkStatus = () => { | |
//check for end time of shared piece | |
let interval = 0; | |
if (this.myPlayer.paused === false) { | |
if (this.end && parseInt(this.end) < parseInt(this.currentTime)) { | |
this.myPlayer.pause(); | |
this.end = null; | |
} else { | |
let newPara = false; | |
//interval = 0; // used to establish next checkPlayHead | |
let indices = this.updateTranscriptVisualState(this.currentTime); | |
let index = indices.currentWordIndex; | |
if (index > 0) { | |
newPara = this.scrollToParagraph(indices.currentParentElementIndex, index); | |
} | |
//minimizedMode is still experimental - it changes document.title upon every new word | |
if (this.minimizedMode) { | |
const elements = transcript.querySelectorAll('[data-m]'); | |
let currentWord = ''; | |
let lastWordIndex = this.wordIndex; | |
for (let i = 0; i < elements.length; i++) { | |
if ((' ' + elements[i].className + ' ').indexOf(' active ') > -1) { | |
currentWord = elements[i].innerHTML; | |
this.wordIndex = i; | |
} | |
} | |
let textShot = ''; | |
if (this.wordIndex != lastWordIndex) { | |
textShot = textShot + currentWord; | |
} | |
if (textShot.length > 16 || newPara === true) { | |
document.title = textShot; | |
textShot = ''; | |
newPara = false; | |
} | |
} | |
if (this.wordArr[index]) { | |
interval = parseInt(this.wordArr[index].n.getAttribute('data-m') - this.currentTime * 1000); | |
} | |
} | |
if (this.webMonetization === true) { | |
//check for payment pointer | |
let activeElements = this.transcript.getElementsByClassName('active'); | |
let paymentPointer = this.checkPaymentPointer(activeElements[activeElements.length - 1]); | |
if (paymentPointer !== null) { | |
let metaElements = document.getElementsByTagName('meta'); | |
let wmMeta = document.querySelector("meta[name='monetization']"); | |
if (wmMeta === null) { | |
wmMeta = document.createElement('meta'); | |
wmMeta.name = 'monetization'; | |
wmMeta.content = paymentPointer; | |
document.getElementsByTagName('head')[0].appendChild(wmMeta); | |
} else { | |
wmMeta.name = 'monetization'; | |
wmMeta.content = paymentPointer; | |
} | |
} | |
} | |
this.timer = setTimeout(() => { | |
this.checkPlayHead(); | |
}, interval + 1); // +1 to avoid rounding issues (better to be over than under) | |
} else { | |
this.clearTimer(); | |
} | |
}; | |
checkPaymentPointer = element => { | |
let paymentPointer = null; | |
if (typeof(element) != "undefined") { | |
paymentPointer = element.getAttribute('data-wm'); | |
} | |
if (paymentPointer !== null) { | |
return paymentPointer; | |
} else { | |
let parent = null; | |
if (typeof element !== 'undefined') { | |
parent = element.parentElement; | |
} | |
if (parent === null) { | |
return null; | |
} else { | |
return this.checkPaymentPointer(parent); | |
} | |
} | |
}; | |
updateTranscriptVisualState = (currentTime) => { | |
let index = 0; | |
let words = this.wordArr.length - 1; | |
// Binary search https://en.wikipedia.org/wiki/Binary_search_algorithm | |
while (index <= words) { | |
const guessIndex = index + ((words - index) >> 1); // >> 1 has the effect of halving and rounding down | |
const difference = this.wordArr[guessIndex].m / 1000 - currentTime; // wordArr[guessIndex].m represents start time of word | |
if (difference < 0) { | |
// comes before the element | |
index = guessIndex + 1; | |
} else if (difference > 0) { | |
// comes after the element | |
words = guessIndex - 1; | |
} else { | |
// equals the element | |
index = guessIndex; | |
break; | |
} | |
} | |
this.wordArr.forEach((word, i) => { | |
let classList = word.n.classList; | |
let parentClassList = word.n.parentNode.classList; | |
if (i < index) { | |
classList.add('read'); | |
classList.remove('unread'); | |
classList.remove('active'); | |
parentClassList.remove('active'); | |
} else { | |
classList.add('unread'); | |
classList.remove('read'); | |
} | |
}); | |
this.parentElements = this.transcript.getElementsByTagName(this.parentTag); | |
//remove active class from all paras | |
Array.from(this.parentElements).forEach(el => { | |
if (el.classList.contains('active')) { | |
el.classList.remove('active'); | |
} | |
}); | |
// set current word and para to active | |
if (index > 0) { | |
if (this.myPlayer.paused === false) { | |
this.wordArr[index - 1].n.classList.add('active'); | |
} | |
if (this.wordArr[index - 1].n.parentNode !== null) { | |
this.wordArr[index - 1].n.parentNode.classList.add('active'); | |
} | |
} | |
// Establish current paragraph index | |
let currentParentElementIndex; | |
Array.from(this.parentElements).every((el, i) => { | |
if (el.classList.contains('active')) { | |
currentParentElementIndex = i; | |
return false; | |
} | |
return true; | |
}); | |
let indices = { | |
currentWordIndex: index, | |
currentParentElementIndex: currentParentElementIndex, | |
}; | |
return indices; | |
}; | |
setScrollParameters = (duration, delay, offset, container) => { | |
this.scrollerContainer = container; | |
this.scrollerDuration = duration; | |
this.scrollerDelay = delay; | |
this.scrollerOffset = offset; | |
}; | |
toggleAutoScroll = () => { | |
this.autoscroll = !this.autoscroll; | |
}; | |
setAutoScroll = state => { | |
this.autoscroll = state; | |
}; | |
} | |
// required for testing | |
if (typeof module !== 'undefined' && module.exports) { | |
module.exports = { HyperaudioLite }; | |
} | |
//export default HyperaudioLite; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment