From 251483a26cde05d9509e96e17cc1b1df9d9d4d6d Mon Sep 17 00:00:00 2001 From: justinhunt Date: Mon, 4 Jan 2021 18:53:35 +0900 Subject: [PATCH] added new presets, and optimised preset loading --- CHANGES.txt | 4 + classes/presets_control.php | 7 +- filter.php | 4 + presets/dictationchat.txt | 1 + presets/dictationcloud.txt | 1 + presets/flipclock.txt | 1 + presets/glossaryttsreadercloud.txt | 1 + presets/icontoggle.txt | 1 + presets/icontoggleright.txt | 1 + presets/onceplayer.txt | 1 + presets/onebuttonaudio.txt | 1 + presets/soundboardcloud.txt | 1 + presets/superinteractiveaudio.txt | 1 + presets/superinteractiveaudiowidget.txt | 1 + presets/superinteractivevideo.txt | 1 + presets/superinteractivevideowidget.txt | 1 + presets/tapwordtohearcloud.txt | 1 + presets/textblockreadercloud.txt | 1 + presets/ttacloud.txt | 1 + refreshtoken.php | 4 +- settings.php | 9 ++- templates/ps-onceaudio.mustache | 103 ++++++++++++++++++++++++ version.php | 4 +- 23 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 presets/dictationchat.txt create mode 100644 presets/dictationcloud.txt create mode 100644 presets/flipclock.txt create mode 100644 presets/glossaryttsreadercloud.txt create mode 100644 presets/icontoggle.txt create mode 100644 presets/icontoggleright.txt create mode 100644 presets/onceplayer.txt create mode 100644 presets/onebuttonaudio.txt create mode 100644 presets/soundboardcloud.txt create mode 100644 presets/superinteractiveaudio.txt create mode 100644 presets/superinteractiveaudiowidget.txt create mode 100644 presets/superinteractivevideo.txt create mode 100644 presets/superinteractivevideowidget.txt create mode 100644 presets/tapwordtohearcloud.txt create mode 100644 presets/textblockreadercloud.txt create mode 100644 presets/ttacloud.txt create mode 100644 templates/ps-onceaudio.mustache diff --git a/CHANGES.txt b/CHANGES.txt index a0fd334..9b4bb81 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,9 @@ Change List ========= +Version 1.4.11(Build 2021010400) +- optimized preset loading +- Fixed cloudpoodlltoken to be callable from template JS +- Added super interactive transcript widgets Version 1.4.10(Build 2019071300) -Fixed cloud poodll token fetch [again] diff --git a/classes/presets_control.php b/classes/presets_control.php index 835554c..f037876 100644 --- a/classes/presets_control.php +++ b/classes/presets_control.php @@ -44,10 +44,13 @@ class presets_control extends \admin_setting { * @param string $heading heading * @param string $information text in box */ - public function __construct($name, $visiblename, $information, $templateindex) { + public function __construct($name, $visiblename, $information, $templateindex, $presetdata=false) { $this->nosave = true; $this->templateindex = $templateindex; - $this->presetdata = $this->fetch_presets(); + if(!$presetdata){ + $presetdata=$this->fetch_presets(); + } + $this->presetdata = $presetdata; $this->visiblename = $visiblename; $this->information = $information; parent::__construct($name, $visiblename, $information, $templateindex); diff --git a/filter.php b/filter.php index ed7a155..3d3372f 100644 --- a/filter.php +++ b/filter.php @@ -193,8 +193,12 @@ function filter_generico_callback(array $link) { $token = \filter_generico\generico_utils::fetch_token($conf['cpapiuser'], $conf['cpapisecret']); if ($token) { $genericotemplate = str_replace('@@CLOUDPOODLLTOKEN@@', $token, $genericotemplate); + //stash this for passing to js + $filterprops['CLOUDPOODLLTOKEN'] = $token; } else { $genericotemplate = str_replace('@@CLOUDPOODLLTOKEN@@', 'INVALID TOKEN', $genericotemplate); + //stash this for passing to js + $filterprops['CLOUDPOODLLTOKEN'] = 'INVALID TOKEN'; } } diff --git a/presets/dictationchat.txt b/presets/dictationchat.txt new file mode 100644 index 0000000..84b2f01 --- /dev/null +++ b/presets/dictationchat.txt @@ -0,0 +1 @@ +{"name":"Dictation Chat","key":"dictationchat","version":"1.0.1","instructions":"Enter the sentences to be used for dictation as an HTML unordered list (bullet points) between the tags.","showatto":"1","showplayers":"0","requirecss":"https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css","requirejs":"","shim":"","defaults":"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\",speaker=\"Male|Female\"comparison=simple|advanced,checkcase=true|false","amd":"1","body":"\n\n
\n Loading\n
\n\n
\n \n
RESULTS
\n\n
Listen and Type
\n\n
\n

Listen and type the sentences spoken by the computer.

\n

Click 'Start' to begin the game!

\n
\n\n
\n\n\n\n
\n \n \n \n \n \n \n \n\n
","bodyend":"
\n\n
","script":"var app = {\n errors:{\n \"no_text\":\"No text items found! Ensure you add your text items between
  • tags!\"\n },\n spliton: new RegExp('([,.!?:;\" ])','g'),\n game:{\n pointer:0\n },\n usevoice: 'Amy',\n \n setvoice: function(){\n //determine voice\n var mf=@@speaker@@;\n var voice='Amy';\n switch(@@language@@){\n case \"English(US)\": voice = mf=='Male'?'Joey':'Kendra';break;\n case \"English(GB)\": voice = mf=='Male'?'Brian':'Amy';break;\n case \"English(AU)\": voice = mf=='Male'?'Russell':'Nicole';break;\n case \"English(IN)\": voice = mf=='Male'?'Aditi':'Raveena';break;\n case \"English(Welsh)\": voice = mf=='Male'? 'Geraint':'Geraint';break;\n case \"Danish\": voice = mf=='Male'?'Mads':'Naja';break;\n case \"Dutch\": voice = mf=='Male'?'Ruben':'Lotte';break;\n case \"French(FR)\": voice = mf=='Male'?'Mathieu':'Celine';break;\n case \"French(CA)\": voice = mf=='Male'?'Chantal':'Chantal';break;\n case \"German\": voice = mf=='Male'?'Hans':'Marlene';break;\n case \"Icelandic\": voice = mf=='Male'?'Karl':'Dora';break;\n case \"Italian\": voice = mf=='Male'?'Carla':'Giorgio';break;\n case \"Japanese\": voice = mf=='Male'?'Takumi':'Mizuki';break;\n case \"Korean\": voice = mf=='Male'?'Seoyan':'Seoyan';break;\n case \"Norwegian\": voice = mf=='Male'?'Liv':'Liv';break;\n case \"Polish\": voice = mf=='Male'?'Jacek':'Ewa';break;\n case \"Portugese(BR)\": voice = mf=='Male'?'Ricardo':'Vitoria';break;\n case \"Portugese(PT)\": voice = mf=='Male'?'Cristiano':'Ines';break;\n case \"Romanian\": voice = mf=='Male'?'Carmen':'Carmen';break;\n case \"Russian\": voice = mf=='Male'?'Maxim':'Tatyana';break;\n case \"Spanish(ES)\": voice = mf=='Male'?'Enrique':'Conchita';break;\n case \"Spanish(US)\": voice = mf=='Male'?'Miguel':'Penelope';break;\n case \"Swedish\": voice = mf=='Male'?'Astrid':'Astrid';break;\n case \"Turkish\": voice = mf=='Male'?'Filiz':'Filiz';break;\n case \"Welsh\": voice = mf=='Male'?'Gwyneth':'Gwyneth';break;\n default: voice = mf=='Male'?'Brian':'Amy';\n }\n this.usevoice=voice;\n },\n\n preloadAudio:function(){\n \n var self=this;\n var fullyLoaded = false;\n \n console.log(\"Preloading audio..\");\n \n self.items.forEach(function(item,idx){\n \n self.getTTS(item.target,self.usevoice,function(pollyurl){\n \n item.audio = new Audio();\n item.audio.src=pollyurl;\n \n fullyLoaded = self.items.filter(function(item){\n return item.audio === null\n }).length === 0;\n \n if(fullyLoaded){\n self.appReady();\n }\n });\n \n });\n },\n getItems:function(){\n \n var self=this;\n var text_items = [];\n \n console.log(\"Getting items..\");\n \n $(\"#dictate_container li\").each(function(){\n text_items.push($(this).text().trim());\n });\n \n this.items = text_items.map(function(target){\n return {\n targetWords:target.split(self.spliton).filter(function(e){return e!==\"\";}),\n target:target,\n typed:\"\",\n answered:false,\n correct:false,\n audio:null\n };\n }).filter(function(e){return e.target!==\"\";});\n\n console.log(this.items);\n\n if(!this.items.length){\n $(\"#dictate_error\").text(this.errors.no_text).show();\n }\n \n else{\n self.preloadAudio();\n }\n \n },\n init:function(){\n\n window.app = this;\n var self = this;\n \n console.log(\"Loading libraries..\");\n \n require(['jquery', 'core/ajax'], function($, Ajax) {\n self.ajax = Ajax;\n self.setvoice();\n self.getItems();\n });\n \n },\n appReady:function(){\n console.log(\"App ready!\");\n $(\"#dictate_container li\").remove();\n this.attachHandlers();\n $(\"#dictate_loading\").hide();\n $(\"#dictate_container\").show();\n },\n gotComparison: function(comparison,typed){\n \n var self = this;\n \n $(\".targetWord\").addClass(\"correct\").removeClass(\"incorrect\");\n \n if(!Object.keys(comparison).length){\n //add success marker here if wish\n $(\".speech.teacher_left\").text(self.items[self.game.pointer].target+\"\");\n\n self.items[self.game.pointer].answered=true;\n self.items[self.game.pointer].correct=true;\n self.items[self.game.pointer].typed=typed;\n \n if(self.game.pointer\"+numCorrect+\"/\"+totalNum).show();\n this.animateCSS(\"#dictate_results\", \"tada\", function(){\n setTimeout(function(){\n $(\"#dictate_results\").fadeOut();\n },2000);\n });\n $(\"#dictate_game\").hide();\n $(\"#dictate_start_btn\").show();\n $(\"#dictate_mainmenu\").show();\n $(\"#dictate_controls\").hide();\n $(\"#dictate_title\").html(\"Listen and Repeat\");\n },\n start:function(){\n this.items.forEach(function(item){\n item.spoken=\"\";\n item.answered=false;\n item.correct=false;\n });\n this.game.pointer=0;\n $(\"#dictate_game\").show();\n $(\"#dictate_start_btn\").hide();\n $(\"#dictate_mainmenu\").hide();\n $(\"#dictate_controls\").show();\n this.nextPrompt();\n },\n nextPrompt:function(){\n \n var self = this;\n\n var target = this.items[this.game.pointer].target;\n var id = \"dictate_prompt_\"+this.game.pointer;\n var code=\"\";\n \n $(\"#dictate_game\").html(code);\n $(\".ctrl-btn\").prop(\"disabled\",false);\n \n var color;\n \n var progress = self.items.map(function(item,idx){\n color = \"gray\";\n if(self.items[idx].answered && self.items[idx].correct){\n color=\"green\";\n }\n else if(self.items[idx].answered && !self.items[idx].correct){\n color=\"red\";\n }\n return \"\";\n }).join(\" \");\n \n $(\"#dictate_title\").html(progress);\n $(\"#\"+id).toggle(\"slide\",{direction:'left'});\n \n self.nextReply();\n \n }, \n nextReply:function(){\n var self = this;\n var id = \"dictate_reply_\"+this.game.pointer;\n var target = this.items[this.game.pointer].target;\n var code=\"\";\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\n\n
    \n
    \n
    ","bodyend":"","script":"//time display function\nfunction poodll_op_sec2time(seconds){\n var date = new Date(null);\n date.setSeconds(seconds); \n return date.toISOString().substr(11, 8);\n}\nfunction poodll_op_fetchtime(theplayer){\n var a_currenttime= isNaN(theplayer[0].currentTime) ? 0 : theplayer[0].currentTime;\n var a_duration= isNaN(theplayer[0].duration) ? 0 : theplayer[0].duration;\n var currenttime = poodll_op_sec2time(Math.floor(a_currenttime));\n var totaltime = poodll_op_sec2time(Math.floor(a_duration)); \n var displaytime = currenttime + ' / ' + totaltime;\n return displaytime;\n}\n\n//get our player\nvar aplayer = $('#' + @@AUTOID@@).children().first();\nvar fa = $('#' + @@AUTOID@@ + ' .fa');\nvar thestate =$('#' + @@AUTOID@@ + '_state'); \nvar thetime = $('#' + @@AUTOID@@ + '_time'); \n\n//The timer display\naplayer.on('timeupdate',function(e){\n var displaytime = poodll_op_fetchtime(aplayer)\n thetime.text(displaytime);\n});\n\n//show current time\n thetime.text(poodll_op_fetchtime(aplayer));\n\n//set status\nthestate.text('- ready -');\n\n//set player ended event\naplayer[0].addEventListener('ended', function(){\n aplayer[0].pause();\n $(fa).removeClass(@@playing@@);\n $(fa).removeClass('fa-spin');\n $(fa).removeClass(@@play@@);\n $(fa).addClass(@@over@@);\n thestate.text('- finished -');\n});\n\n//player clicked event\n$('#' + @@AUTOID@@).click(function(){\n if(aplayer[0].ended){\n return;\n}\n if(!aplayer[0].paused && @@canpause@@ != '0'){\n aplayer[0].pause();\n thestate.text('- paused -');\n $(fa).removeClass(@@playing@@);\n $(fa).removeClass('fa-spin');\n $(fa).addClass(@@play@@);\n }else if(aplayer[0].paused || !aplayer[0].playing){\n aplayer[0].play();\n thestate.text('- playing -');\n $(fa).removeClass(@@play@@);\n $(fa).addClass(@@playing@@);\n $(fa).addClass('fa-spin');\n }\n});\n\naplayer.bind('durationchange',function(){thetime.text(poodll_op_fetchtime(aplayer));});\n\nvar is_https = M.cfg.wwwroot.indexOf('https:')==0;\nvar is_ios = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;\nif(is_https && is_ios){\n var xhr = new XMLHttpRequest();\n xhr.onreadystatechange = function(){\n if (this.readyState == 4 && this.status == 200){\n var url = window.URL || window.webkitURL;\n aplayer[0].src = url.createObjectURL(this.response);\n }\n }\n xhr.open('GET', @@VIDEOURL@@);\n xhr.responseType = 'blob';\n xhr.send(); \n}\n\n//the volume slider\n/*\n$('#' + @@AUTOID@@ + '_slider').slider({\norientation: 'horizontal',\nvalue: aplayer[0].volume,\nmin: 0,\nmax: 1,\nrange: 'min',\nanimate: true,\nstep: 0.1,\nslide: function(e,ui){\n aplayer[0].volume = ui.value;\n}\n});\n*/\n","style":"/* //code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css */\n.poodll_onceplayer{\n border: 3px solid blue;\n/* Safari 3-4, iOS 1-3.2, Android 1.6- */\n -webkit-border-radius: 12px; \n /* Firefox 1-3.6 */\n -moz-border-radius: 12px; \n /* Opera 10.5, IE 9, Safari 5, Chrome, Firefox 4, iOS 4, Android 2.1+ */\n border-radius: 12px; \nbackground-color: white;\n width: 200px;\n height: 110px;\ntext-align: center;\n}\n.poodll_onceplayer_time{\n padding: 3px;\n}\n.poodll_onceplayer_button{\n border: 1px solid;\n width: 50px;\n height: 42px;\n margin-top: 5px;\n margin-top: 5px;\n}","dataset":"","datasetvars":"","alternate":"","alternateend":""} \ No newline at end of file diff --git a/presets/onebuttonaudio.txt b/presets/onebuttonaudio.txt new file mode 100644 index 0000000..0cea2dc --- /dev/null +++ b/presets/onebuttonaudio.txt @@ -0,0 +1 @@ +{"name":"One Button Audio","key":"OneButtonAudio","version":"1.0.7","instructions":"A One Button Audio player","showatto":"0","showplayers":"1","requirecss":"","requirejs":"","shim":"","defaults":"size=80,progress=007E88,background=333,font=fff,","amd":"1","body":"\n
    \n\n\t\n
    ","bodyend":"","script":"var player = $('#' + @@AUTOID@@ + '_audio'); \nvar button= $('#' + @@AUTOID@@ + '_button'); \nvar width =@@size@@;\nvar height =@@size@@;\nvar playing=false;\nvar canvas = document.getElementById(@@AUTOID@@ + '_canvas');\ncanvas.width=width;\ncanvas.height=height;\nvar context = canvas.getContext('2d');\nvar x = width / 2; \nvar y = height / 2;\nvar radius = width * 0.35;\nvar endPercent = 100;\nvar curPerc = 0;\nvar counterClockwise = false;\nvar circ = Math.PI * 2;\nvar quart = Math.PI / 2;\n\n//set icon size\nvar fontsize = @@size@@ > 50 ? 20 : 10;\n$( '#' + @@AUTOID@@ + '_button .fa' ).css( \"font-size\",fontsize+'px' );\n\n//set up drawing\nif(width>60){\n context.lineWidth = 10;\n}else{\n context.lineWidth = 5;\n}\ncontext.strokeStyle = '#' + @@progress@@;\ncontext.shadowOffsetX = 0;\ncontext.shadowOffsetY = 0;\ncontext.shadowBlur = 10;\n//context.shadowColor = '#fff';\n\n//to fetch current % of playback\nvar fetchCurrent= function(){\n var currentTime = player.prop('currentTime');\n var duration = player.prop('duration');\n var current = currentTime/duration;\n return current;\n}\n\nvar clear = function(){\n context.clearRect(0, 0, canvas.width, canvas.height);\n};\n\n//recursive draw arc\nvar draw = function() {\n if(!playing){return;}\n context.beginPath();\n context.arc(x, y, radius, -(quart), ((circ) * fetchCurrent()) - quart, false);\n context.stroke();\n \t var drawFrame = requestAnimationFrame(draw);\n}\n\nvar doPlay = function(){\n player.get(0).load();\n var pp = player.get(0).play();\n if(pp !== 'undefined'){\n pp.then(function(){\n playing=true;\n draw();\n}).catch(function(e){\n //do nothing\n });//end of then catch\n}else{\n playing=true;\n draw(); \n}//endof if pp\n};//end of doplay\n\n\n//add button events\nbutton.click(function(){\nif($(this).hasClass('playing')){\n\t$(this).removeClass('playing');\n\t$(this).empty();\n\t$(this).append('');\n $( '#' + @@AUTOID@@ + '_button .fa' ).css( \"color\",'#'+@@font@@+'' );\n}else{\n\t$(this).addClass('playing');\n\t$(this).empty();\n\t$(this).append('');\n $( '#' + @@AUTOID@@ + '_button .fa' ).css( \"color\",'#'+@@font@@+'' );\n}\n$( '#' + @@AUTOID@@ + '_button .fa' ).css( \"font-size\",fontsize+'px' );\n\n\nif(!player.prop('paused') && !player.prop('ended')){\n //change button style \n player.get(0).pause();\n playing=false;\n clear(); \n}else{\n //change button style\n clear();\n doPlay();\n}//end of if playerprop\n})\n\nplayer.on('ended',function(){\n //change button style\n player.get(0).pause();\n playing=false;\n clear();\n if(button.hasClass('playing')){\n\t$(button).removeClass('playing');\n\t$(button).empty();\n\t$(button).append('');\n $( '#' + @@AUTOID@@ + '_button .fa' ).css(\"color\",'#'+@@font@@+'' );\n $( '#' + @@AUTOID@@ + '_button .fa' ).css( \"font-size\",fontsize+'px' );\n }\n})\n$( '#' + @@AUTOID@@ + '_button .fa' ).css(\"background-color\",'#'+@@background@@+'' );\n$( '#' + @@AUTOID@@ + '_button .fa' ).css( \"color\",'#'+@@font@@+'' );","style":"/* SINGLE BUTTON PLAYER */\n.poodll_onebuttonaudio_container{\n position: relative;\n text-align: center;\n border-radius: 50%;\n background-color: #333;\n}\nbutton.poodll_onebuttonaudio_button{\n height: 60%;\n width: 60%;\n border-radius: 50%;\n border: none;\n background-color: #333;\nbackground-image: none;\n position: absolute;\n top: 50%;\n left: 50%;\n -webkit-transform: translateY(-50%) translateX(-50%);\n -ms-transform: translateY(-50%) translateX(-50%);\n transform: translateY(-50%) translateX(-50%);\n padding: 0px !important;\n text-align: center;\n margin: 0px !important;\n}\n.poodll_onebuttonaudio_button button:focus{\n\tborder-style: none;\n}\n\n.poodll_onebuttonaudio_button:focus{\noutline: none;\nborder: none !important;\n}\n.poodll_onebuttonaudio_button .fa{\n margin-top: 2px;\n font-size: 10px;\n}\n","dataset":"","datasetvars":"","alternate":"","alternateend":""} \ No newline at end of file diff --git a/presets/soundboardcloud.txt b/presets/soundboardcloud.txt new file mode 100644 index 0000000..fed8ad7 --- /dev/null +++ b/presets/soundboardcloud.txt @@ -0,0 +1 @@ +{"name":"Sound Board (Cloud)","key":"soundboardcloud","version":"1.0.0","instructions":"Add an html list (bullets) of phrases to be read, between the tags. They will become buttons that speak when tapped.","showatto":"1","showplayers":"0","requirecss":"","requirejs":"","shim":"","defaults":"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\",\nspeaker=\"Male|Female\"","amd":"1","body":"
    \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','.fp_soundboard_player_trigger', 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 += '@TEXT@';\ntemplate += '
    ';\n\n\n$('#' + @@AUTOID@@ + \" li\" ).each(function(index) {\n var usetext = $(this).text();\n\n fetch_polly_url(usetext,voice,\n function(itemindex,itemusetext){\n return function(pollyurl){\n var usetemplate = template.replace('@AUDIO@',pollyurl); \n usetemplate = usetemplate.replace('@TEXT@',itemusetext); \n usetemplate = usetemplate.replace(/@ID@/g, itemindex);\n theitemscontainer.append(usetemplate);\n \n //add events\n var theaudio= $('#fp_soundboard_player_' + itemindex + ' audio');\n };\n \n }(index,usetext));\n\n \n\n });//end of each loop","style":"","dataset":"","datasetvars":"","alternate":"","alternateend":""} \ No newline at end of file diff --git a/presets/superinteractiveaudio.txt b/presets/superinteractiveaudio.txt new file mode 100644 index 0000000..e3fc5aa --- /dev/null +++ b/presets/superinteractiveaudio.txt @@ -0,0 +1 @@ +{"name":"Super Interactive Audio","key":"superinteractiveaudio","version":"1.0.3","instructions":"Shows a audio + interactive transcript. The captions are made and edited here on Moodle.","showatto":"0","showplayers":"1","requirecss":"","requirejs":"","shim":"","defaults":"lang=\"en\"","amd":"1","body":"
    \n\n\t
    \n
    \n \n \n \n \n \n \n \n
    \n
    \n
    \n
    \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":"
    \n\n\t
    \n
    \n \n \n \n \n \n \n \n
    \n
    \n
    \n
    \n\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='';// 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\n\t
    \n\t
    \n
    \n \n \n \n \n \n \n \n \n \n
    \n
    \n
    \n
    \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":"
    \n\n\t
    \n\t
    \n
    \n \n \n \n \n \n \n \n \n \n
    \n
    \n
    \n
    \n\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=''; //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[^>]*>(.*?)\");\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[^>]*>(.*?)\");\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","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)';