/** JavaScript file of utility functions. * At present contains functions for sanitising of URLs, * since tomcat 8+, being more compliant with URL/URI standards, is more strict about URLs. */ /* Given a string consisting of a single character, returns the %hex (%XX) https://www.w3resource.com/javascript-exercises/javascript-string-exercise-27.php https://stackoverflow.com/questions/40100096/what-is-equivalent-php-chr-and-ord-functions-in-javascript https://www.w3resource.com/javascript-exercises/javascript-string-exercise-27.php */ function urlEncodeChar(single_char_string) { /*let hex = Number(single_char_string.charCodeAt(0)).toString(16); var str = "" + hex; str = "%" + str.toUpperCase(); return str; */ var hex = "%" + Number(single_char_string.charCodeAt(0)).toString(16).toUpperCase(); return hex; } /* Tomcat 8 appears to be stricter in requiring unsafe and reserved chars in URLs to be escaped with URL encoding See section "Character Encoding Chart of https://perishablepress.com/stop-using-unsafe-characters-in-urls/ Reserved chars: ; / ? : @ = & -----> %3B %2F %3F %3A %40 %3D %26 [Now also reserved, but no special meaning yet in URLs (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) and not required to be enforced yet, so we're aren't at present dealing with these: ! ' ( ) * ] Unsafe chars: " < > # % { } | \ ^ ~ [ ] ` and SPACE/BLANK ----> %22 %3C %3E %23 %25 %7B %7D %7C %5C %5E ~ %5B %5D %60 and %20 But the above conflicts with the reserved vs unreserved listings at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI Possibly more info: https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid And the bottom of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent lists additional characters that have been reserved since and which need encoding when in a URL component. Javascript already provides functions encodeURI() and encodeURIComponent(), see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI However, the set of chars they deal with only partially overlap with the set of chars that need encoding as per the RFC3986 for URIs and RFC1738 for URLs discussed at https://perishablepress.com/stop-using-unsafe-characters-in-urls/ We want to handle all the characters listed as unsafe and reserved at https://perishablepress.com/stop-using-unsafe-characters-in-urls/ so we define and use our own conceptually equivalent methods for both existing JavaScript methods: - makeSafeURL() for Javascript's encodeURI() to make sure all unsafe characters in URLs are escaped by being URL encoded - and makeSafeURLComponent() for JavaScript's encodeURIComponent to additionally make sure all reserved characters in a URL portion are escaped by being URL encoded too Function makeSafeURL() is passed a string that represents a URL and therefore only deals with characters that are unsafe in a URL and which therefore require escaping. Function makeSafeURLComponent() deals with portions of a URL that when decoded need not represent a URL at all, for example data like inline templates passed in as a URL query string's parameter values. As such makeSafeURLComponent() should escape both unsafe URL characters and characters that are reserved in URLs since reserved characters in the query string part (as query param values representing data) may take on a different meaning from their reserved meaning in a URL context. */ /* URL encodes both - UNSAFE characters to make URL safe, by calling makeSafeURL() - and RESERVED characters (characters that have reserved meanings within a URL) to make URL valid, since the url component parameter could use reserved characters in a non-URL sense. For example, the inline template (ilt) parameter value of a URL could use '=' and '&' signs where these would have XSLT rather than URL meanings. See end of https://www.w3schools.com/jsref/jsref_replace.asp to use a callback passing each captured element of a regex in str.replace() */ function makeURLComponentSafe(url_part, encode_percentages) { // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript encode_percentages = encode_percentages || 1; // this method forces the URL-encoding of any % in url_part, e.g. do this for inline-templates that haven't ever been encoded var url_encoded = makeURLSafe(url_part, encode_percentages); //return url_encoded.replace(/;/g, "%3B").replace(/\//g, "%2F").replace(/\?/g, "%3F").replace(/\:/g, "%3A").replace(/\@/g, "%40").replace(/=/g, "%3D").replace(/\&/g,"%26"); url_encoded = url_encoded.replace(/[\;\/\?\:\@\=\&]/g, function(s) { return urlEncodeChar(s); }); return url_encoded; } /* URL encode UNSAFE characters to make URL passed in safe. Set encode_percentages to 1 (true) if you don't want % signs encoded: you'd do so if the url is already partly URL encoded. */ function makeURLSafe(url, encode_percentages) { encode_percentages = encode_percentages || 0; // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript var url_encoded = url; if(encode_percentages) { url_encoded = url_encoded.replace(/\%/g,"%25"); } // encode % first //url_encoded = url_encoded.replace(/ /g, "%20").replace(/\"/g,"%22").replace(/\/g,"%3E").replace(/\#/g,"%23").replace(/\{/g,"%7B").replace(/\}/g,"%7D"); //url_encoded = url_encoded.replace(/\|/g,"%7C").replace(/\\/g,"%5C").replace(/\^/g,"%5E").replace(/\[/g,"%5B").replace(/\]/g,"%5D").replace(/\`/g,"%60"); // Should we handle ~, but then what is its URL encoded value? Because https://meyerweb.com/eric/tools/dencoder/ URLencodes ~ to ~. //return url_encoded; url_encoded = url_encoded.replace(/[\ \"\<\>\#\{\}\|\\^\~\[\]\`]/g, function(s) { return urlEncodeChar(s); }); return url_encoded; } /*************** * MENU SCRIPTS * ***************/ function moveScroller() { var move = function() { var editbar = $("#editBar"); var st = $(window).scrollTop(); var fa = $("#float-anchor").offset().top; if(st > fa) { editbar.css({ position: "fixed", top: "0px", width: editbar.data("width"), //width: "30%" }); } else { editbar.data("width", editbar.css("width")); editbar.css({ position: "relative", top: "", width: "" }); } }; $(window).scroll(move); move(); } function floatMenu(enabled) { var menu = $(".tableOfContentsContainer"); if(enabled) { menu.data("position", menu.css("position")); menu.data("width", menu.css("width")); menu.data("right", menu.css("right")); menu.data("top", menu.css("top")); menu.data("max-height", menu.css("max-height")); menu.data("overflow", menu.css("overflow")); menu.data("z-index", menu.css("z-index")); menu.css("position", "fixed"); menu.css("width", "300px"); menu.css("right", "0px"); menu.css("top", "100px"); menu.css("max-height", "600px"); menu.css("overflow", "auto"); menu.css("z-index", "200"); $("#unfloatTOCButton").show(); } else { menu.css("position", menu.data("position")); menu.css("width", menu.data("width")); menu.css("right", menu.data("right")); menu.css("top", menu.data("top")); menu.css("max-height", menu.data("max-height")); menu.css("overflow", menu.data("overflow")); menu.css("z-index", menu.data("z-index")); $("#unfloatTOCButton").hide(); $("#floatTOCToggle").prop("checked", false); } var url = gs.xsltParams.library_name + "?a=d&ftoc=" + (enabled ? "1" : "0") + "&c=" + gs.cgiParams.c; $.ajax(url); } // TK Label Scripts var tkMetadataSetStatus = "needs-to-be-loaded"; var tkMetadataElements = null; function addTKLabelToImage(labelName, definition, name, comment) { // lists of tkLabels and their corresponding codes, in order let tkLabels = ["Attribution","Clan","Family","MultipleCommunities","CommunityVoice","Creative","Verified","NonVerified","Seasonal","WomenGeneral","MenGeneral", "MenRestricted","WomenRestricted","CulturallySensitive","SecretSacred","OpenToCommercialization","NonCommercial","CommunityUseOnly","Outreach","OpenToCollaboration"]; let tkCodes = ["tk_a","tk_cl","tk_f","tk_mc","tk_cv","tk_cr","tk_v","tk_nv","tk_s","tk_wg","tk_mg","tk_mr","tk_wr","tk_cs","tk_ss","tk_oc","tk_nc","tk_co","tk_o","tk_cb"]; for (let i = 0; i < tkLabels.length; i++) { if (labelName == tkLabels[i]) { let labeldiv = document.querySelectorAll(".tklabels img"); for (image of labeldiv) { let labelCode = image.src.substr(image.src.lastIndexOf("/") + 1).replace(".png", ""); // get tk label code from image file name if (labelCode == tkCodes[i]) { image.title = "TK " + name + ": " + definition + " Click for more details."; // set tooltip if (image.parentElement.parentElement.parentElement.classList[0] != "tocSectionTitle") { // disable onclick event in favourites section image.addEventListener("click", function(e) { let currPopup = document.getElementsByClassName("tkPopup")[0]; if (currPopup == undefined || (currPopup != undefined && currPopup.id != labelCode)) { let popup = document.createElement("div"); popup.classList.add("tkPopup"); popup.id = labelCode; let popupText = document.createElement("span"); let heading = "

Traditional Knowledge Label:

" + name + "

"; let moreInformation = "
For more information about TK Labels, "; let link = document.createElement("a"); link.innerHTML = "click here."; link.href = "https://localcontexts.org/labels/traditional-knowledge-labels/"; link.target = "_blank"; popupText.innerHTML = heading + comment + moreInformation; popupText.appendChild(link); let closeButton = document.createElement("span"); closeButton.innerHTML = "×"; closeButton.id = "tkCloseButton"; closeButton.title = "Click to close window." closeButton.addEventListener("click", function(e) { closeButton.parentElement.remove(); }); popup.appendChild(closeButton); popup.appendChild(popupText); e.target.parentElement.appendChild(popup); } if (currPopup) currPopup.remove(); // remove existing popup div }); } } } } } } function addTKLabelsToImages(lang) { if (tkMetadataElements == null) { console.error("ajax call not yet loaded tk label metadata set"); } else { for (label of tkMetadataElements) { // for each tklabel element in tk.mds let tkLabelName = label.attributes.name.value; // Element name="" let attributes = label.querySelectorAll("[code=" + lang + "] Attribute"); // gets attributes for selected language let tkName = attributes[0].textContent; // name="label" let tkDefinition = attributes[1].textContent; // name="definition" let tkComment = attributes[2].textContent; // name="comment" addTKLabelToImage(tkLabelName, tkDefinition, tkName, tkComment); } } } function loadTKMetadataSetOld(lang) { tkMetadataSetStatus = "loading"; $.ajax({ url: gs.variables["tkMetadataURL"], success: function(xml) { tkMetadataSetStatus = "loaded"; let parser = new DOMParser(); let tkmds = parser.parseFromString(xml, "text/xml"); tkMetadataElements = tkmds.querySelectorAll("Element"); if (document.readyState === "complete") { addTKLabelsToImages(lang); } else { window.onload = function() { addTKLabelsToImages(lang); } } }, error: function() { tkMetadataSetStatus = "no-metadata-set-for-this-collection"; console.log("No TK Label Metadata-set found for this collection"); } }); }; function loadTKMetadataSet(lang, type) { if (gs.variables["tkMetadataURL_"+type] == undefined) { console.error("tkMetadataURL_"+type+" variable is not defined, can't load TK Metadata Set"); tkMetadataSetStatus = "no-metadata-set-for-this-"+type; return; } tkMetadataSetStatus = "loading"; $.ajax({ url: gs.variables["tkMetadataURL_"+type], async: false, success: function(xml) { tkMetadataSetStatus = "loaded"; let parser = new DOMParser(); let tkmds = parser.parseFromString(xml, "text/xml"); tkMetadataElements = tkmds.querySelectorAll("Element"); if (document.readyState === "complete") { addTKLabelsToImages(lang); } else { window.onload = function() { addTKLabelsToImages(lang); } } }, error: function() { tkMetadataSetStatus = "no-metadata-set-for-this-"+type; console.log("No TK Label Metadata-set found for this "+type); } }); }; // Audio Scripts for Enriched Playback var wavesurfer; /** * @param audio input audio file * @param sectionData diarization data (.csv) */ function loadAudio(audio, sectionData) { var inputFile = sectionData; var mod_meta_base_url = gs.xsltParams.library_name + "?a=g&rt=r&ro=0&s=ModifyMetadata&s1.collection=" + gs.cgiParams.c + "&s1.site=" + gs.xsltParams.site_name + "&s1.d=" + gs.cgiParams.d; var interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/"; // path to toolbar images var GSSTATUS_SUCCESS = 11; // more information on codes found in: GSStatus.java let editMode = false; let currentRegion = {speaker: '', start: '', end: ''}; let currentRegions = []; let itemType; let dualMode = false; let secondaryLoaded = false; let selectedVersions = ['current']; let waveformCursorX = 0; let snappedToX = 0; let snappedTo = "none"; let cursorPos = 0; let ctrlDown = false; let mouseDown = false; let newRegionOffset = 0; let editsMade = false; let undoLevel = 0; let undoStates = []; let prevUndoState = ""; let tempZoomSave = 0; let isZooming; let canvasImages = {}; // stores canvas images of each version for fast loading from cache let accentColour = "#66d640"; // let accentColour = "#F8C537"; let regionTransparency = "50"; let colourbrewerSet = colorbrewer.Set2[8]; let regionColourSet = []; let waveformContainer = document.getElementById("waveform"); let waveformSpinner = document.getElementById('waveform-blocker'); let loader = document.getElementById('waveform-loader'); let initialLoad = true; wavesurfer = WaveSurfer.create({ // wavesurfer options autoCenterImmediately: true, container: waveformContainer, backend: "WebAudio", // backgroundColor: "rgb(40, 54, 58)", backgroundColor: "rgb(29, 43, 47)", waveColor: "white", progressColor: accentColour, // progressColor: "grey", // barWidth: 1, // barHeight: 1.2, // barGap: 2, // barRadius: 1, height: 140, cursorColor: 'black', // maxCanvasWidth: 32000, minPxPerSec: 15, // default 20 partialRender: true, // use the PeakCache to improve rendering speed of large waveforms pixelRatio: 1, // 1 results in faster rendering scrollParent: true, plugins: [ WaveSurfer.regions.create({ // formatTimeCallback: function(a, b) { // return "TEST"; // } }), WaveSurfer.timeline.create({ container: "#wave-timeline", secondaryColor: "white", secondaryFontColor: "white", notchPercentHeight: "0", fontSize: "12", // zoomDebounce: 30, fontFamily: "Courier New" }), WaveSurfer.cursor.create({ showTime: true, opacity: 1, customShowTimeStyle: { 'background-color': '#000', color: '#fff', padding: '0.25rem', 'font-size': '12px' }, formatTimeCallback: (num) => { return formatCursor(num); } }), ], }); // toolbar elements & event handlers var audioContainer = document.getElementById("audioContainer"); var dualModeCheckbox = document.getElementById("dual-mode-checkbox"); var wave = document.getElementsByTagName("wave")[0]; var caretContainer = document.getElementById("caret-container"); var primaryCaret = document.getElementById("primary-caret"); var secondaryCaret = document.getElementById("secondary-caret"); var chapters = document.getElementById("chapters"); var chaptersContainer = document.getElementById("chapters-container"); var editPanel = document.getElementById("edit-panel"); var chapterButton = document.getElementById("chapterButton"); var chapterSearchInput = document.getElementById("chapter-search-input"); var zoomOutButton = document.getElementById("zoomOutButton"); var zoomSlider = document.getElementById("zoom-slider"); var zoomInButton = document.getElementById("zoomInButton"); var backButton = document.getElementById("backButton"); var playPauseButton = document.getElementById("playPauseButton"); var forwardButton = document.getElementById("forwardButton"); var editButton = document.getElementById("editorModeButton"); var downloadButton = document.getElementById("downloadButton"); var muteButton = document.getElementById("muteButton"); var volumeSlider = document.getElementById("volume-slider"); var fullscreenButton = document.getElementById("fullscreenButton"); var changeAllCheckbox = document.getElementById("change-all-checkbox"); var changeAllLabel = document.getElementById("change-all-label"); var speakerInput = document.getElementById("speaker-input"); var startTimeInput = document.getElementById("start-time-input"); var endTimeInput = document.getElementById("end-time-input"); var removeButton = document.getElementById("remove-button"); var createButton = document.getElementById("create-button"); var discardButton = document.getElementById("discard-button"); var undoButton = document.getElementById("undo-button"); var redoButton = document.getElementById("redo-button"); var saveButton = document.getElementById("save-button"); var hoverSpeaker = document.getElementById("hover-speaker"); var contextMenu = document.getElementById("context-menu"); var contextReplace = document.getElementById("context-menu-replace"); var contextOverdub = document.getElementById("context-menu-overdub"); var contextLock = document.getElementById("context-menu-lock"); var contextDelete = document.getElementById("context-menu-delete"); var timelineMenu = document.getElementById("timeline-menu"); var timelineMenuButton = document.getElementById("timeline-menu-button"); var timelineMenuHide = document.getElementById("timeline-menu-hide"); var timelineMenuDualMode = document.getElementById("timeline-menu-dualmode"); var timelineMenuRegionConflict = document.getElementById("timeline-menu-region"); var timelineMenuSpeakerConflict = document.getElementById("timeline-menu-speaker"); var versionSelectMenu = document.getElementById('version-select-menu'); var versionSelectLabels = document.querySelectorAll(".track-arrow"); var savePopup = document.getElementById("save-popup"); var savePopupBG = document.getElementById("save-popup-bg"); var savePopupCancel = document.getElementById("save-popup-cancel"); var savePopupCommit = document.getElementById("save-popup-commit"); var savePopupCommitMsg = document.getElementById("commit-message"); audioContainer.addEventListener('fullscreenchange', (e) => { fullscreenChanged() }); audioContainer.addEventListener('contextmenu', onRightClick); audioContainer.addEventListener("keyup", keyUp); audioContainer.addEventListener("keydown", keyDown); dualModeCheckbox.addEventListener("change", () => { dualModeChanged() }); wave.addEventListener('scroll', (e) => { waveformScrolled() }) wave.addEventListener('mousemove', (e) => waveformCursorX = e.x); primaryCaret.addEventListener("click", (e) => caretClicked(e.target.id)); secondaryCaret.addEventListener("click", (e) => caretClicked(e.target.id)); chapters.style.height = "0px"; chaptersContainer.style.height = "0px"; editPanel.style.height = "0px"; chapterButton.addEventListener("click", () => { toggleChapters() }); chapterSearchInput.addEventListener("input", chapterSearchInputChange) zoomOutButton.addEventListener("click", () => { zoomSlider.stepDown(); zoomSlider.dispatchEvent(new Event("input")) }); zoomInButton.addEventListener("click", () => { zoomSlider.stepUp(); zoomSlider.dispatchEvent(new Event("input")) }); backButton.addEventListener("click", () => { wavesurfer.skipBackward(); }); playPauseButton.addEventListener("click", () => { wavesurfer.playPause() }); forwardButton.addEventListener("click", () => { wavesurfer.skipForward(); }); editButton.addEventListener("click", toggleEditMode); downloadButton.addEventListener("click", () => { downloadURI(audio, audio.split(".dir/")[1]) }); muteButton.addEventListener("click", () => { if (volumeSlider.value == 0) wavesurfer.setMute(false) else wavesurfer.setMute(true) }); volumeSlider.style["accent-color"] = accentColour; fullscreenButton.addEventListener("click", toggleFullscreen); zoomSlider.style["accent-color"] = accentColour; changeAllCheckbox.addEventListener("change", () => { selectAllCheckboxChanged() }); speakerInput.addEventListener("input", speakerChange); speakerInput.addEventListener("blur", speakerInputUnfocused); createButton.addEventListener("click", createNewRegion); removeButton.addEventListener("click", removeRegion); discardButton.addEventListener("click", () => discardRegionChanges(false)); undoButton.addEventListener("click", undo); redoButton.addEventListener("click", redo); saveButton.addEventListener("click", saveRegionChanges); document.addEventListener('click', documentClicked); document.addEventListener('mouseup', () => mouseDown = false); document.addEventListener('mousedown', (e) => { if (e.target.id !== "create-button") newRegionOffset = 0 }); // resets new region offset on click document.querySelectorAll('input[type=number]').forEach(e => { e.onchange = (e) => { changeStartEndTime(e) }; // updates speaker objects when number input(s) are changed e.onblur = () => { prevUndoState = "" }; }); contextReplace.addEventListener("click", replaceSelected); contextOverdub.addEventListener("click", overdubSelected); contextLock.addEventListener("click", toggleLockSelected); contextDelete.addEventListener("click", removeRightClicked); timelineMenu.addEventListener("click", e => e.stopPropagation()); timelineMenuButton.addEventListener("click", timelineMenuToggle); timelineMenuHide.addEventListener("click", timelineMenuHideClicked); timelineMenuDualMode.addEventListener("click", () => { dualModeChanged() }); timelineMenuRegionConflict.addEventListener("click", showStartStopConflicts); timelineMenuSpeakerConflict.addEventListener("click", showSpeakerNameConflicts); savePopupCancel.addEventListener("click", toggleSavePopup) savePopupCommit.addEventListener("click", commitChanges); savePopupBG.addEventListener("click", toggleSavePopup); versionSelectLabels.forEach(arrow => arrow.addEventListener('click', toggleVersionDropdown)); volumeSlider.addEventListener("input", function() { wavesurfer.setVolume(this.value); if (this.value == 0) { muteButton.src = interface_bootstrap_images + "mute.svg"; muteButton.style.opacity = 0.6; } else { muteButton.src = interface_bootstrap_images + "unmute.svg"; muteButton.style.opacity = 1; } }); zoomSlider.addEventListener('input', function() { // slider changes waveform zoom wavesurfer.zoom(Number(this.value) / 4); if (currentRegion.speaker && getCurrentRegionIndex() != -1) { setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker); drawCurrentRegionBounds(); } let handles = document.getElementsByClassName("wavesurfer-handle"); if (this.value < 20) { for (var handle of handles) handle.style.setProperty("width", "1px", "important"); } else { for (var handle of handles) handle.style.setProperty("width", "3px", "important"); } }); showAudioLoader(); if (gs.variables.allowEditing === '0') { editButton.style.display = "none" } wavesurfer.load(audio); // wavesurfer events wavesurfer.on('region-click', handleRegionClick); wavesurfer.on('region-mouseenter', function(region) { // region hover effects if (!mouseDown) { handleRegionColours(region, true); setHoverSpeaker(region.element.style.left, region.attributes.label.innerText); if (!isInCurrentRegions(region)) { removeRegionBounds(); drawRegionBounds(region, wave.scrollLeft, "black"); } if (isCurrentRegion(region) && editMode) drawRegionBounds(region, wave.scrollLeft, "FireBrick"); } }); wavesurfer.on('region-mouseleave', function(region) { hoverSpeaker.innerHTML = ""; if (!mouseDown) { if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false); if (!editMode) hoverSpeaker.innerHTML = ""; removeRegionBounds(); if (currentRegion && currentRegion.speaker && getCurrentRegionIndex() != -1) { setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker); drawCurrentRegionBounds(); } } }); wavesurfer.on('region-in', function(region) { // play caret enters region if (!mouseDown) { handleRegionColours(region, true); if (itemType == "chapter" && Array.from(chapters.children)[getIndexOfRegion(region)]) { Array.from(chapters.children)[getIndexOfRegion(region)].scrollIntoView({ behavior: "smooth", block: "nearest" }); } } }); wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) }); wavesurfer.on('region-update-end', handleRegionEdit); // end of click-drag event wavesurfer.on('region-updated', handleRegionSnap); wavesurfer.on('error', error => console.log(error)); wavesurfer.on("play", () => { playPauseButton.src = interface_bootstrap_images + "pause.svg"; }); wavesurfer.on("pause", () => { playPauseButton.src = interface_bootstrap_images + "play.svg"; }); wavesurfer.on("mute", function(mute) { if (mute) { muteButton.src = interface_bootstrap_images + "mute.svg"; muteButton.style.opacity = 0.6; volumeSlider.value = 0; } else { muteButton.src = interface_bootstrap_images + "unmute.svg"; muteButton.style.opacity = 1; volumeSlider.value = 1; } }); wavesurfer.on('ready', function() { // retrieve regions once waveforms have loaded window.onbeforeunload = (e) => { if (undoStates.length > 1) { console.log('undoStates.length: ' + undoStates.length); e.returnValue = "Data will be lost if you leave the page, are you sure?"; return "Data will be lost if you leave the page, are you sure?"; } }; if (document.getElementById('new-canvas')) document.getElementById('new-canvas').remove(); setTimeout(() => { // if not delayed exportImage does not retrieve waveform (despite being in waveform-ready?) var currVersion = selectedVersions[(!dualMode || primaryCaret.src.includes("fill")) ? 0 : 1]; for (let key in canvasImages) { if (currVersion == key && canvasImages[key] == undefined) { canvasImages[key] = wavesurfer.exportImage() } // add waveform image to cache if one isn't already assigned to the version } }, 1000); if (initialLoad) { if (inputFile.endsWith("csv")) { // diarization if csv itemType = "chapter"; if (localStorage.getItem('undoStates') && localStorage.getItem('undoLevel')) { console.log('-- Loading regions from localStorage --'); undoStates = JSON.parse(localStorage.getItem('undoStates')); undoLevel = JSON.parse(localStorage.getItem('undoLevel')); primarySet.tempSpeakerObjects = undoStates[undoLevel].state; primarySet.speakerObjects = cloneSpeakerObjectArray(primarySet.tempSpeakerObjects); primarySet.uniqueSpeakers = []; for (var item of primarySet.tempSpeakerObjects) { if (!primarySet.uniqueSpeakers.includes(item.speaker)) primarySet.uniqueSpeakers.push(item.speaker); } populateChaptersAndRegions(primarySet); if (undoStates[undoLevel].secState && undoStates[undoLevel].secState.length > 0) { secondarySet.tempSpeakerObjects = undoStates[undoLevel].secState; secondarySet.speakerObjects = cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects); secondarySet.uniqueSpeakers = []; for (var item of secondarySet.tempSpeakerObjects) { if (!secondarySet.uniqueSpeakers.includes(item.speaker)) secondarySet.uniqueSpeakers.push(item.speaker); } secondaryLoaded = true; } updateRegionEditPanel(); } else { loadCSVFile(inputFile, primarySet); dualModeChanged(true, "true"); setTimeout(()=>{ dualModeChanged(true, "false"); }, 150) } } else if (inputFile.endsWith("json")) { // transcription if json itemType = "word"; loadJSONFile(inputFile); } else { console.log("Filetype of " + inputFile + " not supported.") } chapters.style.cursor = "default"; // remove load cursor wave.className = "audio-scroll"; $.ajax({ type: "GET", url: gs.variables.metadataServerURL, data: { a: 'get-fldv-info', site: gs.xsltParams.site_name, c: gs.cgiParams.c, d: gs.cgiParams.d }, dataType: "json", }).then(data => { for (var version of ["current", ...data]) { canvasImages[version] = undefined; let menuItem = document.createElement("div"); menuItem.classList.add("version-select-menu-item"); menuItem.id = version; let text = version.includes("nminus") ? version.replace("nminus-", "Previous(") + ")" : version; menuItem.innerText = text.charAt(0).toUpperCase() + text.slice(1); menuItem.addEventListener('click', versionClicked); let dataObj = { a: 'get-archives-metadata', site: gs.xsltParams.site_name, c: gs.cgiParams.c, d: gs.cgiParams.d, metaname: "commitmessage" }; if (version != "current") Object.assign(dataObj, {dv: version}); $.ajax({ // get commitmessage metadata to show as hover tooltip type: "GET", url: gs.variables.metadataServerURL, data: dataObj, dataType: "text", }).then(comment => { menuItem.title = "Commit message: " + comment; versionSelectMenu.append(menuItem); [...versionSelectMenu.children].sort((a,b) => a.innerText>b.innerText?1:-1).forEach(n=>versionSelectMenu.appendChild(n)); // sort alphabetically }, (error) => { console.log("get-archives-metadata error:"); console.log(error); }); } }, (error) => { console.log("get-fldv-info error:"); console.log(error); }); initialLoad = false; } // fixes blank waveform/regions when loading Current -> Prev.1 -> Prev.2 zoomSlider.value = 25; zoomSlider.dispatchEvent(new Event("input")); wavesurfer.zoom(50 / 4); hideAudioLoader(); }); /** * Draws string above waveform at the provided offset * @param {number} offset Offset (from left) to desired location * @param {string} name String to be drawn */ function setHoverSpeaker(offset, name) { hoverSpeaker.innerHTML = name; let newOffset = parseInt(offset.slice(0, -2)) - wave.scrollLeft; hoverSpeaker.style.marginLeft = newOffset + "px"; } /** Click handler, manages selected region/s, set swapping, region playing */ function handleRegionClick(region, e) { if (e.target.classList.contains("region-menu")) return; e.stopPropagation(); contextMenu.classList.remove('visible'); if (!editMode) { // play region audio on click wavesurfer.play(region.start); // plays from start of region } else { // select or deselect current region if (!region.element) return; if (region.element.classList.contains("region-top")) { currSpeakerSet = primarySet; swapCarets(true); } else if (region.element.classList.contains("region-bottom")) { currSpeakerSet = secondarySet; swapCarets(false); } prevUndoState = ""; if (!e.ctrlKey && !e.shiftKey) { currentRegions = []; currentRegion = region; currentRegion.speaker = currentRegion.attributes.label.innerText; wavesurfer.backend.seekTo(currentRegion.start); } else if (e.ctrlKey) { // control was held during click if (currentRegions.length == 0 && isCurrentRegion(region)) { removeCurrentRegion(); } else if (getCurrentRegionIndex() != -1 && isInCurrentRegions(region)) { var removeIndex = getIndexInCurrentRegions(region); if (removeIndex != -1) currentRegions.splice(removeIndex, 1); if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed currentRegion = currentRegions[0]; } } else { if (currentRegions.length < 1) currentRegions.push(currentRegion); if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist currentRegion = region; currentRegion.speaker = currentRegion.attributes.label.innerText; wavesurfer.backend.seekTo(currentRegion.start); } if (currentRegions.length == 1) currentRegions = []; // clear selected regions if there is only one } else if (e.shiftKey) { // shift was held during click clearChapterSearch(); if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) { if (currentRegions && currentRegions.length > 0) { if (Math.max(...getCurrentRegionsIndexes()) < getIndexOfRegion(region)) { // shifting forwards / down currentRegions = currSpeakerSet.tempSpeakerObjects.slice(Math.min(...getCurrentRegionsIndexes()), getIndexOfRegion(region)+1); } else { // shifting backwards / up currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), Math.max(...getCurrentRegionsIndexes())+1); } } else { if (getCurrentRegionIndex() < getIndexOfRegion(region)) { // shifting forwards / down currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getCurrentRegionIndex(), getIndexOfRegion(region)+1); } else { // shifting backwards / up currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), getCurrentRegionIndex()+1); } } } } if (changeAllCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) } reloadRegionsAndChapters(); } } /** * Returns index of given region within the currently selected regions * @param {object} region Region within currently selected regions to return index for * @returns {int} Index position of region */ function getIndexInCurrentRegions(region) { for (var reg of currentRegions) { var regSpeaker = reg.attributes ? reg.attributes.label.innerText : reg.speaker; if (reg.start == region.start && reg.end == region.end && regSpeaker == region.attributes.label.innerText) { return currentRegions.indexOf(reg); } } return -1; } /** * Returns index of region within speakerObject array * @param {object} region Region to return index for * @returns {int} Index position of region */ function getIndexOfRegion(region) { for (var reg of currSpeakerSet.tempSpeakerObjects) { if (region.attributes && reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label.innerText) { return currSpeakerSet.tempSpeakerObjects.indexOf(reg); } } return -1; } /** * Builds metadata-server.pl URL to retrieve audio at given version * @param {string} version GS document version to retrieve from (nminus-X) */ function getAudioURLFromVersion(version) { let base_url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d; if (version !== "current") base_url += "&dv=" + version // get fldv if not current version return base_url + "&assocname=" + gs.documentMetadata.Audio; } /** * Builds metadata-server.pl URL to retrieve CSV at given version * @param {string} version GS document version to retrieve from (nminus-X) */ function getCSVURLFromVersion(version) { let base_url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d; if (version !== "current") base_url += "&dv=" + version; // get fldv if not current version return base_url + "&assocname=" + "structured-audio.csv"; } /** Version click handler, first checks if changes have been made and shows popup if true */ function versionClicked(e) { let unsavedChanges = false; if (undoStates.length > 0) { // only if changes have been made in track being changed FROM let clickedVersionPos = e.target.parentElement.classList.contains('versionTop') ? 0 : 1; for (var state of undoStates) { if (state.changedTrack == selectedVersions[clickedVersionPos]) { unsavedChanges = true; break; } } } if (unsavedChanges) { var areYouSure = "There are unsaved changes.\nAre you sure you want to lose changes made in this version?"; if (window.confirm(areYouSure)) { console.log('OK'); discardRegionChanges(true); changeVersion(e); } else { console.log('CANCEL'); return; } } else changeVersion(e); } /** Changes current audio/csv set to clicked version's equivalent */ function changeVersion(e) { removeCurrentRegion(); var audio_url = getAudioURLFromVersion(e.target.id); var csv_url = getCSVURLFromVersion(e.target.id); versionSelectMenu.classList.remove('visible'); var setToUpdate = e.target.parentElement.classList.contains('versionTop') ? primarySet : secondarySet; if (e.target.parentElement.classList.contains('versionTop')) { if (!currSpeakerSet.isSecondary) { if (dualMode) $(".region-top").remove(); else $(".wavesurfer-region").remove(); showAudioLoader(); // if (canvasImages[e.target.id]) { // if waveform image exists in cache // drawImageOnWaveform(canvasImages[e.target.id]); // } wavesurfer.load(audio_url); // load audio } else { $(".region-top").remove(); } document.getElementById('track-set-label-top').children[0].innerText = e.target.id.includes("nminus") ? e.target.id.replace("nminus-", "Previous(") + ")" : "Current"; // update top label text selectedVersions[0] = e.target.id; // update the selected versions } else { if (currSpeakerSet.isSecondary) { if (dualMode) $(".region-bottom").remove(); else $(".wavesurfer-region").remove(); showAudioLoader(); // if (canvasImages[e.target.id]) { // if waveform image exists in cache // drawImageOnWaveform(canvasImages[e.target.id]); // } wavesurfer.load(audio_url); } else { $(".region-bottom").remove(); } document.getElementById('track-set-label-bottom').children[0].innerText = e.target.id.includes("nminus") ? e.target.id.replace("nminus-", "Previous(") + ")" : "Current"; // update bottom label text selectedVersions[1] = e.target.id; } loadCSVFile(csv_url, setToUpdate, true); } /** Utility function to download audio */ function downloadURI(loc, name) { let link = document.createElement("a"); link.download = name; link.href = loc; link.click(); } /** Document click listener for context box closure and region deselection */ function documentClicked(e) { // document on click if (e.target.classList.contains("region-menu")) return; contextMenu.classList.remove('visible'); timelineMenu.classList.remove('visible'); versionSelectMenu.classList.remove('visible'); versionSelectLabels.forEach(arrow => { // arrow.style.transform = 'rotate(90deg)'; // arrow.style.paddingTop = '0'; arrow.style.display = 'inline'; }); if (editMode && e.target.tagName !== "INPUT" && e.target.tagName !== "IMG" && !e.target.classList.contains("ui-button") && !$("#audio-dropdowns").has($(e.target)).length && !e.target.classList.contains("context-menu-item")) { let currReg = getCurrentRegionIndex() != -1 ? currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region : false; // save for deselection let currRegs = getCurrentRegionsIndexes().length > 1 ? currentRegions : false; // save for deselection removeCurrentRegion(); reloadChapterList(); if (currReg != false) regionLeave(currReg); // deselect curr region if (currRegs != false) { for (var reg of currRegs) { regionLeave(reg.region); // deselect curr regions regionLeave(reg.region); // deselect curr regions } } removeRegionBounds(); removeButton.innerHTML = "Remove Selected Region"; updateRegionEditPanel(); } } /** Draws and returns padlock image at given parent element */ function drawPadlock(parent) { let lockedImg = document.createElement("img"); lockedImg.classList.add("region-padlock"); lockedImg.src = interface_bootstrap_images + "lock.svg"; lockedImg.title = "This region is locked. Click to unlock region."; parent.prepend(lockedImg); return lockedImg; } /** * Draws triple dot menu button and attaches click listener * @param {object} region Region to attach menu button to */ function drawMenuButton(region) { let menuImg = document.createElement("img"); menuImg.src = interface_bootstrap_images + "menu.svg"; menuImg.classList.add("region-menu"); menuImg.title = "Show region options"; menuImg.addEventListener("click", e => { audioContainer.dispatchEvent(new MouseEvent("contextmenu", { clientX: menuImg.x + 20, clientY: menuImg.y + 5 })); }); region.element.append(menuImg); } /** * Attaches a click listener to given padlock element * @param padlock Element to attach listener to * @param region Associated region * @param isChapter Whether padlock exists in chapter (true) or wavesurfer region (false) */ function attachPadlockListener(padlock, region, isChapter) { if (isChapter == true) { padlock.addEventListener('click', () => { // attach to chapter padlock let index = getIndexOfRegion(region); currSpeakerSet.tempSpeakerObjects[index].locked = false; padlock.classList.add('hide'); if (currSpeakerSet.tempSpeakerObjects[index].region.element.firstChild) currSpeakerSet.tempSpeakerObjects[index].region.element.firstChild.remove(); addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", index); }); } else { padlock.addEventListener('click', () => { // attach to region padlock let index = getIndexOfRegion(region); currSpeakerSet.tempSpeakerObjects[index].locked = false; padlock.remove(); addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", index); }); } } /** Locks or unlocks selected region based on its current state */ function toggleLockSelected(e) { // locks / unlocks selected region(s) if (e) e.stopPropagation(); if (getCurrentRegionIndex() != -1 && currentRegions.length <= 1) { // single selected let currIndex = getCurrentRegionIndex(); currSpeakerSet.tempSpeakerObjects[currIndex].locked = !e.target.innerText.includes("Unlock"); if (currSpeakerSet.tempSpeakerObjects[currIndex].locked) { chapters.childNodes[currIndex].childNodes[1].classList.remove('hide'); let lock = drawPadlock(currSpeakerSet.tempSpeakerObjects[currIndex].region.element); attachPadlockListener(lock, currSpeakerSet.tempSpeakerObjects[currIndex].region, false); contextLock.innerText = "Unlock Selected"; } else { chapters.childNodes[currIndex].childNodes[1].classList.add('hide'); if (currSpeakerSet.tempSpeakerObjects[currIndex].region.element.getElementsByClassName("region-padlock").length > 0) { currSpeakerSet.tempSpeakerObjects[currIndex].region.element.getElementsByClassName("region-padlock")[0].remove(); } contextLock.innerText = "Lock Selected"; } } else if (currentRegions.length > 1) { // multiple selected let toLock = !e.target.innerText.includes("Unlock"); for (var idx of getCurrentRegionsIndexes()) { currSpeakerSet.tempSpeakerObjects[idx].locked = toLock; if (currSpeakerSet.tempSpeakerObjects[idx].locked) { chapters.childNodes[idx].childNodes[1].classList.remove('hide'); if (currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock").length == 0) { let lock = drawPadlock(currSpeakerSet.tempSpeakerObjects[idx].region.element); attachPadlockListener(lock, currSpeakerSet.tempSpeakerObjects[idx].region, false); } contextLock.innerText = "Unlock Selected"; } else { chapters.childNodes[idx].childNodes[1].classList.add('hide'); if (currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock").length > 0) { currSpeakerSet.tempSpeakerObjects[idx].region.element.getElementsByClassName("region-padlock")[0].remove(); } contextLock.innerText = "Lock Selected"; } } if (document.getElementById("context-menu-lock-2")) document.getElementById("context-menu-lock-2").remove(); } addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "lockChange", getCurrentRegionIndex()); } /** TODO */ function timelineMenuHideClicked(e) { // hides all regions and chapter/edit divs if (!e.target.children[0].checked) { e.target.children[0].checked = true; timelineMenuDualMode.classList.add('disabled'); timelineMenuRegionConflict.classList.add('disabled'); timelineMenuSpeakerConflict.classList.add('disabled'); if (editPanel.style.height != "0px") toggleEditMode(); if (chapters.style.height != "0px") toggleChapters(); $('.wavesurfer-region').fadeOut(100); } else { e.target.children[0].checked = false; timelineMenuDualMode.classList.remove('disabled'); timelineMenuRegionConflict.classList.remove('disabled'); timelineMenuSpeakerConflict.classList.remove('disabled'); let fadeIn = true; if (timelineMenuRegionConflict.firstElementChild.checked) { showStartStopConflicts(e, true); fadeIn = false; } if (timelineMenuSpeakerConflict.firstElementChild.checked) { showSpeakerNameConflicts(e, true); fadeIn = false; } if (fadeIn) $('.wavesurfer-region').fadeIn(100); } } function chapterSearchInputChange(e) { // filters chapters and regions by given speaker name if (e.isTrusted) { // triggered from user action if (document.getElementById("chapter-alert")) document.getElementById("chapter-alert").remove(); let matches = 0; for (var idx in chapters.children) { if (chapters.children[idx].firstChild && chapters.children[idx].classList.contains("chapter") && currSpeakerSet.tempSpeakerObjects[idx] && currSpeakerSet.tempSpeakerObjects[idx].region && currSpeakerSet.tempSpeakerObjects[idx].region.element) { if (e.composed) removeCurrentRegion(); // composed true if called from input, false if manually triggered event if (!chapters.children[idx].firstChild.innerText.toLowerCase().includes(e.target.value.toLowerCase())) { chapters.children[idx].style.display = "none"; currSpeakerSet.tempSpeakerObjects[idx].region.element.style.display = "none"; } else { chapters.children[idx].style.display = "flex"; currSpeakerSet.tempSpeakerObjects[idx].region.element.style.display = ""; matches++; if (e.target.value.length > 0) { var reg = new RegExp(e.target.value, 'gi'); // [g]lobal, [i]gnore case chapters.children[idx].firstChild.innerHTML = chapters.children[idx].firstChild.innerText.replace(reg, '$&'); // highlights matching text } else { chapters.children[idx].firstChild.innerHTML = chapters.children[idx].firstChild.innerText; // highlights matching text } } } } flashChapters(); if (matches == 0) { var msg = document.createElement("span"); msg.innerHTML = "No Matches!"; msg.id = "chapter-alert"; chapters.prepend(msg); } } } function clearChapterSearch() { // clears search filter and updates results chapterSearchInput.value = ""; chapterSearchInput.dispatchEvent(new Event("input")); } function showStartStopConflicts(e, forceRun) { // hides regions that have identical start/stop time removeCurrentRegion(); if ((dualMode && !timelineMenuRegionConflict.children[0].checked) || forceRun) { timelineMenuRegionConflict.children[0].checked = true; let primHide = []; let secHide = []; if (!timelineMenuSpeakerConflict.children[0].checked) hideAll(); for (var primIdx in primarySet.tempSpeakerObjects) { for (var secIdx in secondarySet.tempSpeakerObjects) { if (regionsMatch(primarySet.tempSpeakerObjects[primIdx], secondarySet.tempSpeakerObjects[secIdx])) { // if regions have same start/end time, hide primHide.push(primIdx); secHide.push(secIdx); } } } for (var primIdx in primarySet.tempSpeakerObjects) { if (!primHide.includes(primIdx)) { primarySet.tempSpeakerObjects[primIdx].region.element.style.display = ""; if (primaryCaret.src.includes('fill')) chapters.children[primIdx].style.display = "flex"; } } for (var secIdx in secondarySet.tempSpeakerObjects) { if (!secHide.includes(secIdx)) { secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = ""; if (secondaryCaret.src.includes('fill')) chapters.children[secIdx].style.display = "flex"; } } } else { timelineMenuRegionConflict.children[0].checked = false; if (timelineMenuSpeakerConflict.children[0].checked) showSpeakerNameConflicts(e, true); else clearConflicts(); } } function showSpeakerNameConflicts(e, forceRun) { // shows regions that have identical start/stop time but different names removeCurrentRegion(); if ((dualMode && !timelineMenuSpeakerConflict.children[0].checked) || forceRun) { timelineMenuSpeakerConflict.children[0].checked = true; if (!timelineMenuRegionConflict.children[0].checked) hideAll(); for (var primIdx in primarySet.tempSpeakerObjects) { for (var secIdx in secondarySet.tempSpeakerObjects) { if (regionsMatch(primarySet.tempSpeakerObjects[primIdx], secondarySet.tempSpeakerObjects[secIdx]) && primarySet.tempSpeakerObjects[primIdx].speaker != secondarySet.tempSpeakerObjects[secIdx].speaker) { // hide if regions match but names don't primarySet.tempSpeakerObjects[primIdx].region.element.style.display = ""; secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = ""; if (primaryCaret.src.includes('fill')) chapters.children[primIdx].style.display = "flex"; else chapters.children[secIdx].style.display = "flex"; } } } } else { timelineMenuSpeakerConflict.children[0].checked = false; if (timelineMenuRegionConflict.children[0].checked) showStartStopConflicts(e, true); else clearConflicts(); } } function clearConflicts() { // shows all regions and chapters for (var primIdx in primarySet.tempSpeakerObjects) { for (var secIdx in secondarySet.tempSpeakerObjects) { primarySet.tempSpeakerObjects[primIdx].region.element.style.display = ""; secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = ""; chapters.children[primIdx].style.display = "flex"; } } } function hideAll() { // hides all regions and chapters for (var primIdx in primarySet.tempSpeakerObjects) { for (var secIdx in secondarySet.tempSpeakerObjects) { primarySet.tempSpeakerObjects[primIdx].region.element.style.display = "none"; secondarySet.tempSpeakerObjects[secIdx].region.element.style.display = "none"; chapters.children[primIdx].style.display = "none"; } } } function timelineMenuToggle(e) { // shows / hides timeline menu e.stopPropagation(); if (timelineMenu.classList.contains('visible')) { timelineMenu.classList.remove('visible'); e.target.style.transform = 'rotate(0deg)'; } else { timelineMenu.classList.add('visible'); e.target.style.transform = 'rotate(-90deg)'; } } function handleRegionSnap(region, e) { // clips region to opposite set region if nearby, called on region update (lots) if (editMode && currentRegion && !wavesurfer.isPlaying()) { removeRegionBounds(); setHoverSpeaker(region.element.style.left, currentRegion.speaker); drawRegionBounds(region, wave.scrollLeft, "FireBrick"); // gets set to red if currRegion if (e && e.action === "resize" && dualMode && editMode && !ctrlDown) { // won't actuate on drag let oppositeSet = secondarySet; // look down if (currSpeakerSet.isSecondary) oppositeSet = primarySet; // look up if (e.direction === "left") { region.update({ start: getSnapValue(region.start, oppositeSet.tempSpeakerObjects)}); } else if (e.direction === "right") { region.update({ end: getSnapValue(region.end, oppositeSet.tempSpeakerObjects)}); } } if (e && (e.action === "resize" || e.action === "drag")) { setInputInSeconds(startTimeInput, region.start); setInputInSeconds(endTimeInput, region.end); } } } /** * Returns snap value if near [snapRadius] adjacent region edge * @param newDragPos Drag position in seconds to check for * @param speakerSet Adjacent region set * @returns {number} If found, returns snapped position, otherwise returns input position */ function getSnapValue(newDragPos, speakerSet) { var snapRadius = 1; for (var region of speakerSet) { // scan opposite region for potential snapping points if (newDragPos > parseFloat(region.start) - snapRadius && newDragPos < parseFloat(region.start) + snapRadius) { snappedTo = "start"; if (snappedToX == 0) snappedToX = waveformCursorX; return region.start; } if (newDragPos > parseFloat(region.end) - snapRadius && newDragPos < parseFloat(region.end) + snapRadius) { snappedTo = "end"; if (snappedToX == 0) snappedToX = waveformCursorX; return region.end; } if (snappedTo !== "none" && (waveformCursorX - snappedToX > 10 || waveformCursorX - snappedToX < -10)) { snappedTo = "none"; snappedToX = 0; return cursorPos; } } return newDragPos; } function mmssToSeconds(input) { var arr = input.split(":"); if (arr.length == 2) { return (parseInt(arr[0]) * 60) + parseInt(arr[1]); } else if (arr.length == 3) { return (parseInt(arr[0]) * 3600) + (parseInt(arr[1]) * 60) + parseInt(arr[2]); } else { console.error("unexpected input to mmssToSeconds: " + input); } } function removeRightClicked(e) { if (!e.target.classList.contains('disabled')) { removeRegion(); } } function replaceSelected(e) { // moves selected region across, replaces and removes any overlapping regions in the opposite set if (!e.target.classList.contains('disabled')) { let destinationSet = secondarySet; // replace down if (currSpeakerSet.isSecondary) destinationSet = primarySet; // replace up let currItems = [currentRegion]; if (currentRegions && currentRegions.length > 0) currItems = currentRegions; for (let idx = 0; idx < currItems.length; idx++) { // handles both currentRegion and currentRegions for (let idy = 0; idy < destinationSet.tempSpeakerObjects.length; idy++) { var reg = destinationSet.tempSpeakerObjects[idy]; if ((parseFloat(reg.start) >= parseFloat(currItems[idx].start) && parseFloat(reg.start) <= parseFloat(currItems[idx].end)) || (parseFloat(reg.start) <= parseFloat(currItems[idx].start) && parseFloat(reg.end) >= parseFloat(currItems[idx].start))) { destinationSet.tempSpeakerObjects.splice(idy, 1); // remove subsequent region idy--; } } } copySelected(e, true); reloadRegionsAndChapters(); addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "replace", getCurrentRegionIndex()); } } function containsRegion(set, region) { // true if given region exists in given set for (var item of set) { if (regionsMatch(region, item)) return true; } return false; } function overdubSelected(e) { // moves selected region across, merges any overlapping regions in the opposite set if (!e.target.classList.contains('disabled')) { let destinationSet = secondarySet; // replace down if (currSpeakerSet.isSecondary) destinationSet = primarySet; // replace up let backup; if (destinationSet.isSecondary) backup = cloneSpeakerObjectArray(primarySet.tempSpeakerObjects); // saves selected set as this process changes values in selected set else backup = cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects); copySelected(e, true); if (!currentRegions || currentRegions.length < 1) { // overdub single handleSameSpeakerOverlap(getCurrentRegionIndex(), destinationSet, true); } else { // overdub multiple for (var item of getCurrentRegionsIndexes().reverse()) { // reverse indexes so index doesn't break when regions are removed handleSameSpeakerOverlap(item, destinationSet, true); } } if (destinationSet.isSecondary) primarySet.tempSpeakerObjects = backup; else secondarySet.tempSpeakerObjects = backup; addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "overdub", getCurrentRegionIndex()); reloadRegionsAndChapters(); } } function copySelected(e, skipUndoState) { // copies region to opposite set [utility function for replace and overdub] if (!e.target.classList.contains('disabled')) { let destinationSet = secondarySet; // copy down if (currSpeakerSet.isSecondary) destinationSet = primarySet // copy up var selectedRegion = currentRegion; if (currentRegions && currentRegions.length > 1) { // copy multiple destinationSet.tempSpeakerObjects.push(...selectedRegions); // append current regions to dest. set // currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions) // for (var reg of destinationSet.tempSpeakerObjects) { // restore currentRegions in dest. set // for (var selReg of selectedRegions) { // if (regionsMatch(reg, selReg) && !containsRegion(currentRegions, reg)) { // currentRegions.push(reg); // } // } // if (regionsMatch(reg, selectedRegion)) { currentRegion = reg; } // } } else { // copy singular destinationSet.tempSpeakerObjects.push(selectedRegion); // append current region to dest. set // currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions) // for (var reg of destinationSet.tempSpeakerObjects) { // restore currentRegion in dest. set // if (regionsMatch(reg, selectedRegion)) { // currentRegion = reg; // break; // } // } } reloadRegionsAndChapters(); if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "copy", getCurrentRegionIndex()); } } /** * Shows context menu with various region options * @param {MouseEvent} e Either right click event or left click triple menu click event */ function onRightClick(e) { if ((e.target.classList.contains("wavesurfer-region") || e.target.id === "audioContainer" || e.target.classList.contains("chapter")) && editMode) { e.preventDefault(); e.stopPropagation(); // set current region to clicked region LLLLLLL let clickedRegion; // could be used to select clicked region for (var reg of currSpeakerSet.tempSpeakerObjects) { if (reg.region.element.title == e.target.title) { clickedRegion = reg; break; } } // console.log(clickedRegion) contextMenu.classList.add("visible"); if (e.clientX + 200 > $(window).width()) contextMenu.style.left = ($(window).width() - 220) + "px"; // ensure menu doesn't clip on right else contextMenu.style.left = e.clientX + "px"; contextMenu.style.top = e.clientY + "px"; let lockConflict = false; if (currentRegions.length > 1) { let firstIsLocked = 0; for (var reg of currentRegions) { if (firstIsLocked === 0) firstIsLocked = reg.locked; else if (firstIsLocked != reg.locked) lockConflict = true; } } if (lockConflict) { contextLock.classList.remove('disabled'); if (!document.getElementById("context-menu-lock-2")) { let contextLock2 = contextLock.cloneNode(); contextLock.innerText = "Lock Selected"; contextLock2.innerText = "Unlock Selected"; contextLock2.id = "context-menu-lock-2"; contextLock2.addEventListener('click', toggleLockSelected); contextLock.parentNode.insertBefore(contextLock2, contextLock.nextSibling); } } else { contextLock.classList.remove('disabled'); let currIndex = getCurrentRegionIndex(); if (currSpeakerSet.tempSpeakerObjects[currIndex] && currSpeakerSet.tempSpeakerObjects[currIndex].locked) { contextLock.innerText = "Unlock Selected"; chapters.childNodes[currIndex].childNodes[1].classList.remove('hide'); } else if (currSpeakerSet.tempSpeakerObjects[currIndex]) { contextLock.innerText = "Lock Selected"; chapters.childNodes[currIndex].childNodes[1].classList.add('hide'); } } if (dualMode && currentRegion && currentRegion.speaker !== "") { contextReplace.classList.remove('disabled'); contextOverdub.classList.remove('disabled'); } else { contextDelete.classList.add('disabled'); contextLock.classList.add('disabled'); contextReplace.classList.add('disabled'); contextOverdub.classList.add('disabled'); } if (currentRegion && currentRegion.speaker !== "") { contextDelete.classList.remove('disabled'); contextLock.classList.remove('disabled'); } if (dualMode) { // manipulate context texts var actionDirection = currSpeakerSet.isSecondary ? "Up" : "Down"; contextReplace.innerHTML = "Replace Selected " + actionDirection; contextOverdub.innerHTML = "Overdub Selected " + actionDirection; } } } function saveSelected(e) { let csvContent = "data:text/csv;charset=utf-8," + currSpeakerSet.speakerObjects.map(item => "\n" + [item.speaker, item.start, item.end].join()); console.log(csvContent); var encodedUri = encodeURI(csvContent); window.open(encodedUri); } function keyUp(e) { // key up listener if (e.key == "Control") ctrlDown = false; if (e.target.tagName !== "INPUT") { if (e.code === "Backspace" || e.code === "Delete") removeRegion(); else if (e.code === "Space") { wavesurfer.playPause(); } else if (e.code === "ArrowLeft") wavesurfer.skipBackward(); else if (e.code === "ArrowRight") wavesurfer.skipForward(); else if (e.code === "KeyL") toggleLockSelected(e); } if (e.code == "KeyZ" && e.ctrlKey) undo(); else if (e.code == "KeyY" && e.ctrlKey) redo(); } function keyDown(e) { // keydown listener if (e.key == "Control") ctrlDown = true; if (e.code == "Space" && e.target.tagName.toLowerCase() != "input") e.preventDefault(); } /** * Shows / hides secondary speaker set * @param skipUndoState Utility param - skips the addition of an undo state * @param overrideValue Utility param - overrides the checkbox state */ function dualModeChanged(skipUndoState, overrideValue) { if (overrideValue) dualModeCheckbox.checked = overrideValue == "true" ? true : false; else dualModeCheckbox.checked = !dualModeCheckbox.checked; // toggle dual mode checkbox dualMode = dualModeCheckbox.checked; currSpeakerSet = primarySet; if (!dualMode) removeCurrentRegion(); clearChapterSearch(); reloadRegionsAndChapters(); if (dualMode) { if (!secondaryLoaded) { // var secondaryCSVURL = "http://localhost:8383/greenstone3/cgi-bin/metadata-server.pl?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.collectionMetadata.indexStem + // "&d=" + gs.documentMetadata.Identifier + "&assocname=structured-audio.csv&dv=nminus-1"; var secondaryCSVURL = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.collectionMetadata.indexStem + "&d=" + gs.documentMetadata.Identifier + "&assocname=structured-audio.csv&dv=nminus-1"; loadCSVFile(secondaryCSVURL, secondarySet); secondaryLoaded = true; // ensure secondarySet doesn't get re-read > once } document.getElementById("caret-container").style.display = "flex"; timelineMenuRegionConflict.classList.remove("disabled"); timelineMenuSpeakerConflict.classList.remove("disabled"); $('#track-set-label-bottom').fadeIn(100); selectedVersions[1] = document.getElementById('track-set-label-bottom').children[0].innerText; } else { caretClicked('primary-caret'); document.getElementById("caret-container").style.display = "none"; selectedVersions.splice(1, 1); // trim to one version in array timelineMenuRegionConflict.firstElementChild.checked = false; timelineMenuSpeakerConflict.firstElementChild.checked = false; timelineMenuRegionConflict.classList.add("disabled"); timelineMenuSpeakerConflict.classList.add("disabled"); $('#track-set-label-bottom').fadeOut(100); } currSpeakerSet = primarySet; if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dualModeChange", getCurrentRegionIndex()); } /** * Changes selected speaker set * @param {string} id ID of clicked caret image */ function caretClicked(id) { clearChapterSearch(); if (id === "primary-caret") { currSpeakerSet = primarySet; swapCarets(true); } else if (id === "secondary-caret") { currSpeakerSet = secondarySet; swapCarets(false); } } /** * Loads destination waveform and audio if required, updates caret images * @param {boolean} toPrimary whether destination set is primary (true) or secondary (false) */ function swapCarets(toPrimary) { var currCaretIsPrimary = primaryCaret.src.includes("fill") ? true : false; // initial value before swap if ((toPrimary && !currCaretIsPrimary) || (!toPrimary && currCaretIsPrimary)) { removeCurrentRegion(); // ensure currentRegion is only removed if changing speakerSet flashChapters(); reloadChapterList(); } if (toPrimary) { if (!currCaretIsPrimary) { showAudioLoader(); if (canvasImages[selectedVersions[0]]) { // if waveform image exists in cache drawImageOnWaveform(canvasImages[selectedVersions[0]]); // hideAudioLoader(); } // else showAudioLoader(); let url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d + "&assocname=" + gs.documentMetadata.Audio; if (selectedVersions[0] !== "current") { if (selectedVersions[0].includes("Previous")) url += "&dv=" + selectedVersions[0].replace("Previous(", "nminus-").replace(")", ""); else url += "&dv=" + selectedVersions[0]; } wavesurfer.load(url); } primaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg"; secondaryCaret.src = interface_bootstrap_images + "caret-right.svg"; } else { if (currCaretIsPrimary) { showAudioLoader(); if (canvasImages[selectedVersions[1]]) { drawImageOnWaveform(canvasImages[selectedVersions[1]]); // hideAudioLoader(); } // else showAudioLoader(); let url = gs.variables.metadataServerURL + "?a=get-archives-assocfile&site=" + gs.xsltParams.site_name + "&c=" + gs.cgiParams.c + "&d=" + gs.cgiParams.d + "&assocname=" + gs.documentMetadata.Audio; if (selectedVersions[1] !== "current") { if (selectedVersions[1].includes("Previous")) url += "&dv=" + selectedVersions[1].replace("Previous(", "nminus-").replace(")", ""); else url += "&dv=" + selectedVersions[1]; } wavesurfer.load(url); } primaryCaret.src = interface_bootstrap_images + "caret-right.svg"; secondaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg"; } } /** * Shows spinning loader over waveform, hides regions */ function showAudioLoader() { $('.wavesurfer-region').fadeOut(100); $(".chapter").fadeOut(100); $(".track-set-label").fadeOut(100); waveformSpinner.style.display = 'block'; loader.style.display = "inline"; for (var ele of editPanel.children) ele.classList.add("disabled"); playPauseButton.classList.add("disabled"); } /** * Hides spinning loader, brings back regions */ function hideAudioLoader() { $('.wavesurfer-region').fadeIn(100); $(".chapter").fadeIn(100); $("#track-set-label-top").fadeIn(100); if (dualMode) $('#track-set-label-bottom').fadeIn(100); waveformSpinner.style.display = 'none'; loader.style.display = "none"; for (var ele of editPanel.children) ele.classList.remove("disabled"); updateRegionEditPanel(); playPauseButton.classList.remove("disabled"); } /** * Draws given image URL on waveform * @param image URL of image to be drawn */ function drawImageOnWaveform(image) { // console.log('draw waveform image from cache') if (document.getElementById('new-canvas')) document.getElementById('new-canvas').remove(); var newCanvas = document.createElement("div"); newCanvas.id = "new-canvas"; newCanvas.style.width = wavesurfer.drawer.canvases[0].wave.width + 'px'; newCanvas.style.height = '140px'; newCanvas.style.backgroundImage = "url('" + image + "')"; waveformContainer.appendChild(newCanvas); } /** * Regenerates chapter list to update any changes made in speakerSet */ function reloadChapterList() { chapters.innerHTML = ""; for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) { let chapter = document.createElement("div"); chapter.classList.add("chapter"); chapter.id = "chapter" + i; let speakerName = document.createElement("span"); speakerName.classList.add("speakerName"); speakerName.innerText = currSpeakerSet.tempSpeakerObjects[i].speaker; let regionLocked = document.createElement("img"); regionLocked.src = interface_bootstrap_images + "lock.svg"; regionLocked.classList.add("speakerLocked", "hide"); attachPadlockListener(regionLocked, currSpeakerSet.tempSpeakerObjects[i].region, true); if (currSpeakerSet.tempSpeakerObjects[i].locked && editMode) regionLocked.classList.remove("hide"); let speakerTime = document.createElement("span"); speakerTime.classList.add("speakerTime"); speakerTime.innerHTML = minutize(currSpeakerSet.tempSpeakerObjects[i].start) + " - " + minutize(currSpeakerSet.tempSpeakerObjects[i].end) + "s"; chapter.appendChild(speakerName); chapter.appendChild(regionLocked); chapter.appendChild(speakerTime); chapter.addEventListener("click", chapterClicked); chapter.addEventListener("mouseenter", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) }); chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) }); if (chapterSearchInput.value.length > 0 && !speakerName.innerText.toLowerCase().includes(chapterSearchInput.value.toLowerCase())) { chapter.style.display = "none"; currSpeakerSet.tempSpeakerObjects[i].region.element.style.display = "none"; } chapters.appendChild(chapter); } } /** * Shows / hides chapter section */ let toggleChapters = function() { if (chapters.style.height == "0px") { chapters.style.height = "90%"; chaptersContainer.style.height = "30vh"; chapterSearchInput.placeholder = "Filter by Name..."; } else { chapters.style.height = "0px"; chaptersContainer.style.height = "0px"; chapterSearchInput.placeholder = ""; } } /** * Object representing elements of a diarization output * @param {boolean} isSecondary Whether or not the set is secondary/bottom (true) or primary/top (false) * @param {Array} uniqueSpeakers Array of all unique speaker names within the diarization data, used for colouring regions * @param {Array} speakerObjects Array of objects containing speaker start/stop times and names * @param {Array} tempSpeakerObjects Temporary version of speakerObjects, which can be reverted back to if required */ function SpeakerSet(isSecondary, uniqueSpeakers, speakerObjects, tempSpeakerObjects) { this.isSecondary = isSecondary; this.uniqueSpeakers = uniqueSpeakers; this.speakerObjects = speakerObjects; this.tempSpeakerObjects = tempSpeakerObjects; } let primarySet = new SpeakerSet(false, [], [], [], []); let secondarySet = new SpeakerSet(true, [], [], [], []); let currSpeakerSet = primarySet; /** * Reads diarization CSV file and populates speakerSet * @param {string} filename Source destination of input CSV file * @param {object} speakerSet speaker set to be populated * @param {boolean} forcePopulate Forces redraw of regions and chapters */ function loadCSVFile(filename, speakerSet, forcePopulate) { // based on: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript $.ajax({ type: "GET", url: filename, dataType: "text", }).then(function(data) { let dataLines = data.split(/\r\n|\n/); let headers; let startIndex = 0; speakerSet.uniqueSpeakers = []; // used for obtaining unique colours speakerSet.speakerObjects = []; // list of speaker items if (dataLines[0].split(',').length === 3) headers = ["speaker", "start", "end"]; // assume speaker, start, end else if (dataLines[0].split(',').length === 4) headers = ["speaker", "start", "end", "locked"]; // assume speaker, start, end, locked for (let i = startIndex; i < dataLines.length; i++) { let data = dataLines[i].split(','); if (data.length == headers.length) { let item = {}; for (let j = 0; j < headers.length; j++) { item[headers[j]] = data[j]; if (j == 0 && !speakerSet.uniqueSpeakers.includes(data[j])) { speakerSet.uniqueSpeakers.push(data[j]); } } if (headers.length === 3) item['locked'] = false; speakerSet.speakerObjects.push(item); } } speakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(speakerSet.speakerObjects); if (!speakerSet.isSecondary || forcePopulate) populateChaptersAndRegions(speakerSet); // prevents secondary set being drawn on first load resetUndoStates(); // undo stack init }, (error) => { console.log("loadCSVFile error:"); console.log(error); }); } /** * Populates chapter list div and regions on waveform with given speaker set * @param {object} data Speaker set object with diarization data */ function populateChaptersAndRegions(data) { // colorbrewer is a web tool for guidance in choosing map colour schemes based on a letiety of settings. // this colour scheme is designed for qualitative data if (regionColourSet.length < 1) { for (let i = 0; i < data.uniqueSpeakers.length; i++) { // not tested in cases where there are more than 8 speakers!! var adjIdx = i%8; regionColourSet[adjIdx] = { name: data.uniqueSpeakers[i], colour: colourbrewerSet[adjIdx] } } } let isSelectedSet = false; if ((!data.isSecondary && primaryCaret.src.includes("fill")) || (data.isSecondary && secondaryCaret.src.includes("fill"))) isSelectedSet = true; data.tempSpeakerObjects = sortSpeakerObjectsByStart(data.tempSpeakerObjects); // sort speakerObjects by start time if (isSelectedSet || !dualMode) chapters.innerHTML = ""; // clear chapter div for re-population for (let i = 0; i < data.tempSpeakerObjects.length; i++) { let chapter = document.createElement("div"); chapter.classList.add("chapter"); chapter.id = "chapter" + i; let speakerName = document.createElement("span"); speakerName.classList.add("speakerName"); speakerName.innerText = data.tempSpeakerObjects[i].speaker; let regionLocked = document.createElement("img"); regionLocked.src = interface_bootstrap_images + "lock.svg"; regionLocked.classList.add("speakerLocked", "hide"); attachPadlockListener(regionLocked, data.tempSpeakerObjects[i].region, true); if (data.tempSpeakerObjects[i].locked && editMode) regionLocked.classList.remove("hide"); let speakerTime = document.createElement("span"); speakerTime.classList.add("speakerTime"); speakerTime.innerHTML = minutize(data.tempSpeakerObjects[i].start) + " - " + minutize(data.tempSpeakerObjects[i].end) + "s"; chapter.appendChild(speakerName); chapter.appendChild(regionLocked); chapter.appendChild(speakerTime); chapter.addEventListener("click", chapterClicked); chapter.addEventListener("mouseenter", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) }); chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) }); let selected = false; let dummyRegion = { start: data.tempSpeakerObjects[i].start, end: data.tempSpeakerObjects[i].end }; if ((isSelectedSet || !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) { chapter.classList.add("selected-chapter"); selected = true; } if (isSelectedSet || !dualMode) chapters.appendChild(chapter); let regColour; if (regionColourSet.find(item => item.name === data.tempSpeakerObjects[i].speaker)) { regColour = regionColourSet.find(item => item.name === data.tempSpeakerObjects[i].speaker).colour; } else { regionColourSet.push({ name: data.tempSpeakerObjects[i].speaker, colour: colourbrewerSet[i%8]}); regColour = regionColourSet.at(-1).colour; } let associatedReg = wavesurfer.addRegion({ // create associated wavesurfer region id: "region" + i, start: data.tempSpeakerObjects[i].start, end: data.tempSpeakerObjects[i].end, drag: editMode, resize: editMode, attributes: { label: speakerName, }, color: regColour + regionTransparency, //...(selected) && {color: "rgba(255,50,50,0.5)"}, }); // I think I checked with Finn that this fix by Dr Bainbridge is fine: if(selected) { associatedReg.color="rgba(255,50,50,0.5)"; } data.tempSpeakerObjects[i].region = associatedReg; if (selected && data.tempSpeakerObjects[i].locked) { // add padlock to regions if they are selected and locked let lock = drawPadlock(associatedReg.element); attachPadlockListener(lock, associatedReg, false); } if (selected) drawMenuButton(associatedReg); } if (waveformSpinner.style.display == 'block') $(".wavesurfer-region").fadeOut(100); // keep regions hidden until wavesurfer.load() has finished let handles = document.getElementsByTagName('handle'); for (var handle of handles) handle.addEventListener('mousedown', () => mouseDown = true); let regions = document.getElementsByTagName("region"); if (dualMode) { if (document.getElementsByClassName("region-top").length == 0) { for (var reg of regions) { if (reg.classList.length == 1) reg.classList.add("region-top"); } } else { for (var rego of regions) { if (!rego.classList.contains("region-top") && rego.classList.length == 1) rego.classList.add("region-bottom"); } } } if (editMode) for (var reg of regions) reg.style.setProperty("z-index", "3", "important"); else for (var reg of regions) reg.style.setProperty("z-index", "1", "important"); chapterSearchInput.dispatchEvent(new Event("input")); } function loadJSONFile(filename) { $.ajax({ type: "GET", url: filename, dataType: "text", }).then(function(data){ populateWords(JSON.parse(data)) }, (error) => { console.log("loadJSONFile error:"); console.log(error); }); } function populateWords(data) { // populates word section and adds regions to waveform let transcription = data.transcription; let words = data.words; let wordContainer = document.createElement("div"); wordContainer.id = "word-container"; for (let i = 0; i < words.length; i++) { let word = document.createElement("span"); word.id = "word" + i; word.classList.add("word"); word.innerHTML = transcription.split(" ")[i]; word.addEventListener("click", e => { wordClicked(data, e.target.id) }); word.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) }); word.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) }); wordContainer.appendChild(word); wavesurfer.addRegion({ id: "region" + i, start: words[i].startTime, end: words[i].endTime, drag: false, resize: false, color: "rgba(255, 255, 255, 0.1)", }); } chapters.appendChild(wordContainer); } let chapterClicked = function(e) { // plays audio from start of chapter var index = Array.from(chapters.children).indexOf(e.target); if (currSpeakerSet.tempSpeakerObjects[index]) { let clickedRegion = currSpeakerSet.tempSpeakerObjects[index].region; handleRegionClick(clickedRegion, e); } } function wordClicked(data, id) { // plays audio from start of word let index = id.replace("word", ""); let start = data.words[index].startTime; wavesurfer.play(start); } function chapterEnter(idx) { let reg = currSpeakerSet.tempSpeakerObjects[idx].region; regionEnter(reg); setHoverSpeaker(reg.element.style.left, reg.attributes.label.innerText); if (!isInCurrentRegions(reg)) { removeRegionBounds(); drawRegionBounds(reg, wave.scrollLeft, "black"); } } function chapterLeave(idx) { regionLeave(currSpeakerSet.tempSpeakerObjects[idx].region); removeRegionBounds(); hoverSpeaker.innerHTML = ""; if (currentRegion.speaker && getCurrentRegionIndex() != -1) { setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker); drawCurrentRegionBounds(); } } /** * Handles region and chapter colours * @param {object} region Region element to adjust * @param {boolean} highlight Whether or not region should be white-highlighted */ function handleRegionColours(region, highlight) { // handles region, chapter & word colours if (!dualMode || (region.element.classList.contains("region-top") && primaryCaret.src.includes("fill")) || region.element.classList.contains("region-bottom") && secondaryCaret.src.includes("fill")) { let colour; if (highlight) { colour = "rgb(81, 90, 90)"; regionEnter(region); } else { colour = ""; regionLeave(region); } if (isCurrentRegion(region) || isInCurrentRegions(region)) { colour = "rgba(255, 50, 50, 0.5)"; } if (chapters.childNodes[getIndexOfRegion(region)]) chapters.childNodes[getIndexOfRegion(region)].style.backgroundColor = colour; } } function regionEnter(region) { if (isCurrentRegion(region) || isInCurrentRegions(region)) { region.update({ color: "rgba(255, 50, 50, 0.5)" }); } else { region.update({ color: "rgba(255, 255, 255, 0.3)" }); } if (editMode && currSpeakerSet.tempSpeakerObjects[getIndexOfRegion(region)] && currSpeakerSet.tempSpeakerObjects[getIndexOfRegion(region)].locked && region.element.getElementsByClassName("region-padlock").length == 0) { // hovered region is locked let lock = drawPadlock(region.element); attachPadlockListener(lock, region, false); } if (editMode && region.element.getElementsByClassName("region-menu").length == 0) { drawMenuButton(region); } } function regionLeave(region) { if (itemType == "chapter") { if (isCurrentRegion(region) || isInCurrentRegions(region)) { region.update({ color: "rgba(255, 50, 50, 0.5)" }); // } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) { } else { let index = region.id.replace("region", ""); region.update({ color: regionColourSet.find(item => item.name === currSpeakerSet.tempSpeakerObjects[index].speaker).colour + regionTransparency }); } if (region.element.getElementsByTagName("img").length > 0 && !isCurrentRegion(region) && !isInCurrentRegions(region)) { for (let child of Array.from(region.element.children)) { if (child.tagName == "IMG") { child.remove(); } } } } else { region.update({ color: "rgba(255, 255, 255, 0.1)" }); } } function minutize(num) { // converts seconds to m:ss for chapters & waveform hover let date = new Date(null); date.setSeconds(num); return date.toTimeString().split(" ")[0].substring(3); } function formatCursor(num) { cursorPos = num; return minutize(num); } function getLetter(val) { let speakerNum = parseInt(val.replace("SPEAKER_","")); return String.fromCharCode(65 + speakerNum); // 'A' == UTF-16 65 } function toggleEditMode(skipDualModeToggle) { // toggles edit panel and redraws regions with resize handles if (gs.variables.allowEditing === '1') { toggleEditPanel(); updateRegionEditPanel(); reloadChapterList(); } } function toggleVersionDropdown(e) { e.stopPropagation(); if (versionSelectMenu.classList.contains("visible")) { e.target.style.display = 'inline'; versionSelectMenu.classList.remove("visible"); } else { e.target.style.display = 'none'; versionSelectMenu.classList.add("visible"); versionSelectMenu.style.top = "2rem"; versionSelectMenu.style.height = wave.clientHeight + wavesurfer.timeline.container.clientHeight + document.getElementById("audio-toolbar").clientHeight - 6 + "px"; if (e.target.parentElement.id.includes("top")) versionSelectMenu.classList.add("versionTop"); else versionSelectMenu.classList.remove("versionTop"); for (version of versionSelectMenu.children) { // handle disabling of regions if being viewed if (selectedVersions.includes(version.id) || selectedVersions.includes(version.innerText)) version.classList.add('disabled'); else version.classList.remove('disabled'); } } } function toggleEditPanel() { // show & hide edit panel removeCurrentRegion(); hoverSpeaker.innerHTML = ""; if (editPanel.style.height == "0px") { if (chapters.style.height == "0px") toggleChapters(); // expands chapter panel editPanel.style.height = "30vh"; editPanel.style.padding = "0.5rem"; setRegionEditMode(true); } else { editPanel.style.height = "0px"; editPanel.style.padding = "0px"; setRegionEditMode(false); } } function setRegionEditMode(state) { editMode = state; chapters.innerHTML = ''; $('.wavesurfer-region').hide(); reloadRegionsAndChapters(); // editMode sets drag/resize property when regions are redrawn } /** * Handles the edit of region start time, stop time, or speaker name, updating the speaker set * @param {object} region Region that has been updated */ function handleRegionEdit(region, e) { if (region.element.classList.contains("region-bottom")) { currSpeakerSet = secondarySet; swapCarets(false) } else { currSpeakerSet = primarySet; swapCarets(true) } editsMade = true; currentRegion = region; wavesurfer.backend.seekTo(region.start); let regionIndex = getCurrentRegionIndex(); currentRegion.speaker = currSpeakerSet.tempSpeakerObjects[regionIndex].speaker; currSpeakerSet.tempSpeakerObjects[regionIndex].region = region; currSpeakerSet.tempSpeakerObjects[regionIndex].start = region.start; currSpeakerSet.tempSpeakerObjects[regionIndex].end = region.end; var chaps = chapters.childNodes; // chapter list chaps[regionIndex].childNodes[1].textContent = minutize(region.start) + " - " + minutize(region.end) + "s"; // update chapter item time currSpeakerSet.tempSpeakerObjects[regionIndex].region.update({start: region.start, end: region.end}); // update start/end handleSameSpeakerOverlap(getCurrentRegionIndex(), currSpeakerSet); // recalculate index in case start pos has changed addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dragdrop", getCurrentRegionIndex()); editLockedRegion(currSpeakerSet.tempSpeakerObjects[regionIndex], chaps); editPanel.click(); // fixes buttons needing to be clicked twice (unknown cause!) } /** * Shows popup to ensure user is aware they are editing a locked region * @param {object} region Region that is being edited */ function editLockedRegion(region) { // ensures user is aware region being edited is locked if (region.locked) { let confirm = false; confirm = window.confirm("Editing a locked region will unlock it, are you sure you want to continue?"); if (!confirm) undo(); // undo change if no else { // remove lock if yes region.locked = false; if (region.region && region.region.element.firstChild) region.region.element.firstChild.remove(); // remove region padlock if (chapters.childNodes[getCurrentRegionIndex()] && chapters.childNodes[getCurrentRegionIndex()].childNodes[1].tagName === "IMG") { chapters.childNodes[getCurrentRegionIndex()].childNodes[1].classList.add('hide'); // remove chapter padlock } } } } /** * Merges same-speaker regions with overlapping bounds * @param {int} regionIdx Index of dragged/edited region * @param {object} speakerSet Speaker set dragged region exists in * @param {boolean} skipCurrentRegionUpdate Whether or not to skip the updating of current region */ function handleSameSpeakerOverlap(regionIdx, speakerSet, skipCurrentRegionUpdate) { let draggedRegion = speakerSet.tempSpeakerObjects[regionIdx]; // regionIdx may point to a different region within the for-loop after adjustments, so defined here let draggedRegionSpeaker = draggedRegion.speaker; for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) { if (speakerSet.tempSpeakerObjects[i].speaker === draggedRegionSpeaker && !regionsMatch(draggedRegion, speakerSet.tempSpeakerObjects[i])) { // ensure speaker name match if (parseFloat(speakerSet.tempSpeakerObjects[i].start) <= parseFloat(draggedRegion.end) && parseFloat(draggedRegion.start) <= parseFloat(speakerSet.tempSpeakerObjects[i].end)) { // ensure overlap draggedRegion.start = Math.min(speakerSet.tempSpeakerObjects[i].start, draggedRegion.start); draggedRegion.end = Math.max(speakerSet.tempSpeakerObjects[i].end, draggedRegion.end); draggedRegion.region.update({start: Math.min(speakerSet.tempSpeakerObjects[i].start, draggedRegion.start), end: Math.max(speakerSet.tempSpeakerObjects[i].end, draggedRegion.end)}); if (!skipCurrentRegionUpdate) currentRegion = draggedRegion; speakerSet.tempSpeakerObjects[i].region.remove(); speakerSet.tempSpeakerObjects.splice(i, 1); // remove consumed region setInputInSeconds(startTimeInput, draggedRegion.region.start); // update number inputs setInputInSeconds(endTimeInput, draggedRegion.region.end); i = -1; // reset for loop to support multiple consumptions } } } for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) { // remove duplicates if (speakerSet.tempSpeakerObjects[i] && speakerSet.tempSpeakerObjects[i+1]) { if (regionsMatch(speakerSet.tempSpeakerObjects[i], speakerSet.tempSpeakerObjects[i+1])) { speakerSet.tempSpeakerObjects[i+1].region.remove(); speakerSet.tempSpeakerObjects.splice(i+1, 1); // remove consumed region i--; } } } } /** * Updates the edit panel elements based on various editing states */ function updateRegionEditPanel() { if (currentRegion && currentRegion.speaker == "") { removeButton.classList.add("disabled"); speakerInput.classList.add("disabled"); changeAllCheckbox.classList.add("disabled"); changeAllCheckbox.disabled = true; disableStartEndInputs(); speakerInput.readOnly = true; speakerInput.value = ""; } else { removeButton.classList.remove("disabled"); speakerInput.classList.remove("disabled"); changeAllCheckbox.classList.remove("disabled"); if (!isZooming) changeAllCheckbox.disabled = false; enableStartEndInputs(); speakerInput.readOnly = false; } if (editsMade) { discardButton.classList.remove("disabled"); saveButton.classList.remove("disabled"); } else { discardButton.classList.add("disabled"); saveButton.classList.add("disabled"); } if (changeAllCheckbox.checked) { // changeAllLabel.innerHTML = "Change all (x" + currentRegions.length + ")"; disableStartEndInputs(); } if (currentRegion && currentRegion.speaker != "") { speakerInput.value = currentRegion.speaker; setInputInSeconds(startTimeInput, currentRegion.start); setInputInSeconds(endTimeInput, currentRegion.end); } if (undoLevel - 1 < 0) undoButton.classList.add("disabled"); else undoButton.classList.remove("disabled"); if (undoLevel + 1 >= undoStates.length) redoButton.classList.add("disabled"); else redoButton.classList.remove("disabled"); } /** * Adds a new region to the waveform at the current caret location with the speaker name "NEW_SPEAKER" */ function createNewRegion() { // adds a new region to the waveform clearChapterSearch(); var speaker = "NEW_SPEAKER"; // default name if (!currSpeakerSet.uniqueSpeakers.includes(speaker)) { currSpeakerSet.uniqueSpeakers.push(speaker) } var start = newRegionOffset + wavesurfer.getCurrentTime(); var end = newRegionOffset + wavesurfer.getCurrentTime() + 15; newRegionOffset += 5; // offset new region if multiple new regions are created. currSpeakerSet.tempSpeakerObjects.push({speaker: speaker, start: start, end: end}); editsMade = true; currentRegions = []; currentRegion = getRegionFromProps({speaker: speaker, start: start, end: end}); reloadRegionsAndChapters(); addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "create", getCurrentRegionIndex()); } function getRegionFromProps(props, speakerSet) { // find region using speaker, start & end time if (!speakerSet) speakerSet = currSpeakerSet; for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) { if (speakerSet.tempSpeakerObjects[i].speaker === props.speaker && speakerSet.tempSpeakerObjects[i].start === props.start && speakerSet.tempSpeakerObjects[i].end === props.end) { return speakerSet.tempSpeakerObjects[i]; } } console.log("getRegionFromProps failed to find matching region"); } /** * Removes the currently selected region or regions */ function removeRegion() { if (!removeButton.classList.contains("disabled")) { if (getCurrentRegionIndex() != -1) { // if currentRegion has been set let currentRegionIndex = getCurrentRegionIndex(); let currentRegionIndexes = getCurrentRegionsIndexes(); let lockTemplate = { locked: currSpeakerSet.tempSpeakerObjects[currentRegionIndex].locked }; for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) { if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { currSpeakerSet.tempSpeakerObjects[i].region.remove(); currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove from tempSpeakerObjects editsMade = true; if (i >= 0) i--; // decrement index for side-by-side regions if (!changeAllCheckbox.checked && currentRegions.length < 1) { removeCurrentRegion(); addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex); updateRegionEditPanel(); reloadChapterList(); editLockedRegion(lockTemplate); return; // jump out of function } } else if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i])) { currSpeakerSet.tempSpeakerObjects[i].region.remove(); currSpeakerSet.tempSpeakerObjects.splice(i, 1); if (i >= 0) i--; } } removeCurrentRegion(); addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed updateRegionEditPanel(); reloadChapterList(); editLockedRegion(lockTemplate); } else { console.log("no region selected") } } } function regionsMatch(reg1, reg2) { if (reg1 && reg2 && reg1.start == reg2.start && reg1.end == reg2.end) return true; return false; } function isCurrentRegion(region) { if (regionsMatch(currentRegion, region)) return true; return false; } function isInCurrentRegions(region) { if (currentRegions != []) { for (let i = 0; i < currentRegions.length; i++) { if (currentRegions[i].start == region.start && currentRegions[i].end == region.end) { return true; } } } return false; } function getCurrentRegionIndex() { // returns the index of currently selected region for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) { if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { return i } } return -1; } function getCurrentRegionsIndexes() { // returns the indexes of currently selected regions let indexes = []; for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) { if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i].region)) { indexes.push(i) } } return indexes; } function removeCurrentRegion() { // removes current region, regions and bound markers currentRegion = {speaker: '', start: '', end: ''}; currentRegions = []; removeRegionBounds(); hoverSpeaker.innerHTML = ""; } function getRegionsWithSpeaker(speaker) { // returns all regions with the given speaker name let out = []; for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) { if (currSpeakerSet.tempSpeakerObjects[i].speaker === speaker) { out.push(currSpeakerSet.tempSpeakerObjects[i]) } } return out; } function sortSpeakerObjectsByStart(speakerOb) { // sorts the speaker object array by start time return speakerOb.sort(function(a,b) { return a.start - b.start; }); } /** * Changes the associated speaker name of a region, updating the speaker set */ function speakerChange() { var newSpeaker = speakerInput.value; clearChapterSearch(); if (newSpeaker && newSpeaker.trim() != "") { speakerInput.style.outline = "2px solid transparent"; if (getCurrentRegionIndex() != -1) { // if a region is selected var chaps = chapters.childNodes; if (!currSpeakerSet.uniqueSpeakers.includes(newSpeaker)) { currSpeakerSet.uniqueSpeakers.push(newSpeaker) } if (currentRegions && currentRegions.length < 1) { // single change currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].speaker = newSpeaker; // update corrosponding speakerObject speaker currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.attributes.label.innerText = newSpeaker; chaps[getCurrentRegionIndex()].firstChild.textContent = newSpeaker; // update chapter text } else if (currentRegions && currentRegions.length > 1) { // multiple changes for (idx of getCurrentRegionsIndexes()) { currSpeakerSet.tempSpeakerObjects[idx].speaker = newSpeaker; currSpeakerSet.tempSpeakerObjects[idx].region.attributes.label.innerText = newSpeaker; chaps[idx].firstChild.textContent = newSpeaker; } } currentRegion.speaker = newSpeaker; chapterLeave(getCurrentRegionIndex()); // update region bound text editsMade = true; addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "speaker-change", getCurrentRegionIndex(), getCurrentRegionsIndexes()); editLockedRegion(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()]); } else { console.log("no region selected") } } else { console.log("no text in speaker input"); speakerInput.style.outline = "2px solid firebrick"; } } function speakerInputUnfocused() { prevUndoState = ""; if (speakerInput.value == "" && !speakerInput.classList.contains("disabled")) { speakerInput.style.outline = "2px solid firebrick"; window.alert("Speaker input cannot be left empty. Please enter a speaker name."); setTimeout(() => speakerInput.focus(), 10); // timeout needed otherwise input isn't selected } else speakerInput.style.outline = "2px transparent"; } /** * Selects all (or reverts select-all) regions matching any of the currently selected speaker names * @param {boolean} skipUndoState Whether or not to skip the addition of an undo state */ function selectAllCheckboxChanged(skipUndoState) { // "Change all" toggled if (changeAllCheckbox.checked) { if (!isZooming) { tempZoomSave = zoomSlider.value; zoomTo(0); // zoom out to encompass all selected regions } let uniqueSelectedSpeakers; if (currentRegions && currentRegions.length > 0) { // if more than one region selected uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); } else uniqueSelectedSpeakers = [currentRegion.speaker]; currentRegions = []; for (var speaker of uniqueSelectedSpeakers) { for (var region of getRegionsWithSpeaker(speaker)) { currentRegions.push(region); region.region.update({color: "rgba(255,50,50,0.5)"}); } } } else { if (!isZooming) { zoomTo(tempZoomSave / 4); // zoom back in to previous level } currentRegions = []; // this will lose track of previously selected region*s* } reloadRegionsAndChapters(); if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "selectAllChange", getCurrentRegionIndex(), getCurrentRegionsIndexes()); } function enableStartEndInputs() { // removes the 'disabled' tag from all time inputs for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = false } for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = false } } function disableStartEndInputs() { // adds the 'disabled' tag to all time inputs for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].value = 0; startTimeInput.childNodes[idx].disabled = true; } for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].value = 0; endTimeInput.childNodes[idx].disabled = true; } } /** * Zooms wavesurfer waveform to destination zoom level, used in select all function * @param {number} dest Destination zoom level */ function zoomTo(dest) { isZooming = true; changeAllCheckbox.disabled = true; let isOut = false; if (dest == 0) isOut = true; zoomInterval = setInterval(() => { if (isOut) { if (zoomSlider.value != 0) { if (zoomSlider.value > 50) zoomSlider.value -= 30; // ramp up for finer adjustments else zoomSlider.stepDown(); wavesurfer.zoom(zoomSlider.value / 4); } else { clearInterval(zoomInterval); isZooming = false; changeAllCheckbox.disabled = false; zoomSlider.dispatchEvent(new Event("input")); } } else { if (zoomSlider.value / 4 < dest) { if (zoomSlider.value > 50) zoomSlider.value += 30; // ramp up for finer adjustments else zoomSlider.stepUp(); wavesurfer.zoom(zoomSlider.value / 4); } else { clearInterval(zoomInterval); isZooming = false; changeAllCheckbox.disabled = false; zoomSlider.dispatchEvent(new Event("input")); } } }, 10); // 10ms interval } function toggleSavePopup() { // shows / hides commit popup div savePopupCommitMsg.value = savePopupCommitMsg.value.trim(); // clears initial whitespace caused by if (savePopup.classList.contains("visible")) { savePopup.classList.remove("visible"); savePopupBG.classList.remove("visible"); } else { savePopup.classList.add("visible"); savePopupBG.classList.add("visible"); savePopup.children[0].innerText = "Commit changes for: " + selectedVersions[(!dualMode || primaryCaret.src.includes("fill")) ? 0 : 1]; } } function saveRegionChanges() { // saves tempSpeakerObjects to speakerObjects if (!saveButton.classList.contains("disabled")) { toggleSavePopup(); // old save functionality // currSpeakerSet.speakerObjects = cloneSpeakerObjectArray(currSpeakerSet.tempSpeakerObjects); // editsMade = false; // removeCurrentRegion(); // reloadRegionsAndChapters(); // console.log("saved changes."); } } /** * Commits changes made to the currently selected set to Greenstone's version history system. * Firstly increments FLDV, then saves commit message to document's metadata, then sets document's * associated file to tempSpeakerObjects CSV. */ function commitChanges() { if (savePopupCommitMsg.value && savePopupCommitMsg.value.length > 0) { console.log('committing with message: ' + savePopupCommitMsg.value); // inc fldv_history $.ajax({ type: "GET", url: mod_meta_base_url, data: { "o": "json", "s1.a": "inc-fldv-nminus1" } }).then((out) => { console.log('fldv inc success with status code: ' + out.page.pageResponse.status.code); if (out.page.pageResponse.status.code == GSSTATUS_SUCCESS) { ajaxSetCommitMeta(); } }, (error) => { console.log("inc-fldv-nminus1 error:\n" + error) }); toggleSavePopup(); } else { window.alert("Commit message cannot be left empty."); } } function ajaxSetCommitMeta() { // saves commit message to current document's metadata $.ajax({ type: "GET", url: mod_meta_base_url, data: { "o" : "json", "s1.a": "set-archives-metadata", "s1.metaname": "commitmessage", "s1.metavalue": savePopupCommitMsg.value.trim(), "s1.metamode": "override" }, }).then((out) => { console.log('commit success with status code: ' + out.page.pageResponse.status.code); if (out.page.pageResponse.status.code == GSSTATUS_SUCCESS) { ajaxSetAssocFile(); } }, (error) => { console.log("commit_msg_url error:"); console.log(error); }); } function ajaxSetAssocFile() { // sets current document's associated file to tempSpeakerObjects $.ajax({ type: "POST", url: gs.xsltParams.library_name, data: { "o" : "json", "a": "g", "rt": "r", "ro": "0", "s": "ModifyMetadata", "s1.collection": gs.cgiParams.c, "s1.site": gs.xsltParams.site_name, "s1.d": gs.cgiParams.d, "s1.a": "set-archives-assocfile", "s1.assocname": "structured-audio.csv", "s1.filedata": speakerObjToCSVText() }, }).then((out) => { console.log('set-archives-assocfile success with status code: ' + out.page.pageResponse.status.code); resetUndoStates(); }, (error) => { console.log("set_assoc_url error:"); console.log(error); }); } function speakerObjToCSVText() { // converts tempSpeakerObjects to csv-like string console.log(currSpeakerSet.tempSpeakerObjects.map(item => [item.speaker, item.start, item.end, item.locked]).join('\n')); return currSpeakerSet.tempSpeakerObjects.map(item => [item.speaker, item.start, item.end, item.locked]).join('\n'); } function discardRegionChanges(forceDiscard) { // resets tempSpeakerObjects to speakerObjects if (!discardButton.classList.contains("disabled") || forceDiscard) { let confirm = false; if (!forceDiscard) { confirm = window.confirm("Are you sure you want to discard changes?"); } if (confirm || forceDiscard) { currSpeakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(currSpeakerSet.speakerObjects); editsMade = false; removeCurrentRegion(); resetUndoStates(); reloadRegionsAndChapters(); console.log("discarded changes"); } } } /** * Redraws edit panel, chapter list and wavesurfer regions from speaker set */ function reloadRegionsAndChapters() { // redraws edit panel, chapter list, wavesurfer regions updateRegionEditPanel(); $(".region-top").remove(); $(".region-bottom").remove(); $(".wavesurfer-region").remove(); populateChaptersAndRegions(primarySet); if (dualMode) { populateChaptersAndRegions(secondarySet); currSpeakerSet = primarySet; } updateCurrSpeakerSet(); if (editMode && currentRegion && currentRegion.speaker && getCurrentRegionIndex() != -1 && currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element) { setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker); drawCurrentRegionBounds(); } if (currentRegions.length < 1) { removeButton.innerHTML = "Remove Selected Region"; // enableStartEndInputs(); } else { removeButton.innerHTML = "Remove Selected Regions (x" + currentRegions.length + ")"; var uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); speakerInput.value = uniqueSelectedSpeakers.join(", "); } } /** * Handles the change of a region's start or end time, updating hte speaker set */ function changeStartEndTime(e) { // start/end time input handler let newStart = getTimeInSecondsFromInput(startTimeInput); let newEnd = getTimeInSecondsFromInput(endTimeInput); let duration = Math.floor(wavesurfer.getDuration()); // total duration of current audio if (getCurrentRegionIndex() != -1) { // if there is a selected region if (newEnd <= newStart) newStart = newEnd - 1; // when start time > end time, push region forward if (newEnd <= 0) newEnd = 1; if (newStart < 0) newStart = 0; // ensures region start doesn't go < 0s if (newEnd > duration) newEnd = duration; // ensures region start doesn't go > duration setInputInSeconds(startTimeInput, newStart); setInputInSeconds(endTimeInput, newEnd); let currRegIdx = getCurrentRegionIndex(); currSpeakerSet.tempSpeakerObjects[currRegIdx].start = newStart; currSpeakerSet.tempSpeakerObjects[currRegIdx].end = newEnd; currSpeakerSet.tempSpeakerObjects[currRegIdx].region.update({start: newStart, end: newEnd}); currentRegion.start = newStart; currentRegion.end = newEnd; editsMade = true; handleSameSpeakerOverlap(currRegIdx, currSpeakerSet); addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "change-time", getCurrentRegionIndex()); editLockedRegion(currSpeakerSet.tempSpeakerObjects[currRegIdx]); } else { console.log("no region selected"); setInputInSeconds(startTimeInput, 0); setInputInSeconds(endTimeInput, 0); } } /** * Calculates time in seconds of start or end time input group * @param {element} input Element of time input groups: hh:mm:ss * @returns {int} Time in seconds */ function getTimeInSecondsFromInput(input) { let hours = input.children[0].valueAsNumber; let mins = input.children[1].valueAsNumber; let secs = input.children[2].valueAsNumber; return (hours * 3600) + (mins * 60) + secs; } /** * Sets the start or end time element group inputs * @param {element} input Element of time input group to be updated * @param {int} seconds Duration in seconds to be converted into hh:mm:ss */ function setInputInSeconds(input, seconds) { // sets start or end input time when given seconds let date = new Date(null); date.setMilliseconds(seconds * 1000); input.children[0].value = date.getHours() % 12; input.children[1].value = date.getMinutes(); input.children[2].value = date.getSeconds() + "." + (Math.ceil(date.getMilliseconds() / 100) * 100); document.querySelectorAll('input[type=number]').forEach(e => { e.value = Math.round(e.valueAsNumber * 10) / 10; // to 1dp if (e.classList.contains("seconds") && !e.value.includes(".")) { e.value = e.value + ".0"; } else if (e.value.length === 1){ e.value = '0' + e.value; }// 0 padded on left }); } /** * Adds a new undo state to the global undo state list * @param {object} state Primary set at current state * @param {object} secState Secondary set at current state * @param {boolean} isSec Whether or not current change was made to primary (false) or secondary (true) set * @param {boolean} dualMode Whether or not audio editor was in dual mode when undo state was added * @param {string} type Type of change e.g "remove", "speaker-change" * @param {int} currRegIdx Index of currently selected region (for restoration) * @param {Array} currRegIdxs Index of currently selected regions, if applicable (for restoration) */ function addUndoState(state, secState, isSec, dualMode, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack let newState = cloneSpeakerObjectArray(state.tempSpeakerObjects); // clone method removes references let newSecState = cloneSpeakerObjectArray(secState.tempSpeakerObjects); // clone method removes references let changedTrack = (type == "dualModeChange" || type == "selectAllChange") ? "none" : selectedVersions[isSec ? 1 : 0] // sets changedTrack to version name of edited region set undoButton.classList.remove("disabled"); undoStates = undoStates.slice(0, undoLevel + 1); // trim to current level if undos have already been made undoStates.push({state: newState, secState: newSecState, isSec: isSec, changedTrack: changedTrack, dualMode: dualMode, currentRegionIndex: currRegIdx, currentRegionIndexes: currRegIdxs, type: type}); if ((type === "change-time" && prevUndoState === "change-time") || (type === "speaker-change" && prevUndoState === "speaker-change")) { // checks if similar change was made previously undoStates.splice(-2, 1); // remove second-to-last item in undoStates stack (merge last two changes into one to avoid multiple small edits) prevUndoState = type; } else undoLevel++; prevUndoState = type; redoButton.classList.add("disabled"); for (var item of undoStates) { // remove cyclic object references item.state = cloneSpeakerObjectArray(item.state); item.secState = cloneSpeakerObjectArray(item.secState); } localStorage.setItem('undoStates', JSON.stringify(undoStates)); // update localStorage items localStorage.setItem('undoLevel', undoLevel); } /** * Returns to the previous state in the undo state list */ function undo() { if (!undoButton.classList.contains("disabled") && editMode) { // ensure there exist states to undo to clearChapterSearch(); if (undoLevel - 1 < 0) console.log("ran out of undos"); else { removeCurrentRegion(); let adjustedUndoLevel = undoLevel-1; if (undoStates[undoLevel].type == "dualModeChange") { // toggle dual mode dualModeChanged(true); } else if (undoStates[undoLevel].type == "selectAllChange") { // toggle select all changeAllCheckbox.checked = !changeAllCheckbox.checked; selectAllCheckboxChanged(true); } else { primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].state.slice(0)); // slice & clone removes potential references between arrays if (dualMode && undoStates[adjustedUndoLevel].secState && undoStates[adjustedUndoLevel].secState.length > 0) { // if secondary undoState exists secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].secState.slice(0)); // slice & clone removes potential references between arrays } let selectedSpeakerSet; // handle currentRegion change if (undoStates[undoLevel] && undoStates[undoLevel].type && undoStates[undoLevel].type == "remove") { // if destination state type is remove selectedSpeakerSet = (undoStates[undoLevel].isSec) ? secondarySet : primarySet; if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret"); else caretClicked("primary-caret"); currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex]; // restore previous current state } else if (undoStates[undoLevel].currentRegionIndex) { if (!dualMode) selectedSpeakerSet = primarySet; else { selectedSpeakerSet = (undoStates[undoLevel-1].isSec) ? secondarySet : primarySet; if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret"); else caretClicked("primary-caret"); } currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex]; } // handle currentRegions restoration if (undoStates[undoLevel].currentRegionIndexes && undoStates[undoLevel].currentRegionIndexes.length > 1) { for (var idx of undoStates[undoLevel].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]); } } editsMade = true; undoLevel--; // decrement undoLevel reloadRegionsAndChapters(); localStorage.setItem('undoLevel', undoLevel); if (undoLevel - 1 < 0) undoButton.classList.add("disabled"); else undoButton.classList.remove("disabled"); } if (undoLevel < undoStates.length) redoButton.classList.remove("disabled"); } } /** * Moves forward one state in the undo state list */ function redo() { if (!redoButton.classList.contains("disabled") && editMode) { // ensure there exist states to redo to clearChapterSearch(); if (undoLevel + 1 >= undoStates.length) console.log("ran out of redos"); else { if (undoStates[undoLevel+1].type == "dualModeChange") { // toggle dual mode dualModeChanged(true); } else if (undoStates[undoLevel+1].type == "selectAllChange") { // toggle select all changeAllCheckbox.checked = !changeAllCheckbox.checked; selectAllCheckboxChanged(true); } else { primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].state.slice(0)); // set primary to new state secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].secState.slice(0)); // set secondary to new state let selectedSpeakerSet; // handle currentRegion change removeCurrentRegion(); if (undoLevel+2 < undoStates.length) { if (undoStates[undoLevel+2] && undoStates[undoLevel+2].type && undoStates[undoLevel+2].type == "remove") { selectedSpeakerSet = (undoStates[undoLevel+2].isSec) ? secondarySet : primarySet; if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret"); else caretClicked("primary-caret"); currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+2].currentRegionIndex]; } else { selectedSpeakerSet = (undoStates[undoLevel+1].isSec) ? secondarySet : primarySet; if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret"); else caretClicked("primary-caret"); currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+1].currentRegionIndex]; } if (undoStates[undoLevel+1].currentRegionIndexes && undoStates[undoLevel+1].currentRegionIndexes.length > 1) { for (var idx of undoStates[undoLevel+1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]); } } } editsMade = true; reloadRegionsAndChapters(); undoLevel++; // increment undoLevel localStorage.setItem('undoLevel', undoLevel); if (undoLevel + 1 > undoStates.length - 1) redoButton.classList.add("disabled"); else redoButton.classList.remove("disabled"); } if (undoLevel < undoStates.length) undoButton.classList.remove("disabled"); } } function resetUndoStates() { // clear undo history undoStates = [{state: cloneSpeakerObjectArray(primarySet.tempSpeakerObjects), secState: cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects)}]; undoLevel = 0; localStorage.removeItem('undoLevel'); localStorage.removeItem('undoStates'); undoButton.classList.add("disabled"); redoButton.classList.add("disabled"); } function waveformScrolled() { // waveform scroll handler if (currentRegion.speaker && getCurrentRegionIndex() != -1) { // updates region bound markers if selected region exists setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker); drawCurrentRegionBounds(); } if (document.getElementById('new-canvas')) { document.getElementById('new-canvas').style.left = "-" + wave.scrollLeft + 'px' } // update placeholder waveform scroll position } function drawCurrentRegionBounds() { // draws bounds of current region removeRegionBounds(); let currIndexes = getCurrentRegionsIndexes(); if (getCurrentRegionIndex() != -1) drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, wave.scrollLeft, "FireBrick"); for (let i = 0; i < currIndexes.length; i++) { drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, wave.scrollLeft, "FireBrick"); } } /** * Draws bounding 'n' above hovered or selected region * @param {object} region Region to have bound drawn for * @param {number} scrollPos Scroll position of div, used to offset draw position * @param {string} colour Colour to draw bound (black and FireBrick are used) */ function drawRegionBounds(region, scrollPos, colour) { // draws on canvas to show bounds of hovered/selected region var hoverSpeakerCanvas = document.createElement("canvas"); hoverSpeakerCanvas.id = "hover-speaker-canvas"; hoverSpeakerCanvas.classList.add("region-bounds"); hoverSpeakerCanvas.width = audioContainer.clientWidth; // max width of drawn bounds var ctx = hoverSpeakerCanvas.getContext("2d"); // ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency ctx.lineWidth = 1; if (colour == "FireBrick") ctx.lineWidth = 3; if (currentRegions && currentRegions.length < 1 && isCurrentRegion(region) && editMode) { colour = "FireBrick"; ctx.lineWidth = 3; } ctx.strokeStyle = colour; ctx.beginPath(); ctx.moveTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 28); ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 20); ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 20); ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 28); ctx.stroke(); audioContainer.prepend(hoverSpeakerCanvas); } function removeRegionBounds() { // remove all region bound markers let canvases = document.getElementsByClassName('region-bounds'); while (canvases[0]) canvases[0].parentNode.removeChild(canvases[0]); } function updateCurrSpeakerSet() { // updates 'currSpeakerSet' var if (primaryCaret.src.includes("fill")) currSpeakerSet = primarySet; else if (secondaryCaret.src.includes("fill")) currSpeakerSet = secondarySet; } function cloneSpeakerObjectArray(inputArray) { // clones speakerObjectArray without references (wavesurfer regions) let output = []; for (let i = 0; i < inputArray.length; i++) { output.push({ speaker: inputArray[i].speaker, start: inputArray[i].start, end: inputArray[i].end, locked: (inputArray[i].locked === "true" || inputArray[i].locked === true) }); } return output; } function flashChapters() { // flashes chapters a lighter colour momentarily to indicate an update/change chapters.style.backgroundColor = "rgb(66, 84, 88)"; setTimeout(() => { chapters.style.backgroundColor = "rgb(40, 54, 58)" }, 500); } /** Fullscreen onChange handler, increases waveform height & adjusts padding/margin */ function fullscreenChanged() { if (!audioContainer.classList.contains("fullscreen")) { audioContainer.classList.add("fullscreen"); wavesurfer.setHeight(175); // increase waveform height caretContainer.style.paddingLeft = "2rem"; caretContainer.style.height = wavesurfer.getHeight() + "px"; // set height to waveform height audioContainer.prepend(caretContainer); // attach to audioContainer (otherwise doesn't show due to AC being fullscreen) } else { audioContainer.classList.remove("fullscreen"); wavesurfer.setHeight(140); caretContainer.style.paddingLeft = "0"; caretContainer.style.height = wavesurfer.getHeight() + "px"; audioContainer.parentElement.prepend(caretContainer); // move back up in DOM hierarchy } setTimeout(() => { // ensures waveform shows zoomOutButton.click(); zoomInButton.click(); }, 250); } /** Enables / disables the fullscreen view of audio player / editor */ function toggleFullscreen() { if ((document.fullscreenElement && document.fullscreenElement !== null) || (document.webkitFullscreenElement && document.webkitFullscreenElement !== null) || (document.mozFullScreenElement && document.mozFullScreenElement !== null) || (document.msFullscreenElement && document.msFullscreenElement !== null)) { document.exitFullscreen(); } else { if (audioContainer.requestFullscreen) { audioContainer.requestFullscreen(); } else if (audioContainer.webkitRequestFullscreen) { /* Safari */ audioContainer.webkitRequestFullscreen(); } else if (audioContainer.msRequestFullscreen) { /* IE11 */ audioContainer.msRequestFullscreen(); } } } } /** * Formats seconds to hh:mm:ss * @param {number} duration * @returns {string} Time in hh:mm:ss format */ function formatAudioDuration(duration) { // console.log('duration: ' + duration); let [hrs, mins, secs, ms] = duration.replace(".", ":").split(":"); return hrs + ":" + mins + ":" + secs; }