# HG changeset patch # User Dave Moffat # Date 1500043164 -3600 # Node ID 231d2186f3fc82363dffebdeed5a756f33525661 # Parent 56e72cd184043f4788803669118ba9e49128e377# Parent 5d5542e01fe14fdd41284c065927cc6c7394e72d Merge branch 'master' of https://github.com/BrechtDeMan/WebAudioEvaluationTool diff -r 56e72cd18404 -r 231d2186f3fc css/core.css --- a/css/core.css Fri Jul 14 15:37:53 2017 +0100 +++ b/css/core.css Fri Jul 14 15:39:24 2017 +0100 @@ -8,6 +8,12 @@ margin-bottom: 10px; font-size: 2em; } +div#footer { + position: fixed; + bottom: 0px; + width: 100%; + text-align: center; +} div.indicator-box { position: absolute; left: 150px; @@ -65,9 +71,10 @@ div#popupResponse { width: inherit; min-height: 50px; - max-height: 320px; + max-height: 270px; overflow: auto; position: relative; + margin-bottom: 10px; } button.popupButton { /* Button for popup window @@ -200,7 +207,7 @@ position: absolute; top: 20px; left: 50px; - width: 250px%; + width: 250px; padding: 5px; } div#master-volume-root { @@ -211,17 +218,16 @@ height: 40px; } input#master-volume-control { - width: 200px; + width: 190px; height: 25px; float: left; margin: 0px; padding: 0px; } span#master-volume-feedback { - width: 45px; height: 25px; - margin-left: 5px; - float: left; + margin: 0px 5px; + float: right; } div.error-colour { background-color: #FF8F8F; @@ -262,3 +268,26 @@ div.comment-slider-text-holder span { margin: 0px 5px; } +div.comment-checkbox-inputs-holder { + display: flex; + flex-direction: row; + justify-content: space-around; + margin: 10px 5px; +} +div.comment-checkbox-inputs-flex { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; +} +div.comment-box-playing { + background-color: #FFDDDD; +} +div#imageController { + align-content: center; + text-align: center; + height: 250px; +} +div#imageController img { + max-height: 250px; +} diff -r 56e72cd18404 -r 231d2186f3fc docs/Instructions/img/interface-AB.png Binary file docs/Instructions/img/interface-AB.png has changed diff -r 56e72cd18404 -r 231d2186f3fc docs/Instructions/img/interface-ABC.png Binary file docs/Instructions/img/interface-ABC.png has changed diff -r 56e72cd18404 -r 231d2186f3fc docs/Instructions/img/interface-ABX.png Binary file docs/Instructions/img/interface-ABX.png has changed diff -r 56e72cd18404 -r 231d2186f3fc docs/Instructions/img/interface-MUSHRA.png Binary file docs/Instructions/img/interface-MUSHRA.png has changed diff -r 56e72cd18404 -r 231d2186f3fc docs/Instructions/img/interface-ape.png Binary file docs/Instructions/img/interface-ape.png has changed diff -r 56e72cd18404 -r 231d2186f3fc docs/Instructions/img/interface-discrete.png Binary file docs/Instructions/img/interface-discrete.png has changed diff -r 56e72cd18404 -r 231d2186f3fc docs/Instructions/img/interface-horizontal.png Binary file docs/Instructions/img/interface-horizontal.png has changed diff -r 56e72cd18404 -r 231d2186f3fc docs/Instructions/img/interface-timeline.png Binary file docs/Instructions/img/interface-timeline.png has changed diff -r 56e72cd18404 -r 231d2186f3fc index.html --- a/index.html Fri Jul 14 15:37:53 2017 +0100 +++ b/index.html Fri Jul 14 15:39:24 2017 +0100 @@ -24,7 +24,7 @@
-

Web Audio Evaluation Tool

+

Web Audio Evaluation Toolbox (v1.2.1)

Start menu

+ diff -r 56e72cd18404 -r 231d2186f3fc interfaces/AB.css --- a/interfaces/AB.css Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/AB.css Fri Jul 14 15:39:24 2017 +0100 @@ -2,6 +2,10 @@ /* Set the background colour (note US English spelling) to grey*/ background-color: #fff } +div#feedbackHolder { + display: flex; + flex-direction: column; +} div.pageTitle { width: auto; height: 20px; @@ -34,13 +38,20 @@ position: inherit; margin: 0px 5px; } +div#box-holders { + width: 100%; + text-align: center; +} +div#playback-holder { + float: none; +} div.comparator-holder { width: 260px; height: 300px; border: black 1px solid; - float: left; padding-top: 5px; margin: 25px; + display: inline-block; } div.comparator-selector { width: 248px; @@ -49,12 +60,26 @@ position: relative; background-color: #FF0000; border-radius: 20px; + text-align: center; + display: block; + margin: auto; +} +div.comparator-image { + background-color: rgba(255, 255, 255, 0); +} +img.comparator-image { + width: inherit; + height: inherit; + z-index: -1; + position: absolute; + display: inline; + right: 0px; } div.disabled { background-color: #AAA; } div.selected { - background-color: #008000; + background-color: rgba(0, 200, 0, 0.4); } div.comparator-selector span { font-size: 4em; @@ -72,8 +97,3 @@ float: left; margin: 0px 5px; } -div#master-volume-holder { - position: absolute; - top: 10px; - left: 120px; -} diff -r 56e72cd18404 -r 231d2186f3fc interfaces/AB.js --- a/interfaces/AB.js Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/AB.js Fri Jul 14 15:39:24 2017 +0100 @@ -1,6 +1,6 @@ // Once this is loaded and parsed, begin execution loadInterface(); - +/*globals window, interfaceContext, testState, Interface, audioEngineContext, console, document, specification, $, storage*/ function loadInterface() { // Get the dimensions of the screen available to the page var width = window.innerWidth; @@ -10,34 +10,6 @@ // Custom comparator Object Interface.prototype.comparator = null; - Interface.prototype.checkScaleRange = function (min, max) { - var page = testState.getCurrentTestPage(); - var audioObjects = audioEngineContext.audioObjects; - var state = true; - var str = "Please keep listening. "; - var minRanking = Infinity; - var maxRanking = -Infinity; - for (var ao of audioObjects) { - var rank = ao.interfaceDOM.getValue(); - if (rank < minRanking) { - minRanking = rank; - } - if (rank > maxRanking) { - maxRanking = rank; - } - } - if (maxRanking * 100 < max) { - str += "At least one fragment must be selected." - state = false; - } - if (!state) { - console.log(str); - this.storeErrorNode(str); - interfaceContext.lightbox.post("Message", str); - } - return state; - } - // The injection point into the HTML page interfaceContext.insertPoint = document.getElementById("topLevelBody"); var testContent = document.createElement('div'); @@ -52,7 +24,7 @@ titleSpan.id = "test-title"; // Set title to that defined in XML, else set to default - if (titleAttr != undefined) { + if (titleAttr !== undefined) { titleSpan.textContent = titleAttr; } else { titleSpan.textContent = 'Listening test'; @@ -63,7 +35,8 @@ var pagetitle = document.createElement('div'); pagetitle.className = "pageTitle"; pagetitle.align = "center"; - var titleSpan = document.createElement('span'); + + titleSpan = document.createElement('span'); titleSpan.id = "pageTitle"; pagetitle.appendChild(titleSpan); @@ -100,9 +73,7 @@ // Construct the AB Boxes var boxes = document.createElement('div'); - boxes.align = "center"; boxes.id = "box-holders"; - boxes.style.float = "left"; var submit = document.createElement('button'); submit.id = "submit"; @@ -150,13 +121,18 @@ // Set the page title if (typeof audioHolderObject.title == "string" && audioHolderObject.title.length > 0) { - document.getElementById("test-title").textContent = audioHolderObject.title + document.getElementById("test-title").textContent = audioHolderObject.title; } - if (interfaceObj.title != null) { + if (interfaceObj.title !== undefined) { document.getElementById("pageTitle").textContent = interfaceObj.title; } + if (interfaceObj.image !== undefined) { + feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("box-holders")); + interfaceContext.imageHolder.setImage(interfaceObj.image); + } + var interfaceOptions = interfaceObj.options; // Clear the interfaceElements { @@ -182,7 +158,7 @@ switch (option.name) { case "playhead": var playbackHolder = document.getElementById('playback-holder'); - if (playbackHolder == null) { + if (playbackHolder === null) { playbackHolder = document.createElement('div'); playbackHolder.id = 'playback-holder'; playbackHolder.style.width = "100%"; @@ -194,7 +170,7 @@ break; case "page-count": var pagecountHolder = document.getElementById('page-count'); - if (pagecountHolder == null) { + if (pagecountHolder === null) { pagecountHolder = document.createElement('div'); pagecountHolder.id = 'page-count'; document.getElementById('interface-buttons').appendChild(pagecountHolder); @@ -202,7 +178,7 @@ pagecountHolder.innerHTML = 'Page ' + (testState.stateIndex + 1) + ' of ' + testState.stateMap.length + ''; break; case "volume": - if (document.getElementById('master-volume-holder-float') == null) { + if (document.getElementById('master-volume-holder-float') === null) { feedbackHolder.appendChild(interfaceContext.volume.object); } break; @@ -237,6 +213,13 @@ this.box.id = 'comparator-' + text; this.selector = document.createElement('div'); this.selector.className = 'comparator-selector disabled'; + if (audioElement.specification.image) { + this.selector.className += " comparator-image"; + var image = document.createElement("img"); + image.src = audioElement.specification.image; + image.className = "comparator-image"; + this.selector.appendChild(image); + } var selectorText = document.createElement('span'); selectorText.textContent = text; this.selector.appendChild(selectorText); @@ -246,51 +229,50 @@ this.playback.textContent = "Listen"; this.box.appendChild(this.selector); this.box.appendChild(this.playback); - this.selector.onclick = function (event) { + this.selectorClicked = function () { + var i; var time = audioEngineContext.timer.getTestTime(); - if ($(event.currentTarget).hasClass('disabled')) { + if (this.parent.state !== 1) { + interfaceContext.lightbox.post("Message", "Please wait for the sample to load"); console.log("Please wait until sample has loaded"); return; } - if (audioEngineContext.status == 0) { + if (audioEngineContext.status === 0) { interfaceContext.lightbox.post("Message", "Please listen to the samples before making a selection"); console.log("Please listen to the samples before making a selection"); return; } - var id = event.currentTarget.parentElement.getAttribute('track-id'); - interfaceContext.comparator.selected = id; - if ($(event.currentTarget).hasClass("selected")) { - $(".comparator-selector").removeClass('selected'); - for (var i = 0; i < interfaceContext.comparator.comparators.length; i++) { - var obj = interfaceContext.comparator.comparators[i]; - obj.parent.metric.moved(time, 0); - obj.value = 0; + interfaceContext.comparator.selected = this.id; + $(".comparator-selector").removeClass('selected'); + $(this.selector).addClass('selected'); + this.comparator.comparators.forEach(function (a) { + if (a !== this) { + a.value = 0; + } else { + a.value = 1; } - } else { - $(".comparator-selector").removeClass('selected'); - $(event.currentTarget).addClass('selected'); - for (var i = 0; i < interfaceContext.comparator.comparators.length; i++) { - var obj = interfaceContext.comparator.comparators[i]; - if (i == id) { - obj.value = 1; - } else { - obj.value = 0; - } - obj.parent.metric.moved(time, obj.value); - } - console.log("Selected " + id + ' (' + time + ')'); - } + a.parent.metric.moved(time, a.value); + }, this); + console.log("Selected " + this.id + ' (' + time + ')'); }; this.playback.setAttribute("playstate", "ready"); - this.playback.onclick = function (event) { - var id = event.currentTarget.parentElement.getAttribute('track-id'); - if (event.currentTarget.getAttribute("playstate") == "ready") { - audioEngineContext.play(id); - } else if (event.currentTarget.getAttribute("playstate") == "playing") { + this.playbackClicked = function () { + if (this.playback.getAttribute("playstate") == "ready") { + audioEngineContext.play(this.id); + } else if (this.playback.getAttribute("playstate") == "playing") { audioEngineContext.stop(); } }; + this.handleEvent = function (event) { + if (event.currentTarget === this.selector) { + this.selectorClicked(); + } else if (event.currentTarget === this.playback) { + this.playbackClicked(); + } + }; + this.playback.addEventListener("click", this); + this.selector.addEventListener("click", this); this.enable = function () { if (this.parent.state == 1) { @@ -311,23 +293,34 @@ // audioObject has an error!! this.playback.textContent = "Error"; $(this.playback).addClass("error-colour"); - } + }; this.startPlayback = function () { if (this.parent.specification.parent.playOne || specification.playOne) { $('.comparator-button').text('Wait'); $('.comparator-button').attr("disabled", "true"); - $(this.playback).css("disabled", "false"); + $(this.playback).removeAttr("disabled"); } else { $('.comparator-button').text('Listen'); } $(this.playback).text('Stop'); this.playback.setAttribute("playstate", "playing"); + interfaceContext.commentBoxes.highlightById(audioElement.id); }; this.stopPlayback = function () { if (this.playback.getAttribute("playstate") == "playing") { - $('.comparator-button').text('Listen'); - $('.comparator-button').removeAttr("disabled"); + $(this.playback).text('Listen'); + $(this.playback).removeAttr("disabled"); this.playback.setAttribute("playstate", "ready"); + if (this.parent.specification.parent.playOne || specification.playOne) { + $('.comparator-button').text('Listen'); + $('.comparator-button').removeAttr("disabled"); + } + } + var box = interfaceContext.commentBoxes.boxes.find(function (a) { + return a.id === audioElement.id; + }); + if (box) { + box.highlight(false); } }; this.exportXMLDOM = function (audioObject) { @@ -369,6 +362,11 @@ label = interfaceContext.getLabel(labelType, index, audioHolderObject.labelStart); } var node = new this.comparatorBox(audioObject, index, label); + Object.defineProperties(node, { + 'comparator': { + 'value': this + } + }); audioObject.bindInterface(node); this.comparators.push(node); this.boxHolders.appendChild(node.box); @@ -387,12 +385,9 @@ boxW = numObj * 312; diff = window.innerWidth - boxW; } - document.getElementById('box-holders').style.marginLeft = diff / 2 + 'px'; - document.getElementById('box-holders').style.marginRight = diff / 2 + 'px'; - document.getElementById('box-holders').style.width = boxW + 'px'; var outsideRef = document.getElementById('outside-reference'); - if (outsideRef != null) { + if (outsideRef !== null) { outsideRef.style.left = (window.innerWidth - 120) / 2 + 'px'; } } @@ -400,42 +395,47 @@ function buttonSubmitClick() { var checks = testState.currentStateMap.interfaces[0].options, canContinue = true; + + if (interfaceContext.checkFragmentMinPlays() === false) { + return; +} for (var i = 0; i < checks.length; i++) { if (checks[i].type == 'check') { + var checkState; switch (checks[i].name) { case 'fragmentPlayed': // Check if all fragments have been played - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); + if (checkState === false) { canContinue = false; } break; case 'fragmentFullPlayback': // Check all fragments have been played to their full length - var checkState = interfaceContext.checkFragmentsFullyPlayed(); - if (checkState == false) { + checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage); + if (checkState === false) { canContinue = false; } break; case 'fragmentMoved': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllMoved(); - if (checkState == false) { + checkState = interfaceContext.checkAllMoved(checks[i].errorMessage); + if (checkState === false) { canContinue = false; } break; case 'fragmentComments': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllCommented(); - if (checkState == false) { + checkState = interfaceContext.checkAllCommented(checks[i].errorMessage); + if (checkState === false) { canContinue = false; } break; case 'scalerange': // Check the scale has been used effectively - var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max); - if (checkState == false) { + checkState = interfaceContext.checkScaleRange(checks[i].errorMessage); + if (checkState === false) { canContinue = false; } break; @@ -455,7 +455,7 @@ playback.click(); // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options } else { - if (audioEngineContext.timer.testStarted == false) { + if (audioEngineContext.timer.testStarted === false) { interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!'); return; } diff -r 56e72cd18404 -r 231d2186f3fc interfaces/ABX.css --- a/interfaces/ABX.css Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/ABX.css Fri Jul 14 15:39:24 2017 +0100 @@ -20,13 +20,20 @@ height: 40px; font-size: 1.2em; } +div#box-holders { + width: 100%; + text-align: center; +} +div#playback-holder { + float: none; +} div.comparator-holder { width: 260px; height: 300px; border: black 1px solid; - float: left; padding-top: 5px; margin: 25px; + display: inline-block; } div.comparator-selector { width: 248px; @@ -35,12 +42,26 @@ position: relative; background-color: #FF0000; border-radius: 20px; + text-align: center; + display: block; + margin: auto; +} +div.comparator-image { + background-color: rgba(255, 255, 255, 0); +} +img.comparator-image { + width: inherit; + height: inherit; + z-index: -1; + position: absolute; + display: inline; + right: 0px; } div.disabled { background-color: #AAA; } div.selected { - background-color: #008000; + background-color: rgba(0, 200, 0, 0.4); } div.comparator-selector.inactive { background-color: yellow !important; @@ -61,8 +82,3 @@ float: left; margin: 0px 5px; } -div#master-volume-holder { - position: absolute; - top: 10px; - left: 120px; -} diff -r 56e72cd18404 -r 231d2186f3fc interfaces/ABX.js --- a/interfaces/ABX.js Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/ABX.js Fri Jul 14 15:39:24 2017 +0100 @@ -4,6 +4,7 @@ */ // Once this is loaded and parsed, begin execution +/* globals interfaceContext, Interface, testState, audioEngineContext, console, document, window, feedbackHolder, $, specification, storage*/ loadInterface(); function loadInterface() { @@ -12,36 +13,8 @@ interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema - Interface.prototype.checkScaleRange = function (min, max) { - var page = testState.getCurrentTestPage(); - var audioObjects = audioEngineContext.audioObjects; - var state = true; - var str = "Please keep listening. "; - var minRanking = Infinity; - var maxRanking = -Infinity; - for (var ao of audioObjects) { - var rank = ao.interfaceDOM.getValue(); - if (rank < minRanking) { - minRanking = rank; - } - if (rank > maxRanking) { - maxRanking = rank; - } - } - if (maxRanking * 100 < max) { - str += "At least one fragment must be selected." - state = false; - } - if (!state) { - console.log(str); - this.storeErrorNode(str); - interfaceContext.lightbox.post("Message", str); - } - return state; - } - // Custom comparator Object - Interface.prototype.comparator = null; + interfaceContext.comparator = null; // The injection point into the HTML page interfaceContext.insertPoint = document.getElementById("topLevelBody"); @@ -57,7 +30,7 @@ titleSpan.id = "test-title"; // Set title to that defined in XML, else set to default - if (titleAttr != undefined) { + if (titleAttr !== undefined) { titleSpan.textContent = titleAttr; } else { titleSpan.textContent = 'Listening test'; @@ -68,7 +41,8 @@ var pagetitle = document.createElement('div'); pagetitle.className = "pageTitle"; pagetitle.align = "center"; - var titleSpan = document.createElement('span'); + + titleSpan = document.createElement('span'); titleSpan.id = "pageTitle"; pagetitle.appendChild(titleSpan); @@ -131,7 +105,7 @@ // Load the full interface testState.initialise(); testState.advanceState(); -}; +} function loadTest(page) { // Called each time a new test page is to be build. The page specification node is the only item passed in @@ -148,13 +122,18 @@ // Set the page title if (typeof page.title == "string" && page.title.length > 0) { - document.getElementById("test-title").textContent = page.title + document.getElementById("test-title").textContent = page.title; } - if (interfaceObj.title != null) { + if (interfaceObj.title !== null) { document.getElementById("pageTitle").textContent = interfaceObj.title; } + if (interfaceObj.image !== undefined) { + feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("box-holders")); + interfaceContext.imageHolder.setImage(interfaceObj.image); + } + interfaceContext.comparator = new comparator(page); var interfaceOptions = interfaceObj.options; @@ -163,7 +142,7 @@ switch (option.name) { case "playhead": var playbackHolder = document.getElementById('playback-holder'); - if (playbackHolder == null) { + if (playbackHolder === null) { playbackHolder = document.createElement('div'); playbackHolder.style.width = "100%"; playbackHolder.style.float = "left"; @@ -174,7 +153,7 @@ break; case "page-count": var pagecountHolder = document.getElementById('page-count'); - if (pagecountHolder == null) { + if (pagecountHolder === null) { pagecountHolder = document.createElement('div'); pagecountHolder.id = 'page-count'; } @@ -183,7 +162,7 @@ inject.appendChild(pagecountHolder); break; case "volume": - if (document.getElementById('master-volume-holder') == null) { + if (document.getElementById('master-volume-holder') === null) { feedbackHolder.appendChild(interfaceContext.volume.object); } break; @@ -229,62 +208,65 @@ this.playback.className = 'comparator-button'; this.playback.disabled = true; this.playback.textContent = "Listen"; + if (element.specification.image) { + this.selector.className += " comparator-image"; + var image = document.createElement("img"); + image.src = element.specification.image; + image.className = "comparator-image"; + this.selector.appendChild(image); + } else if (label === "X") { + this.selector.classList.add('inactive'); + } this.box.appendChild(this.selector); this.box.appendChild(this.playback); - this.selector.onclick = function (event) { - var label = event.currentTarget.children[0].textContent; + this.selectorClicked = function (event) { if (label == "X" || label == "x") { return; } var time = audioEngineContext.timer.getTestTime(); - if ($(event.currentTarget).hasClass('disabled')) { + if (this.disabled) { + interfaceContext.lightbox.post("Message", "Please wait until sample has loaded"); console.log("Please wait until sample has loaded"); return; } - if (audioEngineContext.status == 0) { + if (audioEngineContext.status === 0) { interfaceContext.lightbox.post("Message", "Please listen to the samples before making a selection"); console.log("Please listen to the samples before making a selection"); return; } - var id = event.currentTarget.parentElement.getAttribute('track-id'); - interfaceContext.comparator.selected = id; - if ($(event.currentTarget).hasClass("selected")) { - $(".comparator-selector").removeClass('selected'); - for (var i = 0; i < interfaceContext.comparator.pair.length; i++) { - var obj = interfaceContext.comparator.pair[i]; - obj.parent.metric.moved(time, 0); - obj.value = 0; - } - } else { - $(".comparator-selector").removeClass('selected'); - $(event.currentTarget).addClass('selected'); - for (var i = 0; i < interfaceContext.comparator.pair.length; i++) { - var obj = interfaceContext.comparator.pair[i]; - if (i == id) { - obj.value = 1; - } else { - obj.value = 0; - } - obj.parent.metric.moved(time, obj.value); - } - console.log("Selected " + id + ' (' + time + ')'); - } + interfaceContext.comparator.selected = this.id; + $(".comparator-selector").removeClass('selected'); + $(this.selector).addClass('selected'); + interfaceContext.comparator.pair.forEach(function (obj) { + obj.value = 1.0 * (obj === this); + obj.parent.metric.moved(time, obj.value); + }, this); + console.log("Selected " + this.id + ' (' + time + ')'); }; this.playback.setAttribute("playstate", "ready"); - this.playback.onclick = function (event) { - var id = event.currentTarget.parentElement.getAttribute('track-id'); - if (event.currentTarget.getAttribute("playstate") == "ready") { - audioEngineContext.play(id); + this.playbackClicked = function (event) { + if (this.playback.getAttribute("playstate") == "ready") { + audioEngineContext.play(this.id); } else if (event.currentTarget.getAttribute("playstate") == "playing") { audioEngineContext.stop(); } }; + this.handleEvent = function (event) { + if (event.currentTarget === this.playback) { + this.playbackClicked(event); + } else if (event.currentTarget === this.selector) { + this.selectorClicked(event); + } + }; + this.playback.addEventListener("click", this); + this.selector.addEventListener("click", this); this.enable = function () { // This is used to tell the interface object that playback of this node is ready if (this.parent.state == 1) { $(this.selector).removeClass('disabled'); this.playback.disabled = false; + this.disabled = false; } }; this.updateLoading = function (progress) { @@ -312,6 +294,7 @@ } $(this.playback).text('Stop'); this.playback.setAttribute("playstate", "playing"); + interfaceContext.commentBoxes.highlightById(element.id); }; this.stopPlayback = function () { if (this.playback.getAttribute("playstate") == "playing") { @@ -319,6 +302,12 @@ $('.comparator-button').removeAttr("disabled"); this.playback.setAttribute("playstate", "ready"); } + var box = interfaceContext.commentBoxes.boxes.find(function (a) { + return a.id === element.id; + }); + if (box) { + box.highlight(false); + } }; this.getValue = function () { // Return the current value of the object. If there is no value, return 0 @@ -345,7 +334,7 @@ }; this.error = function () { // If there is an error with the audioObject, this will be called to indicate a failure - } + }; }; // Ensure there are only two comparisons per page if (page.audioElements.length != 2) { @@ -353,31 +342,42 @@ return; } // Build the three audio elements + + function buildElement(index, audioObject) { + var label; + switch (index) { + case 0: + label = "A"; + break; + case 1: + label = "B"; + break; + default: + label = "X"; + break; + } + var node = new this.interfaceObject(audioObject, label); + audioObject.bindInterface(node); + return node; + } + this.pair = []; this.X = null; this.boxHolders = document.getElementById('box-holders'); - for (var index = 0; index < page.audioElements.length; index++) { - var element = page.audioElements[index]; + var node; + page.audioElements.forEach(function (element, index) { if (element.type != 'normal') { console.log("WARNING - ABX can only have normal elements. Page " + page.id + ", Element " + element.id); element.type = "normal"; } - var audioObject = audioEngineContext.newTrack(element); - var label; - if (index == 0) { - label = "A"; - } else { - label = "B"; - } - var node = new this.interfaceObject(audioObject, label); - audioObject.bindInterface(node); + node = buildElement.call(this, index, audioEngineContext.newTrack(element)); this.pair.push(node); this.boxHolders.appendChild(node.box); - } + }, this); var elementId = Math.floor(Math.random() * 2); //Randomly pick A or B to be X - var element = new page.audioElementNode(specification); + var element = page.addAudioElement(); for (var atr in page.audioElements[elementId]) { - eval("element." + atr + " = page.audioElements[elementId]." + atr); + element[atr] = page.audioElements[elementId][atr]; } element.id += "-X"; if (typeof element.name == "string") { @@ -397,18 +397,9 @@ aeNode.appendChild(storage.document.createElement('metric')); root.appendChild(aeNode); // Build the 'X' element + var label; var audioObject = audioEngineContext.newTrack(element); - var label; - switch (audioObject.specification.parent.label) { - case "letter": - label = "x"; - break; - default: - label = "X"; - break; - } - var node = new this.interfaceObject(audioObject, label); - node.box.children[0].classList.add('inactive'); + node = buildElement.call(this, 3, audioObject); audioObject.bindInterface(node); this.X = node; this.boxHolders.appendChild(node.box); @@ -433,43 +424,33 @@ var checks = testState.currentStateMap.interfaces[0].options, canContinue = true; + if (interfaceContext.checkFragmentMinPlays() === false) { + return; + } + for (var i = 0; i < checks.length; i++) { + var checkState = true; if (checks[i].type == 'check') { switch (checks[i].name) { case 'fragmentPlayed': // Check if all fragments have been played - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); + break; case 'fragmentFullPlayback': // Check all fragments have been played to their full length - var checkState = interfaceContext.checkFragmentsFullyPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage); break; case 'fragmentMoved': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllMoved(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllMoved(checks[i].errorMessage); break; case 'fragmentComments': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllCommented(); - if (checkState == false) { - canContinue = false; - } break; case 'scalerange': // Check the scale has been used effectively - var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max); - if (checkState == false) { - canContinue = false; - } + console.log("WARNING - Check 'scalerange' does not make sense in AB/ABX! Ignoring!"); break; default: console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface"); @@ -477,17 +458,19 @@ } } - if (!canContinue) { + if (checkState === false) { + canContinue = false; break; } } + if (canContinue) { if (audioEngineContext.status == 1) { var playback = document.getElementById('playback-button'); playback.click(); // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options } else { - if (audioEngineContext.timer.testStarted == false) { + if (audioEngineContext.timer.testStarted === false) { interfaceContext.lightbox.post("Warning", 'You have not started the test! Please listen to a sample to begin the test!'); return; } diff -r 56e72cd18404 -r 231d2186f3fc interfaces/ape.css --- a/interfaces/ape.css Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/ape.css Fri Jul 14 15:39:24 2017 +0100 @@ -89,3 +89,11 @@ top: 10px; left: 120px; } +div.imageController { + align-content: center; + text-align: center; + height: 250px; +} +div.imageController img { + max-height: 250px; +} diff -r 56e72cd18404 -r 231d2186f3fc interfaces/ape.js --- a/interfaces/ape.js Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/ape.js Fri Jul 14 15:39:24 2017 +0100 @@ -3,7 +3,8 @@ * Create the APE interface */ - +/*globals window,interfaceContext, document, audioEngineContext, console, $, Interface, testState, storage, specification */ +/*globals metricTracker */ // Once this is loaded and parsed, begin execution loadInterface(); @@ -21,7 +22,7 @@ // Bindings for interfaceContext interfaceContext.checkAllPlayed = function () { - hasBeenPlayed = audioEngineContext.checkAllPlayed(); + var hasBeenPlayed = audioEngineContext.checkAllPlayed(); if (hasBeenPlayed.length > 0) // if a fragment has not been played yet { var str = ""; @@ -53,14 +54,14 @@ var interfaceTID = []; for (var j = 0; j < this.interfaceSliders[i].metrics.length; j++) { var ao_id = this.interfaceSliders[i].sliders[j].getAttribute("trackIndex"); - if (this.interfaceSliders[i].metrics[j].wasMoved == false && audioEngineContext.audioObjects[ao_id].interfaceDOM.canMove()) { + if (this.interfaceSliders[i].metrics[j].wasMoved === false && audioEngineContext.audioObjects[ao_id].interfaceDOM.canMove()) { state = false; interfaceTID.push(j); } } - if (interfaceTID.length != 0) { + if (interfaceTID.length !== 0) { var interfaceName = this.interfaceSliders[i].interfaceObject.title; - if (interfaceName == undefined) { + if (interfaceName === undefined) { str += 'On axis ' + String(i + 1) + ' you must move '; } else { str += 'On axis "' + interfaceName + '" you must move '; @@ -76,7 +77,7 @@ } } } - if (state != true) { + if (state !== true) { this.storeErrorNode(str); interfaceContext.lightbox.post("Message", str); console.log(str); @@ -84,75 +85,38 @@ return state; }; - Interface.prototype.checkAllCommented = function () { + interfaceContext.checkScaleRange = function () { var audioObjs = audioEngineContext.audioObjects; var audioHolder = testState.stateMap[testState.stateIndex]; - var state = true; - if (audioHolder.elementComments) { - var strNums = []; - for (var i = 0; i < audioObjs.length; i++) { - if (audioObjs[i].commentDOM.trackCommentBox.value.length == 0) { - state = false; - strNums.push(i); - } - } - if (state == false) { - var str = ""; - if (strNums.length > 1) { - - for (var i = 0; i < strNums.length; i++) { - var ao_id = audioEngineContext.audioObjects[strNums[i]].interfaceDOM.getPresentedId(); - str = str + (ao_id); // start from 1 - if (i < strNums.length - 2) { - str += ", "; - } else if (i == strNums.length - 2) { - str += " or "; - } - } - str = 'You have not commented on fragments ' + str + ' yet. Please listen, rate and comment all samples before submitting.'; - } else { - str = 'You have not commented on fragment ' + (audioEngineContext.audioObjects[strNums[0]].interfaceDOM.getPresentedId()) + ' yet. Please listen, rate and comment all samples before submitting.'; - } - this.storeErrorNode(str); - interfaceContext.lightbox.post("Message", str); - console.log(str); - } - } - return state; - }; - - Interface.prototype.checkScaleRange = function () { - var audioObjs = audioEngineContext.audioObjects; - var audioHolder = testState.stateMap[testState.stateIndex]; + var interfaceObject = this.interfaceSliders[0].interfaceObject; var state = true; var str = ''; - for (var i = 0; i < this.interfaceSliders.length; i++) { - var minScale; - var maxScale; - var interfaceObject = interfaceContext.interfaceSliders[0].interfaceObject; - for (var j = 0; j < interfaceObject.options.length; j++) { - if (interfaceObject.options[j].check == "scalerange") { - minScale = interfaceObject.options[j].min; - maxScale = interfaceObject.options[j].max; - break; - } + this.interfaceSliders.forEach(function (sliderHolder, i) { + var scales = (function () { + var scaleRange = interfaceObject.options.find(function (a) { + return a.name == "scalerange"; + }); + return { + min: scaleRange.min, + max: scaleRange.max + }; + })(); + var range = sliderHolder.sliders.reduce(function (a, b) { + var v = convSliderPosToRate(b) * 100.0; + return { + min: Math.min(a.min, v), + max: Math.max(a.max, v) + }; + }, { + min: 100, + max: 0 + }); + if (range.min >= scales.min || range.max <= scales.max) { + state = false; + str += 'On axis "' + sliderHolder.interfaceObject.title + '" you have not used the full width of the scale. '; } - var minRanking = convSliderPosToRate(this.interfaceSliders[i].sliders[0]); - var maxRanking = minRanking; - for (var j = 1; j < this.interfaceSliders[i].sliders.length; j++) { - var ranking = convSliderPosToRate(this.interfaceSliders[i].sliders[j]); - if (ranking < minRanking) { - minRanking = ranking; - } else if (ranking > maxRanking) { - maxRanking = ranking; - } - } - if (minRanking > minScale || maxRanking < maxScale) { - state = false; - str += 'On axis "' + this.interfaceSliders[i].interfaceObject.title + '" you have not used the full width of the scale. '; - } - } - if (state != true) { + }); + if (state !== true) { this.storeErrorNode(str); interfaceContext.lightbox.post("Message", str); console.log(str); @@ -163,13 +127,13 @@ Interface.prototype.objectSelected = null; Interface.prototype.objectMoved = false; Interface.prototype.selectObject = function (object) { - if (this.objectSelected == null) { + if (this.objectSelected === null) { this.objectSelected = object; this.objectMoved = false; } }; Interface.prototype.moveObject = function () { - if (this.objectMoved == false) { + if (this.objectMoved === false) { this.objectMoved = true; } }; @@ -198,7 +162,7 @@ titleSpan.id = "test-title"; // Set title to that defined in XML, else set to default - if (titleAttr != undefined) { + if (titleAttr !== undefined) { titleSpan.textContent = titleAttr; } else { titleSpan.textContent = 'Listening test'; @@ -274,12 +238,12 @@ sliderHolder.innerHTML = ""; // Set labelType if default to number - if (audioHolderObject.label == "default" || audioHolderObject.label == "") { + if (audioHolderObject.label === "default" || audioHolderObject.label === "") { audioHolderObject.label = "number"; } // Set the page title if (typeof audioHolderObject.title == "string" && audioHolderObject.title.length > 0) { - document.getElementById("test-title").textContent = audioHolderObject.title + document.getElementById("test-title").textContent = audioHolderObject.title; } @@ -287,17 +251,17 @@ document.getElementById("outside-reference-holder").innerHTML = ""; var interfaceObj = interfaceContext.getCombinedInterfaces(audioHolderObject); - for (var k = 0; k < interfaceObj.length; k++) { + interfaceObj.forEach(function (interfaceObjectInstance) { // Create the div box to center align - interfaceContext.interfaceSliders.push(new interfaceSliderHolder(interfaceObj[k])); - } + interfaceContext.interfaceSliders.push(new interfaceSliderHolder(interfaceObjectInstance, audioHolderObject)); + }); interfaceObj.forEach(function (interface) { - for (var option of interface.options) { + interface.options.forEach(function (option) { if (option.type == "show") { switch (option.name) { case "playhead": var playbackHolder = document.getElementById('playback-holder'); - if (playbackHolder == null) { + if (playbackHolder === null) { playbackHolder = document.createElement('div'); playbackHolder.style.width = "100%"; playbackHolder.align = 'center'; @@ -307,7 +271,7 @@ break; case "page-count": var pagecountHolder = document.getElementById('page-count'); - if (pagecountHolder == null) { + if (pagecountHolder === null) { pagecountHolder = document.createElement('div'); pagecountHolder.id = 'page-count'; } @@ -316,7 +280,7 @@ inject.appendChild(pagecountHolder); break; case "volume": - if (document.getElementById('master-volume-holder') == null) { + if (document.getElementById('master-volume-holder') === null) { feedbackHolder.appendChild(interfaceContext.volume.object); } break; @@ -325,7 +289,7 @@ break; } } - } + }); }); var commentBoxPrefix = "Comment on fragment"; @@ -334,7 +298,7 @@ var loopPlayback = audioHolderObject.loop; - currentTestHolder = document.createElement('audioHolder'); + var currentTestHolder = document.createElement('audioHolder'); currentTestHolder.id = audioHolderObject.id; currentTestHolder.repeatCount = audioHolderObject.repeatCount; @@ -372,7 +336,7 @@ $('.slider').mousemove(function (event) { event.preventDefault(); var obj = interfaceContext.getSelectedObject(); - if (obj == null) { + if (obj === null) { return; } var move = event.clientX - 6; @@ -386,7 +350,7 @@ $('.slider').on('touchmove', null, function (event) { event.preventDefault(); var obj = interfaceContext.getSelectedObject(); - if (obj == null) { + if (obj === null) { return; } var move = event.originalEvent.targetTouches[0].clientX - 6; @@ -400,14 +364,15 @@ $(document).mouseup(function (event) { event.preventDefault(); var obj = interfaceContext.getSelectedObject(); - if (obj == null) { + if (obj === null) { return; } var interfaceID = obj.parentElement.getAttribute("interfaceid"); var trackID = obj.getAttribute("trackindex"); - if (interfaceContext.hasSelectedObjectMoved() == true) { + var id; + if (interfaceContext.hasSelectedObjectMoved() === true) { var l = $(obj).css("left"); - var id = obj.getAttribute('trackIndex'); + id = obj.getAttribute('trackIndex'); var time = audioEngineContext.timer.getTestTime(); var rate = convSliderPosToRate(obj); audioEngineContext.audioObjects[id].metric.moved(time, rate); @@ -415,7 +380,7 @@ console.log("slider " + id + " moved to " + rate + ' (' + time + ')'); obj.setAttribute("slider-value", convSliderPosToRate(obj)); } else { - var id = Number(obj.attributes['trackIndex'].value); + id = Number(obj.attributes.trackIndex.value); //audioEngineContext.metric.sliderPlayed(id); audioEngineContext.play(id); } @@ -424,12 +389,12 @@ $('.slider').on('touchend', null, function (event) { var obj = interfaceContext.getSelectedObject(); - if (obj == null) { + if (obj === null) { return; } var interfaceID = obj.parentElement.getAttribute("interfaceid"); var trackID = obj.getAttribute("trackindex"); - if (interfaceContext.hasSelectedObjectMoved() == true) { + if (interfaceContext.hasSelectedObjectMoved() === true) { var l = $(obj).css("left"); var id = obj.getAttribute('trackIndex'); var time = audioEngineContext.timer.getTestTime(); @@ -446,7 +411,7 @@ for (var i = 0; i < interfaceList[k].options.length; i++) { if (interfaceList[k].options[i].type == 'show' && interfaceList[k].options[i].name == 'playhead') { var playbackHolder = document.getElementById('playback-holder'); - if (playbackHolder == null) { + if (playbackHolder === null) { playbackHolder = document.createElement('div'); playbackHolder.id = "playback-holder"; playbackHolder.style.width = "100%"; @@ -456,7 +421,7 @@ } } else if (interfaceList[k].options[i].type == 'show' && interfaceList[k].options[i].name == 'page-count') { var pagecountHolder = document.getElementById('page-count'); - if (pagecountHolder == null) { + if (pagecountHolder === null) { pagecountHolder = document.createElement('div'); pagecountHolder.id = 'page-count'; } @@ -464,7 +429,7 @@ var inject = document.getElementById('interface-buttons'); inject.appendChild(pagecountHolder); } else if (interfaceList[k].options[i].type == 'show' && interfaceList[k].options[i].name == 'volume') { - if (document.getElementById('master-volume-holder') == null) { + if (document.getElementById('master-volume-holder') === null) { feedbackHolder.appendChild(interfaceContext.volume.object); } } else if (interfaceList[k].options[i].type == 'show' && interfaceList[k].options[i].name == 'comments') { @@ -482,7 +447,7 @@ //testWaitIndicator(); } -function interfaceSliderHolder(interfaceObject) { +function interfaceSliderHolder(interfaceObject, page) { this.sliders = []; this.metrics = []; this.id = document.getElementsByClassName("sliderCanvasDiv").length; @@ -491,13 +456,28 @@ this.sliderDOM = document.createElement('div'); this.sliderDOM.className = 'sliderCanvasDiv'; this.sliderDOM.id = 'sliderCanvasHolder-' + this.id; + this.imageHolder = (function () { + var imageController = {}; + imageController.root = document.createElement("div"); + imageController.root.className = "imageController"; + imageController.img = document.createElement("img"); + imageController.root.appendChild(imageController.img); + imageController.setImage = function (src) { + imageController.img.src = ""; + if (typeof src !== "string" || src.length === undefined) { + return; + } + imageController.img.src = src; + }; + return imageController; + })(); var pagetitle = document.createElement('div'); pagetitle.className = "pageTitle"; pagetitle.align = "center"; var titleSpan = document.createElement('span'); titleSpan.id = "pageTitle-" + this.id; - if (interfaceObject.title != undefined && typeof interfaceObject.title == "string") { + if (interfaceObject.title !== undefined && typeof interfaceObject.title == "string") { titleSpan.textContent = interfaceObject.title; } else { titleSpan.textContent = "Axis " + String(this.id + 1); @@ -505,9 +485,15 @@ pagetitle.appendChild(titleSpan); this.sliderDOM.appendChild(pagetitle); + if (interfaceObject.image !== undefined || page.audioElements.some(function (a) { + return a.image !== undefined; + })) { + this.sliderDOM.appendChild(this.imageHolder.root); + this.imageHolder.setImage(interfaceObject.image); + } // Create the slider box to hold the slider elements this.canvas = document.createElement('div'); - if (this.name != undefined) + if (this.name !== undefined) this.canvas.id = 'slider-' + this.name; else this.canvas.id = 'slider-' + this.id; @@ -531,16 +517,16 @@ var positionScale = this.canvas.style.width.substr(0, this.canvas.style.width.length - 2); var offset = 50; var dest = document.getElementById("slider-holder").appendChild(this.sliderDOM); - for (var scaleObj of interfaceObject.scales) { + interfaceObject.scales.forEach(function (scaleObj) { var position = Number(scaleObj.position) * 0.01; var pixelPosition = (position * $(this.canvas).width()) + offset; var scaleDOM = document.createElement('span'); scaleDOM.className = "ape-marker-text"; scaleDOM.textContent = scaleObj.text; - scaleDOM.setAttribute('value', position) + scaleDOM.setAttribute('value', position); this.scale.appendChild(scaleDOM); scaleDOM.style.left = Math.floor((pixelPosition - ($(scaleDOM).width() / 2))) + 'px'; - } + }, this); this.createSliderObject = function (audioObject, label) { var trackObj = document.createElement('div'); @@ -548,7 +534,7 @@ trackObj.className = 'track-slider track-slider-disabled track-slider-' + audioObject.id; trackObj.id = 'track-slider-' + this.id + '-' + audioObject.id; trackObj.setAttribute('trackIndex', audioObject.id); - if (this.name != undefined) { + if (this.name !== undefined) { trackObj.setAttribute('interface-name', this.name); } else { trackObj.setAttribute('interface-name', this.id); @@ -572,25 +558,36 @@ }; this.resize = function (event) { - var width = window.innerWidth; var sliderDiv = this.canvas; var sliderScaleDiv = this.scale; var width = $(sliderDiv).width(); var marginsize = 50; // Move sliders into new position - for (var index = 0; index < this.sliders.length; index++) { - var pix = Number(this.sliders[index].getAttribute("slider-value")) * width; - this.sliders[index].style.left = (pix + marginsize) + 'px'; - } + this.sliders.forEach(function (slider, index) { + var pix = Number(slider.getAttribute("slider-value")) * width; + slider.style.left = (pix + marginsize) + 'px'; + }); // Move scale labels for (var index = 0; index < this.scale.children.length; index++) { var scaleObj = this.scale.children[index]; - var position = Number(scaleObj.attributes['value'].value); + var position = Number(scaleObj.attributes.value.value); var pixelPosition = (position * width) + marginsize; scaleObj.style.left = Math.floor((pixelPosition - ($(scaleObj).width() / 2))) + 'px'; } }; + + this.playing = function (id) { + var node = audioEngineContext.audioObjects.find(function (a) { + return a.id == id; + }); + if (node === undefined) { + this.imageHolder.setImage(interfaceObject.image || ""); + return; + } + var imgurl = node.specification.image || interfaceObject.image || ""; + this.imageHolder.setImage(imgurl); + } } function sliderObject(audioObject, interfaceObjects, index) { @@ -626,8 +623,7 @@ $('.track-slider').removeClass('track-slider-playing'); var name = ".track-slider-" + this.parent.id; $(name).addClass('track-slider-playing'); - $('.comment-div').removeClass('comment-box-playing'); - $('#comment-div-' + this.parent.id).addClass('comment-box-playing'); + interfaceContext.commentBoxes.highlightById(audioObject.id); $('.outside-reference').removeClass('track-slider-playing'); this.playing = true; @@ -635,15 +631,23 @@ $('.track-slider').addClass('track-slider-disabled'); $('.outside-reference').addClass('track-slider-disabled'); } + interfaceContext.interfaceSliders.forEach(function (ts) { + ts.playing(this.parent.id); + }, this); }; this.stopPlayback = function () { if (this.playing) { this.playing = false; var name = ".track-slider-" + this.parent.id; $(name).removeClass('track-slider-playing'); - $('#comment-div-' + this.parent.id).removeClass('comment-box-playing'); $('.track-slider').removeClass('track-slider-disabled'); $('.outside-reference').removeClass('track-slider-disabled'); + var box = interfaceContext.commentBoxes.boxes.find(function (a) { + return a.id === audioObject.id; + }); + if (box) { + box.highlight(false); + } } }; this.exportXMLDOM = function (audioObject) { @@ -651,7 +655,9 @@ var obj = []; $(this.trackSliderObjects).each(function (i, trackObj) { var node = storage.document.createElement('value'); - node.setAttribute("interface-name", trackObj.getAttribute("interface-name")); + if (trackObj.getAttribute("interface-name") !== "null") { + node.setAttribute("interface-name", trackObj.getAttribute("interface-name")); + } node.textContent = convSliderPosToRate(trackObj); obj.push(node); }); @@ -671,7 +677,7 @@ // audioObject has an error!! this.playback.textContent = "Error"; $(this.playback).addClass("error-colour"); - } + }; } function outsideReferenceDOM(audioObject, index, inject) { @@ -684,16 +690,13 @@ this.outsideReferenceHolder.appendChild(outsideReferenceHolderspan); this.outsideReferenceHolder.setAttribute('track-id', index); - this.outsideReferenceHolder.onclick = function (event) { - audioEngineContext.play(event.currentTarget.getAttribute('track-id')); + this.handleEvent = function (event) { + audioEngineContext.play(audioObject.id); $('.track-slider').removeClass('track-slider-playing'); $('.comment-div').removeClass('comment-box-playing'); - if (event.currentTarget.nodeName == 'DIV') { - $(event.currentTarget).addClass('track-slider-playing'); - } else { - $(event.currentTarget.parentElement).addClass('track-slider-playing'); - } + $(this.outsideReferenceHolder).addClass('track-slider-playing'); }; + this.outsideReferenceHolder.addEventListener("click", this.handleEvent); inject.appendChild(this.outsideReferenceHolder); this.enable = function () { if (this.parent.state == 1) { @@ -733,7 +736,7 @@ // audioObject has an error!! this.outsideReferenceHolder.textContent = "Error"; $(this.outsideReferenceHolder).addClass("error-colour"); - } + }; } function buttonSubmitClick() { @@ -741,50 +744,39 @@ canContinue = true; // Check that the anchor and reference objects are correctly placed - if (interfaceContext.checkHiddenAnchor() == false) { + if (interfaceContext.checkHiddenAnchor() === false) { return; } - if (interfaceContext.checkHiddenReference() == false) { + if (interfaceContext.checkHiddenReference() === false) { + return; + } + if (interfaceContext.checkFragmentMinPlays() === false) { return; } for (var i = 0; i < checks.length; i++) { + var checkState = true; if (checks[i].type == 'check') { switch (checks[i].name) { case 'fragmentPlayed': // Check if all fragments have been played - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); break; case 'fragmentFullPlayback': // Check all fragments have been played to their full length - var checkState = interfaceContext.checkFragmentsFullyPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage); break; case 'fragmentMoved': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllMoved(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllMoved(checks[i].errorMessage); break; case 'fragmentComments': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllCommented(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllCommented(checks[i].errorMessage); break; case 'scalerange': // Check the scale is used to its full width outlined by the node - var checkState = interfaceContext.checkScaleRange(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkScaleRange(checks[i].errorMessage); break; default: console.log("WARNING - Check option " + checks[i].name + " is not supported on this interface"); @@ -792,7 +784,8 @@ } } - if (!canContinue) { + if (checkState === false) { + canContinue = false; break; } } @@ -803,7 +796,7 @@ playback.click(); // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options } else { - if (audioEngineContext.timer.testStarted == false) { + if (audioEngineContext.timer.testStarted === false) { interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click a fragment to begin the test!'); return; } @@ -847,9 +840,9 @@ var audioelements = store.getElementsByTagName("audioelement"); for (var i = 0; i < audioelements.length; i++) { // Have to append the metric specific nodes - if (pageSpecification.outsideReference == null || pageSpecification.outsideReference.id != audioelements[i].id) { + if (pageSpecification.outsideReference === undefined || pageSpecification.outsideReference.id != audioelements[i].id) { var inject = audioelements[i].getElementsByTagName("metric"); - if (inject.length == 0) { + if (inject.length === 0) { inject = storage.document.createElement("metric"); } else { inject = inject[0]; @@ -859,9 +852,10 @@ for (var j = 0; j < mrnodes.length; j++) { var name = mrnodes[j].getAttribute("name"); if (name == "elementTracker" || name == "elementTrackerFull" || name == "elementInitialPosition" || name == "elementFlagMoved") { - mrnodes[j].setAttribute("interface-name", interfaceContext.interfaceSliders[k].name); + if (interfaceContext.interfaceSliders[k].name !== null) { + mrnodes[j].setAttribute("interface-name", interfaceContext.interfaceSliders[k].name); + } mrnodes[j].setAttribute("interface-id", k); - inject.appendChild(mrnodes[j]); } } } diff -r 56e72cd18404 -r 231d2186f3fc interfaces/discrete.js --- a/interfaces/discrete.js Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/discrete.js Fri Jul 14 15:39:24 2017 +0100 @@ -1,3 +1,4 @@ +/* globals interfaceContext, document, window, $, specification, audioEngineContext, console, window, testState, storage */ // Once this is loaded and parsed, begin execution loadInterface(); @@ -19,7 +20,7 @@ titleSpan.id = "test-title"; // Set title to that defined in XML, else set to default - if (titleAttr != undefined) { + if (titleAttr !== undefined) { titleSpan.textContent = titleAttr; } else { titleSpan.textContent = 'Listening test'; @@ -30,7 +31,8 @@ var pagetitle = document.createElement('div'); pagetitle.className = "pageTitle"; pagetitle.align = "center"; - var titleSpan = document.createElement('span'); + + titleSpan = document.createElement('span'); titleSpan.id = "pageTitle"; pagetitle.appendChild(titleSpan); @@ -112,7 +114,7 @@ // Load the full interface testState.initialise(); testState.advanceState(); -}; +} function loadTest(page) { // Called each time a new test page is to be build. The page specification node is the only item passed in @@ -128,13 +130,20 @@ // Set the page title if (typeof page.title == "string" && page.title.length > 0) { - document.getElementById("test-title").textContent = page.title + document.getElementById("test-title").textContent = page.title; } - if (interfaceObj.title != null) { + if (interfaceObj.title !== null) { document.getElementById("pageTitle").textContent = interfaceObj.title; } + if (interfaceObj.image !== undefined || page.audioElements.some(function (elem) { + return elem.image !== undefined; + })) { + document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider")); + interfaceContext.imageHolder.setImage(interfaceObj.image); + } + // Delete outside reference document.getElementById("outside-reference-holder").innerHTML = ""; @@ -142,17 +151,17 @@ sliderBox.innerHTML = ""; var commentBoxPrefix = "Comment on track"; - if (interfaceObj.commentBoxPrefix != undefined) { + if (interfaceObj.commentBoxPrefix !== undefined) { commentBoxPrefix = interfaceObj.commentBoxPrefix; } var loopPlayback = page.loop; - for (var option of interfaceObj.options) { + interfaceObj.options.forEach(function (option) { if (option.type == "show") { switch (option.name) { case "playhead": var playbackHolder = document.getElementById('playback-holder'); - if (playbackHolder == null) { + if (playbackHolder === null) { playbackHolder = document.createElement('div'); playbackHolder.style.width = "100%"; playbackHolder.align = 'center'; @@ -162,7 +171,7 @@ break; case "page-count": var pagecountHolder = document.getElementById('page-count'); - if (pagecountHolder == null) { + if (pagecountHolder === null) { pagecountHolder = document.createElement('div'); pagecountHolder.id = 'page-count'; } @@ -171,7 +180,7 @@ inject.appendChild(pagecountHolder); break; case "volume": - if (document.getElementById('master-volume-holder') == null) { + if (document.getElementById('master-volume-holder') === null) { feedbackHolder.appendChild(interfaceContext.volume.object); } break; @@ -180,7 +189,7 @@ break; } } - } + }); // Find all the audioElements from the audioHolder var index = 0; @@ -223,9 +232,9 @@ // An example node, you can make this however you want for each audioElement. // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following // You attach them by calling audioObject.bindInterface( ) - if (interfaceScales == null || interfaceScales.length == 0) { + if (interfaceScales === null || interfaceScales.length === 0) { console.log("WARNING: The discrete radio's are built depending on the number of scale points specified! Ensure you have some specified. Defaulting to 5 for now!"); - numOptions = 5; + var numOptions = 5; } this.parent = audioObject; @@ -246,6 +255,23 @@ this.discreteHolder.className = "track-slider-range"; this.discreteHolder.style.width = window.innerWidth - 500 + 'px'; + this.radioClicked = function (event) { + var time = audioEngineContext.timer.getTestTime(); + if (audioEngineContext.status === 0) { + event.currentTarget.checked = false; + return; + } + var id = this.parent.id; + var position = this.getValue(); + this.parent.metric.moved(time, position); + console.log('slider ' + id + ' moved to ' + position + ' (' + time + ')'); + + }; + this.handleEvent = function (event) { + if (event.currentTarget.getAttribute("name") === this.parent.specification.id) { + this.radioClicked(event); + } + }; for (var i = 0; i < interfaceScales.length; i++) { var node = document.createElement('input'); node.setAttribute('type', 'radio'); @@ -256,17 +282,7 @@ node.setAttribute('id', audioObject.specification.id + '-' + String(i)); this.discretes.push(node); this.discreteHolder.appendChild(node); - node.onclick = function (event) { - if (audioEngineContext.status == 0) { - event.currentTarget.checked = false; - return; - } - var time = audioEngineContext.timer.getTestTime(); - var id = Number(event.currentTarget.parentNode.parentNode.getAttribute('trackIndex')); - var value = event.currentTarget.getAttribute('position') / 100.0; - audioEngineContext.audioObjects[id].metric.moved(time, value); - console.log('slider ' + id + ' moved to ' + value + ' (' + time + ')'); - }; + node.addEventListener("click", this); } this.play.className = 'track-slider-button'; @@ -300,9 +316,9 @@ this.play.disabled = false; this.play.textContent = "Play"; $(this.slider).removeClass('track-slider-disabled'); - for (var radio of this.discretes) { - radio.disabled = false; - } + this.discretes.forEach(function (elem) { + elem.disabled = false; + }); }; this.updateLoading = function (progress) { // progress is a value from 0 to 100 indicating the current download state of media files @@ -322,14 +338,18 @@ $(this.holder).addClass('track-slider-playing'); var outsideReference = document.getElementById('outside-reference'); this.play.textContent = "Listening"; - if (outsideReference != null) { + if (outsideReference !== null) { $(outsideReference).removeClass('track-slider-playing'); } if (this.parent.specification.parent.playOne || specification.playOne) { $('.track-slider-button').text = "Wait"; $('.track-slider-button').attr("disabled", "true"); } - } + interfaceContext.commentBoxes.highlightById(audioObject.id); + if (audioObject.specification.image !== undefined) { + interfaceContext.imageHolder.setImage(audioObject.specification.image); + } + }; this.stopPlayback = function () { // Called by audioObject when playback stops if (this.play.getAttribute("playstate") == "playing") { @@ -338,19 +358,29 @@ $('.track-slider-button').text = "Play"; this.play.textContent = "Play"; $('.track-slider-button').removeAttr("disabled"); + var box = interfaceContext.commentBoxes.boxes.find(function (a) { + return a.id === audioObject.id; + }); + if (box) { + box.highlight(false); + } + if (audioObject.specification.parent.interfaces[0].image !== undefined) { + interfaceContext.imageHolder.setImage(audioObject.specification.parent.interfaces[0].image); + } else { + interfaceContext.imageHolder.setImage(""); + } } - } + }; this.getValue = function () { // Return the current value of the object. If there is no value, return -1 - var value = -1; - for (var i = 0; i < this.discretes.length; i++) { - if (this.discretes[i].checked == true) { - value = this.discretes[i].getAttribute('position') / 100.0; - break; - } + var checkedElement = this.discretes.find(function (elem) { + return elem.checked; + }); + if (checkedElement === undefined) { + return -1; } - return value; + return checkedElement.getAttribute("position") / 100.0; }; this.getPresentedId = function () { // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale @@ -374,8 +404,8 @@ // audioObject has an error!! this.playback.textContent = "Error"; $(this.playback).addClass("error-colour"); - } -}; + }; +} function resizeWindow(event) { // Called on every window resize event, use this to scale your page properly @@ -415,7 +445,7 @@ textHolder.innerHTML = ""; ctx.fillStyle = "#000000"; ctx.setLineDash([1, 4]); - for (var scale of scales) { + scales.forEach(function (scale) { var posPercent = scale.position / 100.0; var posPix = Math.round(width * posPercent); if (posPix <= 0) { @@ -437,7 +467,7 @@ textHolder.appendChild(text); text.style.width = $(text.children[0]).width() + 'px'; text.style.left = (posPix + 150 - ($(text).width() / 2)) + 'px'; - } + }); } function buttonSubmitClick() // TODO: Only when all songs have been played! @@ -446,57 +476,48 @@ canContinue = true; // Check that the anchor and reference objects are correctly placed - if (interfaceContext.checkHiddenAnchor() == false) { + if (interfaceContext.checkHiddenAnchor() === false) { return; } - if (interfaceContext.checkHiddenReference() == false) { + if (interfaceContext.checkHiddenReference() === false) { + return; + } + if (interfaceContext.checkFragmentMinPlays() === false) { return; } for (var i = 0; i < checks.length; i++) { + var checkState; if (checks[i].type == 'check') { switch (checks[i].name) { case 'fragmentPlayed': // Check if all fragments have been played - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); break; case 'fragmentFullPlayback': // Check all fragments have been played to their full length - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead'); break; case 'fragmentMoved': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllMoved(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllMoved(checks[i].errorMessage); break; case 'fragmentComments': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllCommented(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllCommented(checks[i].errorMessage); break; case 'scalerange': // Check the scale has been used effectively - var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkScaleRange(checks[i].errorMessage); break; default: console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface"); break; } - + if (checkState === false) { + canContinue = false; + } } if (!canContinue) { break; @@ -509,7 +530,7 @@ playback.click(); // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options } else { - if (audioEngineContext.timer.testStarted == false) { + if (audioEngineContext.timer.testStarted === false) { interfaceContext.lightbox.post("Warning", 'You have not started the test! Please press start to begin the test!'); return; } diff -r 56e72cd18404 -r 231d2186f3fc interfaces/horizontal-sliders.css --- a/interfaces/horizontal-sliders.css Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/horizontal-sliders.css Fri Jul 14 15:39:24 2017 +0100 @@ -76,7 +76,7 @@ margin: 0px 5px; } div.track-slider-playing { - background-color: #FFDDDD; + background-color: rgba(255, 201, 201, 0.5); } input.track-slider-range { float: left; @@ -115,3 +115,6 @@ top: 10px; left: 120px; } +div.comment-box-playing { + background-color: #FFDDDD; +} diff -r 56e72cd18404 -r 231d2186f3fc interfaces/horizontal-sliders.js --- a/interfaces/horizontal-sliders.js Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/horizontal-sliders.js Fri Jul 14 15:39:24 2017 +0100 @@ -1,4 +1,5 @@ // Once this is loaded and parsed, begin execution +/*globals interfaceContext, window, document, specification, audioEngineContext, console, testState, $, storage */ loadInterface(); function loadInterface() { @@ -19,7 +20,7 @@ titleSpan.id = "test-title"; // Set title to that defined in XML, else set to default - if (titleAttr != undefined) { + if (titleAttr !== undefined) { titleSpan.textContent = titleAttr; } else { titleSpan.textContent = 'Listening test'; @@ -30,7 +31,8 @@ var pagetitle = document.createElement('div'); pagetitle.className = "pageTitle"; pagetitle.align = "center"; - var titleSpan = document.createElement('span'); + + titleSpan = document.createElement('span'); titleSpan.id = "pageTitle"; pagetitle.appendChild(titleSpan); @@ -111,7 +113,7 @@ // Load the full interface testState.initialise(); testState.advanceState(); -}; +} function loadTest(page) { // Called each time a new test page is to be build. The page specification node is the only item passed in @@ -128,13 +130,20 @@ // Set the page title if (typeof page.title == "string" && page.title.length > 0) { - document.getElementById("test-title").textContent = page.title + document.getElementById("test-title").textContent = page.title; } - if (interfaceObj.title != null) { + if (interfaceObj.title !== null) { document.getElementById("pageTitle").textContent = interfaceObj.title; } + if (interfaceObj.image !== undefined || page.audioElements.some(function (elem) { + return elem.image !== undefined; + })) { + document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider")); + interfaceContext.imageHolder.setImage(interfaceObj.image); + } + // Delete outside reference document.getElementById("outside-reference-holder").innerHTML = ""; @@ -142,7 +151,7 @@ sliderBox.innerHTML = ""; var commentBoxPrefix = "Comment on track"; - if (interfaceObj.commentBoxPrefix != undefined) { + if (interfaceObj.commentBoxPrefix !== undefined) { commentBoxPrefix = interfaceObj.commentBoxPrefix; } var loopPlayback = page.loop; @@ -186,12 +195,12 @@ } }); - for (var option of interfaceObj.options) { + interfaceObj.options.forEach(function (option) { if (option.type == "show") { switch (option.name) { case "playhead": var playbackHolder = document.getElementById('playback-holder'); - if (playbackHolder == null) { + if (playbackHolder === null) { playbackHolder = document.createElement('div'); playbackHolder.style.width = "100%"; playbackHolder.align = 'center'; @@ -201,7 +210,7 @@ break; case "page-count": var pagecountHolder = document.getElementById('page-count'); - if (pagecountHolder == null) { + if (pagecountHolder === null) { pagecountHolder = document.createElement('div'); pagecountHolder.id = 'page-count'; } @@ -210,7 +219,7 @@ inject.appendChild(pagecountHolder); break; case "volume": - if (document.getElementById('master-volume-holder') == null) { + if (document.getElementById('master-volume-holder') === null) { feedbackHolder.appendChild(interfaceContext.volume.object); } break; @@ -219,7 +228,7 @@ break; } } - } + }); // Auto-align resizeWindow(null); } @@ -228,6 +237,7 @@ // An example node, you can make this however you want for each audioElement. // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following // You attach them by calling audioObject.bindInterface( ) + var playing = false; this.parent = audioObject; this.holder = document.createElement('div'); @@ -266,9 +276,9 @@ this.play.onclick = function (event) { var id = Number(event.currentTarget.value); //audioEngineContext.metric.sliderPlayed(id); - if (event.currentTarget.getAttribute("playstate") == "ready") { + if (!playing) { audioEngineContext.play(id); - } else if (event.currentTarget.getAttribute("playstate") == "playing") { + } else if (playing) { audioEngineContext.stop(); } }; @@ -287,18 +297,35 @@ }; this.startPlayback = function () { // Called when playback has begun - this.play.setAttribute("playstate", "playing"); + playing = true; + this.play.textContent = "Stop"; $(".track-slider").removeClass('track-slider-playing'); $(this.holder).addClass('track-slider-playing'); var outsideReference = document.getElementById('outside-reference'); - if (outsideReference != null) { + if (outsideReference !== null) { $(outsideReference).removeClass('track-slider-playing'); } + interfaceContext.commentBoxes.highlightById(audioObject.id); + if (audioObject.specification.image !== undefined) { + interfaceContext.imageHolder.setImage(audioObject.specification.image); + } }; this.stopPlayback = function () { // Called when playback has stopped. This gets called even if playback never started! - this.play.setAttribute("playstate", "ready"); + playing = false; + this.play.textContent = "Play"; $(this.holder).removeClass('track-slider-playing'); + var box = interfaceContext.commentBoxes.boxes.find(function (a) { + return a.id === audioObject.id; + }); + if (box) { + box.highlight(false); + } + if (audioObject.specification.parent.interfaces[0].image !== undefined) { + interfaceContext.imageHolder.setImage(audioObject.specification.parent.interfaces[0].image); + } else { + interfaceContext.imageHolder.setImage(""); + } }; this.getValue = function () { // Return the current value of the object. If there is no value, return 0 @@ -326,8 +353,8 @@ // audioObject has an error!! this.playback.textContent = "Error"; $(this.playback).addClass("error-colour"); - } -}; + }; +} function resizeWindow(event) { // Called on every window resize event, use this to scale your page properly @@ -367,7 +394,7 @@ textHolder.innerHTML = ""; ctx.fillStyle = "#000000"; ctx.setLineDash([1, 4]); - for (var scale of scales) { + scales.forEach(function (scale) { var posPercent = scale.position / 100.0; var posPix = Math.round(width * posPercent); if (posPix <= 0) { @@ -389,7 +416,7 @@ textHolder.appendChild(text); text.style.width = Math.ceil($(text).width()) + 'px'; text.style.left = (posPix + 100 - ($(text).width() / 2)) + 'px'; - } + }); } function buttonSubmitClick() // TODO: Only when all songs have been played! @@ -398,59 +425,49 @@ canContinue = true; // Check that the anchor and reference objects are correctly placed - if (interfaceContext.checkHiddenAnchor() == false) { + if (interfaceContext.checkHiddenAnchor() === false) { return; } - if (interfaceContext.checkHiddenReference() == false) { + if (interfaceContext.checkHiddenReference() === false) { + return; + } + if (interfaceContext.checkFragmentMinPlays() === false) { return; } for (var i = 0; i < checks.length; i++) { + var checkState = true; if (checks[i].type == 'check') { switch (checks[i].name) { case 'fragmentPlayed': // Check if all fragments have been played - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); break; case 'fragmentFullPlayback': // Check all fragments have been played to their full length - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead'); break; case 'fragmentMoved': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllMoved(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllMoved(checks[i].errorMessage); break; case 'fragmentComments': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllCommented(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllCommented(checks[i].errorMessage); break; case 'scalerange': // Check the scale has been used effectively - var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkScaleRange(checks[i].errorMessage); + break; default: console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface"); break; } - } - if (!canContinue) { + if (checkState === false) { + canContinue = false; break; } } @@ -461,7 +478,7 @@ playback.click(); // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options } else { - if (audioEngineContext.timer.testStarted == false) { + if (audioEngineContext.timer.testStarted === false) { interfaceContext.lightbox.post("Warning", 'You have not started the test! Please press start to begin the test!'); return; } diff -r 56e72cd18404 -r 231d2186f3fc interfaces/interfaces.json --- a/interfaces/interfaces.json Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/interfaces.json Fri Jul 14 15:39:24 2017 +0100 @@ -30,6 +30,10 @@ "name": "timeline", "scripts": ["interfaces/timeline.js"], "css": ["interfaces/timeline.css"] + }, { + "name": "ordinal", + "scripts": ["interfaces/ordinal.js"], + "css": ["interfaces/ordinal.css"] } ] } diff -r 56e72cd18404 -r 231d2186f3fc interfaces/mushra.css --- a/interfaces/mushra.css Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/mushra.css Fri Jul 14 15:39:24 2017 +0100 @@ -7,52 +7,43 @@ /* Set the background colour (note US English spelling) to grey*/ background-color: #ddd } - div.pageTitle { width: auto; height: 20px; margin: 10px 0px; } - div.pageTitle span { font-size: 1.5em; } - button { /* Specify any button structure or style */ min-width: 20px; background-color: #ddd } - div#slider-holder { height: inherit; position: absolute; left: 0px; z-index: 3; } - div#scale-holder { height: inherit; position: absolute; left: 0px; z-index: 2; } - div#scale-text-holder { position: relative; width: 100px; float: left; } - div.scale-text { position: absolute; } - canvas#scale-canvas { position: relative; float: left; } - div.track-slider { float: left; width: 94px; @@ -62,27 +53,22 @@ padding: 2px; margin-left: 50px; } - div#outside-reference-holder { display: flex; align-content: center; justify-content: center; margin-bottom: 5px; } - button.outside-reference { position: inherit; margin: 0px 5px; } - div.track-slider-playing { - background-color: #FFDDDD; + background-color: rgba(255, 201, 201, 0.5); } - input.track-slider-range { margin: 2px 0px; } - input[type=range][orient=vertical] { writing-mode: bt-lr; /* IE */ @@ -92,7 +78,6 @@ padding: 0 5px; color: rgb(255, 144, 144); } - input[type=range]::-webkit-slider-runnable-track { width: 8px; cursor: pointer; @@ -100,7 +85,6 @@ border-radius: 4px; border: 1px solid #000; } - input[type=range]::-moz-range-track { width: 8px; cursor: pointer; @@ -108,79 +92,63 @@ border-radius: 4px; border: 1px solid #000; } - input[type=range]::-ms-track { cursor: pointer; background: #fff; border-radius: 4px; border: 1px solid #000; } - input.track-slider-not-moved[type=range]::-webkit-slider-runnable-track { background: #aaa; } - input.track-slider-not-moved[type=range]::-moz-range-track { background: #aaa; } - input[type=range]::-moz-range-thumb { margin-left: -7px; cursor: pointer; margin-top: -1px; box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; } - input[type=range]::-webkit-slider-thumb { cursor: pointer; margin-top: -1px; margin-left: -4px; } - input[type=range]::-ms-thumb { cursor: pointer; margin-top: -1px; margin-left: -4px; } - input[type=range]::-ms-tooltip { visibility: hidden; } - input.track-slider-range-disabled {} - input.track-slider-range-disabled[type=range]::-webkit-slider-runnable-track { cursor: not-allowed; } - input.track-slider-range-disabled[type=range]::-moz-range-track { cursor: not-allowed; } - input.track-slider-range-disabled[type=range]::-ms-track { cursor: not-allowed; } - input.track-slider-range-disabled[type=range]::-moz-range-thumb { cursor: not-allowed; background-color: #888; } - input.track-slider-range-disabled[type=range]::-webkit-slider-thumb { cursor: not-allowed; background-color: #888; } - input.track-slider-range-disabled[type=range]::-ms-thumb { cursor: not-allowed; background-color: #888; } - div#page-count { float: left; margin: 0px 5px; } - div#master-volume-holder { position: absolute; top: 10px; diff -r 56e72cd18404 -r 231d2186f3fc interfaces/mushra.js --- a/interfaces/mushra.js Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/mushra.js Fri Jul 14 15:39:24 2017 +0100 @@ -2,7 +2,7 @@ * mushra.js * Create the MUSHRA interface */ - +/*globals window, interfaceContext, document, $, specification, audioEngineContext, console, testState, storage */ // Once this is loaded and parsed, begin execution loadInterface(); @@ -25,7 +25,7 @@ titleSpan.id = "test-title"; // Set title to that defined in XML, else set to default - if (titleAttr != undefined) { + if (titleAttr !== undefined) { titleSpan.textContent = titleAttr; } else { titleSpan.textContent = 'Listening test'; @@ -36,7 +36,8 @@ var pagetitle = document.createElement('div'); pagetitle.className = "pageTitle"; pagetitle.align = "center"; - var titleSpan = document.createElement('span'); + + titleSpan = document.createElement('span'); titleSpan.id = "pageTitle"; pagetitle.appendChild(titleSpan); @@ -49,7 +50,7 @@ var playback = document.createElement("button"); playback.innerHTML = 'Stop'; playback.id = 'playback-button'; - playback.style.float = 'left'; + playback.style.display = 'inline-block'; // onclick function. Check if it is playing or not, call the correct function in the // audioEngine, change the button text to reflect the next state. playback.onclick = function () { @@ -65,7 +66,7 @@ submit.innerHTML = 'Next'; submit.onclick = buttonSubmitClick; submit.id = 'submit-button'; - submit.style.float = 'left'; + submit.style.display = 'inline-block'; // Append the interface buttons into the interfaceButtons object. interfaceButtons.appendChild(playback); interfaceButtons.appendChild(submit); @@ -129,13 +130,20 @@ // Set the page title if (typeof audioHolderObject.title == "string" && audioHolderObject.title.length > 0) { - document.getElementById("test-title").textContent = audioHolderObject.title + document.getElementById("test-title").textContent = audioHolderObject.title; } - if (interfaceObj.title != null) { + if (interfaceObj.title !== null) { document.getElementById("pageTitle").textContent = interfaceObj.title; } + if (interfaceObj.image !== undefined || audioHolderObject.audioElements.some(function (elem) { + return elem.image !== undefined; + })) { + document.getElementById("testContent").insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider")); + interfaceContext.imageHolder.setImage(interfaceObj.image); + } + // Delete outside reference var outsideReferenceHolder = document.getElementById("outside-reference-holder"); outsideReferenceHolder.innerHTML = ""; @@ -144,12 +152,12 @@ sliderBox.innerHTML = ""; var commentBoxPrefix = "Comment on track"; - if (interfaceObj.commentBoxPrefix != undefined) { + if (interfaceObj.commentBoxPrefix !== undefined) { commentBoxPrefix = interfaceObj.commentBoxPrefix; } var loopPlayback = audioHolderObject.loop; - currentTestHolder = document.createElement('audioHolder'); + var currentTestHolder = document.createElement('audioHolder'); currentTestHolder.id = audioHolderObject.id; currentTestHolder.repeatCount = audioHolderObject.repeatCount; @@ -192,18 +200,18 @@ if (testState.currentStateMap.restrictMovement) { $(".track-slider-range").addClass("track-slider-range-disabled"); $(".track-slider-range").each(function (i, e) { - e.disabled = true + e.disabled = true; }); } var interfaceOptions = interfaceObj.options; - for (var option of interfaceOptions) { + interfaceOptions.forEach(function (option) { if (option.type == "show") { switch (option.name) { case "playhead": var playbackHolder = document.getElementById('playback-holder'); - if (playbackHolder == null) { + if (playbackHolder === null) { playbackHolder = document.createElement('div'); playbackHolder.style.width = "100%"; playbackHolder.align = 'center'; @@ -213,25 +221,53 @@ break; case "page-count": var pagecountHolder = document.getElementById('page-count'); - if (pagecountHolder == null) { + if (pagecountHolder === null) { pagecountHolder = document.createElement('div'); pagecountHolder.id = 'page-count'; + pagecountHolder.style.display = 'inline-block'; } pagecountHolder.innerHTML = 'Page ' + (testState.stateIndex + 1) + ' of ' + testState.stateMap.length + ''; var inject = document.getElementById('interface-buttons'); inject.appendChild(pagecountHolder); break; case "volume": - if (document.getElementById('master-volume-holder') == null) { + if (document.getElementById('master-volume-holder') === null) { feedbackHolder.appendChild(interfaceContext.volume.object); } break; case "comments": interfaceContext.commentBoxes.showCommentBoxes(feedbackHolder, true); break; + case "fragmentSort": + var button = document.getElementById('sort'); + if (button === null) { + button = document.createElement("button"); + button.id = 'sort'; + button.textContent = "Sort"; + button.style.display = 'inline-block'; + var container = document.getElementById("interface-buttons"); + var neighbour = container.lastElementChild; + container.appendChild(button); + button.onclick = function () { + var sortIndex = interfaceContext.sortFragmentsByScore(); + var sliderBox = document.getElementById("slider-holder"); + var nodes = audioEngineContext.audioObjects.filter(function (ao) { + return ao.specification.type !== "outside-reference"; + }); + var i; + nodes.forEach(function (ao) { + sliderBox.removeChild(ao.interfaceDOM.holder); + }); + for (i = 0; i < nodes.length; i++) { + var j = sortIndex[i]; + sliderBox.appendChild(nodes[j].interfaceDOM.holder); + } + }; + } + break; } } - } + }); $(audioHolderObject.commentQuestions).each(function (index, element) { var node = interfaceContext.createCommentQuestion(element); @@ -257,7 +293,7 @@ this.holder.appendChild(this.slider); this.holder.appendChild(this.play); this.holder.align = "center"; - if (label == 0) { + if (label === 0) { this.holder.style.marginLeft = '0px'; } this.holder.setAttribute('trackIndex', audioObject.id); @@ -315,8 +351,9 @@ this.play.setAttribute("playstate", "playing"); $(".track-slider").removeClass('track-slider-playing'); $(this.holder).addClass('track-slider-playing'); + interfaceContext.commentBoxes.highlightById(audioObject.id); var outsideReference = document.getElementById('outside-reference'); - if (outsideReference != null) { + if (outsideReference !== null) { $(outsideReference).removeClass('track-slider-playing'); } this.play.textContent = "Stop"; @@ -336,6 +373,9 @@ }); } } + if (audioObject.specification.image !== undefined) { + interfaceContext.imageHolder.setImage(audioObject.specification.image); + } }; this.stopPlayback = function () { // Called when playback has stopped. This gets called even if playback never started! @@ -346,14 +386,25 @@ $(this.slider).addClass("track-slider-range-disabled"); this.slider.setAttribute("disabled", "true"); } + var box = interfaceContext.commentBoxes.boxes.find(function (a) { + return a.id === audioObject.id; + }); + if (box) { + box.highlight(false); + } + if (audioObject.specification.parent.interfaces[0].image !== undefined) { + interfaceContext.imageHolder.setImage(audioObject.specification.parent.interfaces[0].image); + } else { + interfaceContext.imageHolder.setImage(""); + } }; this.getValue = function () { return this.slider.value; }; - this.resize = function (event) { - this.holder.style.height = window.innerHeight - 200 + 'px'; - this.slider.style.height = window.innerHeight - 250 + 'px'; + this.resize = function (event, height) { + this.holder.style.height = height - 20 + 'px'; + this.slider.style.height = height - 70 + 'px'; }; this.updateLoading = function (progress) { progress = String(progress); @@ -374,15 +425,21 @@ // audioObject has an error!! this.playback.textContent = "Error"; $(this.playback).addClass("error-colour"); - } + }; } function resizeWindow(event) { // Function called when the window has been resized. // MANDATORY FUNCTION - var outsideRef = document.getElementById('outside-reference'); - if (outsideRef != null) { + var outsideRef = document.getElementById('outside-reference'), + imageHeight = 0, + minHeight = Math.max(Math.floor(window.screen.height * 0.33), 200), + maxHeight = Math.floor(window.screen.height * 0.5); + if (document.getElementById("imageController")) { + imageHeight = $(interfaceContext.imageHolder.root).height(); + } + if (outsideRef !== null) { outsideRef.style.left = (window.innerWidth - 120) / 2 + 'px'; } @@ -390,18 +447,21 @@ var numObj = document.getElementsByClassName('track-slider').length; var totalWidth = (numObj - 1) * 150 + 100; var diff = (window.innerWidth - totalWidth) / 2; - document.getElementById('slider').style.height = window.innerHeight - 180 + 'px'; + var height = window.innerHeight - 180 - imageHeight; + height = Math.min(height, maxHeight); + height = Math.max(height, minHeight); + document.getElementById('slider').style.height = height + 'px'; if (diff <= 0) { diff = 0; } document.getElementById('slider-holder').style.marginLeft = diff + 'px'; for (var i in audioEngineContext.audioObjects) { if (audioEngineContext.audioObjects[i].specification.type != 'outside-reference') { - audioEngineContext.audioObjects[i].interfaceDOM.resize(event); + audioEngineContext.audioObjects[i].interfaceDOM.resize(event, height); } } document.getElementById('scale-holder').style.marginLeft = (diff - 100) + 'px'; - document.getElementById('scale-text-holder').style.height = window.innerHeight - 194 + 'px'; + document.getElementById('scale-text-holder').style.height = height - 14 + 'px'; // Cheers edge for making me delete a canvas every resize. var canvas = document.getElementById('scale-canvas'); var new_canvas = document.createElement("canvas"); @@ -409,7 +469,7 @@ canvas.parentElement.appendChild(new_canvas); canvas.parentElement.removeChild(canvas); new_canvas.width = totalWidth; - new_canvas.height = window.innerHeight - 194; + new_canvas.height = height - 14; drawScale(); } @@ -428,7 +488,7 @@ var textHolder = document.getElementById('scale-text-holder'); textHolder.innerHTML = ""; var lastHeight = 0; - for (var scale of scales) { + scales.forEach(function (scale) { var posPercent = scale.position / 100.0; var posPix = (1 - posPercent) * (draw_heights[1] - draw_heights[0]) + draw_heights[0]; ctx.fillStyle = "#000000"; @@ -446,7 +506,7 @@ text.style.top = (posPix - 9) + 'px'; text.style.left = 100 - ($(text).width() + 3) + 'px'; lastHeight = posPix; - } + }); } function buttonSubmitClick() // TODO: Only when all songs have been played! @@ -455,51 +515,40 @@ canContinue = true; // Check that the anchor and reference objects are correctly placed - if (interfaceContext.checkHiddenAnchor() == false) { + if (interfaceContext.checkHiddenAnchor() === false) { return; } - if (interfaceContext.checkHiddenReference() == false) { + if (interfaceContext.checkHiddenReference() === false) { + return; + } + if (interfaceContext.checkFragmentMinPlays() === false) { return; } for (var i = 0; i < checks.length; i++) { + var checkState = true; if (checks[i].type == 'check') { switch (checks[i].name) { case 'fragmentPlayed': // Check if all fragments have been played - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); break; case 'fragmentFullPlayback': // Check all fragments have been played to their full length - var checkState = interfaceContext.checkAllPlayed(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead'); break; case 'fragmentMoved': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllMoved(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllMoved(checks[i].errorMessage); break; case 'fragmentComments': // Check all fragment sliders have been moved. - var checkState = interfaceContext.checkAllCommented(); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkAllCommented(checks[i].errorMessage); break; case 'scalerange': // Check the scale has been used effectively - var checkState = interfaceContext.checkScaleRange(checks[i].min, checks[i].max); - if (checkState == false) { - canContinue = false; - } + checkState = interfaceContext.checkScaleRange(checks[i].errorMessage); break; default: console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface"); @@ -507,7 +556,8 @@ } } - if (!canContinue) { + if (checkState === false) { + canContinue = false; break; } } @@ -518,7 +568,7 @@ playback.click(); // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options } else { - if (audioEngineContext.timer.testStarted == false) { + if (audioEngineContext.timer.testStarted === false) { interfaceContext.lightbox.post("Message", 'You have not started the test! Please press start to begin the test!'); return; } diff -r 56e72cd18404 -r 231d2186f3fc interfaces/ordinal.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/interfaces/ordinal.css Fri Jul 14 15:39:24 2017 +0100 @@ -0,0 +1,36 @@ +[draggable] { + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + user-select: none; + /* Required to make elements draggable in old WebKit */ + -khtml-user-drag: element; + -webkit-user-drag: element; +} +.ordinal-element { + width: 250px; + height: 250px; + background: #bbffbb; + border: 2px #050 solid; + border-radius: 10px; + float: left; + margin: 10px 5px; + text-align: center; + cursor: move; +} +.disabled { + background-color: grey; +} +.playing { + background-color: #ffbbbb; + border: 2px #500 solid; +} +.dragging { + opacity: 0.4; +} +.over { + border-style: dashed; +} +.ordinal-element-label { + font-size: 2em; +} diff -r 56e72cd18404 -r 231d2186f3fc interfaces/ordinal.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/interfaces/ordinal.js Fri Jul 14 15:39:24 2017 +0100 @@ -0,0 +1,487 @@ +/** + * WAET Blank Template + * Use this to start building your custom interface + */ +/*globals interfaceContext, window, document, specification, audioEngineContext, console, testState, $, storage */ +// Once this is loaded and parsed, begin execution +loadInterface(); + +function loadInterface() { + // Use this to do any one-time page / element construction. For instance, placing any stationary text objects, + // holding div's, or setting up any nodes which are present for the entire test sequence + + // The injection point into the HTML page + interfaceContext.insertPoint = document.getElementById("topLevelBody"); + var testContent = document.createElement('div'); + testContent.id = 'testContent'; + + // Create the top div for the Title element + var titleAttr = specification.title; + var title = document.createElement('div'); + title.className = "title"; + title.align = "center"; + var titleSpan = document.createElement('span'); + titleSpan.id = "test-title"; + + // Set title to that defined in XML, else set to default + if (titleAttr !== undefined) { + titleSpan.textContent = titleAttr; + } else { + titleSpan.textContent = 'Listening test'; + } + // Insert the titleSpan element into the title div element. + title.appendChild(titleSpan); + + var pagetitle = document.createElement('div'); + pagetitle.className = "pageTitle"; + pagetitle.align = "center"; + + titleSpan = document.createElement('span'); + titleSpan.id = "pageTitle"; + pagetitle.appendChild(titleSpan); + + // Create Interface buttons! + var interfaceButtons = document.createElement('div'); + interfaceButtons.id = 'interface-buttons'; + interfaceButtons.style.height = '25px'; + + // Create playback start/stop points + var playback = document.createElement("button"); + playback.innerHTML = 'Stop'; + playback.id = 'playback-button'; + playback.style.float = 'left'; + // onclick function. Check if it is playing or not, call the correct function in the + // audioEngine, change the button text to reflect the next state. + playback.onclick = function () { + if (audioEngineContext.status == 1) { + audioEngineContext.stop(); + this.innerHTML = 'Stop'; + var time = audioEngineContext.timer.getTestTime(); + console.log('Stopped at ' + time); // DEBUG/SAFETY + } + }; + // Create Submit (save) button + var submit = document.createElement("button"); + submit.innerHTML = 'Next'; + submit.onclick = buttonSubmitClick; + submit.id = 'submit-button'; + submit.style.float = 'left'; + // Append the interface buttons into the interfaceButtons object. + interfaceButtons.appendChild(playback); + interfaceButtons.appendChild(submit); + + // Create outside reference holder + var outsideRef = document.createElement("div"); + outsideRef.id = "outside-reference-holder"; + + // Create a slider box + var slider = document.createElement("div"); + slider.id = "slider"; + slider.style.height = "300px"; + + // Global parent for the comment boxes on the page + var feedbackHolder = document.createElement('div'); + feedbackHolder.id = 'feedbackHolder'; + + testContent.style.zIndex = 1; + interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema + + // Inject into HTML + testContent.appendChild(title); // Insert the title + testContent.appendChild(pagetitle); + testContent.appendChild(interfaceButtons); + testContent.appendChild(outsideRef); + testContent.appendChild(slider); + testContent.appendChild(feedbackHolder); + interfaceContext.insertPoint.appendChild(testContent); + + // Load the full interface + testState.initialise(); + testState.advanceState(); +} + +function loadTest(page) { + // Called each time a new test page is to be build. The page specification node is the only item passed in + var id = page.id; + + var feedbackHolder = document.getElementById('feedbackHolder'); + feedbackHolder.innerHTML = ""; + + var interfaceObj = interfaceContext.getCombinedInterfaces(page); + if (interfaceObj.length > 1) { + console.log("WARNING - This interface only supports one node per page. Using first interface node"); + } + interfaceObj = interfaceObj[0]; + + // Set the page title + if (typeof page.title == "string" && page.title.length > 0) { + document.getElementById("test-title").textContent = page.title; + } + + if (interfaceObj.title !== null) { + document.getElementById("pageTitle").textContent = interfaceObj.title; + } + + if (interfaceObj.image !== undefined) { + feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider")); + interfaceContext.imageHolder.setImage(interfaceObj.image); + } + // Delete outside reference + document.getElementById("outside-reference-holder").innerHTML = ""; + + var sliderBox = document.getElementById('slider'); + sliderBox.innerHTML = ""; + + var commentBoxPrefix = "Comment on track"; + if (interfaceObj.commentBoxPrefix !== undefined) { + commentBoxPrefix = interfaceObj.commentBoxPrefix; + } + + $(page.commentQuestions).each(function (index, element) { + var node = interfaceContext.createCommentQuestion(element); + feedbackHolder.appendChild(node.holder); + }); + + var index = 0; + var labelType = page.label; + if (labelType == "default") { + labelType = "number"; + } + page.audioElements.forEach(function (element, pageIndex) { + var audioObject = audioEngineContext.newTrack(element); + if (element.type == 'outside-reference') { + // Construct outside reference; + var orNode = new interfaceContext.outsideReferenceDOM(audioObject, index, document.getElementById("outside-reference-holder")); + audioObject.bindInterface(orNode); + } else { + // Create a slider per track + var label = interfaceContext.getLabel(labelType, index, page.labelStart); + var sliderObj = new interfaceObject(audioObject, label); + + sliderBox.appendChild(sliderObj.root); + audioObject.bindInterface(sliderObj); + interfaceContext.commentBoxes.createCommentBox(audioObject); + index += 1; + } + }); + interfaceObj.options.forEach(function (option) { + if (option.type == "show") { + switch (option.name) { + case "playhead": + var playbackHolder = document.getElementById('playback-holder'); + if (playbackHolder === null) { + playbackHolder = document.createElement('div'); + playbackHolder.style.width = "100%"; + playbackHolder.align = 'center'; + playbackHolder.appendChild(interfaceContext.playhead.object); + feedbackHolder.appendChild(playbackHolder); + } + break; + case "page-count": + var pagecountHolder = document.getElementById('page-count'); + if (pagecountHolder === null) { + pagecountHolder = document.createElement('div'); + pagecountHolder.id = 'page-count'; + } + pagecountHolder.innerHTML = 'Page ' + (testState.stateIndex + 1) + ' of ' + testState.stateMap.length + ''; + var inject = document.getElementById('interface-buttons'); + inject.appendChild(pagecountHolder); + break; + case "volume": + if (document.getElementById('master-volume-holder') === null) { + feedbackHolder.appendChild(interfaceContext.volume.object); + } + break; + case "comments": + interfaceContext.commentBoxes.showCommentBoxes(feedbackHolder, true); + break; + } + } + }); + resizeWindow(); +} + +function interfaceObject(audioObject, label) { + var container = document.getElementById("slider"); + var playing = false; + var root = document.createElement("div"); + root.className = "ordinal-element"; + root.draggable = "true"; + var labelElement = document.createElement("span"); + labelElement.className = "ordinal-element-label"; + labelElement.textContent = label; + root.appendChild(labelElement); + root.classList.add("disabled"); + // An example node, you can make this however you want for each audioElement. + // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following + // You attach them by calling audioObject.bindInterface( ) + root.addEventListener("click", this, true); + root.addEventListener('dragstart', this, true); + root.addEventListener('dragenter', this, true); + root.addEventListener('dragover', this, true); + root.addEventListener('dragleave', this, true); + root.addEventListener('drop', this, true); + root.addEventListener('dragend', this, true); + this.handleEvent = function (event) { + if (event.type == "click") { + if (playing === false) { + audioEngineContext.play(audioObject.id); + } else { + audioEngineContext.stop(); + } + playing = !playing; + return; + } else if (event.type == "dragstart") { + return dragStart.call(this, event); + } else if (event.type == "dragenter") { + return dragEnter.call(this, event); + } else if (event.type == "dragleave") { + return dragLeave.call(this, event); + } else if (event.type == "dragover") { + return dragOver.call(this, event); + } else if (event.type == "drop") { + return drop.call(this, event); + } else if (event.type == "dragend") { + return dragEnd.call(this, event); + } + throw (event); + }; + + function dragStart(e) { + e.currentTarget.classList.add("dragging"); + + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', audioObject.id); + } + + function dragEnter(e) { + // this / e.target is the current hover target. + root.classList.add('over'); + } + + function dragLeave(e) { + root.classList.remove('over'); // this / e.target is previous target element. + } + + function dragOver(e) { + if (e.preventDefault) { + e.preventDefault(); // Necessary. Allows us to drop. + } + + e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object. + + var srcid = Number(e.dataTransfer.getData("text/plain")); + var elements = container.childNodes; + var srcObject = audioEngineContext.audioObjects.find(function (ao) { + return ao.id === srcid; + }); + var src = srcObject.interfaceDOM.root; + if (src !== root) { + var srcpos = srcObject.interfaceDOM.getElementPosition(); + var mypos = this.getElementPosition(); + var neighbour; + if (srcpos <= mypos) { + neighbour = root.nextElementSibling; + } else { + neighbour = root; + } + if (neighbour) + container.insertBefore(src, neighbour); + else { + container.removeChild(src); + container.appendChild(src); + } + + } + + return false; + } + + function drop(e) { + // this / e.target is current target element. + + if (e.stopPropagation) { + e.stopPropagation(); // stops the browser from redirecting. + } + if (e.preventDefault) { + e.preventDefault(); // Necessary. Allows us to drop. + } + + audioEngineContext.audioObjects.forEach(function (ao) { + ao.interfaceDOM.processMovement(); + }); + + return false; + } + + function dragEnd(e) { + // this/e.target is the source node. + $(".ordinal-element").removeClass("dragging"); + $(".ordinal-element").removeClass("over"); + } + + this.getElementPosition = function () { + var elements = container.childNodes, + position = 0, + elem = elements[0]; + while (root !== elem) { + position++; + elem = elem.nextElementSibling; + } + return position; + }; + + this.processMovement = function () { + var time = audioEngineContext.timer.getTestTime(); + var pos = this.getElementPosition(); + var rank = pos / (audioEngineContext.audioObjects.length - 1); + audioObject.metric.moved(time, rank); + console.log('slider ' + audioObject.id + ' moved to ' + rank + ' (' + time + ')'); + }; + + this.enable = function () { + // This is used to tell the interface object that playback of this node is ready + root.classList.remove("disabled"); + labelElement.textContent = label; + }; + this.updateLoading = function (progress) { + // progress is a value from 0 to 100 indicating the current download state of media files + labelElement.textContent = String(progress); + }; + this.startPlayback = function () { + // Called when playback has begun + root.classList.add("playing"); + if (audioObject.commentDOM) { + audioObject.commentDOM.trackComment.classList.add("comment-box-playing"); + } + }; + this.stopPlayback = function () { + // Called when playback has stopped. This gets called even if playback never started! + root.classList.remove("playing"); + playing = false; + if (audioObject.commentDOM) { + audioObject.commentDOM.trackComment.classList.remove("comment-box-playing"); + } + }; + this.getValue = function () { + // Return the current value of the object. If there is no value, return 0 + var pos = this.getElementPosition(); + var rank = pos / (audioEngineContext.audioObjects.length - 1); + }; + this.getPresentedId = function () { + // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale + return label; + }; + this.canMove = function () { + // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale. + // These are checked primarily if the interface check option 'fragmentMoved' is enabled. + return true; + }; + this.exportXMLDOM = function (audioObject) { + // Called by the audioObject holding this element to export the interface node. + // If there is no value node (such as outside reference), return null + // If there are multiple value nodes (such as multiple scale / 2D scales), return an array of nodes with each value node having an 'interfaceName' attribute + // Use storage.document.createElement('value'); to generate the XML node. + var node = storage.document.createElement('value'); + node.textContent = this.getValue(); + return node; + + }; + this.error = function () { + // If there is an error with the audioObject, this will be called to indicate a failure + root.classList.remove("disabled"); + labelElement.textContent = "Error"; + }; + Object.defineProperties(this, { + "root": { + "get": function () { + return root; + }, + "set": function () {} + } + }); +} + +function resizeWindow(event) { + // Called on every window resize event, use this to scale your page properly + var w = $("#slider").width(); + var N = audioEngineContext.audioObjects.length; + w /= N; + w -= 14; + w = Math.floor(w); + $(".ordinal-element").width(w); +} + +function buttonSubmitClick() // TODO: Only when all songs have been played! +{ + var checks = testState.currentStateMap.interfaces[0].options, + canContinue = true; + + // Check that the anchor and reference objects are correctly placed + if (interfaceContext.checkHiddenAnchor() === false) { + return; + } + if (interfaceContext.checkHiddenReference() === false) { + return; + } + + for (var i = 0; i < checks.length; i++) { + var checkState = true; + if (checks[i].type == 'check') { + switch (checks[i].name) { + case 'fragmentPlayed': + // Check if all fragments have been played + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); + break; + case 'fragmentFullPlayback': + // Check all fragments have been played to their full length + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); + console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead'); + break; + case 'fragmentMoved': + // Check all fragment sliders have been moved. + checkState = interfaceContext.checkAllMoved(checks[i].errorMessage); + break; + case 'fragmentComments': + // Check all fragment sliders have been moved. + checkState = interfaceContext.checkAllCommented(checks[i].errorMessage); + break; + case 'scalerange': + // Check the scale has been used effectively + checkState = interfaceContext.checkScaleRange(checks[i].errorMessage); + + break; + default: + console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface"); + break; + } + } + if (checkState === false) { + canContinue = false; + break; + } + } + + if (canContinue) { + if (audioEngineContext.status == 1) { + var playback = document.getElementById('playback-button'); + playback.click(); + // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options + } else { + if (audioEngineContext.timer.testStarted === false) { + interfaceContext.lightbox.post("Warning", 'You have not started the test! Please press start to begin the test!'); + return; + } + } + testState.advanceState(); + } +} + +function pageXMLSave(store, pageSpecification) { + // MANDATORY + // Saves a specific test page + // You can use this space to add any extra nodes to your XML saves + // Get the current information in store (remember to appendChild your data to it) + // pageSpecification is the current page node configuration + // To create new XML nodes, use storage.document.createElement(); +} diff -r 56e72cd18404 -r 231d2186f3fc interfaces/timeline.css --- a/interfaces/timeline.css Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/timeline.css Fri Jul 14 15:39:24 2017 +0100 @@ -3,17 +3,16 @@ justify-content: center; } div.timeline-element-content { - max-width: 800px; + width: 90%; min-width: 200px; border: 1px solid black; margin: 10px 0px; padding: 20px; } div.timeline-element-canvas-holder { - display: flex; width: inherit; height: 160px; - margin-left: 50px; + margin: auto; } canvas.canvas-layer1 { position: absolute; @@ -36,10 +35,12 @@ canvas.canvas-disabled { background-color: gray; } -div.timeline-element-comment-holder { - display: flex; - flex-wrap: wrap; - justify-content: space-between; +div.timeline-element-comment-holder {} +img.timeline-element-image { + height: 150px; + max-width: 20%; + display: inline-block; + float: right; } div.comment-entry { border: 1px solid #444444; @@ -49,14 +50,22 @@ padding: 5px; height: 80px; border-radius: 10px; - display: flex; - flex-direction: column; + display: inline-block; + text-align: center; } div.comment-entry-header { - display: flex; - justify-content: space-between; + display: table; + text-align: unset; + width: 100%; +} +div.comment-entry-header span { + float: left; +} +div.comment-entry-header button { + float: right; } textarea.comment-entry-text { resize: none; margin: auto; + width: 90%; } diff -r 56e72cd18404 -r 231d2186f3fc interfaces/timeline.js --- a/interfaces/timeline.js Fri Jul 14 15:37:53 2017 +0100 +++ b/interfaces/timeline.js Fri Jul 14 15:39:24 2017 +0100 @@ -2,7 +2,7 @@ * WAET Timeline * This interface plots a waveform timeline per audio fragment on a page. Clicking on the fragment will generate a comment box for processing. */ - +/*globals interfaceContext, window, document, console, audioEngineContext, testState, $, storage */ // Once this is loaded and parsed, begin execution loadInterface(); @@ -77,7 +77,7 @@ // Load the full interface testState.initialise(); testState.advanceState(); -}; +} function loadTest(page) { // Called each time a new test page is to be build. The page specification node is the only item passed in @@ -94,16 +94,21 @@ document.getElementById("test-title").textContent = page.title; } - if (interfaceObj.title != null) { + if (interfaceObj.title !== null) { document.getElementById("page-title").textContent = interfaceObj.title; } + if (interfaceObj.image !== undefined) { + document.getElementById("timeline-test-content").parentElement.insertBefore(interfaceContext.imageHolder.root, document.getElementById("timeline-test-content")); + interfaceContext.imageHolder.setImage(interfaceObj.image); + } + // Delete outside reference var outsideReferenceHolder = document.getElementById("outside-reference-holder"); outsideReferenceHolder.innerHTML = ""; var commentBoxPrefix = "Comment on track"; - if (interfaceObj.commentBoxPrefix != undefined) { + if (interfaceObj.commentBoxPrefix !== undefined) { commentBoxPrefix = interfaceObj.commentBoxPrefix; } var index = 0; @@ -116,7 +121,7 @@ var audioObject = audioEngineContext.newTrack(element); if (page.audioElements.type == 'outside-reference') { var refNode = interfaceContext.outsideReferenceDOM(audioObject, index, outsideReferenceHolder); - audioObject.bindInterface(orNode); + audioObject.bindInterface(refNode); } else { var label = interfaceContext.getLabel(labelType, index, page.labelStart); var node = new interfaceObject(audioObject, label); @@ -169,7 +174,7 @@ var titleHolder = document.createElement("div"); titleHolder.className = "comment-entry-header"; this.title = document.createElement("span"); - if (str != undefined) { + if (str !== undefined) { this.title.textContent = str; } else { this.title.textContent = "Time: " + time.toFixed(2) + "s"; @@ -186,7 +191,7 @@ handleEvent: function () { this.parent.parent.deleteComment(this.parent); } - } + }; this.clear.DOM.textContent = "Delete"; this.clear.DOM.addEventListener("click", this.clear); titleHolder.appendChild(this.clear.DOM); @@ -199,7 +204,7 @@ elem_w = Math.max(elem_w, 190); this.DOM.style.width = elem_w + "px"; this.textarea.style.width = (elem_w - 5) + "px"; - } + }; this.buildXML = function (root) { //storage.document.createElement(); var node = storage.document.createElement("comment"); @@ -211,7 +216,7 @@ node.appendChild(question); node.appendChild(comment); root.appendChild(node); - } + }; this.resize(); }, newComment: function (time) { @@ -240,7 +245,7 @@ this.deleteComment(this.list[0]); } } - } + }; this.canvas = { parent: this, @@ -249,7 +254,8 @@ layer2: document.createElement("canvas"), layer3: document.createElement("canvas"), layer4: document.createElement("canvas"), - resize: function (w) { + resize: function () { + var w = $(this.layer1.parentElement).width(); this.layer1.width = w; this.layer2.width = w; this.layer3.width = w; @@ -283,7 +289,7 @@ } }, drawWaveform: function () { - if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) { + if (this.parent.parent === undefined || this.parent.parent.buffer === undefined) { return; } var buffer = this.parent.parent.buffer.buffer; @@ -342,7 +348,7 @@ context.stroke(); }, drawMarkers: function () { - if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) { + if (this.parent.parent === undefined || this.parent.parent.buffer === undefined) { return; } var context = this.layer3.getContext("2d"); @@ -362,23 +368,35 @@ var context = canvas.getContext("2d"); context.clearRect(0, 0, canvas.width, canvas.height); } - } + }; this.canvas.layer1.className = "timeline-element-canvas canvas-layer1 canvas-disabled"; this.canvas.layer2.className = "timeline-element-canvas canvas-layer2"; this.canvas.layer3.className = "timeline-element-canvas canvas-layer3"; this.canvas.layer4.className = "timeline-element-canvas canvas-layer3"; - this.canvas.layer1.height = "150"; - this.canvas.layer2.height = "150"; - this.canvas.layer3.height = "150"; - this.canvas.layer4.height = "150"; - canvasHolder.appendChild(this.canvas.layer1); - canvasHolder.appendChild(this.canvas.layer2); - canvasHolder.appendChild(this.canvas.layer3); - canvasHolder.appendChild(this.canvas.layer4); + this.canvas.layer1.height = "160"; + this.canvas.layer2.height = "160"; + this.canvas.layer3.height = "160"; + this.canvas.layer4.height = "160"; + var canvasDiv = document.createElement("div"); + canvasDiv.appendChild(this.canvas.layer1); + canvasDiv.appendChild(this.canvas.layer2); + canvasDiv.appendChild(this.canvas.layer3); + canvasDiv.appendChild(this.canvas.layer4); + canvasHolder.appendChild(canvasDiv); this.canvas.layer1.addEventListener("mousemove", this.canvas); this.canvas.layer1.addEventListener("mouseleave", this.canvas); this.canvas.layer1.addEventListener("click", this.canvas); + if (audioObject.specification.image) { + canvasDiv.style.width = "80%"; + var image = document.createElement("img"); + image.src = audioObject.specification.image; + image.className = "timeline-element-image"; + canvasHolder.appendChild(image); + } else { + canvasDiv.style.width = "100%"; + } + var canvasIntervalID = null; this.playButton = { @@ -393,7 +411,7 @@ audioEngineContext.stop(); } } - } + }; this.playButton.DOM.addEventListener("click", this.playButton); this.playButton.DOM.className = "timeline-button timeline-button-disabled"; this.playButton.DOM.disabled = true; @@ -402,13 +420,8 @@ buttonHolder.appendChild(this.playButton.DOM); this.resize = function () { - var w = window.innerWidth; - w = Math.min(w, 800); - w = Math.max(w, 200); - root.style.width = w + "px"; - var c_w = w - 100; - this.canvas.resize(c_w); - } + this.canvas.resize(); + }; this.enable = function () { // This is used to tell the interface object that playback of this node is ready @@ -428,14 +441,27 @@ }; this.startPlayback = function () { // Called when playback has begun - canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas), 100); + var animate = function () { + this.canvas.drawTicker.call(this.canvas); + if (this.playButton.DOM.textContent == "Stop") { + window.requestAnimationFrame(animate); + } + }.bind(this); this.playButton.DOM.textContent = "Stop"; + interfaceContext.commentBoxes.highlightById(audioObject.id); + canvasIntervalID = window.requestAnimationFrame(animate); }; this.stopPlayback = function () { // Called when playback has stopped. This gets called even if playback never started! window.clearInterval(canvasIntervalID); this.canvas.clearCanvas(this.canvas.layer2); this.playButton.DOM.textContent = "Play"; + var box = interfaceContext.commentBoxes.boxes.find(function (a) { + return a.id === audioObject.id; + }); + if (box) { + box.highlight(false); + } }; this.getValue = function () { // Return the current value of the object. If there is no value, return 0 @@ -459,8 +485,8 @@ }; this.error = function () { // If there is an error with the audioObject, this will be called to indicate a failure - } -}; + }; +} function resizeWindow(event) { // Called on every window resize event, use this to scale your page properly @@ -470,32 +496,35 @@ } function buttonSubmitClick() { - if (audioEngineContext.timer.testStarted == false) { + if (audioEngineContext.timer.testStarted === false) { interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!'); return; } - var checks = testState.currentStateMap.interfaces[0], + var checks = testState.currentStateMap.interfaces[0].options, canContinue = true; + if (interfaceContext.checkFragmentMinPlays() === false) { + return; + } for (var i = 0; i < checks.length; i++) { var checkState = true; if (checks[i].type == 'check') { switch (checks[i].name) { case 'fragmentPlayed': //Check if all fragments have been played - checkState = interfaceContext.checkAllPlayed(); + checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); break; case 'fragmentFullPlayback': //Check if all fragments have played to their full length - checkState = interfaceContext.checkFragmentsFullyPlayed(); + checkState = interfaceContext.checkFragmentsFullyPlayed(checks[i].errorMessage); break; case 'fragmentComments': - checkState = interfaceContext.checkAllCommented(); + checkState = interfaceContext.checkAllCommented(checks[i].errorMessage); break; default: console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface"); break; } - if (checkState == false) { + if (checkState === false) { canContinue = false; } } diff -r 56e72cd18404 -r 231d2186f3fc js/WAVE.js --- a/js/WAVE.js Fri Jul 14 15:37:53 2017 +0100 +++ b/js/WAVE.js Fri Jul 14 15:39:24 2017 +0100 @@ -1,6 +1,7 @@ // Decode and perform WAVE file byte level manipulation +/*globals console, Uint8Array, Float32Array, Float64Array */ -find_subarray = function (arr, subarr) { +function find_subarray(arr, subarr) { var arr_length = arr.length; var subarr_length = subarr.length; var last_check_index = arr_length - subarr_length; @@ -15,7 +16,7 @@ return i; } return -1; -}; +} function convertToInteger(arr) { var value = 0; @@ -35,24 +36,24 @@ function WAVE() { // The WAVE file object - this.status == 'WAVE_DECLARED' + this.status = 'WAVE_DECLARED'; this.decoded_data = null; this.RIFF = String(); //ChunkID - this.size; //ChunkSize - this.FT_Header; //Format - this.fmt_marker; //Subchunk1ID - this.formatDataLength; //Subchunk1Size - this.type; //AudioFormat - this.num_channels; //NumChannels - this.sample_rate; //SampleRate - this.byte_rate; //ByteRate - this.block_align; //BlockAlign - this.bits_per_sample; //BitsPerSample - this.data_header; //Subchunk2ID - this.data_size; //Subchunk2Size - this.num_samples; + this.size = undefined; //ChunkSize + this.FT_Header = undefined; //Format + this.fmt_marker = undefined; //Subchunk1ID + this.formatDataLength = undefined; //Subchunk1Size + this.type = undefined; //AudioFormat + this.num_channels = undefined; //NumChannels + this.sample_rate = undefined; //SampleRate + this.byte_rate = undefined; //ByteRate + this.block_align = undefined; //BlockAlign + this.bits_per_sample = undefined; //BitsPerSample + this.data_header = undefined; //Subchunk2ID + this.data_size = undefined; //Subchunk2Size + this.num_samples = undefined; this.open = function (IOArrayBuffer) { var IOView8 = new Uint8Array(IOArrayBuffer); diff -r 56e72cd18404 -r 231d2186f3fc js/angular.min.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/js/angular.min.js Fri Jul 14 15:39:24 2017 +0100 @@ -0,0 +1,332 @@ +/* + AngularJS v1.6.4 + (c) 2010-2017 Google, Inc. http://angularjs.org + License: MIT +*/ +(function(x){'use strict';function L(a,b){b=b||Error;return function(){var d=arguments[0],c;c="["+(a?a+":":"")+d+"] http://errors.angularjs.org/1.6.4/"+(a?a+"/":"")+d;for(d=1;dc)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f").append(a).html();try{return a[0].nodeType===Ia?Q(d):d.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+Q(b)})}catch(c){return Q(d)}}function Qc(a){try{return decodeURIComponent(a)}catch(b){}}function Rc(a){var b={};q((a||"").split("&"),function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Qc(e),u(e)&&(f= +u(f)?Qc(f):!0,ua.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Zb(a){var b=[];q(a,function(a,c){H(a)?q(a,function(a){b.push($(c,!0)+(!0===a?"":"="+$(a,!0)))}):b.push($(c,!0)+(!0===a?"":"="+$(a,!0)))});return b.length?b.join("&"):""}function db(a){return $(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function $(a,b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g, +b?"%20":"+")}function te(a,b){var d,c,e=Ja.length;for(c=0;c protocol indicates an extension, document.location.href does not match."))} +function Sc(a,b,d){C(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=B(a);if(a.injector()){var c=a[0]===x.document?"document":xa(a);throw Fa("btstrpd",c.replace(//,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider",function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=eb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector", +d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;x&&e.test(x.name)&&(d.debugInfoEnabled=!0,x.name=x.name.replace(e,""));if(x&&!f.test(x.name))return c();x.name=x.name.replace(f,"");ea.resumeBootstrap=function(a){q(a,function(a){b.push(a)});return c()};D(ea.resumeDeferredBootstrap)&&ea.resumeDeferredBootstrap()}function we(){x.name="NG_ENABLE_DEBUG_INFO!"+x.name;x.location.reload()}function xe(a){a=ea.element(a).injector();if(!a)throw Fa("test");return a.get("$$testability")} +function Tc(a,b){b=b||"_";return a.replace(ye,function(a,c){return(c?b:"")+a.toLowerCase()})}function ze(){var a;if(!Uc){var b=rb();(na=w(b)?x.jQuery:b?x[b]:void 0)&&na.fn.on?(B=na,S(na.fn,{scope:Na.scope,isolateScope:Na.isolateScope,controller:Na.controller,injector:Na.injector,inheritedData:Na.inheritedData}),a=na.cleanData,na.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=na._data(f,"events"))&&c.$destroy&&na(f).triggerHandler("$destroy");a(b)}):B=W;ea.element=B;Uc=!0}}function fb(a, +b,d){if(!a)throw Fa("areq",b||"?",d||"required");return a}function sb(a,b,d){d&&H(a)&&(a=a[a.length-1]);fb(D(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ka(a,b){if("hasOwnProperty"===a)throw Fa("badname",b);}function Vc(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g")+c[2];for(c=c[0];c--;)d=d.lastChild;f=ab(f,d.childNodes); +d=e.firstChild;d.textContent=""}else f.push(b.createTextNode(a));e.textContent="";e.innerHTML="";q(f,function(a){e.appendChild(a)});return e}function W(a){if(a instanceof W)return a;var b;F(a)&&(a=T(a),b=!0);if(!(this instanceof W)){if(b&&"<"!==a.charAt(0))throw dc("nosel");return new W(a)}if(b){b=x.document;var d;a=(d=dg.exec(a))?[b.createElement(d[1])]:(d=dd(a,b))?d.childNodes:[];ec(this,a)}else D(a)?ed(a):ec(this,a)}function fc(a){return a.cloneNode(!0)}function xb(a,b){!b&&bc(a)&&B.cleanData([a]); +a.querySelectorAll&&B.cleanData(a.querySelectorAll("*"))}function fd(a,b,d,c){if(u(c))throw dc("offargs");var e=(c=yb(a))&&c.events,f=c&&c.handle;if(f)if(b){var g=function(b){var c=e[b];u(d)&&$a(c||[],d);u(d)&&c&&0l&&this.remove(p.key);return b}},get:function(a){if(l";b=ta.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function La(a, +b){try{a.addClass(b)}catch(c){}}function ca(a,b,c,d,e){a instanceof B||(a=B(a));var f=Ma(a,b,a,c,d,e);ca.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw fa("multilink");fb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement;h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==wa(d)&&ma.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?B(ha(g,B("
").append(a).html())): +c?Na.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);ca.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Ma(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,n,p,r;if(K)for(r=Array(c.length),m=0;my.priority)break;if(x=y.scope)y.templateUrl||(C(x)?($("new/isolated scope",E||K,y,v),E=y):$("new/isolated scope",E,y,v)),K=K||y;P=y.name;if(!u&&(y.replace&&(y.templateUrl||y.template)||y.transclude&&!y.$$tlb)){for(x=z+1;u=a[x++];)if(u.transclude&&!u.$$tlb||u.replace&&(u.templateUrl||u.template)){La=!0;break}u=!0}!y.templateUrl&& +y.controller&&(G=G||V(),$("'"+P+"' controller",G[P],y,v),G[P]=y);if(x=y.transclude)if(J=!0,y.$$tlb||($("transclusion",t,y,v),t=y),"element"===x)X=!0,p=y.priority,N=v,v=d.$$element=B(ca.$$createComment(P,d[P])),b=v[0],ka(f,va.call(N,0),b),N[0].$$parentNode=N[0].parentNode,A=kc(La,N,e,p,g&&g.name,{nonTlbTranscludeDirective:t});else{var ja=V();if(C(x)){N=[];var Q=V(),jb=V();q(x,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Q[a]=b;ja[b]=null;jb[b]=c});q(v.contents(),function(a){var b=Q[Ba(wa(a))]; +b?(jb[b]=!0,ja[b]=ja[b]||[],ja[b].push(a)):N.push(a)});q(jb,function(a,b){if(!a)throw fa("reqslot",b);});for(var ic in ja)ja[ic]&&(ja[ic]=kc(La,ja[ic],e))}else N=B(fc(b)).contents();v.empty();A=kc(La,N,e,void 0,void 0,{needsNewScope:y.$$isolateScope||y.$$newScope});A.$$slots=ja}if(y.template)if(O=!0,$("template",I,y,v),I=y,x=D(y.template)?y.template(v,d):y.template,x=Ea(x),y.replace){g=y;N=cc.test(x)?pd(ha(y.templateNamespace,T(x))):[];b=N[0];if(1!==N.length||1!==b.nodeType)throw fa("tplrt",P,""); +ka(f,v,b);F={$attr:{}};x=jc(b,[],F);var Y=a.splice(z+1,a.length-(z+1));(E||K)&&aa(x,E,K);a=a.concat(x).concat(Y);da(d,F);F=a.length}else v.html(x);if(y.templateUrl)O=!0,$("template",I,y,v),I=y,y.replace&&(g=y),n=ga(a.splice(z,a.length-z),v,d,f,J&&A,h,k,{controllerDirectives:G,newScopeDirective:K!==y&&K,newIsolateScopeDirective:E,templateDirective:I,nonTlbTranscludeDirective:t}),F=a.length;else if(y.compile)try{R=y.compile(v,d,A);var Z=y.$$originalDirective||y;D(R)?m(null,bb(Z,R),Ma,L):R&&m(bb(Z,R.pre), +bb(Z,R.post),Ma,L)}catch(ea){c(ea,xa(v))}y.terminal&&(n.terminal=!0,p=Math.max(p,y.priority))}n.scope=K&&!0===K.scope;n.transcludeOnThisElement=J;n.templateOnThisElement=O;n.transclude=A;l.hasElementTranscludeDirective=X;return n}function U(a,b,c,d){var e;if(F(b)){var f=b.match(l);b=b.substring(f[0].length);var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e=g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw fa("ctreq",b,a);}else if(H(b))for(e= +[],g=0,f=b.length;gc.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=Vb(c,{$$start:k,$$end:l}));if(!c.$$bindings){var K=m=c,r=c.name,t={isolateScope:null,bindToController:null};C(K.scope)&&(!0===K.bindToController?(t.bindToController=d(K.scope,r,!0),t.isolateScope={}):t.isolateScope=d(K.scope,r,!1));C(K.bindToController)&&(t.bindToController=d(K.bindToController,r,!0));if(t.bindToController&&!K.controller)throw fa("noctrl", +r);m=m.$$bindings=t;C(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function Z(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d"+b+"";return c.childNodes[0].childNodes;default:return b}}function oa(a,b){if("srcdoc"===b)return y.HTML;var c=wa(a);if("src"===b||"ngSrc"===b){if(-1===["img","video","audio","source","track"].indexOf(c))return y.RESOURCE_URL}else if("xlinkHref"===b||"form"===c&&"action"===b||"link"===c&&"href"===b)return y.RESOURCE_URL}function pa(a, +c,d,e,f){var g=oa(a,e),h=k[e]||f,l=b(d,!f,g,h);if(l){if("multiple"===e&&"select"===wa(a))throw fa("selmulti",xa(a));if(m.test(e))throw fa("nodomevents");c.push({priority:100,compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=V());var k=f[e];k!==d&&(l=k&&b(k,!0,g,h),d=k);l&&(f[e]=l(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&f.$$observers[e].$$scope||a).$watch(l,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function ka(a,b,c){var d=b[0],e= +b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Ia&&""===d.nodeValue.trim())&&sg.call(a,b,1)}return a}function qg(a,b){if(b&&F(b))return b;if(F(a)){var d=sd.exec(a);if(d)return d[3]}}function wf(){var a={},b=!1;this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,c){Ka(b,"controller");C(b)? +S(a,b):a[b]=c};this.allowGlobals=function(){b=!0};this.$get=["$injector","$window",function(d,c){function e(a,b,c,d){if(!a||!C(a.$scope))throw L("$controller")("noscp",d,b);a.$scope[b]=c}return function(f,g,h,k){var l,m,n;h=!0===h;k&&F(k)&&(n=k);if(F(f)){k=f.match(sd);if(!k)throw td("ctrlfmt",f);m=k[1];n=n||k[3];f=a.hasOwnProperty(m)?a[m]:Vc(g.$scope,m,!0)||(b?Vc(c,m,!0):void 0);if(!f)throw td("ctrlreg",m);sb(f,m,!0)}if(h)return h=(H(f)?f[f.length-1]:f).prototype,l=Object.create(h||null),n&&e(g,n, +l,m||f.name),S(function(){var a=d.invoke(f,l,g,m);a!==l&&(C(a)||D(a))&&(l=a,n&&e(g,n,l,m||f.name));return l},{instance:l,identifier:n});l=d.instantiate(f,g,m);n&&e(g,n,l,m||f.name);return l}}]}function xf(){this.$get=["$window",function(a){return B(a.document)}]}function yf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden;a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function zf(){this.$get= +["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function mc(a){return C(a)?ga(a)?a.toISOString():cb(a):a}function Ef(){this.$get=function(){return function(a){if(!a)return"";var b=[];Kc(a,function(a,c){null===a||w(a)||(H(a)?q(a,function(a){b.push($(c)+"="+$(mc(a)))}):b.push($(c)+"="+$(mc(a))))});return b.join("&")}}}function Ff(){this.$get=function(){return function(a){function b(a,e,f){null===a||w(a)||(H(a)?q(a,function(a,c){b(a,e+"["+(C(a)?c:"")+"]")}):C(a)&&!ga(a)?Kc(a,function(a, +c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):d.push($(e)+"="+$(mc(a))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function nc(a,b){if(F(a)){var d=a.replace(tg,"").trim();if(d){var c=b("Content-Type");(c=c&&0===c.indexOf(ud))||(c=(c=d.match(ug))&&vg[c[0]].test(d));if(c)try{a=Oc(d)}catch(e){throw oc("baddata",a,e);}}}return a}function vd(a){var b=V(),d;F(a)?q(a.split("\n"),function(a){d=a.indexOf(":");var e=Q(T(a.substr(0,d)));a=T(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):C(a)&&q(a,function(a, +d){var f=Q(d),g=T(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function wd(a){var b;return function(d){b||(b=vd(a));return d?(d=b[Q(d)],void 0===d&&(d=null),d):b}}function xd(a,b,d,c){if(D(c))return c(a,b,d);q(c,function(c){a=c(a,b,d)});return a}function Df(){var a=this.defaults={transformResponse:[nc],transformRequest:[function(a){return C(a)&&"[object File]"!==ma.call(a)&&"[object Blob]"!==ma.call(a)&&"[object FormData]"!==ma.call(a)?cb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"}, +post:pa(pc),put:pa(pc),patch:pa(pc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return u(a)?(b=!!a,this):b};var d=this.interceptors=[];this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(c,e,f,g,h,k,l,m){function n(b){function d(a,b){for(var c=0,e=b.length;ca?b:k.reject(b)}if(!C(b))throw L("$http")("badreq",b);if(!F(m.valueOf(b.url)))throw L("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},b);g.headers= +function(b){var c=a.headers,d=S({},b.headers),f,g,h,c=S({},c.common,c[Q(b.method)]);a:for(f in c){g=Q(f);for(h in d)if(Q(h)===g)continue a;d[f]=c[f]}return e(d,pa(b))}(b);g.method=ub(g.method);g.paramSerializer=F(g.paramSerializer)?l.get(g.paramSerializer):g.paramSerializer;c.$$incOutstandingRequestCount();var h=[],n=[];b=k.resolve(g);q(t,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&n.push(a.response,a.responseError)});b=d(b,h);b=b.then(function(b){var c= +b.headers,d=xd(b.data,wd(c),void 0,b.transformRequest);w(d)&&q(c,function(a,b){"content-type"===Q(b)&&delete c[b]});w(b.withCredentials)&&!w(a.withCredentials)&&(b.withCredentials=a.withCredentials);return p(b,d).then(f,f)});b=d(b,n);return b=b.finally(function(){c.$$completeOutstandingRequest(z)})}function p(c,d){function g(a){if(a){var c={};q(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function l(a,c,d,e){function f(){p(c,a,d,e)}O&& +(200<=a&&300>a?O.put(R,[a,c,vd(d),e]):O.remove(R));b?h.$applyAsync(f):(f(),h.$$phase||h.$apply())}function p(a,b,d,e){b=-1<=b?b:0;(200<=b&&300>b?G.resolve:G.reject)({data:a,status:b,headers:wd(d),config:c,statusText:e})}function K(a){p(a.data,a.status,pa(a.headers()),a.statusText)}function t(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var G=k.defer(),y=G.promise,O,X,P=c.headers,s="jsonp"===Q(c.method),R=c.url;s?R=m.getTrustedResourceUrl(R):F(R)||(R=m.valueOf(R));R=r(R, +c.paramSerializer(c.params));s&&(R=J(R,c.jsonpCallbackParam));n.pendingRequests.push(c);y.then(t,t);!c.cache&&!a.cache||!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(O=C(c.cache)?c.cache:C(a.cache)?a.cache:v);O&&(X=O.get(R),u(X)?X&&D(X.then)?X.then(K,K):H(X)?p(X[1],X[0],pa(X[2]),X[3]):p(X,200,{},"OK"):O.put(R,y));w(X)&&((X=yd(c.url)?f()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(P[c.xsrfHeaderName||a.xsrfHeaderName]=X),e(c.method,R,d,l,P,c.timeout,c.withCredentials,c.responseType,g(c.eventHandlers), +g(c.uploadEventHandlers)));return y}function r(a,b){0=l&&(q.resolve(t),v(A.$$intervalId),delete g[A.$$intervalId]);M||a.$apply()},k);g[A.$$intervalId]=q;return A}var g={};f.cancel=function(a){return a&&a.$$intervalId in g?(g[a.$$intervalId].promise.catch(z),g[a.$$intervalId].reject("canceled"),b.clearInterval(a.$$intervalId),delete g[a.$$intervalId],!0):!1};return f}]}function qc(a){a=a.split("/");for(var b=a.length;b--;)a[b]= +db(a[b]);return a.join("/")}function zd(a,b){var d=Ca(a);b.$$protocol=d.protocol;b.$$host=d.hostname;b.$$port=Z(d.port)||xg[d.protocol]||null}function Ad(a,b){if(yg.test(a))throw kb("badpath",a);var d="/"!==a.charAt(0);d&&(a="/"+a);var c=Ca(a);b.$$path=decodeURIComponent(d&&"/"===c.pathname.charAt(0)?c.pathname.substring(1):c.pathname);b.$$search=Rc(c.search);b.$$hash=decodeURIComponent(c.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function rc(a,b){return a.slice(0,b.length)=== +b}function ka(a,b){if(rc(b,a))return b.substr(a.length)}function Aa(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function lb(a){return a.replace(/(#.+)|#$/,"$1")}function sc(a,b,d){this.$$html5=!0;d=d||"";zd(a,this);this.$$parse=function(a){var d=ka(b,a);if(!F(d))throw kb("ipthprfx",a,b);Ad(d,this);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=Zb(this.$$search),d=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(a?"?"+a:"")+d;this.$$absUrl=b+ +this.$$url.substr(1);this.$$urlUpdatedByLocation=!0};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;u(f=ka(a,c))?(g=f,g=d&&u(f=ka(d,f))?b+(ka("/",f)||f):a+g):u(f=ka(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function tc(a,b,d){zd(a,this);this.$$parse=function(c){var e=ka(a,c)||ka(b,c),f;w(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",w(e)&&(a=c,this.replace())):(f=ka(d,e),w(f)&&(f=e));Ad(f,this);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;rc(f, +e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$compose=function(){var b=Zb(this.$$search),e=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+(this.$$url?d+this.$$url:"");this.$$urlUpdatedByLocation=!0};this.$$parseLinkUrl=function(b,d){return Aa(a)===Aa(b)?(this.$$parse(b),!0):!1}}function Bd(a,b,d){this.$$html5=!0;tc.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)), +!0;var f,g;a===Aa(c)?f=c:(g=ka(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$compose=function(){var b=Zb(this.$$search),e=this.$$hash?"#"+db(this.$$hash):"";this.$$url=qc(this.$$path)+(b?"?"+b:"")+e;this.$$absUrl=a+d+this.$$url;this.$$urlUpdatedByLocation=!0}}function Jb(a){return function(){return this[a]}}function Cd(a,b){return function(d){if(w(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Jf(){var a="!",b={enabled:!1,requireBase:!0,rewriteLinks:!0}; +this.hashPrefix=function(b){return u(b)?(a=b,this):a};this.html5Mode=function(a){if(Ha(a))return b.enabled=a,this;if(C(a)){Ha(a.enabled)&&(b.enabled=a.enabled);Ha(a.requireBase)&&(b.requireBase=a.requireBase);if(Ha(a.rewriteLinks)||F(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function h(a,b,d){var e=l.url(),f=l.$$state;try{c.url(a,b,d),l.$$state=c.state()}catch(g){throw l.url(e),l.$$state= +f,g;}}function k(a,b){d.$broadcast("$locationChangeSuccess",l.absUrl(),a,l.$$state,b)}var l,m;m=c.baseHref();var n=c.url(),p;if(b.enabled){if(!m&&b.requireBase)throw kb("nobase");p=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(m||"/");m=e.history?sc:Bd}else p=Aa(n),m=tc;var r=p.substr(0,Aa(p).lastIndexOf("/")+1);l=new m(p,r,"#"+a);l.$$parseLinkUrl(n,n);l.$$state=c.state();var J=/^\s*(javascript|mailto):/i;f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&& +2!==a.which&&2!==a.button){for(var h=B(a.target);"a"!==wa(h[0]);)if(h[0]===f[0]||!(h=h.parent())[0])return;if(!F(e)||!w(h.attr(e))){var e=h.prop("href"),k=h.attr("href")||h.attr("xlink:href");C(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=Ca(e.animVal).href);J.test(e)||!e||h.attr("target")||a.isDefaultPrevented()||!l.$$parseLinkUrl(e,k)||(a.preventDefault(),l.absUrl()!==c.url()&&(d.$apply(),g.angular["ff-684208-preventDefault"]=!0))}}});lb(l.absUrl())!==lb(n)&&c.url(l.absUrl(),!0);var v=!0; +c.onUrlChange(function(a,b){rc(a,r)?(d.$evalAsync(function(){var c=l.absUrl(),e=l.$$state,f;a=lb(a);l.$$parse(a);l.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;l.absUrl()===a&&(f?(l.$$parse(c),l.$$state=e,h(c,!1,e)):(v=!1,k(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(v||l.$$urlUpdatedByLocation){l.$$urlUpdatedByLocation=!1;var a=lb(c.url()),b=lb(l.absUrl()),f=c.state(),g=l.$$replace,m=a!==b||l.$$html5&&e.history&&f!==l.$$state;if(v||m)v= +!1,d.$evalAsync(function(){var b=l.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,l.$$state,f).defaultPrevented;l.absUrl()===b&&(c?(l.$$parse(a),l.$$state=f):(m&&h(b,g,f===l.$$state?null:l.$$state),k(a,f)))})}l.$$replace=!1});return l}]}function Kf(){var a=!0,b=this;this.debugEnabled=function(b){return u(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){a instanceof Error&&(a.stack&&f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&& +(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||z;a=!1;try{a=!!e.apply}catch(f){}return a?function(){var a=[];q(arguments,function(b){a.push(c(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}var f=za||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function zg(a){return a+""}function Ag(a, +b){return"undefined"!==typeof a?a:b}function Dd(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function U(a,b){var d,c,e;switch(a.type){case s.Program:d=!0;q(a.body,function(a){U(a.expression,b);d=d&&a.expression.constant});a.constant=d;break;case s.Literal:a.constant=!0;a.toWatch=[];break;case s.UnaryExpression:U(a.argument,b);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case s.BinaryExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant; +a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case s.LogicalExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case s.ConditionalExpression:U(a.test,b);U(a.alternate,b);U(a.consequent,b);a.constant=a.test.constant&&a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case s.Identifier:a.constant=!1;a.toWatch=[a];break;case s.MemberExpression:U(a.object,b);a.computed&&U(a.property,b);a.constant=a.object.constant&& +(!a.computed||a.property.constant);a.toWatch=[a];break;case s.CallExpression:d=e=a.filter?!b(a.callee.name).$stateful:!1;c=[];q(a.arguments,function(a){U(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant=d;a.toWatch=e?c:[a];break;case s.AssignmentExpression:U(a.left,b);U(a.right,b);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case s.ArrayExpression:d=!0;c=[];q(a.elements,function(a){U(a,b);d=d&&a.constant;a.constant||c.push.apply(c,a.toWatch)});a.constant= +d;a.toWatch=c;break;case s.ObjectExpression:d=!0;c=[];q(a.properties,function(a){U(a.value,b);d=d&&a.value.constant&&!a.computed;a.value.constant||c.push.apply(c,a.value.toWatch);a.computed&&(U(a.key,b),a.key.constant||c.push.apply(c,a.key.toWatch))});a.constant=d;a.toWatch=c;break;case s.ThisExpression:a.constant=!1;a.toWatch=[];break;case s.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Ed(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Fd(a){return a.type=== +s.Identifier||a.type===s.MemberExpression}function Gd(a){if(1===a.body.length&&Fd(a.body[0].expression))return{type:s.AssignmentExpression,left:a.body[0].expression,right:{type:s.NGValueParameter},operator:"="}}function Hd(a){this.$filter=a}function Id(a){this.$filter=a}function uc(a,b,d){this.ast=new s(a,d);this.astCompiler=d.csp?new Id(b):new Hd(b)}function vc(a){return D(a.valueOf)?a.valueOf():Bg.call(a)}function Lf(){var a=V(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral= +function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=vc(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function g(a,b,c,d,e){var g=d.inputs,h;if(1===g.length){var k=f,g=g[0];return a.$watch(function(a){var b=g(a);f(b,k,d.literal)||(h=d(a,void 0,void 0,[b]),k=b&&vc(b));return h},b,c,e)}for(var l=[],m=[],n=0,E=g.length;n=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;fa)for(b in l++,f)ua.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$stateful=!0;var d=this,e,f,h,k=1t&&(w=4-t,u[w]||(u[w]=[]),u[w].push({msg:D(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:k}));else if(a===c){r=!1;break a}}catch(B){f(B)}if(!(p=q.$$watchersCount&&q.$$childHead||q!==this&&q.$$nextSibling))for(;q!==this&&!(p=q.$$nextSibling);)q=q.$parent}while(q=p);if((r||s.length)&&!t--)throw M.$$phase= +null,d("infdig",b,u);}while(r||s.length);for(M.$$phase=null;Iza)throw ta("iequirks");var c=pa(oa);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ya);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs, +f=c.getTrusted,g=c.trustAs;q(oa,function(a,b){var d=Q(b);c[("parse_as_"+d).replace(xc,gb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(xc,gb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(xc,gb)]=function(b){return g(a,b)}});return c}]}function Rf(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=Z((/android (\d+)/.exec(Q((a.navigator|| +{}).userAgent))||[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},h=g.body&&g.body.style,k=!1,l=!1;h&&(k=!!("transition"in h||"webkitTransition"in h),l=!!("animation"in h||"webkitAnimation"in h));return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&za)return!1;if(w(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ga(),transitions:k,animations:l,android:e}}]}function Tf(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler", +"$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(h,k){g.totalPendingRequests++;if(!F(h)||w(d.get(h)))h=f.getTrustedResourceUrl(h);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==nc}):l===nc&&(l=null);return c.get(h,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){d.put(h,a.data);return a.data},function(a){k||(a=Dg("tpload",h,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests= +0;return g}]}function Uf(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];q(a,function(a){var c=ea.element(a).data("$binding");c&&q(c,function(c){d?(new RegExp("(^|\\s)"+Kd(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-","data-ng-","ng\\:"],h=0;hc&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===zc;e++); +if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===zc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Ud&&(d=d.splice(0,Ud-1),b=c-1,c=1);return{d:d,e:b,i:c}}function Lg(a,b,d,c){var e=a.d,f=e.length-a.i;b=w(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d- +1]++;for(;fh;)k.unshift(0),h++;0=b.lgSize&&h.unshift(k.splice(-b.lgSize,k.length).join(""));k.length> +b.gSize;)h.unshift(k.splice(-b.gSize,k.length).join(""));k.length&&h.unshift(k.join(""));k=h.join(d);f.length&&(k+=c+f.join(""));e&&(k+="e+"+e)}return 0>a&&!g?b.negPre+k+b.negSuf:b.posPre+k+b.posSuf}function Kb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length-d)f+=d;0===f&&-12===d&&(f=12);return Kb(f,b,c,e)}}function mb(a,b,d){return function(c,e){var f= +c["get"+a](),g=ub((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Vd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function Wd(a){return function(b){var d=Vd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Kb(b,a)}}function Ac(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Pd(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,h=b[8]?a.setUTCFullYear:a.setFullYear, +k=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=Z(b[9]+b[10]),g=Z(b[9]+b[11]));h.call(a,Z(b[1]),Z(b[2])-1,Z(b[3]));f=Z(b[4]||0)-f;g=Z(b[5]||0)-g;h=Z(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));k.call(a,f,g,h,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",h=[],k,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;F(c)&&(c=Mg.test(c)?Z(c):b(c));ba(c)&&(c=new Date(c));if(!ga(c)||!isFinite(c.getTime()))return c; +for(;d;)(l=Ng.exec(d))?(h=ab(h,l,1),d=h.pop()):(h.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=Pc(f,m),c=Yb(c,f,!0));q(h,function(b){k=Og[b];g+=k?k(c,a.DATETIME_FORMATS,m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Fg(){return function(a,b){w(b)&&(b=2);return cb(a,b)}}function Gg(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):Z(b);if(da(b))return a;ba(a)&&(a=a.toString());if(!qa(a))return a;d=!d||isNaN(d)?0:Z(d);d=0>d?Math.max(0,a.length+ +d):d;return 0<=b?Bc(a,d,d+b):0===d?Bc(a,b,a.length):Bc(a,Math.max(0,d+b),d)}}function Bc(a,b,d){return F(a)?a.slice(b,d):va.call(a,b,d)}function Rd(a){function b(b){return b.map(function(b){var c=1,d=Ya;if(D(b))d=b;else if(F(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}} +function c(a,b){var c=0,d=a.type,k=b.type;if(d===k){var k=a.value,l=b.value;"string"===d?(k=k.toLowerCase(),l=l.toLowerCase()):"object"===d&&(C(k)&&(k=a.index),C(l)&&(l=b.index));k!==l&&(c=kb||37<=b&&40>=b||m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut", +m)}b.on("change",l);if(ae[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!k){var b=this.validity,c=b.badInput,d=b.typeMismatch;k=f.defer(function(){k=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Nb(a,b){return function(d,c){var e,f;if(ga(d))return d;if(F(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length-1)&&(d=d.substring(1,d.length-1));if(Pg.test(d))return new Date(d); +a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},q(e,function(a,c){c=v};g.$observe("min",function(a){v=p(a);h.$validate()})}if(u(g.max)||g.ngMax){var t; +h.$validators.max=function(a){return!n(a)||w(t)||d(a)<=t};g.$observe("max",function(a){t=p(a);h.$validate()})}}}function Dc(a,b,d,c){(c.$$hasNativeValidators=C(b[0].validity))&&c.$parsers.push(function(a){var c=b.prop("validity")||{};return c.badInput||c.typeMismatch?void 0:a})}function be(a){a.$$parserName="number";a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(Qg.test(b))return parseFloat(b)});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!ba(b))throw pb("numfmt",b);b=b.toString()}return b})} +function Sa(a){u(a)&&!ba(a)&&(a=parseFloat(a));return da(a)?void 0:a}function Ec(a){var b=a.toString(),d=b.indexOf(".");return-1===d?-1a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ce(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Ec(a):0,h=e?Ec(b):0,k=f?Ec(d):0,g=Math.max(g,h,k),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function de(a,b,d,c,e){if(u(c)){a=a(c);if(!a.constant)throw pb("constexpr", +d,c);return a(b)}return e}function Fc(a,b){function d(a,b){if(!a||!a.length)return[];if(!b||!b.length)return a;var c=[],d=0;a:for(;d(?:<\/\1>|)$/, +cc=/<|&#?\w+;/,bg=/<([\w:-]+)/,cg=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,ha={option:[1,'"],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ha.optgroup=ha.option;ha.tbody=ha.tfoot=ha.colgroup=ha.caption=ha.thead;ha.th=ha.td;var jg=x.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)& +16)},Na=W.prototype={ready:ed,toString:function(){var a=[];q(this,function(b){a.push(""+b)});return"["+a.join(", ")+"]"},eq:function(a){return 0<=a?B(this[a]):B(this[this.length+a])},length:0,push:Tg,sort:[].sort,splice:[].splice},Fb={};q("multiple selected checked disabled readOnly required open".split(" "),function(a){Fb[Q(a)]=a});var jd={};q("input select option textarea button form details".split(" "),function(a){jd[a]=!0});var rd={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max", +ngPattern:"pattern",ngStep:"step"};q({data:hc,removeData:gc,hasData:function(a){for(var b in hb[a.ng339])return!0;return!1},cleanData:function(a){for(var b=0,d=a.length;b/,mg=/^[^(]*\(\s*([^)]*)\)/m,Wg=/,/,Xg=/^\s*(_?)(\S+?)\1\s*$/,kg=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,ya=L("$injector");eb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw F(d)&&d||(d=a.name||ng(a)),ya("strictdi",d);b=ld(a);q(b[1].split(Wg),function(a){a.replace(Xg,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,sb(a[b],"fn"),c=a.slice(0,b)):sb(a,"fn", +!0);return c};var fe=L("$animate"),qf=function(){this.$get=z},rf=function(){var a=new Gb,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=F(b)?b.split(" "):H(b)?b:[],q(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){q(b,function(b){var c=a.get(b);if(c){var d=og(b.attr("class")),e="",f="";q(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});q(b,function(a){e&&Cb(a,e);f&&Bb(a,f)});a.delete(b)}});b.length=0}return{enabled:z, +on:z,off:z,pin:z,push:function(g,h,k,l){l&&l();k=k||{};k.from&&g.css(k.from);k.to&&g.css(k.to);if(k.addClass||k.removeClass)if(h=k.addClass,l=k.removeClass,k=a.get(g)||{},h=e(k,h,!0),l=e(k,l,!1),h||l)a.set(g,k),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},of=["$provide",function(a){var b=this,d=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw fe("notcsel",c);var f=c+"-animation";b.$$registeredAnimations[c.substr(1)]= +f;a.factory(f,d)};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,fe("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e <= >= && || ! = |".split(" "),function(a){Qb[a]=!0});var $g={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},wc=function(a){this.options=a};wc.prototype={constructor:wc,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index=a&&"string"===typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue? +this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)},isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"=== +a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b=u(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ua("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index","<=",">=");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:s.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a}, +unary:function(){var a;return(a=this.expect("+","-","!"))?{type:s.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()},primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=ra(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:s.Literal,value:this.options.literals[this.consume().text]}: +this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression",this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:s.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:s.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:s.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE"); +return a},filter:function(a){a=[a];for(var b={type:s.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression());return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:s.Identifier,name:a.text}},constant:function(){return{type:s.Literal,value:this.consume().value}}, +arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]");return{type:s.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:s.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")? +(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"),b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");return{type:s.ObjectExpression,properties:a}},throwError:function(a,b){throw Ua("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ua("ueoe", +this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ua("ueoe",this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:s.ThisExpression}, +$locals:{type:s.LocalsExpression}}};Hd.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[],body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};U(a,b.$filter);var d="",c;this.stage="assign";if(c=Gd(a))this.state.computing="assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Ed(a.body);b.stage="inputs";q(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var h=b.nextId(); +b.recurse(a,h);b.return_(h);b.state.inputs.push(d);a.watchId=c});this.state.computing="fn";this.stage="main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,zg,Ag,Dd);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;q(b,function(b){a.push("var "+ +b+"="+d.generateFunction(b,"s"))});b.length&&a.push("fn.inputs=["+b.join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;q(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")}, +recurse:function(a,b,d,c,e,f){var g,h,k=this,l,m,n;c=c||z;if(!f&&u(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case s.Program:q(a.body,function(b,c){k.recurse(b.expression,void 0,void 0,function(a){h=a});c!==a.body.length-1?k.current().body.push(h,";"):k.return_(h)});break;case s.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case s.UnaryExpression:this.recurse(a.argument,void 0, +void 0,function(a){h=a});m=a.operator+"("+this.ifDefined(h,0)+")";this.assign(b,m);c(m);break;case s.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){h=a});m="+"===a.operator?this.plus(g,h):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(h,0):"("+g+")"+a.operator+"("+h+")";this.assign(b,m);c(m);break;case s.LogicalExpression:b=b||this.nextId();k.recurse(a.left,b);k.if_("&&"===a.operator?b:k.not(b),k.lazyRecurse(a.right, +b));c(b);break;case s.ConditionalExpression:b=b||this.nextId();k.recurse(a.test,b);k.if_(b,k.lazyRecurse(a.alternate,b),k.lazyRecurse(a.consequent,b));c(b);break;case s.Identifier:b=b||this.nextId();d&&(d.context="inputs"===k.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);k.if_("inputs"===k.stage||k.not(k.getHasOwnProperty("l",a.name)),function(){k.if_("inputs"===k.stage||"s",function(){e&&1!==e&&k.if_(k.isNull(k.nonComputedMember("s",a.name)), +k.lazyAssign(k.nonComputedMember("s",a.name),"{}"));k.assign(b,k.nonComputedMember("s",a.name))})},b&&k.lazyAssign(b,k.nonComputedMember("l",a.name)));c(b);break;case s.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();k.recurse(a.object,g,void 0,function(){k.if_(k.notNull(g),function(){a.computed?(h=k.nextId(),k.recurse(a.property,h),k.getStringValue(h),e&&1!==e&&k.if_(k.not(k.computedMember(g,h)),k.lazyAssign(k.computedMember(g,h),"{}")),m=k.computedMember(g,h),k.assign(b, +m),d&&(d.computed=!0,d.name=h)):(e&&1!==e&&k.if_(k.isNull(k.nonComputedMember(g,a.property.name)),k.lazyAssign(k.nonComputedMember(g,a.property.name),"{}")),m=k.nonComputedMember(g,a.property.name),k.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){k.assign(b,"undefined")});c(b)},!!e);break;case s.CallExpression:b=b||this.nextId();a.filter?(h=k.filter(a.callee.name),l=[],q(a.arguments,function(a){var b=k.nextId();k.recurse(a,b);l.push(b)}),m=h+"("+l.join(",")+")",k.assign(b,m),c(b)): +(h=k.nextId(),g={},l=[],k.recurse(a.callee,h,g,function(){k.if_(k.notNull(h),function(){q(a.arguments,function(b){k.recurse(b,a.constant?void 0:k.nextId(),void 0,function(a){l.push(a)})});m=g.name?k.member(g.context,g.name,g.computed)+"("+l.join(",")+")":h+"("+l.join(",")+")";k.assign(b,m)},function(){k.assign(b,"undefined")});c(b)}));break;case s.AssignmentExpression:h=this.nextId();g={};this.recurse(a.left,void 0,g,function(){k.if_(k.notNull(g.context),function(){k.recurse(a.right,h);m=k.member(g.context, +g.name,g.computed)+a.operator+h;k.assign(b,m);c(b||m)})},1);break;case s.ArrayExpression:l=[];q(a.elements,function(b){k.recurse(b,a.constant?void 0:k.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case s.ObjectExpression:l=[];n=!1;q(a.properties,function(a){a.computed&&(n=!0)});n?(b=b||this.nextId(),this.assign(b,"{}"),q(a.properties,function(a){a.computed?(g=k.nextId(),k.recurse(a.key,g)):g=a.key.type===s.Identifier?a.key.name:""+a.key.value;h=k.nextId(); +k.recurse(a.value,h);k.assign(k.member(b,g,a.computed),h)})):(q(a.properties,function(b){k.recurse(b.value,a.constant?void 0:k.nextId(),void 0,function(a){l.push(k.escape(b.key.type===s.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case s.ThisExpression:this.assign(b,"s");c(b||"s");break;case s.LocalsExpression:this.assign(b,"l");c(b||"l");break;case s.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+ +b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a, +b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a, +b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(F(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(ba(a))return a.toString();if(!0===a)return"true";if(!1=== +a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ua("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Id.prototype={compile:function(a){var b=this;U(a,b.$filter);var d,c;if(d=Gd(a))c=this.recurse(d);d=Ed(a.body);var e;d&&(e=[],q(d,function(a,c){var d=b.recurse(a);a.input=d;e.push(d);a.watchId=c}));var f=[];q(a.body,function(a){f.push(b.recurse(a.expression))}); +a=0===a.body.length?z:1===a.body.length?f[0]:function(a,b){var c;q(f,function(d){c=d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case s.Literal:return this.value(a.value,b);case s.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case s.BinaryExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+ +a.operator](c,e,b);case s.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case s.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case s.Identifier:return f.identifier(a.name,b,d);case s.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c,e,b,d):this.nonComputedMember(c, +e,b,d);case s.CallExpression:return g=[],q(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var n=[],p=0;p":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}: +c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h)?b(e,f,g,h):d(e,f,g,h);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c=e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]= +{});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:e}},computedMember:function(a,b,d,c){return function(e,f,g,h){var k=a(e,f,g,h),l,m;null!=k&&(l=b(e,f,g,h),l+="",c&&1!==c&&k&&!k[l]&&(k[l]={}),m=k[l]);return d?{context:k,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,h){e=a(e,f,g,h);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d,c,e,f){return f?f[b]:a(d,c,e)}}};uc.prototype= +{constructor:uc,parse:function(a){a=this.ast.ast(a);var b=this.astCompiler.compile(a);b.literal=0===a.body.length||1===a.body.length&&(a.body[0].expression.type===s.Literal||a.body[0].expression.type===s.ArrayExpression||a.body[0].expression.type===s.ObjectExpression);b.constant=a.constant;return b}};var ta=L("$sce"),oa={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},xc=/_([a-z])/g,Dg=L("$compile"),aa=x.document.createElement("a"),Md=Ca(x.location.href);Nd.$inject=["$document"]; +cd.$inject=["$provide"];var Ud=22,Td=".",zc="0";Od.$inject=["$locale"];Qd.$inject=["$locale"];var Og={yyyy:Y("FullYear",4,0,!1,!0),yy:Y("FullYear",2,0,!0,!0),y:Y("FullYear",1,0,!1,!0),MMMM:mb("Month"),MMM:mb("Month",!0),MM:Y("Month",2,1),M:Y("Month",1,1),LLLL:mb("Month",!1,!0),dd:Y("Date",2),d:Y("Date",1),HH:Y("Hours",2),H:Y("Hours",1),hh:Y("Hours",2,-12),h:Y("Hours",1,-12),mm:Y("Minutes",2),m:Y("Minutes",1),ss:Y("Seconds",2),s:Y("Seconds",1),sss:Y("Milliseconds",3),EEEE:mb("Day"),EEE:mb("Day",!0), +a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Kb(Math[0=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},Ng=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,Mg=/^-?\d+$/;Pd.$inject=["$locale"];var Hg=la(Q),Ig=la(ub);Rd.$inject=["$parse"];var Fe=la({restrict:"E",compile:function(a, +b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===ma.call(b.prop("href"))?"xlink:href":"href";b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),vb={};q(Fb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=Ba("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});vb[c]=function(){return{restrict:"A",priority:100,link:e}}}});q(rd,function(a,b){vb[b]= +function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(Sg))){e.$set("ngPattern",new RegExp(c[1],c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});q(["src","srcset","href"],function(a){var b=Ba("ng-"+a);vb[b]=function(){return{priority:99,link:function(d,c,e){var f=a,g=a;"href"===a&&"[object SVGAnimatedString]"===ma.call(c.prop("href"))&&(g="xlinkHref",e.$attr[g]="xlink:href",f=null);e.$observe(b,function(b){b?(e.$set(g,b), +za&&f&&c.prop(f,e[g])):"href"===a&&e.$set(g,null)})}}}});var Mb={$addControl:z,$$renameControl:function(a,b){a.$name=b},$removeControl:z,$setValidity:z,$setDirty:z,$setPristine:z,$setSubmitted:z};Lb.$inject=["$element","$attrs","$scope","$animate","$interpolate"];Lb.prototype={$rollbackViewValue:function(){q(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){q(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ka(a.$name,"input");this.$$controls.push(a); +a.$name&&(this[a.$name]=a);a.$$parentForm=this},$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&this[a.$name]===a&&delete this[a.$name];q(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);q(this.$error,function(b,d){this.$setValidity(d,null,a)},this);q(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);$a(this.$$controls,a);a.$$parentForm=Mb},$setDirty:function(){this.$$animate.removeClass(this.$$element, +Va);this.$$animate.addClass(this.$$element,Rb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,Va,Rb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;q(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){q(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){this.$$animate.addClass(this.$$element,"ng-submitted");this.$submitted=!0;this.$$parentForm.$setSubmitted()}}; +Zd({clazz:Lb,set:function(a,b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&($a(c,d),0===c.length&&delete a[b])}});var ge=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||z}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Lb,compile:function(d,f){d.addClass(Va).addClass(nb);var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var n=f[0];if(!("action"in +e)){var p=function(b){a.$apply(function(){n.$commitViewValue();n.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",p);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",p)},0,!1)})}(f[1]||n.$$parentForm).$addControl(n);var r=g?c(n.$name):z;g&&(r(a,n),e.$observe(g,function(b){n.$name!==b&&(r(a,void 0),n.$$parentForm.$$renameControl(n,b),r=c(n.$name),r(a,n))}));d.on("$destroy",function(){n.$$parentForm.$removeControl(n);r(a,void 0);S(n,Mb)})}}}}}]},Ge=ge(), +Se=ge(!0),Pg=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,ah=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,bh=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/,Qg=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,he=/^(\d{4,})-(\d{2})-(\d{2})$/,ie=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/, +Hc=/^(\d{4,})-W(\d\d)$/,je=/^(\d{4,})-(\d\d)$/,ke=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ae=V();q(["date","datetime-local","month","time","week"],function(a){ae[a]=!0});var le={text:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c)},date:ob("date",he,Nb(he,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ob("datetimelocal",ie,Nb(ie,"yyyy MM dd HH mm ss sss".split(" ")),"yyyy-MM-ddTHH:mm:ss.sss"),time:ob("time",ke,Nb(ke,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:ob("week",Hc,function(a,b){if(ga(a))return a; +if(F(a)){Hc.lastIndex=0;var d=Hc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,h=0,k=Vd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),h=b.getMilliseconds());return new Date(c,0,k.getDate()+e,d,f,g,h)}}return NaN},"yyyy-Www"),month:ob("month",je,Nb(je,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f){Dc(a,b,d,c);be(c);Ra(a,b,d,c,e,f);var g,h;if(u(d.min)||d.ngMin)c.$validators.min=function(a){return c.$isEmpty(a)||w(g)||a>=g},d.$observe("min",function(a){g=Sa(a);c.$validate()}); +if(u(d.max)||d.ngMax)c.$validators.max=function(a){return c.$isEmpty(a)||w(h)||a<=h},d.$observe("max",function(a){h=Sa(a);c.$validate()});if(u(d.step)||d.ngStep){var k;c.$validators.step=function(a,b){return c.$isEmpty(b)||w(k)||ce(b,g||0,k)};d.$observe("step",function(a){k=Sa(a);c.$validate()})}},url:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c);c.$$parserName="url";c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||ah.test(d)}},email:function(a,b,d,c,e,f){Ra(a,b,d,c,e,f);Cc(c);c.$$parserName= +"email";c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||bh.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==T(d.ngTrim);w(d.name)&&b.attr("name",++qb);b.on("click",function(a){var g;b[0].checked&&(g=d.value,e&&(g=T(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=T(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,c){b.attr(a,d[a]);d.$observe(a,c)}function h(a){n=Sa(a);da(c.$modelValue)|| +(m?(a=b.val(),n>a&&(a=n,b.val(a)),c.$setViewValue(a)):c.$validate())}function k(a){p=Sa(a);da(c.$modelValue)||(m?(a=b.val(),p=n},g("min",h));e&&(c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||w(p)||b<=p},g("max",k));f&&(c.$validators.step=m?function(){return!q.stepMismatch}:function(a,b){return c.$isEmpty(b)||w(r)||ce(b,n||0,r)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,h){var k=de(h,a,"ngTrueValue",d.ngTrueValue,!0),l=de(h,a,"ngFalseValue", +d.ngFalseValue,!1);b.on("click",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty=function(a){return!1===a};c.$formatters.push(function(a){return sa(a,k)});c.$parsers.push(function(a){return a?k:l})},hidden:z,button:z,submit:z,reset:z,file:z},Xc=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,h){h[0]&&(le[Q(g.type)]||le.text)(e,f,g,h[0],b,a,d,c)}}}}],ch=/^(true|false|\d+)$/, +kf=function(){function a(a,d,c){var e=u(c)?c:9===za?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return ch.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue,function(b){a(d,f,b)})}}}},Ke=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=$b(a)})}}}}],Me=["$interpolate", +"$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=w(a)?"":a})}}}}],Le=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g, +function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],jf=la({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ne=Fc("",!0),Pe=Fc("Odd",0),Oe=Fc("Even",1),Qe=Qa({compile:function(a,b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),Re=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],bd={},dh={blur:!0,focus:!0};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "), +function(a){var b=Ba("ng-"+a);bd[b]=["$parse","$rootScope",function(d,c){return{restrict:"A",compile:function(e,f){var g=d(f[b]);return function(b,d){d.on(a,function(d){var e=function(){g(b,{$event:d})};dh[a]&&c.$$phase?b.$evalAsync(e):b.$apply(e)})}}}}]});var Ue=["$animate","$compile",function(a,b){return{multiElement:!0,transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var h,k,l;d.$watch(e.ngIf,function(d){d?k||g(function(d,f){k=f;d[d.length++]=b.$$createComment("end ngIf", +e.ngIf);h={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),k&&(k.$destroy(),k=null),h&&(l=tb(h.clone),a.leave(l).done(function(a){!1!==a&&(l=null)}),h=null))})}}}],Ve=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element",controller:ea.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",h=e.autoscroll;return function(c,e,m,n,p){var r=0,q,s,t,w=function(){s&&(s.remove(),s=null);q&&(q.$destroy(),q= +null);t&&(d.leave(t).done(function(a){!1!==a&&(s=null)}),s=t,t=null)};c.$watch(f,function(f){var m=function(a){!1===a||!u(h)||h&&!c.$eval(h)||b()},s=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&s===r){var b=c.$new();n.template=a;a=p(b,function(a){w();d.enter(a,null,e).done(m)});q=b;t=a;q.$emit("$includeContentLoaded",f);c.$eval(g)}},function(){c.$$destroyed||s!==r||(w(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(w(),n.template=null)})}}}}],mf=["$compile",function(a){return{restrict:"ECA", +priority:-400,require:"ngInclude",link:function(b,d,c,e){ma.call(d[0]).match(/SVG/)?(d.empty(),a(dd(e.template,x.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],We=Qa({priority:450,compile:function(){return{pre:function(a,b,d){a.$eval(d.ngInit)}}}}),hf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?T(e):e;c.$parsers.push(function(a){if(!w(a)){var b= +[];a&&q(a.split(g),function(a){a&&b.push(f?T(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},nb="ng-valid",Yd="ng-invalid",Va="ng-pristine",Rb="ng-dirty",pb=L("ngModel");Ob.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" ");Ob.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+ +"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);D(c)&&(c=a(b));return c};this.$$ngModelSet=function(a,c){D(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw pb("nonassign",this.$$attr.ngModel,xa(this.$$element));},$render:z,$isEmpty:function(a){return w(a)||""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element, +"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Rb);this.$$animate.addClass(this.$$element,Va)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element,Va);this.$$animate.addClass(this.$$element,Rb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched= +!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!da(this.$modelValue)){var a=this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"), +f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;q(k.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(q(k.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;q(k.$asyncValidators,function(e,g){var k=e(a,b);if(!k||!D(k.then))throw pb("nopromise",k);f(g,void 0);c.push(k.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))}); +c.length?k.$$q.all(c).then(function(){g(d)},z):g(!0)}function f(a,b){h===k.$$currentValidationRunId&&k.$setValidity(a,b)}function g(a){h===k.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var h=this.$$currentValidationRunId,k=this;(function(){var a=k.$$parserName||"parse";if(w(k.$$parserValid))f(a,null);else return k.$$parserValid||(q(k.$validators,function(a,b){f(b,null)}),q(k.$asyncValidators,function(a,b){f(b,null)})),f(a,k.$$parserValid),k.$$parserValid;return!0})()?c()?e():g(!1): +g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;if(this.$$parserValid=w(a)?void 0:!0)for(var d=0;de||c.$isEmpty(b)|| +b.length<=e}}}}},$c=function(){return{restrict:"A",require:"?ngModel",link:function(a,b,d,c){if(c){var e=0;d.$observe("minlength",function(a){e=Z(a)||0;c.$validate()});c.$validators.minlength=function(a,b){return c.$isEmpty(b)||b.length>=e}}}}};x.angular.bootstrap?x.console&&console.log("WARNING: Tried to load angular more than once."):(ze(),Ce(ea),ea.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1==b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM", +"PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "),WEEKENDRANGE:[5, +6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a, +c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),B(function(){ue(x.document,Sc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend(''); +//# sourceMappingURL=angular.min.js.map diff -r 56e72cd18404 -r 231d2186f3fc js/core.js --- a/js/core.js Fri Jul 14 15:37:53 2017 +0100 +++ b/js/core.js Fri Jul 14 15:39:24 2017 +0100 @@ -5,6 +5,10 @@ * Also contains all global variables. */ +/*globals window, document, XMLDocument, Element, XMLHttpRequest, DOMParser, console, Blob, $, Promise, navigator */ +/*globals AudioBuffer, AudioBufferSourceNode */ +/*globals Specification, calculateLoudness, WAVE, validateXML, showdown, pageXMLSave, loadTest, resizeWindow */ + /* create the web audio API context and store in audioContext*/ var audioContext; // Hold the browser web audio API var projectXML; // Hold the parsed setup XML @@ -48,13 +52,13 @@ name = String(name); var selected = this.documentElement.getAllElementsByName(name); return selected; -} +}; Element.prototype.getAllElementsByName = function (name) { name = String(name); var selected = []; var node = this.firstElementChild; - while (node != null) { + while (node !== null) { if (node.getAttribute('name') == name) { selected.push(node); } @@ -64,19 +68,19 @@ node = node.nextElementSibling; } return selected; -} +}; XMLDocument.prototype.getAllElementsByTagName = function (name) { name = String(name); var selected = this.documentElement.getAllElementsByTagName(name); return selected; -} +}; Element.prototype.getAllElementsByTagName = function (name) { name = String(name); var selected = []; var node = this.firstElementChild; - while (node != null) { + while (node !== null) { if (node.nodeName == name) { selected.push(node); } @@ -86,7 +90,7 @@ node = node.nextElementSibling; } return selected; -} +}; // Firefox does not have an XMLDocument.prototype.getElementsByName if (typeof XMLDocument.prototype.getElementsByName != "function") { @@ -94,14 +98,14 @@ name = String(name); var node = this.documentElement.firstElementChild; var selected = []; - while (node != null) { + while (node !== null) { if (node.getAttribute('name') == name) { selected.push(node); } node = node.nextElementSibling; } return selected; - } + }; } var check_dependancies = function () { @@ -122,7 +126,7 @@ return false; } return true; -} +}; var onload = function () { // Function called once the browser has loaded all files. @@ -131,7 +135,7 @@ // Create a web audio API context // Fixed for cross-browser support var AudioContext = window.AudioContext || window.webkitAudioContext; - audioContext = new AudioContext; + audioContext = new AudioContext(); // Create test state testState = new stateMachine(); @@ -152,11 +156,11 @@ interfaceContext.resizeWindow(event); }; - if (window.location.search.length != 0) { + if (window.location.search.length !== 0) { var search = window.location.search.split('?')[1]; // Now split the requests into pairs var searchQueries = search.split('&'); - + var url; for (var i in searchQueries) { // Split each key-value pair searchQueries[i] = searchQueries[i].split('='); @@ -165,12 +169,13 @@ switch (key) { case "url": url = value; + specification.url = url; break; case "returnURL": gReturnURL = value; break; case "saveFilenamePrefix": - gSaveFilenamePrefix = value; + storage.filenamePrefix = value; break; } } @@ -188,9 +193,7 @@ var xmlhttp = new XMLHttpRequest(); xmlhttp.open("GET", 'xml/test-schema.xsd', true); xmlhttp.onload = function () { - schemaXSD = xmlhttp.response; - var parse = new DOMParser(); - specification.schema = parse.parseFromString(xmlhttp.response, 'text/xml'); + specification.processSchema(xmlhttp.response); var r = new XMLHttpRequest(); r.open('GET', url, true); r.onload = function () { @@ -204,11 +207,11 @@ span.textContent = "There was an error when loading your XML file. Please check your path in the URL. After the path to this page, there should be '?url=path/to/your/file.xml'. Check the spelling of your filename as well. If you are still having issues, check the log of the python server or your webserver distribution for 404 codes for your file."; document.getElementsByTagName('body')[0].appendChild(msg); document.getElementsByTagName('body')[0].appendChild(span); - } + }; r.send(); }; xmlhttp.send(); -}; +} function loadProjectSpecCallback(response) { // Function called after asynchronous download of XML project specification @@ -219,10 +222,11 @@ var parse = new DOMParser(); var responseDocument = parse.parseFromString(response, 'text/xml'); var errorNode = responseDocument.getElementsByTagName('parsererror'); + var msg, span; if (errorNode.length >= 1) { - var msg = document.createElement("h3"); + msg = document.createElement("h3"); msg.textContent = "FATAL ERROR"; - var span = document.createElement("span"); + span = document.createElement("span"); span.textContent = "The XML parser returned the following errors when decoding your XML file"; document.getElementsByTagName('body')[0].innerHTML = null; document.getElementsByTagName('body')[0].appendChild(msg); @@ -230,10 +234,10 @@ document.getElementsByTagName('body')[0].appendChild(errorNode[0]); return; } - if (responseDocument == undefined || responseDocument.firstChild == undefined) { - var msg = document.createElement("h3"); + if (responseDocument === undefined || responseDocument.firstChild === undefined) { + msg = document.createElement("h3"); msg.textContent = "FATAL ERROR"; - var span = document.createElement("span"); + span = document.createElement("span"); span.textContent = "The project XML was not decoded properly, try refreshing your browser and clearing caches. If the problem persists, contact the test creator."; document.getElementsByTagName('body')[0].innerHTML = null; document.getElementsByTagName('body')[0].appendChild(msg); @@ -246,7 +250,7 @@ // Perform XML schema validation var Module = { xml: response, - schema: schemaXSD, + schema: specification.getSchemaString(), arguments: ["--noout", "--schema", 'test-schema.xsd', 'document.xml'] }; projectXML = responseDocument; @@ -254,16 +258,16 @@ console.log(xmllint); if (xmllint != 'document.xml validates\n') { document.getElementsByTagName('body')[0].innerHTML = null; - var msg = document.createElement("h3"); + msg = document.createElement("h3"); msg.textContent = "FATAL ERROR"; - var span = document.createElement("h4"); + span = document.createElement("h4"); span.textContent = "The XML validator returned the following errors when decoding your XML file"; document.getElementsByTagName('body')[0].appendChild(msg); document.getElementsByTagName('body')[0].appendChild(span); xmllint = xmllint.split('\n'); for (var i in xmllint) { document.getElementsByTagName('body')[0].appendChild(document.createElement('br')); - var span = document.createElement("span"); + span = document.createElement("span"); span.textContent = xmllint[i]; document.getElementsByTagName('body')[0].appendChild(span); } @@ -278,15 +282,16 @@ // document is a result projectXML = document.implementation.createDocument(null, "waet"); projectXML.firstChild.appendChild(responseDocument.getElementsByTagName('waet')[0].getElementsByTagName("setup")[0].cloneNode(true)); - var child = responseDocument.firstChild.firstChild; - while (child != null) { + var child = responseDocument.firstChild.firstChild, + copy; + while (child !== null) { if (child.nodeName == "survey") { // One of the global survey elements if (child.getAttribute("state") == "complete") { // We need to remove this survey from var location = child.getAttribute("location"); var globalSurveys = projectXML.getElementsByTagName("setup")[0].getElementsByTagName("survey")[0]; - while (globalSurveys != null) { + while (globalSurveys !== null) { if (location == "pre" || location == "before") { if (globalSurveys.getAttribute("location") == "pre" || globalSurveys.getAttribute("location") == "before") { projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys); @@ -302,7 +307,7 @@ } } else { // We need to complete this, so it must be regenerated by store - var copy = child; + copy = child; child = child.previousElementSibling; responseDocument.firstChild.removeChild(copy); } @@ -310,7 +315,7 @@ if (child.getAttribute("state") == "empty") { // We need to complete this page projectXML.firstChild.appendChild(responseDocument.getElementById(child.getAttribute("ref")).cloneNode(true)); - var copy = child; + copy = child; child = child.previousElementSibling; responseDocument.firstChild.removeChild(copy); } @@ -323,7 +328,7 @@ storage.initialise(responseDocument); } /// CHECK FOR SAMPLE RATE COMPATIBILITY - if (specification.sampleRate != undefined) { + if (isFinite(specification.sampleRate)) { if (Number(specification.sampleRate) != audioContext.sampleRate) { var errStr = 'Sample rates do not match! Requested ' + Number(specification.sampleRate) + ', got ' + audioContext.sampleRate + '. Please set the sample rate to match before completing this test.'; interfaceContext.lightbox.post("Error", errStr); @@ -335,7 +340,7 @@ getInterfaces.open("GET", "interfaces/interfaces.json"); getInterfaces.onerror = function (e) { throw (e); - } + }; getInterfaces.onload = function () { if (getInterfaces.status !== 200) { throw (new Error(getInterfaces.status)); @@ -363,14 +368,14 @@ css.setAttribute("href", v); head.appendChild(css); }); - } + }; getInterfaces.send(); - if (gReturnURL != undefined) { + if (gReturnURL !== undefined) { console.log("returnURL Overide from " + specification.returnURL + " to " + gReturnURL); specification.returnURL = gReturnURL; } - if (gSaveFilenamePrefix != undefined) { + if (gSaveFilenamePrefix !== undefined) { specification.saveFilenamePrefix = gSaveFilenamePrefix; } @@ -384,7 +389,7 @@ // Save the data from interface into XML and send to destURL // If destURL is null then download XML in client // Now time to render file locally - var xmlDoc = interfaceXMLSave(); + var xmlDoc = storage.finish(); var parent = document.createElement("div"); parent.appendChild(xmlDoc); var file = [parent.innerHTML]; @@ -403,51 +408,22 @@ popup.popupContent.innerHTML = "Please save the file below to give to your test supervisor
"; popup.popupContent.appendChild(a); } else { - var saveUrlSuffix = ""; - var saveFilenamePrefix = specification.saveFilenamePrefix; - if (typeof (saveFilenamePrefix) === "string" && saveFilenamePrefix.length > 0) { - saveUrlSuffix = "&saveFilenamePrefix=" + saveFilenamePrefix; - } var projectReturn = ""; if (typeof specification.projectReturn == "string") { if (specification.projectReturn.substr(0, 4) == "http") { projectReturn = specification.projectReturn; } } - var saveURL = projectReturn + "php/save.php?key=" + storage.SessionKey.key + saveUrlSuffix; - var xmlhttp = new XMLHttpRequest; - xmlhttp.open("POST", saveURL, true); - xmlhttp.setRequestHeader('Content-Type', 'text/xml'); - xmlhttp.onerror = function () { - console.log('Error saving file to server! Presenting download locally'); + storage.SessionKey.finish().then(function (resolved) { + if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) { + window.location = specification.returnURL; + } else { + popup.popupContent.textContent = specification.exitText; + } + }, function (message) { + console.log("Save: Error! " + message.textContent); createProjectSave("local"); - }; - xmlhttp.onload = function () { - console.log(xmlhttp); - if (this.status >= 300) { - console.log("WARNING - Could not update at this time"); - createProjectSave("local"); - } else { - var parser = new DOMParser(); - var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml"); - var response = xmlDoc.getElementsByTagName('response')[0]; - if (response.getAttribute("state") == "OK") { - window.onbeforeunload = undefined; - var file = response.getElementsByTagName("file")[0]; - console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B"); - if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) { - window.location = specification.returnURL; - } else { - popup.popupContent.textContent = specification.exitText; - } - } else { - var message = response.getElementsByTagName("message"); - console.log("Save: Error! " + message.textContent); - createProjectSave("local"); - } - } - }; - xmlhttp.send(file); + }); popup.showPopup(); popup.popupContent.innerHTML = null; popup.popupContent.textContent = "Submitting. Please Wait"; @@ -517,7 +493,7 @@ } function randomString(length) { - var str = "" + var str = ""; for (var i = 0; i < length; i += 2) { var num = Math.floor(Math.random() * 1295); str += num.toString(36); @@ -532,7 +508,7 @@ var inputSequence = []; // For safety purposes: keep track of randomisation for (var counter = 0; counter < N; ++counter) - inputSequence.push(counter) // Fill array + inputSequence.push(counter); // Fill array var inputSequenceClone = inputSequence.slice(0); var holdArr = []; @@ -577,6 +553,7 @@ this.currentIndex = null; this.node = null; this.store = null; + var lastNodeStart; $(window).keypress(function (e) { if (e.keyCode == 13 && popup.popup.style.visibility == 'visible') { console.log(e); @@ -584,6 +561,441 @@ e.preventDefault(); } }); + // Generators & Processors // + + function processConditional(node, value) { + function jumpToId(jumpID) { + var index = this.popupOptions.findIndex(function (item, index, element) { + if (item.specification.id == jumpID) { + return true; + } else { + return false; + } + }, this); + this.currentIndex = index - 1; + } + var conditionFunction; + if (node.specification.type === "question") { + conditionFunction = processQuestionConditional; + } else if (node.specification.type === "checkbox") { + conditionFunction = processCheckboxConditional; + } else if (node.specification.type === "radio") { + conditionFunction = processRadioConditional; + } else if (node.specification.type === "number") { + conditionFunction = processNumberConditional; + } else if (node.specification.type === "slider") { + conditionFunction = processSliderConditional; + } else { + return; + } + for (var i = 0; i < node.specification.conditions.length; i++) { + var condition = node.specification.conditions[i]; + var pass = conditionFunction(condition, value); + var jumpID; + if (pass) { + jumpID = condition.jumpToOnPass; + } else { + jumpID = condition.jumpToOnFail; + } + if (jumpID !== null) { + jumpToId.call(this, jumpID); + break; + } + } + } + + function postQuestion(node) { + var textArea = document.createElement('textarea'); + switch (node.specification.boxsize) { + case 'small': + textArea.cols = "20"; + textArea.rows = "1"; + break; + case 'normal': + textArea.cols = "30"; + textArea.rows = "2"; + break; + case 'large': + textArea.cols = "40"; + textArea.rows = "5"; + break; + case 'huge': + textArea.cols = "50"; + textArea.rows = "10"; + break; + } + if (node.response === undefined) { + node.response = ""; + } else { + textArea.value = node.response; + } + this.popupResponse.appendChild(textArea); + textArea.focus(); + this.popupResponse.style.textAlign = "center"; + this.popupResponse.style.left = "0%"; + } + + function processQuestionConditional(condition, value) { + switch (condition.check) { + case "equals": + // Deliberately loose check + if (value == condition.value) { + return true; + } + break; + case "greaterThan": + case "lessThan": + console.log("Survey Element of type 'question' cannot interpret greaterThan/lessThan conditions. IGNORING"); + break; + case "contains": + if (value.includes(condition.value)) { + return true; + } + break; + } + return false; + } + + function processQuestion(node) { + var textArea = this.popupResponse.getElementsByTagName("textarea")[0]; + if (node.specification.mandatory === true && textArea.value.length === 0) { + interfaceContext.lightbox.post("Error", "This question is mandatory"); + return false; + } + // Save the text content + console.log("Question: " + node.specification.statement); + console.log("Question Response: " + textArea.value); + node.response = textArea.value; + processConditional.call(this, node, textArea.value); + return true; + } + + function postCheckbox(node) { + if (node.response === undefined) { + node.response = Array(node.specification.options.length); + } + var table = document.createElement("table"); + table.className = "popup-option-list"; + table.border = "0"; + node.response = []; + node.specification.options.forEach(function (option, index) { + var tr = document.createElement("tr"); + table.appendChild(tr); + var td = document.createElement("td"); + tr.appendChild(td); + var input = document.createElement('input'); + input.id = option.name; + input.type = 'checkbox'; + td.appendChild(input); + + td = document.createElement("td"); + tr.appendChild(td); + var span = document.createElement('span'); + span.textContent = option.text; + td.appendChild(span); + tr = document.createElement('div'); + tr.setAttribute('name', 'option'); + tr.className = "popup-option-checbox"; + if (node.response[index] !== undefined) { + if (node.response[index].checked === true) { + input.checked = "true"; + } + } + index++; + }); + this.popupResponse.appendChild(table); + } + + function processCheckbox(node) { + console.log("Checkbox: " + node.specification.statement); + var inputs = this.popupResponse.getElementsByTagName('input'); + node.response = []; + var numChecked = 0, + i; + for (i = 0; i < node.specification.options.length; i++) { + if (inputs[i].checked) { + numChecked++; + } + } + if (node.specification.min !== undefined) { + if (node.specification.max === undefined) { + if (numChecked < node.specification.min) { + var msg = "You must select at least " + node.specification.min + " option"; + if (node.specification.min > 1) { + msg += "s"; + } + interfaceContext.lightbox.post("Error", msg); + return; + } + } else { + if (numChecked < node.specification.min || numChecked > node.specification.max) { + if (node.specification.min == node.specification.max) { + interfaceContext.lightbox.post("Error", "You must only select " + node.specification.min); + } else { + interfaceContext.lightbox.post("Error", "You must select between " + node.specification.min + " and " + node.specification.max); + } + return false; + } + } + } + for (i = 0; i < node.specification.options.length; i++) { + node.response.push({ + name: node.specification.options[i].name, + text: node.specification.options[i].text, + checked: inputs[i].checked + }); + console.log(node.specification.options[i].name + ": " + inputs[i].checked); + } + processConditional.call(this, node, node.response); + return true; + } + + function processCheckboxConditional(condition, response) { + switch (condition.check) { + case "contains": + for (var i = 0; i < response.length; i++) { + var value = response[i]; + if (value.name === condition.value && value.checked) { + return true; + } + } + break; + case "equals": + case "greaterThan": + case "lessThan": + console.log("Survey Element of type 'checkbox' cannot interpret equals/greaterThan/lessThan conditions. IGNORING"); + break; + default: + console.log("Unknown condition. IGNORING"); + break; + } + return false; + } + + function postRadio(node) { + if (node.response === null) { + node.response = { + name: "", + text: "" + }; + } + var table = document.createElement("table"); + table.className = "popup-option-list"; + table.border = "0"; + if (node.response === null || node.response.length === 0) { + node.response = []; + } + node.specification.options.forEach(function (option, index) { + var tr = document.createElement("tr"); + table.appendChild(tr); + var td = document.createElement("td"); + tr.appendChild(td); + var input = document.createElement('input'); + input.id = option.name; + input.type = 'radio'; + input.name = node.specification.id; + td.appendChild(input); + + td = document.createElement("td"); + tr.appendChild(td); + var span = document.createElement('span'); + span.textContent = option.text; + td.appendChild(span); + tr = document.createElement('div'); + tr.setAttribute('name', 'option'); + tr.className = "popup-option-checbox"; + table.appendChild(tr); + }); + this.popupResponse.appendChild(table); + } + + function processRadio(node) { + var optHold = this.popupResponse; + console.log("Radio: " + node.specification.statement); + node.response = null; + var i = 0; + var inputs = optHold.getElementsByTagName('input'); + while (node.response === null) { + if (i == inputs.length) { + if (node.specification.mandatory === true) { + interfaceContext.lightbox.post("Error", "Please select one option"); + return false; + } + break; + } + if (inputs[i].checked === true) { + node.response = node.specification.options[i]; + console.log("Selected: " + node.specification.options[i].name); + } + i++; + } + processConditional.call(this, node, node.response.name); + return true; + } + + function processRadioConditional(condition, response) { + switch (condition.check) { + case "equals": + if (response === condition.value) { + return true; + } + break; + case "contains": + case "greaterThan": + case "lessThan": + console.log("Survey Element of type 'radio' cannot interpret contains/greaterThan/lessThan conditions. IGNORING"); + break; + default: + console.log("Unknown condition. IGNORING"); + break; + } + return false; + } + + function postNumber(node) { + var input = document.createElement('input'); + input.type = 'textarea'; + if (node.specification.min !== null) { + input.min = node.specification.min; + } + if (node.specification.max !== null) { + input.max = node.specification.max; + } + if (node.specification.step !== null) { + input.step = node.specification.step; + } + if (node.response !== undefined) { + input.value = node.response; + } + this.popupResponse.appendChild(input); + this.popupResponse.style.textAlign = "center"; + this.popupResponse.style.left = "0%"; + } + + function processNumber(node) { + var input = this.popupContent.getElementsByTagName('input')[0]; + if (node.specification.mandatory === true && input.value.length === 0) { + interfaceContext.lightbox.post("Error", 'This question is mandatory. Please enter a number'); + return false; + } + var enteredNumber = Number(input.value); + if (isNaN(enteredNumber)) { + interfaceContext.lightbox.post("Error", 'Please enter a valid number'); + return false; + } + if (enteredNumber < node.specification.min && node.specification.min !== null) { + interfaceContext.lightbox.post("Error", 'Number is below the minimum value of ' + node.specification.min); + return false; + } + if (enteredNumber > node.specification.max && node.specification.max !== null) { + interfaceContext.lightbox.post("Error", 'Number is above the maximum value of ' + node.specification.max); + return false; + } + node.response = input.value; + processConditional.call(this, node, node.response); + return true; + } + + function processNumberConditional(condtion, value) { + var condition = condition; + switch (condition.check) { + case "greaterThan": + if (value > Number(condition.value)) { + return true; + } + break; + case "lessThan": + if (value < Number(condition.value)) { + return true; + } + break; + case "equals": + if (value == condition.value) { + return true; + } + break; + case "contains": + console.log("Survey Element of type 'number' cannot interpret \"contains\" conditions. IGNORING"); + break; + default: + console.log("Unknown condition. IGNORING"); + break; + } + return false; + } + + function postVideo(node) { + var video = document.createElement("video"); + video.src = node.specification.url; + this.popupResponse.appendChild(video); + } + + function postYoutube(node) { + var iframe = document.createElement("iframe"); + iframe.className = "youtube"; + iframe.src = node.specification.url; + this.popupResponse.appendChild(iframe); + } + + function postSlider(node) { + var hold = document.createElement('div'); + var input = document.createElement('input'); + input.type = 'range'; + input.style.width = "90%"; + if (node.specification.min !== null) { + input.min = node.specification.min; + } + if (node.specification.max !== null) { + input.max = node.specification.max; + } + if (node.response !== undefined) { + input.value = node.response; + } + hold.className = "survey-slider-text-holder"; + var minText = document.createElement('span'); + var maxText = document.createElement('span'); + minText.textContent = node.specification.leftText; + maxText.textContent = node.specification.rightText; + hold.appendChild(minText); + hold.appendChild(maxText); + this.popupResponse.appendChild(input); + this.popupResponse.appendChild(hold); + this.popupResponse.style.textAlign = "center"; + } + + function processSlider(node) { + var input = this.popupContent.getElementsByTagName('input')[0]; + node.response = input.value; + processConditional.call(this, node, node.response); + return true; + } + + function processSliderConditional(condition, value) { + switch (condition.check) { + case "contains": + console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING"); + break; + case "greaterThan": + if (value > Number(condition.value)) { + return true; + } + break; + case "lessThan": + if (value < Number(condition.value)) { + return true; + } + break; + case "equals": + if (value == condition.value) { + return true; + } + break; + default: + console.log("Unknown condition. IGNORING"); + break; + } + return false; + } this.createPopup = function () { // Create popup window interface @@ -614,7 +1026,7 @@ }; this.showPopup = function () { - if (this.popup == null) { + if (this.popup === null) { this.createPopup(); } this.popup.style.visibility = 'visible'; @@ -637,161 +1049,24 @@ var node = this.popupOptions[this.currentIndex], converter = new showdown.Converter(), p = new DOMParser(); + lastNodeStart = new Date(); this.popupResponse.innerHTML = ""; this.popupTitle.innerHTML = ""; this.popupTitle.appendChild(p.parseFromString(converter.makeHtml(node.specification.statement), "text/html").getElementsByTagName("body")[0].firstElementChild); if (node.specification.type == 'question') { - var textArea = document.createElement('textarea'); - switch (node.specification.boxsize) { - case 'small': - textArea.cols = "20"; - textArea.rows = "1"; - break; - case 'normal': - textArea.cols = "30"; - textArea.rows = "2"; - break; - case 'large': - textArea.cols = "40"; - textArea.rows = "5"; - break; - case 'huge': - textArea.cols = "50"; - textArea.rows = "10"; - break; - } - if (node.response == undefined) { - node.response = ""; - } else { - textArea.value = node.response; - } - this.popupResponse.appendChild(textArea); - textArea.focus(); - this.popupResponse.style.textAlign = "center"; - this.popupResponse.style.left = "0%"; + postQuestion.call(this, node); } else if (node.specification.type == 'checkbox') { - if (node.response == undefined) { - node.response = Array(node.specification.options.length); - } - var index = 0; - var table = document.createElement("table"); - table.className = "popup-option-list"; - table.border = "0"; - for (var option of node.specification.options) { - var tr = document.createElement("tr"); - table.appendChild(tr); - var td = document.createElement("td"); - tr.appendChild(td); - var input = document.createElement('input'); - input.id = option.name; - input.type = 'checkbox'; - td.appendChild(input); - - td = document.createElement("td"); - tr.appendChild(td); - var span = document.createElement('span'); - span.textContent = option.text; - td.appendChild(span); - var tr = document.createElement('div'); - tr.setAttribute('name', 'option'); - tr.className = "popup-option-checbox"; - if (node.response[index] != undefined) { - if (node.response[index].checked == true) { - input.checked = "true"; - } - } - index++; - } - this.popupResponse.appendChild(table); + postCheckbox.call(this, node); } else if (node.specification.type == 'radio') { - if (node.response == undefined) { - node.response = { - name: "", - text: "" - }; - } - var index = 0; - var table = document.createElement("table"); - table.className = "popup-option-list"; - table.border = "0"; - for (var option of node.specification.options) { - var tr = document.createElement("tr"); - table.appendChild(tr); - var td = document.createElement("td"); - tr.appendChild(td); - var input = document.createElement('input'); - input.id = option.name; - input.type = 'radio'; - input.name = node.specification.id; - td.appendChild(input); - - td = document.createElement("td"); - tr.appendChild(td); - var span = document.createElement('span'); - span.textContent = option.text; - td.appendChild(span); - var tr = document.createElement('div'); - tr.setAttribute('name', 'option'); - tr.className = "popup-option-checbox"; - if (node.response[index] != undefined) { - if (node.response[index].checked == true) { - input.checked = "true"; - } - } - index++; - } - this.popupResponse.appendChild(table); + postRadio.call(this, node); } else if (node.specification.type == 'number') { - var input = document.createElement('input'); - input.type = 'textarea'; - if (node.specification.min != null) { - input.min = node.specification.min; - } - if (node.specification.max != null) { - input.max = node.specification.max; - } - if (node.specification.step != null) { - input.step = node.specification.step; - } - if (node.response != undefined) { - input.value = node.response; - } - this.popupResponse.appendChild(input); - this.popupResponse.style.textAlign = "center"; - this.popupResponse.style.left = "0%"; + postNumber.call(this, node); } else if (node.specification.type == "video") { - var video = document.createElement("video"); - video.src = node.specification.url; - this.popupResponse.appendChild(video); + postVideo.call(this, node); } else if (node.specification.type == "youtube") { - var iframe = document.createElement("iframe"); - iframe.className = "youtube"; - iframe.src = node.specification.url; - this.popupResponse.appendChild(iframe); + postYoutube.call(this, node); } else if (node.specification.type == "slider") { - var hold = document.createElement('div'); - var input = document.createElement('input'); - input.type = 'range'; - input.style.width = "90%"; - if (node.specification.min != null) { - input.min = node.specification.min; - } - if (node.specification.max != null) { - input.max = node.specification.max; - } - if (node.response != undefined) { - input.value = node.response; - } - hold.className = "survey-slider-text-holder"; - var minText = document.createElement('span'); - var maxText = document.createElement('span'); - minText.textContent = node.specification.leftText; - maxText.textContent = node.specification.rightText; - hold.appendChild(minText); - hold.appendChild(maxText); - this.popupResponse.appendChild(input); - this.popupResponse.appendChild(hold); - this.popupResponse.style.textAlign = "center"; + postSlider.call(this, node); } if (this.currentIndex + 1 == this.popupOptions.length) { if (this.node.location == "pre") { @@ -815,12 +1090,12 @@ this.popupOptions = []; this.node = node; this.store = store; - for (var opt of node.options) { + node.options.forEach(function (opt) { this.popupOptions.push({ specification: opt, response: null }); - } + }, this); this.currentIndex = 0; this.showPopup(); this.postNode(); @@ -831,294 +1106,36 @@ this.proceedClicked = function () { // Each time the popup button is clicked! - if (testState.stateIndex == 0 && specification.calibration) { + if (testState.stateIndex === 0 && specification.calibration) { interfaceContext.calibrationModuleObject.collect(); advanceState(); return; } - var node = this.popupOptions[this.currentIndex]; + var node = this.popupOptions[this.currentIndex], + pass = true, + timeDelta = (new Date() - lastNodeStart) / 1000.0; + if (timeDelta < node.specification.minWait) { + interfaceContext.lightbox.post("Error", "Not enough time has elapsed, please wait " + (node.specification.minWait - timeDelta).toFixed(0) + " seconds"); + return; + } + node.elapsedTime = timeDelta; if (node.specification.type == 'question') { // Must extract the question data - var textArea = $(popup.popupContent).find('textarea')[0]; - if (node.specification.mandatory == true && textArea.value.length == 0) { - interfaceContext.lightbox.post("Error", "This question is mandatory"); - return; - } else { - // Save the text content - console.log("Question: " + node.specification.statement); - console.log("Question Response: " + textArea.value); - node.response = textArea.value; - } - // Perform the conditional - for (var condition of node.specification.conditions) { - var pass = false; - switch (condition.check) { - case "equals": - if (textArea.value == condition.value) { - pass = true; - } - break; - case "greaterThan": - case "lessThan": - console.log("Survey Element of type 'question' cannot interpret greaterThan/lessThan conditions. IGNORING"); - break; - case "contains": - if (textArea.value.includes(condition.value)) { - pass = true; - } - break; - } - var jumpID; - if (pass) { - jumpID = condition.jumpToOnPass; - } else { - jumpID = condition.jumpToOnFail; - } - if (jumpID != undefined) { - var index = this.popupOptions.findIndex(function (item, index, element) { - if (item.specification.id == jumpID) { - return true; - } else { - return false; - } - }, this); - this.currentIndex = index - 1; - break; - } - } + pass = processQuestion.call(this, node); } else if (node.specification.type == 'checkbox') { // Must extract checkbox data - console.log("Checkbox: " + node.specification.statement); - var inputs = this.popupResponse.getElementsByTagName('input'); - node.response = []; - var numChecked = 0; - for (var i = 0; i < node.specification.options.length; i++) { - if (inputs[i].checked) { - numChecked++; - } - } - if (node.specification.min != undefined) { - if (node.specification.max == undefined) { - if (numChecked < node.specification.min) { - var msg = "You must select at least " + node.specification.min + " option"; - if (node.specification.min > 1) { - msg += "s"; - } - interfaceContext.lightbox.post("Error", msg); - return; - } - } else { - if (numChecked < node.specification.min || numChecked > node.specification.max) { - if (node.specification.min == node.specification.max) { - interfaceContext.lightbox.post("Error", "You must only select " + node.specification.min); - } else { - interfaceContext.lightbox.post("Error", "You must select between " + node.specification.min + " and " + node.specification.max); - } - return; - } - } - } - for (var i = 0; i < node.specification.options.length; i++) { - node.response.push({ - name: node.specification.options[i].name, - text: node.specification.options[i].text, - checked: inputs[i].checked - }); - console.log(node.specification.options[i].name + ": " + inputs[i].checked); - } + pass = processCheckbox.call(this, node); + } else if (node.specification.type == "radio") { // Perform the conditional - for (var condition of node.specification.conditions) { - var pass = false; - switch (condition.check) { - case "equals": - case "greaterThan": - case "lessThan": - console.log("Survey Element of type 'checkbox' cannot interpret equals/greaterThan/lessThan conditions. IGNORING"); - break; - case "contains": - for (var response of node.response) { - if (response.name == condition.value && response.checked) { - pass = true; - break; - } - } - break; - } - var jumpID; - if (pass) { - jumpID = condition.jumpToOnPass; - } else { - jumpID = condition.jumpToOnFail; - } - if (jumpID != undefined) { - var index = this.popupOptions.findIndex(function (item, index, element) { - if (item.specification.id == jumpID) { - return true; - } else { - return false; - } - }, this); - this.currentIndex = index - 1; - break; - } - } - } else if (node.specification.type == "radio") { - var optHold = this.popupResponse; - console.log("Radio: " + node.specification.statement); - node.response = null; - var i = 0; - var inputs = optHold.getElementsByTagName('input'); - while (node.response == null) { - if (i == inputs.length) { - if (node.specification.mandatory == true) { - interfaceContext.lightbox.post("Error", "Please select one option"); - return; - } - break; - } - if (inputs[i].checked == true) { - node.response = node.specification.options[i]; - console.log("Selected: " + node.specification.options[i].name); - } - i++; - } + pass = processRadio.call(this, node); + } else if (node.specification.type == "number") { // Perform the conditional - for (var condition of node.specification.conditions) { - var pass = false; - switch (condition.check) { - case "contains": - case "greaterThan": - case "lessThan": - console.log("Survey Element of type 'radio' cannot interpret contains/greaterThan/lessThan conditions. IGNORING"); - break; - case "equals": - if (node.response.name == condition.value) { - pass = true; - } - break; - } - var jumpID; - if (pass) { - jumpID = condition.jumpToOnPass; - } else { - jumpID = condition.jumpToOnFail; - } - if (jumpID != undefined) { - var index = this.popupOptions.findIndex(function (item, index, element) { - if (item.specification.id == jumpID) { - return true; - } else { - return false; - } - }, this); - this.currentIndex = index - 1; - break; - } - } - } else if (node.specification.type == "number") { - var input = this.popupContent.getElementsByTagName('input')[0]; - if (node.mandatory == true && input.value.length == 0) { - interfaceContext.lightbox.post("Error", 'This question is mandatory. Please enter a number'); - return; - } - var enteredNumber = Number(input.value); - if (isNaN(enteredNumber)) { - interfaceContext.lightbox.post("Error", 'Please enter a valid number'); - return; - } - if (enteredNumber < node.min && node.min != null) { - interfaceContext.lightbox.post("Error", 'Number is below the minimum value of ' + node.min); - return; - } - if (enteredNumber > node.max && node.max != null) { - interfaceContext.lightbox.post("Error", 'Number is above the maximum value of ' + node.max); - return; - } - node.response = input.value; - // Perform the conditional - for (var condition of node.specification.conditions) { - var pass = false; - switch (condition.check) { - case "contains": - console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING"); - break; - case "greaterThan": - if (node.response > Number(condition.value)) { - pass = true; - } - break; - case "lessThan": - if (node.response < Number(condition.value)) { - pass = true; - } - break; - case "equals": - if (node.response == condition.value) { - pass = true; - } - break; - } - var jumpID; - if (pass) { - jumpID = condition.jumpToOnPass; - } else { - jumpID = condition.jumpToOnFail; - } - if (jumpID != undefined) { - var index = this.popupOptions.findIndex(function (item, index, element) { - if (item.specification.id == jumpID) { - return true; - } else { - return false; - } - }, this); - this.currentIndex = index - 1; - break; - } - } + pass = processNumber.call(this, node); } else if (node.specification.type == 'slider') { - var input = this.popupContent.getElementsByTagName('input')[0]; - node.response = input.value; - for (var condition of node.specification.conditions) { - var pass = false; - switch (condition.check) { - case "contains": - console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING"); - break; - case "greaterThan": - if (node.response > Number(condition.value)) { - pass = true; - } - break; - case "lessThan": - if (node.response < Number(condition.value)) { - pass = true; - } - break; - case "equals": - if (node.response == condition.value) { - pass = true; - } - break; - } - var jumpID; - if (pass) { - jumpID = condition.jumpToOnPass; - } else { - jumpID = condition.jumpToOnFail; - } - if (jumpID != undefined) { - var index = this.popupOptions.findIndex(function (item, index, element) { - if (item.specification.id == jumpID) { - return true; - } else { - return false; - } - }, this); - this.currentIndex = index - 1; - break; - } - } + pass = processSlider.call(this, node); + } + if (pass === false) { + return; } this.currentIndex++; if (this.currentIndex < this.popupOptions.length) { @@ -1128,9 +1145,9 @@ this.popupTitle.innerHTML = ""; this.popupResponse.innerHTML = ""; this.hidePopup(); - for (var node of this.popupOptions) { + this.popupOptions.forEach(function (node) { this.store.postResult(node); - } + }, this); this.store.complete(); advanceState(); } @@ -1146,7 +1163,7 @@ this.resize = function (event) { // Called on window resize; - if (this.popup != null) { + if (this.popup !== null) { this.popup.style.left = (window.innerWidth / 2) - 250 + 'px'; this.popup.style.top = (window.innerHeight / 2) - 125 + 'px'; var blank = document.getElementsByClassName('testHalt')[0]; @@ -1156,16 +1173,16 @@ }; this.hideNextButton = function () { this.buttonProceed.style.visibility = "hidden"; - } + }; this.hidePreviousButton = function () { this.buttonPrevious.style.visibility = "hidden"; - } + }; this.showNextButton = function () { this.buttonProceed.style.visibility = "visible"; - } + }; this.showPreviousButton = function () { this.buttonPrevious.style.visibility = "visible"; - } + }; } function advanceState() { @@ -1175,6 +1192,21 @@ function stateMachine() { // Object prototype for tracking and managing the test state + + function pickSubPool(pool, numElements) { + // Assumes each element of pool has function "alwaysInclude" + + // First extract those excluded from picking process + var picked = []; + pool.forEach(function (e, i) { + if (e.alwaysInclude) { + picked.push(pool.splice(i, 1)[0]); + } + }); + + return picked.concat(randomSubArray(pool, numElements - picked.length)); + } + this.stateMap = []; this.preTestSurvey = null; this.postTestSurvey = null; @@ -1186,73 +1218,65 @@ // Get the data from Specification var pagePool = []; - var pageInclude = []; - for (var page of specification.pages) { - if (page.alwaysInclude) { - pageInclude.push(page); - } else { - pagePool.push(page); + specification.pages.forEach(function (page) { + if (page.position !== null || page.alwaysInclude) { + page.alwaysInclude = true; } + pagePool.push(page); + }); + if (specification.numPages > 0) { + specification.randomiseOrder = true; + pagePool = pickSubPool(pagePool, specification.numPages); } - // Find how many are left to get - var numPages = specification.poolSize; - if (numPages > pagePool.length) { - console.log("WARNING - You have specified more pages in than you have created!!"); - numPages = specification.pages.length; - } - if (specification.poolSize == 0) { - numPages = specification.pages.length; - } - numPages -= pageInclude.length; + // Now get the order of pages + var fixed = []; + pagePool.forEach(function (page) { + if (page.position !== undefined) { + fixed.push(page); + var i = pagePool.indexOf(page); + pagePool.splice(i, 1); + } + }); - if (numPages > 0) { - // Go find the rest of the pages from the pool - var subarr = null; - if (specification.randomiseOrder) { - // Append a random sub-array - subarr = randomSubArray(pagePool, numPages); - } else { - // Append the matching number - subarr = pagePool.slice(0, numPages); - } - pageInclude = pageInclude.concat(subarr); + if (specification.randomiseOrder) { + pagePool = randomiseOrder(pagePool); } - // We now have our selected pages in pageInclude array - if (specification.randomiseOrder) { - pageInclude = randomiseOrder(pageInclude); - } - for (var i = 0; i < pageInclude.length; i++) { - pageInclude[i].presentedId = i; - this.stateMap.push(pageInclude[i]); - // For each selected page, we must get the sub pool - if (pageInclude[i].poolSize != 0 && pageInclude[i].poolSize != pageInclude[i].audioElements.length) { - var elemInclude = []; - var elemPool = []; - for (var elem of pageInclude[i].audioElements) { - if (elem.alwaysInclude || elem.type != "normal") { - elemInclude.push(elem); - } else { - elemPool.push(elem); - } + // Place in the correct order + fixed.forEach(function (page) { + pagePool.splice(page.position, 0, page); + }); + + // Now process the pages + pagePool.forEach(function (page, i) { + page.presentedId = i; + this.stateMap.push(page); + var elements = page.audioElements; + if (page.poolSize > 0 || page.randomiseOrder) { + page.randomiseOrder = true; + if (page.poolSize === 0) { + page.poolSize = elements.length; } - var numElems = pageInclude[i].poolSize - elemInclude.length; - pageInclude[i].audioElements = elemInclude.concat(randomSubArray(elemPool, numElems)); + elements = pickSubPool(elements, page.poolSize); } - storage.createTestPageStore(pageInclude[i]); - audioEngineContext.loadPageData(pageInclude[i]); - } + if (page.randomiseOrder) { + elements = randomiseOrder(elements); + } + page.audioElements = elements; + storage.createTestPageStore(page); + audioEngineContext.loadPageData(page); + }, this); - if (specification.preTest != null) { + if (specification.preTest !== null) { this.preTestSurvey = specification.preTest; } - if (specification.postTest != null) { + if (specification.postTest !== null) { this.postTestSurvey = specification.postTest; } if (this.stateMap.length > 0) { - if (this.stateIndex != null) { + if (this.stateIndex !== null) { console.log('NOTE - State already initialise'); } this.stateIndex = -2; @@ -1262,7 +1286,7 @@ } }; this.advanceState = function () { - if (this.stateIndex == null) { + if (this.stateIndex === null) { this.initialise(); } if (this.stateIndex > -2) { @@ -1270,7 +1294,7 @@ } if (this.stateIndex == -2) { this.stateIndex++; - if (this.preTestSurvey != null) { + if (this.preTestSurvey !== undefined) { popup.initState(this.preTestSurvey, storage.globalPreTest); } else { this.advanceState(); @@ -1290,7 +1314,7 @@ // All test pages complete, post test console.log('Ending test ...'); this.stateIndex++; - if (this.postTestSurvey == null) { + if (this.postTestSurvey === undefined) { this.advanceState(); } else { popup.initState(this.postTestSurvey, storage.globalPostTest); @@ -1299,18 +1323,19 @@ createProjectSave(specification.projectReturn); } else { popup.hidePopup(); - if (this.currentStateMap == null) { + if (this.currentStateMap === null) { this.currentStateMap = this.stateMap[this.stateIndex]; // Find and extract the outside reference var elements = [], ref = []; - var elem; - while (elem = this.currentStateMap.audioElements.pop()) { + var elem = this.currentStateMap.audioElements.pop(); + while (elem) { if (elem.type == "outside-reference") { ref.push(elem); } else { elements.push(elem); } + elem = this.currentStateMap.audioElements.pop(); } elements = elements.reverse(); if (this.currentStateMap.randomiseOrder) { @@ -1319,7 +1344,7 @@ this.currentStateMap.audioElements = elements.concat(ref); this.currentStore = storage.testPages[this.stateIndex]; - if (this.currentStateMap.preTest != null) { + if (this.currentStateMap.preTest !== undefined) { this.currentStatePosition = 'pre'; popup.initState(this.currentStateMap.preTest, storage.testPages[this.stateIndex].preTest); } else { @@ -1336,7 +1361,7 @@ this.currentStatePosition = 'post'; // Save the data this.testPageCompleted(); - if (this.currentStateMap.postTest == null) { + if (this.currentStateMap.postTest === undefined) { this.advanceState(); return; } else { @@ -1348,7 +1373,7 @@ this.currentStateMap = null; this.advanceState(); break; - }; + } } }; @@ -1366,12 +1391,12 @@ } var audioObjects = audioEngineContext.audioObjects; - for (var ao of audioEngineContext.audioObjects) { + audioEngineContext.audioObjects.forEach(function (ao) { ao.exportXMLDOM(); - } - for (var element of interfaceContext.commentQuestions) { + }); + interfaceContext.commentQuestions.forEach(function (element) { element.exportXMLDOM(storePoint); - } + }); pageXMLSave(storePoint.XMLDOM, this.currentStateMap); storePoint.complete(); }; @@ -1382,14 +1407,14 @@ } else { return null; } - } + }; this.getCurrentTestPageStore = function () { if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) { return this.currentStore; } else { return null; } - } + }; } function AudioEngine(specification) { @@ -1444,7 +1469,7 @@ } for (var i = 0; i < this.users.length; i++) { this.users[i].state = 1; - if (this.users[i].interfaceDOM != null) { + if (this.users[i].interfaceDOM !== null) { this.users[i].bufferLoaded(this); } } @@ -1472,7 +1497,7 @@ } } return false; - } + }; this.getMedia = function () { var self = this; var currentUrlIndex = 0; @@ -1516,7 +1541,7 @@ return true; }, function (e) { var waveObj = new WAVE(); - if (waveObj.open(response) == 0) { + if (waveObj.open(response) === 0) { self.buffer = audioContext.createBuffer(waveObj.num_channels, waveObj.num_samples, waveObj.sample_rate); for (var c = 0; c < waveObj.num_channels; c++) { var buffer_ptr = self.buffer.getChannelData(c); @@ -1524,14 +1549,13 @@ buffer_ptr[n] = waveObj.decoded_data[c][n]; } } - - delete waveObj; } - if (self.buffer != undefined) { + if (self.buffer !== undefined) { self.status = 2; calculateLoudness(self, "I"); return true; } + waveObj = undefined; return false; }); } @@ -1541,7 +1565,7 @@ this.status = -1; for (var i = 0; i < this.users.length; i++) { this.users[i].state = -1; - if (this.users[i].interfaceDOM != null) { + if (this.users[i].interfaceDOM !== null) { this.users[i].bufferLoaded(this); } } @@ -1552,14 +1576,14 @@ if (event.lengthComputable) { this.progress = event.loaded / event.total; for (var i = 0; i < this.users.length; i++) { - if (this.users[i].interfaceDOM != null) { + if (this.users[i].interfaceDOM !== null) { if (typeof this.users[i].interfaceDOM.updateLoading === "function") { this.users[i].interfaceDOM.updateLoading(this.progress * 100); } } } } - }; + } this.progress = 0; this.status = 1; @@ -1570,11 +1594,11 @@ this.registerAudioObject = function (audioObject) { // Called by an audioObject to register to the buffer for use // First check if already in the register pool - for (var objects of this.users) { - if (audioObject.id == objects.id) { + this.users.forEach(function (object) { + if (audioObject.id == object.id) { return 0; } - } + }); this.users.push(audioObject); if (this.status == 3 || this.status == -1) { // The buffer is already ready, trigger bufferLoaded @@ -1584,23 +1608,24 @@ this.copyBuffer = function (preSilenceTime, postSilenceTime) { // Copies the entire bufferObj. - if (preSilenceTime == undefined) { + if (preSilenceTime === undefined) { preSilenceTime = 0; } - if (postSilenceTime == undefined) { + if (postSilenceTime === undefined) { postSilenceTime = 0; } var preSilenceSamples = secondsToSamples(preSilenceTime, this.buffer.sampleRate); var postSilenceSamples = secondsToSamples(postSilenceTime, this.buffer.sampleRate); var newLength = this.buffer.length + preSilenceSamples + postSilenceSamples; var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate); + var c; // Now we can use some efficient background copy schemes if we are just padding the end - if (preSilenceSamples == 0 && typeof copybuffer.copyToChannel == "function") { - for (var c = 0; c < this.buffer.numberOfChannels; c++) { + if (preSilenceSamples === 0 && typeof copybuffer.copyToChannel === "function") { + for (c = 0; c < this.buffer.numberOfChannels; c++) { copybuffer.copyToChannel(this.buffer.getChannelData(c), c); } } else { - for (var c = 0; c < this.buffer.numberOfChannels; c++) { + for (c = 0; c < this.buffer.numberOfChannels; c++) { var src = this.buffer.getChannelData(c); var dst = copybuffer.getChannelData(c); for (var n = 0; n < src.length; n++) @@ -1611,7 +1636,7 @@ copybuffer.lufs = this.buffer.lufs; copybuffer.playbackGain = this.buffer.playbackGain; return copybuffer; - } + }; this.cropBuffer = function (startTime, stopTime) { // Copy and return the cropped buffer @@ -1632,21 +1657,17 @@ } } return copybuffer; - } + }; }; this.loadPageData = function (page) { // Load the URL from pages - for (var element of page.audioElements) { + function loadAudioElementData(element) { var URL = page.hostURL + element.url; - var buffer = null; - for (var buffObj of this.buffers) { - if (buffObj.hasUrl(URL)) { - buffer = buffObj; - break; - } - } - if (buffer == null) { + var buffer = this.buffers.find(function (buffObj) { + return buffObj.hasUrl(URL); + }); + if (buffer === undefined) { buffer = new this.bufferObj(); var urls = [{ url: URL, @@ -1663,44 +1684,56 @@ this.buffers.push(buffer); } } + page.audioElements.forEach(loadAudioElementData, this); }; + function playNormal(id) { + var playTime = audioContext.currentTime + 0.1; + var stopTime = playTime + specification.crossFade; + this.audioObjects.forEach(function (ao) { + if (ao.id === id) { + ao.play(playTime); + } else { + ao.stop(stopTime); + } + }); + } + + function playLoopSync(id) { + var playTime = audioContext.currentTime + 0.1; + var stopTime = playTime + specification.crossFade; + this.audioObjects.forEach(function (ao) { + ao.play(playTime); + if (ao.id === id) { + ao.loopStart(playTime); + } else { + ao.loopStop(stopTime); + } + }); + } + this.play = function (id) { // Start the timer and set the audioEngine state to playing (1) - if (this.status == 0) { - // Check if all audioObjects are ready - this.bufferReady(id); - } else { - this.status = 1; + if (typeof id !== "number" || id < 0 || id > this.audioObjects.length) { + throw ('FATAL - Passed id was undefined - AudioEngineContext.play(id)'); } - if (this.status == 1) { + var maxPlays = this.audioObjects[id].specification.maxNumberPlays || this.audioObjects[id].specification.parent.maxNumberPlays || specification.maxNumberPlays; + if (maxPlays !== undefined && this.audioObjects[id].numberOfPlays >= maxPlays) { + interfaceContext.lightbox.post("Error", "Cannot play this fragment more than " + maxPlays + " times"); + return; + } + if (this.status === 1) { this.timer.startTest(); - if (id == undefined) { - id = -1; - console.error('FATAL - Passed id was undefined - AudioEngineContext.play(id)'); - return; - } else { - interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]); - } - var setTime = audioContext.currentTime; + interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]); if (this.synchPlayback && this.loopPlayback) { // Traditional looped playback - for (var i = 0; i < this.audioObjects.length; i++) { - this.audioObjects[i].play(audioContext.currentTime); - if (id == i) { - this.audioObjects[i].loopStart(setTime); - } else { - this.audioObjects[i].loopStop(setTime + specification.crossFade); - } + playLoopSync.call(this, id); + } else { + if (this.bufferReady(id) === false) { + console.log("Cannot play. Buffer not ready"); + return; } - } else { - for (var i = 0; i < this.audioObjects.length; i++) { - if (i != id) { - this.audioObjects[i].stop(setTime + specification.crossFade); - } else if (i == id) { - this.audioObjects[id].play(setTime); - } - } + playNormal.call(this, id); } interfaceContext.playhead.start(); } @@ -1710,9 +1743,9 @@ // Send stop and reset command to all playback buffers if (this.status == 1) { var setTime = audioContext.currentTime + 0.1; - for (var i = 0; i < this.audioObjects.length; i++) { - this.audioObjects[i].stop(setTime); - } + this.audioObjects.forEach(function (a) { + a.stop(setTime); + }); interfaceContext.playhead.stop(); } }; @@ -1722,19 +1755,15 @@ // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin' // Create the audioObject with ID of the new track length; - audioObjectId = this.audioObjects.length; + var audioObjectId = this.audioObjects.length; this.audioObjects[audioObjectId] = new audioObject(audioObjectId); // Check if audioObject buffer is currently stored by full URL var URL = testState.currentStateMap.hostURL + element.url; - var buffer = null; - for (var i = 0; i < this.buffers.length; i++) { - if (this.buffers[i].hasUrl(URL)) { - buffer = this.buffers[i]; - break; - } - } - if (buffer == null) { + var buffer = this.buffers.find(function (buffObj) { + return buffObj.hasUrl(URL); + }); + if (buffer === undefined) { console.log("[WARN]: Buffer was not loaded in pre-test! " + URL); buffer = new this.bufferObj(); this.buffers.push(buffer); @@ -1760,9 +1789,9 @@ this.status = 0; this.audioObjectsReady = false; this.metric.reset(); - for (var i = 0; i < this.buffers.length; i++) { - this.buffers[i].users = []; - } + this.buffers.forEach(function (buffer) { + buffer.users = []; + }); this.audioObjects = []; this.timer = new timer(); this.loopPlayback = audioHolderObject.loop; @@ -1770,9 +1799,9 @@ }; this.checkAllPlayed = function () { - arr = []; + var arr = []; for (var id = 0; id < this.audioObjects.length; id++) { - if (this.audioObjects[id].metric.wasListenedTo == false) { + if (this.audioObjects[id].metric.wasListenedTo === false) { arr.push(this.audioObjects[id].id); } } @@ -1782,11 +1811,11 @@ this.checkAllReady = function () { var ready = true; for (var i = 0; i < this.audioObjects.length; i++) { - if (this.audioObjects[i].state == 0) { + if (this.audioObjects[i].state === 0) { // Track not ready console.log('WAIT -- audioObject ' + i + ' not ready yet!'); ready = false; - }; + } } return ready; }; @@ -1803,11 +1832,11 @@ } } // Extract the audio and zero-pad - for (var ao of this.audioObjects) { + this.audioObjects.forEach(function (ao) { if (ao.buffer.buffer.duration !== duration) { ao.buffer.buffer = ao.buffer.copyBuffer(0, duration - ao.buffer.buffer.duration); } - } + }); }; this.bufferReady = function (id) { @@ -1819,10 +1848,6 @@ return true; } return false; - } - - this.exportXML = function () { - }; } @@ -1830,7 +1855,9 @@ function audioObject(id) { // The main buffer object with common control nodes to the AudioEngine - this.specification; + var playCounter = 0; + + this.specification = undefined; this.id = id; this.state = 0; // 0 - no data, 1 - ready this.url = null; // Hold the URL given for the output back to the results. @@ -1853,7 +1880,7 @@ // the audiobuffer is not designed for multi-start playback // When stopeed, the buffer node is deleted and recreated with the stored buffer. - this.buffer; + this.buffer = undefined; this.bufferLoaded = function (callee) { // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the @@ -1861,7 +1888,7 @@ if (callee.status == -1) { // ERROR this.state = -1; - if (this.interfaceDOM != null) { + if (this.interfaceDOM !== null) { this.interfaceDOM.error(); } this.buffer = callee; @@ -1875,7 +1902,7 @@ var copybuffer = new callee.constructor(); copybuffer.buffer = callee.cropBuffer(startTime || 0, stopTime || callee.buffer.duration); - if (preSilenceTime != 0 || postSilenceTime != 0) { + if (preSilenceTime !== 0 || postSilenceTime !== 0) { copybuffer.buffer = copybuffer.copyBuffer(preSilenceTime, postSilenceTime); } @@ -1888,7 +1915,7 @@ } else { this.buffer.buffer.playbackGain = 1.0; } - if (this.interfaceDOM != null) { + if (this.interfaceDOM !== null) { this.interfaceDOM.enable(); } this.onplayGain = decibelToLinear(this.specification.gain) * (this.buffer.buffer.playbackGain || 1.0); @@ -1917,7 +1944,7 @@ }; this.loopStop = function (setTime) { - if (this.outputGain.gain.value != 0.0) { + if (this.outputGain.gain.value !== 0.0) { this.outputGain.gain.linearRampToValueAtTime(0.0, setTime); this.metric.stopListening(audioEngineContext.timer.getTestTime()); } @@ -1925,7 +1952,8 @@ }; this.play = function (startTime) { - if (this.bufferNode == undefined && this.buffer.buffer != undefined) { + if (this.bufferNode === undefined && this.buffer.buffer !== undefined) { + playCounter++; this.bufferNode = audioContext.createBufferSource(); this.bufferNode.owner = this; this.bufferNode.connect(this.outputGain); @@ -1934,7 +1962,7 @@ this.bufferNode.onended = function (event) { // Safari does not like using 'this' to reference the calling object! //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition()); - if (event.currentTarget != null) { + if (event.currentTarget !== null) { event.currentTarget.owner.stop(audioContext.currentTime + 1); } }; @@ -1959,7 +1987,7 @@ this.stop = function (stopTime) { this.outputGain.gain.cancelScheduledValues(audioContext.currentTime); - if (this.bufferNode != undefined) { + if (this.bufferNode !== undefined) { this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition()); this.bufferNode.stop(stopTime); this.bufferNode = undefined; @@ -1970,7 +1998,7 @@ this.getCurrentPosition = function () { var time = audioEngineContext.timer.getTestTime(); - if (this.bufferNode != undefined) { + if (this.bufferNode !== undefined) { var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration; if (isNaN(position)) { return 0; @@ -1990,8 +2018,8 @@ this.storeDOM.appendChild(file); if (this.specification.type != 'outside-reference') { var interfaceXML = this.interfaceDOM.exportXMLDOM(this); - if (interfaceXML != null) { - if (interfaceXML.length == undefined) { + if (interfaceXML !== null) { + if (interfaceXML.length === undefined) { this.storeDOM.appendChild(interfaceXML); } else { for (var i = 0; i < interfaceXML.length; i++) { @@ -1999,16 +2027,23 @@ } } } - if (this.commentDOM != null) { + if (this.commentDOM !== null) { this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this)); } } - var nodes = this.metric.exportXMLDOM(); - var mroot = this.storeDOM.getElementsByTagName('metric')[0]; - for (var i = 0; i < nodes.length; i++) { - mroot.appendChild(nodes[i]); + this.metric.exportXMLDOM(this.storeDOM.getElementsByTagName('metric')[0]); + }; + + Object.defineProperties(this, { + "numberOfPlays": { + 'get': function () { + return playCounter; + }, + 'set': function () { + return playCounter; + } } - }; + }); } function timer() { @@ -2020,7 +2055,7 @@ this.testDuration = 0; this.minimumTestTime = 0; // No minimum test time this.startTest = function () { - if (this.testStarted == false) { + if (this.testStarted === false) { this.testStartTime = audioContext.currentTime; this.testStarted = true; this.updateTestTime(); @@ -2128,7 +2163,7 @@ }; this.startListening = function (time) { - if (this.listenHold == false) { + if (this.listenHold === false) { this.wasListenedTo = true; this.listenStart = time; this.listenHold = true; @@ -2147,7 +2182,7 @@ }; this.stopListening = function (time, bufferStopTime) { - if (this.listenHold == true) { + if (this.listenHold === true) { var diff = time - this.listenStart; this.listenedTimer += (diff); this.listenStart = 0; @@ -2157,7 +2192,7 @@ var testTime = evnt.getElementsByTagName('testTime')[0]; var bufferTime = evnt.getElementsByTagName('bufferTime')[0]; testTime.setAttribute('stop', time); - if (bufferStopTime == undefined) { + if (bufferStopTime === undefined) { bufferTime.setAttribute('stop', this.parent.getCurrentPosition()); } else { bufferTime.setAttribute('stop', bufferStopTime); @@ -2166,64 +2201,99 @@ } }; - this.exportXMLDOM = function () { - var storeDOM = []; + function exportElementTimer(parentElement) { + var mElementTimer = storage.document.createElement('metricresult'); + mElementTimer.setAttribute('name', 'enableElementTimer'); + mElementTimer.textContent = this.listenedTimer; + parentElement.appendChild(mElementTimer); + return mElementTimer; + } + + function exportElementTrack(parentElement) { + var elementTrackerFull = storage.document.createElement('metricresult'); + elementTrackerFull.setAttribute('name', 'elementTrackerFull'); + for (var k = 0; k < this.movementTracker.length; k++) { + var timePos = storage.document.createElement('movement'); + timePos.setAttribute("time", this.movementTracker[k][0]); + timePos.setAttribute("value", this.movementTracker[k][1]); + elementTrackerFull.appendChild(timePos); + } + parentElement.appendChild(elementTrackerFull); + return elementTrackerFull; + } + + function exportElementListenTracker(parentElement) { + var elementListenTracker = storage.document.createElement('metricresult'); + elementListenTracker.setAttribute('name', 'elementListenTracker'); + for (var k = 0; k < this.listenTracker.length; k++) { + elementListenTracker.appendChild(this.listenTracker[k]); + } + parentElement.appendChild(elementListenTracker); + return elementListenTracker; + } + + function exportElementInitialPosition(parentElement) { + var elementInitial = storage.document.createElement('metricresult'); + elementInitial.setAttribute('name', 'elementInitialPosition'); + elementInitial.textContent = this.initialPosition; + parentElement.appendChild(elementInitial); + return elementInitial; + } + + function exportFlagListenedTo(parentElement) { + var flagListenedTo = storage.document.createElement('metricresult'); + flagListenedTo.setAttribute('name', 'elementFlagListenedTo'); + flagListenedTo.textContent = this.wasListenedTo; + parentElement.appendChild(flagListenedTo); + return flagListenedTo; + } + + function exportFlagMoved(parentElement) { + var flagMoved = storage.document.createElement('metricresult'); + flagMoved.setAttribute('name', 'elementFlagMoved'); + flagMoved.textContent = this.wasMoved; + parentElement.appendChild(flagMoved); + return flagMoved; + } + + function exportFlagComments(parentElement) { + var flagComments = storage.document.createElement('metricresult'); + flagComments.setAttribute('name', 'elementFlagComments'); + if (this.parent.commentDOM === null) { + flagComments.textContent = 'false'; + } else if (this.parent.commentDOM.textContent.length === 0) { + flagComments.textContent = 'false'; + } else { + flagComments.textContet = 'true'; + } + parentElement.appendChild(flagComments); + return flagComments; + } + + this.exportXMLDOM = function (parentElement) { + var elems = []; if (audioEngineContext.metric.enableElementTimer) { - var mElementTimer = storage.document.createElement('metricresult'); - mElementTimer.setAttribute('name', 'enableElementTimer'); - mElementTimer.textContent = this.listenedTimer; - storeDOM.push(mElementTimer); + elems.push(exportElementTimer.call(this, parentElement)); } if (audioEngineContext.metric.enableElementTracker) { - var elementTrackerFull = storage.document.createElement('metricresult'); - elementTrackerFull.setAttribute('name', 'elementTrackerFull'); - for (var k = 0; k < this.movementTracker.length; k++) { - var timePos = storage.document.createElement('movement'); - timePos.setAttribute("time", this.movementTracker[k][0]); - timePos.setAttribute("value", this.movementTracker[k][1]); - elementTrackerFull.appendChild(timePos); - } - storeDOM.push(elementTrackerFull); + elems.push(exportElementTrack.call(this, parentElement)); } if (audioEngineContext.metric.enableElementListenTracker) { - var elementListenTracker = storage.document.createElement('metricresult'); - elementListenTracker.setAttribute('name', 'elementListenTracker'); - for (var k = 0; k < this.listenTracker.length; k++) { - elementListenTracker.appendChild(this.listenTracker[k]); - } - storeDOM.push(elementListenTracker); + elems.push(exportElementListenTracker.call(this, parentElement)); } if (audioEngineContext.metric.enableElementInitialPosition) { - var elementInitial = storage.document.createElement('metricresult'); - elementInitial.setAttribute('name', 'elementInitialPosition'); - elementInitial.textContent = this.initialPosition; - storeDOM.push(elementInitial); + elems.push(exportElementInitialPosition.call(this, parentElement)); } if (audioEngineContext.metric.enableFlagListenedTo) { - var flagListenedTo = storage.document.createElement('metricresult'); - flagListenedTo.setAttribute('name', 'elementFlagListenedTo'); - flagListenedTo.textContent = this.wasListenedTo; - storeDOM.push(flagListenedTo); + elems.push(exportFlagListenedTo.call(this, parentElement)); } if (audioEngineContext.metric.enableFlagMoved) { - var flagMoved = storage.document.createElement('metricresult'); - flagMoved.setAttribute('name', 'elementFlagMoved'); - flagMoved.textContent = this.wasMoved; - storeDOM.push(flagMoved); + elems.push(exportFlagMoved.call(this, parentElement)); } if (audioEngineContext.metric.enableFlagComments) { - var flagComments = storage.document.createElement('metricresult'); - flagComments.setAttribute('name', 'elementFlagComments'); - if (this.parent.commentDOM == null) { - flag.textContent = 'false'; - } else if (this.parent.commentDOM.textContent.length == 0) { - flag.textContent = 'false'; - } else { - flag.textContet = 'true'; - } - storeDOM.push(flagComments); + elems.push(exportFlagComments.call(this, parentElement)); } - return storeDOM; + return elems; }; } @@ -2249,12 +2319,12 @@ popup.resize(event); this.volume.resize(); this.lightbox.resize(); - for (var i = 0; i < this.commentBoxes.length; i++) { - this.commentBoxes[i].resize(); - } - for (var i = 0; i < this.commentQuestions.length; i++) { - this.commentQuestions[i].resize(); - } + this.commentBoxes.boxes.forEach(function (elem) { + elem.resize(); + }); + this.commentQuestions.forEach(function (elem) { + elem.resize(); + }); try { resizeWindow(event); } catch (err) { @@ -2303,7 +2373,7 @@ hold.appendChild(time); return hold; - } + }; this.lightbox = { parent: this, @@ -2345,7 +2415,7 @@ resize: function (event) { this.root.style.left = (window.innerWidth / 2) - 250 + 'px'; } - } + }; this.lightbox.root.appendChild(this.lightbox.content); this.lightbox.root.appendChild(this.lightbox.accept); @@ -2361,10 +2431,11 @@ document.getElementsByTagName("body")[0].appendChild(this.lightbox.root); document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker); - this.commentBoxes = new function () { - this.boxes = []; - this.injectPoint = null; - this.elementCommentBox = function (audioObject) { + this.commentBoxes = (function () { + var commentBoxes = {}; + commentBoxes.boxes = []; + commentBoxes.injectPoint = null; + commentBoxes.elementCommentBox = function (audioObject) { var element = audioObject.specification; this.audioObject = audioObject; this.id = audioObject.id; @@ -2410,39 +2481,60 @@ this.trackCommentBox.style.width = boxwidth - 6 + "px"; }; this.resize(); + this.highlight = function (state) { + if (state === true) { + $(this.trackComment).addClass("comment-box-playing"); + } else { + $(this.trackComment).removeClass("comment-box-playing"); + } + }; }; - this.createCommentBox = function (audioObject) { + commentBoxes.createCommentBox = function (audioObject) { var node = new this.elementCommentBox(audioObject); this.boxes.push(node); audioObject.commentDOM = node; return node; }; - this.sortCommentBoxes = function () { + commentBoxes.sortCommentBoxes = function () { this.boxes.sort(function (a, b) { return a.id - b.id; }); }; - this.showCommentBoxes = function (inject, sort) { + commentBoxes.showCommentBoxes = function (inject, sort) { this.injectPoint = inject; if (sort) { this.sortCommentBoxes(); } - for (var box of this.boxes) { + this.boxes.forEach(function (box) { inject.appendChild(box.trackComment); - } + }); }; - this.deleteCommentBoxes = function () { - if (this.injectPoint != null) { - for (var box of this.boxes) { + commentBoxes.deleteCommentBoxes = function () { + if (this.injectPoint !== null) { + this.boxes.forEach(function (box) { this.injectPoint.removeChild(box.trackComment); - } + }, this); this.injectPoint = null; } this.boxes = []; }; - } + commentBoxes.highlightById = function (id) { + if (id === undefined || typeof id !== "number" || id >= this.boxes.length) { + console.log("Error - Invalid id"); + id = -1; + } + this.boxes.forEach(function (a) { + if (a.id === id) { + a.highlight(true); + } else { + a.highlight(false); + } + }); + }; + return commentBoxes; + })(); this.commentQuestions = []; @@ -2501,47 +2593,32 @@ // Create a string next to each comment asking for a comment this.string = document.createElement('span'); this.string.innerHTML = commentQuestion.statement; - var br = document.createElement('br'); // Add to the holder. this.holder.appendChild(this.string); - this.holder.appendChild(br); this.options = []; this.inputs = document.createElement('div'); - this.span = document.createElement('div'); - this.inputs.align = 'center'; - this.inputs.style.marginLeft = '12px'; - this.inputs.className = "comment-radio-inputs-holder"; - this.span.style.marginLeft = '12px'; - this.span.align = 'center'; - this.span.style.marginTop = '15px'; - this.span.className = "comment-radio-span-holder"; + this.inputs.className = "comment-checkbox-inputs-holder"; var optCount = commentQuestion.options.length; - for (var optNode of commentQuestion.options) { + for (var i = 0; i < optCount; i++) { var div = document.createElement('div'); - div.style.width = '80px'; - div.style.float = 'left'; + div.className = "comment-checkbox-inputs-flex"; + + var span = document.createElement('span'); + span.textContent = commentQuestion.options[i].text; + span.className = 'comment-radio-span'; + div.appendChild(span); + var input = document.createElement('input'); input.type = 'radio'; input.name = commentQuestion.id; - input.setAttribute('setvalue', optNode.name); + input.setAttribute('setvalue', commentQuestion.options[i].name); input.className = 'comment-radio'; div.appendChild(input); + this.inputs.appendChild(div); - - - div = document.createElement('div'); - div.style.width = '80px'; - div.style.float = 'left'; - div.align = 'center'; - var span = document.createElement('span'); - span.textContent = optNode.text; - span.className = 'comment-radio-span'; - div.appendChild(span); - this.span.appendChild(div); this.options.push(input); } - this.holder.appendChild(this.span); this.holder.appendChild(this.inputs); this.exportXMLDOM = function (storePoint) { @@ -2552,7 +2629,7 @@ question.textContent = this.string.textContent; var response = document.createElement('response'); var i = 0; - while (this.options[i].checked == false) { + while (this.options[i].checked === false) { i++; if (i >= this.options.length) { break; @@ -2579,24 +2656,6 @@ boxwidth = 400; } this.holder.style.width = boxwidth + "px"; - var text = this.holder.getElementsByClassName("comment-radio-span-holder")[0]; - var options = this.holder.getElementsByClassName("comment-radio-inputs-holder")[0]; - var optCount = options.childElementCount; - var spanMargin = Math.floor(((boxwidth - 20 - (optCount * 80)) / (optCount)) / 2) + 'px'; - var options = options.firstChild; - var text = text.firstChild; - options.style.marginRight = spanMargin; - options.style.marginLeft = spanMargin; - text.style.marginRight = spanMargin; - text.style.marginLeft = spanMargin; - while (options.nextSibling != undefined) { - options = options.nextSibling; - text = text.nextSibling; - options.style.marginRight = spanMargin; - options.style.marginLeft = spanMargin; - text.style.marginRight = spanMargin; - text.style.marginLeft = spanMargin; - } }; this.resize(); }; @@ -2609,47 +2668,32 @@ // Create a string next to each comment asking for a comment this.string = document.createElement('span'); this.string.innerHTML = commentQuestion.statement; - var br = document.createElement('br'); // Add to the holder. this.holder.appendChild(this.string); - this.holder.appendChild(br); this.options = []; this.inputs = document.createElement('div'); - this.span = document.createElement('div'); - this.inputs.align = 'center'; - this.inputs.style.marginLeft = '12px'; this.inputs.className = "comment-checkbox-inputs-holder"; - this.span.style.marginLeft = '12px'; - this.span.align = 'center'; - this.span.style.marginTop = '15px'; - this.span.className = "comment-checkbox-span-holder"; var optCount = commentQuestion.options.length; for (var i = 0; i < optCount; i++) { var div = document.createElement('div'); - div.style.width = '80px'; - div.style.float = 'left'; + div.className = "comment-checkbox-inputs-flex"; + + var span = document.createElement('span'); + span.textContent = commentQuestion.options[i].text; + span.className = 'comment-radio-span'; + div.appendChild(span); + var input = document.createElement('input'); input.type = 'checkbox'; input.name = commentQuestion.id; input.setAttribute('setvalue', commentQuestion.options[i].name); input.className = 'comment-radio'; div.appendChild(input); + this.inputs.appendChild(div); - - - div = document.createElement('div'); - div.style.width = '80px'; - div.style.float = 'left'; - div.align = 'center'; - var span = document.createElement('span'); - span.textContent = commentQuestion.options[i].text; - span.className = 'comment-radio-span'; - div.appendChild(span); - this.span.appendChild(div); this.options.push(input); } - this.holder.appendChild(this.span); this.holder.appendChild(this.inputs); this.exportXMLDOM = function (storePoint) { @@ -2678,24 +2722,6 @@ boxwidth = 400; } this.holder.style.width = boxwidth + "px"; - var text = this.holder.getElementsByClassName("comment-checkbox-span-holder")[0]; - var options = this.holder.getElementsByClassName("comment-checkbox-inputs-holder")[0]; - var optCount = options.childElementCount; - var spanMargin = Math.floor(((boxwidth - 20 - (optCount * 80)) / (optCount)) / 2) + 'px'; - var options = options.firstChild; - var text = text.firstChild; - options.style.marginRight = spanMargin; - options.style.marginLeft = spanMargin; - text.style.marginRight = spanMargin; - text.style.marginLeft = spanMargin; - while (options.nextSibling != undefined) { - options = options.nextSibling; - text = text.nextSibling; - options.style.marginRight = spanMargin; - options.style.marginLeft = spanMargin; - text.style.marginRight = spanMargin; - text.style.marginLeft = spanMargin; - } }; this.resize(); }; @@ -2783,10 +2809,10 @@ this.outsideReferenceHolder.setAttribute('track-id', index); this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference"; this.outsideReferenceHolder.disabled = true; - - this.outsideReferenceHolder.onclick = function (event) { - audioEngineContext.play(event.currentTarget.getAttribute('track-id')); + this.handleEvent = function (event) { + audioEngineContext.play(this.parent.id); }; + this.outsideReferenceHolder.addEventListener("click", this); inject.appendChild(this.outsideReferenceHolder); this.enable = function () { if (this.parent.state == 1) { @@ -2828,33 +2854,34 @@ // audioObject has an error!! this.outsideReferenceHolder.textContent = "Error"; this.outsideReferenceHolder.style.backgroundColor = "#F00"; - } - } + }; + }; - this.playhead = new function () { - this.object = document.createElement('div'); - this.object.className = 'playhead'; - this.object.align = 'left'; + this.playhead = (function () { + var playhead = {}; + playhead.object = document.createElement('div'); + playhead.object.className = 'playhead'; + playhead.object.align = 'left'; var curTime = document.createElement('div'); curTime.style.width = '50px'; - this.curTimeSpan = document.createElement('span'); - this.curTimeSpan.textContent = '00:00'; - curTime.appendChild(this.curTimeSpan); - this.object.appendChild(curTime); - this.scrubberTrack = document.createElement('div'); - this.scrubberTrack.className = 'playhead-scrub-track'; + playhead.curTimeSpan = document.createElement('span'); + playhead.curTimeSpan.textContent = '00:00'; + curTime.appendChild(playhead.curTimeSpan); + playhead.object.appendChild(curTime); + playhead.scrubberTrack = document.createElement('div'); + playhead.scrubberTrack.className = 'playhead-scrub-track'; - this.scrubberHead = document.createElement('div'); - this.scrubberHead.id = 'playhead-scrubber'; - this.scrubberTrack.appendChild(this.scrubberHead); - this.object.appendChild(this.scrubberTrack); + playhead.scrubberHead = document.createElement('div'); + playhead.scrubberHead.id = 'playhead-scrubber'; + playhead.scrubberTrack.appendChild(playhead.scrubberHead); + playhead.object.appendChild(playhead.scrubberTrack); - this.timePerPixel = 0; - this.maxTime = 0; + playhead.timePerPixel = 0; + playhead.maxTime = 0; - this.playbackObject; + playhead.playbackObject = undefined; - this.setTimePerPixel = function (audioObject) { + playhead.setTimePerPixel = function (audioObject) { //maxTime must be in seconds this.playbackObject = audioObject; this.maxTime = audioObject.buffer.buffer.duration; @@ -2867,7 +2894,7 @@ } }; - this.update = function () { + playhead.update = function () { // Update the playhead position, startPlay must be called if (this.timePerPixel > 0) { var time = this.playbackObject.getCurrentPosition(); @@ -2895,66 +2922,65 @@ } } } + if (this.playbackObject !== undefined && this.interval === undefined) { + window.requestAnimationFrame(this.update.bind(this)); + } }; - this.interval = undefined; + playhead.interval = undefined; - this.start = function () { - if (this.playbackObject != undefined && this.interval == undefined) { - if (this.maxTime < 60) { - this.interval = setInterval(function () { - interfaceContext.playhead.update(); - }, 10); - } else { - this.interval = setInterval(function () { - interfaceContext.playhead.update(); - }, 100); - } + playhead.start = function () { + if (this.playbackObject !== undefined && this.interval === undefined) { + window.requestAnimationFrame(this.update.bind(this)); } }; - this.stop = function () { - clearInterval(this.interval); - this.interval = undefined; - this.scrubberHead.style.left = '0px'; - if (this.maxTime < 60) { - this.curTimeSpan.textContent = '0.00'; - } else { - this.curTimeSpan.textContent = '00:00'; - } + playhead.stop = function () { + this.timePerPixel = 0; }; - }; + return playhead; + })(); - this.volume = new function () { + this.volume = (function () { // An in-built volume module which can be viewed on page // Includes trackers on page-by-page data // Volume does NOT reset to 0dB on each page load - this.valueLin = 1.0; - this.valueDB = 0.0; - this.root = document.createElement('div'); - this.root.id = 'master-volume-root'; - this.object = document.createElement('div'); - this.object.className = 'master-volume-holder-float'; - this.object.appendChild(this.root); - this.slider = document.createElement('input'); - this.slider.id = 'master-volume-control'; - this.slider.type = 'range'; - this.valueText = document.createElement('span'); - this.valueText.id = 'master-volume-feedback'; - this.valueText.textContent = '0dB'; + var volume = {}; + volume.valueLin = 1.0; + volume.valueDB = 0.0; + volume.root = document.createElement('div'); + volume.root.id = 'master-volume-root'; + volume.object = document.createElement('div'); + volume.object.className = 'master-volume-holder-float'; + volume.object.appendChild(volume.root); + volume.slider = document.createElement('input'); + volume.slider.id = 'master-volume-control'; + volume.slider.type = 'range'; + volume.valueText = document.createElement('span'); + volume.valueText.id = 'master-volume-feedback'; + volume.valueText.textContent = '0dB'; - this.slider.min = -60; - this.slider.max = 12; - this.slider.value = 0; - this.slider.step = 1; - this.slider.onmousemove = function (event) { - interfaceContext.volume.valueDB = event.currentTarget.value; - interfaceContext.volume.valueLin = decibelToLinear(interfaceContext.volume.valueDB); - interfaceContext.volume.valueText.textContent = interfaceContext.volume.valueDB + 'dB'; - audioEngineContext.outputGain.gain.value = interfaceContext.volume.valueLin; - } - this.slider.onmouseup = function (event) { + volume.slider.min = -60; + volume.slider.max = 12; + volume.slider.value = 0; + volume.slider.step = 1; + volume.handleEvent = function (event) { + if (event.type == "mousemove") { + this.valueDB = Number(this.slider.value); + this.valueLin = decibelToLinear(this.valueDB); + this.valueText.textContent = this.valueDB + 'dB'; + audioEngineContext.outputGain.gain.value = this.valueLin; + } else if (event.type == "mouseup") { + this.onmouseup(); + } + this.slider.value = this.valueDB; + + if (event.stopPropagation) { + event.stopPropagation(); + } + }; + volume.onmouseup = function () { var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker'); - if (storePoint.length == 0) { + if (storePoint.length === 0) { storePoint = storage.document.createElement('metricresult'); storePoint.setAttribute('name', 'volumeTracker'); testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint); @@ -2963,29 +2989,48 @@ } var node = storage.document.createElement('movement'); node.setAttribute('test-time', audioEngineContext.timer.getTestTime()); - node.setAttribute('volume', interfaceContext.volume.valueDB); + node.setAttribute('volume', this.valueDB); node.setAttribute('format', 'dBFS'); storePoint.appendChild(node); - } + }; + volume.slider.addEventListener("mousemove", volume); + volume.root.addEventListener("mouseup", volume); var title = document.createElement('div'); title.innerHTML = 'Master Volume Control'; title.style.fontSize = '0.75em'; title.style.width = "100%"; title.align = 'center'; - this.root.appendChild(title); + volume.root.appendChild(title); - this.root.appendChild(this.slider); - this.root.appendChild(this.valueText); + volume.root.appendChild(volume.slider); + volume.root.appendChild(volume.valueText); - this.resize = function (event) { + volume.resize = function (event) { if (window.innerWidth < 1000) { - this.object.className = "master-volume-holder-inline" + this.object.className = "master-volume-holder-inline"; } else { this.object.className = 'master-volume-holder-float'; } - } - } + }; + return volume; + })(); + + this.imageHolder = (function () { + var imageController = {}; + imageController.root = document.createElement("div"); + imageController.root.id = "imageController"; + imageController.img = document.createElement("img"); + imageController.root.appendChild(imageController.img); + imageController.setImage = function (src) { + imageController.img.src = ""; + if (typeof src !== "string" || src.length === undefined) { + return; + } + imageController.img.src = src; + }; + return imageController; + })(); this.calibrationModuleObject = null; this.calibrationModule = function () { @@ -3001,6 +3046,7 @@ this.holder.className = "calibration-holder"; this.calibrationNodes = []; while (f0 < 20000) { + /* jshint loopfunc: true */ var obj = { root: document.createElement("div"), input: document.createElement("input"), @@ -3025,7 +3071,7 @@ audioEngineContext.outputGain.gain.value = value; interfaceContext.volume.slider.value = this.input.value; } else { - this.gain.gain.value = value + this.gain.gain.value = value; } break; } @@ -3033,7 +3079,7 @@ disconnect: function () { this.gain.disconnect(); } - } + }; obj.root.className = "calibration-slider"; obj.root.appendChild(obj.input); obj.oscillator.connect(obj.gain); @@ -3061,53 +3107,63 @@ f0 *= 2; } inject.appendChild(this.holder); - } + }; this.collect = function () { - for (var obj of this.calibrationNodes) { + this.calibrationNodes.forEach(function (obj) { var node = storage.document.createElement("calibrationresult"); node.setAttribute("frequency", obj.f); node.setAttribute("range-min", obj.input.min); node.setAttribute("range-max", obj.input.max); node.setAttribute("gain-lin", obj.gain.gain.value); this.storeDOM.appendChild(node); - } - } - } + }, this); + }; + }; // Global Checkers // These functions will help enforce the checkers - this.checkHiddenAnchor = function () { - for (var ao of audioEngineContext.audioObjects) { - if (ao.specification.type == "anchor") { - if (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0) { - // Anchor is not set below - console.log('Anchor node not below marker value'); - interfaceContext.lightbox.post("Message", 'Please keep listening'); - this.storeErrorNode('Anchor node not below marker value'); - return false; - } + this.checkHiddenAnchor = function (message) { + var anchors = audioEngineContext.audioObjects.filter(function (ao) { + return ao.specification.type === "anchor"; + }); + var state = anchors.some(function (ao) { + return (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0); + }); + if (state) { + console.log('Anchor node not below marker value'); + if (message) { + interfaceContext.lightbox.post("Message", message); + } else { + interfaceContext.lightbox.post("Message", 'Please keep listening'); } + this.storeErrorNode('Anchor node not below marker value'); + return false; } return true; }; - this.checkHiddenReference = function () { - for (var ao of audioEngineContext.audioObjects) { - if (ao.specification.type == "reference") { - if (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0) { - // Anchor is not set below - console.log('Reference node not above marker value'); - this.storeErrorNode('Reference node not above marker value'); - interfaceContext.lightbox.post("Message", 'Please keep listening'); - return false; - } + this.checkHiddenReference = function (message) { + var references = audioEngineContext.audioObjects.filter(function (ao) { + return ao.specification.type === "reference"; + }); + var state = references.some(function (ao) { + return (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0); + }); + if (state) { + console.log('Reference node not below marker value'); + if (message) { + interfaceContext.lightbox.post("Message", message); + } else { + interfaceContext.lightbox.post("Message", 'Please keep listening'); } + this.storeErrorNode('Reference node not below marker value'); + return false; } return true; }; - this.checkFragmentsFullyPlayed = function () { + this.checkFragmentsFullyPlayed = function (message) { // Checks the entire file has been played back // NOTE ! This will return true IF playback is Looped!!! if (audioEngineContext.loopPlayback) { @@ -3115,8 +3171,9 @@ return true; } var check_pass = true; - var error_obj = []; - for (var i = 0; i < audioEngineContext.audioObjects.length; i++) { + var error_obj = [], + i; + for (i = 0; i < audioEngineContext.audioObjects.length; i++) { var object = audioEngineContext.audioObjects[i]; var time = object.buffer.buffer.duration; var metric = object.metric; @@ -3131,37 +3188,40 @@ break; } } - if (passed == false) { + if (passed === false) { check_pass = false; console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId()); error_obj.push(object.interfaceDOM.getPresentedId()); } } - if (check_pass == false) { + if (check_pass === false) { var str_start = "You have not completely listened to fragments "; - for (var i = 0; i < error_obj.length; i++) { + for (i = 0; i < error_obj.length; i++) { str_start += error_obj[i]; if (i != error_obj.length - 1) { str_start += ', '; } } str_start += ". Please keep listening"; - console.log("[ALERT]: " + str_start); - this.storeErrorNode("[ALERT]: " + str_start); + console.log(str_start); + this.storeErrorNode(str_start); + if (message) { + str_start = message; + } interfaceContext.lightbox.post("Error", str_start); return false; } return true; }; - this.checkAllMoved = function () { + this.checkAllMoved = function (message) { var str = "You have not moved "; var failed = []; - for (var ao of audioEngineContext.audioObjects) { - if (ao.metric.wasMoved == false && ao.interfaceDOM.canMove() == true) { + audioEngineContext.audioObjects.forEach(function (ao) { + if (ao.metric.wasMoved === false && ao.interfaceDOM.canMove() === true) { failed.push(ao.interfaceDOM.getPresentedId()); } - } - if (failed.length == 0) { + }, this); + if (failed.length === 0) { return true; } else if (failed.length == 1) { str += 'track ' + failed[0]; @@ -3173,20 +3233,23 @@ str += 'and ' + failed[i]; } str += '.'; - interfaceContext.lightbox.post("Error", str); console.log(str); this.storeErrorNode(str); + if (message) { + str = message; + } + interfaceContext.lightbox.post("Error", str); return false; }; - this.checkAllPlayed = function () { + this.checkAllPlayed = function (message) { var str = "You have not played "; var failed = []; - for (var ao of audioEngineContext.audioObjects) { - if (ao.metric.wasListenedTo == false) { + audioEngineContext.audioObjects.forEach(function (ao) { + if (ao.metric.wasListenedTo === false) { failed.push(ao.interfaceDOM.getPresentedId()); } - } - if (failed.length == 0) { + }, this); + if (failed.length === 0) { return true; } else if (failed.length == 1) { str += 'track ' + failed[0]; @@ -3198,12 +3261,15 @@ str += 'and ' + failed[i]; } str += '.'; - interfaceContext.lightbox.post("Error", str); console.log(str); this.storeErrorNode(str); + if (message) { + str = message; + } + interfaceContext.lightbox.post("Error", str); return false; }; - this.checkAllCommented = function () { + this.checkAllCommented = function (message) { var str = "You have not commented on all the fragments."; var cont = true, boxes = this.commentBoxes.boxes, @@ -3211,45 +3277,102 @@ i; for (i = 0; i < numBoxes; i++) { if (boxes[i].trackCommentBox.value === "") { - interfaceContext.lightbox.post("Error", str); console.log(str); this.storeErrorNode(str); + if (message) { + str = message; + } + interfaceContext.lightbox.post("Error", str); return false; } } return true; - } - this.checkScaleRange = function (min, max) { + }; + this.checkScaleRange = function (message) { var page = testState.getCurrentTestPage(); - var audioObjects = audioEngineContext.audioObjects; + var interfaceObject = page.interfaces; var state = true; var str = "Please keep listening. "; - var minRanking = Infinity; - var maxRanking = -Infinity; - for (var ao of audioObjects) { - var rank = ao.interfaceDOM.getValue(); - if (rank < minRanking) { - minRanking = rank; - } - if (rank > maxRanking) { - maxRanking = rank; - } + if (interfaceObject === undefined) { + return true; } - if (minRanking * 100 > min) { - str += "At least one fragment must be below the " + min + " mark."; + interfaceObject = interfaceObject[0]; + var scales = (function () { + var scaleRange = interfaceObject.options.find(function (a) { + return a.name == "scalerange"; + }); + return { + min: scaleRange.min, + max: scaleRange.max + }; + })(); + var range = audioEngineContext.audioObjects.reduce(function (a, b) { + var v = b.interfaceDOM.getValue() * 100.0; + return { + min: Math.min(a.min, v), + max: Math.max(a.max, v) + }; + }, { + min: 100, + max: 0 + }); + if (range.min > scales.min) { + str += "At least one fragment must be below the " + scales.min + " mark."; + state = false; + } else if (range.max < scales.max) { + str += "At least one fragment must be above the " + scales.max + " mark."; state = false; } - if (maxRanking * 100 < max) { - str += "At least one fragment must be above the " + max + " mark." - state = false; - } - if (!state) { + if (state === false) { console.log(str); this.storeErrorNode(str); + if (message) { + str = message; + } interfaceContext.lightbox.post("Error", str); } return state; - } + }; + this.checkFragmentMinPlays = function () { + var failedObjects = audioEngineContext.audioObjects.filter(function (a) { + var minPlays = a.specification.minNumberPlays || a.specification.parent.minNumberPlays || specification.minNumberPlays; + if (minPlays === undefined || a.numberOfPlays >= minPlays) { + return false; + } + return true; + }); + if (failedObjects.length === 0) { + return true; + } + var failedString = []; + failedObjects.forEach(function (a) { + failedString.push(a.interfaceDOM.getPresentedId()); + }); + var str = "You have not played fragments " + failedString.join(", ") + " enough. Please keep listening"; + interfaceContext.lightbox.post("Message", str); + this.storeErrorNode(str); + return false; + }; + + + this.sortFragmentsByScore = function () { + var elements = audioEngineContext.audioObjects.filter(function (elem) { + return elem.specification.type !== "outside-reference"; + }); + var indexes = []; + var i = 0; + while (indexes.push(i++) < elements.length); + return indexes.sort(function (x, y) { + var a = elements[x].interfaceDOM.getValue(); + var b = elements[y].interfaceDOM.getValue(); + if (a > b) { + return 1; + } else if (a < b) { + return -1; + } + return 0; + }, elements[0].interfaceDOM.getValue()); + }; this.storeErrorNode = function (errorMessage) { var time = audioEngineContext.timer.getTestTime(); @@ -3274,13 +3397,12 @@ case "capital": return String.fromCharCode((index + offset) % 26 + 65); case "samediff": - if (index == 0) { + if (index === 0) { return "Same"; } else if (index == 1) { return "Difference"; - } else { - return ""; } + return ""; case "number": return String(index + offset); default: @@ -3288,7 +3410,7 @@ } } - if (typeof labelStart !== "string" || labelStart.length == 0) { + if (typeof labelStart !== "string" || labelStart.length === 0) { labelStart = String.fromCharCode(0); } @@ -3313,7 +3435,6 @@ labelStart = 1; } break; - case "none": default: labelStart = 0; } @@ -3330,7 +3451,7 @@ } else { throw ("Invalid arguments"); } - } + }; this.getCombinedInterfaces = function (page) { // Combine the interfaces with the global interface nodes @@ -3355,7 +3476,7 @@ } }); return local; - } + }; } function Storage() { @@ -3366,16 +3487,17 @@ this.document = null; this.root = null; this.state = 0; + var pFilenamePrefix = "save"; this.initialise = function (existingStore) { - if (existingStore == undefined) { + if (existingStore === undefined) { // We need to get the sessionKey this.SessionKey.requestKey(); this.document = document.implementation.createDocument(null, "waetresult", null); this.root = this.document.childNodes[0]; var projectDocument = specification.projectXML; - projectDocument.setAttribute('file-name', url); - projectDocument.setAttribute('url', qualifyURL(url)); + projectDocument.setAttribute('file-name', specification.url); + projectDocument.setAttribute('url', qualifyURL(specification.url)); this.root.appendChild(projectDocument); this.root.appendChild(interfaceContext.returnDateNode()); this.root.appendChild(interfaceContext.returnNavigator()); @@ -3384,10 +3506,10 @@ this.root = existingStore.firstChild; this.SessionKey.key = this.root.getAttribute("key"); } - if (specification.preTest != undefined) { + if (specification.preTest !== undefined) { this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest); } - if (specification.postTest != undefined) { + if (specification.postTest !== undefined) { this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest); } }; @@ -3399,7 +3521,7 @@ handleEvent: function () { var parse = new DOMParser(); var xml = parse.parseFromString(this.request.response, "text/xml"); - if (this.request.response.length == 0) { + if (this.request.response.length === 0) { console.error("An unspecified error occured, no server key could be generated"); return; } @@ -3432,7 +3554,7 @@ this.request.send(); }, update: function () { - if (this.key == null) { + if (this.key === null) { console.log("Cannot save as key == null"); return; } @@ -3444,7 +3566,7 @@ returnURL = specification.projectReturn; } } - xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key); + xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix); xmlhttp.setRequestHeader('Content-Type', 'text/xml'); xmlhttp.onerror = function () { console.log('Error updating file to server!'); @@ -3467,10 +3589,51 @@ console.log("Intermediate save: Error! " + message.textContent); } } + }; + xmlhttp.send([hold.innerHTML]); + }, + finish: function () { + // Final upload to complete the test + this.parent.finish(); + var hold = document.createElement("div"); + var clone = this.parent.root.cloneNode(true); + hold.appendChild(clone); + var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix="; + if (this.parent.filenamePrefix.length === 0) { + saveURL += "save"; + } else { + saveURL += this.parent.filenamePrefix; } - xmlhttp.send([hold.innerHTML]); + return new Promise(function (resolve, reject) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.open("POST", saveURL); + xmlhttp.setRequestHeader('Content-Type', 'text/xml'); + xmlhttp.onerror = function () { + console.log('Error updating file to server!'); + createProjectSave("local"); + }; + xmlhttp.onload = function () { + if (this.status >= 300) { + console.log("WARNING - Could not update at this time"); + createProjectSave("local"); + } else { + var parser = new DOMParser(); + var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml"); + var response = xmlDoc.getElementsByTagName('response')[0]; + if (response.getAttribute("state") == "OK") { + var file = response.getElementsByTagName("file")[0]; + console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B"); + resolve(response); + } else { + var message = response.getElementsByTagName("message"); + reject(message); + } + } + }; + xmlhttp.send([hold.innerHTML]); + }); } - } + }; this.createTestPageStore = function (specification) { var store = new this.pageNode(this, specification); @@ -3485,55 +3648,67 @@ this.XMLDOM = this.parent.document.createElement('survey'); this.XMLDOM.setAttribute('location', this.specification.location); this.XMLDOM.setAttribute("state", this.state); - for (var optNode of this.specification.options) { + this.specification.options.forEach(function (optNode) { if (optNode.type != 'statement') { var node = this.parent.document.createElement('surveyresult'); node.setAttribute("ref", optNode.id); node.setAttribute('type', optNode.type); this.XMLDOM.appendChild(node); } - } + }, this); root.appendChild(this.XMLDOM); this.postResult = function (node) { + function postNumber(doc, value) { + var child = doc.createElement("response"); + child.textContent = value; + return child; + } + + function postRadio(doc, node) { + var child = doc.createElement('response'); + if (node.response !== null) { + child.setAttribute('name', node.response.name); + child.textContent = node.response.text; + } + return child; + } + + function postCheckbox(doc, node) { + var checkNode = doc.createElement('response'); + checkNode.setAttribute('name', node.name); + checkNode.setAttribute('checked', node.checked); + return checkNode; + } // From popup: node is the popupOption node containing both spec. and results // ID is the position if (node.specification.type == 'statement') { return; } var surveyresult = this.XMLDOM.firstChild; - while (surveyresult != null) { + while (surveyresult !== null) { if (surveyresult.getAttribute("ref") == node.specification.id) { break; } surveyresult = surveyresult.nextElementSibling; } + surveyresult.setAttribute("duration", node.elapsedTime); switch (node.specification.type) { case "number": case "question": case "slider": - var child = this.parent.document.createElement('response'); - child.textContent = node.response; - surveyresult.appendChild(child); + surveyresult.appendChild(postNumber(this.parent.document, node.response)); break; case "radio": - var child = this.parent.document.createElement('response'); - if (node.response !== null) { - child.setAttribute('name', node.response.name); - child.textContent = node.response.text; - } - surveyresult.appendChild(child); + surveyresult.appendChild(postRadio(this.parent.document, node)); break; case "checkbox": - if (node.response == undefined) { + if (node.response === undefined) { surveyresult.appendChild(this.parent.document.createElement('response')); break; } for (var i = 0; i < node.response.length; i++) { - var checkNode = this.parent.document.createElement('response'); - checkNode.setAttribute('name', node.response[i].name); - checkNode.setAttribute('checked', node.response[i].checked); - surveyresult.appendChild(checkNode); + surveyresult.appendChild(postCheckbox(this.parent.document, node.response[i])); } break; } @@ -3541,7 +3716,7 @@ this.complete = function () { this.state = "complete"; this.XMLDOM.setAttribute("state", this.state); - } + }; }; this.pageNode = function (parent, specification) { @@ -3553,10 +3728,10 @@ this.XMLDOM.setAttribute('ref', specification.id); this.XMLDOM.setAttribute('presentedId', specification.presentedId); this.XMLDOM.setAttribute("state", this.state); - if (specification.preTest != undefined) { + if (specification.preTest !== undefined) { this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest); } - if (specification.postTest != undefined) { + if (specification.postTest !== undefined) { this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest); } @@ -3565,12 +3740,12 @@ this.XMLDOM.appendChild(page_metric); // Add the audioelement - for (var element of this.specification.audioElements) { + this.specification.audioElements.forEach(function (element) { var aeNode = this.parent.document.createElement('audioelement'); aeNode.setAttribute('ref', element.id); - if (element.name != undefined) { - aeNode.setAttribute('name', element.name) - }; + if (element.name !== undefined) { + aeNode.setAttribute('name', element.name); + } aeNode.setAttribute('type', element.type); aeNode.setAttribute('url', element.url); aeNode.setAttribute('fqurl', qualifyURL(element.url)); @@ -3583,26 +3758,38 @@ var ae_metric = this.parent.document.createElement('metric'); aeNode.appendChild(ae_metric); this.XMLDOM.appendChild(aeNode); - } + }, this); this.parent.root.appendChild(this.XMLDOM); this.complete = function () { this.state = "complete"; this.XMLDOM.setAttribute("state", "complete"); - } + }; }; this.update = function () { this.SessionKey.update(); - } + }; this.finish = function () { - if (this.state == 0) { - this.update(); - } this.state = 1; this.root.setAttribute("state", "complete"); return this.root; }; + + Object.defineProperties(this, { + 'filenamePrefix': { + 'get': function () { + return pFilenamePrefix; + }, + 'set': function (value) { + if (typeof value !== "string") { + value = String(value); + } + pFilenamePrefix = value; + return value; + } + } + }); } var window_depedancy_callback; diff -r 56e72cd18404 -r 231d2186f3fc js/loader.js --- a/js/loader.js Fri Jul 14 15:37:53 2017 +0100 +++ b/js/loader.js Fri Jul 14 15:39:24 2017 +0100 @@ -1,8 +1,8 @@ // Script to load the relevant JS files if the system supports it - +/*globals window, document */ window.onload = function () { // First check if the Web Audio API is supported - if (window.AudioContext == undefined && window.webkitAudioContext == undefined) { + if (window.AudioContext === undefined && window.webkitAudioContext === undefined) { // Display unsuported error message var body = document.getElementsByTagName("body")[0]; body.innerHTML = "

Sorry! Your browser is not supported :(

Your browser does not support the HTML5 Web Audio API. Please use one of the following supported browsers instead.

"; @@ -26,4 +26,4 @@ head.appendChild(script); } } -} +}; diff -r 56e72cd18404 -r 231d2186f3fc js/loudness.js --- a/js/loudness.js Fri Jul 14 15:37:53 2017 +0100 +++ b/js/loudness.js Fri Jul 14 15:39:24 2017 +0100 @@ -5,7 +5,7 @@ * return gain values to correct for a target loudness or match loudness between * multiple objects */ - +/* globals webkitOfflineAudioContext, navigator, audioContext, Float32Array */ var interval_cal_loudness_event = null; if (typeof OfflineAudioContext == "undefined") { @@ -21,16 +21,16 @@ if (navigator.platform == 'iPad' || navigator.platform == 'iPhone') { buffer.ready(); } - if (buffer == undefined) { + if (buffer === undefined) { return 0; } - if (timescale == undefined) { + if (timescale === undefined) { timescale = "I"; } - if (target == undefined) { + if (target === undefined) { target = -23; } - if (offlineContext == undefined) { + if (offlineContext === undefined) { offlineContext = new OfflineAudioContext(audioContext.destination.channelCount, buffer.buffer.duration * audioContext.sampleRate, audioContext.sampleRate); } // Create the required filters @@ -77,18 +77,23 @@ } function calculateMeanSquared(buffer, frame_dur, frame_overlap) { - frame_size = Math.floor(buffer.sampleRate * frame_dur); - step_size = Math.floor(frame_size * (1.0 - frame_overlap)); - num_frames = Math.floor((buffer.length - frame_size) / step_size); + var frame_size = Math.floor(buffer.sampleRate * frame_dur); + var step_size = Math.floor(frame_size * (1.0 - frame_overlap)); + var num_frames = Math.floor((buffer.length - frame_size) / step_size); + num_frames = Math.max(num_frames, 0); - MS = Array(buffer.numberOfChannels); + var MS = Array(buffer.numberOfChannels); for (var c = 0; c < buffer.numberOfChannels; c++) { MS[c] = new Float32Array(num_frames); var data = buffer.getChannelData(c); for (var no = 0; no < num_frames; no++) { MS[c][no] = 0.0; for (var ptr = 0; ptr < frame_size; ptr++) { - var sample = data[no * step_size + ptr]; + var i = no * step_size + ptr; + if (i >= buffer.length) { + break; + } + var sample = data[i]; MS[c][no] += sample * sample; } MS[c][no] /= frame_size; @@ -119,13 +124,14 @@ var num_frames = source[0].length; var num_channels = source.length; var LK = Array(num_channels); - for (var c = 0; c < num_channels; c++) { + var n, c; + for (c = 0; c < num_channels; c++) { LK[c] = []; } - for (var n = 0; n < num_frames; n++) { + for (n = 0; n < num_frames; n++) { if (blocks[n] > threshold) { - for (var c = 0; c < num_channels; c++) { + for (c = 0; c < num_channels; c++) { LK[c].push(source[c][n]); } } diff -r 56e72cd18404 -r 231d2186f3fc js/specification.js --- a/js/specification.js Fri Jul 14 15:37:53 2017 +0100 +++ b/js/specification.js Fri Jul 14 15:39:24 2017 +0100 @@ -1,45 +1,58 @@ +/* globals document, console, DOMParser */ function Specification() { + var schemaRoot; + var schemaString; // Handles the decoding of the project specification XML into a simple JavaScript Object. // attributes - this.interface = null; - this.projectReturn = null; - this.returnURL = null; - this.randomiseOrder = null; - this.poolSize = null; - this.loudness = null; - this.sampleRate = null; - this.calibration = null; - this.crossFade = null; - this.preSilence = null; - this.postSilence = null; - this.playOne = null; + this.interface = undefined; + this.projectReturn = undefined; + this.returnURL = undefined; + this.randomiseOrder = undefined; + this.poolSize = undefined; + this.loudness = undefined; + this.sampleRate = undefined; + this.calibration = undefined; + this.crossFade = undefined; + this.preSilence = undefined; + this.postSilence = undefined; + this.playOne = undefined; + this.minNumberPlays = undefined; + this.maxNumberPlays = undefined; // nodes - this.metrics = null; - this.preTest = undefined; - this.postTest = undefined; + this.metrics = new metricNode(); + this.preTest = new surveyNode(this); + this.postTest = new surveyNode(this); + this.preTest.location = "pre"; + this.postTest.location = "post"; this.pages = []; - this.interfaces = null; + this.interfaces = new interfaceNode(this); this.errors = []; - this.schema = null; this.exitText = "Thank you."; - this.processAttribute = function (attribute, schema, schemaRoot) { + // Creators + this.createNewPage = function () { + var newpage = new page(this); + this.pages.push(newpage); + return newpage; + }; + + var processAttribute = function (attribute, schema) { // attribute is the string returned from getAttribute on the XML // schema is the node - if (schema.getAttribute('name') == undefined && schema.getAttribute('ref') != undefined) { - schema = schemaRoot.getAllElementsByName(schema.getAttribute('ref'))[0]; + if (schema.getAttribute('name') === null && schema.getAttribute('ref') !== undefined) { + schema = schemaRoot.querySelector("[name=" + schema.getAttribute('ref') + "]"); } var defaultOpt = schema.getAttribute('default'); - if (attribute == null) { + if (attribute === null) { attribute = defaultOpt; } var dataType = schema.getAttribute('type'); if (typeof dataType == "string") { dataType = dataType.substr(3); } else { - var rest = schema.getAllElementsByTagName("xs:restriction").concat(schema.getAllElementsByTagName("xs:enumeration")); + var rest = schema.querySelectorAll("restriction,enumeration"); if (rest.length > 0) { dataType = rest[0].getAttribute("base"); if (typeof dataType == "string") { @@ -51,7 +64,7 @@ dataType = "string"; } } - if (attribute == null) { + if (attribute === null) { return attribute; } switch (dataType) { @@ -71,7 +84,6 @@ case "short": attribute = Number(attribute); break; - case "string": default: attribute = String(attribute); break; @@ -79,26 +91,44 @@ return attribute; }; + this.processSchema = function (schemaXSD) { + if (schemaRoot === undefined) { + schemaString = schemaXSD; + var parse = new DOMParser(); + schemaRoot = parse.parseFromString(schemaString, 'text/xml'); + Object.defineProperties(this, { + 'schema': { + 'value': schemaRoot + }, + 'schemaString': { + 'value': schemaString + } + }); + } + }; + this.getSchema = function () { + return schemaRoot; + }; + this.getSchemaString = function () { + return schemaString; + }; + this.decode = function (projectXML) { + schemaRoot = this.schema; this.errors = []; // projectXML - DOM Parsed document this.projectXML = projectXML.childNodes[0]; var setupNode = projectXML.getElementsByTagName('setup')[0]; - var schemaSetup = this.schema.getAllElementsByName('setup')[0]; + var schemaSetup = schemaRoot.querySelector('[name=setup]'); // First decode the attributes - var attributes = schemaSetup.getAllElementsByTagName('xs:attribute'); - for (var i = 0; i < attributes.length; i++) { + var attributes = schemaSetup.querySelectorAll('attribute'); + var i; + for (i = 0; i < attributes.length; i++) { var attributeName = attributes[i].getAttribute('name') || attributes[i].getAttribute('ref'); var projectAttr = setupNode.getAttribute(attributeName); - projectAttr = this.processAttribute(projectAttr, attributes[i], this.schema); - switch (typeof projectAttr) { - case "number": - case "boolean": - eval('this.' + attributeName + ' = ' + projectAttr); - break; - case "string": - eval('this.' + attributeName + ' = "' + projectAttr + '"'); - break; + projectAttr = processAttribute(projectAttr, attributes[i]); + if (projectAttr !== null) { + this[attributeName] = projectAttr; } } @@ -108,23 +138,19 @@ this.exitText = exitTextNode[0].textContent; } - this.metrics = new this.metricNode(); - this.metrics.decode(this, setupNode.getElementsByTagName('metric')[0]); // Now process the survey node options var survey = setupNode.getElementsByTagName('survey'); - for (var i = 0; i < survey.length; i++) { + for (i = 0; i < survey.length; i++) { var location = survey[i].getAttribute('location'); switch (location) { case 'pre': case 'before': - this.preTest = new this.surveyNode(this); this.preTest.decode(this, survey[i]); break; case 'post': case 'after': - this.postTest = new this.surveyNode(this); this.postTest.decode(this, survey[i]); break; } @@ -134,17 +160,17 @@ if (interfaceNode.length > 1) { this.errors.push("Only one node in the node allowed! Others except first ingnored!"); } - this.interfaces = new this.interfaceNode(this); - if (interfaceNode.length != 0) { + + if (interfaceNode.length !== 0) { interfaceNode = interfaceNode[0]; - this.interfaces.decode(this, interfaceNode, this.schema.getAllElementsByName('interface')[1]); + this.interfaces.decode(this, interfaceNode, this.schema.querySelectorAll('[name=interface]')[1]); } // Page tags var pageTags = projectXML.getElementsByTagName('page'); - var pageSchema = this.schema.getAllElementsByName('page')[0]; - for (var i = 0; i < pageTags.length; i++) { - var node = new this.page(this); + var pageSchema = this.schema.querySelector('[name=page]'); + for (i = 0; i < pageTags.length; i++) { + var node = new page(this); node.decode(this, pageTags[i], pageSchema); this.pages.push(node); } @@ -157,21 +183,21 @@ root.setAttribute("xsi:noNamespaceSchemaLocation", "test-schema.xsd"); // Build setup node var setup = RootDocument.createElement("setup"); - var schemaSetup = this.schema.getAllElementsByName('setup')[0]; + var schemaSetup = schemaRoot.querySelector('[name=setup]'); // First decode the attributes - var attributes = schemaSetup.getAllElementsByTagName('xs:attribute'); + var attributes = schemaSetup.querySelectorAll('attribute'); for (var i = 0; i < attributes.length; i++) { var name = attributes[i].getAttribute("name"); - if (name == undefined) { + if (name === null) { name = attributes[i].getAttribute("ref"); } - if (eval("this." + name + " != undefined") || attributes[i].getAttribute("use") == "required") { - eval("setup.setAttribute('" + name + "',this." + name + ")"); + if (this[name] !== undefined || attributes[i].getAttribute("use") == "required") { + setup.setAttribute(name, this[name]); } } root.appendChild(setup); // Survey node - if (this.exitText != null) { + if (this.exitText !== null) { var exitTextNode = RootDocument.createElement('exitText'); exitTextNode.textContent = this.exitText; setup.appendChild(exitTextNode); @@ -180,19 +206,24 @@ setup.appendChild(this.postTest.encode(RootDocument)); setup.appendChild(this.metrics.encode(RootDocument)); setup.appendChild(this.interfaces.encode(RootDocument)); - for (var page of this.pages) { + this.pages.forEach(function (page) { root.appendChild(page.encode(RootDocument)); - } + }); return RootDocument; }; - this.surveyNode = function (specification) { - this.location = null; + function surveyNode(specification) { + this.location = undefined; this.options = []; - this.parent = null; - this.schema = specification.schema.getAllElementsByName('survey')[0]; + this.parent = undefined; this.specification = specification; + this.addOption = function () { + var node = new this.OptionNode(this.specification); + this.options.push(node); + return node; + }; + this.OptionNode = function (specification) { this.type = undefined; this.schema = undefined; @@ -204,27 +235,23 @@ this.options = []; this.min = undefined; this.max = undefined; + this.minWait = undefined; this.step = undefined; this.conditions = []; this.decode = function (parent, child) { - this.schema = specification.schema.getAllElementsByName(child.nodeName)[0]; - var attributeMap = this.schema.getAllElementsByTagName('xs:attribute'); - for (var i in attributeMap) { - if (isNaN(Number(i)) == true) { + this.schema = schemaRoot.querySelector("[name=" + child.nodeName + "]"); + var attributeMap = this.schema.querySelectorAll('attribute'); + var i; + for (i in attributeMap) { + if (isNaN(Number(i)) === true) { break; } var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref'); var projectAttr = child.getAttribute(attributeName); - projectAttr = parent.processAttribute(projectAttr, attributeMap[i], parent.schema); - switch (typeof projectAttr) { - case "number": - case "boolean": - eval('this.' + attributeName + ' = ' + projectAttr); - break; - case "string": - eval('this.' + attributeName + ' = "' + projectAttr + '"'); - break; + projectAttr = processAttribute(projectAttr, attributeMap[i]); + if (projectAttr !== null) { + this[attributeName] = projectAttr; } } if (child.nodeName == 'surveyentry') { @@ -239,13 +266,13 @@ this.statement = child.getElementsByTagName('statement')[0].textContent; if (this.type == "checkbox" || this.type == "radio") { var children = child.getElementsByTagName('option'); - if (children.length == null) { + if (children.length === null) { console.log('Malformed' + child.nodeName + 'entry'); this.statement = 'Malformed' + child.nodeName + 'entry'; this.type = 'statement'; } else { this.options = []; - for (var i = 0; i < children.length; i++) { + for (i = 0; i < children.length; i++) { this.options.push({ name: children[i].getAttribute('name'), text: children[i].textContent @@ -265,14 +292,14 @@ } } var conditionElements = child.getElementsByTagName("conditional"); - for (var i = 0; i < conditionElements.length; i++) { + for (i = 0; i < conditionElements.length; i++) { var condition = conditionElements[i]; var obj = { check: condition.getAttribute("check"), value: condition.getAttribute("value"), jumpToOnPass: condition.getAttribute("jumpToOnPass"), jumpToOnFail: condition.getAttribute("jumpToOnFail") - } + }; this.conditions.push(obj); } }; @@ -283,28 +310,28 @@ statement.textContent = this.statement; node.appendChild(statement); node.id = this.id; - if (this.name != undefined) { + if (this.name !== undefined) { node.setAttribute("name", this.name); } - if (this.mandatory != undefined) { + if (this.mandatory !== undefined) { node.setAttribute("mandatory", this.mandatory); } node.id = this.id; - if (this.name != undefined) { + if (this.name !== undefined) { node.setAttribute("name", this.name); } switch (this.type) { case "checkbox": - if (this.min != undefined) { + if (this.min !== undefined) { node.setAttribute("min", this.min); } else { node.setAttribute("min", "0"); } - if (this.max != undefined) { + if (this.max !== undefined) { node.setAttribute("max", this.max); } else { node.setAttribute("max", "undefined"); - } + } /* falls through */ case "radio": for (var i = 0; i < this.options.length; i++) { var option = this.options[i]; @@ -315,27 +342,27 @@ } break; case "number": - if (this.min != undefined) { + if (this.min !== undefined) { node.setAttribute("min", this.min); } - if (this.max != undefined) { + if (this.max !== undefined) { node.setAttribute("max", this.max); } break; case "question": - if (this.boxsize != undefined) { + if (this.boxsize !== undefined) { node.setAttribute("boxsize", this.boxsize); } - if (this.mandatory != undefined) { + if (this.mandatory !== undefined) { node.setAttribute("mandatory", this.mandatory); } break; case "video": - if (this.mandatory != undefined) { + if (this.mandatory !== undefined) { node.setAttribute("mandatory", this.mandatory); - } + } /* falls through */ case "youtube": - if (this.url != undefined) { + if (this.url !== undefined) { node.setAttribute("url", this.url); } break; @@ -352,21 +379,23 @@ maxText.textContent = this.rightText; node.appendChild(maxText); } + break; default: break; } - for (var condition of this.conditions) { + this.conditions.forEach(function (condition) { var conditionDOM = doc.createElement("conditional"); conditionDOM.setAttribute("check", condition.check); conditionDOM.setAttribute("value", condition.value); conditionDOM.setAttribute("jumpToOnPass", condition.jumpToOnPass); conditionDOM.setAttribute("jumpToOnFail", condition.jumpToOnFail); node.appendChild(conditionDOM); - } + }); return node; }; }; this.decode = function (parent, xml) { + this.schema = schemaRoot.querySelector('[name=survey]'); this.parent = parent; this.location = xml.getAttribute('location'); if (this.location == 'before') { @@ -374,14 +403,14 @@ } else if (this.location == 'after') { this.location = 'post'; } - var child = xml.firstElementChild + var child = xml.firstElementChild; while (child) { var node = new this.OptionNode(this.specification); node.decode(parent, child); this.options.push(node); child = child.nextElementSibling; } - if (this.options.length == 0) { + if (this.options.length === 0) { console.log("Empty survey node"); console.log(this); } @@ -394,16 +423,18 @@ } return node; }; - }; + } - this.interfaceNode = function (specification) { - this.title = null; - this.name = null; + function interfaceNode(specification) { + this.title = undefined; + this.name = undefined; + this.image = undefined; this.options = []; this.scales = []; - this.schema = specification.schema.getAllElementsByName('interface')[1]; + this.schema = undefined; this.decode = function (parent, xml) { + this.schema = schemaRoot.querySelectorAll('[name=interface]')[1]; this.name = xml.getAttribute('name'); var titleNode = xml.getElementsByTagName('title'); if (titleNode.length == 1) { @@ -411,38 +442,36 @@ } var interfaceOptionNodes = xml.getElementsByTagName('interfaceoption'); // Extract interfaceoption node schema - var interfaceOptionNodeSchema = this.schema.getAllElementsByName('interfaceoption')[0]; - var attributeMap = interfaceOptionNodeSchema.getAllElementsByTagName('xs:attribute'); - for (var i = 0; i < interfaceOptionNodes.length; i++) { + var interfaceOptionNodeSchema = this.schema.querySelector('[name=interfaceoption]'); + var attributeMap = interfaceOptionNodeSchema.querySelectorAll('attribute'); + var i, j; + for (i = 0; i < interfaceOptionNodes.length; i++) { var ioNode = interfaceOptionNodes[i]; var option = {}; - for (var j = 0; j < attributeMap.length; j++) { + for (j = 0; j < attributeMap.length; j++) { var attributeName = attributeMap[j].getAttribute('name') || attributeMap[j].getAttribute('ref'); var projectAttr = ioNode.getAttribute(attributeName); - if (parent.processAttribute) { - parent.processAttribute(projectAttr, attributeMap[j], parent.schema) - } else { - parent.parent.processAttribute(projectAttr, attributeMap[j], parent.parent.schema) + projectAttr = processAttribute(projectAttr, attributeMap[j]); + if (projectAttr !== null) { + option[attributeName] = projectAttr; } - switch (typeof projectAttr) { - case "number": - case "boolean": - eval('option.' + attributeName + ' = ' + projectAttr); - break; - case "string": - eval('option.' + attributeName + ' = "' + projectAttr + '"'); - break; - } + } + if (option.type == "check" && ioNode.firstElementChild) { + option.errorMessage = ioNode.firstElementChild.textContent; } this.options.push(option); } - + // Get the image node + var imageNode = xml.getElementsByTagName("image"); + if (imageNode.length == 1) { + this.image = imageNode[0].getAttribute("src"); + } // Now the scales nodes var scaleParent = xml.getElementsByTagName('scales'); if (scaleParent.length == 1) { scaleParent = scaleParent[0]; - var scalelabels = scaleParent.getAllElementsByTagName('scalelabel'); - for (var i = 0; i < scalelabels.length; i++) { + var scalelabels = scaleParent.querySelectorAll('scalelabel'); + for (i = 0; i < scalelabels.length; i++) { this.scales.push({ text: scalelabels[i].textContent, position: Number(scalelabels[i].getAttribute('position')) @@ -453,48 +482,58 @@ this.encode = function (doc) { var node = doc.createElement("interface"); - if (typeof name == "string" && name.length > 0) + if (typeof this.name == "string" && this.name.length > 0) node.setAttribute("name", this.name); if (typeof this.title == "string") { var titleNode = doc.createElement("title"); titleNode.textContent = this.title; node.appendChild(titleNode); } - for (var option of this.options) { + this.options.forEach(function (option) { var child = doc.createElement("interfaceoption"); child.setAttribute("type", option.type); child.setAttribute("name", option.name); + if (option.type == "check" && option.errorMessage !== undefined) { + var errorMessage = doc.createElement("errormessage"); + errorMessage.textContent = option.errorMessage; + child.appendChild(errorMessage); + } node.appendChild(child); + }); + if (typeof this.image == "string" && this.image.length !== 0) { + var imgNode = doc.createElement("image"); + imgNode.setAttribute("src", this.image); + node.appendChild(imgNode); } - if (this.scales.length != 0) { + if (this.scales.length !== 0) { var scales = doc.createElement("scales"); - for (var scale of this.scales) { + this.scales.forEach(function (scale) { var child = doc.createElement("scalelabel"); child.setAttribute("position", scale.position); child.textContent = scale.text; scales.appendChild(child); - } + }); node.appendChild(scales); } return node; }; - }; + } - this.metricNode = function () { + function metricNode() { this.enabled = []; this.decode = function (parent, xml) { var children = xml.getElementsByTagName('metricenable'); for (var i in children) { - if (isNaN(Number(i)) == true) { + if (isNaN(Number(i)) === true) { break; } this.enabled.push(children[i].textContent); } - } + }; this.encode = function (doc) { var node = doc.createElement('metric'); for (var i in this.enabled) { - if (isNaN(Number(i)) == true) { + if (isNaN(Number(i)) === true) { break; } var child = doc.createElement('metricenable'); @@ -502,87 +541,101 @@ node.appendChild(child); } return node; - } + }; } - this.page = function (specification) { + function page(specification) { this.presentedId = undefined; this.id = undefined; this.title = undefined; this.hostURL = undefined; this.randomiseOrder = undefined; this.loop = undefined; - this.outsideReference = null; - this.loudness = null; - this.label = null; - this.labelStart = ""; - this.preTest = null; - this.postTest = null; + this.outsideReference = undefined; + this.loudness = undefined; + this.label = undefined; + this.labelStart = undefined; + this.preTest = new surveyNode(specification); + this.postTest = new surveyNode(specification); + this.preTest.location = "pre"; + this.postTest.location = "post"; this.interfaces = []; - this.playOne = null; - this.restrictMovement = null; + this.playOne = undefined; + this.restrictMovement = undefined; + this.position = undefined; this.commentBoxPrefix = "Comment on track"; + this.minNumberPlays = undefined; + this.maxNumberPlays = undefined; this.audioElements = []; this.commentQuestions = []; - this.schema = specification.schema.getAllElementsByName("page")[0]; + this.schema = schemaRoot.querySelector("[name=page]"); this.specification = specification; - this.parent = null; + this.parent = undefined; + + this.addInterface = function () { + var node = new interfaceNode(specification); + this.interfaces.push(node); + return node; + }; + this.addCommentQuestion = function () { + var node = new commentQuestionNode(specification); + this.commentQuestions.push(node); + return node; + }; + this.addAudioElement = function () { + var node = new audioElementNode(specification); + this.audioElements.push(node); + return node; + }; this.decode = function (parent, xml) { this.parent = parent; - var attributeMap = this.schema.getAllElementsByTagName('xs:attribute'); - for (var i = 0; i < attributeMap.length; i++) { + var attributeMap = this.schema.querySelectorAll('attribute'); + var i, node; + for (i = 0; i < attributeMap.length; i++) { var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref'); var projectAttr = xml.getAttribute(attributeName); - projectAttr = parent.processAttribute(projectAttr, attributeMap[i], parent.schema); - switch (typeof projectAttr) { - case "number": - case "boolean": - eval('this.' + attributeName + ' = ' + projectAttr); - break; - case "string": - eval('this.' + attributeName + ' = "' + projectAttr + '"'); - break; + projectAttr = processAttribute(projectAttr, attributeMap[i]); + if (projectAttr !== null) { + this[attributeName] = projectAttr; } } // Get the title var title = xml.getElementsByTagName('title'); - if (title.length != 0 && title[0].parentElement == xml) { + if (title.length !== 0 && title[0].parentElement == xml) { this.title = title[0].textContent; } // Get the Comment Box Prefix var CBP = xml.getElementsByTagName('commentboxprefix'); - if (CBP.length != 0 && CBP[0].parentElement == xml) { + if (CBP.length !== 0 && CBP[0].parentElement == xml) { this.commentBoxPrefix = CBP[0].textContent; } // Now decode the interfaces - var interfaceNode = xml.getElementsByTagName('interface'); - for (var i = 0; i < interfaceNode.length; i++) { - var node = new parent.interfaceNode(this.specification); - node.decode(this, interfaceNode[i], parent.schema.getAllElementsByName('interface')[1]); + var interfaceNodes = xml.getElementsByTagName('interface'); + for (i = 0; i < interfaceNodes.length; i++) { + node = new interfaceNode(this.specification); + node.decode(this, interfaceNodes[i], parent.schema.querySelectorAll('[name=interface]')[1]); this.interfaces.push(node); } // Now process the survey node options var survey = xml.getElementsByTagName('survey'); - var surveySchema = parent.schema.getAllElementsByName('survey')[0]; - for (var i = 0; i < survey.length; i++) { + var surveySchema = parent.schema.querySelector('[name=survey]'); + for (i = 0; i < survey.length; i++) { var location = survey[i].getAttribute('location'); if (location == 'pre' || location == 'before') { - if (this.preTest != null) { + if (this.preTest.options.length !== 0) { this.errors.push("Already a pre/before test survey defined! Ignoring second!!"); } else { - this.preTest = new parent.surveyNode(this.specification); this.preTest.decode(parent, survey[i], surveySchema); } } else if (location == 'post' || location == 'after') { - if (this.postTest != null) { + if (this.postTest.options.length !== 0) { this.errors.push("Already a post/after test survey defined! Ignoring second!!"); } else { - this.postTest = new parent.surveyNode(this.specification); this.postTest.decode(parent, survey[i], surveySchema); } } @@ -590,19 +643,19 @@ // Now process the audioelement tags var audioElements = xml.getElementsByTagName('audioelement'); - for (var i = 0; i < audioElements.length; i++) { - var node = new this.audioElementNode(this.specification); - node.decode(this, audioElements[i]); - this.audioElements.push(node); + for (i = 0; i < audioElements.length; i++) { + var audioNode = new audioElementNode(this.specification); + audioNode.decode(this, audioElements[i]); + this.audioElements.push(audioNode); } // Now decode the commentquestions var cqNode = xml.getElementsByTagName('commentquestions'); - if (cqNode.length != 0) { + if (cqNode.length !== 0) { cqNode = cqNode[0]; var commentQuestion = cqNode.firstElementChild; while (commentQuestion) { - var node = new this.commentQuestionNode(this.specification); + node = new commentQuestionNode(this.specification); node.decode(parent, commentQuestion); this.commentQuestions.push(node); commentQuestion = commentQuestion.nextElementSibling; @@ -613,33 +666,31 @@ this.encode = function (root) { var AHNode = root.createElement("page"); // First decode the attributes - var attributes = this.schema.getAllElementsByTagName('xs:attribute'); - for (var i = 0; i < attributes.length; i++) { + var attributes = this.schema.querySelectorAll('attribute'); + var i; + for (i = 0; i < attributes.length; i++) { var name = attributes[i].getAttribute("name"); - if (name == undefined) { + if (name === null) { name = attributes[i].getAttribute("ref"); } - if (eval("this." + name + " != undefined") || attributes[i].getAttribute("use") == "required") { - eval("AHNode.setAttribute('" + name + "',this." + name + ")"); + if (this[name] !== undefined || attributes[i].getAttribute("use") == "required") { + AHNode.setAttribute(name, this[name]); } } - if (this.loudness != null) { - AHNode.setAttribute("loudness", this.loudness); - } // var commentboxprefix = root.createElement("commentboxprefix"); commentboxprefix.textContent = this.commentBoxPrefix; AHNode.appendChild(commentboxprefix); - for (var i = 0; i < this.interfaces.length; i++) { + for (i = 0; i < this.interfaces.length; i++) { AHNode.appendChild(this.interfaces[i].encode(root)); } - for (var i = 0; i < this.audioElements.length; i++) { + for (i = 0; i < this.audioElements.length; i++) { AHNode.appendChild(this.audioElements[i].encode(root)); } // Create - for (var i = 0; i < this.commentQuestions.length; i++) { + for (i = 0; i < this.commentQuestions.length; i++) { AHNode.appendChild(this.commentQuestions[i].encode(root)); } @@ -648,15 +699,18 @@ return AHNode; }; - this.commentQuestionNode = function (specification) { - this.id = null; + function commentQuestionNode(specification) { + this.id = undefined; this.name = undefined; this.type = undefined; this.statement = undefined; - this.schema = specification.schema.getAllElementsByName('commentquestion')[0]; + this.schema = schemaRoot.querySelector('[name=commentquestion]'); this.decode = function (parent, xml) { this.id = xml.id; this.name = xml.getAttribute('name'); + if (this.name === null) { + this.name = undefined; + } switch (xml.nodeName) { case "commentradio": this.type = "radio"; @@ -673,9 +727,10 @@ this.step = undefined; break; case "commentquestion": - default: this.type = "question"; break; + default: + throw ("Unknown comment type " + xml.nodeName); } this.statement = xml.getElementsByTagName('statement')[0].textContent; if (this.type == "radio" || this.type == "checkbox") { @@ -692,12 +747,12 @@ this.min = Number(xml.getAttribute("min")); this.max = Number(xml.getAttribute("max")); this.step = Number(xml.getAttribute("step")); - if (this.step == undefined) { + if (this.step === undefined) { this.step = 1; } this.value = Number(xml.getAttribute("value")); - if (this.value == undefined) { - this.value = min; + if (this.value === undefined) { + this.value = this.min; } this.leftText = xml.getElementsByTagName("minText"); if (this.leftText && this.leftText.length > 0) { @@ -727,25 +782,26 @@ node = root.createElement("commentslider"); break; case "question": - default: node = root.createElement("commentquestion"); break; + default: + throw ("Unknown type " + this.type); } node.id = this.id; node.setAttribute("type", this.type); - if (this.name != undefined) { + if (this.name !== undefined) { node.setAttribute("name", this.name); } var statement = root.createElement("statement"); statement.textContent = this.statement; node.appendChild(statement); if (this.type == "radio" || this.type == "checkbox") { - for (var option of this.options) { + this.options.forEach(function (option) { var child = root.createElement("option"); child.setAttribute("name", option.name); child.textContent = option.text; node.appendChild(child); - } + }); } if (this.type == "slider") { node.setAttribute("min", this.min); @@ -769,39 +825,36 @@ } return node; }; - }; + } - this.audioElementNode = function (specification) { - this.url = null; - this.id = null; - this.name = null; - this.parent = null; - this.type = null; - this.marker = null; + function audioElementNode(specification) { + this.url = undefined; + this.id = undefined; + this.name = undefined; + this.parent = undefined; + this.type = undefined; + this.marker = undefined; this.enforce = false; this.gain = 0.0; - this.label = null; + this.label = undefined; this.startTime = undefined; this.stopTime = undefined; this.sampleRate = undefined; + this.image = undefined; + this.minNumberPlays = undefined; + this.maxNumberPlays = undefined; this.alternatives = []; - this.schema = specification.schema.getAllElementsByName('audioelement')[0];; - this.parent = null; + this.schema = schemaRoot.querySelector('[name=audioelement]'); + this.parent = undefined; this.decode = function (parent, xml) { this.parent = parent; - var attributeMap = this.schema.getAllElementsByTagName('xs:attribute'); + var attributeMap = this.schema.querySelectorAll('attribute'); for (var i = 0; i < attributeMap.length; i++) { var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref'); var projectAttr = xml.getAttribute(attributeName); - projectAttr = parent.parent.processAttribute(projectAttr, attributeMap[i], parent.parent.schema); - switch (typeof projectAttr) { - case "number": - case "boolean": - eval('this.' + attributeName + ' = ' + projectAttr); - break; - case "string": - eval('this.' + attributeName + ' = "' + projectAttr + '"'); - break; + projectAttr = processAttribute(projectAttr, attributeMap[i]); + if (projectAttr !== null) { + this[attributeName] = projectAttr; } } // Get the alternative nodes @@ -819,14 +872,14 @@ }; this.encode = function (root) { var AENode = root.createElement("audioelement"); - var attributes = this.schema.getAllElementsByTagName('xs:attribute'); + var attributes = this.schema.querySelectorAll('attribute'); for (var i = 0; i < attributes.length; i++) { var name = attributes[i].getAttribute("name"); - if (name == undefined) { + if (name === null) { name = attributes[i].getAttribute("ref"); } - if (eval("this." + name + " != undefined") || attributes[i].getAttribute("use") == "required") { - eval("AENode.setAttribute('" + name + "',this." + name + ")"); + if (this[name] !== undefined || attributes[i].getAttribute("use") == "required") { + AENode.setAttribute(name, this[name]); } } this.alternatives.forEach(function (alt) { @@ -837,6 +890,6 @@ }); return AENode; }; - }; - }; + } + } } diff -r 56e72cd18404 -r 231d2186f3fc php/requestKey.php --- a/php/requestKey.php Fri Jul 14 15:37:53 2017 +0100 +++ b/php/requestKey.php Fri Jul 14 15:39:24 2017 +0100 @@ -58,7 +58,8 @@ $doc_struct = new DOMDocument; $doc_struct->preserveWhiteSpace = false; $doc_struct->formatOutput = true; -$doc_struct->loadXML(""); +$doc_struct->loadXML(""); +$doc_struct->documentElement->setAttribute("key", $key); // Add the root if (file_exists($testURL)) { $test_proto_doc = new DOMDocument; diff -r 56e72cd18404 -r 231d2186f3fc php/save.php --- a/php/save.php Fri Jul 14 15:37:53 2017 +0100 +++ b/php/save.php Fri Jul 14 15:39:24 2017 +0100 @@ -28,10 +28,12 @@ $saveFilenamePrefix = ''; if (isset($_GET['saveFilenamePrefix'])) { $saveFilenamePrefix = $_GET['saveFilenamePrefix'].'-'; +} else { + $saveFilenamePrefix = "save-"; } $postText = file_get_contents('php://input'); $file_key = $_GET['key']; -$filename = '../saves/'.$saveFilenamePrefix.'save-'.$file_key.".xml"; +$filename = '../saves/'.$saveFilenamePrefix.$file_key.".xml"; if (!file_exists($filename)) { die('Could not find save'); diff -r 56e72cd18404 -r 231d2186f3fc python/comment_parser.py --- a/python/comment_parser.py Fri Jul 14 15:37:53 2017 +0100 +++ b/python/comment_parser.py Fri Jul 14 15:39:24 2017 +0100 @@ -57,7 +57,7 @@ # for page [page_name], print comments related to fragment [id] for audioelement in audioholder.findall("./audioelement"): - if audioelement is not None: # Check it exists + if audioelement is not None and audioelement.get('type') != "outside-reference": audio_id = str(audioelement.get('ref')) csv_name = folder_name +'/' + page_name+'/'+page_name+'-comments-'+audio_id+'.csv' @@ -73,7 +73,10 @@ delimiter=',', dialect="excel", quoting=csv.QUOTE_ALL) - commentstr = audioelement.find("./comment/response").text + try: + commentstr = audioelement.find("./comment/response").text + except AttributeError: + commentstr = "" valuestr = audioelement.find("./value").text if commentstr is None: diff -r 56e72cd18404 -r 231d2186f3fc python/commentquestion_parser.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/python/commentquestion_parser.py Fri Jul 14 15:39:24 2017 +0100 @@ -0,0 +1,78 @@ +#!/usr/bin/python + +import xml.etree.ElementTree as ET +import os +import sys +import csv + +# COMMAND LINE ARGUMENTS + +assert len(sys.argv)<3, "commentquestion_parser takes at most 1 command line argument\nUse: python commentquestion_parser.py [rating_folder_location]" + +# XML results files location +if len(sys.argv) == 1: + folder_name = "../saves" # Looks in 'saves/' folder from 'scripts/' folder + print("Use: python commentquestion_parser.py [rating_folder_location]") + print("Using default path: " + folder_name) +elif len(sys.argv) == 2: + folder_name = sys.argv[1] # First command line argument is folder + +# check if folder_name exists +if not os.path.exists(folder_name): + #the file is not there + print("Folder '"+folder_name+"' does not exist.") + sys.exit() # terminate script execution +elif not os.access(os.path.dirname(folder_name), os.W_OK): + #the file does exist but write privileges are not given + print("No write privileges in folder '"+folder_name+"'.") + +# create folder 'ratings' if not yet created +if not os.path.exists(folder_name + '/comments'): + os.makedirs(folder_name + '/comments') + +pagestore = {} + +for filename in os.listdir(folder_name): + if (filename.endswith(".xml")): + tree = ET.parse(folder_name + '/' + filename) + root = tree.getroot() + + subject_id = root.get('key'); + + # get the list of pages + for page in root.findall("./page"): + pagename = page.get("ref") + if pagename is None: # ignore 'empty' audio_holders + print("WARNING: " + filename + " contains empty audio holder. (commentquestion_parser.py)") + break + + if page.get('state') != "complete": + print("WARNING: " + filename + " contains incomplete page " +pagename+ ". (commentquestion_parser.py)") + break + try: + questionStore = pagestore[pagename] + except KeyError: + questionStore = {} + pagestore[pagename] = questionStore + + for cq in page.findall("./comment"): + cqid = cq.get("id"); + response = cq.find("./response").text + try: + commentStore = questionStore[cqid] + except KeyError: + commentStore = []; + questionStore[cqid] = commentStore + commentStore.append({"subject": subject_id, "value": response}) + +for page in pagestore.keys(): + print page + pagedir = folder_name + '/comments/'+page + if not os.path.exists(pagedir): + os.makedirs(pagedir) + for comment in pagestore[page].keys(): + with open(pagedir+"/"+comment+".csv", "w") as csvfile: + filewriter = csv.writer(csvfile, delimiter=',') + filewriter.writerow(("save_id", "value")) + for entry in pagestore[page][comment]: + filewriter.writerow((entry["subject"], entry["value"])) \ No newline at end of file diff -r 56e72cd18404 -r 231d2186f3fc python/pythonServer.py --- a/python/pythonServer.py Fri Jul 14 15:37:53 2017 +0100 +++ b/python/pythonServer.py Fri Jul 14 15:39:24 2017 +0100 @@ -137,16 +137,27 @@ global curFileName global curSaveIndex options = self.path.rsplit('?') - options = options[1].rsplit('=') - key = options[1] + options = options[1].rsplit('&') + for option in options: + optionPair = option.rsplit('=') + if optionPair[0] == "key": + key = optionPair[1] + elif optionPair[0] == "saveFilenamePrefix": + prefix = optionPair[1] + if key == None: + self.send_response(404) + return + if prefix == None: + prefix = "save" varLen = int(self.headers['Content-Length']) postVars = self.rfile.read(varLen) print("Saving file key "+key) - file = open('../saves/save-'+key+'.xml','wb') + filename = prefix+'-'+key+'.xml' + file = open('../saves/'+filename,'wb') file.write(postVars) file.close() try: - wbytes = os.path.getsize('../saves/save-'+key+'.xml') + wbytes = os.path.getsize('../saves/'+filename) except OSError: self.send_response(200) self.send_header("Content-type", "text/xml") @@ -155,7 +166,7 @@ self.send_response(200) self.send_header("Content-type", "text/xml") self.end_headers() - reply = 'OK"saves/'+curFileName+'"' + reply = 'OK"saves/'+filename+'"' if sys.version_info[0] == 2: self.wfile.write(reply) elif sys.version_info[0] == 3: diff -r 56e72cd18404 -r 231d2186f3fc python/score_parser.py --- a/python/score_parser.py Fri Jul 14 15:37:53 2017 +0100 +++ b/python/score_parser.py Fri Jul 14 15:39:24 2017 +0100 @@ -85,7 +85,7 @@ audioElement = page.find("./audioelement/[@ref='"+ fragmentname+ "']") # Get the element for value in audioElement.findall('./value'): axisName = value.get('interface-name') - if axisName == None: + if axisName == None or axisName == "null": axisName = 'default' axisStore = storage[page_name]['axis'][axisName] if hasattr(value, 'text'): diff -r 56e72cd18404 -r 231d2186f3fc test.html --- a/test.html Fri Jul 14 15:37:53 2017 +0100 +++ b/test.html Fri Jul 14 15:39:24 2017 +0100 @@ -37,6 +37,7 @@

+ diff -r 56e72cd18404 -r 231d2186f3fc test_create.html --- a/test_create.html Fri Jul 14 15:37:53 2017 +0100 +++ b/test_create.html Fri Jul 14 15:39:24 2017 +0100 @@ -1,31 +1,1086 @@ - - - - - - - - - - - - - - -
-
-
- - - + + + + + + + + + + + + + + + WAET 1.2.1 Test Creator + + + +
+
+

Web Audio Evaluation Tool - Test Creator

+
+ + +
+
+
+ +

Invalid Specification!

+
+
+

Your specification is invalid. Please fix the following issues!

+
    +
  • Errors
  • +
+
+
+ +
+
+

Setup

+
+
+ Interface: + +
+
+ Save URL: + +
+
+ Exit URL: + +
+
+ Randomise Page Order: + +
+
+ Page Pool Size: + +
+
+ Loudness Normalisation (LUFS): + +
+
+ Fixed Sampling Rate: + +
+
+ Pre-Test audio calibration: + +
+
+ Global Cross-fade time: + +
+
+ Global Fragment Pre-Silence: + +
+
+ Global Fragment Post-Silence: + +
+
+ Play audio one-at-a-time: + +
+
+ Minimum number of fragment plays + +
+
+ Maximum number of fragment plays + +
+
+
+

Test Completed Message

+ +
+
+

Session Metrics

+
+
+ Collect Total Test Time: + +
+
+ Collect Fragment Listen Time: + +
+
+ Collect Fragment Initial Position: + +
+
+ Collect Fragment Movements: + +
+
+ Collect Fragment Listened To Flag: + +
+
+ Collect Fragment Moved Flag: + +
+
+ Collect Fragment Listened Flag: + +
+
+
+
+

Pre Test Survey

+ +
+

Survey Entry

+ +
+
+ Survey Type: + +
+
+ Unique Survey Entry ID: + +
+
+ Entry Name: + +
+
+ Mandatory: + +
+
+ Minimum Wait Time (s): + +
+
+ Box Size: + +
+
+ Minimum Selected: + +
+
+ Maximum Selected: + +
+
+ Minimum Value: + +
+
+ Maximum Value: + +
+
+ Video URL: + +
+
+
+

Statement

+ +
+
+

Options

+
+ +
+
+
+
+ +
+
+ Name: + +
+
+ Displayed Text: + +
+
+
+
+
+

Conditionals

+ +
+
+
+ +
+
+ Check Type: + +
+
+ Value: + +
+
+ Jump To On Pass: + +
+
+ Jump To On Fail: + +
+
+
+
+
+
+
+

Post Test Survey

+ +
+

Survey Entry

+ +
+
+ Survey Type: + +
+
+ Unique Survey Entry ID: + +
+
+ Entry Name: + +
+
+ Mandatory: + +
+
+ Minimum Wait Time (s): + +
+
+ Box Size: + +
+
+ Minimum Selected: + +
+
+ Maximum Selected: + +
+
+ Minimum Value: + +
+
+ Maximum Value: + +
+
+ Video URL: + +
+
+
+

Statement

+ +
+
+

Options

+
+ +
+
+
+
+ +
+
+ Name: + +
+
+ Displayed Text: + +
+
+
+
+
+

Conditionals

+ +
+
+
+ +
+
+ Check Type: + +
+
+ Value: + +
+
+ Jump To On Pass: + +
+
+ Jump To On Fail: + +
+
+
+
+
+
+
+

Interface (Globals)

+
+
+
+ Check all fragments played: + +
+
+ Check all fragments fully played: + +
+
+ Check all fragments have been moved: + +
+
+ Check all fragments have comments: + +
+
+ Enforce a scale usage: + + Minimum: + + Maximum: + +
+
+ Show master volume control: + +
+
+ Show playhead: + +
+
+ Show Page Count: + +
+
+ Show Fragment Comments: + +
+
+
+
+
+
+ +
+
+

Page

+ +
+
+ Unique ID: + +
+
+ Fragment common-root URL: + +
+
+ Randomise Fragment Order: + +
+
+ Repeat Page N-times: + +
+
+ Loop audio: + +
+
+ Synchronous audio playback: + +
+
+ Loudness (page): + +
+
+ Label type: + +
+
+ Label Start: + +
+
+ Fragment pool size: + +
+
+ Always include page: + +
+
+ Fixed Page Position: + +
+
+ Fragment pre-silence: + +
+
+ Fragment post-silence: + +
+
+ Cannot interupt audio: + +
+
+ Only move playing audio: + +
+
+ Minimum number of fragment plays + +
+
+ Maximum number of fragment plays + +
+
+
+

Page Title

+ +
+
+

Comment box text prefix

+ +

Example: + {{page.commentboxprefix}} A +

+
+
+

Pre Page Survey

+ +
+

Survey Entry

+ +
+
+ Survey Type: + +
+
+ Unique Survey Entry ID: + +
+
+ Entry Name: + +
+
+ Mandatory: + +
+
+ Minimum Wait Time (s): + +
+
+ Box Size: + +
+
+ Minimum Selected: + +
+
+ Maximum Selected: + +
+
+ Minimum Value: + +
+
+ Maximum Value: + +
+
+ Video URL: + +
+
+
+

Statement

+ +
+
+

Options

+
+ +
+
+
+
+ +
+
+ Name: + +
+
+ Displayed Text: + +
+
+
+
+
+

Conditionals

+ +
+
+
+ +
+
+ Check Type: + +
+
+ Value: + +
+
+ Jump To On Pass: + +
+
+ Jump To On Fail: + +
+
+
+
+
+
+
+

Post Page Survey

+ +
+

Survey Entry

+ +
+
+ Survey Type: + +
+
+ Unique Survey Entry ID: + +
+
+ Entry Name: + +
+
+ Mandatory: + +
+
+ Minimum Wait Time (s): + +
+
+ Box Size: + +
+
+ Minimum Selected: + +
+
+ Maximum Selected: + +
+
+ Minimum Value: + +
+
+ Maximum Value: + +
+
+ Video URL: + +
+
+
+

Statement

+ +
+
+

Options

+
+ +
+
+
+
+ +
+
+ Name: + +
+
+ Displayed Text: + +
+
+
+
+
+

Conditionals

+ +
+
+
+ +
+
+ Check Type: + +
+
+ Value: + +
+
+ Jump To On Pass: + +
+
+ Jump To On Fail: + +
+
+
+
+
+
+ +
+

Interface

+ +
+
+
+ Check all fragments played: + +
+
+ Check all fragments fully played: + +
+
+ Check all fragments have been moved: + +
+
+ Check all fragments have comments: + +
+
+ Enforce a scale usage: + + Minimum: + + Maximum: + +
+
+ Show master volume control: + +
+
+ Show playhead: + +
+
+ Show Page Count: + +
+
+ Show Fragment Comments: + +
+
+
+
+

Axis Title

+ +
+
+ Axis name (in saves): + +
+
+
+
+

Axis Image

+ +
+
+

Axis Scales

+ + + +
+
+
+ +
+
+ Position: + +
+
+ Text: + +
+
+
+
+
+
+

Comment Questions

+ +
+ +
+
+ Unique ID: + +
+
+ Common Name: + +
+
+ Minimum: + +
+
+ Maximum: + +
+
+ Step size: + +
+
+ Initial Value: + +
+
+
+

Question:

+ +
+
+

Options

+
+
+
+ +
+
+ Name: + +
+
+ Display Text: + +
+
+
+
+
+
+ +
+

Audio Fragment

+ +
+
+ Unique ID: + +
+
+ URL: + + Full URL: {{page.hostURL}}{{fragment.url}} +
+
+ Fragment Gain (dB): + +
+
+ Fragment Label: + +
+
+ Fragment Common name: + +
+
+ Fragment Type: + +
+
+ Anchor must be below: + +
+
+ Reference must be above: + +
+
+ Loudness: + +
+
+ Always include fragment: + +
+
+ Fragment Pre-Silence: + +
+
+ Fragment Post-Silence: + +
+
+ Fragment playback start position (s): + +
+
+ Fragment playback stop position (s): + +
+
+ Fragment sampling rate: + +
+
+ Fragment Image (URL): + +
+
+ Minimum number of plays + +
+
+ Maximum number of plays + +
+
+
+
+
+
+ +
+
+ + + diff -r 56e72cd18404 -r 231d2186f3fc test_create/attributes.json --- a/test_create/attributes.json Fri Jul 14 15:37:53 2017 +0100 +++ b/test_create/attributes.json Fri Jul 14 15:39:24 2017 +0100 @@ -1,35 +1,1 @@ -{ - "id": "ID", - "mandatory": "Mandatory", - "name": "Name", - "interface": "Interface Module", - "projectReturn": "Save Return URL", - "returnURL": "On complete redirect URL", - "randomiseOrder": "Randomise Order", - "testPages": "Test Pages", - "loudness": "Target Loudness (LUFS)", - "sampleRate": "Required Sample Rate", - "hostURL": "Element URL Prefix", - "repeatCount": "Repeat Count", - "loop": "Loop playback", - "synchronous": "Synchronous playback", - "type": "Type", - "min": "Minimum", - "max": "Maximum", - "position": "Position", - "url": "URL", - "gain": "Gain (dB)", - "marker": "Marker", - "boxsize": "Box Size", - "label": "Label", - "calibration": "Perform Calibration", - "preSilence": "Pre Silence", - "postSilence": "Post Silence", - "poolSize": "Pool Size", - "alwaysInclude": "Always Include", - "crossFade": "Cross Fade", - "check": "Check", - "value": "Value", - "jumpToOnPass": "Jump To ID On Pass", - "jumpToOnFail": "Jump To ID On Fail" -} + diff -r 56e72cd18404 -r 231d2186f3fc test_create/custom.css --- a/test_create/custom.css Fri Jul 14 15:37:53 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -div#content > div.node { - background-color: rgb(200, 228, 151); -} -div#content > div#setup { - background-color: coral; -} -input:disabled+span { - text-decoration: line-through; -} -div.attribute { - float: none; -} -div.attribute input { - max-width: 100%; - width: 300px; -} -div.attribute input[type=radio], -div.attribute input[type=checkbox] { - width: 10px; -} diff -r 56e72cd18404 -r 231d2186f3fc test_create/interface-specs.xml --- a/test_create/interface-specs.xml Fri Jul 14 15:37:53 2017 +0100 +++ b/test_create/interface-specs.xml Fri Jul 14 15:39:24 2017 +0100 @@ -500,7 +500,7 @@ - Each page has only two audio fragments. The user must select one of the two fragments to proceed. There can be one hidden reference. + A page contains a number of audio fragments. The user must select one of the fragments to proceed. There can be a hidden reference. @@ -520,7 +520,7 @@ - Each page has two audio fragments presented as A and B. The test duplicates one of the fragments and presents it as X. The user must choose which, out of A or B, is closest to X. + Each page has a number of audio fragments presented as A and B (and C, ...). The test duplicates one of the fragments and presents it as X. The user must choose which, out of A or B (or C, ...), is closest to X. diff -r 56e72cd18404 -r 231d2186f3fc test_create/interfaces/specifications.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test_create/interfaces/specifications.json Fri Jul 14 15:39:24 2017 +0100 @@ -0,0 +1,573 @@ +{ + "interfaces": [ + { + "name": "Audio Perceptual Evaluation (APE)", + "interface": "APE", + "description": { + "en": "Audio Perceptual Evaluation. A multi-stimulus test where each audio fragment is shown on one continuous slider. Fragments are randomnly positioned along the slider. The user clicks a fragment to play and drags to move." + }, + "checks": [], + "show": [], + "elements": [] + }, { + "name": "MUSHRA", + "interface": "MUSHRA", + "description": { + "en": "Multi-stimulus with hidden reference and anchor. Each fragment is shown on its own vertical slider. One fragment must be labelled as a reference and another labelled as an anchor. One external reference must also be shown." + }, + "scales": ["ACR"], + "checks": [{ + "name": "fragmentMoved", + "support": "none" + }, { + "name": "fragmentPlayed", + "support": "none" + }, { + "name": "fragmentFullPlayback", + "support": "none" + }, { + "name": "fragmentComments", + "support": "none" + }, { + "name": "scalerange", + "support": "none" + }], + "show": [{ + "name": "volume", + "support": "none" + }, { + "name": "page-count", + "support": "none" + }, { + "name": "playhead", + "support": "none" + }, { + "name": "comments", + "support": "none" + }], + "elements": [{ + "anchor": { + "min": 1, + "max": "undefined" + }, + "reference": { + "min": 1, + "max": "undefined" + }, + "outsidereference": { + "min": 1, + "max": "undefined" + } + }] + }, { + "name": "Vertical Sliders", + "interface": "MUSHRA", + "description": { + "en": "Each element is given its own vertical slider with user defined scale markers." + } + }, { + "name": "Horizontal Sliders", + "interface": "horizontal", + "description": { + "en": "Each element is given its own horizontal slider with user defined scale markers." + } + }, { + "name": "Discrete", + "interface": "discrete", + "description": { + "en": "Each element is given a horizontal scale broken into a number of discrete choices. The number of choices is defined by the scale markers." + } + }, { + "name": "Rank", + "interface": "ordinal", + "description": { + "en": "Each stimulus is placed on a discrete scale equalling the number of fragments. The fragments are then ranked based on the question posed. Only one element can occupy a rank position" + } + }, { + "name": "Likert", + "interface": "discrete", + "description": { + "en": "Each stimulus is placed on a discrete scale. The scale is fixed to the Likert scale options of 'Strongly Disagree', 'Disagree', 'Neutral', 'Agree' and 'Strongly Agree'" + }, + "scales": ["Likert"], + "checks": [{ + "name": "fragmentPlayed", + "support": "none" + }, { + "name": "fragmentFullPlayback", + "support": "none" + }, { + "name": "fragmentComments", + "support": "none" + }], + "show": [{ + "name": "volume", + "support": "none" + }, { + "name": "page-count", + "support": "none" + }, { + "name": "playhead", + "support": "none" + }, { + "name": "comments", + "support": "none" + }] + }, { + "name": "ABC/HR", + "interface": "MUSHRA", + "description": { + "en": "Each stimulus is placed on a vertical slider. The scale is fixed with the labels 'Imperceptible' to 'Very Annoying'" + }, + "scales": ["ABC"], + "checks": [{ + "name": "fragmentMoved", + "support": "none" + }, { + "name": "fragmentPlayed", + "support": "none" + }, { + "name": "fragmentFullPlayback", + "support": "none" + }, { + "name": "fragmentComments", + "support": "none" + }], + "show": [{ + "name": "volume", + "support": "none" + }, { + "name": "page-count", + "support": "none" + }, { + "name": "playhead", + "support": "none" + }, { + "name": "comments", + "support": "none" + }] + }, { + "name": "Bipolar", + "interface": "horizontal", + "description": { + "en": "Each stimulus is placed on a horizontal slider and initialised to the value '0'. The scale operates from -50 to +5-. In the results this is normalised, like all other interfaces, from 0 (-50) to 1 (+50)" + }, + "scales": ["Bipolar"], + "checks": [{ + "name": "fragmentMoved", + "support": "none" + }, { + "name": "fragmentPlayed", + "support": "none" + }, { + "name": "fragmentFullPlayback", + "support": "none" + }, { + "name": "fragmentComments", + "support": "none" + }], + "show": [{ + "name": "volume", + "support": "none" + }, { + "name": "page-count", + "support": "none" + }, { + "name": "playhead", + "support": "none" + }, { + "name": "comments", + "support": "none" + }] + }, { + "name": "Absolute Category Rating", + "interface": "discrete", + "description": { + "en": "Each element is on a discrete scale of 'Bad', 'Poor', 'Fair', 'Good' and 'Excellent'. Each element must be given a rating." + }, + "scales": ["ACR"], + "checks": [{ + "name": "fragmentMoved", + "support": "mandatory" + }, { + "name": "fragmentPlayed", + "support": "none" + }, { + "name": "fragmentFullPlayback", + "support": "none" + }, { + "name": "fragmentComments", + "support": "none" + }], + "show": [{ + "name": "volume", + "support": "none" + }, { + "name": "page-count", + "support": "none" + }, { + "name": "playhead", + "support": "none" + }, { + "name": "comments", + "support": "none" + }] + }, { + "name": "Discrete Category Rating", + "interface": "discrete", + "description": { + "en": "" + }, + "scales": ["DCR"], + "checks": [{ + "name": "fragmentPlayed", + "support": "none" + }, { + "name": "fragmentFullPlayback", + "support": "none" + }, { + "name": "fragmentComments", + "support": "none" + }], + "show": [{ + "name": "volume", + "support": "none" + }, { + "name": "page-count", + "support": "none" + }, { + "name": "playhead", + "support": "none" + }, { + "name": "comments", + "support": "none" + }] + }, { + "name": "Hedonic Cat. Rating", + "interface": "MUSHRA", + "description": { + "en": "" + }, + "scales": ["Hedonic Category Rating Scale"], + "checks": [{ + "name": "fragmentMoved", + "support": "mandatory" + }, { + "name": "fragmentPlayed", + "support": "none" + }, { + "name": "fragmentFullPlayback", + "support": "none" + }, { + "name": "fragmentComments", + "support": "none" + }], + "show": [{ + "name": "volume", + "support": "none" + }, { + "name": "page-count", + "support": "none" + }, { + "name": "playhead", + "support": "none" + }, { + "name": "comments", + "support": "none" + }], + "elements": { + "outsidereference": { + "min": 1, + "max": 1 + } + } + }, { + "name": "ITUR5PCIS", + "interface": "MUSHRA", + "description": { + "en": "" + }, + "scales": ["ABC"], + "checks": [{ + "name": "fragmentMoved", + "support": "none" + }, { + "name": "fragmentPlayed", + "support": "none" + }, { + "name": "fragmentFullPlayback", + "support": "none" + }, { + "name": "fragmentComments", + "support": "none" + }], + "show": [{ + "name": "volume", + "support": "none" + }, { + "name": "page-count", + "support": "none" + }, { + "name": "playhead", + "support": "none" + }, { + "name": "comments", + "support": "none" + }], + "elements": { + "outsidereference": { + "min": 1, + "max": 1 + } + } + }, { + "name": "Pairwise", + "interface": "AB", + "description": { + "en": "A discrete interface where each page holds each fragment. The user must select one fragment. All other fragments are not selected" + }, + "hasScales": "false", + "elements": { + "number": { + "min": 2, + "max": "undefined" + } + } + }, { + "name": "AB", + "interface": "AB", + "description": { + "en": "Each page contains two audio fragments. The user must select one of the fragments to proceed. There can be an outside reference." + }, + "hasScales": "false", + "checks": [{ + "name": "fragmentPlayed", + "support": "mandatory" + }], + "elements": { + "number": { + "min": 2, + "max": 2 + }, + "outsidereference": { + "min": 0, + "max": 1 + } + } + }, { + "name": "ABX", + "interface": "ABX", + "description": { + "en": "Each page has two audio fragments presented as A and B. The test duplicates one of the fragments and presents it as X. The user must choose which, out of A or B, is closest to X." + }, + "hasScales": "false", + "checks": [{ + "name": "fragmentPlayed", + "support": "mandatory" + }], + "elements": { + "number": { + "min": 2, + "max": 2 + }, + "outsidereference": { + "min": 0, + "max": 1 + } + } + }, { + "name": "Timeline", + "interface": "timeline", + "description": { + "en": "Each fragment is displayed with a clickable waveform of itself. The user must click on the waveform at the location that a specific event occured. Users can then enter in information about this event. This test is unit-/value-less." + } + } + ], + "scales": [ + { + "name": "Likert", + "scales": [ + { + "text": "Strongly Disagree", + "position": 0 + }, + { + "text": "Disagree", + "position": 25 + }, + { + "text": "Neutral", + "position": 50 + }, + { + "text": "Agree", + "position": 75 + }, + { + "text": "Strongly Agree", + "position": 100 + } + ] + }, { + "name": "ABC", + "scales": [ + { + "text": "Very annoying", + "position": 0 + }, + { + "text": "Annoying", + "position": 25 + }, + { + "text": "Slightly annoying", + "position": 50 + }, + { + "text": "Perceptible but not annoying", + "position": 75 + }, + { + "text": "Imperceptible", + "position": 100 + } + ] + }, { + "name": "Bipolar", + "scales": [ + { + "text": "-50", + "position": 0 + }, + { + "text": "0", + "position": 50 + }, + { + "text": "50", + "position": 100 + } + ] + }, { + "name": "ACR", + "scales": [ + { + "text": "Bad", + "position": 0 + }, + { + "text": "Poor", + "position": 25 + }, + { + "text": "Fair", + "position": 50 + }, + { + "text": "Good", + "position": 75 + }, + { + "text": "Excellent", + "position": 100 + } + ] + }, { + "name": "DCR", + "scales": [ + { + "text": "(1) Very Annoying", + "position": 0 + }, + { + "text": "(2) Annoying", + "position": 25 + }, + { + "text": "(3) Slightly Annoying", + "position": 50 + }, + { + "text": "(4) Audible but not Annoying", + "position": 75 + }, + { + "text": "(5) Inaudible", + "position": 100 + } + ] + }, { + "name": "CCR", + "scales": [ + { + "text": "Much Worse", + "position": 12 + }, + { + "text": "Worse", + "position": 25 + }, + { + "text": "Slightly Worse", + "position": 38 + }, + { + "text": "About the same", + "position": 50 + }, + { + "text": "Slightly Better", + "position": 62 + }, + { + "text": "Better", + "position": 75 + }, + { + "text": "Much Better", + "position": 88 + } + ] + }, { + "name": "Hedonic Category Rating Scale", + "scales": [ + { + "text": "Dislike Extremeley", + "position": 10 + }, + { + "text": "Dislike Very Much", + "position": 20 + }, + { + "text": "Dislike Moderate", + "position": 30 + }, + { + "text": "Dislike Slightly", + "position": 40 + }, + { + "text": "Neither like nor dislike", + "position": 50 + }, + { + "text": "Like Slightly", + "position": 60 + }, + { + "text": "Like Moderate", + "position": 70 + }, + { + "text": "Like Very Much", + "position": 80 + }, + { + "text": "Like Extremely", + "position": 90 + } + ] + } + ] +} diff -r 56e72cd18404 -r 231d2186f3fc test_create/style.css --- a/test_create/style.css Fri Jul 14 15:37:53 2017 +0100 +++ b/test_create/style.css Fri Jul 14 15:39:24 2017 +0100 @@ -1,131 +1,95 @@ -div#blanket { - z-index: 2; - background-color: rgba(0, 0, 0, 0.5); +#screenblank { + z-index: 1; width: 100%; height: 100%; position: fixed; + top: 0px; left: 0px; + background-color: rgba(0, 0, 0, 0.75); +} +#popupHolder { + text-align: center; + width: 100%; + height: 100%; + position: fixed; top: 0px; + z-index: 2; } -div#popupHolder { +.popup { + position: relative; z-index: 3; background-color: rgba(255, 255, 255, 1); width: 730px; height: 480px; - position: fixed; + display: inline-block; + text-align: left; border-radius: 10px; box-shadow: 0px 0px 50px #000; padding: 10px; + margin-top: 20px; } -div#popup-title-holder { +.popupTitle { width: 100%; height: 50px; font-size: 2em; + text-align: center; } -button.popup-button { - width: 60px; - height: 27px; - padding: 5px; +.popupButtons { position: absolute; - bottom: 10px; + bottom: 5px; + width: 90%; + margin-left: 30px; + display: block; + align-self: center; } -button#popup-proceed { - right: 10px; +#popupBack { + float: left; } -button#popup-back { - left: 10px; +#popupNext { + float: right; } -div.drag-area { - border: 3px black dashed; +#introdragdrop { + width: 100%; + height: 100px; + border: 2px dashed black; + font-size: 1.5em; + text-align: center; + padding-top: 30px; + color: grey; } -div.drag-over { - background-color: aquamarine; +.new-test { + cursor: pointer; } -div.drag-dropped { - background-color: aqua; +.new-test:hover { + font-style: italic; } -div.drag-error { - background-color: coral +.node { + padding: 10px 20px; + border: 2px solid black; + margin: 20px; + border-radius: 20px; + background-color: inherit; } -div#project-drop { - width: 99%; - height: 50px; - margin: 10px 0px; +.node > textarea { + width: 80%; } -div.popup-checkbox { - padding: 5px; +.node > h1, +h2, +h3, +h4, +h5 { + text-align: center; } -div.popup-checkbox input { - margin: 0px 5px; +.attribute { + display: inline-block; + margin: 0px 10px; + border-left: 1px solid grey; + border-right: 1px solid grey; + padding: 5px 5px; } -div.popup-option-entry { - padding: 5px 0px; - border-bottom: 1px solid; +#setupNode { + background-color: rgba(255, 10, 10, 0.25); } -div.disabled { - color: rgb(100, 100, 100); +.pageNode { + background-color: rgba(10, 255, 10, 0.25); } -div#page-holder > div.node { - background-color: rgb(200, 228, 151); -} -div#content > div#setup { - background-color: coral; -} -div.node { - float: left; - padding: 10px; - border: black 2px solid; - border-radius: 10px; - margin: 10px; - min-width: 92%; - background-color: rgba(255, 255, 255, 0.5); -} -div.node-title { - float: left; - width: 100%; - font-size: 2em; - margin: 5px 0px; -} -div.node-attributes { - min-width: 92%; - float: none; - padding: 10px; -} -div.attribute { - float: left; - margin-right: 10px; -} -div.node-children { - float: left; - min-width: 92%; -} -div.node-buttons { - float: left; - min-width: 92%; -} -div.attribute input { - max-width: 100%; - width: 300px; - margin-right: 10px; -} -div.attribute input[type=number] { - width: 80px; -} -div.attribute input[type=radio], -div.attribute input[type=checkbox] { - width: 10px; -} -input:disabled+label { - text-decoration: line-through; -} -div.survey-entry-attribute { - margin: 10px 0px; - border: 1px gray solid; - border-radius: 5px; - height: 40px; - line-height: 40px; - padding: 0px 10px; -} -div.survey-entry-attribute span { - margin-right: 10px; -} diff -r 56e72cd18404 -r 231d2186f3fc test_create/test_core.js --- a/test_create/test_core.js Fri Jul 14 15:37:53 2017 +0100 +++ b/test_create/test_core.js Fri Jul 14 15:39:24 2017 +0100 @@ -1,2339 +1,453 @@ -var interfaceSpecs; -var xmlHttp; -var popupObject; -var popupStateNodes; -var specification; -var convert; -var attributeText; -var page_lang = "en"; +/* globals document, angular, window, Promise, XMLHttpRequest, Specification, XMLSerializer, Blob, DOMParser, FileReader, $*/ +function get(url) { + // Return a new promise. + return new Promise(function (resolve, reject) { + // Do the usual XHR stuff + var req = new XMLHttpRequest(); + req.open('GET', url); -// Firefox does not have an XMLDocument.prototype.getElementsByName -// and there is no searchAll style command, this custom function will -// search all children recusrively for the name. Used for XSD where all -// element nodes must have a name and therefore can pull the schema node -XMLDocument.prototype.getAllElementsByName = function (name) { - name = String(name); - var selected = this.documentElement.getAllElementsByName(name); - return selected; + req.onload = function () { + // This is called even on 404 etc + // so check the status + if (req.status == 200) { + // Resolve the promise with the response text + resolve(req.response); + } else { + // Otherwise reject with the status text + // which will hopefully be a meaningful error + reject(Error(req.statusText)); + } + }; + + // Handle network errors + req.onerror = function () { + reject(Error("Network Error")); + }; + + // Make the request + req.send(); + }); } -Element.prototype.getAllElementsByName = function (name) { - name = String(name); - var selected = []; - var node = this.firstElementChild; - while (node != null) { - if (node.getAttribute('name') == name) { - selected.push(node); - } - if (node.childElementCount > 0) { - selected = selected.concat(node.getAllElementsByName(name)); - } - node = node.nextElementSibling; - } - return selected; +var AngularInterface = angular.module("creator", []); + +var specification = new Specification(); + +window.onload = function () { + // Get the test interface specifications + $(function () { + $('[data-toggle="popover"]').popover(); + }); +}; + +function handleFiles(event) { + var s = angular.element(event.currentTarget).scope(); + s.handleFiles(event); + s.$apply(); } -XMLDocument.prototype.getAllElementsByTagName = function (name) { - name = String(name); - var selected = this.documentElement.getAllElementsByTagName(name); - return selected; -} +AngularInterface.controller("view", ['$scope', '$element', '$window', function ($s, $e, $w) { + $s.popupVisible = true; + $s.testSpecifications = {}; -Element.prototype.getAllElementsByTagName = function (name) { - name = String(name); - var selected = []; - var node = this.firstElementChild; - while (node != null) { - if (node.nodeName == name) { - selected.push(node); + (function () { + new Promise(function (resolve, reject) { + var xml = new XMLHttpRequest(); + xml.open("GET", "test_create/interfaces/specifications.json"); + xml.onload = function () { + if (xml.status === 200) { + resolve(xml.responseText); + return; + } + reject(xml.status); + }; + xml.onerror = function () { + reject(new Error("Network Error")); + }; + xml.send(); + }).then(JSON.parse).then(function (data) { + $s.testSpecifications = data; + $s.$apply(); + }); + })(); + + $s.showPopup = function () { + $s.popupVisible = true; + }; + $s.hidePopup = function () { + $s.popupVisible = false; + }; + $s.globalSchema = undefined; + get("xml/test-schema.xsd").then(function (text) { + specification.processSchema(text); + $s.globalSchema = specification.getSchema(); + }); + $s.specification = specification; + $s.selectedTestPrototype = undefined; + $s.setTestPrototype = function (obj) { + $s.selectedTestPrototype = obj; + $w.specification.interface = obj.interface; + } + + $s.addPage = function () { + $s.specification.createNewPage(); + }; + + $s.removePage = function (page) { + var index = $s.specification.pages.findIndex(function (a) { + return a == page; + }); + if (index === -1) { + throw ("Invalid Page"); } - if (node.childElementCount > 0) { - selected = selected.concat(node.getAllElementsByTagName(name)); + $s.specification.pages.splice(index, 1); + }; + + $s.exportXML = function () { + var s = new XMLSerializer(); + var doc = specification.encode(); + var xmlstr = s.serializeToString(doc); + var bb = new Blob([s.serializeToString(doc)], { + type: 'application/xml' + }); + var dnlk = window.URL.createObjectURL(bb); + var a = document.createElement("a"); + a.href = dnlk; + a.download = "test.xml"; + a.click(); + window.URL.revokeObjectURL(dnlk); + }; + $s.validated = false; + $s.showValidationMessages = false; + $s.validate = function () { + var s = new XMLSerializer(); + var Module = { + xml: s.serializeToString(specification.encode()), + schema: specification.getSchemaString(), + arguments: ["--noout", "--schema", 'test-schema.xsd', 'document.xml'] + }; + var xmllint = validateXML(Module); + console.log(xmllint); + if (xmllint != 'document.xml validates\n') { + $s.validated = false; + var list = $e[0].querySelector("#validation-error-list"); + while (list.firstChild) { + list.removeChild(list.firstChild); + } + var errors = xmllint.split('\n'); + errors = errors.slice(0, errors.length - 2); + errors.forEach(function (str) { + var li = document.createElement("li"); + li.textContent = str; + list.appendChild(li); + }); + } else { + $s.validated = true; } - node = node.nextElementSibling; + $s.showValidationMessages = true; } - return selected; -} + $s.hideValidationMessages = function () { + $s.showValidationMessages = false; + } +}]); -// Firefox does not have an XMLDocument.prototype.getElementsByName -if (typeof XMLDocument.prototype.getElementsByName != "function") { - XMLDocument.prototype.getElementsByName = function (name) { - name = String(name); - var node = this.documentElement.firstElementChild; - var selected = []; - while (node != null) { - if (node.getAttribute('name') == name) { - selected.push(node); +AngularInterface.controller("introduction", ['$scope', '$element', '$window', function ($s, $e, $w) { + $s.state = 0; + $s.next = function () { + $s.state++; + if ($s.state > 1 || $s.file) { + $s.hidePopup(); + } + }; + $s.back = function () { + $s.state--; + }; + $s.mouseover = function (name) { + var obj = $s.testSpecifications.interfaces.find(function (i) { + return i.name == name; + }); + if (obj) { + $s.description = obj.description.en; + } + }; + $s.initialise = function (name) { + var obj = $s.testSpecifications.interfaces.find(function (i) { + return i.name == name; + }); + if (obj === undefined) { + throw ("Cannot find specification"); + } + $s.setTestPrototype(obj); + }; + // Get the test interface specifications + $s.file = undefined; + $s.description = ""; + + $s.handleFiles = function ($event) { + $s.file = $event.currentTarget.files[0]; + var r = new FileReader(); + r.onload = function () { + var p = new DOMParser(); + specification.decode(p.parseFromString(r.result, "text/xml")); + $s.$apply(); + }; + r.readAsText($s.file); + }; +}]); + +AngularInterface.controller("setup", ['$scope', '$element', '$window', function ($s, $e, $w) { + function initialise() { + if ($s.globalSchema) { + $s.schema = $s.globalSchema.querySelector("[name=setup]"); + } + } + $s.schema = undefined; + $s.attributes = []; + + $s.$watch("globalSchema", initialise); + $s.$watch("specification.metrics.enabled.length", function () { + var metricsNode = document.getElementById("metricsNode"); + if (!$s.specification.metrics) { + return; + } + metricsNode.querySelectorAll("input").forEach(function (DOM) { + DOM.checked = false; + }); + $s.specification.metrics.enabled.forEach(function (metric) { + var DOM = metricsNode.querySelector("[value=" + metric + "]"); + if (DOM) { + DOM.checked = true; } - node = node.nextElementSibling; - } - return selected; - } -} + }); + }); -window.onload = function () { - specification = new Specification(); - convert = new SpecificationToHTML(); - xmlHttp = new XMLHttpRequest(); - xmlHttp.open("GET", "test_create/interface-specs.xml", true); - xmlHttp.onload = function () { - var parse = new DOMParser(); - interfaceSpecs = parse.parseFromString(xmlHttp.response, 'text/xml'); - buildPage(); - popupObject.postNode(popupStateNodes.state[0]) - } - xmlHttp.send(); - - var xsdGet = new XMLHttpRequest(); - xsdGet.open("GET", "xml/test-schema.xsd", true); - xsdGet.onload = function () { - var parse = new DOMParser(); - specification.schema = parse.parseFromString(xsdGet.response, 'text/xml');; - } - xsdGet.send(); - - var jsonAttribute = new XMLHttpRequest(); - jsonAttribute.open("GET", "test_create/attributes.json", true); - jsonAttribute.onload = function () { - attributeText = JSON.parse(jsonAttribute.response) - } - jsonAttribute.send(); -} - -function buildPage() { - popupObject = new function () { - this.object = document.getElementById("popupHolder"); - this.blanket = document.getElementById("blanket"); - - this.popupTitle = document.createElement("div"); - this.popupTitle.id = "popup-title-holder"; - this.popupTitle.align = "center"; - this.titleDOM = document.createElement("span"); - this.titleDOM.id = "popup-title"; - this.popupTitle.appendChild(this.titleDOM); - this.object.appendChild(this.popupTitle); - - this.popupContent = document.createElement("div"); - this.popupContent.id = "popup-content"; - this.object.appendChild(this.popupContent); - - this.proceedButton = document.createElement("button"); - this.proceedButton.id = "popup-proceed"; - this.proceedButton.className = "popup-button"; - this.proceedButton.textContent = "Next"; - this.proceedButton.onclick = function () { - popupObject.popupContent.innerHTML = null; - if (typeof popupObject.shownObject.continue == "function") { - popupObject.shownObject.continue(); - } else { - popupObject.hide(); + $s.enableMetric = function ($event) { + var metric = $event.currentTarget.value; + var index = specification.metrics.enabled.findIndex(function (a) { + return a == metric; + }); + if ($event.currentTarget.checked) { + if (index == -1) { + specification.metrics.enabled.push(metric); } - }; - this.object.appendChild(this.proceedButton); - - this.backButton = document.createElement("button"); - this.backButton.id = "popup-back"; - this.backButton.className = "popup-button"; - this.backButton.textContent = "Back"; - this.backButton.onclick = function () { - popupObject.popupContent.innerHTML = null; - popupObject.shownObject.back(); - }; - this.object.appendChild(this.backButton); - - this.shownObject; - - this.resize = function () { - var w = window.innerWidth; - var h = window.innerHeight; - this.object.style.left = Math.floor((w - 750) / 2) + 'px'; - this.object.style.top = Math.floor((h - 500) / 2) + 'px'; - } - - this.show = function () { - this.object.style.visibility = "visible"; - this.blanket.style.visibility = "visible"; - if (typeof this.shownObject.back == "function") { - this.backButton.style.visibility = "visible"; - } else { - this.backButton.style.visibility = "hidden"; + } else { + if (index >= 0) { + specification.metrics.enabled.splice(index, 1); } } - - this.hide = function () { - this.object.style.visibility = "hidden"; - this.blanket.style.visibility = "hidden"; - this.backButton.style.visibility = "hidden"; - } - - this.postNode = function (postObject) { - //Passed object must have the following: - // Title: text to show in the title - // Content: HTML DOM to show on the page - // On complete this HTML DOM is destroyed so make sure it is referenced elsewhere for processing - this.titleDOM.textContent = postObject.title; - this.popupContent.appendChild(postObject.content); - this.shownObject = postObject; - if (typeof this.shownObject.back == "function") { - this.backButton.style.visibility = "visible"; - } else { - this.backButton.style.visibility = "hidden"; - } - if (typeof this.shownObject.continue == "function") { - this.proceedButton.textContent = "Next"; - } else { - this.proceedButton.textContent = "Finish"; - } - this.show(); - } - - this.resize(); - this.hide(); }; - popupStateNodes = new function () { - // This defines the several popup states wanted - this.state = []; - this.state[0] = new function () { - this.title = "Welcome"; - this.content = document.createElement("div"); - this.content.id = "state-0"; - var span = document.createElement("span"); - span.textContent = "Welcome to the WAET test creator tool. This will allow you to create a new test from scratch to suit your testing needs. If you wish to update a test file, please drag and drop the XML document into the area below for processing, otherwise press 'Next' to start a new test. This tool generates files for the WAET 1.2.0 version." - this.content.appendChild(span); - this.dragArea = document.createElement("div"); - this.dragArea.className = "drag-area"; - this.dragArea.id = "project-drop"; - this.content.appendChild(this.dragArea); + $s.configure = function () {} - this.dragArea.addEventListener('dragover', function (e) { - e.stopPropagation(); - e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; - e.currentTarget.className = "drag-area drag-over"; + $s.$watch("selectedTestPrototype", $s.configure); +}]); + +AngularInterface.controller("survey", ['$scope', '$element', '$window', function ($s, $e, $w) { + $s.addSurveyEntry = function () { + $s.survey.addOption(); + }; + $s.removeSurveyEntry = function (entry) { + var index = $s.survey.options.findIndex(function (a) { + return a == entry; + }); + if (index === -1) { + throw ("Invalid Entry"); + } + $s.survey.options.splice(index, 1); + }; +}]); + +AngularInterface.controller("surveyOption", ['$scope', '$element', '$window', function ($s, $e, $w) { + + $s.removeOption = function (option) { + var index = $s.opt.options.findIndex(function (a) { + return a == option; + }); + if (index === -1) { + throw ("Invalid option"); + } + $s.opt.options.splice(index, 1); + }; + $s.addOption = function () { + $s.opt.options.push({ + name: "", + text: "" + }); + }; + + $s.addCondition = function () { + $s.opt.conditions.push({ + check: "equals", + value: "", + jumpToOnPass: undefined, + jumpToOnFail: undefined + }); + }; + + $s.removeCondition = function (condition) { + var index = $s.opt.conditions.findIndex(function (c) { + return c == condition; + }); + if (index === -1) { + throw ("Invalid Condition"); + } + $s.opt.conditions.splice(index, 1); + }; +}]); + +AngularInterface.controller("interfaceNode", ['$scope', '$element', '$window', function ($s, $e, $w) { + $s.$watch("interface.options.length", function () { + if (!$s.interface || !$s.interface.options) { + return; + } + var options = $e[0].querySelector(".interfaceOptions").querySelectorAll(".attribute"); + options.forEach(function (option) { + var name = option.getAttribute("name"); + var index = $s.interface.options.findIndex(function (io) { + return io.name == name; }); + option.querySelector("input").checked = (index >= 0); + if (name == "scalerange" && index >= 0) { + option.querySelector("[name=min]").value = $s.interface.options[index].min; + option.querySelector("[name=max]").value = $s.interface.options[index].max; + } + }); + }); + $s.enableInterfaceOption = function ($event) { + var name = $event.currentTarget.parentElement.getAttribute("name"); + var type = $event.currentTarget.parentElement.getAttribute("type"); + var index = $s.interface.options.findIndex(function (io) { + return io.name == name; + }); + if (index == -1 && $event.currentTarget.checked) { + var obj = $s.interface.options.push({ + name: name, + type: type + }); + if (name == "scalerange") { + obj.min = $event.currentTarget.parentElement.querySelector("[name=min]").value; + obj.max = $event.currentTarget.parentElement.querySelector("[name=max]").value; + } + } else if (index >= 0 && !$event.currentTarget.checked) { + $s.interface.options.splice(index, 1); + } + }; + $s.scales = []; + $s.removeScale = function (scale) { + var index = $s.interface.scales.findIndex(function (s) { + return s == scale; + }); + if (index >= 0) { + $s.interface.scales.splice(index, 1); + } + }; + $s.addScale = function () { + $s.interface.scales.push({ + position: undefined, + text: undefined + }); + }; + $s.clearScales = function () { + $s.interface.scales = []; + }; + $s.useScales = function (scale) { + $s.clearScales(); + scale.scales.forEach(function (s) { + $s.interface.scales.push(s); + }); + $s.selectedScale = scale.name; + }; + $s.selectedScale = undefined; - this.dragArea.addEventListener('dragexit', function (e) { - e.stopPropagation(); - e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; - e.currentTarget.className = "drag-area"; + $s.configure = function () { + if ($s.selectedTestPrototype === undefined) { + return; + } + if ($s.selectedTestPrototype.checks && $s.selectedTestPrototype.checks.length >= 1) { + $s.selectedTestPrototype.checks.forEach(function (entry) { + var dom = $e[0].querySelector("[name=\"" + entry.name + "\"] input"); + if (entry.support == "none") { + dom.checked = false; + dom.disabled = true; + } }); - - this.dragArea.addEventListener('drop', function (e) { - e.stopPropagation(); - e.preventDefault(); - e.currentTarget.className = "drag-area drag-dropped"; - var files = e.dataTransfer.files[0]; - var reader = new FileReader(); - reader.onload = function (decoded) { - var parse = new DOMParser(); - specification.decode(parse.parseFromString(decoded.target.result, 'text/xml')); - popupObject.hide(); - popupObject.popupContent.innerHTML = null; - convert.convert(document.getElementById('content')); + } + if ($s.selectedTestPrototype.show && $s.selectedTestPrototype.show.length >= 1) { + $s.selectedTestPrototype.show.forEach(function (entry) { + var dom = $e[0].querySelector("[name=\"" + entry.name + "\"] input"); + if (entry.support == "none") { + dom.checked = false; + dom.disabled = true; } - reader.readAsText(files); }); - - - this.continue = function () { - popupObject.postNode(popupStateNodes.state[1]); + } + if ($s.interface !== specification.interfaces) { + // Page specific interface nodes + if ($s.selectedTestPrototype.hasScales !== undefined && ($s.selectedTestPrototype.hasScales == "false" || $s.selectedTestPrototype.hasScales == false)) { + var elem = $e[0].querySelector("[name=\"scale-selection\"]") + elem.style.visibility = "hidden"; + elem.style.height = "0px"; + } + if ($s.selectedTestPrototype.scales && $s.selectedTestPrototype.show.length >= 1) { + $s.scales = []; + $s.selectedTestPrototype.scales.forEach(function (scalename) { + var obj = $s.testSpecifications.scales.find(function (a) { + return a.name == scalename; + }); + $s.scales.push(obj); + }); + if ($s.selectedTestPrototype.scales.includes($s.selectedScale) == false) { + $s.clearScales(); + } + if ($s.scales.length == 1) { + $s.clearScales(); + $s.useScales($s.scales[0]); + } + } else { + $s.scales = $s.testSpecifications.scales; } } - this.state[1] = new function () { - this.title = "Select your interface"; - this.content = document.createElement("div"); - this.content.id = "state-1"; - var spnH = document.createElement('div'); - var span = document.createElement("span"); - span.textContent = "Please select your interface from the list shown below. This will define the various options which are available. This can later be changed."; - spnH.appendChild(span); - this.content.appendChild(spnH); - this.select = document.createElement("select"); - this.content.appendChild(this.select); - this.description = document.createElement("p"); - this.content.appendChild(this.description); - this.testsXML = interfaceSpecs.getElementsByTagName('tests')[0].getElementsByTagName('test'); - for (var i = 0; i < this.testsXML.length; i++) { - var option = document.createElement('option'); - option.value = this.testsXML[i].getAttribute('name'); - option.textContent = this.testsXML[i].getAttribute('name'); - this.select.appendChild(option); - } - this.handleEvent = function (event) { - var testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(this.select.value)[0]; - var descriptors = testXML.getAllElementsByTagName("description"); - this.description.textContent = ""; - for (var i = 0; i < descriptors.length; i++) { - if (descriptors[i].getAttribute("lang") == page_lang) { - this.description.textContent = descriptors[i].textContent; - } - } - } - this.select.addEventListener("change", this); - this.handleEvent(); - this.continue = function () { - var testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(this.select.value)[0]; - specification.interface = testXML.getAttribute("interface"); - if (specification.interfaces == null) { - specification.interfaces = new specification.interfaceNode(specification); - } - if (specification.metrics == null) { - specification.metrics = new specification.metricNode(); - } - popupStateNodes.state[2].generate(); - popupObject.postNode(popupStateNodes.state[2]); - } - this.back = function () { - popupObject.postNode(popupStateNodes.state[0]); - } + }; + + $s.$watch("selectedTestPrototype", $s.configure); + $s.configure(); +}]); +AngularInterface.controller("page", ['$scope', '$element', '$window', function ($s, $e, $w) { + $s.addInterface = function () { + $s.page.addInterface(); + }; + $s.removeInterface = function (node) { + var index = $s.page.interfaces.findIndex(function (a) { + return a == node; + }); + if (index === -1) { + throw ("Invalid node"); } - this.state[2] = new function () { - this.title = "Test Checks & Restrictions"; - this.content = document.createElement("div"); - this.content.id = "state-1"; - var spnH = document.createElement('div'); - var span = document.createElement("span"); - span.textContent = "Select your test checks and restrictions. Greyed out items are fixed by the test/interface and cannot be changed"; - spnH.appendChild(span); - this.content.appendChild(spnH); - var holder = document.createElement("div"); - this.options = []; - this.testXML = null; - this.interfaceXML = null; - this.dynamicContent = document.createElement("div"); - this.content.appendChild(this.dynamicContent); - this.generate = function () { - this.options = []; - this.dynamicContent.innerHTML = null; - var interfaceName = popupStateNodes.state[1].select.value; - this.checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("checks")[0]; - this.testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0]; - this.interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(this.testXML.getAttribute("interface"))[0].getAllElementsByTagName("checks")[0]; - this.testXML = this.testXML.getAllElementsByTagName("checks"); - var interfaceXMLChildren = this.interfaceXML.getElementsByTagName('entry'); - for (var i = 0; i < interfaceXMLChildren.length; i++) { - var interfaceNode = interfaceXMLChildren[i]; - var checkName = interfaceNode.getAttribute('name'); - var testNode - if (this.testXML.length > 0) { - testNode = this.testXML[0].getAllElementsByName(checkName); - if (testNode.length != 0) { - testNode = testNode[0]; - } else { - testNode = undefined; - } - } else { - testNode = undefined; - } - var obj = { - root: document.createElement("div"), - text: document.createElement("label"), - input: document.createElement("input"), - parent: this, - name: checkName, - handleEvent: function (event) { - if (this.input.checked) { - // Add to specification.interfaces.option - var included = specification.interfaces.options.find(function (element, index, array) { - if (element.name == this.name) { - return true; - } else { - return false; - } - }, this); - if (included == null) { - specification.interfaces.options.push({ - type: "check", - name: this.name - }); - } - } else { - // Remove from specification.interfaces.option - var position = specification.interfaces.options.findIndex(function (element, index, array) { - if (element.name == this.name) { - return true; - } else { - return false; - } - }, this); - if (position >= 0) { - specification.interfaces.options.splice(position, 1); - } - } - } - } + $s.page.interfaces.splice(index, 1); + }; - obj.input.addEventListener("click", obj); - obj.root.className = "popup-checkbox"; - obj.input.type = "checkbox"; - obj.input.setAttribute('id', checkName); - obj.text.setAttribute("for", checkName); - obj.text.textContent = this.checkText.getAllElementsByName(checkName)[0].textContent; - obj.root.appendChild(obj.input); - obj.root.appendChild(obj.text); - if (testNode != undefined) { - if (testNode.getAttribute('default') == 'on') { - obj.input.checked = true; - } - if (testNode.getAttribute('support') == "none") { - obj.input.disabled = true; - obj.input.checked = false; - obj.root.className = "popup-checkbox disabled"; - } else if (interfaceNode.getAttribute('support') == "mandatory") { - obj.input.disabled = true; - obj.input.checked = true; - obj.root.className = "popup-checkbox disabled"; - } - } else { - if (interfaceNode.getAttribute('default') == 'on') { - obj.input.checked = true; - } - if (interfaceNode.getAttribute('support') == "none") { - obj.input.disabled = true; - obj.input.checked = false; - obj.root.className = "popup-checkbox disabled"; - } else if (interfaceNode.getAttribute('support') == "mandatory") { - obj.input.disabled = true; - obj.input.checked = true; - obj.root.className = "popup-checkbox disabled"; - } - } - var included = specification.interfaces.options.find(function (element, index, array) { - if (element.name == this.name) { - return true; - } else { - return false; - } - }, obj); - if (included != undefined) { - obj.input.checked = true; - } - obj.handleEvent(); - this.options.push(obj); - this.dynamicContent.appendChild(obj.root); - } - } - this.continue = function () { - popupStateNodes.state[3].generate(); - popupObject.postNode(popupStateNodes.state[3]); - } - this.back = function () { - popupObject.postNode(popupStateNodes.state[1]); - } + $s.addCommentQuestion = function () { + $s.page.addCommentQuestion(); + }; + $s.removeCommentQuestion = function (node) { + var index = $s.page.commentQuestions.findIndex(function (a) { + return a == node; + }); + if (index === -1) { + throw ("Invalid node"); } - this.state[3] = new function () { - this.title = "Test Metrics"; - this.content = document.createElement("div"); - this.content.id = "state-1"; - var spnH = document.createElement('div'); - var span = document.createElement("span"); - span.textContent = "Select which data points to include in the exported results XML. Some of this is required for certain post script analysis. See the documentation for further details"; - spnH.appendChild(span); - this.content.appendChild(spnH); - this.options = []; - this.checkText; - this.testXML; - this.interfaceXML; - this.dynamicContent = document.createElement("div"); - this.content.appendChild(this.dynamicContent); - this.generate = function () { - this.options = []; - this.dynamicContent.innerHTML = null; - var interfaceName = popupStateNodes.state[1].select.value; - this.checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("metrics")[0]; - this.testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0]; - this.interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(this.testXML.getAttribute("interface"))[0].getAllElementsByTagName("metrics")[0]; - this.testXML = this.testXML.getAllElementsByTagName("metrics"); - var interfaceXMLChildren = this.interfaceXML.getElementsByTagName('entry'); - for (var i = 0; i < interfaceXMLChildren.length; i++) { - var interfaceNode = interfaceXMLChildren[i]; - var checkName = interfaceNode.getAttribute('name'); - var testNode - if (this.testXML.length > 0) { - testNode = this.testXML[0].getAllElementsByName(checkName); - if (testNode.length != 0) { - testNode = testNode[0]; - } else { - testNode = undefined; - } - } else { - testNode = undefined; - } - var obj = { - root: document.createElement("div"), - text: document.createElement("label"), - input: document.createElement("input"), - parent: this, - name: checkName, - handleEvent: function (event) { - if (this.input.checked) { - // Add to specification.interfaces.option - var included = specification.metrics.enabled.find(function (element, index, array) { - if (element == this.name) { - return true; - } else { - return false; - } - }, this); - if (included == null) { - specification.metrics.enabled.push(this.name); - } - } else { - // Remove from specification.interfaces.option - var position = specification.metrics.enabled.findIndex(function (element, index, array) { - if (element == this.name) { - return true; - } else { - return false; - } - }, this); - if (position >= 0) { - specification.metrics.enabled.splice(position, 1); - } - } - } - } - - obj.input.addEventListener("click", obj); - obj.root.className = "popup-checkbox"; - obj.input.type = "checkbox"; - obj.input.setAttribute('id', checkName); - obj.text.setAttribute("for", checkName); - obj.text.textContent = this.checkText.getAllElementsByName(checkName)[0].textContent; - obj.root.appendChild(obj.input); - obj.root.appendChild(obj.text); - if (testNode != undefined) { - if (testNode.getAttribute('default') == 'on') { - obj.input.checked = true; - } - if (testNode.getAttribute('support') == "none") { - obj.input.disabled = true; - obj.input.checked = false; - obj.root.className = "popup-checkbox disabled"; - } else if (interfaceNode.getAttribute('support') == "mandatory") { - obj.input.disabled = true; - obj.input.checked = true; - obj.root.className = "popup-checkbox disabled"; - } - } else { - if (interfaceNode.getAttribute('default') == 'on') { - obj.input.checked = true; - } - if (interfaceNode.getAttribute('support') == "none") { - obj.input.disabled = true; - obj.input.checked = false; - obj.root.className = "popup-checkbox disabled"; - } else if (interfaceNode.getAttribute('support') == "mandatory") { - obj.input.disabled = true; - obj.input.checked = true; - obj.root.className = "popup-checkbox disabled"; - } - } - var included = specification.metrics.enabled.find(function (element, index, array) { - if (element == this.name) { - return true; - } else { - return false; - } - }, obj); - obj.handleEvent(); - if (included != undefined) { - obj.input.checked = true; - } - this.options.push(obj); - this.dynamicContent.appendChild(obj.root); - } - } - this.continue = function () { - popupStateNodes.state[4].generate(); - popupObject.postNode(popupStateNodes.state[4]); - } - this.back = function () { - popupObject.postNode(popupStateNodes.state[2]); - } + $s.page.commentQuestions.splice(index, 1); + }; + $s.addAudioElement = function () { + $s.page.addAudioElement(); + }; + $s.removeAudioElement = function (element) { + var index = $s.page.audioElements.findIndex(function (a) { + return a == element; + }); + if (index === -1) { + throw ("Invalid node"); } - this.state[4] = new function () { - this.title = "Test Visuals"; - this.content = document.createElement("div"); - this.content.id = "state-1"; - var spnH = document.createElement('div'); - var span = document.createElement("span"); - span.textContent = "You can display extra visual content with your interface for the test user to interact with. Select from the available options below. Greyed out options are unavailable for your selected interface"; - spnH.appendChild(span); - this.content.appendChild(spnH); - this.options = []; - this.checkText; - this.testXML; - this.interfaceXML; - this.dynamicContent = document.createElement("div"); - this.content.appendChild(this.dynamicContent); - this.generate = function () { - this.options = []; - this.dynamicContent.innerHTML = null; - var interfaceName = popupStateNodes.state[1].select.value; - this.checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("show")[0]; - this.testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0]; - this.interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(this.testXML.getAttribute("interface"))[0].getAllElementsByTagName("show")[0]; - this.testXML = this.testXML.getAllElementsByTagName("show"); - var interfaceXMLChildren = this.interfaceXML.getElementsByTagName('entry'); - for (var i = 0; i < interfaceXMLChildren.length; i++) { - var interfaceNode = interfaceXMLChildren[i]; - var checkName = interfaceNode.getAttribute('name'); - var testNode - if (this.testXML.length > 0) { - testNode = this.testXML[0].getAllElementsByName(checkName); - if (testNode.length != 0) { - testNode = testNode[0]; - } else { - testNode = undefined; - } - } else { - testNode = undefined; - } - var obj = { - root: document.createElement("div"), - text: document.createElement("label"), - input: document.createElement("input"), - parent: this, - name: checkName, - handleEvent: function (event) { - if (this.input.checked) { - // Add to specification.interfaces.option - var included = specification.interfaces.options.find(function (element, index, array) { - if (element.name == this.name) { - return true; - } else { - return false; - } - }, this); - if (included == null) { - specification.interfaces.options.push({ - type: "show", - name: this.name - }); - } - } else { - // Remove from specification.interfaces.option - var position = specification.interfaces.options.findIndex(function (element, index, array) { - if (element.name == this.name) { - return true; - } else { - return false; - } - }, this); - if (position >= 0) { - specification.interfaces.options.splice(position, 1); - } - } - } - } - - obj.input.addEventListener("click", obj); - obj.root.className = "popup-checkbox"; - obj.input.type = "checkbox"; - obj.input.setAttribute('id', checkName); - obj.text.setAttribute("for", checkName); - obj.text.textContent = this.checkText.getAllElementsByName(checkName)[0].textContent; - obj.root.appendChild(obj.input); - obj.root.appendChild(obj.text); - if (testNode != undefined) { - if (testNode.getAttribute('default') == 'on') { - obj.input.checked = true; - } - if (testNode.getAttribute('support') == "none") { - obj.input.disabled = true; - obj.input.checked = false; - obj.root.className = "popup-checkbox disabled"; - } else if (interfaceNode.getAttribute('support') == "mandatory") { - obj.input.disabled = true; - obj.input.checked = true; - obj.root.className = "popup-checkbox disabled"; - } - } else { - if (interfaceNode.getAttribute('default') == 'on') { - obj.input.checked = true; - } - if (interfaceNode.getAttribute('support') == "none") { - obj.input.disabled = true; - obj.input.checked = false; - obj.root.className = "popup-checkbox disabled"; - } else if (interfaceNode.getAttribute('support') == "mandatory") { - obj.input.disabled = true; - obj.input.checked = true; - obj.root.className = "popup-checkbox disabled"; - } - } - var included = specification.interfaces.options.find(function (element, index, array) { - if (element.name == this.name) { - return true; - } else { - return false; - } - }, obj); - if (included != undefined) { - obj.input.checked = true; - } - obj.handleEvent(); - this.options.push(obj); - this.dynamicContent.appendChild(obj.root); - } - } - this.continue = function () { - popupObject.hide(); - convert.convert(document.getElementById('content')); - } - this.back = function () { - popupObject.postNode(popupStateNodes.state[3]); - } - } - this.state[5] = new function () { - this.title = "Add/Edit Survey Element"; - this.content = document.createElement("div"); - this.content.id = "state-1"; - var spnH = document.createElement('div'); - var span = document.createElement("span"); - span.textContent = "You can configure your survey element here. Press 'Continue' to complete your changes."; - spnH.appendChild(span); - this.content.appendChild(spnH); - this.dynamic = document.createElement("div"); - this.option = null; - this.parent = null; - this.optionLists = []; - this.select = document.createElement("select"); - this.select.setAttribute("name", "type"); - this.select.addEventListener("change", this, false); - this.content.appendChild(this.select); - this.content.appendChild(this.dynamic); - this.generate = function (option, parent) { - this.option = option; - this.parent = parent; - if (this.select.childElementCount == 0) { - var optionList = specification.schema.getAllElementsByName("survey")[0].getAllElementsByName("type")[0].getAllElementsByTagName("xs:enumeration"); - for (var i = 0; i < optionList.length; i++) { - var selectOption = document.createElement("option"); - selectOption.value = optionList[i].getAttribute("value"); - selectOption.textContent = selectOption.value; - this.select.appendChild(selectOption); - } - } - if (this.option.type != undefined) { - this.select.value = this.option.type - } else { - this.select.value = "statement"; - this.option.type = "statement"; - } - - this.dynamic.innerHTML = null; - var statement = document.createElement("div"); - var statementText = document.createElement("span"); - var statementEntry = document.createElement("input"); - statement.appendChild(statementText); - statement.appendChild(statementEntry); - statement.className = "survey-entry-attribute"; - statementText.textContent = "Statement/Question"; - statementEntry.style.width = "500px"; - statementEntry.addEventListener("change", this, false); - statementEntry.setAttribute("name", "statement"); - statementEntry.value = this.option.statement; - this.dynamic.appendChild(statement); - - var id = document.createElement("div"); - var idText = document.createElement("span"); - var idEntry = document.createElement("input"); - id.appendChild(idText); - id.appendChild(idEntry); - id.className = "survey-entry-attribute"; - idText.textContent = "ID: "; - idEntry.addEventListener("change", this, false); - idEntry.setAttribute("name", "id"); - idEntry.value = this.option.id; - - this.dynamic.appendChild(id); - - switch (this.option.type) { - case "statement": - break; - case "question": - var boxsizeSelect = document.createElement("select"); - var optionList = specification.schema.getAllElementsByName("survey")[0].getAllElementsByName("boxsize")[0].getAllElementsByTagName("xs:enumeration"); - for (var i = 0; i < optionList.length; i++) { - var selectOption = document.createElement("option"); - selectOption.value = optionList[i].getAttribute("value"); - selectOption.textContent = selectOption.value; - boxsizeSelect.appendChild(selectOption); - } - if (this.option.boxsize != undefined) { - boxsizeSelect.value = this.option.boxsize; - } else { - boxsizeSelect.value = "normal"; - this.option.boxsize = "normal"; - } - boxsizeSelect.setAttribute("name", "boxsize"); - boxsizeSelect.addEventListener("change", this, false); - var boxsize = document.createElement("div"); - var boxsizeText = document.createElement("span"); - boxsizeText.textContent = "Entry Size: "; - boxsize.appendChild(boxsizeText); - boxsize.appendChild(boxsizeSelect); - boxsize.className = "survey-entry-attribute"; - this.dynamic.appendChild(boxsize); - - var mandatory = document.createElement("div"); - var mandatoryInput = document.createElement("input"); - var mandatoryText = document.createElement("span"); - mandatoryText.textContent = "Mandatory: "; - mandatory.appendChild(mandatoryText); - mandatory.appendChild(mandatoryInput); - mandatory.className = "survey-entry-attribute"; - mandatoryInput.type = "checkbox"; - if (this.option.mandatory) { - mandatoryInput.checked = true; - } else { - mandatoryInput.checked = false; - } - mandatoryInput.setAttribute("name", "mandatory"); - mandatoryInput.addEventListener("change", this, false); - this.dynamic.appendChild(mandatory); - break; - case "number": - this.dynamic.appendChild(id); - - var mandatory = document.createElement("div"); - var mandatoryInput = document.createElement("input"); - var mandatoryText = document.createElement("span"); - mandatoryText.textContent = "Mandatory: "; - mandatory.appendChild(mandatoryText); - mandatory.appendChild(mandatoryInput); - mandatory.className = "survey-entry-attribute"; - mandatoryInput.type = "checkbox"; - if (this.option.mandatory) { - mandatoryInput.checked = true; - } else { - mandatoryInput.checked = false; - } - mandatoryInput.setAttribute("name", "mandatory"); - mandatoryInput.addEventListener("change", this, false); - this.dynamic.appendChild(mandatory); - - var minimum = document.createElement("div"); - var minimumEntry = document.createElement("input"); - var minimumText = document.createElement("span"); - minimumText.textContent = "Minimum: "; - minimum.appendChild(minimumText); - minimum.appendChild(minimumEntry); - minimum.className = "survey-entry-attribute"; - minimumEntry.type = "number"; - minimumEntry.setAttribute("name", "min"); - minimumEntry.addEventListener("change", this, false); - minimumEntry.value = this.option.min; - this.dynamic.appendChild(minimum); - - var maximum = document.createElement("div"); - var maximumEntry = document.createElement("input"); - var maximumText = document.createElement("span"); - maximumText.textContent = "Maximum: "; - maximum.appendChild(maximumText); - maximum.appendChild(maximumEntry); - maximum.className = "survey-entry-attribute"; - maximumEntry.type = "number"; - maximumEntry.setAttribute("name", "max"); - maximumEntry.addEventListener("change", this, false); - maximumEntry.value = this.option.max; - this.dynamic.appendChild(maximum); - break; - case "checkbox": - case "radio": - this.dynamic.appendChild(id); - var optionHolder = document.createElement("div"); - optionHolder.className = 'node'; - optionHolder.id = 'popup-option-holder'; - var optionObject = function (parent, option) { - this.rootDOM = document.createElement("div"); - this.rootDOM.className = "popup-option-entry"; - this.inputName = document.createElement("input"); - this.inputName.setAttribute("name", "name"); - this.inputLabel = document.createElement("input"); - this.inputLabel.setAttribute("name", "text"); - this.specification = option; - this.parent = parent; - this.handleEvent = function () { - var target = event.currentTarget.getAttribute("name"); - eval("this.specification." + target + " = event.currentTarget.value"); - }; - - var nameText = document.createElement("span"); - nameText.textContent = "Name: "; - var labelText = document.createElement("span"); - labelText.textContent = "Label: "; - this.rootDOM.appendChild(nameText); - this.rootDOM.appendChild(this.inputName); - this.rootDOM.appendChild(labelText); - this.rootDOM.appendChild(this.inputLabel); - this.inputName.addEventListener("change", this, false); - this.inputLabel.addEventListener("change", this, false); - this.inputName.value = this.specification.name; - this.inputLabel.value = this.specification.text; - this.inputLabel.style.width = "350px"; - - this.deleteEntry = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - document.getElementById("popup-option-holder").removeChild(this.parent.rootDOM); - var index = this.parent.parent.option.options.findIndex(function (element, index, array) { - if (element == this.parent.specification) - return true; - else - return false; - }, this); - var optionList = this.parent.parent.option.options; - if (index == optionList.length - 1) { - optionList = optionList.slice(0, index); - } else { - optionList = optionList.slice(0, index).concat(optionList.slice(index + 1)); - } - this.parent.parent.option.options = optionList; - } - }; - this.deleteEntry.root.textContent = "Delete Option"; - this.deleteEntry.root.addEventListener("click", this.deleteEntry, false); - this.rootDOM.appendChild(this.deleteEntry.root); - } - this.addEntry = { - parent: this, - root: document.createElement("button"), - handleEvent: function () { - var node = { - name: "name", - text: "text" - }; - var optionsList = this.parent.option.options; - optionsList.push(node); - var obj = new optionObject(this.parent, optionsList[optionsList.length - 1]); - this.parent.optionLists.push(obj); - document.getElementById("popup-option-holder").appendChild(obj.rootDOM); - } - } - this.addEntry.root.textContent = "Add Option"; - this.addEntry.root.addEventListener("click", this.addEntry); - this.dynamic.appendChild(this.addEntry.root); - for (var i = 0; i < this.option.options.length; i++) { - var obj = new optionObject(this, this.option.options[i]); - this.optionLists.push(obj); - optionHolder.appendChild(obj.rootDOM); - } - this.dynamic.appendChild(optionHolder); - } - } - this.handleEvent = function (event) { - var name = event.currentTarget.getAttribute("name"); - var nodeName = event.currentTarget.nodeName; - if (name == "type" && nodeName == "SELECT") { - // If type has changed, we may need to rebuild the entire state node - if (event.currentTarget.value != this.option.name) { - this.option.type = event.currentTarget.value; - this.generate(this.option, this.parent); - } - return; - } - switch (event.currentTarget.getAttribute("type")) { - case "checkbox": - eval("this.option." + name + " = event.currentTarget.checked"); - break; - default: - eval("this.option." + name + " = event.currentTarget.value"); - break; - } - } - this.continue = function () { - if (this.parent.type == "surveyNode") { - var newNode = new this.parent.surveyEntryNode(this.parent, this.option); - this.parent.children.push(newNode); - this.parent.childrenDOM.appendChild(newNode.rootDOM); - } else if (this.parent.type == "surveyEntryNode") { - this.parent.build(); - } - popupObject.hide(); - } - } - this.state[6] = new function () { - this.title = "Edit Scale Markers"; - this.content = document.createElement("div"); - this.content.id = "state-6"; - var spnH = document.createElement('div'); - var span = document.createElement("span"); - span.textContent = "You can edit your scale markers here for the selected interface."; - spnH.appendChild(span); - this.scaleRoot; - this.parent; - this.markerNodes = []; - this.preset = { - input: document.createElement("select"), - parent: this, - handleEvent: function (event) { - this.parent.scaleRoot.scales = []; - var protoScale = interfaceSpecs.getAllElementsByTagName('scaledefinitions')[0].getAllElementsByName(event.currentTarget.value)[0]; - var protoMarkers = protoScale.getElementsByTagName("scalelabel"); - for (var i = 0; i < protoMarkers.length; i++) { - var marker = { - position: protoMarkers[i].getAttribute("position"), - text: protoMarkers[i].textContent - } - this.parent.scaleRoot.scales.push(marker); - } - this.parent.buildMarkerList(); - } - } - this.preset.input.addEventListener("change", this.preset); - this.content.appendChild(this.preset.input); - var optionHolder = document.createElement("div"); - optionHolder.className = 'node'; - optionHolder.id = 'popup-option-holder'; - this.content.appendChild(optionHolder); - this.addMarker = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - var marker = { - position: 0, - text: "text" - }; - this.parent.scaleRoot.scales.push(marker); - var markerNode = new this.parent.buildMarkerNode(this.parent, marker); - document.getElementById("popup-option-holder").appendChild(markerNode.root); - this.parent.markerNodes.push(markerNode); - } - }; - this.addMarker.root.textContent = "Add Marker"; - this.addMarker.root.addEventListener("click", this.addMarker); - this.generate = function (scaleRoot, parent) { - this.scaleRoot = scaleRoot; - this.parent = parent; - - // Generate Pre-Set dropdown - var protoScales = interfaceSpecs.getAllElementsByTagName('scaledefinitions')[0].getElementsByTagName("scale"); - this.preset.input.innerHTML = ""; - - for (var i = 0; i < protoScales.length; i++) { - var selectOption = document.createElement("option"); - var scaleName = protoScales[i].getAttribute("name"); - selectOption.setAttribute("name", scaleName); - selectOption.textContent = scaleName; - this.preset.input.appendChild(selectOption); - } - this.content.appendChild(this.addMarker.root); - - // Create Marker List - this.buildMarkerList(); - } - this.buildMarkerList = function () { - var markerInject = document.getElementById("popup-option-holder"); - markerInject.innerHTML = ""; - this.markerNodes = []; - for (var i = 0; i < this.scaleRoot.scales.length; i++) { - var markerNode = new this.buildMarkerNode(this, this.scaleRoot.scales[i]); - markerInject.appendChild(markerNode.root); - this.markerNodes.push(markerNode); - - } - } - - this.buildMarkerNode = function (parent, specification) { - this.root = document.createElement("div"); - this.root.className = "popup-option-entry"; - this.positionInput = document.createElement("input"); - this.positionInput.min = 0; - this.positionInput.max = 100; - this.positionInput.value = specification.position; - this.positionInput.setAttribute("name", "position"); - this.textInput = document.createElement("input"); - this.textInput.setAttribute("name", "text"); - this.textInput.style.width = "300px"; - this.textInput.value = specification.text; - this.specification = specification; - this.parent = parent; - this.handleEvent = function (event) { - switch (event.currentTarget.getAttribute("name")) { - case "position": - this.specification.position = Number(event.currentTarget.value); - break; - case "text": - this.specification.text = event.currentTarget.value; - break; - } - } - this.positionInput.addEventListener("change", this, false); - this.textInput.addEventListener("change", this, false); - - var posText = document.createElement("span"); - posText.textContent = "Position: "; - var textText = document.createElement("span"); - textText.textContent = "Text: "; - this.root.appendChild(posText); - this.root.appendChild(this.positionInput); - this.root.appendChild(textText); - this.root.appendChild(this.textInput); - - this.deleteMarker = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - var index = this.parent.parent.scaleRoot.scales.findIndex(function (element, index, array) { - if (element == this) { - return true; - } else { - return false; - } - }, this.parent.specification) - if (index >= 0) { - this.parent.parent.scaleRoot.scales.splice(index, 1); - } - document.getElementById("popup-option-holder").removeChild(this.parent.root); - } - } - this.deleteMarker.root.addEventListener("click", this.deleteMarker); - this.deleteMarker.root.textContent = "Delete Marker" - this.root.appendChild(this.deleteMarker.root); - } - } - } -} - -function SpecificationToHTML() { - // This takes the specification node and converts it to an on-page HTML object - // Each Specification Node is given its own JS object which listens to the XSD for instant verification - // Once generated, it directly binds into the specification object to update with changes - // Fixed DOM entries - this.injectDOM; - this.setupDOM; - this.pages = []; - - // Self-contained generators - this.createGeneralNodeDOM = function (name, id, parent) { - this.type = name; - var root = document.createElement('div'); - root.id = id; - root.className = "node"; - - var titleDiv = document.createElement('div'); - titleDiv.className = "node-title"; - var title = document.createElement('span'); - title.className = "node-title"; - title.textContent = name; - titleDiv.appendChild(title); - - var attributeDiv = document.createElement('div'); - attributeDiv.className = "node-attributes"; - - var childrenDiv = document.createElement('div'); - childrenDiv.className = "node-children"; - - var buttonsDiv = document.createElement('div'); - buttonsDiv.className = "node-buttons"; - - root.appendChild(titleDiv); - root.appendChild(attributeDiv); - root.appendChild(childrenDiv); - root.appendChild(buttonsDiv); - - var obj = { - rootDOM: root, - titleDOM: title, - attributeDOM: attributeDiv, - attributes: [], - childrenDOM: childrenDiv, - children: [], - buttonDOM: buttonsDiv, - parent: parent - } - return obj; - } - - this.convertAttributeToDOM = function (node, schema) { - // This takes an attribute schema node and returns an object with the input node and any bindings - if (schema.getAttribute('name') == undefined && schema.getAttribute('ref') != undefined) { - schema = specification.schema.getAllElementsByName(schema.getAttribute('ref'))[0]; - } - var obj = new function () { - this.input; - this.name; - this.owner; - this.holder; - - this.name = schema.getAttribute('name'); - this.default = schema.getAttribute('default'); - this.dataType = schema.getAttribute('type'); - if (this.dataType == undefined) { - if (schema.childElementCount > 0) { - if (schema.firstElementChild.nodeName == "xs:simpleType") { - this.dataType = schema.getAllElementsByTagName("xs:restriction")[0].getAttribute("base"); - } - } - } - if (typeof this.dataType == "string") { - this.dataType = this.dataType.substr(3); - } else { - this.dataType = "string"; - } - var minVar = undefined; - var maxVar = undefined; - switch (this.dataType) { - case "negativeInteger": - maxVar = -1; - break; - case "positiveInteger": - minVar = 1; - break; - case "nonNegativeInteger": - minVar = 0; - break; - case "nonPositiveInteger": - maxVar = 0; - break; - case "byte": - minVar = 0; - maxVar = 256; - break; - case "short": - minVar = 0; - maxVar = 65536; - break; - default: - break; - } - - this.enumeration = schema.getAllElementsByTagName("xs:enumeration"); - if (this.enumeration.length == 0) { - this.input = document.createElement('input'); - switch (this.dataType) { - case "boolean": - this.input.type = "checkbox"; - break; - case "negativeInteger": - case "positiveInteger": - case "nonNegativeInteger": - case "nonPositiveInteger": - case "integer": - case "short": - case "byte": - this.input.step = 1; - case "decimal": - this.input.type = "number"; - this.input.min = minVar; - this.input.max = maxVar; - break; - default: - break; - } - } else { - this.input = document.createElement("select"); - for (var i = 0; i < this.enumeration.length; i++) { - var option = document.createElement("option"); - var value = this.enumeration[i].getAttribute("value"); - option.setAttribute("value", value); - option.textContent = value; - this.input.appendChild(option); - } - } - var value; - eval("value = node." + this.name) - if (this.default != undefined && value == undefined) { - value = this.default; - } - if (this.input.type == "checkbox") { - if (value == "true" || value == "True") { - this.input.checked = false; - } else { - this.input.checked = false; - } - } else { - this.input.value = value; - } - this.handleEvent = function (event) { - var value; - if (this.input.nodeName == "INPUT") { - switch (this.input.type) { - case "checkbox": - value = event.currentTarget.checked; - break; - case "number": - if (event.currentTarget.value != "") { - value = Number(event.currentTarget.value); - } else { - value = undefined; - } - break; - default: - if (event.currentTarget.value != "") { - value = event.currentTarget.value; - } else { - value = undefined; - } - break; - } - } else if (this.input.nodeName == "SELECT") { - value = event.currentTarget.value; - } - eval("this.owner." + this.name + " = value"); - } - this.holder = document.createElement('div'); - this.holder.className = "attribute"; - this.holder.setAttribute('name', this.name); - var text = document.createElement('span'); - eval("text.textContent = attributeText." + this.name + "+': '"); - this.holder.appendChild(text); - this.holder.appendChild(this.input); - this.owner = node; - this.input.addEventListener("change", this, false); - } - if (obj.attribute != null) { - obj.input.value = obj.attribute; - } - return obj; - } - - this.convert = function (root) { - //Performs the actual conversion using the given root DOM as the root - this.injectDOM = root; - - // Build the export button - var exportButton = document.createElement("button"); - exportButton.textContent = "Export to XML"; - exportButton.onclick = function () { - var doc = specification.encode(); - var obj = {}; - obj.title = "Export"; - obj.content = document.createElement("div"); - obj.content.id = "finish"; - var span = document.createElement("span"); - span.textContent = "Your XML document is linked below. On most browsers, simply right click on the link and select 'Save As'. Or clicking on the link may download the file directly." - obj.content.appendChild(span); - span = document.createElement("p"); - span.textContent = "NOTE FOR SAFARI! You cannot right click on the below link and save it as a file, Safari does not like that at all. Instead click on it to open the XML, the Press Cmd+S to open the save dialogue. Make sure you have 'save as Page Source' selected on the bottom of the window. Currently Safari has no plans to support the HTML 'download' attribute which causes this problem"; - obj.content.appendChild(span); - var link = document.createElement("div"); - link.appendChild(doc.firstChild); - var file = [link.innerHTML]; - var bb = new Blob(file, { - type: 'application/xml' - }); - var dnlk = window.URL.createObjectURL(bb); - var a = document.createElement("a"); - a.hidden = ''; - a.href = dnlk; - a.download = "project-specification.xml"; - a.textContent = "Save File"; - obj.content.appendChild(a); - popupObject.show(); - popupObject.postNode(obj); - } - this.injectDOM.appendChild(exportButton); - - // First perform the setupNode; - var setupSchema = specification.schema.getAllElementsByName('setup')[0]; - this.setupDOM = new this.createGeneralNodeDOM('Global Configuration', 'setup', null); - this.injectDOM.appendChild(this.setupDOM.rootDOM); - var setupAttributes = setupSchema.getAllElementsByTagName('xs:attribute'); - for (var i = 0; i < setupAttributes.length; i++) { - var attributeName = setupAttributes[i].getAttribute('name'); - var attrObject = this.convertAttributeToDOM(specification, setupAttributes[i]); - this.setupDOM.attributeDOM.appendChild(attrObject.holder); - this.setupDOM.attributes.push(attrObject); - } - - // Build the exit Text node - var exitText = new this.createGeneralNodeDOM("Exit Text", "exit-test", this.setupDOM); - exitText.rootDOM.removeChild(exitText.attributeDOM); - this.setupDOM.children.push(exitText); - this.setupDOM.childrenDOM.appendChild(exitText.rootDOM); - var obj = { - rootDOM: document.createElement("div"), - labelDOM: document.createElement("label"), - inputDOM: document.createElement("textarea"), - parent: exitText, - specification: specification, - handleEvent: function (event) { - this.specification.exitText = this.inputDOM.value; - } - } - var exitWarning = document.createElement("div"); - obj.rootDOM.appendChild(exitWarning); - exitWarning.textContent = "Only visible when the above 'On complete redirect URL' field is empty."; - obj.rootDOM.appendChild(obj.labelDOM); - obj.rootDOM.appendChild(obj.inputDOM); - obj.labelDOM.textContent = "Text: "; - obj.inputDOM.value = obj.specification.exitText; - obj.inputDOM.addEventListener("change", obj); - exitText.children.push(obj); - exitText.childrenDOM.appendChild(obj.rootDOM); - - // Now we must build the interface Node - this.interfaceDOM = new this.interfaceNode(this, specification.interfaces); - this.interfaceDOM.build("Interface", "setup-interface", this.setupDOM.rootDOM); - - // Now build the Metrics selection node - var metric = this.createGeneralNodeDOM("Session Metrics", "setup-metric", this.setupDOM); - metric.rootDOM.removeChild(metric.attributeDOM); - this.setupDOM.children.push(metric); - this.setupDOM.childrenDOM.appendChild(metric.rootDOM); - var interfaceName = popupStateNodes.state[1].select.value; - var checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("metrics")[0]; - var testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0]; - var interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(testXML.getAttribute("interface"))[0].getAllElementsByTagName("metrics")[0]; - testXML = testXML.getAllElementsByTagName("metrics"); - var interfaceXMLChild = interfaceXML.firstElementChild; - while (interfaceXMLChild) { - var obj = { - input: document.createElement('input'), - root: document.createElement('div'), - text: document.createElement('span'), - specification: specification.metrics.enabled, - name: interfaceXMLChild.getAttribute("name"), - handleEvent: function () { - for (var i = 0; i < this.specification.length; i++) { - if (this.specification[i] == this.name) { - var options = this.specification; - if (this.input.checked == false) { - if (i == options.length) { - options = options.slice(0, i); - } else { - options = options.slice(0, i).concat(options.slice(i + 1)); - } - } else { - return; - } - this.specification = options; - break; - } - } - if (this.input.checked) { - this.specification.push(this.name); - } - } - }; - obj.root.className = "attribute"; - obj.input.type = "checkbox"; - obj.root.appendChild(obj.text); - obj.root.appendChild(obj.input); - obj.text.textContent = checkText.getAllElementsByName(interfaceXMLChild.getAttribute("name"))[0].textContent; - metric.children.push(obj); - metric.childrenDOM.appendChild(obj.root); - for (var j = 0; j < specification.metrics.enabled.length; j++) { - if (specification.metrics.enabled[j] == obj.name) { - obj.input.checked = true; - break; - } - } - interfaceXMLChild = interfaceXMLChild.nextElementSibling; - } - - // Now both before and after surveys - if (specification.preTest == undefined) { - specification.preTest = new specification.surveyNode(specification); - specification.preTest.location = "pre"; - } - if (specification.postTest == undefined) { - specification.postTest = new specification.surveyNode(specification); - specification.postTest.location = "post"; - } - var surveyBefore = new this.surveyNode(this, specification.preTest, "Pre"); - var surveyAfter = new this.surveyNode(this, specification.postTest, "Post"); - this.setupDOM.children.push(surveyBefore); - this.setupDOM.children.push(surveyAfter); - this.setupDOM.childrenDOM.appendChild(surveyBefore.rootDOM); - this.setupDOM.childrenDOM.appendChild(surveyAfter.rootDOM); - - // Add in the page creator button - this.addPage = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - var pageObj = new specification.page(specification); - specification.pages.push(pageObj); - var newPage = new this.parent.pageNode(this.parent, pageObj); - document.getElementById("page-holder").appendChild(newPage.rootDOM); - this.parent.pages.push(newPage); - } - } - this.addPage.root.textContent = "Add Page"; - this.addPage.root.id = "new-page-button"; - this.addPage.root.style.float = "left"; - this.addPage.root.addEventListener("click", this.addPage, false); - - var pageHolder = document.createElement("div"); - pageHolder.id = "page-holder"; - this.injectDOM.appendChild(pageHolder); - - // Build each page - for (var page of specification.pages) { - var newPage = new this.pageNode(this, page); - pageHolder.appendChild(newPage.rootDOM); - this.pages.push(newPage); - } - - this.injectDOM.appendChild(this.addPage.root); - } - - this.interfaceNode = function (parent, rootObject) { - this.type = "interfaceNode"; - this.rootDOM; - this.titleDOM; - this.attributeDOM; - this.attributes = []; - this.childrenDOM; - this.children = []; - this.buttonDOM; - this.parent = parent; - this.HTMLPoint; - this.specification = rootObject; - this.schema = specification.schema.getAllElementsByName("interface")[1]; - - this.createIOasAttr = function (name, specification, parent, type) { - this.root = document.createElement('div'); - this.input = document.createElement("input"); - this.name = name; - this.type = type; - this.parent = parent; - this.specification = specification; - this.handleEvent = function (event) { - for (var i = 0; i < this.specification.options.length; i++) { - if (this.specification.options[i].name == this.name) { - var options = this.specification.options; - if (this.input.checked == false) { - if (i == options.length) { - options = options.slice(0, i); - } else { - options = options.slice(0, i).concat(options.slice(i + 1)); - } - } else { - return; - } - this.specification.options = options; - break; - } - } - if (this.input.checked) { - var obj = { - name: this.name, - type: this.type - }; - this.specification.options.push(obj); - } - if (this.parent.HTMLPoint.id == "setup") { - // We've changed a global setting, must update all child 'interfaces' and disable them - for (pages of convert.pages) { - for (interface of pages.interfaces) { - if (this.type == "check") { - for (node of interface.children[0].attributes) { - if (node.name == this.name) { - if (this.input.checked) { - node.input.disabled = true; - node.input.checked = false; - } else { - node.input.disabled = false; - } - break; - } - } - } else if (this.type == "show") { - for (node of interface.children[1].attributes) { - if (node.name == this.name) { - if (this.input.checked) { - node.input.disabled = true; - } else { - node.input.disabled = false; - } - break; - } - } - } - } - } - } - }; - this.findIndex = function (element, index, array) { - if (element.name == this.name) - return true; - else - return false; - }; - this.findNode = function (element, index, array) { - if (element.name == this.name) - return true; - else - return false; - }; - this.input.type = "checkbox"; - this.input.setAttribute("name", name); - this.input.addEventListener("change", this, false); - this.root.appendChild(this.input); - this.root.className = "attribute"; - return this; - } - - this.build = function (name, id, parent) { - var obj = this.parent.createGeneralNodeDOM(name, id, parent); - - this.rootDOM = obj.rootDOM; - this.titleDOM = obj.titleDOM; - this.attributeDOM = obj.attributeDOM; - this.childrenDOM = obj.childrenDOM; - this.buttonDOM = obj.buttonsDOM; - this.HTMLPoint = parent; - this.rootDOM.removeChild(this.attributeDOM); - if (parent.id != "setup") { - // Put in the node: - this.titleNode = { - root: document.createElement("div"), - label: document.createElement("span"), - input: document.createElement("input"), - parent: this, - handleEvent: function (event) { - this.parent.specification.title = event.currentTarget.value; - } - } - this.titleNode.label.textContent = "Presented Axis Title:"; - this.titleNode.root.className = "node-children"; - this.titleNode.root.appendChild(this.titleNode.label); - this.titleNode.root.appendChild(this.titleNode.input); - this.titleNode.input.addEventListener("change", this.titleNode, false); - this.titleNode.input.value = this.specification.title; - this.children.push(this.titleNode); - this.childrenDOM.appendChild(this.titleNode.root); - // Set the interface-name attribute - this.axisName = { - root: document.createElement("div"), - label: document.createElement("span"), - input: document.createElement("input"), - parent: this, - handleEvent: function (event) { - this.parent.specification.name = event.currentTarget.value; - } - } - this.axisName.label.textContent = "Saved Axis Name (no spaces):"; - this.axisName.root.className = "node-children"; - this.axisName.root.appendChild(this.axisName.label); - this.axisName.root.appendChild(this.axisName.input); - this.axisName.input.addEventListener("change", this.axisName, false); - this.axisName.input.value = this.specification.name; - this.children.push(this.axisName); - this.childrenDOM.appendChild(this.axisName.root); - } - - // Put in the check / show options as individual children - var checks = this.parent.createGeneralNodeDOM("Checks", "setup-interface-checks", this); - - var interfaceName = popupStateNodes.state[1].select.value; - var checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("checks")[0]; - var testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0]; - var interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(testXML.getAttribute("interface"))[0].getAllElementsByTagName("checks")[0]; - testXML = testXML.getAllElementsByTagName("checks"); - var interfaceXMLChild = interfaceXML.firstElementChild; - while (interfaceXMLChild) { - var obj = new this.createIOasAttr(interfaceXMLChild.getAttribute("name"), this.specification, this, "check"); - for (var option of this.specification.options) { - if (option.name == obj.name) { - obj.input.checked = true; - break; - } - } - if (parent.id != "setup") { - var node = convert.interfaceDOM.children[0].attributes.find(obj.findNode, obj); - if (node != undefined) { - if (node.input.checked) { - obj.input.checked = false; - obj.input.disabled = true; - } - } - } - var text = document.createElement('span'); - text.textContent = checkText.getAllElementsByName(interfaceXMLChild.getAttribute("name"))[0].textContent; - obj.root.appendChild(text); - checks.attributeDOM.appendChild(obj.root); - checks.attributes.push(obj); - interfaceXMLChild = interfaceXMLChild.nextElementSibling; - } - this.children.push(checks); - this.childrenDOM.appendChild(checks.rootDOM); - - var show = this.parent.createGeneralNodeDOM("Show", "setup-interface-show", this); - interfaceName = popupStateNodes.state[1].select.value; - checkText = interfaceSpecs.getElementsByTagName("global")[0].getAllElementsByTagName("show")[0]; - testXML = interfaceSpecs.getElementsByTagName("tests")[0].getAllElementsByName(interfaceName)[0]; - interfaceXML = interfaceSpecs.getAllElementsByTagName("interfaces")[0].getAllElementsByName(testXML.getAttribute("interface"))[0].getAllElementsByTagName("show")[0]; - testXML = testXML.getAllElementsByTagName("show"); - interfaceXMLChild = interfaceXML.firstElementChild; - while (interfaceXMLChild) { - var obj = new this.createIOasAttr(interfaceXMLChild.getAttribute("name"), this.specification, this, "show"); - for (var option of this.specification.options) { - if (option.name == obj.name) { - obj.input.checked = true; - break; - } - } - if (parent.id != "setup") { - var node = convert.interfaceDOM.children[0].attributes.find(obj.findNode, obj); - if (node != undefined) { - if (node.input.checked) { - obj.input.checked = false; - obj.input.disabled = true; - } - } - } - var text = document.createElement('span'); - text.textContent = checkText.getAllElementsByName(interfaceXMLChild.getAttribute("name"))[0].textContent; - obj.root.appendChild(text); - show.attributeDOM.appendChild(obj.root); - show.attributes.push(obj); - interfaceXMLChild = interfaceXMLChild.nextElementSibling; - } - this.children.push(show); - this.childrenDOM.appendChild(show.rootDOM); - - if (parent.id == "setup") {} else { - var nameAttr = this.parent.convertAttributeToDOM(this, specification.schema.getAllElementsByName("name")[0]); - this.attributeDOM.appendChild(nameAttr.holder); - this.attributes.push(nameAttr); - var scales = new this.scalesNode(this, this.specification); - this.children.push(scales); - this.childrenDOM.appendChild(scales.rootDOM); - } - if (parent != undefined) { - parent.appendChild(this.rootDOM); - } - } - - this.scalesNode = function (parent, rootObject) { - this.type = "scalesNode"; - this.rootDOM = document.createElement("div"); - this.titleDOM = document.createElement("span"); - this.attributeDOM = document.createElement("div"); - this.attributes = []; - this.childrenDOM = document.createElement("div"); - this.children = []; - this.buttonDOM = document.createElement("div"); - this.parent = parent; - this.specification = rootObject; - this.schema = specification.schema.getAllElementsByName("page")[0]; - this.rootDOM.className = "node"; - - var titleDiv = document.createElement('div'); - titleDiv.className = "node-title"; - this.titleDOM.className = "node-title"; - this.titleDOM.textContent = "Interface Scales"; - titleDiv.appendChild(this.titleDOM); - - this.attributeDOM.className = "node-attributes"; - this.childrenDOM.className = "node-children"; - this.buttonDOM.className = "node-buttons"; - - this.rootDOM.appendChild(titleDiv); - this.rootDOM.appendChild(this.attributeDOM); - this.rootDOM.appendChild(this.childrenDOM); - this.rootDOM.appendChild(this.buttonDOM); - - this.editButton = { - button: document.createElement("button"), - parent: this, - handleEvent: function (event) { - popupObject.show(); - popupObject.postNode(popupStateNodes.state[6]); - popupStateNodes.state[6].generate(this.parent.specification, this.parent); - } - }; - this.editButton.button.textContent = "Edit Scales/Markers"; - this.editButton.button.addEventListener("click", this.editButton, false); - this.buttonDOM.appendChild(this.editButton.button); - } - } - - this.surveyNode = function (parent, rootObject, location) { - this.type = "surveyNode"; - this.rootDOM = document.createElement("div"); - this.titleDOM = document.createElement("span"); - this.attributeDOM = document.createElement("div"); - this.attributes = []; - this.childrenDOM = document.createElement("div"); - this.children = []; - this.buttonDOM = document.createElement("div"); - this.parent = parent; - this.specification = rootObject; - this.schema = specification.schema.getAllElementsByName("survey")[1]; - this.rootDOM.className = "node"; - - var titleDiv = document.createElement('div'); - titleDiv.className = "node-title"; - this.titleDOM.className = "node-title"; - this.titleDOM.textContent = "Survey"; - titleDiv.appendChild(this.titleDOM); - - this.attributeDOM.className = "node-attributes"; - var locationAttr = document.createElement("span"); - this.attributeDOM.appendChild(locationAttr); - if (location == "Pre" || location == "pre") { - locationAttr.textContent = "Location: Before"; - } else { - locationAttr.textContent = "Location: After"; - } - this.childrenDOM.className = "node-children"; - this.buttonDOM.className = "node-buttons"; - - this.rootDOM.appendChild(titleDiv); - this.rootDOM.appendChild(this.attributeDOM); - this.rootDOM.appendChild(this.childrenDOM); - this.rootDOM.appendChild(this.buttonDOM); - - this.surveyEntryNode = function (parent, rootObject) { - this.type = "surveyEntryNode"; - this.rootDOM = document.createElement("div"); - this.titleDOM = document.createElement("span"); - this.attributeDOM = document.createElement("div"); - this.attributes = []; - this.childrenDOM = document.createElement("div"); - this.children = []; - this.buttonDOM = document.createElement("div"); - this.parent = parent; - this.specification = rootObject; - this.schema = specification.schema.getAllElementsByName("surveyentry")[1]; - - this.rootDOM.className = "node"; - this.rootDOM.style.minWidth = "50%"; - - var titleDiv = document.createElement('div'); - titleDiv.className = "node-title"; - this.titleDOM.className = "node-title"; - titleDiv.appendChild(this.titleDOM); - - this.attributeDOM.className = "node-attributes"; - this.childrenDOM.className = "node-children"; - this.buttonDOM.className = "node-buttons"; - - this.rootDOM.appendChild(titleDiv); - this.rootDOM.appendChild(this.attributeDOM); - this.rootDOM.appendChild(this.childrenDOM); - this.rootDOM.appendChild(this.buttonDOM); - - this.build = function () { - this.attributeDOM.innerHTML = null; - this.childrenDOM.innerHTML = null; - var statementRoot = document.createElement("div"); - var statement = document.createElement("span"); - statement.textContent = "Statement / Question: " + this.specification.statement; - statementRoot.appendChild(statement); - this.children.push(statementRoot); - this.childrenDOM.appendChild(statementRoot); - switch (this.specification.type) { - case "statement": - this.titleDOM.textContent = "Statement"; - break; - case "question": - this.titleDOM.textContent = "Question"; - var id = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("id")[0]); - var mandatory = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("mandatory")[0]); - var boxsize = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("boxsize")[0]); - this.attributeDOM.appendChild(id.holder); - this.attributes.push(id); - this.attributeDOM.appendChild(mandatory.holder); - this.attributes.push(mandatory); - this.attributeDOM.appendChild(boxsize.holder); - this.attributes.push(boxsize); - break; - case "number": - this.titleDOM.textContent = "Number"; - var id = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("id")[0]); - var mandatory = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("mandatory")[0]); - var min = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("min")[0]); - var max = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("max")[0]); - this.attributeDOM.appendChild(id.holder); - this.attributes.push(id); - this.attributeDOM.appendChild(min.holder); - this.attributes.push(min); - this.attributeDOM.appendChild(max.holder); - this.attributes.push(max); - break; - case "checkbox": - this.titleDOM.textContent = "Checkbox"; - var id = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("id")[0]); - this.attributeDOM.appendChild(id.holder); - this.attributes.push(id); - break; - case "radio": - this.titleDOM.textContent = "Radio"; - var id = convert.convertAttributeToDOM(this.specification, specification.schema.getAllElementsByName("id")[0]); - this.attributeDOM.appendChild(id.holder); - this.attributes.push(id); - break; - } - } - this.build(); - - this.editNode = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - popupObject.show(); - popupStateNodes.state[5].generate(this.parent.specification, this.parent); - popupObject.postNode(popupStateNodes.state[5]); - } - } - this.editNode.root.textContent = "Edit Entry"; - this.editNode.root.addEventListener("click", this.editNode, false); - this.buttonDOM.appendChild(this.editNode.root); - - this.deleteNode = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - var optionList = this.parent.parent.specification.options; - var childList = this.parent.parent.children; - for (var i = 0; i < this.parent.parent.specification.options.length; i++) { - var option = this.parent.parent.specification.options[i]; - if (option == this.parent.specification) { - this.parent.parent.childrenDOM.removeChild(this.parent.rootDOM); - if (i == this.parent.parent.specification.options.length - 1) { - optionList = optionList.slice(0, i); - childList = childList.slice(0, i); - } else { - optionList = optionList.slice(0, i).concat(optionList.slice(i + 1)); - childList = childList.slice(0, i).concat(childList.slice(i + 1)); - } - this.parent.parent.specification.options = optionList; - this.parent.parent.children = childList; - } - } - } - } - this.deleteNode.root.textContent = "Delete Entry"; - this.deleteNode.root.addEventListener("click", this.deleteNode, false); - this.buttonDOM.appendChild(this.deleteNode.root); - - this.moveToPosition = function (new_index) { - new_index = Math.min(new_index, this.parent.children.length); - var curr_index = this.parent.children.findIndex(function (elem) { - if (elem == this) { - return true; - } else { - return false; - } - }, this); - // Split at the current location to remove the node and shift all the children up - var tail = this.parent.children.splice(curr_index + 1); - this.parent.children.pop(); - this.parent.children = this.parent.children.concat(tail); - - //Split at the new location and insert the node - tail = this.parent.children.splice(new_index); - this.parent.children.push(this); - this.parent.children = this.parent.children.concat(tail); - - // Re-build the specification - this.parent.specification.options = []; - this.parent.childrenDOM.innerHTML = ""; - for (var obj of this.parent.children) { - this.parent.specification.options.push(obj.specification); - this.parent.childrenDOM.appendChild(obj.rootDOM); - } - this.parent.children.forEach(function (obj, index) { - obj.moveButtons.disable(index); - }); - } - - this.moveButtons = { - root_up: document.createElement("button"), - root_down: document.createElement("button"), - parent: this, - handleEvent: function (event) { - var index = this.parent.parent.children.indexOf(this.parent); - if (event.currentTarget.getAttribute("direction") == "up") { - index = Math.max(index - 1, 0); - } else if (event.currentTarget.getAttribute("direction") == "down") { - index = Math.min(index + 1, this.parent.parent.children.length - 1); - } - this.parent.moveToPosition(index); - this.disable(index); - }, - disable: function (index) { - if (index == 0) { - this.root_up.disabled = true; - } else { - this.root_up.disabled = false; - } - if (index == this.parent.parent.children.length - 1) { - this.root_down.disabled = true; - } else { - this.root_down.disabled = false; - } - } - } - this.moveButtons.root_up.setAttribute("direction", "up"); - this.moveButtons.root_down.setAttribute("direction", "down"); - this.moveButtons.root_up.addEventListener("click", this.moveButtons, false); - this.moveButtons.root_down.addEventListener("click", this.moveButtons, false); - this.moveButtons.root_up.textContent = "Move Up"; - this.moveButtons.root_down.textContent = "Move Down"; - this.buttonDOM.appendChild(this.moveButtons.root_up); - this.buttonDOM.appendChild(this.moveButtons.root_down); - } - this.addNode = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - var newNode = new this.parent.specification.OptionNode(this.parent.specification); - this.parent.specification.options.push(newNode); - popupObject.show(); - popupStateNodes.state[5].generate(newNode, this.parent); - popupObject.postNode(popupStateNodes.state[5]); - } - } - this.addNode.root.textContent = "Add Survey Entry"; - this.addNode.root.addEventListener("click", this.addNode, false); - this.buttonDOM.appendChild(this.addNode.root); - - for (var option of this.specification.options) { - var newNode = new this.surveyEntryNode(this, option); - this.children.push(newNode); - this.childrenDOM.appendChild(newNode.rootDOM); - } - - this.children.forEach(function (obj, index) { - obj.moveButtons.disable(index); - }); - } - - this.pageNode = function (parent, rootObject) { - this.type = "pageNode"; - this.rootDOM = document.createElement("div"); - this.titleDOM = document.createElement("span"); - this.attributeDOM = document.createElement("div"); - this.attributes = []; - this.childrenDOM = document.createElement("div"); - this.children = []; - this.buttonDOM = document.createElement("div"); - this.parent = parent; - this.specification = rootObject; - this.schema = specification.schema.getAllElementsByName("page")[0]; - this.rootDOM.className = "node"; - - var titleDiv = document.createElement('div'); - titleDiv.className = "node-title"; - this.titleDOM.className = "node-title"; - this.titleDOM.textContent = "Test Page"; - titleDiv.appendChild(this.titleDOM); - - this.attributeDOM.className = "node-attributes"; - this.childrenDOM.className = "node-children"; - this.buttonDOM.className = "node-buttons"; - - this.rootDOM.appendChild(titleDiv); - this.rootDOM.appendChild(this.attributeDOM); - this.rootDOM.appendChild(this.childrenDOM); - this.rootDOM.appendChild(this.buttonDOM); - - // Do the comment prefix node - var cpn = this.parent.createGeneralNodeDOM("Comment Prefix", "" + this.specification.id + "-commentprefix", this.parent); - cpn.rootDOM.removeChild(cpn.attributeDOM); - var obj = { - root: document.createElement("div"), - input: document.createElement("input"), - parent: this, - handleEvent: function () { - this.parent.specification.commentBoxPrefix = event.currentTarget.value; - } - } - cpn.children.push(obj); - cpn.childrenDOM.appendChild(obj.root); - obj.root.appendChild(obj.input); - obj.input.addEventListener("change", obj, false); - obj.input.value = this.specification.commentBoxPrefix; - this.childrenDOM.appendChild(cpn.rootDOM); - this.children.push(cpn); - - // Now both before and after surveys - if (this.specification.preTest == undefined) { - this.specification.preTest = new specification.surveyNode(specification); - this.specification.preTest.location = "pre"; - } - if (this.specification.postTest == undefined) { - this.specification.postTest = new specification.surveyNode(specification); - this.specification.postTest.location = "post"; - } - var surveyBefore = new this.parent.surveyNode(this, this.specification.preTest, "Pre"); - var surveyAfter = new this.parent.surveyNode(this, this.specification.postTest, "Post"); - this.children.push(surveyBefore); - this.children.push(surveyAfter); - this.childrenDOM.appendChild(surveyBefore.rootDOM); - this.childrenDOM.appendChild(surveyAfter.rootDOM); - - // Build the attributes - var attributeList = this.schema.getAllElementsByTagName("xs:attribute"); - for (var i = 0; i < attributeList.length; i++) { - var attributeName = attributeList[i].getAttribute('name'); - var attrObject = this.parent.convertAttributeToDOM(rootObject, attributeList[i]); - this.attributeDOM.appendChild(attrObject.holder); - this.attributes.push(attrObject); - } - - this.interfaces = []; - - this.getAudioElements = function () { - var array = []; - for (var i = 0; i < this.children.length; i++) { - if (this.children[i].type == "audioElementNode") { - array[array.length] = this.children[i]; - } - } - return array; - } - - this.redrawChildren = function () { - this.childrenDOM.innerHTML = ""; - for (var child of this.children) { - this.childrenDOM.appendChild(child.rootDOM); - } - } - - this.audioElementNode = function (parent, rootObject) { - this.type = "audioElementNode"; - this.rootDOM = document.createElement("div"); - this.titleDOM = document.createElement("span"); - this.attributeDOM = document.createElement("div"); - this.attributes = []; - this.childrenDOM = document.createElement("div"); - this.children = []; - this.buttonDOM = document.createElement("div"); - this.parent = parent; - this.specification = rootObject; - this.schema = specification.schema.getAllElementsByName("audioelement")[0]; - this.rootDOM.className = "node"; - - var titleDiv = document.createElement('div'); - titleDiv.className = "node-title"; - this.titleDOM.className = "node-title"; - this.titleDOM.textContent = "Audio Element"; - titleDiv.appendChild(this.titleDOM); - - this.attributeDOM.className = "node-attributes"; - this.childrenDOM.className = "node-children"; - this.buttonDOM.className = "node-buttons"; - - this.rootDOM.appendChild(titleDiv); - this.rootDOM.appendChild(this.attributeDOM); - this.rootDOM.appendChild(this.childrenDOM); - this.rootDOM.appendChild(this.buttonDOM); - - // Build the attributes - var attributeList = this.schema.getAllElementsByTagName("xs:attribute"); - for (var i = 0; i < attributeList.length; i++) { - var attributeName = attributeList[i].getAttribute('name'); - var attrObject = this.parent.parent.convertAttributeToDOM(rootObject, attributeList[i]); - this.attributeDOM.appendChild(attrObject.holder); - this.attributes.push(attrObject); - } - - this.deleteNode = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - var i = this.parent.parent.specification.audioElements.findIndex(this.findNode, this); - if (i >= 0) { - var aeList = this.parent.parent.specification.audioElements; - if (i < aeList.length - 1) { - aeList = aeList.slice(0, i).concat(aeList.slice(i + 1)); - } else { - aeList = aeList.slice(0, i); - } - } - i = this.parent.parent.children.findIndex(function (element, index, array) { - if (element == this.parent) - return true; - else - return false; - }, this); - if (i >= 0) { - var childList = this.parent.children; - if (i < aeList.length - 1) { - childList = childList.slice(0, i).concat(childList.slice(i + 1)); - } else { - childList = childList.slice(0, i); - } - this.parent.parent.childrenDOM.removeChild(this.parent.rootDOM); - } - }, - findNode: function (element, index, array) { - if (element == this.parent.specification) - return true; - else - return false; - } - } - this.deleteNode.root.textContent = "Delete Entry"; - this.deleteNode.root.addEventListener("click", this.deleteNode, false); - this.buttonDOM.appendChild(this.deleteNode.root); - - this.moveButtons = { - root_up: document.createElement("button"), - root_down: document.createElement("button"), - parent: this, - handleEvent: function (event) { - var index = this.parent.parent.getAudioElements().indexOf(this.parent); - if (event.currentTarget.getAttribute("direction") == "up") { - index = Math.max(index - 1, 0); - } else if (event.currentTarget.getAttribute("direction") == "down") { - index = Math.min(index + 1, this.parent.parent.getAudioElements().length - 1); - } - this.parent.moveToPosition(index); - this.disable(index); - }, - disable: function (index) { - if (index == 0) { - this.root_up.disabled = true; - } else { - this.root_up.disabled = false; - } - if (index == this.parent.parent.getAudioElements().length - 1) { - this.root_down.disabled = true; - } else { - this.root_down.disabled = false; - } - } - } - this.moveButtons.root_up.setAttribute("direction", "up"); - this.moveButtons.root_down.setAttribute("direction", "down"); - this.moveButtons.root_up.addEventListener("click", this.moveButtons, false); - this.moveButtons.root_down.addEventListener("click", this.moveButtons, false); - this.moveButtons.root_up.textContent = "Move Up"; - this.moveButtons.root_down.textContent = "Move Down"; - this.buttonDOM.appendChild(this.moveButtons.root_up); - this.buttonDOM.appendChild(this.moveButtons.root_down); - - this.moveToPosition = function (new_index) { - - // Get the zero-th Object - var zero_object = this.parent.getAudioElements()[0]; - var parent_children_root_index = this.parent.children.indexOf(zero_object); - // splice out the array for processing - var process_array = this.parent.children.splice(parent_children_root_index); - - - new_index = Math.min(new_index, process_array.length); - var curr_index = process_array.findIndex(function (elem) { - if (elem == this) { - return true; - } else { - return false; - } - }, this); - - // Split at the current location to remove the node and shift all the children up - var tail = process_array.splice(curr_index + 1); - process_array.pop(); - process_array = process_array.concat(tail); - - //Split at the new location and insert the node - tail = process_array.splice(new_index); - process_array.push(this); - process_array = process_array.concat(tail); - - // Re-attach to the parent.children - this.parent.children = this.parent.children.concat(process_array); - - // Re-build the specification - this.parent.specification.audioElements = []; - for (var obj of process_array) { - this.parent.specification.audioElements.push(obj.specification); - } - this.parent.redrawChildren(); - - process_array.forEach(function (obj, index) { - obj.moveButtons.disable(index); - }); - - } - } - - this.commentQuestionNode = function (parent, rootObject) { - this.type = "commentQuestionNode"; - this.rootDOM = document.createElement("div"); - this.titleDOM = document.createElement("span"); - this.attributeDOM = document.createElement("div"); - this.attributes = []; - this.childrenDOM = document.createElement("div"); - this.children = []; - this.buttonDOM = document.createElement("div"); - this.parent = parent; - this.specification = rootObject; - this.schema = specification.schema.getAllElementsByName("page")[0]; - this.rootDOM.className = "node audio-element"; - - var titleDiv = document.createElement('div'); - titleDiv.className = "node-title"; - this.titleDOM.className = "node-title"; - this.titleDOM.textContent = "Test Page"; - titleDiv.appendChild(this.titleDOM); - - this.attributeDOM.className = "node-attributes"; - this.childrenDOM.className = "node-children"; - this.buttonDOM.className = "node-buttons"; - - this.rootDOM.appendChild(titleDiv); - this.rootDOM.appendChild(this.attributeDOM); - this.rootDOM.appendChild(this.childrenDOM); - this.rootDOM.appendChild(this.buttonDOM); - - } - - // Build the components - if (this.specification.interfaces.length == 0) { - this.specification.interfaces.push(new specification.interfaceNode(specification)); - } - for (var interfaceObj of this.specification.interfaces) { - var newInterface = new this.parent.interfaceNode(this.parent, interfaceObj); - newInterface.build("Interface", "" + this.specification.id + "-interface", this.childrenDOM); - this.children.push(newInterface); - this.interfaces.push(newInterface); - } - - for (var elements of this.specification.audioElements) { - var audioElementDOM = new this.audioElementNode(this, elements); - this.children.push(audioElementDOM); - this.childrenDOM.appendChild(audioElementDOM.rootDOM); - } - - this.getAudioElements().forEach(function (elem) { - elem.moveButtons.disable(); - }); - - this.addInterface = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - var InterfaceObj = new specification.interfaceNode(specification); - var newInterface = new this.parent.parent.interfaceNode(this.parent.parent, InterfaceObj); - newInterface.build("Interface", "" + this.parent.specification.id + "-interface", this.parent.childrenDOM); - this.parent.children.push(newInterface); - this.parent.specification.interfaces.push(InterfaceObj); - this.parent.interfaces.push(newInterface); - } - } - this.addInterface.root.textContent = "Add Interface"; - this.addInterface.root.addEventListener("click", this.addInterface, false); - this.buttonDOM.appendChild(this.addInterface.root); - - this.addAudioElement = { - root: document.createElement("button"), - parent: this, - handleEvent: function () { - var audioElementObject = new this.parent.specification.audioElementNode(specification); - var audioElementDOM = new this.parent.audioElementNode(this.parent, audioElementObject); - this.parent.specification.audioElements.push(audioElementObject); - this.parent.children.push(audioElementDOM); - this.parent.childrenDOM.appendChild(audioElementDOM.rootDOM); - } - } - this.addAudioElement.root.textContent = "Add Audio Element"; - this.addAudioElement.root.addEventListener("click", this.addAudioElement, false); - this.buttonDOM.appendChild(this.addAudioElement.root); - } -} + $s.page.audioElements.splice(index, 1); + }; +}]); diff -r 56e72cd18404 -r 231d2186f3fc tests/examples/ABX_example.xml --- a/tests/examples/ABX_example.xml Fri Jul 14 15:37:53 2017 +0100 +++ b/tests/examples/ABX_example.xml Fri Jul 14 15:39:24 2017 +0100 @@ -58,8 +58,8 @@ <interface> <title>Depth
- - + + A two way comparison using randomised element order, automatic loudness and synchronised looping. diff -r 56e72cd18404 -r 231d2186f3fc tests/examples/AB_example.xml --- a/tests/examples/AB_example.xml Fri Jul 14 15:37:53 2017 +0100 +++ b/tests/examples/AB_example.xml Fri Jul 14 15:39:24 2017 +0100 @@ -1,39 +1,39 @@ - + - + Please enter your name. - - + + Please select with which activities you have any experience (example checkbox question) - - + + This is an example of an 'AB'-style test, with two pages, using the test stimuli in 'example_eval/'. The 'playOne' configuration option means a fragment has to be finished playing before another fragment can be auditioned. - + - + Please enter your location. (example mandatory text question) - - + + Please enter your age (example non-mandatory number question) - - + + Please rate this interface (example radio button question) - - + + Thank you for taking this listening test. Please click 'submit' and your results will appear in the 'saves/' folder. - + testTimer @@ -46,7 +46,9 @@ - + + Test Error Message + @@ -56,19 +58,20 @@ Comment on fragment - Depth + Which sounds most like a drum? + - - A two way comparison using randomised element order, automatic loudness and synchronised looping. - + + A two way comparison using randomised element order, automatic loudness and synchronised looping. Also an embedded image + - + Please enter the genre. - + @@ -84,14 +87,14 @@ - + A 7 way comparison using randomised element order and synchronised looping. - + - + Please enter the genre. - + diff -r 56e72cd18404 -r 231d2186f3fc tests/examples/mushra_example.xml --- a/tests/examples/mushra_example.xml Fri Jul 14 15:37:53 2017 +0100 +++ b/tests/examples/mushra_example.xml Fri Jul 14 15:39:24 2017 +0100 @@ -48,6 +48,7 @@ + diff -r 56e72cd18404 -r 231d2186f3fc tests/examples/ordinal_example.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/examples/ordinal_example.xml Fri Jul 14 15:39:24 2017 +0100 @@ -0,0 +1,33 @@ + + + + + testTimer + elementTimer + elementInitialPosition + elementTracker + elementFlagListenedTo + elementFlagMoved + elementListenTracker + + + + + + + + + + + Ordinal Evaluation + + + + + + + + + + + diff -r 56e72cd18404 -r 231d2186f3fc tests/examples/timeline.xml --- a/tests/examples/timeline.xml Fri Jul 14 15:37:53 2017 +0100 +++ b/tests/examples/timeline.xml Fri Jul 14 15:39:24 2017 +0100 @@ -30,8 +30,8 @@ (5) Inaudible - - + + Please enter your overall preference @@ -61,8 +61,8 @@ (5) Inaudible - - + + Please enter your overall preference diff -r 56e72cd18404 -r 231d2186f3fc tests/pool.xml --- a/tests/pool.xml Fri Jul 14 15:37:53 2017 +0100 +++ b/tests/pool.xml Fri Jul 14 15:39:24 2017 +0100 @@ -1,6 +1,6 @@ - + testTimer elementTimer diff -r 56e72cd18404 -r 231d2186f3fc xml/test-schema.xsd --- a/xml/test-schema.xsd Fri Jul 14 15:37:53 2017 +0100 +++ b/xml/test-schema.xsd Fri Jul 14 15:39:24 2017 +0100 @@ -27,8 +27,13 @@ + + + + + @@ -65,6 +70,8 @@ + + @@ -100,10 +107,13 @@ + + + @@ -119,8 +129,16 @@ + + + + + + + + @@ -226,6 +244,9 @@ + + + @@ -331,6 +352,7 @@ + @@ -364,6 +386,7 @@ + @@ -385,6 +408,7 @@ + @@ -395,6 +419,7 @@ + @@ -408,6 +433,7 @@ + @@ -423,6 +449,7 @@ + @@ -434,6 +461,7 @@ + @@ -444,13 +472,14 @@ + - +