source: main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js@ 37031

Last change on this file since 37031 was 37031, checked in by davidb, 17 months ago

added/improved diarization edit functionality in enriched audio player

File size: 74.0 KB
Line 
1/** JavaScript file of utility functions.
2 * At present contains functions for sanitising of URLs,
3 * since tomcat 8+, being more compliant with URL/URI standards, is more strict about URLs.
4 */
5
6/*
7 Given a string consisting of a single character, returns the %hex (%XX)
8 https://www.w3resource.com/javascript-exercises/javascript-string-exercise-27.php
9 https://stackoverflow.com/questions/40100096/what-is-equivalent-php-chr-and-ord-functions-in-javascript
10 https://www.w3resource.com/javascript-exercises/javascript-string-exercise-27.php
11*/
12function urlEncodeChar(single_char_string) {
13 /*let hex = Number(single_char_string.charCodeAt(0)).toString(16);
14 var str = "" + hex;
15 str = "%" + str.toUpperCase();
16 return str;
17 */
18
19 var hex = "%" + Number(single_char_string.charCodeAt(0)).toString(16).toUpperCase();
20 return hex;
21}
22
23/*
24 Tomcat 8 appears to be stricter in requiring unsafe and reserved chars
25 in URLs to be escaped with URL encoding
26 See section "Character Encoding Chart of
27 https://perishablepress.com/stop-using-unsafe-characters-in-urls/
28 Reserved chars:
29 ; / ? : @ = &
30 -----> %3B %2F %3F %3A %40 %3D %26
31 [Now also reserved, but no special meaning yet in URLs (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)
32 and not required to be enforced yet, so we're aren't at present dealing with these:
33 ! ' ( ) *
34 ]
35 Unsafe chars:
36 " < > # % { } | \ ^ ~ [ ] ` and SPACE/BLANK
37 ----> %22 %3C %3E %23 %25 %7B %7D %7C %5C %5E ~ %5B %5D %60 and %20
38 But the above conflicts with the reserved vs unreserved listings at
39 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
40 Possibly more info: https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid
41
42 And the bottom of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
43 lists additional characters that have been reserved since and which need encoding when in a URL component.
44
45 Javascript already provides functions encodeURI() and encodeURIComponent(), see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
46 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
47 https://perishablepress.com/stop-using-unsafe-characters-in-urls/
48 We want to handle all the characters listed as unsafe and reserved at https://perishablepress.com/stop-using-unsafe-characters-in-urls/
49 so we define and use our own conceptually equivalent methods for both existing JavaScript methods:
50 - makeSafeURL() for Javascript's encodeURI() to make sure all unsafe characters in URLs are escaped by being URL encoded
51 - and makeSafeURLComponent() for JavaScript's encodeURIComponent to additionally make sure all reserved characters in a URL portion are escaped by being URL encoded too
52
53 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.
54 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
55 URL query string's parameter values. As such makeSafeURLComponent() should escape both unsafe URL characters and characters that are reserved in URLs since reserved
56 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.
57*/
58
59/* URL encodes both
60 - UNSAFE characters to make URL safe, by calling makeSafeURL()
61 - and RESERVED characters (characters that have reserved meanings within a URL) to make URL valid, since the url component parameter could use reserved characters
62 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.
63
64 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()
65*/
66function makeURLComponentSafe(url_part, encode_percentages) {
67 // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript
68 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
69
70 var url_encoded = makeURLSafe(url_part, encode_percentages);
71 //return url_encoded.replace(/;/g, "%3B").replace(/\//g, "%2F").replace(/\?/g, "%3F").replace(/\:/g, "%3A").replace(/\@/g, "%40").replace(/=/g, "%3D").replace(/\&/g,"%26");
72 url_encoded = url_encoded.replace(/[\;\/\?\:\@\=\&]/g, function(s) {
73 return urlEncodeChar(s);
74 });
75 return url_encoded;
76}
77
78/*
79 URL encode UNSAFE characters to make URL passed in safe.
80 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.
81*/
82function makeURLSafe(url, encode_percentages) {
83 encode_percentages = encode_percentages || 0; // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript
84
85 var url_encoded = url;
86 if(encode_percentages) { url_encoded = url_encoded.replace(/\%/g,"%25"); } // encode % first
87 //url_encoded = url_encoded.replace(/ /g, "%20").replace(/\"/g,"%22").replace(/\</g,"%3C").replace(/\>/g,"%3E").replace(/\#/g,"%23").replace(/\{/g,"%7B").replace(/\}/g,"%7D");
88 //url_encoded = url_encoded.replace(/\|/g,"%7C").replace(/\\/g,"%5C").replace(/\^/g,"%5E").replace(/\[/g,"%5B").replace(/\]/g,"%5D").replace(/\`/g,"%60");
89 // Should we handle ~, but then what is its URL encoded value? Because https://meyerweb.com/eric/tools/dencoder/ URLencodes ~ to ~.
90 //return url_encoded;
91 url_encoded = url_encoded.replace(/[\ \"\<\>\#\{\}\|\\^\~\[\]\`]/g, function(s) {
92 return urlEncodeChar(s);
93 });
94 return url_encoded;
95}
96
97/***************
98* MENU SCRIPTS *
99***************/
100function moveScroller() {
101 var move = function() {
102 var editbar = $("#editBar");
103 var st = $(window).scrollTop();
104 var fa = $("#float-anchor").offset().top;
105 if(st > fa) {
106
107 editbar.css({
108 position: "fixed",
109 top: "0px",
110 width: editbar.data("width"),
111 //width: "30%"
112 });
113 } else {
114 editbar.data("width", editbar.css("width"));
115 editbar.css({
116 position: "relative",
117 top: "",
118 width: ""
119 });
120 }
121 };
122 $(window).scroll(move);
123 move();
124}
125
126
127function floatMenu(enabled)
128{
129 var menu = $(".tableOfContentsContainer");
130 if(enabled)
131 {
132 menu.data("position", menu.css("position"));
133 menu.data("width", menu.css("width"));
134 menu.data("right", menu.css("right"));
135 menu.data("top", menu.css("top"));
136 menu.data("max-height", menu.css("max-height"));
137 menu.data("overflow", menu.css("overflow"));
138 menu.data("z-index", menu.css("z-index"));
139
140 menu.css("position", "fixed");
141 menu.css("width", "300px");
142 menu.css("right", "0px");
143 menu.css("top", "100px");
144 menu.css("max-height", "600px");
145 menu.css("overflow", "auto");
146 menu.css("z-index", "200");
147
148 $("#unfloatTOCButton").show();
149 }
150 else
151 {
152 menu.css("position", menu.data("position"));
153 menu.css("width", menu.data("width"));
154 menu.css("right", menu.data("right"));
155 menu.css("top", menu.data("top"));
156 menu.css("max-height", menu.data("max-height"));
157 menu.css("overflow", menu.data("overflow"));
158 menu.css("z-index", menu.data("z-index"));
159
160 $("#unfloatTOCButton").hide();
161 $("#floatTOCToggle").prop("checked", false);
162 }
163
164 var url = gs.xsltParams.library_name + "?a=d&ftoc=" + (enabled ? "1" : "0") + "&c=" + gs.cgiParams.c;
165
166 $.ajax(url);
167}
168
169// TK Label Scripts
170
171var tkMetadataSetStatus = "needs-to-be-loaded";
172var tkMetadataElements = null;
173
174
175function addTKLabelToImage(labelName, definition, name, comment) {
176 // lists of tkLabels and their corresponding codes, in order
177 let tkLabels = ["Attribution","Clan","Family","MultipleCommunities","CommunityVoice","Creative","Verified","NonVerified","Seasonal","WomenGeneral","MenGeneral",
178 "MenRestricted","WomenRestricted","CulturallySensitive","SecretSacred","OpenToCommercialization","NonCommercial","CommunityUseOnly","Outreach","OpenToCollaboration"];
179 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"];
180 for (let i = 0; i < tkLabels.length; i++) {
181 if (labelName == tkLabels[i]) {
182 let labeldiv = document.querySelectorAll(".tklabels img");
183 for (image of labeldiv) {
184 let labelCode = image.src.substr(image.src.lastIndexOf("/") + 1).replace(".png", ""); // get tk label code from image file name
185 if (labelCode == tkCodes[i]) {
186 image.title = "TK " + name + ": " + definition + " Click for more details."; // set tooltip
187 if (image.parentElement.parentElement.parentElement.classList[0] != "tocSectionTitle") { // disable onclick event in favourites section
188 image.addEventListener("click", function(e) {
189 let currPopup = document.getElementsByClassName("tkPopup")[0];
190 if (currPopup == undefined || (currPopup != undefined && currPopup.id != labelCode)) {
191 let popup = document.createElement("div");
192 popup.classList.add("tkPopup");
193 popup.id = labelCode;
194 let popupText = document.createElement("span");
195 let heading = "<h1>Traditional Knowledge Label:<br><h2>" + name + "</h2></h1>";
196 let moreInformation = "<br> For more information about TK Labels, ";
197 let link = document.createElement("a");
198 link.innerHTML = "click here.";
199 link.href = "https://localcontexts.org/labels/traditional-knowledge-labels/";
200 link.target = "_blank";
201 popupText.innerHTML = heading + comment + moreInformation;
202 popupText.appendChild(link);
203 let closeButton = document.createElement("span");
204 closeButton.innerHTML = "&#215;";
205 closeButton.id = "tkCloseButton";
206 closeButton.title = "Click to close window."
207 closeButton.addEventListener("click", function(e) {
208 closeButton.parentElement.remove();
209 });
210 popup.appendChild(closeButton);
211 popup.appendChild(popupText);
212 e.target.parentElement.appendChild(popup);
213 }
214 if (currPopup) currPopup.remove(); // remove existing popup div
215 });
216 }
217 }
218 }
219 }
220 }
221}
222
223function addTKLabelsToImages(lang) {
224 if (tkMetadataElements == null) {
225 console.error("ajax call not yet loaded tk label metadata set");
226 } else {
227 for (label of tkMetadataElements) { // for each tklabel element in tk.mds
228 let tkLabelName = label.attributes.name.value; // Element name=""
229 let attributes = label.querySelectorAll("[code=" + lang + "] Attribute"); // gets attributes for selected language
230 let tkName = attributes[0].textContent; // name="label"
231 let tkDefinition = attributes[1].textContent; // name="definition"
232 let tkComment = attributes[2].textContent; // name="comment"
233 addTKLabelToImage(tkLabelName, tkDefinition, tkName, tkComment);
234 }
235 }
236}
237
238function loadTKMetadataSetOld(lang) {
239 tkMetadataSetStatus = "loading";
240 $.ajax({
241 url: gs.variables["tkMetadataURL"],
242 success: function(xml) {
243 tkMetadataSetStatus = "loaded";
244 let parser = new DOMParser();
245 let tkmds = parser.parseFromString(xml, "text/xml");
246 tkMetadataElements = tkmds.querySelectorAll("Element");
247 if (document.readyState === "complete") {
248 addTKLabelsToImages(lang);
249 } else {
250 window.onload = function() {
251 addTKLabelsToImages(lang);
252 }
253 }
254 },
255 error: function() {
256 tkMetadataSetStatus = "no-metadata-set-for-this-collection";
257 console.log("No TK Label Metadata-set found for this collection");
258 }
259 });
260};
261function loadTKMetadataSet(lang, type) {
262 if (gs.variables["tkMetadataURL_"+type] == undefined) {
263 console.error("tkMetadataURL_"+type+" variable is not defined, can't load TK Metadata Set");
264 tkMetadataSetStatus = "no-metadata-set-for-this-"+type;
265 return;
266 }
267 tkMetadataSetStatus = "loading";
268 $.ajax({
269 url: gs.variables["tkMetadataURL_"+type],
270 async: false,
271 success: function(xml) {
272 tkMetadataSetStatus = "loaded";
273 let parser = new DOMParser();
274 let tkmds = parser.parseFromString(xml, "text/xml");
275 tkMetadataElements = tkmds.querySelectorAll("Element");
276 if (document.readyState === "complete") {
277 addTKLabelsToImages(lang);
278 } else {
279 window.onload = function() {
280 addTKLabelsToImages(lang);
281 }
282 }
283 },
284 error: function() {
285 tkMetadataSetStatus = "no-metadata-set-for-this-"+type;
286 console.log("No TK Label Metadata-set found for this "+type);
287 }
288 });
289};
290
291// Audio Scripts for Enriched Playback
292
293function loadAudio(audio, sectionData) {
294 let editMode = false;
295 let currentRegion = {speaker: '', start: '', end: ''};
296 let currentRegions = [];
297
298 // let speakerObjects = [];
299 // let tempSpeakerObjects = [];
300 // let uniqueSpeakers;
301 const inputFile = sectionData;
302 let itemType;
303
304 let dualMode = false;
305 let secondaryLoaded = false;
306
307 let editsMade = false;
308 let undoLevel = 0;
309 let undoStates = [];
310 let prevUndoState = "";
311 let tempZoomSave = 0;
312 let isZooming;
313
314 let accentColour = "#66d640";
315 // let accentColour = "#F8C537";
316 let regionTransparency = "50";
317
318 let waveformContainer = document.getElementById("waveform");
319
320 let wavesurfer = WaveSurfer.create({ // wavesurfer options
321 container: waveformContainer,
322 backend: "MediaElement",
323 backgroundColor: "rgb(40, 54, 58)",
324 // backgroundColor: "rgb(24, 36, 39)",
325 waveColor: "white",
326 progressColor: accentColour,
327 // progressColor: "grey",
328 // barWidth: 2,
329 barHeight: 1.2,
330 // barGap: 2,
331 // barRadius: 1,
332 cursorColor: 'black',
333 cursorWidth: 2,
334 normalize: true, // normalizes by maximum peak
335 plugins: [
336 WaveSurfer.regions.create({
337 // formatTimeCallback: function(a, b) {
338 // return "TEST";
339 // }
340 }),
341 WaveSurfer.timeline.create({
342 container: "#wave-timeline",
343 secondaryColor: "white",
344 secondaryFontColor: "white",
345 notchPercentHeight: "0",
346 fontSize: "12"
347 }),
348 WaveSurfer.cursor.create({
349 showTime: true,
350 opacity: 1,
351 customShowTimeStyle: {
352 'background-color': '#000',
353 color: '#fff',
354 padding: '0.25rem',
355 'font-size': '12px'
356 },
357 formatTimeCallback: (num) => { return minutize(num); }
358 }),
359 ],
360 });
361
362 wavesurfer.load(audio);
363
364 // wavesurfer events
365
366 wavesurfer.on('region-click', handleRegionClick);
367
368 function handleRegionClick(region, e) {
369 e.stopPropagation();
370 if (!editMode) { // play region audio on click
371 wavesurfer.play(region.start); // plays from start of region
372 } else { // select / deselect current region
373 if (region.element.classList.contains("region-top")) caretClicked("primary-caret");
374 else if (region.element.classList.contains("region-bottom")) caretClicked("secondary-caret");
375 prevUndoState = "";
376
377 if (!e.ctrlKey && !e.shiftKey) {
378 currentRegions = [];
379 if (getCurrentRegionIndex() != -1 && isCurrentRegion(region)) {
380 removeCurrentRegion(); // deselect current region on click
381 } else {
382 currentRegion = region;
383 currentRegion.speaker = currentRegion.attributes.label;
384 region.play(); // start and stop to move play cursor to beginning of region
385 wavesurfer.playPause();
386 }
387 } else if (e.ctrlKey) { // control was held during click
388 if (currentRegions.length == 0 && isCurrentRegion(region)) {
389 removeCurrentRegion();
390 } else if (getCurrentRegionIndex() != -1 && isInCurrentRegions(region)) {
391 const removeIndex = getIndexInCurrentRegions(region);
392 if (removeIndex != -1) currentRegions.splice(removeIndex, 1);
393 if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed
394 currentRegion = currentRegions[0];
395 // currentRegions = [];
396 }
397 } else {
398 if (currentRegions.length < 1) currentRegions.push(currentRegion);
399 if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist
400 currentRegion = region;
401 currentRegion.speaker = currentRegion.attributes.label;
402 region.play();
403 wavesurfer.playPause();
404 }
405 if (currentRegions.length == 1) currentRegions = []; // clear selected regions if there is only one
406 } else if (e.shiftKey) { // shift was held during click
407 if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) {
408 if (currentRegions && currentRegions.length > 0) {
409 if (Math.max(...getCurrentRegionsIndexes()) < getIndexOfRegion(region)) { // shifting forwards / down
410 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(Math.min(...getCurrentRegionsIndexes()), getIndexOfRegion(region)+1);
411 } else { // shifting backwards / up
412 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), Math.max(...getCurrentRegionsIndexes())+1);
413 }
414 } else {
415 if (getCurrentRegionIndex() < getIndexOfRegion(region)) { // shifting forwards / down
416 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getCurrentRegionIndex(), getIndexOfRegion(region)+1);
417 } else { // shifting backwards / up
418 currentRegions = currSpeakerSet.tempSpeakerObjects.slice(getIndexOfRegion(region), getCurrentRegionIndex()+1);
419 }
420 }
421 }
422 }
423 if (speakerCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) }
424 reloadRegionsAndChapters();
425 }
426 }
427
428 function getIndexInCurrentRegions(region) {
429 for (const reg of currentRegions) {
430 const regSpeaker = reg.attributes ? reg.attributes.label : reg.speaker;
431 if (reg.start == region.start && reg.end == region.end && regSpeaker == region.attributes.label) {
432 return currentRegions.indexOf(reg);
433 }
434 }
435 return -1;
436 }
437
438 function getIndexOfRegion(region) {
439 for (const reg of currSpeakerSet.tempSpeakerObjects) {
440 if (reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label) {
441 return currSpeakerSet.tempSpeakerObjects.indexOf(reg);
442 }
443 }
444 return -1;
445 }
446
447 wavesurfer.on('region-mouseenter', function(region) { // region hover effects
448 handleRegionColours(region, true);
449 hoverSpeaker.innerHTML = region.attributes.label;
450 hoverSpeaker.style.marginLeft = parseInt(region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
451 if (!isInCurrentRegions(region)) {
452 removeRegionBounds();
453 drawRegionBounds(region, waveform.scrollLeft, "black");
454 }
455 if (isCurrentRegion(region)) drawRegionBounds(region, waveform.scrollLeft);
456 });
457 wavesurfer.on('region-mouseleave', function(region) {
458 if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false);
459 removeRegionBounds();
460 if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
461 hoverSpeaker.innerHTML = currentRegion.speaker;
462 hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
463 let currIndexes = getCurrentRegionsIndexes();
464 for (let i = 0; i < currIndexes.length; i++) {
465 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
466 }
467 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
468 }
469 if (!currentRegion.speaker) hoverSpeaker.innerHTML = "";
470 });
471 wavesurfer.on('region-in', function(region) {
472 handleRegionColours(region, true);
473 if (itemType == "chapter") {
474 document.getElementById("chapter" + region.id.replace("region", "")).scrollIntoView({
475 behavior: "smooth",
476 block: "nearest"
477 });
478 }
479 });
480 wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) });
481 wavesurfer.on('region-update-end', handleRegionEdit); // end of click-drag event
482 // wavesurfer.on('region-update-end', (region, e) => { handleRegionEdit(region, e)} ); // end of click-drag event
483
484 let loader = document.createElement("span"); // loading audio element
485 loader.innerHTML = "Loading audio";
486 loader.id = "waveform-loader";
487 document.querySelector("#waveform wave").prepend(loader);
488
489 wavesurfer.on('waveform-ready', function() { // retrieve regions once waveforms have loaded
490 if (inputFile.endsWith("csv")) { // diarization if csv
491 itemType = "chapter";
492 loadCSVFile(inputFile, ["speaker", "start", "end"], primarySet);
493 } else if (inputFile.endsWith("json")) { // transcription if json
494 itemType = "word";
495 loadJSONFile(inputFile);
496 } else {
497 console.log("Filetype of " + inputFile + " not supported.")
498 }
499 loader.remove(); // remove load text
500 chapters.style.cursor = "pointer";
501 waveform.className = "audio-scroll";
502 });
503
504 function downloadURI(loc, name) {
505 let link = document.createElement("a");
506 link.download = name;
507 link.href = loc;
508 link.click();
509 }
510
511 // toolbar elements & event handlers
512 const audioContainer = document.getElementById("audioContainer");
513 const dualModeCheckbox = document.getElementById("dual-mode-checkbox");
514 const waveform = document.getElementsByTagName("wave")[0];
515 const primaryCaret = document.getElementById("primary-caret");
516 const secondaryCaret = document.getElementById("secondary-caret");
517 const chapters = document.getElementById("chapters");
518 const editPanel = document.getElementById("edit-panel");
519 const chapterButton = document.getElementById("chapterButton");
520 const zoomOutButton = document.getElementById("zoomOutButton");
521 const zoomSlider = document.getElementById("zoom-slider");
522 const zoomInButton = document.getElementById("zoomInButton");
523 const backButton = document.getElementById("backButton");
524 const playPauseButton = document.getElementById("playPauseButton");
525 const forwardButton = document.getElementById("forwardButton");
526 const editButton = document.getElementById("editButton");
527 const downloadButton = document.getElementById("downloadButton");
528 const muteButton = document.getElementById("muteButton");
529 const volumeSlider = document.getElementById("volume-slider");
530 const fullscreenButton = document.getElementById("fullscreenButton");
531 const speakerCheckbox = document.getElementById("change-all-checkbox");
532 const changeAllLabel = document.getElementById("change-all-label");
533 const speakerInput = document.getElementById("speaker-input");
534 const startTimeInput = document.getElementById("start-time-input");
535 const endTimeInput = document.getElementById("end-time-input");
536 const removeButton = document.getElementById("remove-button");
537 const createButton = document.getElementById("create-button");
538 const discardButton = document.getElementById("discard-button");
539 const undoButton = document.getElementById("undo-button");
540 const redoButton = document.getElementById("redo-button");
541 const saveButton = document.getElementById("save-button");
542 const hoverSpeaker = document.getElementById("hover-speaker");
543
544 audioContainer.addEventListener('fullscreenchange', (e) => { fullscreenChanged() });
545 dualModeCheckbox.addEventListener("change", dualModeChanged);
546 waveform.addEventListener('scroll', (e) => { waveformScrolled() })
547 primaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
548 secondaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
549 chapters.style.height = "0px";
550 editPanel.style.height = "0px";
551 chapterButton.addEventListener("click", () => { toggleChapters() });
552 zoomOutButton.addEventListener("click", () => { zoomSlider.stepDown(); zoomSlider.dispatchEvent(new Event("input")) });
553 zoomInButton.addEventListener("click", () => { zoomSlider.stepUp(); zoomSlider.dispatchEvent(new Event("input")) });
554 backButton.addEventListener("click", () => { wavesurfer.skipBackward(); });
555 playPauseButton.addEventListener("click", () => { wavesurfer.playPause() });
556 forwardButton.addEventListener("click", () => { wavesurfer.skipForward(); });
557 editButton.addEventListener("click", toggleEditMode);
558 downloadButton.addEventListener("click", () => { downloadURI(audio, audio.split(".dir/")[1]) });
559 muteButton.addEventListener("click", () => { wavesurfer.toggleMute() });
560 volumeSlider.style["accent-color"] = accentColour;
561 fullscreenButton.addEventListener("click", toggleFullscreen);
562 zoomSlider.style["accent-color"] = accentColour;
563 speakerCheckbox.addEventListener("change", speakerCheckboxChanged);
564 speakerInput.addEventListener("input", speakerChange);
565 speakerInput.addEventListener("blur", speakerInputUnfocused);
566 createButton.addEventListener("click", createNewRegion);
567 removeButton.addEventListener("click", removeRegion);
568 discardButton.addEventListener("click", discardRegionChanges);
569 undoButton.addEventListener("click", undo);
570 redoButton.addEventListener("click", redo);
571 saveButton.addEventListener("click", saveRegionChanges);
572 document.querySelectorAll('input[type=number]').forEach(e => {
573 e.onchange = (e) => { changeStartEndTime(e) }; // updates speaker objects when number input(s) are changed
574 e.onblur = () => { prevUndoState = "" };
575 });
576 audioContainer.addEventListener("keyup", keyPressed);
577
578 function keyPressed(e) {
579 if (e.target.tagName !== "INPUT") {
580 if (e.code === "Backspace" || e.code === "Delete") removeRegion();
581 else if (e.code === "Space") wavesurfer.playPause();
582 else if (e.code === "ArrowLeft") wavesurfer.skipBackward();
583 else if (e.code === "ArrowRight") wavesurfer.skipForward();
584 }
585 }
586
587 function dualModeChanged(e) { // on dualmode checkbox value change
588 dualMode = e.target.checked;
589 currSpeakerSet = primarySet;
590 reloadRegionsAndChapters();
591 if (dualMode) {
592 if (!secondaryLoaded) {
593 loadCSVFile(inputFile.replace(".csv", "-2.csv"), ["speaker", "start", "end"], secondarySet);
594 secondaryLoaded = true; // ensure secondarySet doesn't get re-read > once
595 }
596 document.getElementById("caret-container").style.display = "flex";
597 } else {
598 document.getElementById("caret-container").style.display = "none";
599 }
600 currSpeakerSet = primarySet;
601 }
602
603 // path to toolbar images
604 let interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/";
605
606 function caretClicked(id) {
607 wavesurfer.clearRegions();
608 // flashChapters();
609 if (id === "primary-caret") {
610 currSpeakerSet = primarySet;
611 swapCarets(true);
612 } else if (id === "secondary-caret") {
613 currSpeakerSet = secondarySet;
614 swapCarets(false);
615 }
616 $(".region-top").remove();
617 $(".region-bottom").remove();
618 populateChapters(primarySet);
619 populateChapters(secondarySet);
620 }
621
622 function swapCarets(toPrimary) {
623 const currCaretIsPrimary = primaryCaret.src.includes("fill") ? true : false;
624 if ((toPrimary && !currCaretIsPrimary) || (!toPrimary && currCaretIsPrimary)) removeCurrentRegion(); // ensure currentRegion is only removed if changing speakerSet
625 if (toPrimary) {
626 primaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
627 secondaryCaret.src = interface_bootstrap_images + "caret-right.svg";
628 } else {
629 primaryCaret.src = interface_bootstrap_images + "caret-right.svg";
630 secondaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
631 }
632 }
633
634 wavesurfer.on("play", () => { playPauseButton.src = interface_bootstrap_images + "pause.svg"; });
635 wavesurfer.on("pause", () => { playPauseButton.src = interface_bootstrap_images + "play.svg"; });
636 wavesurfer.on("mute", function(mute) {
637 if (mute) {
638 muteButton.src = interface_bootstrap_images + "mute.svg";
639 muteButton.style.opacity = 0.6;
640 volumeSlider.value = 0;
641 }
642 else {
643 muteButton.src = interface_bootstrap_images + "unmute.svg";
644 muteButton.style.opacity = 1;
645 volumeSlider.value = 1;
646 }
647 });
648
649 volumeSlider.addEventListener("input", function() {
650 wavesurfer.setVolume(this.value);
651 if (this.value == 0) {
652 muteButton.src = interface_bootstrap_images + "mute.svg";
653 muteButton.style.opacity = 0.6;
654 } else {
655 muteButton.src = interface_bootstrap_images + "unmute.svg";
656 muteButton.style.opacity = 1;
657 }
658 });
659
660 zoomSlider.addEventListener("input", function() { // slider changes waveform zoom
661 wavesurfer.zoom(Number(this.value) / 4);
662 if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
663 hoverSpeaker.innerHTML = currentRegion.speaker;
664 hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
665 removeRegionBounds();
666 let currIndexes = getCurrentRegionsIndexes();
667 for (let i = 0; i < currIndexes.length; i++) {
668 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
669 }
670 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
671 }
672 let handles = document.getElementsByClassName("wavesurfer-handle");
673 if (this.value < 20) {
674 for (const handle of handles) {
675 handle.style.setProperty("width", "1px", "important");
676 }
677 } else {
678 for (const handle of handles) {
679 handle.style.setProperty("width", "3px", "important");
680 }
681 }
682 });
683 wavesurfer.zoom(zoomSlider.value / 4); // set default zoom point
684
685 let toggleChapters = function() { // show & hide chapter section
686 if (chapters.style.height == "0px") {
687 chapters.style.height = "30vh";
688 } else {
689 chapters.style.height = "0px";
690 }
691 }
692
693 function SpeakerSet(isSecondary, uniqueSpeakers, speakerObjects, tempSpeakerObjects) {
694 this.isSecondary = isSecondary;
695 this.uniqueSpeakers = uniqueSpeakers;
696 this.speakerObjects = speakerObjects;
697 this.tempSpeakerObjects = tempSpeakerObjects;
698 }
699 let primarySet = new SpeakerSet(false, [], [], []);
700 let secondarySet = new SpeakerSet(true, [], [], []);
701 let currSpeakerSet = primarySet;
702
703 function loadCSVFile(filename, manualHeader, speakerSet) { // based on: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript
704 // if (speakerSet) currSpeakerSet = speakerSet; // if parameter is given, set
705 $.ajax({
706 type: "GET",
707 url: filename,
708 dataType: "text",
709 }).then(function(data) {
710 let dataLines = data.split(/\r\n|\n/);
711 let headers;
712 let startIndex;
713 speakerSet.uniqueSpeakers = []; // used for obtaining unique colours
714 speakerSet.speakerObjects = []; // list of speaker items
715
716 if (manualHeader) { // headers for columns can be provided if not existent in csv
717 headers = manualHeader;
718 startIndex = 0;
719 } else {
720 headers = dataLines[0].split(',');
721 startIndex = 1;
722 }
723
724 for (let i = startIndex; i < dataLines.length; i++) {
725 let data = dataLines[i].split(',');
726 if (data.length == headers.length) {
727 let item = {};
728 for (let j = 0; j < headers.length; j++) {
729 item[headers[j]] = data[j];
730 if (j == 0 && !speakerSet.uniqueSpeakers.includes(data[j])) {
731 speakerSet.uniqueSpeakers.push(data[j]);
732 }
733 }
734 speakerSet.speakerObjects.push(item);
735 }
736 }
737 speakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(speakerSet.speakerObjects);
738 populateChapters(speakerSet);
739 resetUndoStates(); // undo stack init
740 });
741 }
742
743 function populateChapters(data) { // populates chapter section and adds regions to waveform
744 // colorbrewer is a web tool for guidance in choosing map colour schemes based on a letiety of settings.
745 // this colour scheme is designed for qualitative data
746
747 if (data.uniqueSpeakers.length > 8) colourbrewerset = colorbrewer.Set2[8];
748 else if (data.uniqueSpeakers.length < 3) colourbrewerset = colorbrewer.Set2[3];
749 else colourbrewerset = colorbrewer.Set2[data.uniqueSpeakers.length];
750
751 let dataIsSelected = false;
752
753 if ((!data.isSecondary && primaryCaret.src.includes("fill")) || (data.isSecondary && secondaryCaret.src.includes("fill"))) dataIsSelected = true;
754 if (dataIsSelected || !dualMode) chapters.innerHTML = ""; // clear chapter div for re-population
755 data.tempSpeakerObjects = sortSpeakerObjectsByStart(data.tempSpeakerObjects); // sort speakerObjects by start time
756
757 for (let i = 0; i < data.tempSpeakerObjects.length; i++) {
758 let chapter = document.createElement("div");
759 chapter.classList.add("chapter");
760 chapter.id = "chapter" + i;
761 let speakerName = data.tempSpeakerObjects[i].speaker;
762 let speakerTime = document.createElement("span");
763 speakerTime.classList.add("speakerTime");
764 speakerTime.innerHTML = minutize(data.tempSpeakerObjects[i].start) + " - " + minutize(data.tempSpeakerObjects[i].end) + "s";
765 chapter.innerHTML = speakerName;
766 chapter.appendChild(speakerTime);
767 chapter.addEventListener("click", chapterClicked);
768 chapter.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
769 chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
770
771 let selected = false;
772 let dummyRegion = { start: data.tempSpeakerObjects[i].start, end: data.tempSpeakerObjects[i].end };
773
774 if ((dataIsSelected || !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) {
775 chapter.classList.add("selected-chapter");
776 selected = true;
777 }
778
779 if (dataIsSelected || !dualMode) chapters.appendChild(chapter);
780
781 let associatedReg = wavesurfer.addRegion({ // create associated wavesurfer region
782 id: "region" + i,
783 start: data.tempSpeakerObjects[i].start,
784 end: data.tempSpeakerObjects[i].end,
785 drag: editMode,
786 resize: editMode,
787 attributes: {
788 label: speakerName,
789 },
790 color: colourbrewerset[data.uniqueSpeakers.indexOf(data.tempSpeakerObjects[i].speaker)%8] + regionTransparency,
791 ...(selected) && {color: "rgba(255,50,50,0.5)"},
792 });
793 data.tempSpeakerObjects[i].region = associatedReg;
794 }
795
796 let regions = document.getElementsByTagName("region");
797 if (dualMode) {
798 if (document.getElementsByClassName("region-top").length === 0) for (const reg of regions) reg.classList.add("region-top");
799 else for (const rego of regions) if (!rego.classList.contains("region-top")) rego.classList.add("region-bottom");
800 }
801 if (editMode) for (const reg of regions) reg.style.setProperty("z-index", "3", "important");
802 else for (const reg of regions) reg.style.setProperty("z-index", "1", "important");
803 }
804
805 function loadJSONFile(filename) {
806 $.ajax({
807 type: "GET",
808 url: filename,
809 dataType: "text",
810 }).then(function(data){ populateWords(JSON.parse(data)) });
811 }
812
813 function populateWords(data) { // populates word section and adds regions to waveform
814 let transcription = data.transcription;
815 let words = data.words;
816 let wordContainer = document.createElement("div");
817 wordContainer.id = "word-container";
818 for (let i = 0; i < words.length; i++) {
819 let word = document.createElement("span");
820 word.id = "word" + i;
821 word.classList.add("word");
822 word.innerHTML = transcription.split(" ")[i];
823 word.addEventListener("click", e => { wordClicked(data, e.target.id) });
824 word.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
825 word.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
826 wordContainer.appendChild(word);
827 wavesurfer.addRegion({
828 id: "region" + i,
829 start: words[i].startTime,
830 end: words[i].endTime,
831 drag: false,
832 resize: false,
833 color: "rgba(255, 255, 255, 0.1)",
834 });
835 }
836 chapters.appendChild(wordContainer);
837 }
838
839 let chapterClicked = function(e) { // plays audio from start of chapter
840 let index = e.target.id.replace("chapter", "");
841 let clickedRegion = currSpeakerSet.tempSpeakerObjects[index].region;
842 handleRegionClick(clickedRegion, e);
843 }
844
845 function wordClicked(data, id) { // plays audio from start of word
846 let index = id.replace("word", "");
847 let start = data.words[index].startTime;
848 wavesurfer.play(start);
849 }
850
851 function chapterEnter(idx) {
852 let reg = currSpeakerSet.tempSpeakerObjects[idx].region;
853 regionEnter(reg);
854 hoverSpeaker.innerHTML = reg.attributes.label;
855 hoverSpeaker.style.marginLeft = parseInt(reg.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
856 if (!isInCurrentRegions(reg)) {
857 removeRegionBounds();
858 drawRegionBounds(reg, waveform.scrollLeft, "black");
859 }
860 }
861
862 function chapterLeave(idx) {
863 regionLeave(currSpeakerSet.tempSpeakerObjects[idx].region);
864 removeRegionBounds();
865 hoverSpeaker.innerHTML = "";
866 if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
867 hoverSpeaker.innerHTML = currentRegion.speaker;
868 hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
869 let currIndexes = getCurrentRegionsIndexes();
870 for (let i = 0; i < currIndexes.length; i++) {
871 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
872 }
873 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
874 }
875 }
876
877 function handleRegionColours(region, highlight) { // handles region, chapter & word colours
878 if (!dualMode || (region.element.classList.contains("region-top") && primaryCaret.src.includes("fill")) || region.element.classList.contains("region-bottom") && secondaryCaret.src.includes("fill")) {
879 let colour;
880 if (highlight) {
881 colour = "rgb(101, 116, 116)";
882 regionEnter(region);
883 } else {
884 colour = "";
885 regionLeave(region);
886 }
887 if (isCurrentRegion(region) || isInCurrentRegions(region)) {
888 colour = "rgba(255, 50, 50, 0.5)";
889 }
890 let regionIndex = region.id.replace("region","");
891 let corrItem = document.getElementById(itemType + regionIndex);
892 corrItem.style.backgroundColor = colour; // updates chapter background (not region)
893 }
894 }
895
896 function regionEnter(region) {
897 if (isCurrentRegion(region) || isInCurrentRegions(region)) {
898 if (region.element.classList.contains("region-top") && !currSpeakerSet.isSecondary) region.update({ color: "rgba(255, 50, 50, 0.5)" });
899 } else {
900 region.update({ color: "rgba(255, 255, 255, 0.35)" });
901 }
902 }
903
904 function regionLeave(region) {
905 if (itemType == "chapter") {
906 if (isCurrentRegion(region) || isInCurrentRegions(region)) {
907 region.update({ color: "rgba(255, 50, 50, 0.5)" });
908 } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) {
909 let index = region.id.replace("region", "");
910 region.update({ color: colourbrewerset[currSpeakerSet.uniqueSpeakers.indexOf(currSpeakerSet.tempSpeakerObjects[index].speaker)%8] + regionTransparency });
911 }
912 } else {
913 region.update({ color: "rgba(255, 255, 255, 0.1)" });
914 }
915 }
916
917 function minutize(num) { // converts seconds to m:ss for chapters & waveform hover
918 // return (num - (num %= 60)) / 60 + (9 < num ? ':' : ':0') + ~~num; // https://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds
919
920 let date = new Date(null);
921 date.setSeconds(num);
922 return date.toTimeString().split(" ")[0].substring(3);
923 }
924
925 function getLetter(val) {
926 // return val.replace("SPEAKER_","");
927 let speakerNum = parseInt(val.replace("SPEAKER_",""));
928 return String.fromCharCode(65 + speakerNum); // 'A' == UTF-16 65
929 }
930
931
932
933 // edit functionality
934
935 function toggleEditMode() { // toggles edit panel and redraws regions with resize handles
936 toggleEditPanel();
937 updateRegionEditPanel();
938 }
939
940 function toggleEditPanel() { // show & hide edit panel
941 currentRegion.speaker = '';
942 currentRegion.start = '';
943 currentRegion.end = '';
944 currentRegions = [];
945 removeRegionBounds();
946 hoverSpeaker.innerHTML = "";
947 if (editPanel.style.height == "0px") {
948 if (chapters.style.height == "0px") chapters.style.height = "30vh"; // expands chapter panel
949 editPanel.style.height = "30vh";
950 editPanel.style.padding = "1rem";
951 setRegionEditMode(true);
952 } else {
953 editPanel.style.height = "0px";
954 editPanel.style.padding = "0px";
955 setRegionEditMode(false);
956 }
957 }
958
959 function setRegionEditMode(state) {
960 editMode = state;
961 chapters.innerHTML = '';
962 wavesurfer.clearRegions();
963 populateChapters(currSpeakerSet);
964 }
965
966 function handleRegionEdit(region, e) {
967 if (e.target.localName === "region" || e.target.localName === "handle") {
968 if (region.element.classList.contains("region-top")) { currSpeakerSet = primarySet; swapCarets(true) }
969 else if (region.element.classList.contains("region-bottom")) { currSpeakerSet = secondarySet; swapCarets(false) }
970 editsMade = true;
971 currentRegion = region;
972 region.play();
973 wavesurfer.pause();
974 let regionIndex = getCurrentRegionIndex();
975 currentRegion.speaker = currSpeakerSet.tempSpeakerObjects[regionIndex].speaker;
976 currSpeakerSet.tempSpeakerObjects[regionIndex].region = region;
977 currSpeakerSet.tempSpeakerObjects[regionIndex].start = region.start;
978 currSpeakerSet.tempSpeakerObjects[regionIndex].end = region.end;
979 reloadRegionsAndChapters();
980 handleSameSpeakerOverlap(getCurrentRegionIndex()); // recalculate index in case start pos has changed
981 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "dragdrop", getCurrentRegionIndex());
982 editPanel.click(); // fixes buttons needing to be clicked twice (unknown cause!)
983 } else console.log("resizing too fast, selected region not updated.");
984 }
985
986 function handleSameSpeakerOverlap(regionIdx) { // consumes/merges same-speaker regions with overlapping bounds
987 let draggedRegion = currSpeakerSet.tempSpeakerObjects[regionIdx]; // regionIdx may point to a different region within the for-loop after adjustments, so defined here
988 let draggedRegionSpeaker = draggedRegion.speaker;
989 for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
990 if (currSpeakerSet.tempSpeakerObjects[i].speaker === draggedRegionSpeaker && !regionsMatch(draggedRegion, currSpeakerSet.tempSpeakerObjects[i])) { // ensure speaker name match
991 if (currSpeakerSet.tempSpeakerObjects[i].start < draggedRegion.end && draggedRegion.start < currSpeakerSet.tempSpeakerObjects[i].end) { // ensure overlap
992 draggedRegion.start = Math.min(currSpeakerSet.tempSpeakerObjects[i].start, draggedRegion.start);
993 draggedRegion.end = Math.max(currSpeakerSet.tempSpeakerObjects[i].end, draggedRegion.end);
994 currentRegion = draggedRegion;
995 currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove consumed region
996 i = -1; // reset for loop to support multiple consumptions
997 }
998 }
999 }
1000 reloadRegionsAndChapters();
1001 }
1002
1003 function updateRegionEditPanel() { // updates edit panel content/inputs
1004 if (currentRegion && currentRegion.speaker == "") {
1005 removeButton.classList.add("disabled");
1006 speakerInput.classList.add("disabled");
1007 speakerCheckbox.classList.add("disabled");
1008 speakerCheckbox.disabled = true;
1009 disableStartEndInputs();
1010 speakerInput.readOnly = true;
1011 speakerInput.value = "";
1012 } else {
1013 removeButton.classList.remove("disabled");
1014 speakerInput.classList.remove("disabled");
1015 speakerCheckbox.classList.remove("disabled");
1016 if (!isZooming) speakerCheckbox.disabled = false;
1017 enableStartEndInputs();
1018 speakerInput.readOnly = false;
1019 }
1020 if (editsMade) {
1021 discardButton.classList.remove("disabled");
1022 saveButton.classList.remove("disabled");
1023 } else {
1024 discardButton.classList.add("disabled");
1025 saveButton.classList.add("disabled");
1026 }
1027 if (speakerCheckbox.checked) {
1028 // changeAllLabel.innerHTML = "Change all (x" + currentRegions.length + ")";
1029 disableStartEndInputs();
1030 }
1031 if (currentRegion && currentRegion.speaker != "") {
1032 speakerInput.value = currentRegion.speaker;
1033 setInputInSeconds(startTimeInput, currentRegion.start);
1034 setInputInSeconds(endTimeInput, currentRegion.end);
1035 }
1036 }
1037
1038 function createNewRegion() { // adds a new region to the waveform
1039 const speaker = "NEW_SPEAKER"; // default name
1040 let offset = 0;
1041 if (!currSpeakerSet.uniqueSpeakers.includes(speaker)) { currSpeakerSet.uniqueSpeakers.push(speaker) }
1042 else { offset = 5 * getRegionsWithSpeaker(speaker).length } // offset new region if multiple new regions are created. TODO: check region has different start time
1043 const start = offset + wavesurfer.getCurrentTime();
1044 const end = offset + wavesurfer.getCurrentTime() + 15;
1045 currSpeakerSet.tempSpeakerObjects.push({speaker: speaker, start: start, end: end});
1046 editsMade = true;
1047 currentRegions = [];
1048 currentRegion = getRegionFromProps({speaker: speaker, start: start, end: end});
1049 reloadRegionsAndChapters();
1050 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "create", getCurrentRegionIndex());
1051 }
1052
1053 function getRegionFromProps(props) { // find region using speaker, start & end time
1054 for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
1055 if (currSpeakerSet.tempSpeakerObjects[i].speaker === props.speaker && currSpeakerSet.tempSpeakerObjects[i].start === props.start && currSpeakerSet.tempSpeakerObjects[i].end === props.end) {
1056 return currSpeakerSet.tempSpeakerObjects[i];
1057 }
1058 }
1059 console.log("getRegionFromProps failed to find matching region");
1060 }
1061
1062 function removeRegion() { // removes currently selected region or regions
1063 if (!removeButton.classList.contains("disabled")) {
1064 if (getCurrentRegionIndex() != -1) { // if currentRegion has been set
1065 let currentRegionIndex = getCurrentRegionIndex();
1066 let currentRegionIndexes = getCurrentRegionsIndexes();
1067 for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
1068 if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) {
1069 // if (!currentRegion.region) currentRegion.remove(); // remove from wavesurfer.regions.list
1070 currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove from tempSpeakerObjects
1071 // else currentRegion.region.remove(); // remove if region was just added
1072 editsMade = true;
1073 if (i >= 0) i--; // decrement index for side-by-side regions
1074 if (!speakerCheckbox.checked && currentRegions.length < 1) {
1075 removeCurrentRegion();
1076 reloadRegionsAndChapters();
1077 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex);
1078 return; // jump out of for loop
1079 }
1080 } else if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i])) {
1081 currSpeakerSet.tempSpeakerObjects.splice(i, 1);
1082 if (i >= 0) i--;
1083 }
1084 }
1085 removeCurrentRegion();
1086 reloadRegionsAndChapters();
1087 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed
1088 } else { console.log("no region selected") }
1089 }
1090 }
1091
1092 function regionsMatch(reg1, reg2) {
1093 if (reg1.start == reg2.start && reg1.end == reg2.end) return true;
1094 return false;
1095 }
1096
1097 function isCurrentRegion(region) {
1098 if (regionsMatch(currentRegion, region)) return true;
1099 return false;
1100 }
1101
1102 function isInCurrentRegions(region) {
1103 if (currentRegions != []) {
1104 for (let i = 0; i < currentRegions.length; i++) {
1105 if (currentRegions[i].start == region.start && currentRegions[i].end == region.end) {
1106 return true;
1107 }
1108 }
1109 }
1110 return false;
1111 }
1112
1113 function getCurrentRegionIndex() { // returns the index of currently selected region
1114 for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
1115 if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { return i }
1116 }
1117 // if (dualMode) {
1118 // for (let i = 0; i < secondarySet.tempSpeakerObjects.length; i++) {
1119 // if (isCurrentRegion(secondarySet.tempSpeakerObjects[i].region)) { return i }
1120 // }
1121 // }
1122 return -1;
1123 }
1124
1125 function getCurrentRegionsIndexes() { // returns the indexes of currently selected regions
1126 let indexes = [];
1127 for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
1128 if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i].region)) { indexes.push(i) }
1129 }
1130 return indexes;
1131 }
1132
1133 function removeCurrentRegion() { // removes current region, regions and bound markers
1134 currentRegion = {speaker: '', start: '', end: ''};
1135 currentRegions = [];
1136 removeRegionBounds();
1137 hoverSpeaker.innerHTML = "";
1138 }
1139
1140 function getRegionsWithSpeaker(speaker) { // returns all regions with the given speaker name
1141 let out = [];
1142 for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
1143 if (currSpeakerSet.tempSpeakerObjects[i].speaker === speaker) { out.push(currSpeakerSet.tempSpeakerObjects[i]) }
1144 }
1145 return out;
1146 }
1147
1148 function sortSpeakerObjectsByStart(speakerOb) { // sorts the speaker object array by start time
1149 return speakerOb.sort(function(a,b) {
1150 return a.start - b.start;
1151 });
1152 }
1153
1154 function speakerChange() { // speaker input name onInput handler
1155 const newSpeaker = speakerInput.value;
1156 if (newSpeaker && newSpeaker != "") {
1157 speakerInput.style.border = "2px solid transparent";
1158 if (getCurrentRegionIndex() != -1) { // if a region is selected
1159 if (!currSpeakerSet.uniqueSpeakers.includes(newSpeaker)) { currSpeakerSet.uniqueSpeakers.push(newSpeaker) }
1160 if (currentRegions && currentRegions.length < 1) { currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].speaker = newSpeaker } // single change
1161 else if (currentRegions && currentRegions.length > 1) { // multiple changes
1162 for (idx of getCurrentRegionsIndexes()) currSpeakerSet.tempSpeakerObjects[idx].speaker = newSpeaker;
1163 }
1164 speakerInput.value = "";
1165 currentRegion.speaker = newSpeaker;
1166 editsMade = true;
1167 reloadRegionsAndChapters();
1168 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "speaker-change", getCurrentRegionIndex(), getCurrentRegionsIndexes());
1169 } else { console.log("no region selected") }
1170 } else { console.log("no text in speaker input"); speakerInput.style.border = "2px solid firebrick"; }
1171 }
1172
1173 function speakerInputUnfocused() {
1174 prevUndoState = "";
1175 if (speakerInput.value == "" && !speakerInput.classList.contains("disabled")) {
1176 speakerInput.style.border = "2px solid firebrick";
1177 window.alert("Speaker input cannot be left empty. Please enter a speaker name.");
1178 setTimeout(() => speakerInput.focus(), 10); // timeout needed otherwise input isn't selected
1179 } else speakerInput.style.border = "2px transparent";
1180 }
1181
1182 function speakerCheckboxChanged() { // "Change all" toggled
1183 if (speakerCheckbox.checked) {
1184 if (!isZooming) {
1185 tempZoomSave = zoomSlider.value;
1186 zoomTo(0); // zoom out to encompass all selected regions
1187 }
1188 let uniqueSelectedSpeakers;
1189 if (currentRegions && currentRegions.length > 0) { // if more than one region selected
1190 uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
1191 uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1192 } else uniqueSelectedSpeakers = [currentRegion.speaker];
1193 currentRegions = [];
1194 for (const speaker of uniqueSelectedSpeakers) {
1195 for (const region of getRegionsWithSpeaker(speaker)) currentRegions.push(region);
1196 }
1197 reloadRegionsAndChapters();
1198 } else {
1199 if (!isZooming) {
1200 zoomTo(tempZoomSave / 4); // zoom back in to previous level
1201 }
1202 currentRegions = []; // this will lose track of previously selected region*s*
1203 // changeAllLabel.innerHTML = "Change all";
1204 reloadRegionsAndChapters();
1205 }
1206 }
1207
1208 function enableStartEndInputs() { // removes the 'disabled' tag from all time inputs
1209 for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = false }
1210 for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = false }
1211 }
1212
1213 function disableStartEndInputs() { // adds the 'disabled' tag to all time inputs
1214 for (idx in startTimeInput.childNodes) { startTimeInput.childNodes[idx].disabled = true; startTimeInput.childNodes[idx].value = 0; }
1215 for (idx in endTimeInput.childNodes) { endTimeInput.childNodes[idx].disabled = true; endTimeInput.childNodes[idx].value = 0; }
1216 }
1217
1218 function zoomTo(dest) { // (smoothly?) zooms wavesurfer waveform to destination
1219 isZooming = true;
1220 speakerCheckbox.disabled = true;
1221 let isOut = false;
1222 if (dest == 0) isOut = true;
1223 zoomInterval = setInterval(() => {
1224 if (isOut) {
1225 if (zoomSlider.value != 0) {
1226 if (zoomSlider.value > 50) zoomSlider.value -= 30; // ramp up for finer adjustments
1227 else zoomSlider.stepDown();
1228 wavesurfer.zoom(zoomSlider.value / 4);
1229 } else {
1230 clearInterval(zoomInterval);
1231 isZooming = false;
1232 speakerCheckbox.disabled = false;
1233 zoomSlider.dispatchEvent(new Event("input"));
1234 }
1235 } else {
1236 if (zoomSlider.value / 4 < dest) {
1237 if (zoomSlider.value > 50) zoomSlider.value += 30; // ramp up for finer adjustments
1238 else zoomSlider.stepUp();
1239 wavesurfer.zoom(zoomSlider.value / 4);
1240 } else {
1241 clearInterval(zoomInterval);
1242 isZooming = false;
1243 speakerCheckbox.disabled = false;
1244 zoomSlider.dispatchEvent(new Event("input"));
1245 }
1246 }
1247 }, 10); // interval
1248
1249 }
1250
1251 function saveRegionChanges() { // saves tempSpeakerObjects to speakerObjects
1252 if (!saveButton.classList.contains("disabled")) {
1253 currSpeakerSet.speakerObjects = cloneSpeakerObjectArray(currSpeakerSet.tempSpeakerObjects);
1254 editsMade = false;
1255 removeCurrentRegion();
1256 reloadRegionsAndChapters();
1257 console.log("saved changes");
1258 }
1259 }
1260
1261 function discardRegionChanges() { // resets tempSpeakerObjects to speakerObjects
1262 if (!discardButton.classList.contains("disabled")) {
1263 let confirm = window.confirm("Are you sure you want to discard changes?");
1264 if (confirm) {
1265 currSpeakerSet.tempSpeakerObjects = cloneSpeakerObjectArray(currSpeakerSet.speakerObjects);
1266 editsMade = false;
1267 removeCurrentRegion();
1268 resetUndoStates();
1269 reloadRegionsAndChapters();
1270 console.log("discarded changes");
1271 }
1272 }
1273 }
1274
1275 function reloadRegionsAndChapters() { // redraws edit panel, chapter list, wavesurfer regions
1276 updateRegionEditPanel();
1277 wavesurfer.clearRegions();
1278 $(".region-top").remove();
1279 $(".region-bottom").remove();
1280 populateChapters(primarySet);
1281 if (dualMode) {
1282 populateChapters(secondarySet);
1283 currSpeakerSet = primarySet;
1284 }
1285 updateCurrSpeakerSet();
1286 if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
1287 hoverSpeaker.innerHTML = currentRegion.speaker;
1288 hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
1289 removeRegionBounds();
1290 let currIndexes = getCurrentRegionsIndexes();
1291 for (let i = 0; i < currIndexes.length; i++) {
1292 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
1293 }
1294 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
1295 }
1296 if (currentRegions.length < 1) {
1297 removeButton.innerHTML = "Remove Selected Region";
1298 enableStartEndInputs();
1299 } else {
1300 removeButton.innerHTML = "Remove Selected Regions (x" + currentRegions.length + ")";
1301 disableStartEndInputs();
1302 const uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
1303 uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1304 // console.log(uniqueSelectedSpeakers); // CLG
1305 speakerInput.value = uniqueSelectedSpeakers.join(", ");
1306 }
1307 }
1308
1309 function changeStartEndTime(e) { // start/end time input handler
1310 let newStart = getTimeInSecondsFromInput(startTimeInput);
1311 let newEnd = getTimeInSecondsFromInput(endTimeInput);
1312 let duration = Math.floor(wavesurfer.getDuration()); // total duration of current audio
1313
1314 if (getCurrentRegionIndex() != -1) { // if there is a selected region
1315 if (newEnd <= newStart) newStart = newEnd - 1; // when start time > end time, push region forward
1316 if (newEnd <= 0) newEnd = 1;
1317 if (newStart < 0) newStart = 0; // ensures region start doesn't go < 0s
1318 if (newEnd > duration) newEnd = duration; // ensures region start doesn't go > duration
1319
1320 setInputInSeconds(startTimeInput, newStart);
1321 setInputInSeconds(endTimeInput, newEnd);
1322
1323 let currRegIdx = getCurrentRegionIndex();
1324 currSpeakerSet.tempSpeakerObjects[currRegIdx].start = newStart;
1325 currSpeakerSet.tempSpeakerObjects[currRegIdx].end = newEnd;
1326 currentRegion.start = newStart;
1327 currentRegion.end = newEnd;
1328 editsMade = true;
1329 reloadRegionsAndChapters();
1330 handleSameSpeakerOverlap(currRegIdx);
1331 addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "change-time", getCurrentRegionIndex());
1332 } else {
1333 console.log("no region selected");
1334 setInputInSeconds(startTimeInput, 0);
1335 setInputInSeconds(endTimeInput, 0);
1336 }
1337 }
1338
1339 function getTimeInSecondsFromInput(input) { // returns time in seconds from start or end input
1340 let hours = input.children[0].valueAsNumber;
1341 let mins = input.children[1].valueAsNumber;
1342 let secs = input.children[2].valueAsNumber;
1343 return (hours * 3600) + (mins * 60) + secs;
1344 }
1345
1346 function setInputInSeconds(input, seconds) { // sets start or end input time when given seconds
1347 let date = new Date(null);
1348 date.setMilliseconds(seconds * 1000);
1349 input.children[0].value = date.getHours() % 12;
1350 input.children[1].value = date.getMinutes();
1351 input.children[2].value = date.getSeconds() + "." + date.getMilliseconds();
1352
1353 document.querySelectorAll('input[type=number]').forEach(e => {
1354 e.value = Math.round(e.valueAsNumber * 10) / 10; // to 1dp
1355 if (e.classList.contains("seconds") && !e.value.includes(".")) e.value = e.value + ".0";
1356 else if (e.value.length === 1) e.value = '0' + e.value; // 0 on left for hrs & mins
1357 if (e.value.length === 3) e.value = '0' + e.value; // 0 on the left (doesn't work on FF)
1358 // if (e.value.length < 4) e.value = e.value.slice(0, 4); // always 4 digits (3 numbers, 1 fullstop)
1359 // if (!e.value) e.value = '00'; // avoiding letters on FF
1360 });
1361 }
1362
1363 function addUndoState(state, secState, isSec, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack
1364 let newState = cloneSpeakerObjectArray(state.tempSpeakerObjects); // clone method removes references
1365 let newSecState = cloneSpeakerObjectArray(secState.tempSpeakerObjects); // clone method removes references
1366 undoButton.classList.remove("disabled");
1367 undoStates = undoStates.slice(0, undoLevel + 1); // trim to current level if undos have already been made
1368 undoStates.push({state: newState, secState: newSecState, isSec: isSec, currentRegionIndex: currRegIdx, currentRegionIndexes: currRegIdxs, type: type});
1369 if ((type === "change-time" && prevUndoState === "change-time") || (type === "speaker-change" && prevUndoState === "speaker-change")) { // checks if similar change was made previously
1370 undoStates.splice(-2, 1); // remove second-to-last item in undoStates stack (merge last two changes into one to avoid multiple small edits)
1371 prevUndoState = type;
1372 } else undoLevel++;
1373 prevUndoState = type;
1374 redoButton.classList.add("disabled");
1375 // console.log(undoStates.at(-1));
1376 }
1377
1378 function undo() { // undo action: go back one state in the undoStates stack
1379 if (!undoButton.classList.contains("disabled")) { // ensure there exist states to undo to
1380 if (undoLevel - 1 < 0) console.log("ran out of undos");
1381 else {
1382 let adjustedUndoLevel = undoLevel-1;
1383 primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].state.slice(0)); // slice & clone removes potential references between arrays
1384 if (dualMode && undoStates[adjustedUndoLevel].secState && undoStates[adjustedUndoLevel].secState.length > 0) { // if secondary undoState exists
1385 secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].secState.slice(0)); // slice & clone removes potential references between arrays
1386 }
1387 editsMade = true;
1388
1389 let selectedSpeakerSet;
1390
1391 // handle currentRegion change
1392 removeCurrentRegion();
1393 if (undoStates[undoLevel] && undoStates[undoLevel].type && undoStates[undoLevel].type == "remove") { // if destination state type is remove
1394 selectedSpeakerSet = (undoStates[undoLevel].isSec) ? secondarySet : primarySet;
1395 if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
1396 else caretClicked("primary-caret");
1397 currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex]; // restore previous current state
1398 // console.log("undo-ing to index " + undoStates[undoLevel].currentRegionIndex);
1399 } else if (undoStates[undoLevel-1].currentRegionIndex) {
1400 if (!dualMode) selectedSpeakerSet = primarySet;
1401 else {
1402 selectedSpeakerSet = (undoStates[undoLevel-1].isSec) ? secondarySet : primarySet;
1403 if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
1404 else caretClicked("primary-caret");
1405 }
1406 currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel-1].currentRegionIndex]; // restore previous current state
1407 // console.log("undo-ing to index " + undoStates[undoLevel-1].currentRegionIndex);
1408 }
1409 // handle currentRegions change NEEDS REVISION xxxxx
1410 if (undoStates[undoLevel-1].currentRegionIndexes && undoStates[undoLevel-1].currentRegionIndexes.length > 1) {
1411 for (const idx of undoStates[undoLevel-1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
1412
1413 // currentRegions = getRegionsWithSpeaker(currentRegion.speaker);
1414 // if (!speakerCheckbox.checked) speakerCheckbox.click(); // manually fires onChange event
1415 }
1416 reloadRegionsAndChapters();
1417 undoLevel--; // decrement undoLevel
1418 if (undoLevel - 1 < 0) undoButton.classList.add("disabled");
1419 else undoButton.classList.remove("disabled");
1420 }
1421 if (undoLevel < undoStates.length) redoButton.classList.remove("disabled");
1422 }
1423 }
1424
1425 function redo() { // redo action: go forward one state in the undoStates stack
1426 if (!redoButton.classList.contains("disabled")) { // ensure there exist states to redo to
1427 if (undoLevel + 1 > undoStates.length - 1) console.log("ran out of redos");
1428 else {
1429 primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].state.slice(0)); // set primary to new state
1430 secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].secState.slice(0)); // set secondary to new state
1431 editsMade = true;
1432 let selectedSpeakerSet;
1433
1434 // handle currentRegion change
1435 if (undoLevel+1 <= undoStates.length-1) {
1436 removeCurrentRegion();
1437 if (undoStates[undoLevel+2] && undoStates[undoLevel+2].type && undoStates[undoLevel+2].type == "remove") {
1438 selectedSpeakerSet = (undoStates[undoLevel+2].isSec) ? secondarySet : primarySet;
1439 if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
1440 else caretClicked("primary-caret");
1441 currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+2].currentRegionIndex];
1442 } else {
1443 selectedSpeakerSet = (undoStates[undoLevel+1].isSec) ? secondarySet : primarySet;
1444 if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
1445 else caretClicked("primary-caret");
1446 currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+1].currentRegionIndex];
1447 }
1448
1449 // console.log("redo-ing to index " + undoStates[undoLevel+1].currentRegionIndex);
1450 if (undoStates[undoLevel+1].currentRegionIndexes && undoStates[undoLevel+1].currentRegionIndexes.length > 1) {
1451 for (const idx of undoStates[undoLevel-1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
1452 // currentRegions = getRegionsWithSpeaker(currentRegion.speaker);
1453 // if (!speakerCheckbox.checked) speakerCheckbox.click(); // ensures onchange event is fired
1454 }
1455 }
1456
1457 reloadRegionsAndChapters();
1458 undoLevel++; // increment undoLevel
1459 if (undoLevel + 1 > undoStates.length - 1) redoButton.classList.add("disabled");
1460 else redoButton.classList.remove("disabled");
1461 }
1462 if (undoLevel < undoStates.length) undoButton.classList.remove("disabled");
1463 // console.log("new undoLevel: " + undoLevel);
1464 }
1465 }
1466
1467 function resetUndoStates() { // clear undo history
1468 undoStates = [{state: cloneSpeakerObjectArray(primarySet.tempSpeakerObjects), secState: cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects)}];
1469 undoLevel = 0;
1470 undoButton.classList.add("disabled");
1471 redoButton.classList.add("disabled");
1472 }
1473
1474 function waveformScrolled() { // waveform scroll handler
1475 if (currentRegion.speaker && getCurrentRegionIndex() != -1) { // updates region bound markers if selected region exists
1476 hoverSpeaker.innerHTML = currentRegion.speaker;
1477 hoverSpeaker.style.marginLeft = parseInt(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
1478 removeRegionBounds();
1479 let currIndexes = getCurrentRegionsIndexes();
1480 for (let i = 0; i < currIndexes.length; i++) {
1481 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, waveform.scrollLeft, "black");
1482 }
1483 drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, waveform.scrollLeft);
1484 }
1485 }
1486
1487 function drawRegionBounds(region, scrollPos) { // draws on canvas to show bounds of hovered/selected region
1488 const hoverSpeakerCanvas = document.createElement("canvas");
1489 let colour = "black";
1490 hoverSpeakerCanvas.id = "hover-speaker-canvas";
1491 hoverSpeakerCanvas.classList.add("region-bounds");
1492 hoverSpeakerCanvas.width = audioContainer.clientWidth; // max width of drawn bounds
1493 const ctx = hoverSpeakerCanvas.getContext("2d");
1494
1495 ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency
1496 ctx.lineWidth = 1;
1497 if (currentRegions && currentRegions.length < 1 && isCurrentRegion(region)) {
1498 colour = "FireBrick";
1499 ctx.lineWidth = 3;
1500 }
1501 ctx.strokeStyle = colour;
1502 ctx.beginPath();
1503 ctx.moveTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 28);
1504 ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) - scrollPos, 20);
1505 ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 20);
1506 ctx.lineTo(parseInt(region.element.style.left.slice(0, -2)) + parseInt(region.element.style.width.slice(0, -2)) - scrollPos, 28);
1507 ctx.stroke();
1508 audioContainer.prepend(hoverSpeakerCanvas);
1509 }
1510
1511 function removeRegionBounds() { // remove all region bound markers
1512 let canvases = document.getElementsByClassName('region-bounds');
1513 while (canvases[0]) canvases[0].parentNode.removeChild(canvases[0]);
1514 }
1515
1516 function updateCurrSpeakerSet() {
1517 if (primaryCaret.src.includes("fill")) currSpeakerSet = primarySet;
1518 else if (secondaryCaret.src.includes("fill")) currSpeakerSet = secondarySet;
1519 }
1520
1521 function cloneSpeakerObjectArray(inputArray) { // clones speakerObjectArray without references (wavesurfer regions)
1522 let output = [];
1523 for (let i = 0; i < inputArray.length; i++) { output.push({speaker: inputArray[i].speaker, start: inputArray[i].start, end: inputArray[i].end }) }
1524 return output;
1525 }
1526
1527 function flashInput(valid) { // flashes background of input to show validity of input
1528 if (valid) speakerInput.style.backgroundColor = "rgb(50,255,50)";
1529 else speakerInput.style.backgroundColor = "rgb(255,50,50)";
1530 setTimeout(() => { speakerInput.style.backgroundColor = "rgb(255,255,255)" }, 750);
1531 }
1532
1533 function flashChapters() {
1534 chapters.style.backgroundColor = "rgb(66, 84, 88)";
1535 setTimeout(() => { chapters.style.backgroundColor = "rgb(40, 54, 58)" }, 500);
1536 }
1537
1538 function fullscreenChanged() { // fullscreen onChange handler, increases waveform height & adjusts padding/margin
1539 if (!audioContainer.classList.contains("fullscreen")) {
1540 audioContainer.classList.add("fullscreen");
1541 wavesurfer.setHeight(175);
1542 } else {
1543 audioContainer.classList.remove("fullscreen");
1544 wavesurfer.setHeight(128);
1545 }
1546 }
1547
1548 function toggleFullscreen() { // toggles fullscreen mode of audio player/editor
1549 if ((document.fullscreenElement && document.fullscreenElement !== null) ||
1550 (document.webkitFullscreenElement && document.webkitFullscreenElement !== null) ||
1551 (document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
1552 (document.msFullscreenElement && document.msFullscreenElement !== null)) {
1553 document.exitFullscreen();
1554 } else {
1555 audioContainer.requestFullscreen();
1556 }
1557 }
1558}
1559
1560function formatAudioDuration(duration) {
1561 // console.log('duration: ' + duration);
1562 let [hrs, mins, secs, ms] = duration.replace(".", ":").split(":");
1563 return hrs + ":" + mins + ":" + secs;
1564}
Note: See TracBrowser for help on using the repository browser.