\";\n $(\"#dictate_game\").append(code);\n $(\"#\"+id).toggle(\"slide\",{direction:'right'});\n },\n getTTS:function(speaktext,voice,callback){\n\n //The REST API we are calling\n var functionname = 'local_cpapi_fetch_polly_url';\n \n //fetch the Posturl. We need this.\n //set up our ajax request\n var xhr = new XMLHttpRequest();\n var that = this;\n \n //set up our handler for the response\n xhr.onreadystatechange = function (e) {\n if (this.readyState === 4) {\n if (xhr.status == 200) {\n \n //get a yes or forgetit or tryagain\n var payload = xhr.responseText;\n var payloadobject = JSON.parse(payload);\n if (payloadobject) {\n //returnCode > 0 indicates an error\n if (payloadobject.returnCode > 0) {\n console.log(payloadobject.returnMessage);\n return false;\n //if all good, then lets do the embed\n } else if (payloadobject.returnCode === 0){\n var pollyurl = payloadobject.returnMessage;\n callback(pollyurl);\n } else {\n console.log('Polly Signed URL Request failed:');\n console.log(payloadobject);\n }\n } else {\n console.log('Polly Signed URL Request something bad happened');\n }\n } else {\n console.log('Polly Signed URL Request Not 200 response:' + xhr.status);\n }\n }\n };\n \n //make our request\n var xhrparams = \"wstoken=\" + @@CLOUDPOODLLTOKEN@@\n + \"&wsfunction=\" + functionname\n + \"&moodlewsrestformat=\" + 'json'\n + \"&text=\" + encodeURIComponent(speaktext)\n + '&texttype=text'\n + '&voice=' + voice\n + '&appid=' + 'filter_poodll'\n + '&owner=poodll'\n + '®ion=useast1';\n \n var serverurl = 'https://cloud.poodll.com' + \"/webservice/rest/server.php\";\n xhr.open(\"POST\", serverurl, true);\n xhr.setRequestHeader(\"Cache-Control\", \"no-cache\");\n xhr.setRequestHeader(\"Content-Type\", \"application/x-www-form-urlencoded\");\n xhr.send(xhrparams);\n },\n render:function(){\n var self = this;\n },\n attachHandlers:function(){\n var self = this;\n $(\"#dictate_start_btn\").on(\"click\",function(){\n self.start();\n });\n $(\"#dictate_listen_btn\").on(\"click\",function(){\n self.items[self.game.pointer].audio.load();\n self.items[self.game.pointer].audio.play();\n });\n $(\"#dictate_skip_btn\").on(\"click\",function(){\n if(self.game.pointer\n\n\n
\n","bodyend":"
","script":"//FUNCTION fetch polly url\n var fetch_polly_url = function(speaktext, voice, callback) {\n\n //The REST API we are calling\n var functionname = 'local_cpapi_fetch_polly_url';\n\n //fetch the Posturl. We need this.\n //set up our ajax request\n var xhr = new XMLHttpRequest();\n var that = this;\n\n //set up our handler for the response\n xhr.onreadystatechange = function (e) {\n if (this.readyState === 4) {\n if (xhr.status == 200) {\n\n //get a yes or forgetit or tryagain\n var payload = xhr.responseText;\n var payloadobject = JSON.parse(payload);\n if (payloadobject) {\n //returnCode > 0 indicates an error\n if (payloadobject.returnCode > 0) {\n console.log(payloadobject.returnMessage);\n return false;\n //if all good, then lets do the embed\n } else if (payloadobject.returnCode === 0){\n var pollyurl = payloadobject.returnMessage;\n callback(pollyurl);\n } else {\n console.log('Polly Signed URL Request failed:');\n console.log(payloadobject);\n }\n } else {\n console.log('Polly Signed URL Request something bad happened');\n }\n } else {\n console.log('Polly Signed URL Request Not 200 response:' + xhr.status);\n }\n }\n };\n\n //make our request\n var xhrparams = \"wstoken=\" + @@CLOUDPOODLLTOKEN@@\n + \"&wsfunction=\" + functionname\n + \"&moodlewsrestformat=\" + 'json'\n + \"&text=\" + encodeURIComponent(speaktext)\n + '&texttype=text'\n + '&voice=' + voice\n + '&appid=' + 'filter_poodll'\n + '&owner=poodll'\n + '®ion=useast1';\n\n var serverurl = 'https://cloud.poodll.com' + \"/webservice/rest/server.php\";\n xhr.open(\"POST\", serverurl, true);\n xhr.setRequestHeader(\"Cache-Control\", \"no-cache\");\n xhr.setRequestHeader(\"Content-Type\", \"application/x-www-form-urlencoded\");\n xhr.send(xhrparams);\n };\n\n//remove any HTML editor artifacts\n$('#' + @@AUTOID@@ + ' p').remove();\n$('#' + @@AUTOID@@ + ' span').remove();\n\n//fetch the controls we inserted in \"body\"\nvar theplayer = $('#' + @@AUTOID@@ + '_player');\nvar theulcontainer = $('#' + @@AUTOID@@); \nvar theitemscontainer = $('#' + @@AUTOID@@ + '_container'); \n\ntheitemscontainer.on('click','.dictationtrigger', function(e){\n theplayer.attr('src',$(this).attr('data-src'));\n theplayer[0].play();\n});\n\n\n//determine voice\nvar mf=@@speaker@@\nswitch(@@language@@){\ncase \"English(US)\": voice = mf=='Male'?'Joey':'Kendra';break;\ncase \"English(GB)\": voice = mf=='Male'?'Brian':'Amy';break;\ncase \"English(AU)\": voice = mf=='Male'?'Russell':'Nicole';break;\ncase \"English(IN)\": voice = mf=='Male'?'Aditi':'Raveena';break;\ncase \"English(WELSH)\": voice = mf=='Male'? 'Geraint':'Geraint';break;\ncase \"Danish\": voice = mf=='Male'?'Mads':'Naja';break;\ncase \"Dutch\": voice = mf=='Male'?'Ruben':'Lotte';break;\ncase \"French(FR)\": voice = mf=='Male'?'Mathieu':'Celine';break;\ncase \"French(CA)\": voice = mf=='Male'?'Chantal':'Chantal';break;\ncase \"German\": voice = mf=='Male'?'Hans':'Marlene';break;\ncase \"Icelandic\": voice = mf=='Male'?'Karl':'Dora';break;\ncase \"Italian\": voice = mf=='Male'?'Carla':'Giorgio';break;\ncase \"Japanese\": voice = mf=='Male'?'Takumi':'Mizuki';break;\ncase \"Korean\": voice = mf=='Male'?'Seoyan':'Seoyan';break;\ncase \"Norwegian\": voice = mf=='Male'?'Liv':'Liv';break;\ncase \"Polish\": voice = mf=='Male'?'Jacek':'Ewa';break;\ncase \"Portugese(BR)\": voice = mf=='Male'?'Ricardo':'Vitoria';break;\ncase \"Portugese(PT)\": voice = mf=='Male'?'Cristiano':'Ines';break;\ncase \"Romanian\": voice = mf=='Male'?'Carmen':'Carmen';break;\ncase \"Russian\": voice = mf=='Male'?'Maxim':'Tatyana';break;\ncase \"Spanish(ES)\": voice = mf=='Male'?'Enrique':'Conchita';break;\ncase \"Spanish(US)\": voice = mf=='Male'?'Miguel':'Penelope';break;\ncase \"Swedish\": voice = mf=='Male'?'Astrid':'Astrid';break;\ncase \"Turkish\": voice = mf=='Male'?'Filiz':'Filiz';break;\ncase \"Welsh\": voice = mf=='Male'?'Gwyneth':'Gwyneth';break;\ndefault: voice = mf=='Male'?'Brian':'Amy';\n}\n\n\n\n//build template for one dictation item\nvar template ='
';\ntemplate += '
';\ntemplate += '
';\ntemplate += '';\ntemplate += '
';\ntemplate +='
';\n\nvar container = \n\n$('#' + @@AUTOID@@ + \" li\" ).each(function(index) {\n var usetext = $(this).text();\n fetch_polly_url(usetext,voice,\n function(bulletindex){return function(pollyurl){\n\n //create the item and get the TTS audio\n var usetemplate = template.replace('@AUDIO@',pollyurl); \n usetemplate = usetemplate.replace(/@ID@/g, bulletindex);\n theitemscontainer.append(usetemplate);\n\n //add events\n var theinput= $('#dictationinput_' + bulletindex + ' input');\n var thestatus= $('#dictationstatus_' + bulletindex);\n var theaudio= $('#dictationplayer_' + bulletindex + ' audio');\n theinput.on('keyup',function(){\n if($(this).val()==usetext){\n thestatus.addClass('dictationcorrect');\n }else{\n thestatus.removeClass('dictationcorrect');\n }\n });//end of change event\n //we use function inside function so that: index = bullet index. (index value changing before callback called)\n }}(index)\n );\n\n });//end of each loop","style":"div.poodlldictationstatus {\n padding: 3px;\n border: black 1px solid;\n border-width: 2px;\n border-radius: 10px;\n width: 30px;\n text-align: center;\n}\ndiv.dictationcorrect{\n color: white;\nbackground-color: green;\n}\ndiv.row.dictationrow{\n max-width: 600px;\n min-height: 50px;\ndisplay: flex;\n}\ndiv.dictationrow input {\n width: 100%;\n}","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/flipclock.txt b/presets/flipclock.txt
new file mode 100644
index 0000000..d126ac5
--- /dev/null
+++ b/presets/flipclock.txt
@@ -0,0 +1 @@
+{"name":"flipclock","key":"flipclock","version":"1.0.0","instructions":"Shows a countdown clock","showatto":"1","showplayers":"0","requirecss":"https://cdnjs.cloudflare.com/ajax/libs/flipclock/0.7.8/flipclock.min.css","requirejs":"https://cdnjs.cloudflare.com/ajax/libs/flipclock/0.7.8/flipclock.min.js","shim":"FlipClock","defaults":"seconds=120","amd":"1","body":"","bodyend":"","script":"var clock = new FlipClock($('#' + @@AUTOID@@),@@seconds@@, {\nclockFace: 'MinuteCounter',\ncountdown: true\n});","style":"","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/glossaryttsreadercloud.txt b/presets/glossaryttsreadercloud.txt
new file mode 100644
index 0000000..069a361
--- /dev/null
+++ b/presets/glossaryttsreadercloud.txt
@@ -0,0 +1 @@
+{"name":"Glossary TTS Reader (Cloud)","key":"glossaryttsreadercloud","version":"1.0.1","instructions":"This is a small html5 audio player that will read aloud the enclosed text block. It works anywhere but mainly in the glossary popup (Uses Cloud Poodll) ","showatto":"1","showplayers":"0","requirecss":"","requirejs":"","shim":"","defaults":"play=\"fa-volume-up\",background=\"red\",color=\"#fff\",width=\"40\",height=\"40\",defaultvoice=\"Kendra|Justin|Amy|Emma|Russell\"","amd":"1","body":"\n\n\n\n
\n
","bodyend":"
\n
","script":"","style":"button.roundmini-audio-player{\n\tborder: none;\n\tcolor: #fff;\n}\nbutton.roundmini-audio-player .fa{\n font-size: 28px;\n}\n.activesentence {\n background: #c5efcf;\n /* font-weight: bold; */\n}","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/icontoggle.txt b/presets/icontoggle.txt
new file mode 100644
index 0000000..4d6d806
--- /dev/null
+++ b/presets/icontoggle.txt
@@ -0,0 +1 @@
+{"name":"Toggle with icon","key":"icontoggle","version":"1.0.2","instructions":"Enter content to toggle in between the tags in the page\n","showatto":"1","showplayers":"0","requirecss":"","requirejs":"","shim":"","defaults":"closedtext=\"Show\",opentext=\"Hide\"","amd":"1","body":"
@@closedtext@@
\n\t
","bodyend":"
","script":"var state=0;\n$(\"#\" + @@AUTOID@@).click(function(e){\n$(\"#\" + @@AUTOID@@ + \"_target\").toggle(\n{duration: 300, complete: doToggleState}\n); \nreturn false;\n});\n\nvar doToggleState=function(){\nvar tb = $(\"#\" + @@AUTOID@@ + ' .togglebutton');\nif(state==0){\ntb.removeClass('fa-toggle-off');\ntb.addClass('fa-toggle-on');\n $(\"#\" + @@AUTOID@@ + ' .toggletext').text(@@opentext@@);\nstate=1;\n}else{\ntb.removeClass('fa-toggle-on');\ntb.addClass('fa-toggle-off');\n $(\"#\" + @@AUTOID@@ + ' .toggletext').text(@@closedtext@@);\nstate=0;\n}\n};","style":"","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/icontoggleright.txt b/presets/icontoggleright.txt
new file mode 100644
index 0000000..9953b02
--- /dev/null
+++ b/presets/icontoggleright.txt
@@ -0,0 +1 @@
+{"name":"Toggle with icon (horizontal)","key":"icontoggleright","version":"1.0.2","instructions":"Enter content to toggle in between the tags in the page. It will display to the right of the toggle buttons\n","showatto":"1","showplayers":"0","requirecss":"","requirejs":"","shim":"","defaults":"closedtext=\"Show\",opentext=\"Hide\"","amd":"1","body":"
\n
\n
\n
@@closedtext@@
\n
\n
\n \t
\n\n\n","bodyend":"
\n
\n
\n
","script":"var state=0;\n$(\"#\" + @@AUTOID@@).click(function(e){\n$(\"#\" + @@AUTOID@@ + \"_target\").toggle(\n{duration: 300, complete: doToggleState}\n); \nreturn false;\n});\n\nvar doToggleState=function(){\nvar tb = $(\"#\" + @@AUTOID@@ + ' .togglebutton');\nif(state==0){\ntb.removeClass('fa-toggle-off');\ntb.addClass('fa-toggle-on');\n $(\"#\" + @@AUTOID@@ + ' .toggletext').text(@@opentext@@);\nstate=1;\n}else{\ntb.removeClass('fa-toggle-on');\ntb.addClass('fa-toggle-off');\n $(\"#\" + @@AUTOID@@ + ' .toggletext').text(@@closedtext@@);\nstate=0;\n}\n};","style":"","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/onceplayer.txt b/presets/onceplayer.txt
new file mode 100644
index 0000000..c144da5
--- /dev/null
+++ b/presets/onceplayer.txt
@@ -0,0 +1 @@
+{"name":"OncePlayerJS","key":"onceplayer","version":"1.0.1","instructions":"This is an HTML5 audio player that will only play the audio file it manages once.","showatto":"0","showplayers":"1","requirecss":"//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css","requirejs":"","shim":"","defaults":"canpause=0,pause=fa-stop,play=fa-play,over=fa-minus,playing=fa-circle-o-notch","amd":"1","body":"
\n","bodyend":"","script":"//Define interactive_transcript (it) see cloud poodll submission amd/src/interactivetranscript.js\nvar it={\n init: function(itoptions){\n var config={};\n var that =this; \n config.settings ={};\n if(itoptions['theplayer']) {\n config.prefix = itoptions['cssprefix'];\n config.theplayer = itoptions['theplayer'];\n config.dummyplayerid = itoptions['dummyplayerid'];\n config.dummyplayer = $('#' + config.dummyplayerid)[0]; \n config.title = itoptions['title'];\n config.containerid = itoptions['containerid'];\n config.scrollingthing = itoptions['scrollingthing'];\n config.currentTrack = 0;\n config.theduration= itoptions['duration'];\n config.textTranscript='';\n var transcript = this.transcript(config);\n\n $('#' + itoptions['scrollcontainerid']).append(transcript.el());\n \n //unregisgter old, then register download transcript event\n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'download').off('click');\n \n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'download').on('click',function(){\n that.downloadTranscript(config);\n return false;\n });\n \n }//end of if player\n\n },\n\n downloadTranscript: function(config){\n\t\t\t\tvar element = document.createElement('a');\n\t\t\t\telement.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(config.textTranscript));\n\t\t\t\telement.setAttribute('download', config.title + '.txt');\n\t\t\t\telement.style.display = 'none';\n\t\t\t\tdocument.body.appendChild(element);\n\t\t\t\telement.click();\n\t\t\t\tdocument.body.removeChild(element);\n },\n\n // Defaults\n defaults: {\n autoscroll: true,\n clickArea: 'line', //the clickable part of line text,line,timestamp, none\n showTrackSelector: false, //the drop down box of caption tracks\n followPlayerTrack: true,\n scrollToCenter: false, //show current text in center\n stopScrollWhenInUse: false, //stop scrolling when user interacting\n },\n\n /*global */\n utils: {\n prefix: 'transcript',\n secondsToTime: function (timeInSeconds) {\n var hour = Math.floor(timeInSeconds / 3600);\n var min = Math.floor(timeInSeconds % 3600 / 60);\n var sec = Math.floor(timeInSeconds % 60);\n sec = (sec < 10) ? '0' + sec : sec;\n min = (hour > 0 && min < 10) ? '0' + min : min;\n if (hour > 0) {\n return hour + ':' + min + ':' + sec;\n }\n return min + ':' + sec;\n },\n localize: function (string) {\n return string; // TODO: do something here;\n },\n createEl: function (elementName, className) {\n className = className || '';\n var el = document.createElement(elementName);\n el.className = className;\n return el;\n },\n extend: function(obj) {\n var type = typeof obj;\n if (!(type === 'function' || type === 'object' && !!obj)) {\n return obj;\n }\n var source, prop;\n for (var i = 1, length = arguments.length; i < length; i++) {\n source = arguments[i];\n for (prop in source) {\n obj[prop] = source[prop];\n }\n }\n return obj;\n }\n \n },\n\n eventEmitter: {\n handlers_: [],\n on: function on (object, eventtype, callback) {\n if (typeof callback === 'function') {\n this.handlers_.push([object, eventtype, callback]);\n } else {\n throw new TypeError('Callback is not a function.');\n }\n },\n trigger: function trigger (object, eventtype) {\n this.handlers_.forEach( function(h) {\n if (h[0] === object &&\n h[1] === eventtype) {\n h[2].apply();\n }\n });\n }\n },\n\n scrollerProto: function(config) {\n\n var initHandlers = function (el) {\n var self = this;\n // The scroll event. We want to keep track of when the user is scrolling the transcript.\n el.addEventListener('scroll', function () {\n if (self.isAutoScrolling) {\n\n // If isAutoScrolling was set to true, we can set it to false and then ignore this event.\n // It wasn't the user.\n self.isAutoScrolling = false; // event handled\n } else {\n\n // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class.\n self.userIsScrolling = true;\n el.classList.add('is-inuse');\n }\n });\n\n // The mouseover event.\n el.addEventListener('mouseenter', function () {\n self.mouseIsOverTranscript = true;\n });\n el.addEventListener('mouseleave', function () {\n self.mouseIsOverTranscript = false;\n\n // Have a small delay before deciding user as done interacting.\n setTimeout(function () {\n\n // Make sure the user didn't move the pointer back in.\n if (!self.mouseIsOverTranscript) {\n self.userIsScrolling = false;\n el.classList.remove('is-inuse');\n }\n }, 1000);\n });\n };\n\n // Init instance variables\n var init = function (element) {\n this.element = element;\n this.userIsScrolling = false;\n\n //default to true in case user isn't using a mouse;\n this.mouseIsOverTranscript = true;\n this.isAutoScrolling = true;\n initHandlers.call(this, this.element);\n return this;\n };\n\n // Easing function for smoothness.\n var easeOut = function (time, start, change, duration) {\n return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2));\n };\n\n // Animate the scrolling.\n var scrollTo = function (element, newPos, duration) {\n var startTime = Date.now();\n var startPos = element.scrollTop;\n var self = this;\n\n // Don't try to scroll beyond the limits. You won't get there and this will loop forever.\n newPos = Math.max(0, newPos);\n newPos = Math.min(element.scrollHeight - element.clientHeight, newPos);\n var change = newPos - startPos;\n \n //if not animating\n if(true){ \n element.scrollTop= newPos;\n //if animating ... which doesn't work\n }else{\n // This inner function is called until the elements scrollTop reaches newPos.\n var updateScroll = function () {\n var now = Date.now();\n var time = now - startTime;\n self.isAutoScrolling = true;\n element.scrollTop = easeOut(time, startPos, change, duration);\n if (element.scrollTop !== newPos) {\n window.requestAnimationFrame(updateScroll, element);\n }\n };//end of update scroll\n window.requestAnimationFrame(updateScroll, element);\n }\n };\n\n // Scroll an element's parent so the element is brought into view.\n var scrollToElement = function (element) {\n if (this.canScroll()) {\n //elem=\"line\" parent=\"body\" parent.parent=\"scrolling\" parent.parent.parent.scrollable\n var parent = element.parentElement.parentElement.parentElement;\n var parentOffsetBottom = parent.offsetTop + parent.clientHeight;\n var elementOffsetBottom = element.offsetTop + element.clientHeight;\n var relTop = element.offsetTop;\n var relBottom = (element.offsetTop + element.clientHeight);\n var centerPosCorrection = 0;\n var newPos;\n /*\n console.log('element.offsetTop: ' + element.offsetTop );\n console.log('element.clientHeight: ' + element.clientHeight );\n console.log('parent.offsetTop: ' + parent.offsetTop );\n console.log('parent.scrollTop: ' + parent.scrollTop );\n console.log('parent.clientHeight: ' + parent.clientHeight );\n console.log(element);\n console.log(parent);\n */ \n \n //scroll to center if we must\n if (config.settings.scrollToCenter){\n centerPosCorrection = Math.round(parent.clientHeight/2 - element.clientHeight/2);\n }\n // If the top of the line is above the top of the parent view, were scrolling up,\n // so we want to move the top of the element downwards to match the top of the parent.\n if (relTop < parent.scrollTop + centerPosCorrection) {\n newPos = element.offsetTop -centerPosCorrection;\n\n // If the bottom of the line is below the parent view, we're scrolling down, so we want the\n // bottom edge of the line to move up to meet the bottom edge of the parent.\n } else if (relBottom > (parent.scrollTop + parent.clientHeight) - centerPosCorrection) {\n newPos = elementOffsetBottom + centerPosCorrection;\n }\n\n // Don't try to scroll if we haven't set a new position. If we didn't\n // set a new position the line is already in view (i.e. It's not above\n // or below the view)\n // And don't try to scroll when the element is already in position.\n if (newPos !== undefined && parent.scrollTop !== newPos) {\n scrollTo(parent, newPos, 400);\n }\n }\n };\n\n\n // Return whether the element is scrollable.\n var canScroll = function () {\n var el = this.element;\n //console.log(el.scrollHeight + ' ' + el.offsetHeight);\n return true;//el.scrollHeight > el.offsetHeight;\n };\n\n // Return whether the user is interacting with the transcript.\n var inUse = function () {\n return this.userIsScrolling;\n };\n\n return {\n init: init,\n to : scrollToElement,\n canScroll : canScroll,\n inUse : inUse\n }\n },\n\n scroller: function(element,config) {\n return Object.create(this.scrollerProto(config)).init(element);\n },\n\n\n /*global config*/\n trackList: function(config) {\n var activeTrack;\n return {\n get: function () {\n var validTracks = [];\n var i, track;\n config.tracks = config.dummyplayer.textTracks;\n for (i = 0; i < config.tracks.length; i++) {\n track = config.tracks[i];\n if (track.kind === 'captions' || track.kind === 'subtitles') {\n validTracks.push(track);\n }\n }\n return validTracks;\n },\n active: function (tracks) {\n var i, track;\n for (i = 0; i < config.tracks.length; i++) {\n track = config.tracks[i];\n if (track.mode === 'showing') {\n activeTrack = track;\n return track;\n }\n }\n // fallback to first track\n return activeTrack || tracks[0];\n },\n };\n },\n\n /*globals utils, eventEmitter,scrollable*/\n\n widget: function(config) {\n var that = this;\n var thewidget = {};\n thewidget.element = {};\n thewidget.body = {};\n var on = function (event, callback) {\n eventEmitter.on(that, event, callback);\n };\n var trigger = function (event) {\n eventEmitter.trigger(that, event);\n };\n var initToolbar = function () {\n $('#' + config.containerid + ' .' + config.prefix + 'title').text(config.title);\n };\n var createSelector = function (){\n var selector = that.utils.createEl('select', config.prefix + '-selector');\n config.validTracks.forEach(function (track, i) {\n var option = document.createElement('option');\n option.value = i;\n option.textContent = track.label + ' (' + track.language + ')';\n selector.appendChild(option);\n });\n selector.addEventListener('change', function (e) {\n setTrack(document.querySelector('#' + config.prefix + '-' + config.dummyplayerid + ' option:checked').value);\n trigger('trackchanged');\n });\n return selector;\n };\n var clickToSeekHandler = function (event) {\n var clickedClasses = event.target.classList;\n var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin');\n if (clickedTime !== undefined && clickedTime !== null) { // can be zero\n if ((config.settings.clickArea === 'line') || // clickArea: 'line' activates on all elements\n (config.settings.clickArea === 'timestamp' && clickedClasses.contains(config.prefix + '-timestamp')) ||\n (config.settings.clickArea === 'text' && clickedClasses.contains(config.prefix + '-text'))) {\n config.theplayer.currentTime= clickedTime;\n }\n }\n };\n var createLine = function (cue) {\n var line = that.utils.createEl('div', config.prefix +'-line');\n var timestamp = that.utils.createEl('span',config.prefix + '-timestamp');\n var text = that.utils.createEl('span', config.prefix + '-text');\n line.setAttribute('data-begin', cue.startTime);\n line.setAttribute('tabindex', thewidget._options.tabIndex || 0);\n timestamp.textContent = that.utils.secondsToTime(cue.startTime);\n text.innerHTML = cue.text;\n line.appendChild(timestamp);\n line.appendChild(text);\n return line;\n };\n \n var createTranscriptBody = function (track) {\n if (typeof track !== 'object') {\n track = config.dummyplayer.textTracks()[track];\n }\n var body = that.utils.createEl('div', config.prefix + '-body');\n var line, i;\n var fragment = document.createDocumentFragment();\n // activeCues returns null when the track isn't loaded (for now?)\n if (!track.activeCues) {\n // If cues aren't loaded, set mode to hidden, wait, and try again.\n // But don't hide an active track. In that case, just wait and try again.\n if (track.mode !== 'showing') {\n track.mode = 'hidden';\n }\n window.setTimeout(function() {\n createTranscriptBody(track);\n }, 100);\n } else {\n var cues = track.cues;\n var textTranscript =[];\n for (i = 0; i < cues.length; i++) {\n line = createLine(cues[i]);\n if(!(cues[i].text==='')){\n textTranscript.push(cues[i].text);\n }\n fragment.appendChild(line);\n }\n //prepare text transcript\n config.textTranscript=textTranscript.join(' ');\n \n //build body of transcript\n body.innerHTML = '';\n body.appendChild(fragment);\n body.setAttribute('lang', track.language);\n body.scroll = that.scroller(thewidget.element,config);\n body.addEventListener('click', clickToSeekHandler);\n thewidget.element.replaceChild(body, thewidget.body);\n thewidget.body = body;\n }\n\n };\n var create = function (options) {\n initToolbar();\n var el = document.createElement('div');\n thewidget._options = options;\n thewidget.element = el;\n el.setAttribute('id', config.scrollingthing);\n \n if (config.settings.showTrackSelector) {\n var selector = createSelector();\n el.appendChild(selector);\n }\n thewidget.body = that.utils.createEl('div',config.prefix + '-body');\n el.appendChild(thewidget.body);\n setTrack(config.currentTrack);\n return this;\n };\n var setTrack = function (track, trackCreated) {\n createTranscriptBody(track, trackCreated);\n };\n var setCue = function (time) {\n var active, i, line, begin, end;\n var lines = thewidget.body.children;\n for (i = 0; i < lines.length; i++) {\n line = lines[i];\n begin = line.getAttribute('data-begin');\n if (i < lines.length - 1) {\n end = lines[i + 1].getAttribute('data-begin');\n } else {\n end = config.theduration;\n }\n if (time > begin && time < end) {\n if (!line.classList.contains('is-active')) { // don't update if it hasn't changed\n line.classList.add('is-active');\n if (config.settings.autoscroll && !(config.settings.stopScrollWhenInUse && thewidget.body.scroll.inUse())) {\n thewidget.body.scroll.to(line);\n }\n }\n } else {\n line.classList.remove('is-active');\n }\n }\n };\n var el = function () {\n return thewidget.element;\n };\n return {\n create: create,\n setTrack: setTrack,\n setCue: setCue,\n el : el,\n on: on,\n trigger: trigger,\n };\n },\n\n transcript: function(config){\n var that=this;\n var options=this.defaults;\n this.utils.prefix='transcript';\n\n config.validTracks = this.trackList(config).get();\n config.currentTrack = this.trackList(config).active(config.validTracks);\n config.settings = options;\n config.widget = this.widget(config).create(options);\n\n var timeUpdate = function (eventdata) {\n config.widget.setCue(config.theplayer.currentTime);\n };\n var updateTrack = function () {\n config.currentTrack = that.trackList(config).active(config.validTracks);\n config.widget.setTrack(config.currentTrack);\n };\n if (config.validTracks.length > 0) {\n config.theplayer.ontimeupdate =timeUpdate;\n } else {\n throw new Error('transcript: No tracks found!');\n }\n return {\n el: function () {\n return config.widget.el();\n },\n setTrack: config.widget.setTrack\n };\n }\n };//end of interactive transcript\n\nvar processNewVideo = function(player, itoptions){\n //first of all clear the existing transcript if we have one\n $('#' + @@AUTOID@@ + '_transcriptscrollable').empty();\n\t itoptions.title=@@TITLE@@;\n\t itoptions.theduration=player.duration;\n\t //finally load interactive transcript\n\t it.init(itoptions); \n\n\n}// end of processNewVideo\n \n//Set up our player \nvar theplayer = $('#' + @@AUTOID@@ + '_player')[0];\n\n \n //init our interactive transcript options (IT)\nvar itoptions = {};\nitoptions.containerid = @@AUTOID@@ + '_transcriptcontainer';\nitoptions.scrollcontainerid = @@AUTOID@@ + '_transcriptscrollable';\nitoptions.scrollingthing = @@AUTOID@@ + '_transcriptscrolling';\nitoptions.dummyplayerid = @@AUTOID@@ + '_dummyplayer';\nitoptions.splitter = @@AUTOID@@ + '_splitter';\nitoptions.playerid = @@AUTOID@@ + '_player';\nitoptions.theplayer = theplayer;\nitoptions.cssprefix = 'filterpoodll_sia_transcript';\n\n\n//window stuff for resizing\nif ($(window).width() >= 992) {\n $('#' + @@AUTOID@@ + '_player').resizable({\n handles: {'e' : $('#' + @@AUTOID@@ + '_splitter')},\n resizeHeight: false\n });\n}\n\n//display toolbar buttons (they appear too soon otherwise)\n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'toolbar .' + itoptions.cssprefix + 'tools').show();\n\n//Toggle time stamp \n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'showtime').on('click',function(){\n \n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + '-body').toggleClass('notimestamp');\n return false;\n\n});\n\n//kick it all off\nprocessNewVideo(theplayer,itoptions);\n","style":".fpv_flexrow{\n display: flex;flex-direction: row;\n}\n.fpv_flexcol{\n display: flex;flex-direction: column;\n max-width: 800px;\n\n}\n.fpv_flexcol .sivvideo{\n\twidth: 100%;\n} \n.fpv_flexcol .filterpoodll_sia_transcriptcontainer{\n height: 320px;\n}\n.sivvideo{\n\twidth: 50%;\n} \n.filterpoodll_sia_transcriptcontainer{\n flex: 1 1 auto;\n}\n.transcript-body{\n\theight: 100% !important;\n}\n.filterpoodll_sia_transcriptcontainer{\n height: 480px;\n}\n.filterpoodll_sia_transcriptscrollable {\n height: calc(100% - 37px);\n}\n@media screen and (max-width: 992px) {\n\t.filterpoodll_sia_transcriptmain_container{\n display: block !important;\n }\n .splitter{\n display: none;\n }\n\n .filterpoodll_sia_transcripttools{\n display: none;\n }\n\n .sivvideo{\n\t\twidth: 100%;\n\t\tfloat: none;\n\t\theight: 200px;\n } \n .filterpoodll_sia_transcriptcontainer{\n\t\tfloat: none;\n\t\tmargin-left: 0%;\t\t\n }\n .filterpoodll_sia_transcriptcontainer{\n\t\theight: 200px !important;\n }\n .filterpoodll_sia_transcriptscrollable {\n\t\theight: calc(100%-37px);\n /* 163px !important; height - toolbar height */\n }\n}\n\n/* interactive transcript */\n.filterpoodll_sia_transcriptcontainer {\n font-family: Arial, sans-serif;\n border: 1px solid #111;\n}\n\n.filterpoodll_sia_transcript-header {\n height: 19px;\n padding: 2px;\n font-weight: bold;\n text-align: center;\n}\n.filterpoodll_sia_transcript-selector {\n height: 25px;\n}\n.filterpoodll_sia_transcript-body {\n overflow-y: scroll;\n background-color: #e7e7e7;\n position: relative;\n margin: auto;\n}\n\n.filterpoodll_sia_transcript-line {\n position: relative;\n padding: 5px;\n cursor: pointer;\n line-height: 1.3;\n}\n\n.filterpoodll_sia_transcript-line:nth-child(odd) {\n background-color: #f5f5f5;\n}\n\n\n.filterpoodll_sia_transcript-timestamp {\n position: absolute;\n display: inline-block;\n color: #333;\n width: 50px;\n}\n\n.filterpoodll_sia_transcript-text {\n display: block;\n margin-left: 50px;\n}\n\n.filterpoodll_sia_transcript-line:hover,\n.filterpoodll_sia_transcript-line:hover .filterpoodll_sia_transcript-timestamp,\n.filterpoodll_sia_transcript-line:hover .filterpoodll_sia_transcript-text {\n background-color: #777;\n color: #e7e7e7;\n}\n\n.filterpoodll_sia_transcript-line.is-active,\n.filterpoodll_sia_transcript-line.is-active .filterpoodll_sia_transcript-timestamp,\n.filterpoodll_sia_transcript-line.is-active .filterpoodll_sia_transcript-text {\n background-color: #555;\n color: #e7e7e7;\n width: auto;\n}\n\n.filterpoodll_sia_transcripttitle {\n /* Set line height to same height at toolbar */\n line-height: 35px;\n margin-left: 5px;\n font-weight: bold;\n}\n\n.filterpoodll_sia_transcripttoolbar {\n background-color: #CCC;\n max-height: 35px;\n min-height: 35px;\n overflow:hidden\n}\n.filterpoodll_sia_transcriptscrollable {\n width: 100%;\n font-family: Arial, sans-serif;\n overflow-x: hidden;\n overflow-y: scroll;\n scroll-behavior: smooth;\n}\n.filterpoodll_sia_transcripttools {\n float: right;\n}\n\n/* toggle timestamp on and off */\n.filterpoodll_sia_transcript-body.notimestamp .filterpoodll_sia_transcript-timestamp {\n display: none;\n}\n\n.filterpoodll_sia_transcript-body.notimestamp .filterpoodll_sia_transcript-text {\n margin-left: 5px;\n}\n","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/superinteractiveaudiowidget.txt b/presets/superinteractiveaudiowidget.txt
new file mode 100644
index 0000000..c5c1d20
--- /dev/null
+++ b/presets/superinteractiveaudiowidget.txt
@@ -0,0 +1 @@
+{"name":"Super Interactive Audio Widget","key":"superinteractiveaudiowidget","version":"1.0.3","instructions":"Shows a audio + interactive transcript. The captions are made and edited here on Moodle. This is a widget. So the audio player or link should be inserted between the widget tags","showatto":"1","showplayers":"0","requirecss":"","requirejs":"","shim":"","defaults":"","amd":"1","body":"
","script":"//Define interactive_transcript (it) see cloud poodll submission amd/src/interactivetranscript.js\nvar it={\n init: function(itoptions){\n var config={};\n var that =this; \n config.settings ={};\n if(itoptions['theplayer']) {\n config.prefix = itoptions['cssprefix'];\n config.theplayer = itoptions['theplayer'];\n config.dummyplayerid = itoptions['dummyplayerid'];\n config.dummyplayer = $('#' + config.dummyplayerid)[0]; \n config.title = itoptions['title'];\n config.containerid = itoptions['containerid'];\n config.scrollingthing = itoptions['scrollingthing'];\n config.currentTrack = 0;\n config.theduration= itoptions['duration'];\n config.textTranscript='';\n var transcript = this.transcript(config);\n\n $('#' + itoptions['scrollcontainerid']).append(transcript.el());\n \n //unregisgter old, then register download transcript event\n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'download').off('click');\n \n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'download').on('click',function(){\n that.downloadTranscript(config);\n return false;\n });\n \n }//end of if player\n\n },\n\n downloadTranscript: function(config){\n\t\t\t\tvar element = document.createElement('a');\n\t\t\t\telement.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(config.textTranscript));\n\t\t\t\telement.setAttribute('download', config.title + '.txt');\n\t\t\t\telement.style.display = 'none';\n\t\t\t\tdocument.body.appendChild(element);\n\t\t\t\telement.click();\n\t\t\t\tdocument.body.removeChild(element);\n },\n\n // Defaults\n defaults: {\n autoscroll: true,\n clickArea: 'line', //the clickable part of line text,line,timestamp, none\n showTrackSelector: false, //the drop down box of caption tracks\n followPlayerTrack: true,\n scrollToCenter: false, //show current text in center\n stopScrollWhenInUse: false, //stop scrolling when user interacting\n },\n\n /*global */\n utils: {\n prefix: 'transcript',\n secondsToTime: function (timeInSeconds) {\n var hour = Math.floor(timeInSeconds / 3600);\n var min = Math.floor(timeInSeconds % 3600 / 60);\n var sec = Math.floor(timeInSeconds % 60);\n sec = (sec < 10) ? '0' + sec : sec;\n min = (hour > 0 && min < 10) ? '0' + min : min;\n if (hour > 0) {\n return hour + ':' + min + ':' + sec;\n }\n return min + ':' + sec;\n },\n localize: function (string) {\n return string; // TODO: do something here;\n },\n createEl: function (elementName, className) {\n className = className || '';\n var el = document.createElement(elementName);\n el.className = className;\n return el;\n },\n extend: function(obj) {\n var type = typeof obj;\n if (!(type === 'function' || type === 'object' && !!obj)) {\n return obj;\n }\n var source, prop;\n for (var i = 1, length = arguments.length; i < length; i++) {\n source = arguments[i];\n for (prop in source) {\n obj[prop] = source[prop];\n }\n }\n return obj;\n }\n \n },\n\n eventEmitter: {\n handlers_: [],\n on: function on (object, eventtype, callback) {\n if (typeof callback === 'function') {\n this.handlers_.push([object, eventtype, callback]);\n } else {\n throw new TypeError('Callback is not a function.');\n }\n },\n trigger: function trigger (object, eventtype) {\n this.handlers_.forEach( function(h) {\n if (h[0] === object &&\n h[1] === eventtype) {\n h[2].apply();\n }\n });\n }\n },\n\n scrollerProto: function(config) {\n\n var initHandlers = function (el) {\n var self = this;\n // The scroll event. We want to keep track of when the user is scrolling the transcript.\n el.addEventListener('scroll', function () {\n if (self.isAutoScrolling) {\n\n // If isAutoScrolling was set to true, we can set it to false and then ignore this event.\n // It wasn't the user.\n self.isAutoScrolling = false; // event handled\n } else {\n\n // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class.\n self.userIsScrolling = true;\n el.classList.add('is-inuse');\n }\n });\n\n // The mouseover event.\n el.addEventListener('mouseenter', function () {\n self.mouseIsOverTranscript = true;\n });\n el.addEventListener('mouseleave', function () {\n self.mouseIsOverTranscript = false;\n\n // Have a small delay before deciding user as done interacting.\n setTimeout(function () {\n\n // Make sure the user didn't move the pointer back in.\n if (!self.mouseIsOverTranscript) {\n self.userIsScrolling = false;\n el.classList.remove('is-inuse');\n }\n }, 1000);\n });\n };\n\n // Init instance variables\n var init = function (element) {\n this.element = element;\n this.userIsScrolling = false;\n\n //default to true in case user isn't using a mouse;\n this.mouseIsOverTranscript = true;\n this.isAutoScrolling = true;\n initHandlers.call(this, this.element);\n return this;\n };\n\n // Easing function for smoothness.\n var easeOut = function (time, start, change, duration) {\n return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2));\n };\n\n // Animate the scrolling.\n var scrollTo = function (element, newPos, duration) {\n var startTime = Date.now();\n var startPos = element.scrollTop;\n var self = this;\n\n // Don't try to scroll beyond the limits. You won't get there and this will loop forever.\n newPos = Math.max(0, newPos);\n newPos = Math.min(element.scrollHeight - element.clientHeight, newPos);\n var change = newPos - startPos;\n \n //if not animating\n if(true){ \n element.scrollTop= newPos;\n //if animating ... which doesn't work\n }else{\n // This inner function is called until the elements scrollTop reaches newPos.\n var updateScroll = function () {\n var now = Date.now();\n var time = now - startTime;\n self.isAutoScrolling = true;\n element.scrollTop = easeOut(time, startPos, change, duration);\n if (element.scrollTop !== newPos) {\n window.requestAnimationFrame(updateScroll, element);\n }\n };//end of update scroll\n window.requestAnimationFrame(updateScroll, element);\n }\n };\n\n // Scroll an element's parent so the element is brought into view.\n var scrollToElement = function (element) {\n if (this.canScroll()) {\n //elem=\"line\" parent=\"body\" parent.parent=\"scrolling\" parent.parent.parent.scrollable\n var parent = element.parentElement.parentElement.parentElement;\n var parentOffsetBottom = parent.offsetTop + parent.clientHeight;\n var elementOffsetBottom = element.offsetTop + element.clientHeight;\n var relTop = element.offsetTop;\n var relBottom = (element.offsetTop + element.clientHeight);\n var centerPosCorrection = 0;\n var newPos;\n /*\n console.log('element.offsetTop: ' + element.offsetTop );\n console.log('element.clientHeight: ' + element.clientHeight );\n console.log('parent.offsetTop: ' + parent.offsetTop );\n console.log('parent.scrollTop: ' + parent.scrollTop );\n console.log('parent.clientHeight: ' + parent.clientHeight );\n console.log(element);\n console.log(parent);\n */ \n \n //scroll to center if we must\n if (config.settings.scrollToCenter){\n centerPosCorrection = Math.round(parent.clientHeight/2 - element.clientHeight/2);\n }\n // If the top of the line is above the top of the parent view, were scrolling up,\n // so we want to move the top of the element downwards to match the top of the parent.\n if (relTop < parent.scrollTop + centerPosCorrection) {\n newPos = element.offsetTop -centerPosCorrection;\n\n // If the bottom of the line is below the parent view, we're scrolling down, so we want the\n // bottom edge of the line to move up to meet the bottom edge of the parent.\n } else if (relBottom > (parent.scrollTop + parent.clientHeight) - centerPosCorrection) {\n newPos = elementOffsetBottom + centerPosCorrection;\n }\n\n // Don't try to scroll if we haven't set a new position. If we didn't\n // set a new position the line is already in view (i.e. It's not above\n // or below the view)\n // And don't try to scroll when the element is already in position.\n if (newPos !== undefined && parent.scrollTop !== newPos) {\n scrollTo(parent, newPos, 400);\n }\n }\n };\n\n\n // Return whether the element is scrollable.\n var canScroll = function () {\n var el = this.element;\n //console.log(el.scrollHeight + ' ' + el.offsetHeight);\n return true;//el.scrollHeight > el.offsetHeight;\n };\n\n // Return whether the user is interacting with the transcript.\n var inUse = function () {\n return this.userIsScrolling;\n };\n\n return {\n init: init,\n to : scrollToElement,\n canScroll : canScroll,\n inUse : inUse\n }\n },\n\n scroller: function(element,config) {\n return Object.create(this.scrollerProto(config)).init(element);\n },\n\n\n /*global config*/\n trackList: function(config) {\n var activeTrack;\n return {\n get: function () {\n var validTracks = [];\n var i, track;\n config.tracks = config.dummyplayer.textTracks;\n for (i = 0; i < config.tracks.length; i++) {\n track = config.tracks[i];\n if (track.kind === 'captions' || track.kind === 'subtitles') {\n validTracks.push(track);\n }\n }\n return validTracks;\n },\n active: function (tracks) {\n var i, track;\n for (i = 0; i < config.tracks.length; i++) {\n track = config.tracks[i];\n if (track.mode === 'showing') {\n activeTrack = track;\n return track;\n }\n }\n // fallback to first track\n return activeTrack || tracks[0];\n },\n };\n },\n\n /*globals utils, eventEmitter,scrollable*/\n\n widget: function(config) {\n var that = this;\n var thewidget = {};\n thewidget.element = {};\n thewidget.body = {};\n var on = function (event, callback) {\n eventEmitter.on(that, event, callback);\n };\n var trigger = function (event) {\n eventEmitter.trigger(that, event);\n };\n var initToolbar = function () {\n $('#' + config.containerid + ' .' + config.prefix + 'title').text(config.title);\n };\n var createSelector = function (){\n var selector = that.utils.createEl('select', config.prefix + '-selector');\n config.validTracks.forEach(function (track, i) {\n var option = document.createElement('option');\n option.value = i;\n option.textContent = track.label + ' (' + track.language + ')';\n selector.appendChild(option);\n });\n selector.addEventListener('change', function (e) {\n setTrack(document.querySelector('#' + config.prefix + '-' + config.dummyplayerid + ' option:checked').value);\n trigger('trackchanged');\n });\n return selector;\n };\n var clickToSeekHandler = function (event) {\n var clickedClasses = event.target.classList;\n var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin');\n if (clickedTime !== undefined && clickedTime !== null) { // can be zero\n if ((config.settings.clickArea === 'line') || // clickArea: 'line' activates on all elements\n (config.settings.clickArea === 'timestamp' && clickedClasses.contains(config.prefix + '-timestamp')) ||\n (config.settings.clickArea === 'text' && clickedClasses.contains(config.prefix + '-text'))) {\n config.theplayer.currentTime= clickedTime;\n }\n }\n };\n var createLine = function (cue) {\n var line = that.utils.createEl('div', config.prefix +'-line');\n var timestamp = that.utils.createEl('span',config.prefix + '-timestamp');\n var text = that.utils.createEl('span', config.prefix + '-text');\n line.setAttribute('data-begin', cue.startTime);\n line.setAttribute('tabindex', thewidget._options.tabIndex || 0);\n timestamp.textContent = that.utils.secondsToTime(cue.startTime);\n text.innerHTML = cue.text;\n line.appendChild(timestamp);\n line.appendChild(text);\n return line;\n };\n \n var createTranscriptBody = function (track) {\n if (typeof track !== 'object') {\n track = config.dummyplayer.textTracks()[track];\n }\n var body = that.utils.createEl('div', config.prefix + '-body');\n var line, i;\n var fragment = document.createDocumentFragment();\n // activeCues returns null when the track isn't loaded (for now?)\n if (!track.activeCues) {\n // If cues aren't loaded, set mode to hidden, wait, and try again.\n // But don't hide an active track. In that case, just wait and try again.\n if (track.mode !== 'showing') {\n track.mode = 'hidden';\n }\n window.setTimeout(function() {\n createTranscriptBody(track);\n }, 100);\n } else {\n var cues = track.cues;\n var textTranscript =[];\n for (i = 0; i < cues.length; i++) {\n line = createLine(cues[i]);\n if(!(cues[i].text==='')){\n textTranscript.push(cues[i].text);\n }\n fragment.appendChild(line);\n }\n //prepare text transcript\n config.textTranscript=textTranscript.join(' ');\n \n //build body of transcript\n body.innerHTML = '';\n body.appendChild(fragment);\n body.setAttribute('lang', track.language);\n body.scroll = that.scroller(thewidget.element,config);\n body.addEventListener('click', clickToSeekHandler);\n thewidget.element.replaceChild(body, thewidget.body);\n thewidget.body = body;\n }\n\n };\n var create = function (options) {\n initToolbar();\n var el = document.createElement('div');\n thewidget._options = options;\n thewidget.element = el;\n el.setAttribute('id', config.scrollingthing);\n \n if (config.settings.showTrackSelector) {\n var selector = createSelector();\n el.appendChild(selector);\n }\n thewidget.body = that.utils.createEl('div',config.prefix + '-body');\n el.appendChild(thewidget.body);\n setTrack(config.currentTrack);\n return this;\n };\n var setTrack = function (track, trackCreated) {\n createTranscriptBody(track, trackCreated);\n };\n var setCue = function (time) {\n var active, i, line, begin, end;\n var lines = thewidget.body.children;\n for (i = 0; i < lines.length; i++) {\n line = lines[i];\n begin = line.getAttribute('data-begin');\n if (i < lines.length - 1) {\n end = lines[i + 1].getAttribute('data-begin');\n } else {\n end = config.theduration;\n }\n if (time > begin && time < end) {\n if (!line.classList.contains('is-active')) { // don't update if it hasn't changed\n line.classList.add('is-active');\n if (config.settings.autoscroll && !(config.settings.stopScrollWhenInUse && thewidget.body.scroll.inUse())) {\n thewidget.body.scroll.to(line);\n }\n }\n } else {\n line.classList.remove('is-active');\n }\n }\n };\n var el = function () {\n return thewidget.element;\n };\n return {\n create: create,\n setTrack: setTrack,\n setCue: setCue,\n el : el,\n on: on,\n trigger: trigger,\n };\n },\n\n transcript: function(config){\n var that=this;\n var options=this.defaults;\n this.utils.prefix='transcript';\n\n config.validTracks = this.trackList(config).get();\n config.currentTrack = this.trackList(config).active(config.validTracks);\n config.settings = options;\n config.widget = this.widget(config).create(options);\n\n var timeUpdate = function (eventdata) {\n config.widget.setCue(config.theplayer.currentTime);\n };\n var updateTrack = function () {\n config.currentTrack = that.trackList(config).active(config.validTracks);\n config.widget.setTrack(config.currentTrack);\n };\n if (config.validTracks.length > 0) {\n config.theplayer.ontimeupdate =timeUpdate;\n } else {\n throw new Error('transcript: No tracks found!');\n }\n return {\n el: function () {\n return config.widget.el();\n },\n setTrack: config.widget.setTrack\n };\n }\n };//end of interactive transcript\n\nvar processNewVideo = function(player, itoptions){\n //first of all clear the existing transcript if we have one\n $('#' + @@AUTOID@@ + '_transcriptscrollable').empty();\n\t itoptions.title='';// Set TITLE here\n\t itoptions.theduration=player.duration;\n\t //finally load interactive transcript\n\t it.init(itoptions); \n\n\n}// end of processNewVideo\n\n//Insert or configure our player\nvar createPlayer = function(){\n //do we have an audio player?\n var originalplayer = $('#' + @@AUTOID@@ + '_original audio').first();\n if(originalplayer.length===1){\n var mediaurl=false;\n var lang = $('#' + @@AUTOID@@ + '_original audio track[kind=\"captions\"]').first().attr('srclang');\n var subtitlesurl = $('#' + @@AUTOID@@ + '_original audio track[kind=\"captions\"]').first().attr('src'); \n //make sure moodle and poodll leave it alone from here on\n originalplayer.addClass('nomediaplugin');\n originalplayer.addClass('nopoodll');\n }else{\n\n //hopefully we have data attributes in the a link\n var originallink = $('#' + @@AUTOID@@ + '_original a').first();\n var mediaurl = originallink.attr('href');\n var lang = originallink.attr('data-lang');\n var subtitlesurl = originallink.attr('data-subtitles');\n \n //but we might be in the old form where they were params on the url\n if(subtitlesurl===undefined && mediaurl.split('?').length>0){\n var urlParams = new URLSearchParams(mediaurl.split('?')[1] );\n subtitlesurl = urlParams.get('data-subtitles');\n lang = urlParams.get('data-language'); \n mediaurl = mediaurl.split('?')[0] \n } \n \n //make sure moodle and poodll leave it alone from here on\n originallink.addClass('nomediaplugin');\n originallink.addClass('nopoodll');\n}\n \n//the audio player template\n var atemplate ='';\n\n \n var aplayer = atemplate.replace(/@lang@/g, lang);\n aplayer = aplayer.replace(/@subtitlesurl@/g, subtitlesurl);\n aplayer = aplayer.replace(/@AUTOID@/g, @@AUTOID@@); \n $('#' + @@AUTOID@@ + '_widget').prepend(aplayer);\n \n //move any source tracks over if we had a full html5 player as source\n if(originalplayer.length===1){\n $('#' + @@AUTOID@@ + '_original source').clone().appendTo('#' + @@AUTOID@@ + '_player');\n var mediaurl = originalplayer.attr('src');\n if(mediaurl !==undefined){\n $('#' + @@AUTOID@@ + '_player').attr('src', mediaurl);\n }\n }else{\n //otherwise the media source is from the a link href\n $('#' + @@AUTOID@@ + '_player').attr('src', mediaurl);\n }\n //set the dummy player subtitle url\n var dummytrack = $('#' + @@AUTOID@@ + '_dummyplayer track[kind=\"captions\"]').first();\n //set titles\n dummytrack.attr('src', subtitlesurl); \n dummytrack.attr('srclang', lang); \n dummytrack.attr('label', lang); \n\n}\n \n//Set up our player \ncreatePlayer();\nvar theplayer = $('#' + @@AUTOID@@ + '_player')[0];\n\n \n //init our interactive transcript options (IT)\nvar itoptions = {};\nitoptions.containerid = @@AUTOID@@ + '_transcriptcontainer';\nitoptions.scrollcontainerid = @@AUTOID@@ + '_transcriptscrollable';\nitoptions.scrollingthing = @@AUTOID@@ + '_transcriptscrolling';\nitoptions.dummyplayerid = @@AUTOID@@ + '_dummyplayer';\nitoptions.splitter = @@AUTOID@@ + '_splitter';\nitoptions.playerid = @@AUTOID@@ + '_player';\nitoptions.theplayer = theplayer;\nitoptions.cssprefix = 'filterpoodll_sia_transcript';\n\n\n//window stuff for resizing\nif ($(window).width() >= 992) {\n $('#' + @@AUTOID@@ + '_player').resizable({\n handles: {'e' : $('#' + @@AUTOID@@ + '_splitter')},\n resizeHeight: false\n });\n}\n\n//display toolbar buttons (they appear too soon otherwise)\n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'toolbar .' + itoptions.cssprefix + 'tools').show();\n\n//Toggle time stamp \n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'showtime').on('click',function(){\n \n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + '-body').toggleClass('notimestamp');\n return false;\n\n});\n\n//kick it all off\nprocessNewVideo(theplayer,itoptions);\n","style":".fpv_flexrow{\n display: flex;flex-direction: row;\n}\n.fpv_flexcol{\n display: flex;flex-direction: column;\n max-width: 800px;\n\n}\n.fpv_flexcol .sivvideo{\n\twidth: 100%;\n} \n.fpv_flexcol .filterpoodll_sia_transcriptcontainer{\n height: 320px;\n}\n.sivvideo{\n\twidth: 50%;\n} \n.filterpoodll_sia_transcriptcontainer{\n flex: 1 1 auto;\n}\n.transcript-body{\n\theight: 100% !important;\n}\n.filterpoodll_sia_transcriptcontainer{\n height: 480px;\n}\n.filterpoodll_sia_transcriptscrollable {\n height: calc(100% - 37px);\n}\n@media screen and (max-width: 992px) {\n\t.filterpoodll_sia_transcriptmain_container{\n display: block !important;\n }\n .splitter{\n display: none;\n }\n\n .filterpoodll_sia_transcripttools{\n display: none;\n }\n\n .sivvideo{\n\t\twidth: 100%;\n\t\tfloat: none;\n\t\theight: 200px;\n } \n .filterpoodll_sia_transcriptcontainer{\n\t\tfloat: none;\n\t\tmargin-left: 0%;\t\t\n }\n .filterpoodll_sia_transcriptcontainer{\n\t\theight: 200px !important;\n }\n .filterpoodll_sia_transcriptscrollable {\n\t\theight: calc(100%-37px);\n /* 163px !important; height - toolbar height */\n }\n}\n\n/* interactive transcript */\n.filterpoodll_sia_transcriptcontainer {\n font-family: Arial, sans-serif;\n border: 1px solid #111;\n}\n\n.filterpoodll_sia_transcript-header {\n height: 19px;\n padding: 2px;\n font-weight: bold;\n text-align: center;\n}\n.filterpoodll_sia_transcript-selector {\n height: 25px;\n}\n.filterpoodll_sia_transcript-body {\n overflow-y: scroll;\n background-color: #e7e7e7;\n position: relative;\n margin: auto;\n}\n\n.filterpoodll_sia_transcript-line {\n position: relative;\n padding: 5px;\n cursor: pointer;\n line-height: 1.3;\n}\n\n.filterpoodll_sia_transcript-line:nth-child(odd) {\n background-color: #f5f5f5;\n}\n\n\n.filterpoodll_sia_transcript-timestamp {\n position: absolute;\n display: inline-block;\n color: #333;\n width: 50px;\n}\n\n.filterpoodll_sia_transcript-text {\n display: block;\n margin-left: 50px;\n}\n\n.filterpoodll_sia_transcript-line:hover,\n.filterpoodll_sia_transcript-line:hover .filterpoodll_sia_transcript-timestamp,\n.filterpoodll_sia_transcript-line:hover .filterpoodll_sia_transcript-text {\n background-color: #777;\n color: #e7e7e7;\n}\n\n.filterpoodll_sia_transcript-line.is-active,\n.filterpoodll_sia_transcript-line.is-active .filterpoodll_sia_transcript-timestamp,\n.filterpoodll_sia_transcript-line.is-active .filterpoodll_sia_transcript-text {\n background-color: #555;\n color: #e7e7e7;\n width: auto;\n}\n\n.filterpoodll_sia_transcripttitle {\n /* Set line height to same height at toolbar */\n line-height: 35px;\n margin-left: 5px;\n font-weight: bold;\n}\n\n.filterpoodll_sia_transcripttoolbar {\n background-color: #CCC;\n max-height: 35px;\n min-height: 35px;\n overflow:hidden\n}\n.filterpoodll_sia_transcriptscrollable {\n width: 100%;\n font-family: Arial, sans-serif;\n overflow-x: hidden;\n overflow-y: scroll;\n scroll-behavior: smooth;\n}\n.filterpoodll_sia_transcripttools {\n float: right;\n}\n\n/* toggle timestamp on and off */\n.filterpoodll_sia_transcript-body.notimestamp .filterpoodll_sia_transcript-timestamp {\n display: none;\n}\n\n.filterpoodll_sia_transcript-body.notimestamp .filterpoodll_sia_transcript-text {\n margin-left: 5px;\n}\n","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/superinteractivevideo.txt b/presets/superinteractivevideo.txt
new file mode 100644
index 0000000..fae73f8
--- /dev/null
+++ b/presets/superinteractivevideo.txt
@@ -0,0 +1 @@
+{"name":"Super Interactive Video","key":"superinteractivevideo","version":"1.0.4","instructions":"Shows a video + interactive transcript. The captions are made and edited here on Moodle. ","showatto":"0","showplayers":"1","requirecss":"","requirejs":"","shim":"","defaults":"lang=\"en\",vimeoid=empty","amd":"1","body":"
\n","bodyend":"","script":"//Define interactive_transcript (it) see cloud poodll submission amd/src/interactivetranscript.js\nvar it={\n init: function(itoptions){\n var config={};\n var that =this; \n config.settings ={};\n if(itoptions['theplayer']) {\n config.prefix = itoptions['cssprefix'];\n config.theplayer = itoptions['theplayer'];\n config.dummyplayerid = itoptions['dummyplayerid'];\n config.dummyplayer = $('#' + config.dummyplayerid)[0]; \n config.title = itoptions['title'];\n config.containerid = itoptions['containerid'];\n config.scrollingthing = itoptions['scrollingthing'];\n config.currentTrack = 0;\n config.theduration= itoptions['duration'];\n config.textTranscript='';\n var transcript = this.transcript(config);\n\n $('#' + itoptions['scrollcontainerid']).append(transcript.el());\n \n //unregisgter old, then register download transcript event\n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'download').off('click');\n \n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'download').on('click',function(){\n that.downloadTranscript(config);\n return false;\n });\n \n }//end of if player\n\n },\n\n downloadTranscript: function(config){\n\t\t\t\tvar element = document.createElement('a');\n\t\t\t\telement.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(config.textTranscript));\n\t\t\t\telement.setAttribute('download', config.title + '.txt');\n\t\t\t\telement.style.display = 'none';\n\t\t\t\tdocument.body.appendChild(element);\n\t\t\t\telement.click();\n\t\t\t\tdocument.body.removeChild(element);\n },\n\n // Defaults\n defaults: {\n autoscroll: true,\n clickArea: 'line', //the clickable part of line text,line,timestamp, none\n showTrackSelector: false, //the drop down box of caption tracks\n followPlayerTrack: true,\n scrollToCenter: false, //show current text in center\n stopScrollWhenInUse: false, //stop scrolling when user interacting\n },\n\n /*global */\n utils: {\n prefix: 'transcript',\n secondsToTime: function (timeInSeconds) {\n var hour = Math.floor(timeInSeconds / 3600);\n var min = Math.floor(timeInSeconds % 3600 / 60);\n var sec = Math.floor(timeInSeconds % 60);\n sec = (sec < 10) ? '0' + sec : sec;\n min = (hour > 0 && min < 10) ? '0' + min : min;\n if (hour > 0) {\n return hour + ':' + min + ':' + sec;\n }\n return min + ':' + sec;\n },\n localize: function (string) {\n return string; // TODO: do something here;\n },\n createEl: function (elementName, className) {\n className = className || '';\n var el = document.createElement(elementName);\n el.className = className;\n return el;\n },\n extend: function(obj) {\n var type = typeof obj;\n if (!(type === 'function' || type === 'object' && !!obj)) {\n return obj;\n }\n var source, prop;\n for (var i = 1, length = arguments.length; i < length; i++) {\n source = arguments[i];\n for (prop in source) {\n obj[prop] = source[prop];\n }\n }\n return obj;\n }\n \n },\n\n eventEmitter: {\n handlers_: [],\n on: function on (object, eventtype, callback) {\n if (typeof callback === 'function') {\n this.handlers_.push([object, eventtype, callback]);\n } else {\n throw new TypeError('Callback is not a function.');\n }\n },\n trigger: function trigger (object, eventtype) {\n this.handlers_.forEach( function(h) {\n if (h[0] === object &&\n h[1] === eventtype) {\n h[2].apply();\n }\n });\n }\n },\n\n scrollerProto: function(config) {\n\n var initHandlers = function (el) {\n var self = this;\n // The scroll event. We want to keep track of when the user is scrolling the transcript.\n el.addEventListener('scroll', function () {\n if (self.isAutoScrolling) {\n\n // If isAutoScrolling was set to true, we can set it to false and then ignore this event.\n // It wasn't the user.\n self.isAutoScrolling = false; // event handled\n } else {\n\n // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class.\n self.userIsScrolling = true;\n el.classList.add('is-inuse');\n }\n });\n\n // The mouseover event.\n el.addEventListener('mouseenter', function () {\n self.mouseIsOverTranscript = true;\n });\n el.addEventListener('mouseleave', function () {\n self.mouseIsOverTranscript = false;\n\n // Have a small delay before deciding user as done interacting.\n setTimeout(function () {\n\n // Make sure the user didn't move the pointer back in.\n if (!self.mouseIsOverTranscript) {\n self.userIsScrolling = false;\n el.classList.remove('is-inuse');\n }\n }, 1000);\n });\n };\n\n // Init instance variables\n var init = function (element) {\n this.element = element;\n this.userIsScrolling = false;\n\n //default to true in case user isn't using a mouse;\n this.mouseIsOverTranscript = true;\n this.isAutoScrolling = true;\n initHandlers.call(this, this.element);\n return this;\n };\n\n // Easing function for smoothness.\n var easeOut = function (time, start, change, duration) {\n return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2));\n };\n\n // Animate the scrolling.\n var scrollTo = function (element, newPos, duration) {\n var startTime = Date.now();\n var startPos = element.scrollTop;\n var self = this;\n\n // Don't try to scroll beyond the limits. You won't get there and this will loop forever.\n newPos = Math.max(0, newPos);\n newPos = Math.min(element.scrollHeight - element.clientHeight, newPos);\n var change = newPos - startPos;\n \n //if not animating\n if(true){ \n element.scrollTop= newPos;\n //if animating ... which doesn't work\n }else{\n // This inner function is called until the elements scrollTop reaches newPos.\n var updateScroll = function () {\n var now = Date.now();\n var time = now - startTime;\n self.isAutoScrolling = true;\n element.scrollTop = easeOut(time, startPos, change, duration);\n if (element.scrollTop !== newPos) {\n window.requestAnimationFrame(updateScroll, element);\n }\n };//end of update scroll\n window.requestAnimationFrame(updateScroll, element);\n }\n };\n\n // Scroll an element's parent so the element is brought into view.\n var scrollToElement = function (element) {\n if (this.canScroll()) {\n //elem=\"line\" parent=\"body\" parent.parent=\"scrolling\" parent.parent.parent.scrollable\n var parent = element.parentElement.parentElement.parentElement;\n var parentOffsetBottom = parent.offsetTop + parent.clientHeight;\n var elementOffsetBottom = element.offsetTop + element.clientHeight;\n var relTop = element.offsetTop;\n var relBottom = (element.offsetTop + element.clientHeight);\n var centerPosCorrection = 0;\n var newPos;\n /*\n console.log('element.offsetTop: ' + element.offsetTop );\n console.log('element.clientHeight: ' + element.clientHeight );\n console.log('parent.offsetTop: ' + parent.offsetTop );\n console.log('parent.scrollTop: ' + parent.scrollTop );\n console.log('parent.clientHeight: ' + parent.clientHeight );\n console.log(element);\n console.log(parent);\n */ \n \n //scroll to center if we must\n if (config.settings.scrollToCenter){\n centerPosCorrection = Math.round(parent.clientHeight/2 - element.clientHeight/2);\n }\n // If the top of the line is above the top of the parent view, were scrolling up,\n // so we want to move the top of the element downwards to match the top of the parent.\n if (relTop < parent.scrollTop + centerPosCorrection) {\n newPos = element.offsetTop -centerPosCorrection;\n\n // If the bottom of the line is below the parent view, we're scrolling down, so we want the\n // bottom edge of the line to move up to meet the bottom edge of the parent.\n } else if (relBottom > (parent.scrollTop + parent.clientHeight) - centerPosCorrection) {\n newPos = elementOffsetBottom + centerPosCorrection;\n }\n\n // Don't try to scroll if we haven't set a new position. If we didn't\n // set a new position the line is already in view (i.e. It's not above\n // or below the view)\n // And don't try to scroll when the element is already in position.\n if (newPos !== undefined && parent.scrollTop !== newPos) {\n scrollTo(parent, newPos, 400);\n }\n }\n };\n\n\n // Return whether the element is scrollable.\n var canScroll = function () {\n var el = this.element;\n //console.log(el.scrollHeight + ' ' + el.offsetHeight);\n return true;//el.scrollHeight > el.offsetHeight;\n };\n\n // Return whether the user is interacting with the transcript.\n var inUse = function () {\n return this.userIsScrolling;\n };\n\n return {\n init: init,\n to : scrollToElement,\n canScroll : canScroll,\n inUse : inUse\n }\n },\n\n scroller: function(element,config) {\n return Object.create(this.scrollerProto(config)).init(element);\n },\n\n\n /*global config*/\n trackList: function(config) {\n var activeTrack;\n return {\n get: function () {\n var validTracks = [];\n var i, track;\n config.tracks = config.dummyplayer.textTracks;\n for (i = 0; i < config.tracks.length; i++) {\n track = config.tracks[i];\n if (track.kind === 'captions' || track.kind === 'subtitles') {\n validTracks.push(track);\n }\n }\n return validTracks;\n },\n active: function (tracks) {\n var i, track;\n for (i = 0; i < config.tracks.length; i++) {\n track = config.tracks[i];\n if (track.mode === 'showing') {\n activeTrack = track;\n return track;\n }\n }\n // fallback to first track\n return activeTrack || tracks[0];\n },\n };\n },\n\n /*globals utils, eventEmitter,scrollable*/\n\n widget: function(config) {\n var that = this;\n var thewidget = {};\n thewidget.element = {};\n thewidget.body = {};\n var on = function (event, callback) {\n eventEmitter.on(that, event, callback);\n };\n var trigger = function (event) {\n eventEmitter.trigger(that, event);\n };\n var initToolbar = function () {\n $('#' + config.containerid + ' .' + config.prefix + 'title').text(config.title);\n };\n var createSelector = function (){\n var selector = that.utils.createEl('select', config.prefix + '-selector');\n config.validTracks.forEach(function (track, i) {\n var option = document.createElement('option');\n option.value = i;\n option.textContent = track.label + ' (' + track.language + ')';\n selector.appendChild(option);\n });\n selector.addEventListener('change', function (e) {\n setTrack(document.querySelector('#' + config.prefix + '-' + config.dummyplayerid + ' option:checked').value);\n trigger('trackchanged');\n });\n return selector;\n };\n var clickToSeekHandler = function (event) {\n var clickedClasses = event.target.classList;\n var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin');\n if (clickedTime !== undefined && clickedTime !== null) { // can be zero\n if ((config.settings.clickArea === 'line') || // clickArea: 'line' activates on all elements\n (config.settings.clickArea === 'timestamp' && clickedClasses.contains(config.prefix + '-timestamp')) ||\n (config.settings.clickArea === 'text' && clickedClasses.contains(config.prefix + '-text'))) {\n config.theplayer.currentTime= clickedTime;\n }\n }\n };\n var createLine = function (cue) {\n var line = that.utils.createEl('div', config.prefix +'-line');\n var timestamp = that.utils.createEl('span',config.prefix + '-timestamp');\n var text = that.utils.createEl('span', config.prefix + '-text');\n line.setAttribute('data-begin', cue.startTime);\n line.setAttribute('tabindex', thewidget._options.tabIndex || 0);\n timestamp.textContent = that.utils.secondsToTime(cue.startTime);\n text.innerHTML = cue.text;\n line.appendChild(timestamp);\n line.appendChild(text);\n return line;\n };\n \n var createTranscriptBody = function (track) {\n if (typeof track !== 'object') {\n track = config.dummyplayer.textTracks()[track];\n }\n var body = that.utils.createEl('div', config.prefix + '-body');\n var line, i;\n var fragment = document.createDocumentFragment();\n // activeCues returns null when the track isn't loaded (for now?)\n if (!track.activeCues) {\n // If cues aren't loaded, set mode to hidden, wait, and try again.\n // But don't hide an active track. In that case, just wait and try again.\n if (track.mode !== 'showing') {\n track.mode = 'hidden';\n }\n window.setTimeout(function() {\n createTranscriptBody(track);\n }, 100);\n } else {\n var cues = track.cues;\n var textTranscript =[];\n for (i = 0; i < cues.length; i++) {\n line = createLine(cues[i]);\n if(!(cues[i].text==='')){\n textTranscript.push(cues[i].text);\n }\n fragment.appendChild(line);\n }\n //prepare text transcript\n config.textTranscript=textTranscript.join(' ');\n \n //build body of transcript\n body.innerHTML = '';\n body.appendChild(fragment);\n body.setAttribute('lang', track.language);\n body.scroll = that.scroller(thewidget.element,config);\n body.addEventListener('click', clickToSeekHandler);\n thewidget.element.replaceChild(body, thewidget.body);\n thewidget.body = body;\n }\n\n };\n var create = function (options) {\n initToolbar();\n var el = document.createElement('div');\n thewidget._options = options;\n thewidget.element = el;\n el.setAttribute('id', config.scrollingthing);\n \n if (config.settings.showTrackSelector) {\n var selector = createSelector();\n el.appendChild(selector);\n }\n thewidget.body = that.utils.createEl('div',config.prefix + '-body');\n el.appendChild(thewidget.body);\n setTrack(config.currentTrack);\n return this;\n };\n var setTrack = function (track, trackCreated) {\n createTranscriptBody(track, trackCreated);\n };\n var setCue = function (time) {\n var active, i, line, begin, end;\n var lines = thewidget.body.children;\n for (i = 0; i < lines.length; i++) {\n line = lines[i];\n begin = line.getAttribute('data-begin');\n if (i < lines.length - 1) {\n end = lines[i + 1].getAttribute('data-begin');\n } else {\n end = config.theduration;\n }\n if (time > begin && time < end) {\n if (!line.classList.contains('is-active')) { // don't update if it hasn't changed\n line.classList.add('is-active');\n if (config.settings.autoscroll && !(config.settings.stopScrollWhenInUse && thewidget.body.scroll.inUse())) {\n thewidget.body.scroll.to(line);\n }\n }\n } else {\n line.classList.remove('is-active');\n }\n }\n };\n var el = function () {\n return thewidget.element;\n };\n return {\n create: create,\n setTrack: setTrack,\n setCue: setCue,\n el : el,\n on: on,\n trigger: trigger,\n };\n },\n\n transcript: function(config){\n var that=this;\n var options=this.defaults;\n this.utils.prefix='transcript';\n\n config.validTracks = this.trackList(config).get();\n config.currentTrack = this.trackList(config).active(config.validTracks);\n config.settings = options;\n config.widget = this.widget(config).create(options);\n\n var timeUpdate = function (eventdata) {\n config.widget.setCue(config.theplayer.currentTime);\n };\n var updateTrack = function () {\n config.currentTrack = that.trackList(config).active(config.validTracks);\n config.widget.setTrack(config.currentTrack);\n };\n if (config.validTracks.length > 0) {\n config.theplayer.ontimeupdate =timeUpdate;\n } else {\n throw new Error('transcript: No tracks found!');\n }\n return {\n el: function () {\n return config.widget.el();\n },\n setTrack: config.widget.setTrack\n };\n }\n };//end of interactive transcript\n\nvar processNewVideo = function(player, itoptions){\n //first of all clear the existing transcript if we have one\n $('#' + @@AUTOID@@ + '_transcriptscrollable').empty();\n\t itoptions.title=@@TITLE@@;\n\t itoptions.theduration=player.duration;\n\t //finally load interactive transcript\n\t it.init(itoptions); \n\n\n}// end of processNewVideo\n \n//Set up our player \nvar theplayer = $('#' + @@AUTOID@@ + '_player')[0];\n\n \n //init our interactive transcript options (IT)\nvar itoptions = {};\nitoptions.containerid = @@AUTOID@@ + '_transcriptcontainer';\nitoptions.scrollcontainerid = @@AUTOID@@ + '_transcriptscrollable';\nitoptions.scrollingthing = @@AUTOID@@ + '_transcriptscrolling';\nitoptions.dummyplayerid = @@AUTOID@@ + '_dummyplayer';\nitoptions.splitter = @@AUTOID@@ + '_splitter';\nitoptions.playerid = @@AUTOID@@ + '_player';\nitoptions.theplayer = theplayer;\nitoptions.cssprefix = 'filterpoodll_siv_transcript';\n\n\n//window stuff for resizing\nif ($(window).width() >= 992) {\n $('#' + @@AUTOID@@ + '_player').resizable({\n handles: {'e' : $('#' + @@AUTOID@@ + '_splitter')},\n resizeHeight: false\n });\n}\n\n//display toolbar buttons (they appear too soon otherwise)\n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'toolbar .' + itoptions.cssprefix + 'tools').show();\n\n//Toggle time stamp \n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'showtime').on('click',function(){\n \n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + '-body').toggleClass('notimestamp');\n return false;\n\n});\n\n//Toggle layout \n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'layout').on('click',function(){\n \n var maincontainer = $('.filterpoodll_siv_transcriptmain_container');\n var sivvideo = $('.sivvideo') ;\n if(maincontainer.hasClass('fpv_flexrow')){\n maincontainer.removeClass('fpv_flexrow');\n maincontainer.addClass('fpv_flexcol'); \n $('#' + itoptions.splitter).hide();\n //clear any resized widths \n sivvideo.css('width','');\n }else{\n maincontainer.removeClass('fpv_flexcol'); \n maincontainer.addClass('fpv_flexrow');\n $('#' + itoptions.splitter).show();\n\n }\n return false;\n\n});\n\n\n//kick it all off\nprocessNewVideo(theplayer,itoptions);\n","style":".splitter {\n flex: 0 0 auto;\n width: 2%;\n background: url(https://raw.githubusercontent.com/RickStrahl/jquery-resizable/master/assets/vsizegrip.png) center center no-repeat #535353;\n min-height: 200px;\n cursor: col-resize;\n height: 480px;\n}\n.fpv_flexrow{\n display: flex;flex-direction: row;\n}\n.fpv_flexcol{\n display: flex;flex-direction: column;\n max-width: 800px;\n\n}\n.fpv_flexcol .sivvideo{\n\twidth: 100%;\n} \n.fpv_flexcol .filterpoodll_siv_transcriptcontainer{\n height: 320px;\n}\n.sivvideo{\n\twidth: 50%;\n} \n.filterpoodll_siv_transcriptcontainer{\n flex: 1 1 auto;\n}\n.transcript-body{\n\theight: 100% !important;\n}\n.filterpoodll_siv_transcriptcontainer{\n height: 480px;\n}\n.filterpoodll_siv_transcriptscrollable {\n height: calc(100% - 37px);\n}\n@media screen and (max-width: 992px) {\n\t.filterpoodll_siv_transcriptmain_container{\n display: block !important;\n }\n .splitter{\n display: none;\n }\n\n .filterpoodll_siv_transcripttools{\n display: none;\n }\n\n .sivvideo{\n\t\twidth: 100%;\n\t\tfloat: none;\n\t\theight: 200px;\n } \n .filterpoodll_siv_transcriptcontainer{\n\t\tfloat: none;\n\t\tmargin-left: 0%;\t\t\n }\n .filterpoodll_siv_transcriptcontainer{\n\t\theight: 200px !important;\n }\n .filterpoodll_siv_transcriptscrollable {\n\t\theight: calc(100%-37px);\n /* 163px !important; height - toolbar height */\n }\n}\n\n/* interactive transcript */\n.filterpoodll_siv_transcriptcontainer {\n font-family: Arial, sans-serif;\n border: 1px solid #111;\n}\n\n.filterpoodll_siv_transcript-header {\n height: 19px;\n padding: 2px;\n font-weight: bold;\n text-align: center;\n}\n.filterpoodll_siv_transcript-selector {\n height: 25px;\n}\n.filterpoodll_siv_transcript-body {\n overflow-y: scroll;\n background-color: #e7e7e7;\n position: relative;\n margin: auto;\n}\n\n.filterpoodll_siv_transcript-line {\n position: relative;\n padding: 5px;\n cursor: pointer;\n line-height: 1.3;\n}\n\n.filterpoodll_siv_transcript-line:nth-child(odd) {\n background-color: #f5f5f5;\n}\n\n\n.filterpoodll_siv_transcript-timestamp {\n position: absolute;\n display: inline-block;\n color: #333;\n width: 50px;\n}\n\n.filterpoodll_siv_transcript-text {\n display: block;\n margin-left: 50px;\n}\n\n.filterpoodll_siv_transcript-line:hover,\n.filterpoodll_siv_transcript-line:hover .filterpoodll_siv_transcript-timestamp,\n.filterpoodll_siv_transcript-line:hover .filterpoodll_siv_transcript-text {\n background-color: #777;\n color: #e7e7e7;\n}\n\n.filterpoodll_siv_transcript-line.is-active,\n.filterpoodll_siv_transcript-line.is-active .filterpoodll_siv_transcript-timestamp,\n.filterpoodll_siv_transcript-line.is-active .filterpoodll_siv_transcript-text {\n background-color: #555;\n color: #e7e7e7;\n width: auto;\n}\n\n.filterpoodll_siv_transcripttitle {\n /* Set line height to same height at toolbar */\n line-height: 35px;\n margin-left: 5px;\n font-weight: bold;\n}\n\n.filterpoodll_siv_transcripttoolbar {\n background-color: #CCC;\n max-height: 35px;\n min-height: 35px;\n overflow:hidden\n}\n.filterpoodll_siv_transcriptscrollable {\n width: 100%;\n font-family: Arial, sans-serif;\n overflow-x: hidden;\n overflow-y: scroll;\n scroll-behavior: smooth;\n}\n.filterpoodll_siv_transcripttools {\n float: right;\n}\n\n/* toggle timestamp on and off */\n.filterpoodll_siv_transcript-body.notimestamp .filterpoodll_siv_transcript-timestamp {\n display: none;\n}\n\n.filterpoodll_siv_transcript-body.notimestamp .filterpoodll_siv_transcript-text {\n margin-left: 5px;\n}\n","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/superinteractivevideowidget.txt b/presets/superinteractivevideowidget.txt
new file mode 100644
index 0000000..5637e64
--- /dev/null
+++ b/presets/superinteractivevideowidget.txt
@@ -0,0 +1 @@
+{"name":"Super Interactive Video Widget","key":"superinteractivevideowidget","version":"1.0.4","instructions":"Shows a video + interactive transcript. The captions are made and edited here on Moodle. Enter the video player or links between the widget tags.","showatto":"1","showplayers":"0","requirecss":"","requirejs":"","shim":"","defaults":"","amd":"1","body":"
","script":"//Define interactive_transcript (it) see cloud poodll submission amd/src/interactivetranscript.js\nvar it={\n init: function(itoptions){\n var config={};\n var that =this; \n config.settings ={};\n if(itoptions['theplayer']) {\n config.prefix = itoptions['cssprefix'];\n config.theplayer = itoptions['theplayer'];\n config.dummyplayerid = itoptions['dummyplayerid'];\n config.dummyplayer = $('#' + config.dummyplayerid)[0]; \n config.title = itoptions['title'];\n config.containerid = itoptions['containerid'];\n config.scrollingthing = itoptions['scrollingthing'];\n config.currentTrack = 0;\n config.theduration= itoptions['duration'];\n config.textTranscript='';\n var transcript = this.transcript(config);\n\n $('#' + itoptions['scrollcontainerid']).append(transcript.el());\n \n //unregisgter old, then register download transcript event\n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'download').off('click');\n \n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'download').on('click',function(){\n that.downloadTranscript(config);\n return false;\n });\n \n }//end of if player\n\n },\n\n downloadTranscript: function(config){\n\t\t\t\tvar element = document.createElement('a');\n\t\t\t\telement.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(config.textTranscript));\n\t\t\t\telement.setAttribute('download', config.title + '.txt');\n\t\t\t\telement.style.display = 'none';\n\t\t\t\tdocument.body.appendChild(element);\n\t\t\t\telement.click();\n\t\t\t\tdocument.body.removeChild(element);\n },\n\n // Defaults\n defaults: {\n autoscroll: true,\n clickArea: 'line', //the clickable part of line text,line,timestamp, none\n showTrackSelector: false, //the drop down box of caption tracks\n followPlayerTrack: true,\n scrollToCenter: false, //show current text in center\n stopScrollWhenInUse: false, //stop scrolling when user interacting\n },\n\n /*global */\n utils: {\n prefix: 'transcript',\n secondsToTime: function (timeInSeconds) {\n var hour = Math.floor(timeInSeconds / 3600);\n var min = Math.floor(timeInSeconds % 3600 / 60);\n var sec = Math.floor(timeInSeconds % 60);\n sec = (sec < 10) ? '0' + sec : sec;\n min = (hour > 0 && min < 10) ? '0' + min : min;\n if (hour > 0) {\n return hour + ':' + min + ':' + sec;\n }\n return min + ':' + sec;\n },\n localize: function (string) {\n return string; // TODO: do something here;\n },\n createEl: function (elementName, className) {\n className = className || '';\n var el = document.createElement(elementName);\n el.className = className;\n return el;\n },\n extend: function(obj) {\n var type = typeof obj;\n if (!(type === 'function' || type === 'object' && !!obj)) {\n return obj;\n }\n var source, prop;\n for (var i = 1, length = arguments.length; i < length; i++) {\n source = arguments[i];\n for (prop in source) {\n obj[prop] = source[prop];\n }\n }\n return obj;\n }\n \n },\n\n eventEmitter: {\n handlers_: [],\n on: function on (object, eventtype, callback) {\n if (typeof callback === 'function') {\n this.handlers_.push([object, eventtype, callback]);\n } else {\n throw new TypeError('Callback is not a function.');\n }\n },\n trigger: function trigger (object, eventtype) {\n this.handlers_.forEach( function(h) {\n if (h[0] === object &&\n h[1] === eventtype) {\n h[2].apply();\n }\n });\n }\n },\n\n scrollerProto: function(config) {\n\n var initHandlers = function (el) {\n var self = this;\n // The scroll event. We want to keep track of when the user is scrolling the transcript.\n el.addEventListener('scroll', function () {\n if (self.isAutoScrolling) {\n\n // If isAutoScrolling was set to true, we can set it to false and then ignore this event.\n // It wasn't the user.\n self.isAutoScrolling = false; // event handled\n } else {\n\n // We only care about when the user scrolls. Set userIsScrolling to true and add a nice class.\n self.userIsScrolling = true;\n el.classList.add('is-inuse');\n }\n });\n\n // The mouseover event.\n el.addEventListener('mouseenter', function () {\n self.mouseIsOverTranscript = true;\n });\n el.addEventListener('mouseleave', function () {\n self.mouseIsOverTranscript = false;\n\n // Have a small delay before deciding user as done interacting.\n setTimeout(function () {\n\n // Make sure the user didn't move the pointer back in.\n if (!self.mouseIsOverTranscript) {\n self.userIsScrolling = false;\n el.classList.remove('is-inuse');\n }\n }, 1000);\n });\n };\n\n // Init instance variables\n var init = function (element) {\n this.element = element;\n this.userIsScrolling = false;\n\n //default to true in case user isn't using a mouse;\n this.mouseIsOverTranscript = true;\n this.isAutoScrolling = true;\n initHandlers.call(this, this.element);\n return this;\n };\n\n // Easing function for smoothness.\n var easeOut = function (time, start, change, duration) {\n return start + change * Math.sin(Math.min(1, time / duration) * (Math.PI / 2));\n };\n\n // Animate the scrolling.\n var scrollTo = function (element, newPos, duration) {\n var startTime = Date.now();\n var startPos = element.scrollTop;\n var self = this;\n\n // Don't try to scroll beyond the limits. You won't get there and this will loop forever.\n newPos = Math.max(0, newPos);\n newPos = Math.min(element.scrollHeight - element.clientHeight, newPos);\n var change = newPos - startPos;\n \n //if not animating\n if(true){ \n element.scrollTop= newPos;\n //if animating ... which doesn't work\n }else{\n // This inner function is called until the elements scrollTop reaches newPos.\n var updateScroll = function () {\n var now = Date.now();\n var time = now - startTime;\n self.isAutoScrolling = true;\n element.scrollTop = easeOut(time, startPos, change, duration);\n if (element.scrollTop !== newPos) {\n window.requestAnimationFrame(updateScroll, element);\n }\n };//end of update scroll\n window.requestAnimationFrame(updateScroll, element);\n }\n };\n\n // Scroll an element's parent so the element is brought into view.\n var scrollToElement = function (element) {\n if (this.canScroll()) {\n //elem=\"line\" parent=\"body\" parent.parent=\"scrolling\" parent.parent.parent.scrollable\n var parent = element.parentElement.parentElement.parentElement;\n var parentOffsetBottom = parent.offsetTop + parent.clientHeight;\n var elementOffsetBottom = element.offsetTop + element.clientHeight;\n var relTop = element.offsetTop;\n var relBottom = (element.offsetTop + element.clientHeight);\n var centerPosCorrection = 0;\n var newPos;\n /*\n console.log('element.offsetTop: ' + element.offsetTop );\n console.log('element.clientHeight: ' + element.clientHeight );\n console.log('parent.offsetTop: ' + parent.offsetTop );\n console.log('parent.scrollTop: ' + parent.scrollTop );\n console.log('parent.clientHeight: ' + parent.clientHeight );\n console.log(element);\n console.log(parent);\n */ \n \n //scroll to center if we must\n if (config.settings.scrollToCenter){\n centerPosCorrection = Math.round(parent.clientHeight/2 - element.clientHeight/2);\n }\n // If the top of the line is above the top of the parent view, were scrolling up,\n // so we want to move the top of the element downwards to match the top of the parent.\n if (relTop < parent.scrollTop + centerPosCorrection) {\n newPos = element.offsetTop -centerPosCorrection;\n\n // If the bottom of the line is below the parent view, we're scrolling down, so we want the\n // bottom edge of the line to move up to meet the bottom edge of the parent.\n } else if (relBottom > (parent.scrollTop + parent.clientHeight) - centerPosCorrection) {\n newPos = elementOffsetBottom + centerPosCorrection;\n }\n\n // Don't try to scroll if we haven't set a new position. If we didn't\n // set a new position the line is already in view (i.e. It's not above\n // or below the view)\n // And don't try to scroll when the element is already in position.\n if (newPos !== undefined && parent.scrollTop !== newPos) {\n scrollTo(parent, newPos, 400);\n }\n }\n };\n\n\n // Return whether the element is scrollable.\n var canScroll = function () {\n var el = this.element;\n //console.log(el.scrollHeight + ' ' + el.offsetHeight);\n return true;//el.scrollHeight > el.offsetHeight;\n };\n\n // Return whether the user is interacting with the transcript.\n var inUse = function () {\n return this.userIsScrolling;\n };\n\n return {\n init: init,\n to : scrollToElement,\n canScroll : canScroll,\n inUse : inUse\n }\n },\n\n scroller: function(element,config) {\n return Object.create(this.scrollerProto(config)).init(element);\n },\n\n\n /*global config*/\n trackList: function(config) {\n var activeTrack;\n return {\n get: function () {\n var validTracks = [];\n var i, track;\n config.tracks = config.dummyplayer.textTracks;\n for (i = 0; i < config.tracks.length; i++) {\n track = config.tracks[i];\n if (track.kind === 'captions' || track.kind === 'subtitles') {\n validTracks.push(track);\n }\n }\n return validTracks;\n },\n active: function (tracks) {\n var i, track;\n for (i = 0; i < config.tracks.length; i++) {\n track = config.tracks[i];\n if (track.mode === 'showing') {\n activeTrack = track;\n return track;\n }\n }\n // fallback to first track\n return activeTrack || tracks[0];\n },\n };\n },\n\n /*globals utils, eventEmitter,scrollable*/\n\n widget: function(config) {\n var that = this;\n var thewidget = {};\n thewidget.element = {};\n thewidget.body = {};\n var on = function (event, callback) {\n eventEmitter.on(that, event, callback);\n };\n var trigger = function (event) {\n eventEmitter.trigger(that, event);\n };\n var initToolbar = function () {\n $('#' + config.containerid + ' .' + config.prefix + 'title').text(config.title);\n };\n var createSelector = function (){\n var selector = that.utils.createEl('select', config.prefix + '-selector');\n config.validTracks.forEach(function (track, i) {\n var option = document.createElement('option');\n option.value = i;\n option.textContent = track.label + ' (' + track.language + ')';\n selector.appendChild(option);\n });\n selector.addEventListener('change', function (e) {\n setTrack(document.querySelector('#' + config.prefix + '-' + config.dummyplayerid + ' option:checked').value);\n trigger('trackchanged');\n });\n return selector;\n };\n var clickToSeekHandler = function (event) {\n var clickedClasses = event.target.classList;\n var clickedTime = event.target.getAttribute('data-begin') || event.target.parentElement.getAttribute('data-begin');\n if (clickedTime !== undefined && clickedTime !== null) { // can be zero\n if ((config.settings.clickArea === 'line') || // clickArea: 'line' activates on all elements\n (config.settings.clickArea === 'timestamp' && clickedClasses.contains(config.prefix + '-timestamp')) ||\n (config.settings.clickArea === 'text' && clickedClasses.contains(config.prefix + '-text'))) {\n config.theplayer.currentTime= clickedTime;\n }\n }\n };\n var createLine = function (cue) {\n var line = that.utils.createEl('div', config.prefix +'-line');\n var timestamp = that.utils.createEl('span',config.prefix + '-timestamp');\n var text = that.utils.createEl('span', config.prefix + '-text');\n line.setAttribute('data-begin', cue.startTime);\n line.setAttribute('tabindex', thewidget._options.tabIndex || 0);\n timestamp.textContent = that.utils.secondsToTime(cue.startTime);\n text.innerHTML = cue.text;\n line.appendChild(timestamp);\n line.appendChild(text);\n return line;\n };\n \n var createTranscriptBody = function (track) {\n if (typeof track !== 'object') {\n track = config.dummyplayer.textTracks()[track];\n }\n var body = that.utils.createEl('div', config.prefix + '-body');\n var line, i;\n var fragment = document.createDocumentFragment();\n // activeCues returns null when the track isn't loaded (for now?)\n if (!track.activeCues) {\n // If cues aren't loaded, set mode to hidden, wait, and try again.\n // But don't hide an active track. In that case, just wait and try again.\n if (track.mode !== 'showing') {\n track.mode = 'hidden';\n }\n window.setTimeout(function() {\n createTranscriptBody(track);\n }, 100);\n } else {\n var cues = track.cues;\n var textTranscript =[];\n for (i = 0; i < cues.length; i++) {\n line = createLine(cues[i]);\n if(!(cues[i].text==='')){\n textTranscript.push(cues[i].text);\n }\n fragment.appendChild(line);\n }\n //prepare text transcript\n config.textTranscript=textTranscript.join(' ');\n \n //build body of transcript\n body.innerHTML = '';\n body.appendChild(fragment);\n body.setAttribute('lang', track.language);\n body.scroll = that.scroller(thewidget.element,config);\n body.addEventListener('click', clickToSeekHandler);\n thewidget.element.replaceChild(body, thewidget.body);\n thewidget.body = body;\n }\n\n };\n var create = function (options) {\n initToolbar();\n var el = document.createElement('div');\n thewidget._options = options;\n thewidget.element = el;\n el.setAttribute('id', config.scrollingthing);\n \n if (config.settings.showTrackSelector) {\n var selector = createSelector();\n el.appendChild(selector);\n }\n thewidget.body = that.utils.createEl('div',config.prefix + '-body');\n el.appendChild(thewidget.body);\n setTrack(config.currentTrack);\n return this;\n };\n var setTrack = function (track, trackCreated) {\n createTranscriptBody(track, trackCreated);\n };\n var setCue = function (time) {\n var active, i, line, begin, end;\n var lines = thewidget.body.children;\n for (i = 0; i < lines.length; i++) {\n line = lines[i];\n begin = line.getAttribute('data-begin');\n if (i < lines.length - 1) {\n end = lines[i + 1].getAttribute('data-begin');\n } else {\n end = config.theduration;\n }\n if (time > begin && time < end) {\n if (!line.classList.contains('is-active')) { // don't update if it hasn't changed\n line.classList.add('is-active');\n if (config.settings.autoscroll && !(config.settings.stopScrollWhenInUse && thewidget.body.scroll.inUse())) {\n thewidget.body.scroll.to(line);\n }\n }\n } else {\n line.classList.remove('is-active');\n }\n }\n };\n var el = function () {\n return thewidget.element;\n };\n return {\n create: create,\n setTrack: setTrack,\n setCue: setCue,\n el : el,\n on: on,\n trigger: trigger,\n };\n },\n\n transcript: function(config){\n var that=this;\n var options=this.defaults;\n this.utils.prefix='transcript';\n\n config.validTracks = this.trackList(config).get();\n config.currentTrack = this.trackList(config).active(config.validTracks);\n config.settings = options;\n config.widget = this.widget(config).create(options);\n\n var timeUpdate = function (eventdata) {\n config.widget.setCue(config.theplayer.currentTime);\n };\n var updateTrack = function () {\n config.currentTrack = that.trackList(config).active(config.validTracks);\n config.widget.setTrack(config.currentTrack);\n };\n if (config.validTracks.length > 0) {\n config.theplayer.ontimeupdate =timeUpdate;\n } else {\n throw new Error('transcript: No tracks found!');\n }\n return {\n el: function () {\n return config.widget.el();\n },\n setTrack: config.widget.setTrack\n };\n }\n };//end of interactive transcript\n\nvar processNewVideo = function(player, itoptions){\n //first of all clear the existing transcript if we have one\n $('#' + @@AUTOID@@ + '_transcriptscrollable').empty();\n\t itoptions.title=''; //Set player TITLE here\n\t itoptions.theduration=player.duration;\n\t //finally load interactive transcript\n\t it.init(itoptions); \n\n\n}// end of processNewVideo\n\n\n//Insert or configure our player\nvar createPlayer = function(){\n //do we have a player?\n var originalplayer = $('#' + @@AUTOID@@ + '_original video').first();\n if(originalplayer.length===1){\n var mediaurl=false;\n var lang = $('#' + @@AUTOID@@ + '_original video track[kind=\"captions\"]').first().attr('srclang');\n var subtitlesurl = $('#' + @@AUTOID@@ + '_original video track[kind=\"captions\"]').first().attr('src'); \n //make sure moodle and poodll leave it alone from here on\n originalplayer.addClass('nomediaplugin');\n originalplayer.addClass('nopoodll');\n }else{\n\n //hopefully we have data attributes in the a link\n var originallink = $('#' + @@AUTOID@@ + '_original a').first();\n var mediaurl = originallink.attr('href');\n var lang = originallink.attr('data-lang');\n var subtitlesurl = originallink.attr('data-subtitles');\n \n //but we might be in the old form where they were params on the url\n if(subtitlesurl===undefined && mediaurl.split('?').length>0){\n var urlParams = new URLSearchParams(mediaurl.split('?')[1] );\n subtitlesurl = urlParams.get('data-subtitles');\n lang = urlParams.get('data-language'); \n mediaurl = mediaurl.split('?')[0] \n } \n \n //make sure moodle and poodll leave it alone from here on\n originallink.addClass('nomediaplugin');\n originallink.addClass('nopoodll');\n}\n \n//the player template\n var atemplate = '';\n\n var aplayer = atemplate.replace(/@lang@/g, lang);\n aplayer = aplayer.replace(/@subtitlesurl@/g, subtitlesurl);\n aplayer = aplayer.replace(/@AUTOID@/g, @@AUTOID@@); \n $('#' + @@AUTOID@@ + '_widget').prepend(aplayer);\n \n //move any source tracks over if we had a full html5 player as source\n if(originalplayer.length===1){\n $('#' + @@AUTOID@@ + '_original source').clone().appendTo('#' + @@AUTOID@@ + '_player');\n var mediaurl = originalplayer.attr('src');\n if(mediaurl !==undefined){\n $('#' + @@AUTOID@@ + '_player').attr('src', mediaurl);\n }\n }else{\n //otherwise the media source is from the a link href\n $('#' + @@AUTOID@@ + '_player').attr('src', mediaurl);\n }\n //set the dummy player subtitle url\n var dummytrack = $('#' + @@AUTOID@@ + '_dummyplayer track[kind=\"captions\"]').first();\n //set titles\n dummytrack.attr('src', subtitlesurl); \n dummytrack.attr('srclang', lang); \n dummytrack.attr('label', lang); \n\n}\n \n//Set up our player \ncreatePlayer();\nvar theplayer = $('#' + @@AUTOID@@ + '_player')[0];\n\n \n //init our interactive transcript options (IT)\nvar itoptions = {};\nitoptions.containerid = @@AUTOID@@ + '_transcriptcontainer';\nitoptions.scrollcontainerid = @@AUTOID@@ + '_transcriptscrollable';\nitoptions.scrollingthing = @@AUTOID@@ + '_transcriptscrolling';\nitoptions.dummyplayerid = @@AUTOID@@ + '_dummyplayer';\nitoptions.splitter = @@AUTOID@@ + '_splitter';\nitoptions.playerid = @@AUTOID@@ + '_player';\nitoptions.theplayer = theplayer;\nitoptions.cssprefix = 'filterpoodll_siv_transcript';\n\n\n//window stuff for resizing\nif ($(window).width() >= 992) {\n $('#' + @@AUTOID@@ + '_player').resizable({\n handles: {'e' : $('#' + @@AUTOID@@ + '_splitter')},\n resizeHeight: false\n });\n}\n\n//display toolbar buttons (they appear too soon otherwise)\n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'toolbar .' + itoptions.cssprefix + 'tools').show();\n\n//Toggle time stamp \n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'showtime').on('click',function(){\n \n $('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + '-body').toggleClass('notimestamp');\n return false;\n\n});\n\n//Toggle layout \n$('#' + itoptions.containerid + ' .' +\n itoptions.cssprefix + 'layout').on('click',function(){\n \n var maincontainer = $('.filterpoodll_siv_transcriptmain_container');\n var sivvideo = $('.sivvideo') ;\n if(maincontainer.hasClass('fpv_flexrow')){\n maincontainer.removeClass('fpv_flexrow');\n maincontainer.addClass('fpv_flexcol'); \n $('#' + itoptions.splitter).hide();\n //clear any resized widths \n sivvideo.css('width','');\n }else{\n maincontainer.removeClass('fpv_flexcol'); \n maincontainer.addClass('fpv_flexrow');\n $('#' + itoptions.splitter).show();\n\n }\n return false;\n\n});\n\n\n//kick it all off\nprocessNewVideo(theplayer,itoptions);\n","style":".splitter {\n flex: 0 0 auto;\n width: 2%;\n background: url(https://raw.githubusercontent.com/RickStrahl/jquery-resizable/master/assets/vsizegrip.png) center center no-repeat #535353;\n min-height: 200px;\n cursor: col-resize;\n height: 480px;\n}\n.fpv_flexrow{\n display: flex;flex-direction: row;\n}\n.fpv_flexcol{\n display: flex;flex-direction: column;\n max-width: 800px;\n\n}\n.fpv_flexcol .sivvideo{\n\twidth: 100%;\n} \n.fpv_flexcol .filterpoodll_siv_transcriptcontainer{\n height: 320px;\n}\n.sivvideo{\n\twidth: 50%;\n} \n.filterpoodll_siv_transcriptcontainer{\n flex: 1 1 auto;\n}\n.transcript-body{\n\theight: 100% !important;\n}\n.filterpoodll_siv_transcriptcontainer{\n height: 480px;\n}\n.filterpoodll_siv_transcriptscrollable {\n height: calc(100% - 37px);\n}\n@media screen and (max-width: 992px) {\n\t.filterpoodll_siv_transcriptmain_container{\n display: block !important;\n }\n .splitter{\n display: none;\n }\n\n .filterpoodll_siv_transcripttools{\n display: none;\n }\n\n .sivvideo{\n\t\twidth: 100%;\n\t\tfloat: none;\n\t\theight: 200px;\n } \n .filterpoodll_siv_transcriptcontainer{\n\t\tfloat: none;\n\t\tmargin-left: 0%;\t\t\n }\n .filterpoodll_siv_transcriptcontainer{\n\t\theight: 200px !important;\n }\n .filterpoodll_siv_transcriptscrollable {\n\t\theight: calc(100%-37px);\n /* 163px !important; height - toolbar height */\n }\n}\n\n/* interactive transcript */\n.filterpoodll_siv_transcriptcontainer {\n font-family: Arial, sans-serif;\n border: 1px solid #111;\n}\n\n.filterpoodll_siv_transcript-header {\n height: 19px;\n padding: 2px;\n font-weight: bold;\n text-align: center;\n}\n.filterpoodll_siv_transcript-selector {\n height: 25px;\n}\n.filterpoodll_siv_transcript-body {\n overflow-y: scroll;\n background-color: #e7e7e7;\n position: relative;\n margin: auto;\n}\n\n.filterpoodll_siv_transcript-line {\n position: relative;\n padding: 5px;\n cursor: pointer;\n line-height: 1.3;\n}\n\n.filterpoodll_siv_transcript-line:nth-child(odd) {\n background-color: #f5f5f5;\n}\n\n\n.filterpoodll_siv_transcript-timestamp {\n position: absolute;\n display: inline-block;\n color: #333;\n width: 50px;\n}\n\n.filterpoodll_siv_transcript-text {\n display: block;\n margin-left: 50px;\n}\n\n.filterpoodll_siv_transcript-line:hover,\n.filterpoodll_siv_transcript-line:hover .filterpoodll_siv_transcript-timestamp,\n.filterpoodll_siv_transcript-line:hover .filterpoodll_siv_transcript-text {\n background-color: #777;\n color: #e7e7e7;\n}\n\n.filterpoodll_siv_transcript-line.is-active,\n.filterpoodll_siv_transcript-line.is-active .filterpoodll_siv_transcript-timestamp,\n.filterpoodll_siv_transcript-line.is-active .filterpoodll_siv_transcript-text {\n background-color: #555;\n color: #e7e7e7;\n width: auto;\n}\n\n.filterpoodll_siv_transcripttitle {\n /* Set line height to same height at toolbar */\n line-height: 35px;\n margin-left: 5px;\n font-weight: bold;\n}\n\n.filterpoodll_siv_transcripttoolbar {\n background-color: #CCC;\n max-height: 35px;\n min-height: 35px;\n overflow:hidden\n}\n.filterpoodll_siv_transcriptscrollable {\n width: 100%;\n font-family: Arial, sans-serif;\n overflow-x: hidden;\n overflow-y: scroll;\n scroll-behavior: smooth;\n}\n.filterpoodll_siv_transcripttools {\n float: right;\n}\n\n/* toggle timestamp on and off */\n.filterpoodll_siv_transcript-body.notimestamp .filterpoodll_siv_transcript-timestamp {\n display: none;\n}\n\n.filterpoodll_siv_transcript-body.notimestamp .filterpoodll_siv_transcript-text {\n margin-left: 5px;\n}\n","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/presets/tapwordtohearcloud.txt b/presets/tapwordtohearcloud.txt
new file mode 100644
index 0000000..aba8a25
--- /dev/null
+++ b/presets/tapwordtohearcloud.txt
@@ -0,0 +1 @@
+{"name":"Tap Word to Hear (Cloud)","key":"tapwordtohearcloud","version":"1.0.0","instructions":"This will read aloud words in the enclosed text block when tapped. Currently does not support \"sentence\" mode. (Uses Cloud Poodll) ","showatto":"1","showplayers":"0","requirecss":"","requirejs":"","shim":"","defaults":"playmode=\"word|sentence\",speaker=\"Male|Female\",language=\"English(US)|English(GB)|English(AU)|English(In)|English(Welsh)|Danish|Dutch|French(FR)|French(CA)|German|Icelandic|Italian|Japanese|Korean|Norwegian|Polish|Portugese(BR)|Portugese(PT)|Romanian|Russian|Spanish(ES)|Spanish(US)|Swedish|Turkish|Welsh\"","amd":"1","body":"\n\n
","bodyend":"
","script":"//DECLARATIONS and INITs ...........................\nvar thesentence_number =0;\nvar lettered= false;\n\n//audio player declarations\nvar aplayer = $('#' + @@AUTOID@@ + '_audio');\n\n//determine the voice\nvar mf=@@speaker@@\nswitch(@@language@@){\ncase \"English(US)\": voice = mf=='Male'?'Joey':'Kendra';break;\ncase \"English(GB)\": voice = mf=='Male'?'Brian':'Amy';break;\ncase \"English(AU)\": voice = mf=='Male'?'Russell':'Nicole';break;\ncase \"English(IN)\": voice = mf=='Male'?'Aditi':'Raveena';break;\ncase \"English(WELSH)\": voice = mf=='Male'? 'Geraint':'Geraint';break;\ncase \"Danish\": voice = mf=='Male'?'Mads':'Naja';break;\ncase \"Dutch\": voice = mf=='Male'?'Ruben':'Lotte';break;\ncase \"French(FR)\": voice = mf=='Male'?'Mathieu':'Celine';break;\ncase \"French(CA)\": voice = mf=='Male'?'Chantal':'Chantal';break;\ncase \"German\": voice = mf=='Male'?'Hans':'Marlene';break;\ncase \"Icelandic\": voice = mf=='Male'?'Karl':'Dora';break;\ncase \"Italian\": voice = mf=='Male'?'Carla':'Giorgio';break;\ncase \"Japanese\": voice = mf=='Male'?'Takumi':'Mizuki';break;\ncase \"Korean\": voice = mf=='Male'?'Seoyan':'Seoyan';break;\ncase \"Norwegian\": voice = mf=='Male'?'Liv':'Liv';break;\ncase \"Polish\": voice = mf=='Male'?'Jacek':'Ewa';break;\ncase \"Portugese(BR)\": voice = mf=='Male'?'Ricardo':'Vitoria';break;\ncase \"Portugese(PT)\": voice = mf=='Male'?'Cristiano':'Ines';break;\ncase \"Romanian\": voice = mf=='Male'?'Carmen':'Carmen';break;\ncase \"Russian\": voice = mf=='Male'?'Maxim':'Tatyana';break;\ncase \"Spanish(ES)\": voice = mf=='Male'?'Enrique':'Conchita';break;\ncase \"Spanish(US)\": voice = mf=='Male'?'Miguel':'Penelope';break;\ncase \"Swedish\": voice = mf=='Male'?'Astrid':'Astrid';break;\ncase \"Turkish\": voice = mf=='Male'?'Filiz':'Filiz';break;\ncase \"Welsh\": voice = mf=='Male'?'Gwyneth':'Gwyneth';break;\ndefault: voice = mf=='Male'?'Brian':'Amy';\n}\n\n\n//fetch the text to read\nvar useblock = $('#' + @@AUTOID@@ + '_textblock');\nvar usetext = useblock.text();\n\n//some common selectors\nvar wordselector = '#' + @@AUTOID@@+ '_textblock span.tbr_word';\nvar sentenceselector = '#' + @@AUTOID@@+ '_textblock span.tbr_sentence';\n\n//FUNCTIONS ...........................\n\n\n//FUNCTION fetch polly url\n var fetch_polly_url = function(speaktext, voice, callback) {\n\n //The REST API we are calling\n var functionname = 'local_cpapi_fetch_polly_url';\n\n //fetch the Posturl. We need this.\n //set up our ajax request\n var xhr = new XMLHttpRequest();\n var that = this;\n\n //set up our handler for the response\n xhr.onreadystatechange = function (e) {\n if (this.readyState === 4) {\n if (xhr.status == 200) {\n\n //get a yes or forgetit or tryagain\n var payload = xhr.responseText;\n var payloadobject = JSON.parse(payload);\n if (payloadobject) {\n //returnCode > 0 indicates an error\n if (payloadobject.returnCode > 0) {\n console.log(payloadobject.returnMessage);\n return false;\n //if all good, then lets do the embed\n } else if (payloadobject.returnCode === 0){\n var pollyurl = payloadobject.returnMessage;\n callback(pollyurl);\n } else {\n console.log('Polly Signed URL Request failed:');\n console.log(payloadobject);\n }\n } else {\n console.log('Polly Signed URL Request something bad happened');\n }\n } else {\n console.log('Polly Signed URL Request Not 200 response:' + xhr.status);\n }\n }\n };\n\n //make our request\n var xhrparams = \"wstoken=\" + @@CLOUDPOODLLTOKEN@@\n + \"&wsfunction=\" + functionname\n + \"&moodlewsrestformat=\" + 'json'\n + \"&text=\" + encodeURIComponent(speaktext)\n + '&texttype=text'\n + '&voice=' + voice\n + '&appid=' + 'filter_poodll'\n + '&owner=poodll'\n + '®ion=useast1';\n\n var serverurl = 'https://cloud.poodll.com' + \"/webservice/rest/server.php\";\n xhr.open(\"POST\", serverurl, true);\n xhr.setRequestHeader(\"Cache-Control\", \"no-cache\");\n xhr.setRequestHeader(\"Content-Type\", \"application/x-www-form-urlencoded\");\n xhr.send(xhrparams);\n };\n\n//FUNCTION: determine if the string is text or HTML\nvar isHTML = function (testString) {\n var htmlRegex = new RegExp(\"<([A-Za-z][A-Za-z0-9]*)\\\\b[^>]*>(.*?)\\\\1>\");\n return htmlRegex.test(testString);\n};\n\n//FUNCTION: split a text passage into words\nvar split_into_words= function(thetext){\n thetext = thetext.replace(/\\s+/g,' ').trim();\n if(thetext==''){return[]};\n return thetext.split(' '); \n};\n\n//FUNCTION: split a text passage into sentences\nvar split_into_sentences = function(thetext){\n thetext = thetext.replace(/\\s+/g,' ').trim();\n if(thetext ==''){return[]};\n return thetext.match(/([^\\.!\\?]+[\\.!\\?\"']+)|([^\\.!\\?\"']+$)/g); \n};\n\n//FUNCTION: break a text passage into words/sentences, and surround the words with marker tags\nvar spanify_text_passage = function(){\n //the itemcount er\n var itemcount = -1;\n\n //get all the text nodes in the useblock\n var textnodes = useblock.find('*').contents().filter(function(){ return this.nodeType == 3; });\n //wrap sentence or words in text block with spans\n textnodes.each(function(){\n var retpieces = ''; \n if(@@playmode@@=='word'){\n //for words\n var thewords = split_into_words($(this).text());\n for (var theword=0; theword < thewords.length; theword++){\n itemcount++;\n retpieces = retpieces + '' + thewords[theword] + ' ';\n }//end of for loop\n }else{\n //for sentences\n //something is wrong in sentence mode. For now, just use word mode.\n var thesentences = split_into_sentences($(this).text());\n for (var thesentence=0; thesentence < thesentences.length; thesentence++){\n itemcount++;\n retpieces = retpieces + '' + thesentences[thesentence] + ' ';\n }//end of for loop\n }\n $(this).replaceWith(retpieces);\n });//end of textnodes each\n};\n\n//FUNCTION: unhighlight a sentence as active\nvar dehighlight_all = function(){\n switch(@@playmode@@){\n case 'word':\n $(wordselector).removeClass('activesentence');\n break;\n case 'sentence':\n $(sentenceselector).removeClass('activesentence');\n case 'none':\n default:\n //do nothing\n }\n};\n\n//FUNCTION: highlight a word as active\nvar highlight_word = function(theword){\n $(wordselector).removeClass('activesentence');\n theword.addClass('activesentence');\n};\n\n//FUNCTION: highlight a sentence as active\nvar highlight_sentence = function(thesentence){\n $(sentenceselector).removeClass('activesentence');\n $(sentenceselector + '[data-sentenceindex=' + thesentence + ']').addClass('activesentence');\n};\n\n//FUNCTION: play a single sentence and mark it active for display purposes\nvar doplaysentence = function(thesentence){\n highlight_sentence(thesentence);\n aplayer.attr('src', instancedata[@@AUTOID@@].sentenceURLs[thesentence]);\n aplayer[0].play();\n};\n\nvar doplayword = function(theword){\n highlight_word(theword);\n fetch_polly_url(theword.text(),voice,function(pollyurl){\n aplayer.attr('src', pollyurl);\n aplayer[0].play();\n });\n \n};\n\n//AUDIO PLAYER events\naplayer[0].addEventListener('ended', function(){\n dehighlight_all();\n aplayer[0].pause();\n});\n\n//handle sentence or word clicks\n$('#' + @@AUTOID@@ + '_textblock .tbr_innerdiv').on('click', '.tbr_sentence, .tbr_word',function(){\n var clickeditem= $(this);\n var startnewplay = function(){ switch(@@playmode@@){\n case 'sentence':\n var index = clickeditem.attr('data-sentenceindex');\n doplaysentence(index);\n break;\n case 'word':\n default:\n //var index = clickeditem.attr('data-wordindex');\n doplayword(clickeditem);\n }//end of switch\n \n };//end of startnewplay function\n\n if(aplayer[0].playing){\n console.log('was playing but then got iterrupted');\n aplayer[0].pause().then(startnewplay);\n }else{ \n startnewplay();\n }//end of if\n});//end of on click handler\n\n//PROCEDURAL stuff ...........................\n//break it into sentences, and fetch data + TTS URL for each sentence\nvar sentences = split_into_sentences(usetext);\nwordstarts=[];\nwordcounts=[];\nsentenceURLs=[];\nvar previousend=0;\n\nfor (var currentsentence=0;currentsentence\n\t\n\t\n\n
\n\n
","bodyend":"
\n
","script":"//now we need to ensure multiple passages so we wrap it all in a function and call it at the end.\n//start of instance wrapper\nvar passagereader = function(PASSAGEID){\n\n//DECLARATIONS and INITs ...........................\nvar thesentence_number =0;\nvar lettered= false;\n\n//audio player declarations\nvar aplayer = $('#' + PASSAGEID + '_player');\nvar fa = $('#' + PASSAGEID + ' .fa');\n\n//text to audio preparation\nvar format = \"text\";\n\n//determine the voice\nvar mf=@@speaker@@\nswitch(@@language@@){\ncase \"English(US)\": voice = mf=='Male'?'Joey':'Kendra';break;\ncase \"English(GB)\": voice = mf=='Male'?'Brian':'Amy';break;\ncase \"English(AU)\": voice = mf=='Male'?'Russell':'Nicole';break;\ncase \"English(IN)\": voice = mf=='Male'?'Aditi':'Raveena';break;\ncase \"English(WELSH)\": voice = mf=='Male'? 'Geraint':'Geraint';break;\ncase \"Danish\": voice = mf=='Male'?'Mads':'Naja';break;\ncase \"Dutch\": voice = mf=='Male'?'Ruben':'Lotte';break;\ncase \"French(FR)\": voice = mf=='Male'?'Mathieu':'Celine';break;\ncase \"French(CA)\": voice = mf=='Male'?'Chantal':'Chantal';break;\ncase \"German\": voice = mf=='Male'?'Hans':'Marlene';break;\ncase \"Icelandic\": voice = mf=='Male'?'Karl':'Dora';break;\ncase \"Italian\": voice = mf=='Male'?'Carla':'Giorgio';break;\ncase \"Japanese\": voice = mf=='Male'?'Takumi':'Mizuki';break;\ncase \"Korean\": voice = mf=='Male'?'Seoyan':'Seoyan';break;\ncase \"Norwegian\": voice = mf=='Male'?'Liv':'Liv';break;\ncase \"Polish\": voice = mf=='Male'?'Jacek':'Ewa';break;\ncase \"Portugese(BR)\": voice = mf=='Male'?'Ricardo':'Vitoria';break;\ncase \"Portugese(PT)\": voice = mf=='Male'?'Cristiano':'Ines';break;\ncase \"Romanian\": voice = mf=='Male'?'Carmen':'Carmen';break;\ncase \"Russian\": voice = mf=='Male'?'Maxim':'Tatyana';break;\ncase \"Spanish(ES)\": voice = mf=='Male'?'Enrique':'Conchita';break;\ncase \"Spanish(US)\": voice = mf=='Male'?'Miguel':'Penelope';break;\ncase \"Swedish\": voice = mf=='Male'?'Astrid':'Astrid';break;\ncase \"Turkish\": voice = mf=='Male'?'Filiz':'Filiz';break;\ncase \"Welsh\": voice = mf=='Male'?'Gwyneth':'Gwyneth';break;\ndefault: voice = mf=='Male'?'Brian':'Amy';\n}\n\n\n//fetch the text to read\nvar useblock = $('#' + PASSAGEID + '_textblock');\nvar usetext = useblock.text();\n\n//some common selectors\nvar wordselector = '#' + PASSAGEID+ '_textblock span.tbr_word';\nvar sentenceselector = '#' + PASSAGEID+ '_textblock span.tbr_sentence';\n\n//FUNCTIONS ...........................\n//FUNCTION fetch polly url\n var fetch_polly_url = function(speaktext, voice, callback) {\n\n //The REST API we are calling\n var functionname = 'local_cpapi_fetch_polly_url';\n\n //fetch the Posturl. We need this.\n //set up our ajax request\n var xhr = new XMLHttpRequest();\n var that = this;\n\n //set up our handler for the response\n xhr.onreadystatechange = function (e) {\n if (this.readyState === 4) {\n if (xhr.status == 200) {\n\n //get a yes or forgetit or tryagain\n var payload = xhr.responseText;\n var payloadobject = JSON.parse(payload);\n if (payloadobject) {\n //returnCode > 0 indicates an error\n if (payloadobject.returnCode > 0) {\n console.log(payloadobject.returnMessage);\n return false;\n //if all good, then lets do the embed\n } else if (payloadobject.returnCode === 0){\n var pollyurl = payloadobject.returnMessage;\n callback(pollyurl);\n } else {\n console.log('Polly Signed URL Request failed:');\n console.log(payloadobject);\n }\n } else {\n console.log('Polly Signed URL Request something bad happened');\n }\n } else {\n console.log('Polly Signed URL Request Not 200 response:' + xhr.status);\n }\n }\n };\n\n //make our request\n var xhrparams = \"wstoken=\" + @@CLOUDPOODLLTOKEN@@\n + \"&wsfunction=\" + functionname\n + \"&moodlewsrestformat=\" + 'json'\n + \"&text=\" + encodeURIComponent(speaktext)\n + '&texttype=text'\n + '&voice=' + voice\n + '&appid=' + 'filter_poodll'\n + '&owner=poodll'\n + '®ion=useast1';\n\n var serverurl = 'https://cloud.poodll.com' + \"/webservice/rest/server.php\";\n xhr.open(\"POST\", serverurl, true);\n xhr.setRequestHeader(\"Cache-Control\", \"no-cache\");\n xhr.setRequestHeader(\"Content-Type\", \"application/x-www-form-urlencoded\");\n xhr.send(xhrparams);\n };\n\n//FUNCTION: determine if the string is text or HTML\nvar isHTML = function (testString) {\n var htmlRegex = new RegExp(\"<([A-Za-z][A-Za-z0-9]*)\\\\b[^>]*>(.*?)\\\\1>\");\n return htmlRegex.test(testString);\n};\n\n//FUNCTION: split a text passage into words\nvar split_into_words= function(thetext){\n thetext = thetext.replace(/\\s+/g,' ').trim();\n if(thetext==''){return[]};\n return thetext.split(' '); \n};\n\n//FUNCTION: split a text passage into sentences\nvar split_into_sentences = function(thetext){\n thetext = thetext.replace(/\\s+/g,' ').trim();\n if(thetext ==''){return[]};\n return thetext.match(/([^\\.!\\?]+[\\.!\\?\"']+)|([^\\.!\\?\"']+$)/g); \n};\n\n//FUNCTION: break a text passage into words/sentences, and surround the words with marker tags\nvar spanify_text_passage = function(){\n //the itemcount er\n var itemcount = -1;\n\n //get all the text nodes in the useblock\n var textnodes = useblock.find('*').contents().filter(function(){ return this.nodeType == 3; });\n //wrap sentence or words in text block with spans\n textnodes.each(function(){\n var retpieces = ''; \n if(@@highlightmode@@=='word'){\n //for words\n var thewords = split_into_words($(this).text());\n for (var theword=0; theword < thewords.length; theword++){\n itemcount++;\n retpieces = retpieces + '' + thewords[theword] + ' ';\n }//end of for loop\n }else{\n //for sentences\n var thesentences = split_into_sentences($(this).text());\n for (var thesentence=0; thesentence < thesentences.length; thesentence++){\n itemcount++;\n retpieces = retpieces + '' + thesentences[thesentence] + ' ';\n }//end of for loop\n }\n $(this).replaceWith(retpieces);\n });//end of textnodes each\n};\n\n//FUNCTION: unhighlight a sentence as active\nvar dehighlight_all = function(){\n switch(@@highlightmode@@){\n case 'word':\n $(wordselector,useblock).removeClass('activesentence');\n break;\n case 'sentence':\n $(sentenceselector,useblock).removeClass('activesentence');\n break;\n case 'none':\n default:\n //do nothing\n }\n}\n\n//FUNCTION: highlight a sentence as active\nvar highlight_sentence = function(thesentence){\n switch(@@highlightmode@@){\n case 'word':\n $(wordselector,useblock).removeClass('activesentence');\n $(wordselector,useblock).slice(wordstarts[thesentence],\n wordstarts[thesentence] + \n wordcounts[thesentence]).addClass('activesentence');\n break;\n case 'sentence':\n $(sentenceselector).removeClass('activesentence');\n $(sentenceselector + '[data-sentenceindex=' + thesentence + ']').addClass('activesentence');\n break;\n case 'none':\n default:\n //do nothing\n }\n}\n\n//FUNCTION: play a single sentence and mark it active for display purposes\nvar doplayaudio = function(thesentence){\n highlight_sentence(thesentence);\n aplayer.attr('src',sentenceURLs[thesentence]);\n aplayer[0].play();\n};\n\n//AUDIO PLAYER events\naplayer[0].addEventListener('ended', function(){\n if(thesentence_number< sentences.length -1){\n thesentence_number++;\n doplayaudio(thesentence_number);\n }else{\n dehighlight_all();\n $(fa).removeClass(@@pause@@);\n $(fa).addClass(@@play@@);\n aplayer[0].pause();\n }\n});\n\n//handle audio player button clicks\n$('#' + PASSAGEID).click(function(){\n if(!aplayer[0].paused && !aplayer[0].ended){\n aplayer[0].pause();\n if(@@stoporpause@@=='stop'){\n aplayer[0].load();\n thesentence_number=0;\n }\n $(fa).removeClass(@@pause@@);\n $(fa).addClass(@@play@@);\n\n //if paused and in limbo no src state\n }else if(aplayer[0].paused && aplayer.attr('src')){\n aplayer[0].play();\n $(fa).removeClass(@@play@@);\n $(fa).addClass(@@pause@@);\n//play \n}else{\n if(!lettered){\n spanify_text_passage();\n lettered=true;\n };//end of if lettered\n if(@@stoporpause@@=='stop'){\n thesentence_number=0;\n } \n doplayaudio(thesentence_number);\n $(fa).removeClass(@@play@@);\n $(fa).addClass(@@pause@@);\n }//end of if paused ended\n});\n\n//handle sentence clicks\n$('#' + PASSAGEID + '_textblock .tbr_innerdiv').on('click', '.tbr_sentence',function(){\naplayer[0].pause();\n var sentenceindex = $(this).attr('data-sentenceindex');\n $(fa).removeClass(@@play@@);\n $(fa).addClass(@@pause@@);\n thesentence_number = sentenceindex; \n doplayaudio(sentenceindex );\n});\n\n//PROCEDURAL stuff ...........................\n//break it into sentences, and fetch data + TTS URL for each sentence\nvar sentences = split_into_sentences(usetext);\nvar wordstarts=[];\nvar wordcounts=[];\nvar sentenceURLs=[];\nvar previousend=0;\nfor (var currentsentence=0;currentsentence\n\n
","bodyend":"
","script":"//FUNCTION fetch polly url\n var fetch_polly_url = function(speaktext, voice, callback) {\n\n //The REST API we are calling\n var functionname = 'local_cpapi_fetch_polly_url';\n\n //fetch the Posturl. We need this.\n //set up our ajax request\n var xhr = new XMLHttpRequest();\n var that = this;\n\n //set up our handler for the response\n xhr.onreadystatechange = function (e) {\n if (this.readyState === 4) {\n if (xhr.status == 200) {\n\n //get a yes or forgetit or tryagain\n var payload = xhr.responseText;\n var payloadobject = JSON.parse(payload);\n if (payloadobject) {\n //returnCode > 0 indicates an error\n if (payloadobject.returnCode > 0) {\n console.log(payloadobject.returnMessage);\n return false;\n //if all good, then lets do the embed\n } else if (payloadobject.returnCode === 0){\n var pollyurl = payloadobject.returnMessage;\n callback(pollyurl);\n } else {\n console.log('Polly Signed URL Request failed:');\n console.log(payloadobject);\n }\n } else {\n console.log('Polly Signed URL Request something bad happened');\n }\n } else {\n console.log('Polly Signed URL Request Not 200 response:' + xhr.status);\n }\n }\n };\n\n //make our request\n var xhrparams = \"wstoken=\" + @@CLOUDPOODLLTOKEN@@\n + \"&wsfunction=\" + functionname\n + \"&moodlewsrestformat=\" + 'json'\n + \"&text=\" + encodeURIComponent(speaktext)\n + '&texttype=text'\n + '&voice=' + voice\n + '&appid=' + 'filter_poodll'\n + '&owner=poodll'\n + '®ion=useast1';\n\n var serverurl = 'https://cloud.poodll.com' + \"/webservice/rest/server.php\";\n xhr.open(\"POST\", serverurl, true);\n xhr.setRequestHeader(\"Cache-Control\", \"no-cache\");\n xhr.setRequestHeader(\"Content-Type\", \"application/x-www-form-urlencoded\");\n xhr.send(xhrparams);\n };\n\n\nvar usetext = $('#' + @@AUTOID@@).text();\nvar theplayer = $('#' + @@AUTOID@@ + '_audioplayer');\nvar mf=@@speaker@@\n\nswitch(@@language@@){\ncase \"English(US)\": voice = mf=='Male'?'Joey':'Kendra';break;\ncase \"English(GB)\": voice = mf=='Male'?'Brian':'Amy';break;\ncase \"English(AU)\": voice = mf=='Male'?'Russell':'Nicole';break;\ncase \"English(IN)\": voice = mf=='Male'?'Aditi':'Raveena';break;\ncase \"English(WELSH)\": voice = mf=='Male'? 'Geraint':'Geraint';break;\ncase \"Danish\": voice = mf=='Male'?'Mads':'Naja';break;\ncase \"Dutch\": voice = mf=='Male'?'Ruben':'Lotte';break;\ncase \"French(FR)\": voice = mf=='Male'?'Mathieu':'Celine';break;\ncase \"French(CA)\": voice = mf=='Male'?'Chantal':'Chantal';break;\ncase \"German\": voice = mf=='Male'?'Hans':'Marlene';break;\ncase \"Icelandic\": voice = mf=='Male'?'Karl':'Dora';break;\ncase \"Italian\": voice = mf=='Male'?'Carla':'Giorgio';break;\ncase \"Japanese\": voice = mf=='Male'?'Takumi':'Mizuki';break;\ncase \"Korean\": voice = mf=='Male'?'Seoyan':'Seoyan';break;\ncase \"Norwegian\": voice = mf=='Male'?'Liv':'Liv';break;\ncase \"Polish\": voice = mf=='Male'?'Jacek':'Ewa';break;\ncase \"Portugese(BR)\": voice = mf=='Male'?'Ricardo':'Vitoria';break;\ncase \"Portugese(PT)\": voice = mf=='Male'?'Cristiano':'Ines';break;\ncase \"Romanian\": voice = mf=='Male'?'Carmen':'Carmen';break;\ncase \"Russian\": voice = mf=='Male'?'Maxim':'Tatyana';break;\ncase \"Spanish(ES)\": voice = mf=='Male'?'Enrique':'Conchita';break;\ncase \"Spanish(US)\": voice = mf=='Male'?'Miguel':'Penelope';break;\ncase \"Swedish\": voice = mf=='Male'?'Astrid':'Astrid';break;\ncase \"Turkish\": voice = mf=='Male'?'Filiz':'Filiz';break;\ncase \"Welsh\": voice = mf=='Male'?'Gwyneth':'Gwyneth';break;\ndefault: voice = mf=='Male'?'Brian':'Amy';\n}\n\n\n//we replace tags with markers to survive going into a URL and out again\n//usetext = usetext.replace(//gi, \"dddd\");\n\nvar datastring= @@format@@ + '|' + voice + '|' + usetext;\n\nfetch_polly_url(usetext,voice,function(audiourl){\n theplayer.attr('src',audiourl);\n }\n);\n\n","style":"","dataset":"","datasetvars":"","alternate":"","alternateend":""}
\ No newline at end of file
diff --git a/refreshtoken.php b/refreshtoken.php
index 24b9354..bb51472 100644
--- a/refreshtoken.php
+++ b/refreshtoken.php
@@ -16,10 +16,10 @@
// along with Moodle. If not, see .
/**
- * A token refreshing helper for Read Aloud
+ * A token refreshing helper for Generico
*
*
- * @package filter_poodll
+ * @package filter_generico
* @copyright Justin Hunt (justin@poodll.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
diff --git a/settings.php b/settings.php
index 310a0aa..668d6fe 100644
--- a/settings.php
+++ b/settings.php
@@ -73,10 +73,13 @@
$ADMIN->add($generico_category_name, $genericotemplatesadmin_settings);
+
//Templates
- $template_pages = \filter_generico\settingstools::fetch_template_pages($conf);
- foreach ($template_pages as $template_page) {
- $ADMIN->add($generico_category_name, $template_page);
+ if($ADMIN->fulltree) {
+ $template_pages = \filter_generico\settingstools::fetch_template_pages($conf);
+ foreach ($template_pages as $template_page) {
+ $ADMIN->add($generico_category_name, $template_page);
+ }
}
}
diff --git a/templates/ps-onceaudio.mustache b/templates/ps-onceaudio.mustache
new file mode 100644
index 0000000..0631544
--- /dev/null
+++ b/templates/ps-onceaudio.mustache
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+{{#js}}
+ require(['jquery'], function($) {
+
+ //time display function
+ function poodll_op_sec2time(seconds){
+ var date = new Date(null);
+ date.setSeconds(seconds);
+ return date.toISOString().substr(11, 8);
+ }
+ function poodll_op_fetchtime(theplayer){
+ var a_currenttime= isNaN(theplayer[0].currentTime) ? 0 : theplayer[0].currentTime;
+ var a_duration= isNaN(theplayer[0].duration) ? 0 : theplayer[0].duration;
+ var currenttime = poodll_op_sec2time(Math.floor(a_currenttime));
+ var totaltime = poodll_op_sec2time(Math.floor(a_duration));
+ var displaytime = currenttime + ' / ' + totaltime;
+ return displaytime;
+ }
+
+ //get our player
+ var aplayer = $('#{{AUTOID}}').children().first();
+ var fa = $('#{{AUTOID}} .fa');
+ var thestate =$('#{{AUTOID}}_state');
+ var thetime = $('#{{AUTOID}}_time');
+
+ //The timer display
+ aplayer.on('timeupdate',function(e){
+ var displaytime = poodll_op_fetchtime(aplayer)
+ thetime.text(displaytime);
+ });
+
+ //show current time
+ thetime.text(poodll_op_fetchtime(aplayer));
+
+ //set status
+ thestate.text('- ready -');
+
+ //set player ended event
+ aplayer[0].addEventListener('ended', function(){
+ aplayer[0].pause();
+ $(fa).removeClass('{{playing}}');
+ $(fa).removeClass('fa-spin');
+ $(fa).removeClass('{{play}}');
+ $(fa).addClass('{{over}}');
+ thestate.text('- finished -');
+ });
+
+ //player clicked event
+ $('#{{AUTOID}}').click(function(){
+ if(aplayer[0].ended){
+ return;
+ }
+ if(!aplayer[0].paused && {{canpause}} != '0'){
+ aplayer[0].pause();
+ thestate.text('- paused -');
+ $(fa).removeClass('{{playing}}');
+ $(fa).removeClass('fa-spin');
+ $(fa).addClass('{{play}}');
+ }else if(aplayer[0].paused || !aplayer[0].playing){
+ aplayer[0].play();
+ thestate.text('- playing -');
+ $(fa).removeClass('{{play}}');
+ $(fa).addClass('{{playing}}');
+ $(fa).addClass('fa-spin');
+ }
+ });
+
+ aplayer.bind('durationchange',function(){thetime.text(poodll_op_fetchtime(aplayer));});
+
+ });
+{{/js}}
diff --git a/version.php b/version.php
index e033c41..ffe20cd 100644
--- a/version.php
+++ b/version.php
@@ -25,8 +25,8 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2019071300; // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version = 2021010400; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2011070100; // Requires this Moodle version
$plugin->component = 'filter_generico'; // Full name of the plugin (used for diagnostics)
$plugin->maturity = MATURITY_STABLE;
-$plugin->release = 'Version 1.4.10(Build 2019071300)';
+$plugin->release = 'Version 1.4.11(Build 2021010400)';