nicholas@2844: /** nicholas@2844: * WAET Blank Template nicholas@2844: * Use this to start building your custom interface nicholas@2844: */ n@2980: /*globals interfaceContext, window, document, specification, audioEngineContext, console, testState, $, storage, sessionStorage */ nicholas@2844: // Once this is loaded and parsed, begin execution nicholas@2844: loadInterface(); nicholas@2844: nicholas@2844: function loadInterface() { nicholas@2844: // Use this to do any one-time page / element construction. For instance, placing any stationary text objects, nicholas@2844: // holding div's, or setting up any nodes which are present for the entire test sequence nicholas@2844: nicholas@2844: // The injection point into the HTML page nicholas@2844: interfaceContext.insertPoint = document.getElementById("topLevelBody"); nicholas@2844: var testContent = document.createElement('div'); nicholas@2844: testContent.id = 'testContent'; nicholas@2844: nicholas@2844: // Create the top div for the Title element nicholas@2844: var titleAttr = specification.title; nicholas@2844: var title = document.createElement('div'); nicholas@2844: title.className = "title"; nicholas@2844: title.align = "center"; nicholas@2844: var titleSpan = document.createElement('span'); nicholas@2844: titleSpan.id = "test-title"; nicholas@2844: nicholas@2844: // Set title to that defined in XML, else set to default nicholas@2844: if (titleAttr !== undefined) { nicholas@2844: titleSpan.textContent = titleAttr; nicholas@2844: } else { nicholas@2844: titleSpan.textContent = 'Listening test'; nicholas@2844: } nicholas@2844: // Insert the titleSpan element into the title div element. nicholas@2844: title.appendChild(titleSpan); nicholas@2844: nicholas@2844: var pagetitle = document.createElement('div'); nicholas@2844: pagetitle.className = "pageTitle"; nicholas@2844: pagetitle.align = "center"; nicholas@2844: nicholas@2844: titleSpan = document.createElement('span'); nicholas@2844: titleSpan.id = "pageTitle"; nicholas@2844: pagetitle.appendChild(titleSpan); nicholas@2844: nicholas@2844: // Create Interface buttons! nicholas@2844: var interfaceButtons = document.createElement('div'); nicholas@2844: interfaceButtons.id = 'interface-buttons'; nicholas@2844: interfaceButtons.style.height = '25px'; nicholas@2844: nicholas@2844: // Create playback start/stop points nicholas@2844: var playback = document.createElement("button"); nicholas@2844: playback.innerHTML = 'Stop'; nicholas@2844: playback.id = 'playback-button'; nicholas@2844: playback.style.float = 'left'; nicholas@2844: // onclick function. Check if it is playing or not, call the correct function in the nicholas@2844: // audioEngine, change the button text to reflect the next state. nicholas@2844: playback.onclick = function () { nicholas@2844: if (audioEngineContext.status == 1) { nicholas@2844: audioEngineContext.stop(); nicholas@2844: this.innerHTML = 'Stop'; nicholas@2844: var time = audioEngineContext.timer.getTestTime(); nicholas@2844: console.log('Stopped at ' + time); // DEBUG/SAFETY nicholas@2844: } nicholas@2844: }; nicholas@2844: // Create Submit (save) button nicholas@2844: var submit = document.createElement("button"); nicholas@2844: submit.innerHTML = 'Next'; nicholas@2844: submit.onclick = buttonSubmitClick; nicholas@2844: submit.id = 'submit-button'; nicholas@2844: submit.style.float = 'left'; nicholas@2844: // Append the interface buttons into the interfaceButtons object. nicholas@2844: interfaceButtons.appendChild(playback); nicholas@2844: interfaceButtons.appendChild(submit); nicholas@2844: nicholas@2844: // Create outside reference holder nicholas@2844: var outsideRef = document.createElement("div"); nicholas@2844: outsideRef.id = "outside-reference-holder"; nicholas@2844: nicholas@2844: // Create a slider box nicholas@2844: var slider = document.createElement("div"); nicholas@2844: slider.id = "slider"; nicholas@2845: slider.style.height = "300px"; nicholas@2844: nicholas@2844: // Global parent for the comment boxes on the page nicholas@2844: var feedbackHolder = document.createElement('div'); nicholas@2844: feedbackHolder.id = 'feedbackHolder'; nicholas@2844: nicholas@2844: testContent.style.zIndex = 1; nicholas@2844: interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema nicholas@2844: nicholas@2844: // Inject into HTML nicholas@2844: testContent.appendChild(title); // Insert the title nicholas@2844: testContent.appendChild(pagetitle); nicholas@2844: testContent.appendChild(interfaceButtons); nicholas@2844: testContent.appendChild(outsideRef); nicholas@2844: testContent.appendChild(slider); nicholas@2844: testContent.appendChild(feedbackHolder); nicholas@2844: interfaceContext.insertPoint.appendChild(testContent); nicholas@2844: nicholas@2844: // Load the full interface nicholas@2844: testState.initialise(); nicholas@2844: testState.advanceState(); nicholas@2844: } nicholas@2844: nicholas@2844: function loadTest(page) { nicholas@2844: // Called each time a new test page is to be build. The page specification node is the only item passed in nicholas@2844: var id = page.id; nicholas@2844: nicholas@2844: var feedbackHolder = document.getElementById('feedbackHolder'); nicholas@2844: feedbackHolder.innerHTML = ""; nicholas@2844: nicholas@2844: var interfaceObj = interfaceContext.getCombinedInterfaces(page); nicholas@2844: if (interfaceObj.length > 1) { nicholas@2844: console.log("WARNING - This interface only supports one node per page. Using first interface node"); nicholas@2844: } nicholas@2844: interfaceObj = interfaceObj[0]; nicholas@2844: nicholas@2844: // Set the page title nicholas@2844: if (typeof page.title == "string" && page.title.length > 0) { nicholas@2844: document.getElementById("test-title").textContent = page.title; nicholas@2844: } nicholas@2844: nicholas@2844: if (interfaceObj.title !== null) { nicholas@2844: document.getElementById("pageTitle").textContent = interfaceObj.title; nicholas@2844: } nicholas@2844: nicholas@2844: if (interfaceObj.image !== undefined) { nicholas@2844: feedbackHolder.insertBefore(interfaceContext.imageHolder.root, document.getElementById("slider")); nicholas@2844: interfaceContext.imageHolder.setImage(interfaceObj.image); nicholas@2844: } nicholas@2844: // Delete outside reference nicholas@2844: document.getElementById("outside-reference-holder").innerHTML = ""; nicholas@2844: nicholas@2844: var sliderBox = document.getElementById('slider'); nicholas@2844: sliderBox.innerHTML = ""; nicholas@2844: nicholas@2844: var commentBoxPrefix = "Comment on track"; nicholas@2844: if (interfaceObj.commentBoxPrefix !== undefined) { nicholas@2844: commentBoxPrefix = interfaceObj.commentBoxPrefix; nicholas@2844: } nicholas@2844: nicholas@2844: $(page.commentQuestions).each(function (index, element) { nicholas@2844: var node = interfaceContext.createCommentQuestion(element); nicholas@2844: feedbackHolder.appendChild(node.holder); nicholas@2844: }); nicholas@2844: nicholas@2844: var index = 0; nicholas@2844: var labelType = page.label; nicholas@2844: if (labelType == "default") { nicholas@2844: labelType = "number"; nicholas@2844: } nicholas@2844: page.audioElements.forEach(function (element, pageIndex) { nicholas@2844: var audioObject = audioEngineContext.newTrack(element); nicholas@2844: if (element.type == 'outside-reference') { nicholas@2844: // Construct outside reference; nicholas@2844: var orNode = new interfaceContext.outsideReferenceDOM(audioObject, index, document.getElementById("outside-reference-holder")); nicholas@2844: audioObject.bindInterface(orNode); nicholas@2844: } else { nicholas@2844: // Create a slider per track nicholas@2844: var label = interfaceContext.getLabel(labelType, index, page.labelStart); nicholas@2844: var sliderObj = new interfaceObject(audioObject, label); nicholas@2844: nicholas@2844: sliderBox.appendChild(sliderObj.root); nicholas@2844: audioObject.bindInterface(sliderObj); nicholas@2844: interfaceContext.commentBoxes.createCommentBox(audioObject); nicholas@2844: index += 1; nicholas@2844: } nicholas@2844: }); nicholas@2845: interfaceObj.options.forEach(function (option) { nicholas@2845: if (option.type == "show") { nicholas@2845: switch (option.name) { nicholas@2845: case "playhead": nicholas@2845: var playbackHolder = document.getElementById('playback-holder'); nicholas@2845: if (playbackHolder === null) { nicholas@2845: playbackHolder = document.createElement('div'); nicholas@2845: playbackHolder.style.width = "100%"; nicholas@2845: playbackHolder.align = 'center'; nicholas@2845: playbackHolder.appendChild(interfaceContext.playhead.object); nicholas@2845: feedbackHolder.appendChild(playbackHolder); nicholas@2845: } nicholas@2845: break; nicholas@2845: case "page-count": nicholas@2845: var pagecountHolder = document.getElementById('page-count'); nicholas@2845: if (pagecountHolder === null) { nicholas@2845: pagecountHolder = document.createElement('div'); nicholas@2845: pagecountHolder.id = 'page-count'; nicholas@2845: } nicholas@2845: pagecountHolder.innerHTML = 'Page ' + (testState.stateIndex + 1) + ' of ' + testState.stateMap.length + ''; nicholas@2845: var inject = document.getElementById('interface-buttons'); nicholas@2845: inject.appendChild(pagecountHolder); nicholas@2845: break; nicholas@2845: case "volume": nicholas@2845: if (document.getElementById('master-volume-holder') === null) { nicholas@2845: feedbackHolder.appendChild(interfaceContext.volume.object); nicholas@2845: } nicholas@2845: break; nicholas@2845: case "comments": nicholas@2845: interfaceContext.commentBoxes.showCommentBoxes(feedbackHolder, true); nicholas@2845: break; nicholas@2845: } nicholas@2845: } nicholas@2845: }); nicholas@2844: resizeWindow(); nicholas@2844: } nicholas@2844: nicholas@2844: function interfaceObject(audioObject, label) { nicholas@2844: var container = document.getElementById("slider"); nicholas@2844: var playing = false; nicholas@2844: var root = document.createElement("div"); nicholas@2844: root.className = "ordinal-element"; nicholas@2844: root.draggable = "true"; nicholas@2844: var labelElement = document.createElement("span"); nicholas@2844: labelElement.className = "ordinal-element-label"; nicholas@2844: labelElement.textContent = label; nicholas@2844: root.appendChild(labelElement); nicholas@2844: root.classList.add("disabled"); nicholas@2844: // An example node, you can make this however you want for each audioElement. nicholas@2844: // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following nicholas@2844: // You attach them by calling audioObject.bindInterface( ) nicholas@2844: root.addEventListener("click", this, true); nicholas@2844: root.addEventListener('dragstart', this, true); nicholas@2844: root.addEventListener('dragenter', this, true); nicholas@2844: root.addEventListener('dragover', this, true); nicholas@2844: root.addEventListener('dragleave', this, true); nicholas@2844: root.addEventListener('drop', this, true); nicholas@2844: root.addEventListener('dragend', this, true); nicholas@2844: this.handleEvent = function (event) { nicholas@2844: if (event.type == "click") { nicholas@2844: if (playing === false) { nicholas@2844: audioEngineContext.play(audioObject.id); nicholas@2844: } else { nicholas@2844: audioEngineContext.stop(); nicholas@2844: } nicholas@2844: playing = !playing; nicholas@2844: return; nicholas@2844: } else if (event.type == "dragstart") { nicholas@2844: return dragStart.call(this, event); nicholas@2844: } else if (event.type == "dragenter") { nicholas@2844: return dragEnter.call(this, event); nicholas@2844: } else if (event.type == "dragleave") { nicholas@2844: return dragLeave.call(this, event); nicholas@2844: } else if (event.type == "dragover") { nicholas@2844: return dragOver.call(this, event); nicholas@2844: } else if (event.type == "drop") { nicholas@2844: return drop.call(this, event); nicholas@2844: } else if (event.type == "dragend") { nicholas@2844: return dragEnd.call(this, event); nicholas@2844: } nicholas@2844: throw (event); nicholas@2844: }; nicholas@2844: nicholas@2844: function dragStart(e) { nicholas@2844: e.currentTarget.classList.add("dragging"); nicholas@2844: nicholas@2844: e.dataTransfer.effectAllowed = 'move'; nicholas@2953: e.dataTransfer.setData('text/plain', String(audioObject.id)); nicholas@2953: sessionStorage.setItem("drag-object", String(audioObject.id)); nicholas@2844: } nicholas@2844: nicholas@2844: function dragEnter(e) { nicholas@2844: // this / e.target is the current hover target. nicholas@2844: root.classList.add('over'); nicholas@2844: } nicholas@2844: nicholas@2844: function dragLeave(e) { nicholas@2844: root.classList.remove('over'); // this / e.target is previous target element. nicholas@2844: } nicholas@2844: nicholas@2844: function dragOver(e) { nicholas@2844: if (e.preventDefault) { nicholas@2844: e.preventDefault(); // Necessary. Allows us to drop. nicholas@2844: } nicholas@2844: nicholas@2844: e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object. nicholas@2953: var srcid = e.dataTransfer.getData('text/plain'); n@2980: if (srcid === "") { nicholas@2953: srcid = sessionStorage.getItem("drag-object"); nicholas@2953: } nicholas@2953: console.log(srcid); n@2980: srcid = Number(srcid); nicholas@2844: var elements = container.childNodes; nicholas@2844: var srcObject = audioEngineContext.audioObjects.find(function (ao) { nicholas@2844: return ao.id === srcid; nicholas@2844: }); nicholas@2844: var src = srcObject.interfaceDOM.root; nicholas@2844: if (src !== root) { nicholas@2844: var srcpos = srcObject.interfaceDOM.getElementPosition(); nicholas@2844: var mypos = this.getElementPosition(); nicholas@2844: var neighbour; nicholas@2844: if (srcpos <= mypos) { nicholas@2844: neighbour = root.nextElementSibling; nicholas@2844: } else { nicholas@2844: neighbour = root; nicholas@2844: } nicholas@2844: if (neighbour) nicholas@2844: container.insertBefore(src, neighbour); nicholas@2844: else { nicholas@2844: container.removeChild(src); nicholas@2844: container.appendChild(src); nicholas@2844: } nicholas@2844: nicholas@2844: } nicholas@2844: nicholas@2844: return false; nicholas@2844: } nicholas@2844: nicholas@2844: function drop(e) { nicholas@2844: // this / e.target is current target element. nicholas@2844: nicholas@2844: if (e.stopPropagation) { nicholas@2844: e.stopPropagation(); // stops the browser from redirecting. nicholas@2844: } nicholas@2844: if (e.preventDefault) { nicholas@2844: e.preventDefault(); // Necessary. Allows us to drop. nicholas@2844: } nicholas@2844: nicholas@2844: audioEngineContext.audioObjects.forEach(function (ao) { nicholas@2844: ao.interfaceDOM.processMovement(); nicholas@2844: }); nicholas@2844: nicholas@2952: sessionStorage.removeItem("drag-object"); nicholas@2952: nicholas@2844: return false; nicholas@2844: } nicholas@2844: nicholas@2844: function dragEnd(e) { nicholas@2844: // this/e.target is the source node. nicholas@2844: $(".ordinal-element").removeClass("dragging"); nicholas@2844: $(".ordinal-element").removeClass("over"); nicholas@2844: } nicholas@2844: nicholas@2844: this.getElementPosition = function () { nicholas@2844: var elements = container.childNodes, nicholas@2844: position = 0, nicholas@2844: elem = elements[0]; nicholas@2844: while (root !== elem) { nicholas@2844: position++; nicholas@2844: elem = elem.nextElementSibling; nicholas@2844: } nicholas@2844: return position; nicholas@2845: }; nicholas@2844: nicholas@2844: this.processMovement = function () { nicholas@2844: var time = audioEngineContext.timer.getTestTime(); nicholas@2844: var pos = this.getElementPosition(); nicholas@2844: var rank = pos / (audioEngineContext.audioObjects.length - 1); nicholas@2844: audioObject.metric.moved(time, rank); nicholas@2844: console.log('slider ' + audioObject.id + ' moved to ' + rank + ' (' + time + ')'); nicholas@2845: }; nicholas@2844: nicholas@2844: this.enable = function () { nicholas@2844: // This is used to tell the interface object that playback of this node is ready nicholas@2844: root.classList.remove("disabled"); nicholas@2844: labelElement.textContent = label; nicholas@2844: }; nicholas@2844: this.updateLoading = function (progress) { nicholas@2844: // progress is a value from 0 to 100 indicating the current download state of media files nicholas@2844: labelElement.textContent = String(progress); nicholas@2844: }; nicholas@2844: this.startPlayback = function () { nicholas@2844: // Called when playback has begun nicholas@2844: root.classList.add("playing"); nicholas@2845: if (audioObject.commentDOM) { nicholas@2845: audioObject.commentDOM.trackComment.classList.add("comment-box-playing"); nicholas@2845: } nicholas@2844: }; nicholas@2844: this.stopPlayback = function () { nicholas@2844: // Called when playback has stopped. This gets called even if playback never started! nicholas@2844: root.classList.remove("playing"); nicholas@2845: playing = false; nicholas@2845: if (audioObject.commentDOM) { nicholas@2845: audioObject.commentDOM.trackComment.classList.remove("comment-box-playing"); nicholas@2845: } nicholas@2844: }; nicholas@2844: this.getValue = function () { nicholas@2844: // Return the current value of the object. If there is no value, return 0 nicholas@2844: var pos = this.getElementPosition(); nicholas@2844: var rank = pos / (audioEngineContext.audioObjects.length - 1); nicholas@2992: return rank; nicholas@2844: }; nicholas@2844: this.getPresentedId = function () { nicholas@2844: // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale nicholas@2844: return label; nicholas@2844: }; nicholas@2844: this.canMove = function () { nicholas@2844: // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale. nicholas@2844: // These are checked primarily if the interface check option 'fragmentMoved' is enabled. nicholas@2844: return true; nicholas@2844: }; nicholas@2844: this.exportXMLDOM = function (audioObject) { nicholas@2844: // Called by the audioObject holding this element to export the interface node. nicholas@2844: // If there is no value node (such as outside reference), return null nicholas@2844: // 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 nicholas@2844: // Use storage.document.createElement('value'); to generate the XML node. nicholas@2844: var node = storage.document.createElement('value'); nicholas@2847: node.textContent = this.getValue(); nicholas@2844: return node; nicholas@2844: nicholas@2844: }; nicholas@2844: this.error = function () { nicholas@2844: // If there is an error with the audioObject, this will be called to indicate a failure nicholas@2844: root.classList.remove("disabled"); nicholas@2844: labelElement.textContent = "Error"; nicholas@2844: }; nicholas@2844: Object.defineProperties(this, { nicholas@2844: "root": { nicholas@2844: "get": function () { nicholas@2844: return root; nicholas@2844: }, nicholas@2844: "set": function () {} nicholas@2844: } nicholas@2844: }); nicholas@2844: } nicholas@2844: nicholas@2844: function resizeWindow(event) { nicholas@2844: // Called on every window resize event, use this to scale your page properly nicholas@2844: var w = $("#slider").width(); nicholas@2844: var N = audioEngineContext.audioObjects.length; nicholas@2844: w /= N; nicholas@2844: w -= 14; nicholas@2845: w = Math.floor(w); nicholas@2844: $(".ordinal-element").width(w); nicholas@2844: } nicholas@2844: nicholas@2846: function buttonSubmitClick() // TODO: Only when all songs have been played! nicholas@2846: { nicholas@2846: var checks = testState.currentStateMap.interfaces[0].options, nicholas@2846: canContinue = true; nicholas@2844: nicholas@2846: // Check that the anchor and reference objects are correctly placed nicholas@2846: if (interfaceContext.checkHiddenAnchor() === false) { nicholas@2846: return; nicholas@2846: } nicholas@2846: if (interfaceContext.checkHiddenReference() === false) { nicholas@2846: return; nicholas@2846: } nicholas@2846: nicholas@2846: for (var i = 0; i < checks.length; i++) { nicholas@2846: var checkState = true; nicholas@2846: if (checks[i].type == 'check') { nicholas@2846: switch (checks[i].name) { nicholas@2846: case 'fragmentPlayed': nicholas@2846: // Check if all fragments have been played nicholas@2846: checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); nicholas@2846: break; nicholas@2846: case 'fragmentFullPlayback': nicholas@2846: // Check all fragments have been played to their full length nicholas@2846: checkState = interfaceContext.checkAllPlayed(checks[i].errorMessage); nicholas@2846: console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead'); nicholas@2846: break; nicholas@2846: case 'fragmentMoved': nicholas@2846: // Check all fragment sliders have been moved. nicholas@2846: checkState = interfaceContext.checkAllMoved(checks[i].errorMessage); nicholas@2846: break; nicholas@2846: case 'fragmentComments': nicholas@2846: // Check all fragment sliders have been moved. nicholas@2846: checkState = interfaceContext.checkAllCommented(checks[i].errorMessage); nicholas@2846: break; nicholas@2846: case 'scalerange': nicholas@2846: // Check the scale has been used effectively nicholas@2846: checkState = interfaceContext.checkScaleRange(checks[i].errorMessage); nicholas@2846: nicholas@2846: break; nicholas@2846: default: nicholas@2846: console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface"); nicholas@2846: break; nicholas@2846: } nicholas@2846: } nicholas@2846: if (checkState === false) { nicholas@2846: canContinue = false; nicholas@2846: break; nicholas@2846: } nicholas@2846: } nicholas@2846: nicholas@2846: if (canContinue) { nicholas@2846: if (audioEngineContext.status == 1) { nicholas@2846: var playback = document.getElementById('playback-button'); nicholas@2846: playback.click(); nicholas@2846: // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options nicholas@2846: } else { nicholas@2846: if (audioEngineContext.timer.testStarted === false) { nicholas@2846: interfaceContext.lightbox.post("Warning", 'You have not started the test! Please press start to begin the test!'); nicholas@2846: return; nicholas@2846: } nicholas@2846: } nicholas@2846: testState.advanceState(); nicholas@2846: } nicholas@2844: } nicholas@2844: nicholas@2844: function pageXMLSave(store, pageSpecification) { nicholas@2844: // MANDATORY nicholas@2844: // Saves a specific test page nicholas@2844: // You can use this space to add any extra nodes to your XML saves nicholas@2844: // Get the current information in store (remember to appendChild your data to it) nicholas@2844: // pageSpecification is the current page node configuration nicholas@2844: // To create new XML nodes, use storage.document.createElement(); nicholas@2844: }