Changeset 37287


Ignore:
Timestamp:
2023-02-07T16:25:32+13:00 (16 months ago)
Author:
davidb
Message:

main features for interactive editing of audio in dual mode complete

Location:
main/trunk/greenstone3/web/interfaces/default
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js

    r37031 r37287  
    291291// Audio Scripts for Enriched Playback
    292292
     293var wavesurfer;
     294
    293295function loadAudio(audio, sectionData) {
    294296   let editMode = false;
     
    304306   let dualMode = false;
    305307   let secondaryLoaded = false;
     308
     309   let waveformCursorX = 0;
     310   let snappedToX = 0;
     311   let snappedTo = "none";
     312   let cursorPos = 0;
     313   let ctrlDown = false;
     314   let mouseDown = false;
     315   let newRegionOffset = 0;
    306316
    307317   let editsMade = false;
     
    315325   // let accentColour = "#F8C537";
    316326   let regionTransparency = "50";
     327   let colourbrewerSet = colorbrewer.Set2[8];
     328   let regionColourSet = [];
    317329
    318330   let waveformContainer = document.getElementById("waveform");
    319331   
    320    let wavesurfer = WaveSurfer.create({ // wavesurfer options
     332   wavesurfer = WaveSurfer.create({ // wavesurfer options
    321333      container: waveformContainer,
    322334      backend: "MediaElement",
     
    355367                'font-size': '12px'
    356368            },
    357             formatTimeCallback: (num) => { return minutize(num); }
     369            formatTimeCallback: (num) => { return formatCursor(num); }
    358370         }),
    359371      ],
     
    367379
    368380   function handleRegionClick(region, e) {
     381      contextMenu.classList.remove('visible');
    369382      e.stopPropagation();
    370383      if (!editMode) { // play region audio on click
    371384         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");
     385      } else { // select or deselect current region
     386         if (region.element.classList.contains("region-top")) {
     387            currSpeakerSet = primarySet;
     388            swapCarets(true);
     389         } else if (region.element.classList.contains("region-bottom")) {
     390            currSpeakerSet = secondarySet;
     391            swapCarets(false);
     392         }
    375393         prevUndoState = "";
    376394
     
    378396            currentRegions = [];
    379397            if (getCurrentRegionIndex() != -1 && isCurrentRegion(region)) {
    380                removeCurrentRegion(); // deselect current region on click
     398               // removeCurrentRegion(); // deselect current region on click
    381399            } else {
    382400               currentRegion = region;
    383                currentRegion.speaker = currentRegion.attributes.label;
     401               currentRegion.speaker = currentRegion.attributes.label.innerText;
    384402               region.play(); // start and stop to move play cursor to beginning of region
    385403               wavesurfer.playPause();
     
    393411               if (currentRegions.length > 0 && isCurrentRegion(region)) { // change current region if removed
    394412                  currentRegion = currentRegions[0];
    395                   // currentRegions = [];
    396413               }
    397414            } else {
     
    399416               if (getIndexInCurrentRegions(region) == -1) currentRegions.push(region); // add if it doesn't already exist
    400417               currentRegion = region;
    401                currentRegion.speaker = currentRegion.attributes.label;
     418               currentRegion.speaker = currentRegion.attributes.label.innerText;
    402419               region.play();
    403420               wavesurfer.playPause();
     
    405422            if (currentRegions.length == 1)  currentRegions = []; // clear selected regions if there is only one
    406423         } else if (e.shiftKey) { // shift was held during click
     424            clearChapterSearch();
    407425            if (getCurrentRegionIndex() != -1 && getIndexOfRegion(region) != -1) {
    408426               if (currentRegions && currentRegions.length > 0) {
     
    421439            }
    422440         }
    423          if (speakerCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) }
     441         if (changeAllCheckbox.checked) { currentRegions = getRegionsWithSpeaker(currentRegion.speaker) }
    424442         reloadRegionsAndChapters();
    425443      }
     
    428446   function getIndexInCurrentRegions(region) {
    429447      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) {
     448         const regSpeaker = reg.attributes ? reg.attributes.label.innerText : reg.speaker;
     449         if (reg.start == region.start && reg.end == region.end && regSpeaker == region.attributes.label.innerText) {
    432450            return currentRegions.indexOf(reg);
    433451         }
     
    438456   function getIndexOfRegion(region) {
    439457      for (const reg of currSpeakerSet.tempSpeakerObjects) {
    440          if (reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label) {
     458         if (reg.start == region.start && reg.end == region.end && reg.speaker == region.attributes.label.innerText) {
    441459            return currSpeakerSet.tempSpeakerObjects.indexOf(reg);
    442460         }
     
    446464
    447465   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)) {
     466      if (!mouseDown) {
     467         handleRegionColours(region, true);
     468         setHoverSpeaker(region.element.style.left, region.attributes.label.innerText);
     469         if (!isInCurrentRegions(region)) {
     470            removeRegionBounds();
     471            drawRegionBounds(region, wave.scrollLeft, "black");
     472         }
     473         if (isCurrentRegion(region) && editMode) drawRegionBounds(region, wave.scrollLeft, "FireBrick");
     474      }
     475   });
     476
     477   function setHoverSpeaker(offset, name) {
     478      hoverSpeaker.innerHTML = name; 
     479      let newOffset = parseInt(offset.slice(0, -2)) - wave.scrollLeft;
     480      // if (newOffset < 0) newOffset = 0;
     481      hoverSpeaker.style.marginLeft = newOffset + "px";
     482   }
     483
     484   wavesurfer.on('region-mouseleave', function(region) {
     485      hoverSpeaker.innerHTML = "";
     486      if (!mouseDown) {
     487         if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false);
     488         if (!editMode) hoverSpeaker.innerHTML = "";
    452489         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 = "";
     490         if (currentRegion.speaker && getCurrentRegionIndex() != -1) {
     491            setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
     492            drawCurrentRegionBounds();
     493         }
     494      }
    470495   });
    471496   wavesurfer.on('region-in', function(region) {
    472       handleRegionColours(region, true);
    473       if (itemType == "chapter") {
    474          document.getElementById("chapter" + region.id.replace("region", "")).scrollIntoView({
     497      // handleRegionColours(region, true);
     498      if (itemType == "chapter" && Array.from(chapters.children)[getIndexOfRegion(region)]) {
     499         Array.from(chapters.children)[getIndexOfRegion(region)].scrollIntoView({ 
    475500            behavior: "smooth",
    476501            block: "nearest"
     
    480505   wavesurfer.on('region-out', function(region) { handleRegionColours(region, false) });
    481506   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
     507   wavesurfer.on('region-updated', handleRegionSnap);
    483508
    484509   let loader = document.createElement("span"); // loading audio element
     
    490515      if (inputFile.endsWith("csv")) { // diarization if csv
    491516         itemType = "chapter";
    492          loadCSVFile(inputFile, ["speaker", "start", "end"], primarySet);
     517         if (localStorage.getItem('undoStates') && localStorage.getItem('undoLevel')) {
     518            console.log('-- Loading regions from localStorage --');
     519            undoStates = JSON.parse(localStorage.getItem('undoStates'));
     520            undoLevel = JSON.parse(localStorage.getItem('undoLevel'));
     521            primarySet.tempSpeakerObjects = undoStates[undoLevel].state;
     522            primarySet.uniqueSpeakers = [];
     523            for (const item of primarySet.tempSpeakerObjects) {
     524               if (!primarySet.uniqueSpeakers.includes(item.speaker)) primarySet.uniqueSpeakers.push(item.speaker);
     525            }
     526            populateChapters(primarySet);
     527            if (undoStates[undoLevel].secState && undoStates[undoLevel].secState.length > 0) {
     528               secondarySet.tempSpeakerObjects = undoStates[undoLevel].secState;
     529               secondarySet.uniqueSpeakers = [];
     530               for (const item of secondarySet.tempSpeakerObjects) {
     531                  if (!secondarySet.uniqueSpeakers.includes(item.speaker)) secondarySet.uniqueSpeakers.push(item.speaker);
     532               }
     533               secondaryLoaded = true;
     534               // editButton.click(); // open edit panel and enable dual mode if secondary set was previously altered
     535               // dualModeCheckbox.checked = true;
     536               // dualModeChanged(true);
     537            }
     538            updateRegionEditPanel();
     539         } else {
     540            loadCSVFile(inputFile, ["speaker", "start", "end"], primarySet);
     541            dualModeCheckbox.checked = true;
     542            dualModeChanged(true);
     543           
     544            setTimeout(()=>{
     545               dualModeCheckbox.checked = false;
     546               dualModeChanged(true);
     547            }, 150)
     548         }
    493549      } else if (inputFile.endsWith("json")) { // transcription if json
    494550         itemType = "word";
     
    497553         console.log("Filetype of " + inputFile + " not supported.")
    498554      }
     555     
    499556      loader.remove(); // remove load text
    500       chapters.style.cursor = "pointer";
    501       waveform.className = "audio-scroll";
     557      chapters.style.cursor = "default"; // remove load cursor
     558      wave.className = "audio-scroll";
     559      drawVersionNames(); // draw version names if editPanel is expanded
    502560   });
    503561   
     
    512570   const audioContainer = document.getElementById("audioContainer");
    513571   const dualModeCheckbox = document.getElementById("dual-mode-checkbox");
    514    const waveform = document.getElementsByTagName("wave")[0];
     572   const wave = document.getElementsByTagName("wave")[0];
    515573   const primaryCaret = document.getElementById("primary-caret");
    516574   const secondaryCaret = document.getElementById("secondary-caret");
    517575   const chapters = document.getElementById("chapters");
     576   const chaptersContainer = document.getElementById("chapters-container");
    518577   const editPanel = document.getElementById("edit-panel");
    519578   const chapterButton = document.getElementById("chapterButton");
     579   const chapterSearchInput = document.getElementById("chapter-search-input");
    520580   const zoomOutButton = document.getElementById("zoomOutButton");
    521581   const zoomSlider = document.getElementById("zoom-slider");
     
    524584   const playPauseButton = document.getElementById("playPauseButton");
    525585   const forwardButton = document.getElementById("forwardButton");
    526    const editButton = document.getElementById("editButton");
     586   const editButton = document.getElementById("editorModeButton");
    527587   const downloadButton = document.getElementById("downloadButton");
    528588   const muteButton = document.getElementById("muteButton");
    529589   const volumeSlider = document.getElementById("volume-slider");
    530590   const fullscreenButton = document.getElementById("fullscreenButton");
    531    const speakerCheckbox = document.getElementById("change-all-checkbox");
     591   const changeAllCheckbox = document.getElementById("change-all-checkbox");
    532592   const changeAllLabel = document.getElementById("change-all-label");
    533593   const speakerInput = document.getElementById("speaker-input");
     
    541601   const saveButton = document.getElementById("save-button");
    542602   const hoverSpeaker = document.getElementById("hover-speaker");
     603   const contextMenu = document.getElementById("context-menu");
     604   const contextDelete = document.getElementById("context-menu-delete");
     605   const contextReplace = document.getElementById("context-menu-replace");
     606   const contextOverdub = document.getElementById("context-menu-overdub");
     607   // const contextCopy = document.getElementById("context-menu-copy");
     608   const contextSave = document.getElementById("context-menu-save");
     609   const dualModeMenuButton = document.getElementById("dual-mode-menu-button");
     610   const dualModeMenu = document.getElementById("dual-mode-menu");
    543611
    544612   audioContainer.addEventListener('fullscreenchange', (e) => { fullscreenChanged() });
    545    dualModeCheckbox.addEventListener("change", dualModeChanged);
    546    waveform.addEventListener('scroll', (e) => { waveformScrolled() })
     613   audioContainer.addEventListener('contextmenu', onRightClick);
     614   audioContainer.addEventListener("keyup", keyUp);
     615   audioContainer.addEventListener("keydown", keyDown);
     616   dualModeCheckbox.addEventListener("change", () => { dualModeChanged() });
     617   wave.addEventListener('scroll', (e) => { waveformScrolled() })
     618   wave.addEventListener('mousemove', (e) => waveformCursorX = e.x);
    547619   primaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
    548620   secondaryCaret.addEventListener("click", (e) => caretClicked(e.target.id));
    549621   chapters.style.height = "0px";
     622   chaptersContainer.style.height = "0px";
    550623   editPanel.style.height = "0px";
    551624   chapterButton.addEventListener("click", () => { toggleChapters() });
     625   chapterSearchInput.addEventListener("input", chapterSearchInputChange)
    552626   zoomOutButton.addEventListener("click", () => { zoomSlider.stepDown(); zoomSlider.dispatchEvent(new Event("input")) });
    553627   zoomInButton.addEventListener("click", () => { zoomSlider.stepUp(); zoomSlider.dispatchEvent(new Event("input")) });
     
    561635   fullscreenButton.addEventListener("click", toggleFullscreen);
    562636   zoomSlider.style["accent-color"] = accentColour;
    563    speakerCheckbox.addEventListener("change", speakerCheckboxChanged);
     637   changeAllCheckbox.addEventListener("change", () => { selectAllCheckboxChanged() });
    564638   speakerInput.addEventListener("input", speakerChange);
    565639   speakerInput.addEventListener("blur", speakerInputUnfocused);
     
    570644   redoButton.addEventListener("click", redo);
    571645   saveButton.addEventListener("click", saveRegionChanges);
     646   document.addEventListener('click', () => contextMenu.classList.remove('visible'));
     647   document.addEventListener('mouseup', () => mouseDown = false);
     648   document.addEventListener('mousedown', (e) => { if (e.target.id !== "create-button") newRegionOffset = 0 }); // resets new region offset on click
    572649   document.querySelectorAll('input[type=number]').forEach(e => {
    573650      e.onchange = (e) => { changeStartEndTime(e) }; // updates speaker objects when number input(s) are changed
    574651      e.onblur = () => { prevUndoState = "" };
    575652   });
    576    audioContainer.addEventListener("keyup", keyPressed);
    577 
    578    function keyPressed(e) {
     653   contextDelete.addEventListener("click", removeRightClicked);
     654   contextReplace.addEventListener("click", replaceSelected);
     655   contextOverdub.addEventListener("click", overdubSelected);
     656   // contextCopy.addEventListener("click", copySelected);
     657   contextSave.addEventListener("click", saveSelected);
     658   dualModeMenuButton.addEventListener("click", dualModeMenuToggle);
     659   dualModeMenuButton.addEventListener("click", dualModeMenuToggle);
     660
     661   if (gs.variables.allowEditing === '0') { editButton.style.display = "none" }
     662
     663   function chapterSearchInputChange(e) {
     664      if (e.isTrusted) { // triggered from user action
     665         if (document.getElementById("chapter-alert")) document.getElementById("chapter-alert").remove();
     666         let matches = 0;
     667         for (const idx in chapters.children) {
     668            if (chapters.children[idx].firstChild && chapters.children[idx].classList.contains("chapter") && currSpeakerSet.tempSpeakerObjects[idx]
     669               && currSpeakerSet.tempSpeakerObjects[idx].region && currSpeakerSet.tempSpeakerObjects[idx].region.element) {
     670               if (e.composed) removeCurrentRegion(); // composed true if called from input, false if manually triggered event
     671               if (!chapters.children[idx].firstChild.innerText.toLowerCase().includes(e.target.value.toLowerCase())) {
     672                  chapters.children[idx].style.display = "none";
     673                  currSpeakerSet.tempSpeakerObjects[idx].region.element.style.display = "none";
     674               } else {
     675                  chapters.children[idx].style.display = "flex";
     676                  currSpeakerSet.tempSpeakerObjects[idx].region.element.style.display = "";
     677                  matches++;
     678                  if (e.target.value.length > 0) {
     679                     const reg = new RegExp(e.target.value, 'gi'); // [g]lobal, [i]gnore case
     680                     chapters.children[idx].firstChild.innerHTML = chapters.children[idx].firstChild.innerText.replace(reg, '<b>$&</b>'); // highlights matching text
     681                  } else {
     682                     chapters.children[idx].firstChild.innerHTML = chapters.children[idx].firstChild.innerText; // highlights matching text
     683                  }
     684               }
     685            }
     686         }
     687         flashChapters();
     688         if (matches == 0) {
     689            const msg = document.createElement("span");
     690            msg.innerHTML = "No Matches!";
     691            msg.id = "chapter-alert";
     692            chapters.prepend(msg);
     693         } 
     694      }
     695   }
     696
     697   function clearChapterSearch() {
     698      chapterSearchInput.value = "";
     699      chapterSearchInput.dispatchEvent(new Event("input"));
     700   }
     701
     702   function dualModeMenuToggle() {
     703      if (editMode && dualMode) {
     704         if (dualModeMenu.classList.contains('visible')) dualModeMenu.classList.remove('visible');
     705         else dualModeMenu.classList.add('visible');
     706      }
     707   }
     708
     709   function handleRegionSnap(region, e) {
     710      if (editMode && currentRegion) {
     711         removeRegionBounds();
     712         setHoverSpeaker(region.element.style.left, currentRegion.speaker);
     713         drawRegionBounds(region, wave.scrollLeft, "FireBrick");
     714         if (e && e.action === "resize" && dualMode && editMode && !ctrlDown) { // won't actuate on drag
     715            let oppositeSet = secondarySet; // look down
     716            if (currSpeakerSet.isSecondary) oppositeSet = primarySet; // look up
     717            if (e.direction === "left") {
     718               region.update({ start: getSnapValue(region.start, oppositeSet.tempSpeakerObjects)});
     719            } else if (e.direction === "right") {
     720               region.update({ end: getSnapValue(region.end, oppositeSet.tempSpeakerObjects)});
     721            }
     722         }
     723         if (e && (e.action === "resize" || e.action === "drag")) {
     724            setInputInSeconds(startTimeInput, region.start);
     725            setInputInSeconds(endTimeInput, region.end);
     726         }
     727      }
     728   }
     729
     730   function getSnapValue(newDragPos, speakerSet) {
     731      const snapRadius = 1;     
     732      for (const region of speakerSet) { // scan opposite region for potential snapping points
     733         if (newDragPos > parseFloat(region.start) - snapRadius && newDragPos < parseFloat(region.start) + snapRadius) {
     734            // console.log("snap to start: " + region.start);
     735            snappedTo = "start";
     736            if (snappedToX == 0) snappedToX = waveformCursorX;
     737            return region.start;
     738         }
     739         if (newDragPos > parseFloat(region.end) - snapRadius && newDragPos < parseFloat(region.end) + snapRadius) {
     740            // console.log("snap to end: " + region.end);
     741            snappedTo = "end";
     742            if (snappedToX == 0) snappedToX = waveformCursorX;
     743            return region.end;
     744         }
     745
     746         if (snappedTo !== "none" && (waveformCursorX - snappedToX > 10 || waveformCursorX - snappedToX < -10)) {
     747            // console.log('released!');
     748            snappedTo = "none";
     749            snappedToX = 0;
     750            return cursorPos;
     751         }
     752      }
     753      return newDragPos;
     754   }
     755
     756   function mmssToSeconds(input) {
     757      const arr = input.split(":");
     758      if (arr.length == 2) {
     759         return (parseInt(arr[0]) * 60) + parseInt(arr[1]);
     760      } else if (arr.length == 3) {
     761         return (parseInt(arr[0]) * 3600) + (parseInt(arr[1]) * 60) + parseInt(arr[2]);
     762      } else {
     763         console.error("unexpected input to mmssToSeconds: " + input);
     764      }
     765   }
     766
     767   function removeRightClicked(e) {
     768      if (!e.target.classList.contains('faded')) {
     769         removeRegion();
     770      }
     771   }
     772
     773   function replaceSelected(e) {
     774      if (!e.target.classList.contains('faded')) {
     775         let destinationSet = secondarySet; // replace down
     776         if (currSpeakerSet.isSecondary) destinationSet = primarySet; // replace up
     777         let currItems = [currentRegion];
     778         if (currentRegions && currentRegions.length > 0) currItems = currentRegions;
     779         for (let idx = 0; idx < currItems.length; idx++) { // handles both currentRegion and currentRegions
     780            for (let idy = 0; idy < destinationSet.tempSpeakerObjects.length; idy++) {
     781               const reg = destinationSet.tempSpeakerObjects[idy];
     782               if ((parseFloat(reg.start) >= parseFloat(currItems[idx].start) && parseFloat(reg.start) <= parseFloat(currItems[idx].end)) ||
     783                   (parseFloat(reg.start) <= parseFloat(currItems[idx].start) && parseFloat(reg.end) >= parseFloat(currItems[idx].start))) {
     784                  destinationSet.tempSpeakerObjects.splice(idy, 1); // remove subsequent region
     785                  idy--;
     786               }
     787            }
     788         }
     789         copySelected(e, true);
     790         reloadRegionsAndChapters();
     791         addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "replace", getCurrentRegionIndex());
     792      }
     793   }
     794
     795   function containsRegion(set, region) {
     796      for (const item of set) {
     797         if (regionsMatch(region, item)) return true;
     798      }
     799      return false;
     800   }
     801
     802   function overdubSelected(e) {
     803      if (!e.target.classList.contains('faded')) {
     804         let destinationSet = secondarySet; // replace down
     805         if (currSpeakerSet.isSecondary) destinationSet = primarySet; // replace up
     806         let backup;
     807         if (destinationSet.isSecondary) backup = cloneSpeakerObjectArray(primarySet.tempSpeakerObjects); // saves selected set as this process changes values in selected set (unknown reason)
     808         else backup = cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects);
     809            copySelected(e, true);
     810         if (!currentRegions || currentRegions.length < 1) { // overdub single
     811            handleSameSpeakerOverlap(getCurrentRegionIndex(), destinationSet);
     812         } else { // overdub multiple
     813            for (const item of getCurrentRegionsIndexes().reverse()) { // reverse indexes so index doesn't break when regions are removed
     814               handleSameSpeakerOverlap(item, destinationSet);
     815            }
     816         }
     817         if (destinationSet.isSecondary) primarySet.tempSpeakerObjects = backup;
     818         else secondarySet.tempSpeakerObjects = backup;
     819         addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "overdub", getCurrentRegionIndex());
     820         reloadRegionsAndChapters();
     821      }
     822   }
     823
     824   function copySelected(e, skipUndoState) {
     825      if (!e.target.classList.contains('faded')) {
     826         let out = -1;
     827         let destinationSet = secondarySet; // copy down
     828         if (currSpeakerSet.isSecondary) { destinationSet = primarySet } // copy up
     829         if (currentRegions && currentRegions.length > 1) { // copy multiple
     830            const selectedRegion = currentRegion;
     831            const selectedRegions = currentRegions;
     832            destinationSet.tempSpeakerObjects.push(...selectedRegions);
     833            currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions)
     834            for (const reg of destinationSet.tempSpeakerObjects) { // restore currentRegions in dest. set
     835               for (const selReg of selectedRegions) {
     836                  if (regionsMatch(reg, selReg) && !containsRegion(currentRegions, reg)) {
     837                     currentRegions.push(reg);
     838                  }
     839               }
     840               if (regionsMatch(reg, selectedRegion)) { currentRegion = reg; }
     841            }
     842         } else { // copy singular
     843            const selectedRegion = currentRegion; // copy currRegion as caretClicked wipes it
     844            destinationSet.tempSpeakerObjects.push(selectedRegion); // append current region to dest. set
     845            currSpeakerSet.isSecondary ? caretClicked("primary-caret") : caretClicked("secondary-caret"); // swap selected speakerSet (clears current regions)
     846            for (const reg of destinationSet.tempSpeakerObjects) { // restore currentRegion in dest. set
     847               if (regionsMatch(reg, selectedRegion)) {
     848                  currentRegion = reg;
     849                  break;
     850               }
     851            }
     852         }
     853         reloadRegionsAndChapters();
     854         if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "copy", getCurrentRegionIndex());
     855      }
     856   }
     857
     858   function onRightClick(e) {
     859      if (e.target.classList.contains("wavesurfer-region") && editMode) {
     860         e.preventDefault();
     861         contextMenu.classList.add("visible");
     862         if (e.clientX + 200 > $(window).width()) contextMenu.style.left = ($(window).width() - 220) + "px"; // ensure menu doesn't clip on right
     863         else contextMenu.style.left = e.clientX + "px";
     864         contextMenu.style.top = e.clientY + "px";
     865
     866         if (dualMode && currentRegion && currentRegion.speaker !== "") {
     867            contextReplace.classList.remove('faded');
     868            contextOverdub.classList.remove('faded');
     869            // contextCopy.classList.remove('faded');
     870         } else {
     871            contextDelete.classList.add('faded');
     872            contextReplace.classList.add('faded');
     873            contextOverdub.classList.add('faded');
     874            // contextCopy.classList.add('faded');
     875         }
     876         if (currentRegion && currentRegion.speaker !== "") contextDelete.classList.remove('faded');
     877         if (dualMode) { // manipulate context texts
     878            const actionDirection = currSpeakerSet.isSecondary ? "Up" : "Down";
     879            contextReplace.innerHTML = "Replace Selected " + actionDirection;
     880            contextOverdub.innerHTML = "Overdub Selected " + actionDirection;
     881            // contextCopy.innerHTML = "Copy Selected " + actionDirection;
     882         }
     883      }
     884   }
     885
     886   function saveSelected(e) {
     887      let csvContent = "data:text/csv;charset=utf-8," + currSpeakerSet.speakerObjects.map(item => "\n" + [item.speaker, item.start, item.end].join());
     888      console.log(csvContent);
     889      var encodedUri = encodeURI(csvContent);
     890      window.open(encodedUri);
     891   }
     892
     893   function keyUp(e) {
     894      if (e.key == "Control") ctrlDown = false;
    579895      if (e.target.tagName !== "INPUT") {
    580896         if (e.code === "Backspace" || e.code === "Delete") removeRegion();
     
    583899         else if (e.code === "ArrowRight") wavesurfer.skipForward();
    584900      }
    585    }
    586 
    587    function dualModeChanged(e) { // on dualmode checkbox value change
    588       dualMode = e.target.checked;
     901      if (e.code == "KeyZ" && e.ctrlKey) undo();
     902      else if (e.code == "KeyY" && e.ctrlKey) redo();
     903   }
     904
     905   function keyDown(e) {
     906      if (e.key == "Control") ctrlDown = true;
     907   }
     908
     909   function dualModeChanged(skipUndoState) { // on dualmode checkbox value change
     910      clearChapterSearch();
     911      dualMode = dualModeCheckbox.checked;
    589912      currSpeakerSet = primarySet;
     913      if (!dualMode) removeCurrentRegion();
    590914      reloadRegionsAndChapters();
    591915      if (dualMode) {
     916         dualModeMenuButton.classList.add('visible');
    592917         if (!secondaryLoaded) {
    593918            loadCSVFile(inputFile.replace(".csv", "-2.csv"), ["speaker", "start", "end"], secondarySet);
     
    596921         document.getElementById("caret-container").style.display = "flex";
    597922      } else {
     923         dualModeMenuButton.classList.remove('visible');
     924         caretClicked('primary-caret');
    598925         document.getElementById("caret-container").style.display = "none";
    599926      }
    600927      currSpeakerSet = primarySet;
     928      drawVersionNames();
     929      if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dualModeChange", getCurrentRegionIndex());
    601930   }
    602931
     
    605934
    606935   function caretClicked(id) {
    607       wavesurfer.clearRegions();
    608       // flashChapters();
     936      clearChapterSearch();
    609937      if (id === "primary-caret") {
    610938         currSpeakerSet = primarySet;
     
    614942         swapCarets(false);
    615943      }
    616       $(".region-top").remove();
    617       $(".region-bottom").remove();
    618       populateChapters(primarySet);
    619       populateChapters(secondarySet);
    620944   }
    621945
    622946   function swapCarets(toPrimary) {
    623947      const currCaretIsPrimary = primaryCaret.src.includes("fill") ? true : false;
    624       if ((toPrimary && !currCaretIsPrimary) || (!toPrimary && currCaretIsPrimary)) removeCurrentRegion(); // ensure currentRegion is only removed if changing speakerSet
     948      if ((toPrimary && !currCaretIsPrimary) || (!toPrimary && currCaretIsPrimary)) {
     949         removeCurrentRegion(); // ensure currentRegion is only removed if changing speakerSet
     950         flashChapters();
     951         reloadChapterList();
     952      }
    625953      if (toPrimary) {
    626954         primaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
     
    629957         primaryCaret.src = interface_bootstrap_images + "caret-right.svg";
    630958         secondaryCaret.src = interface_bootstrap_images + "caret-right-fill.svg";
     959      }
     960   }
     961
     962   function reloadChapterList() {
     963      chapters.innerHTML = "";
     964      for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
     965         let chapter = document.createElement("div");
     966         chapter.classList.add("chapter");
     967         chapter.id = "chapter" + i;
     968         let speakerName = document.createElement("span");
     969         speakerName.classList.add("speakerName");
     970         speakerName.innerText = currSpeakerSet.tempSpeakerObjects[i].speaker;
     971         let speakerTime = document.createElement("span");
     972         speakerTime.classList.add("speakerTime");
     973         speakerTime.innerHTML = minutize(currSpeakerSet.tempSpeakerObjects[i].start) + " - " + minutize(currSpeakerSet.tempSpeakerObjects[i].end) + "s";
     974         chapter.appendChild(speakerName);
     975         chapter.appendChild(speakerTime);
     976         chapter.addEventListener("click", chapterClicked);
     977         chapter.addEventListener("mouseenter", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
     978         chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
     979         if (chapterSearchInput.value.length > 0 && !speakerName.innerText.toLowerCase().includes(chapterSearchInput.value.toLowerCase())) {
     980            chapter.style.display = "none";
     981            currSpeakerSet.tempSpeakerObjects[i].region.element.style.display = "none";
     982         }
     983         chapters.appendChild(chapter);
    631984      }
    632985   }
     
    6611014      wavesurfer.zoom(Number(this.value) / 4);
    6621015      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);
     1016         setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
     1017         drawCurrentRegionBounds();
    6711018      }
    6721019      let handles = document.getElementsByClassName("wavesurfer-handle");
     
    6851032   let toggleChapters = function() { // show & hide chapter section
    6861033      if (chapters.style.height == "0px") {
    687          chapters.style.height = "30vh";
     1034         chapters.style.height = "90%";
     1035         chaptersContainer.style.height = "30vh";
     1036         chapterSearchInput.placeholder = "Filter by Name...";
    6881037      } else {
    6891038         chapters.style.height = "0px";
     1039         chaptersContainer.style.height = "0px";
     1040         chapterSearchInput.placeholder = "";
    6901041      }
    6911042   }
     
    7021053
    7031054   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
    7051055      $.ajax({
    7061056         type: "GET",
     
    7451095      // this colour scheme is designed for qualitative data
    7461096
    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
     1097      if (regionColourSet.length < 1) {
     1098         for (let i = 0; i < data.uniqueSpeakers.length; i++) { // not tested in cases where there are more than 8 speakers!!
     1099            const adjIdx = i%8;
     1100            regionColourSet[adjIdx] = { name: data.uniqueSpeakers[i], colour: colourbrewerSet[adjIdx] }
     1101         }
     1102      }
     1103
     1104      let isSelectedSet = false;
     1105
     1106      if ((!data.isSecondary && primaryCaret.src.includes("fill")) || (data.isSecondary && secondaryCaret.src.includes("fill"))) isSelectedSet = true;
    7551107      data.tempSpeakerObjects = sortSpeakerObjectsByStart(data.tempSpeakerObjects); // sort speakerObjects by start time
     1108      if (isSelectedSet || !dualMode) chapters.innerHTML = ""; // clear chapter div for re-population
    7561109
    7571110      for (let i = 0; i < data.tempSpeakerObjects.length; i++) {
     
    7591112         chapter.classList.add("chapter");
    7601113         chapter.id = "chapter" + i;
    761          let speakerName = data.tempSpeakerObjects[i].speaker;
     1114         let speakerName = document.createElement("span");
     1115         speakerName.classList.add("speakerName");
     1116         speakerName.innerText = data.tempSpeakerObjects[i].speaker;
    7621117         let speakerTime = document.createElement("span");
    7631118         speakerTime.classList.add("speakerTime");
    7641119         speakerTime.innerHTML = minutize(data.tempSpeakerObjects[i].start) + " - " + minutize(data.tempSpeakerObjects[i].end) + "s";
    765          chapter.innerHTML = speakerName;
     1120         chapter.appendChild(speakerName);
    7661121         chapter.appendChild(speakerTime);
    7671122         chapter.addEventListener("click", chapterClicked);
    768          chapter.addEventListener("mouseover", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
     1123         chapter.addEventListener("mouseenter", e => { chapterEnter(Array.from(e.target.parentElement.children).indexOf(e.target)) });
    7691124         chapter.addEventListener("mouseleave", e => { chapterLeave(Array.from(e.target.parentElement.children).indexOf(e.target)) });
    7701125
     
    7721127         let dummyRegion = { start: data.tempSpeakerObjects[i].start, end: data.tempSpeakerObjects[i].end };
    7731128
    774          if ((dataIsSelected || !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) {
     1129         if ((isSelectedSet || !dualMode) && (isCurrentRegion(dummyRegion) || isInCurrentRegions(dummyRegion))) {
    7751130            chapter.classList.add("selected-chapter");
    7761131            selected = true;
    7771132         }
    7781133
    779          if (dataIsSelected || !dualMode) chapters.appendChild(chapter);
     1134         if (isSelectedSet || !dualMode) chapters.appendChild(chapter);
     1135
     1136         let regColour;
     1137         if (regionColourSet.find(item => item.name === data.tempSpeakerObjects[i].speaker)) {
     1138            regColour = regionColourSet.find(item => item.name === data.tempSpeakerObjects[i].speaker).colour;
     1139         } else {
     1140            regionColourSet.push({ name: data.tempSpeakerObjects[i].speaker, colour: colourbrewerSet[i%8]});
     1141            regColour = regionColourSet.at(-1).colour;
     1142         }
    7801143
    7811144         let associatedReg = wavesurfer.addRegion({ // create associated wavesurfer region
     
    7881151               label: speakerName,
    7891152            },
    790             color: colourbrewerset[data.uniqueSpeakers.indexOf(data.tempSpeakerObjects[i].speaker)%8] + regionTransparency,
     1153            // color: colourbrewerSet[data.uniqueSpeakers.indexOf(data.tempSpeakerObjects[i].speaker)%8] + regionTransparency,
     1154            color: regColour + regionTransparency,
    7911155            ...(selected) && {color: "rgba(255,50,50,0.5)"},
    7921156         });
    7931157         data.tempSpeakerObjects[i].region = associatedReg;
    7941158      }
     1159
     1160      let handles = document.getElementsByTagName('handle');
     1161      for (const handle of handles) handle.addEventListener('mousedown', () => mouseDown = true);
    7951162
    7961163      let regions = document.getElementsByTagName("region");
     
    8011168      if (editMode) for (const reg of regions) reg.style.setProperty("z-index", "3", "important");
    8021169      else for (const reg of regions) reg.style.setProperty("z-index", "1", "important");
     1170
     1171      chapterSearchInput.dispatchEvent(new Event("input"));
    8031172   }
    8041173
     
    8381207
    8391208   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);
     1209      const index = Array.from(chapters.children).indexOf(e.target);
     1210      if (currSpeakerSet.tempSpeakerObjects[index]) {
     1211         let clickedRegion = currSpeakerSet.tempSpeakerObjects[index].region;
     1212         handleRegionClick(clickedRegion, e);
     1213      }
    8431214   }
    8441215
     
    8521223      let reg = currSpeakerSet.tempSpeakerObjects[idx].region;
    8531224      regionEnter(reg);
    854       hoverSpeaker.innerHTML = reg.attributes.label; 
    855       hoverSpeaker.style.marginLeft = parseInt(reg.element.style.left.slice(0, -2)) - waveform.scrollLeft + "px";
     1225      setHoverSpeaker(reg.element.style.left, reg.attributes.label.innerText);
    8561226      if (!isInCurrentRegions(reg)) {
    8571227         removeRegionBounds();
    858          drawRegionBounds(reg, waveform.scrollLeft, "black");
     1228         drawRegionBounds(reg, wave.scrollLeft, "black");
    8591229      }
    8601230   }
     
    8651235      hoverSpeaker.innerHTML = "";
    8661236      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       }
     1237         setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
     1238         drawCurrentRegionBounds();
     1239      }
    8751240   }
    8761241
     
    8881253            colour = "rgba(255, 50, 50, 0.5)";
    8891254         }
    890          let regionIndex = region.id.replace("region","");
    891          let corrItem = document.getElementById(itemType + regionIndex);
    892          corrItem.style.backgroundColor = colour; // updates chapter background (not region)
     1255         chapters.childNodes[region.id.replace("region","")].style.backgroundColor = colour;
    8931256      }
    8941257   }
    8951258
    8961259   function regionEnter(region) {
     1260      // console.log("regionEnter");
    8971261      if (isCurrentRegion(region) || isInCurrentRegions(region)) {
    898          if (region.element.classList.contains("region-top") && !currSpeakerSet.isSecondary) region.update({ color: "rgba(255, 50, 50, 0.5)" });
     1262         region.update({ color: "rgba(255, 50, 50, 0.5)" });
    8991263      } else {
    9001264         region.update({ color: "rgba(255, 255, 255, 0.35)" });
     
    9081272         } else if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) {
    9091273            let index = region.id.replace("region", "");
    910             region.update({ color: colourbrewerset[currSpeakerSet.uniqueSpeakers.indexOf(currSpeakerSet.tempSpeakerObjects[index].speaker)%8] + regionTransparency });
     1274            region.update({ color: regionColourSet.find(item => item.name === currSpeakerSet.tempSpeakerObjects[index].speaker).colour + regionTransparency });
    9111275         }
    9121276      } else {
     
    9161280
    9171281   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 
    9201282      let date = new Date(null);
    9211283      date.setSeconds(num);
    9221284      return date.toTimeString().split(" ")[0].substring(3);
     1285   }
     1286
     1287   function formatCursor(num) {
     1288      cursorPos = num;
     1289      return minutize(num);
    9231290   }
    9241291
     
    9341301
    9351302   function toggleEditMode() { // toggles edit panel and redraws regions with resize handles
    936       toggleEditPanel();
    937       updateRegionEditPanel();
     1303      if (gs.variables.allowEditing === '1') {
     1304         if (dualMode) dualModeCheckbox.click(); // dual mode is disabled when leaving edit mode
     1305         toggleEditPanel();
     1306         updateRegionEditPanel();
     1307         drawVersionNames();
     1308      }
     1309   }
     1310
     1311   function drawVersionNames() {
     1312      if (document.getElementById("prim-set-label")) document.getElementById("prim-set-label").remove();
     1313      if (document.getElementById("sec-set-label")) document.getElementById("sec-set-label").remove();
     1314      if (editMode && !document.body.contains(loader)) { // editmode is opposite here
     1315         let dataLabel = document.createElement("span");
     1316         dataLabel.textContent = gs.documentMetadata.Title + " V1.0";
     1317         dataLabel.id = "prim-set-label";
     1318         waveformContainer.prepend(dataLabel);
     1319         if (dualMode) {
     1320            let dataLabel = document.createElement("span");
     1321            dataLabel.textContent = gs.documentMetadata.Title + " V2.0";
     1322            dataLabel.id = "sec-set-label";
     1323            waveformContainer.prepend(dataLabel);
     1324         }
     1325      }
    9381326   }
    9391327
    9401328   function toggleEditPanel() { // show & hide edit panel
    941       currentRegion.speaker = '';
    942       currentRegion.start = '';
    943       currentRegion.end = '';
    944       currentRegions = [];
    945       removeRegionBounds();
     1329      removeCurrentRegion();
    9461330      hoverSpeaker.innerHTML = "";
    9471331      if (editPanel.style.height == "0px") {
    948          if (chapters.style.height == "0px") chapters.style.height = "30vh"; // expands chapter panel
     1332         if (chapters.style.height == "0px") { // expands chapter panel
     1333            toggleChapters();
     1334         }
    9491335         editPanel.style.height = "30vh";
    9501336         editPanel.style.padding = "1rem";
     
    9651351
    9661352   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
     1353      if (region.element.classList.contains("region-bottom")) { currSpeakerSet = secondarySet; swapCarets(false) }
     1354      else { currSpeakerSet = primarySet; swapCarets(true) }
     1355      editsMade = true;
     1356      currentRegion = region;
     1357      region.play();
     1358      wavesurfer.pause();
     1359      let regionIndex = getCurrentRegionIndex();
     1360      currentRegion.speaker = currSpeakerSet.tempSpeakerObjects[regionIndex].speaker;
     1361      currSpeakerSet.tempSpeakerObjects[regionIndex].region = region;
     1362      currSpeakerSet.tempSpeakerObjects[regionIndex].start = region.start;
     1363      currSpeakerSet.tempSpeakerObjects[regionIndex].end = region.end;
     1364
     1365      const chaps = chapters.childNodes; // chapter list
     1366      chaps[regionIndex].childNodes[1].textContent = minutize(region.start) + " - " + minutize(region.end) + "s"; // update chapter item time
     1367      currSpeakerSet.tempSpeakerObjects[regionIndex].region.update({start: region.start, end: region.end}); // update start/end
     1368
     1369      handleSameSpeakerOverlap(getCurrentRegionIndex(), currSpeakerSet); // recalculate index in case start pos has changed
     1370      addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "dragdrop", getCurrentRegionIndex());
     1371      editPanel.click(); // fixes buttons needing to be clicked twice (unknown cause!)
     1372   }
     1373
     1374   function handleSameSpeakerOverlap(regionIdx, speakerSet) { // consumes/merges same-speaker regions with overlapping bounds
     1375      let draggedRegion = speakerSet.tempSpeakerObjects[regionIdx]; // regionIdx may point to a different region within the for-loop after adjustments, so defined here
    9881376      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);
     1377      for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) {
     1378         if (speakerSet.tempSpeakerObjects[i].speaker === draggedRegionSpeaker && !regionsMatch(draggedRegion, speakerSet.tempSpeakerObjects[i])) { // ensure speaker name match
     1379            if (parseFloat(speakerSet.tempSpeakerObjects[i].start) <= parseFloat(draggedRegion.end) && parseFloat(draggedRegion.start) <= parseFloat(speakerSet.tempSpeakerObjects[i].end)) { // ensure overlap
     1380               draggedRegion.start = Math.min(speakerSet.tempSpeakerObjects[i].start, draggedRegion.start);
     1381               draggedRegion.end = Math.max(speakerSet.tempSpeakerObjects[i].end, draggedRegion.end);
     1382               draggedRegion.region.update({start: Math.min(speakerSet.tempSpeakerObjects[i].start, draggedRegion.start), end: Math.max(speakerSet.tempSpeakerObjects[i].end, draggedRegion.end)});
    9941383               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();
     1384               speakerSet.tempSpeakerObjects[i].region.remove();
     1385               speakerSet.tempSpeakerObjects.splice(i, 1); // remove consumed region
     1386               setInputInSeconds(startTimeInput, draggedRegion.region.start); // update number inputs
     1387               setInputInSeconds(endTimeInput, draggedRegion.region.end);
     1388               i = -1; // reset for loop to support multiple consumptions
     1389            }
     1390         }
     1391      }
     1392      for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) { // remove duplicates
     1393         if (speakerSet.tempSpeakerObjects[i] && speakerSet.tempSpeakerObjects[i+1]) {
     1394            if (regionsMatch(speakerSet.tempSpeakerObjects[i], speakerSet.tempSpeakerObjects[i+1])) {
     1395               speakerSet.tempSpeakerObjects[i+1].region.remove();
     1396               speakerSet.tempSpeakerObjects.splice(i+1, 1); // remove consumed region
     1397               i--;
     1398            }
     1399         }
     1400      }
    10011401   }
    10021402
    10031403   function updateRegionEditPanel() { // updates edit panel content/inputs
     1404      // console.log('updating regionEditPanel')
    10041405      if (currentRegion && currentRegion.speaker == "") {
    10051406         removeButton.classList.add("disabled");
    10061407         speakerInput.classList.add("disabled");
    1007          speakerCheckbox.classList.add("disabled");
    1008          speakerCheckbox.disabled = true;
     1408         changeAllCheckbox.classList.add("disabled");
     1409         changeAllCheckbox.disabled = true;
    10091410         disableStartEndInputs();
    10101411         speakerInput.readOnly = true;
     
    10131414         removeButton.classList.remove("disabled");
    10141415         speakerInput.classList.remove("disabled");
    1015          speakerCheckbox.classList.remove("disabled");
    1016          if (!isZooming) speakerCheckbox.disabled = false;
     1416         changeAllCheckbox.classList.remove("disabled");
     1417         if (!isZooming) changeAllCheckbox.disabled = false;
    10171418         enableStartEndInputs();
    10181419         speakerInput.readOnly = false;
     
    10251426         saveButton.classList.add("disabled");
    10261427      }
    1027       if (speakerCheckbox.checked) {
     1428      if (changeAllCheckbox.checked) {
    10281429         // changeAllLabel.innerHTML = "Change all (x" + currentRegions.length + ")";
    10291430         disableStartEndInputs();
     
    10341435         setInputInSeconds(endTimeInput, currentRegion.end);
    10351436      }
     1437      if (undoLevel - 1 < 0) undoButton.classList.add("disabled");
     1438      else undoButton.classList.remove("disabled");
     1439      if (undoLevel + 1 >= undoStates.length) redoButton.classList.add("disabled");
     1440      else redoButton.classList.remove("disabled");
    10361441   }
    10371442
    10381443   function createNewRegion() { // adds a new region to the waveform
     1444      clearChapterSearch();
    10391445      const speaker = "NEW_SPEAKER"; // default name
    1040       let offset = 0;
    10411446      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;
     1447      const start = newRegionOffset + wavesurfer.getCurrentTime();
     1448      const end = newRegionOffset + wavesurfer.getCurrentTime() + 15;
     1449      newRegionOffset += 5; // offset new region if multiple new regions are created. TODO: check region has different start time
    10451450      currSpeakerSet.tempSpeakerObjects.push({speaker: speaker, start: start, end: end});
     1451
    10461452      editsMade = true;
    10471453      currentRegions = [];
    10481454      currentRegion = getRegionFromProps({speaker: speaker, start: start, end: end});
    10491455      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];
     1456      addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "create", getCurrentRegionIndex());
     1457   }
     1458
     1459   function getRegionFromProps(props, speakerSet) { // find region using speaker, start & end time
     1460      if (!speakerSet) speakerSet = currSpeakerSet;
     1461      for (let i = 0; i < speakerSet.tempSpeakerObjects.length; i++) {
     1462         if (speakerSet.tempSpeakerObjects[i].speaker === props.speaker && speakerSet.tempSpeakerObjects[i].start === props.start && speakerSet.tempSpeakerObjects[i].end === props.end) {
     1463            return speakerSet.tempSpeakerObjects[i];
    10571464         }
    10581465      }
     
    10671474            for (let i = 0; i < currSpeakerSet.tempSpeakerObjects.length; i++) {
    10681475               if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) {
    1069                   // if (!currentRegion.region) currentRegion.remove(); // remove from wavesurfer.regions.list
     1476                  currSpeakerSet.tempSpeakerObjects[i].region.remove();
    10701477                  currSpeakerSet.tempSpeakerObjects.splice(i, 1); // remove from tempSpeakerObjects
    1071                   // else currentRegion.region.remove(); // remove if region was just added
    10721478                  editsMade = true;
    10731479                  if (i >= 0) i--; // decrement index for side-by-side regions
    1074                   if (!speakerCheckbox.checked && currentRegions.length < 1) {
     1480                  if (!changeAllCheckbox.checked && currentRegions.length < 1) {
    10751481                     removeCurrentRegion();
    1076                      reloadRegionsAndChapters();
    1077                      addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex);
    1078                      return; // jump out of for loop
     1482                     addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex);
     1483                     updateRegionEditPanel();
     1484                     return; // jump out of function
    10791485                  }
    10801486               } else if (isInCurrentRegions(currSpeakerSet.tempSpeakerObjects[i])) {
     1487                  currSpeakerSet.tempSpeakerObjects[i].region.remove();
    10811488                  currSpeakerSet.tempSpeakerObjects.splice(i, 1);
    10821489                  if (i >= 0) i--;
     
    10841491            }
    10851492            removeCurrentRegion();
    1086             reloadRegionsAndChapters();
    1087             addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed
     1493            addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "remove", currentRegionIndex, currentRegionIndexes); // multiple regions removed
     1494            updateRegionEditPanel();
    10881495         } else { console.log("no region selected") }
    10891496      }
     
    10911498
    10921499   function regionsMatch(reg1, reg2) {
    1093       if (reg1.start == reg2.start && reg1.end == reg2.end) return true;
     1500      if (reg1 && reg2 && reg1.start == reg2.start && reg1.end == reg2.end) return true;
    10941501      return false;
    10951502   }
     
    11151522         if (isCurrentRegion(currSpeakerSet.tempSpeakerObjects[i].region)) { return i }
    11161523      }
    1117       // if (dualMode) {
    1118       //    for (let i = 0; i < secondarySet.tempSpeakerObjects.length; i++) {
    1119       //       if (isCurrentRegion(secondarySet.tempSpeakerObjects[i].region)) { return i }
    1120       //    }
    1121       // }
    11221524      return -1;
    11231525   }
     
    11541556   function speakerChange() { // speaker input name onInput handler
    11551557      const newSpeaker = speakerInput.value;
     1558      clearChapterSearch();
    11561559      if (newSpeaker && newSpeaker != "") {
    1157          speakerInput.style.border = "2px solid transparent";
     1560         speakerInput.style.outline = "2px solid transparent";
    11581561         if (getCurrentRegionIndex() != -1) { // if a region is selected
     1562            const chaps = chapters.childNodes;
    11591563            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;
     1564            if (currentRegions && currentRegions.length < 1) {  // single change
     1565               currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].speaker = newSpeaker; // update corrosponding speakerObject speaker
     1566               currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.attributes.label.innerText = newSpeaker;
     1567               chaps[getCurrentRegionIndex()].firstChild.textContent = newSpeaker; // update chapter text
     1568            } else if (currentRegions && currentRegions.length > 1) { // multiple changes
     1569               for (idx of getCurrentRegionsIndexes()) {
     1570                  currSpeakerSet.tempSpeakerObjects[idx].speaker = newSpeaker;
     1571                  currSpeakerSet.tempSpeakerObjects[idx].region.attributes.label.innerText = newSpeaker;
     1572                  chaps[idx].firstChild.textContent = newSpeaker;
     1573               }
    11631574            }
    1164             speakerInput.value = "";
    11651575            currentRegion.speaker = newSpeaker;
     1576            chapterLeave(getCurrentRegionIndex()); // update region bound text
    11661577            editsMade = true;
    1167             reloadRegionsAndChapters();
    1168             addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "speaker-change", getCurrentRegionIndex(), getCurrentRegionsIndexes());
     1578            addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "speaker-change", getCurrentRegionIndex(), getCurrentRegionsIndexes());
    11691579         } else { console.log("no region selected") }
    1170       } else { console.log("no text in speaker input"); speakerInput.style.border = "2px solid firebrick"; }
     1580      } else { console.log("no text in speaker input"); speakerInput.style.outline = "2px solid firebrick"; }
    11711581   }
    11721582
     
    11741584      prevUndoState = "";
    11751585      if (speakerInput.value == "" && !speakerInput.classList.contains("disabled")) {
    1176          speakerInput.style.border = "2px solid firebrick";
     1586         speakerInput.style.outline = "2px solid firebrick";
    11771587         window.alert("Speaker input cannot be left empty. Please enter a speaker name.");
    11781588         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) {
     1589      } else speakerInput.style.outline = "2px transparent";
     1590   }
     1591
     1592   function selectAllCheckboxChanged(skipUndoState) { // "Change all" toggled
     1593      if (changeAllCheckbox.checked) {
    11841594         if (!isZooming) {
    11851595            tempZoomSave = zoomSlider.value;
     
    11931603         currentRegions = [];
    11941604         for (const speaker of uniqueSelectedSpeakers) {
    1195             for (const region of getRegionsWithSpeaker(speaker)) currentRegions.push(region);
    1196          }
    1197          reloadRegionsAndChapters();
     1605            for (const region of getRegionsWithSpeaker(speaker)) {
     1606               currentRegions.push(region);
     1607               region.region.update({color: "rgba(255,50,50,0.5)"});
     1608            }
     1609         }
    11981610      } else {
    11991611         if (!isZooming) {
     
    12011613         }
    12021614         currentRegions = []; // this will lose track of previously selected region*s*
    1203          // changeAllLabel.innerHTML = "Change all";
    1204          reloadRegionsAndChapters();
    1205       }
     1615      }
     1616      reloadRegionsAndChapters();
     1617      if (!skipUndoState) addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "selectAllChange", getCurrentRegionIndex(), getCurrentRegionsIndexes());
    12061618   }
    12071619
     
    12181630   function zoomTo(dest) { // (smoothly?) zooms wavesurfer waveform to destination
    12191631      isZooming = true;
    1220       speakerCheckbox.disabled = true;
     1632      changeAllCheckbox.disabled = true;
    12211633      let isOut = false;
    12221634      if (dest == 0) isOut = true;
     
    12301642               clearInterval(zoomInterval);
    12311643               isZooming = false;
    1232                speakerCheckbox.disabled = false;
     1644               changeAllCheckbox.disabled = false;
    12331645               zoomSlider.dispatchEvent(new Event("input"));
    12341646            }
     
    12411653               clearInterval(zoomInterval);
    12421654               isZooming = false;
    1243                speakerCheckbox.disabled = false;
     1655               changeAllCheckbox.disabled = false;
    12441656               zoomSlider.dispatchEvent(new Event("input"));
    12451657            }
     
    12781690      $(".region-top").remove();
    12791691      $(".region-bottom").remove();
     1692      $(".wavesurfer-region").remove();
    12801693      populateChapters(primarySet);
    12811694      if (dualMode) {
     
    12841697      }
    12851698      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);
     1699      if (editMode && currentRegion && currentRegion.speaker && getCurrentRegionIndex() != -1 && currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element) {
     1700         setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
     1701         drawCurrentRegionBounds();
    12951702      }
    12961703      if (currentRegions.length < 1) {
     
    13021709         const uniqueSelectedSpeakers = [... new Set(currentRegions.map(a => a.speaker))]; // gets unique speakers in currentRegions
    13031710         uniqueSelectedSpeakers.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
    1304          // console.log(uniqueSelectedSpeakers); // CLG
    13051711         speakerInput.value = uniqueSelectedSpeakers.join(", ");
    13061712      }
     
    13111717      let newEnd = getTimeInSecondsFromInput(endTimeInput);
    13121718      let duration = Math.floor(wavesurfer.getDuration()); // total duration of current audio
    1313 
    13141719      if (getCurrentRegionIndex() != -1) { // if there is a selected region
    13151720         if (newEnd <= newStart) newStart = newEnd - 1; // when start time > end time, push region forward
     
    13241729         currSpeakerSet.tempSpeakerObjects[currRegIdx].start = newStart;
    13251730         currSpeakerSet.tempSpeakerObjects[currRegIdx].end = newEnd;
     1731         currSpeakerSet.tempSpeakerObjects[currRegIdx].region.update({start: newStart, end: newEnd});
    13261732         currentRegion.start = newStart;
    13271733         currentRegion.end = newEnd;
    13281734         editsMade = true;
    1329          reloadRegionsAndChapters();
    1330          handleSameSpeakerOverlap(currRegIdx);
    1331          addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, "change-time", getCurrentRegionIndex());
     1735         handleSameSpeakerOverlap(currRegIdx, currSpeakerSet);
     1736         addUndoState(primarySet, secondarySet, currSpeakerSet.isSecondary, dualMode, "change-time", getCurrentRegionIndex());
    13321737      } else {
    13331738         console.log("no region selected");
     
    13491754      input.children[0].value = date.getHours() % 12;
    13501755      input.children[1].value = date.getMinutes();
    1351       input.children[2].value = date.getSeconds() + "." + date.getMilliseconds();
    1352      
     1756      input.children[2].value = date.getSeconds() + "." + (Math.ceil(date.getMilliseconds() / 100) * 100);
    13531757      document.querySelectorAll('input[type=number]').forEach(e => {
    13541758         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
     1759         if (e.classList.contains("seconds") && !e.value.includes(".")) { e.value = e.value + ".0"; }
     1760         else if (e.value.length === 1){ e.value = '0' + e.value; }// 0 padded on left
     1761         // if (e.value.length === 3) {e.value = '0' + e.value ; console.log('3: ' + e.value)} // 0 on the left (doesn't work on FF)
    13601762      });     
    13611763   }
    13621764
    1363    function addUndoState(state, secState, isSec, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack
     1765   function addUndoState(state, secState, isSec, dualMode, type, currRegIdx, currRegIdxs) { // adds a new state to the undoStates stack
    13641766      let newState = cloneSpeakerObjectArray(state.tempSpeakerObjects); // clone method removes references
    13651767      let newSecState = cloneSpeakerObjectArray(secState.tempSpeakerObjects); // clone method removes references
    13661768      undoButton.classList.remove("disabled");
    13671769      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});
     1770      undoStates.push({state: newState, secState: newSecState, isSec: isSec, dualMode: dualMode, currentRegionIndex: currRegIdx, currentRegionIndexes: currRegIdxs, type: type});
    13691771      if ((type === "change-time" && prevUndoState === "change-time") || (type === "speaker-change" && prevUndoState === "speaker-change")) { // checks if similar change was made previously
    13701772         undoStates.splice(-2, 1); // remove second-to-last item in undoStates stack (merge last two changes into one to avoid multiple small edits)
     
    13731775      prevUndoState = type;
    13741776      redoButton.classList.add("disabled");
    1375       // console.log(undoStates.at(-1));
     1777      for (const item of undoStates) { // remove cyclic object references
     1778         item.state = cloneSpeakerObjectArray(item.state);
     1779         item.secState = cloneSpeakerObjectArray(item.secState);
     1780      }
     1781      localStorage.setItem('undoStates', JSON.stringify(undoStates)); // update localStorage items
     1782      localStorage.setItem('undoLevel', undoLevel);
    13761783   }
    13771784
    13781785   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
     1786      if (!undoButton.classList.contains("disabled") && editMode) { // ensure there exist states to undo to
     1787         clearChapterSearch();
    13801788         if (undoLevel - 1 < 0) console.log("ran out of undos");
    13811789         else {           
     1790            removeCurrentRegion(); 
    13821791            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
     1792            if (undoStates[undoLevel].type == "dualModeChange") { // toggle dual mode
     1793               dualModeCheckbox.checked = !dualMode;
     1794               dualModeChanged(true);
     1795            } else if (undoStates[undoLevel].type == "selectAllChange") { // toggle select all
     1796               changeAllCheckbox.checked = !changeAllCheckbox.checked;
     1797               selectAllCheckboxChanged(true);
     1798            } else {
     1799               primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].state.slice(0)); // slice & clone removes potential references between arrays
     1800               if (dualMode && undoStates[adjustedUndoLevel].secState && undoStates[adjustedUndoLevel].secState.length > 0) { // if secondary undoState exists
     1801                  secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[adjustedUndoLevel].secState.slice(0)); // slice & clone removes potential references between arrays
     1802               }
     1803               let selectedSpeakerSet;
     1804               // handle currentRegion change
     1805               if (undoStates[undoLevel] && undoStates[undoLevel].type && undoStates[undoLevel].type == "remove") { // if destination state type is remove
     1806                  selectedSpeakerSet = (undoStates[undoLevel].isSec) ? secondarySet : primarySet;
     1807                  if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
     1808                  else caretClicked("primary-caret");
     1809                  currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex]; // restore previous current state
     1810                  // console.log("undo-ing to index " + undoStates[undoLevel].currentRegionIndex);
     1811               } else if (undoStates[undoLevel].currentRegionIndex) {
     1812                  if (!dualMode) selectedSpeakerSet = primarySet;
     1813                  else {
     1814                     selectedSpeakerSet = (undoStates[undoLevel-1].isSec) ? secondarySet : primarySet;
     1815                     if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
     1816                     else caretClicked("primary-caret");
     1817                  }
     1818                  currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel].currentRegionIndex];
     1819               }
     1820               // handle currentRegions restoration
     1821               if (undoStates[undoLevel].currentRegionIndexes && undoStates[undoLevel].currentRegionIndexes.length > 1) {
     1822                  for (const idx of undoStates[undoLevel].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
     1823               }
    13861824            }
    13871825            editsMade = true;
    13881826           
    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             }
     1827            undoLevel--; // decrement undoLevel
    14161828            reloadRegionsAndChapters();
    1417             undoLevel--; // decrement undoLevel
     1829            localStorage.setItem('undoLevel', undoLevel);
    14181830            if (undoLevel - 1 < 0) undoButton.classList.add("disabled");
    14191831            else undoButton.classList.remove("disabled");
     
    14241836
    14251837   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");
     1838      if (!redoButton.classList.contains("disabled") && editMode) { // ensure there exist states to redo to
     1839         clearChapterSearch();
     1840         if (undoLevel + 1 >= undoStates.length) console.log("ran out of redos");
    14281841         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) {
     1842            if (undoStates[undoLevel+1].type == "dualModeChange") { // toggle dual mode
     1843               dualModeCheckbox.checked = !dualMode;
     1844               dualModeChanged(true);
     1845            } else if (undoStates[undoLevel+1].type == "selectAllChange") { // toggle select all
     1846               changeAllCheckbox.checked = !changeAllCheckbox.checked;
     1847               selectAllCheckboxChanged(true);
     1848            } else {
     1849               primarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].state.slice(0)); // set primary to new state
     1850               secondarySet.tempSpeakerObjects = cloneSpeakerObjectArray(undoStates[undoLevel+1].secState.slice(0)); // set secondary to new state
     1851               let selectedSpeakerSet;
     1852
     1853               // handle currentRegion change
    14361854               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];
     1855               if (undoLevel+2 < undoStates.length) {
     1856                  if (undoStates[undoLevel+2] && undoStates[undoLevel+2].type && undoStates[undoLevel+2].type == "remove") {
     1857                     selectedSpeakerSet = (undoStates[undoLevel+2].isSec) ? secondarySet : primarySet;
     1858                     if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
     1859                     else caretClicked("primary-caret");
     1860                     currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+2].currentRegionIndex];
     1861                  } else {
     1862                     selectedSpeakerSet = (undoStates[undoLevel+1].isSec) ? secondarySet : primarySet;
     1863                     if (selectedSpeakerSet.isSecondary) caretClicked("secondary-caret");
     1864                     else caretClicked("primary-caret");
     1865                     currentRegion = selectedSpeakerSet.tempSpeakerObjects[undoStates[undoLevel+1].currentRegionIndex];
     1866                  }
     1867
     1868                  // console.log("redo-ing to index " + undoStates[undoLevel+1].currentRegionIndex);
     1869                  if (undoStates[undoLevel+1].currentRegionIndexes && undoStates[undoLevel+1].currentRegionIndexes.length > 1) {
     1870                     for (const idx of undoStates[undoLevel+1].currentRegionIndexes) currentRegions.push(currSpeakerSet.tempSpeakerObjects[idx]);
     1871                  }
    14471872               }
    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             }
     1873            }
     1874            editsMade = true; 
     1875           
    14561876           
    14571877            reloadRegionsAndChapters();
    14581878            undoLevel++; // increment undoLevel
     1879            localStorage.setItem('undoLevel', undoLevel);
    14591880            if (undoLevel + 1 > undoStates.length - 1) redoButton.classList.add("disabled");
    14601881            else redoButton.classList.remove("disabled");
     
    14661887
    14671888   function resetUndoStates() { // clear undo history
     1889      // console.log('resetUndoStates')
    14681890      undoStates = [{state: cloneSpeakerObjectArray(primarySet.tempSpeakerObjects), secState: cloneSpeakerObjectArray(secondarySet.tempSpeakerObjects)}];
    14691891      undoLevel = 0;
     1892      localStorage.removeItem('undoLevel');
     1893      localStorage.removeItem('undoStates');
    14701894      undoButton.classList.add("disabled");
    14711895      redoButton.classList.add("disabled");
     
    14741898   function waveformScrolled() { // waveform scroll handler
    14751899      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();
     1900         setHoverSpeaker(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region.element.style.left, currentRegion.speaker);
     1901         drawCurrentRegionBounds();
     1902      }
     1903   }
     1904
     1905   function drawCurrentRegionBounds() {
     1906      removeRegionBounds();
     1907      if (editMode) {
    14791908         let currIndexes = getCurrentRegionsIndexes();
     1909         if (getCurrentRegionIndex != 0) drawRegionBounds(currSpeakerSet.tempSpeakerObjects[getCurrentRegionIndex()].region, wave.scrollLeft, "FireBrick");
    14801910         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
     1911            drawRegionBounds(currSpeakerSet.tempSpeakerObjects[currIndexes[i]].region, wave.scrollLeft, "FireBrick");
     1912         }
     1913      }
     1914   }
     1915
     1916   function drawRegionBounds(region, scrollPos, colour) { // draws on canvas to show bounds of hovered/selected region
    14881917      const hoverSpeakerCanvas = document.createElement("canvas");
    1489       let colour = "black";
    14901918      hoverSpeakerCanvas.id = "hover-speaker-canvas";
    14911919      hoverSpeakerCanvas.classList.add("region-bounds");
     
    14951923      ctx.translate(0.5, 0.5); // fixes lineWidth inconsistency
    14961924      ctx.lineWidth = 1;
    1497       if (currentRegions && currentRegions.length < 1 && isCurrentRegion(region)) {
     1925      if (colour == "FireBrick") ctx.lineWidth = 3;
     1926      if (currentRegions && currentRegions.length < 1 && isCurrentRegion(region) && editMode) {
    14981927         colour = "FireBrick";
    14991928         ctx.lineWidth = 3;
     
    15251954   }
    15261955
    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 
    15331956   function flashChapters() {
    15341957      chapters.style.backgroundColor = "rgb(66, 84, 88)";
  • main/trunk/greenstone3/web/interfaces/default/style/core.css

    r37068 r37287  
    14081408#waveform {
    14091409    background-color: white;
     1410        position: relative;
    14101411}
    14111412
     
    14441445
    14451446#chapters {
    1446     width: 50%;
    1447     height: 0;
    1448     max-height: 30vh;
    1449     font-size: 14px;
    1450     background-color: rgb(40, 54, 58);
    1451     color: white;
     1447    width: 100%;
     1448        height: 90%;
     1449        max-height: 90%;
    14521450    overflow-y: scroll;
    1453         /* transition: background-color 0.4s ease-in-out;
    1454     transition: height 0.4s ease; */
    1455         transition: 0.3s ease-in-out;
     1451        transition: background-color 0.3s ease, height 0.3s ease;
     1452        user-select: none;
    14561453        cursor: wait;
    1457         user-select: none;
    14581454}
    14591455
    1460 /* .audio-scroll::-webkit-scrollbar {
    1461     height: 8px;
    1462     width: 8px;
    1463     background: rgb(24, 36, 39);
    1464 }
    1465 
    1466 .audio-scroll::-webkit-scrollbar-thumb {
    1467     background: #66d640;
    1468 } */
     1456.audio-scroll {
     1457    scroll-behavior: smooth;
     1458}
    14691459
    14701460.chapter {
     
    14721462    border-style: solid;
    14731463    border-width: 1px 0 0 0;
    1474     border-top-right-radius: 5px;
     1464    /* border-top-right-radius: 5px; */
    14751465    border-top-left-radius: 5px;
    14761466    padding: 0.5rem;
    14771467    transition: 0.1s ease;
    1478        
     1468        display: flex;
     1469        flex-direction: row;
     1470        flex-wrap: nowrap;
     1471        justify-content: space-between;
     1472        cursor: pointer;
    14791473}
    14801474
    14811475.chapter:hover {
    14821476    background-color: rgb(101, 116, 116);
    1483     cursor: pointer;
     1477}
     1478
     1479#chapter-alert {
     1480    display: inline-block;
     1481    margin: 1rem;
    14841482}
    14851483
     
    14901488.selected-chapter:hover {
    14911489    background-color: rgba(255, 100, 100, 0.5);
     1490}
     1491
     1492.speakerTime {
     1493    /* float: right; */
     1494    white-space: nowrap;
     1495}
     1496
     1497.speakerName {
     1498    /* white-space: normal; */
    14921499}
    14931500
     
    14971504}
    14981505
    1499 #downloadButton, #editButton, #fullscreenButton {
     1506#downloadButton, #editorModeButton, #fullscreenButton {
    15001507    transform: scale(0.75);
    15011508    padding-right: 0.75rem;
     
    15401547}
    15411548
    1542 .speakerTime {
    1543     float: right;
    1544 }
    1545 
    15461549#zoom-slider {
    15471550    width: 10rem;
     
    16401643.metadataTable td {
    16411644    padding: 2px;
    1642 <<<<<<< .mine
    16431645}
    16441646
    16451647/* edit functionality */
    16461648#edit-panel {
     1649    max-width: 70%;
    16471650    width: 50%;
    16481651    height: 0px;
     1652    max-height: 30vh;
    16491653    position: relative;
    16501654    right: 0;
    1651     max-height: 30vh;
    1652     font-size: 15px;
    1653     background-color: rgb(40, 54, 58);
    16541655    color: white;
    1655     overflow-y: auto;
    1656     transition: height 0.4s ease;
     1656    overflow-y: hidden;
     1657    transition: height 0.4s ease, padding 0.4s ease;
    16571658    box-sizing: border-box; /* ensures padding doesn't modify width */
    16581659    font-family: 'Courier New', monospace;
    16591660    font-size: 0.85rem;
    16601661    border-left: 1px solid rgb(24, 36, 39);
     1662    user-select: none;
    16611663
    16621664    display: flex;
     
    16641666    flex-wrap: nowrap;
    16651667    justify-content: space-between;
     1668    flex-grow: 1;
    16661669}
    16671670
     
    17511754#audio-dropdowns {
    17521755    width: 100%;
     1756    background-color: rgb(40, 54, 58);
    17531757    display: flex;
    17541758    flex-direction: row;
    17551759    flex-wrap: nowrap;
    1756     justify-content: space-between;
     1760    justify-content: flex-start;
    17571761}
    17581762
     
    18011805    filter: grayscale(100%);
    18021806    -webkit-filter: grayscale(100%);
    1803     cursor: not-allowed !important;
     1807    /* cursor: not-allowed !important; */
     1808    pointer-events: none;
    18041809}
    18051810
     
    18211826.region-top {
    18221827  height: 30% !important;
    1823   top: 10% !important;
     1828  top: 15% !important;
    18241829}
    18251830
    18261831.region-bottom {
    18271832    height: 30% !important;
    1828     top: 60% !important;
     1833    top: 55% !important;
    18291834}
    18301835
     
    18511856}
    18521857
    1853 #selected-header {
     1858.selected-header {
    18541859    justify-content: space-between;
    18551860    padding-top: 0.5rem;
     
    18571862}
    18581863
     1864#context-menu {
     1865    position: fixed;
     1866    z-index: 10000;
     1867    width: 200px;
     1868    background: rgb(20, 30, 32);
     1869    box-shadow: 1px 1px 15px -5px black;
     1870    border-radius: 5px;
     1871    display: none;
     1872}
     1873
     1874#context-menu.visible {
     1875    display: block;
     1876    transition: display 200ms ease-in-out;
     1877}
     1878
     1879.context-menu-item {
     1880    padding: 8px 10px;
     1881    font-size: 14px;
     1882    user-select: none;
     1883    color: #eee;
     1884    cursor: pointer;
     1885    border-radius: inherit;
     1886}
     1887
     1888.context-menu-item:hover {
     1889    background: #343434;
     1890}
     1891
     1892.context-menu-item.faded {
     1893    color: rgb(94, 94, 94);
     1894}
     1895
     1896.context-menu-item.faded:hover {
     1897    background-color: rgb(20, 30, 32);
     1898}
     1899
     1900#dual-mode-menu-button {
     1901    position: absolute;
     1902    right: 0;
     1903    padding: 0.2rem;
     1904    width: 0.8rem !important;
     1905    z-index: 1;
     1906    display: none;
     1907}
     1908
     1909#dual-mode-menu-button.visible {
     1910    display: block;
     1911}
     1912
     1913#dual-mode-menu {
     1914    position: absolute;
     1915    right: 1.5rem;
     1916    width: 150px;
     1917    background: rgb(20, 30, 32);
     1918    box-shadow: 1px 1px 15px -5px black;
     1919    border-radius: 5px;
     1920    z-index: 10;
     1921    display: none;
     1922}
     1923
     1924#dual-mode-menu.visible {
     1925    display: block;
     1926    transition: display 200ms ease-in-out;
     1927}
     1928
     1929.dual-mode-menu-item {
     1930    padding: 8px 10px;
     1931    font-size: 14px;
     1932    user-select: none;
     1933    color: #eee;
     1934    cursor: pointer;
     1935    border-radius: inherit;
     1936}
     1937
     1938.dual-mode-menu-item:hover {
     1939    background: #343434;
     1940}
     1941
     1942#prim-set-label {
     1943    position: absolute;
     1944    top: 0px;
     1945    left: 0.2rem;
     1946    color: #eee;
     1947    font-size: 12px;
     1948}
     1949
     1950#sec-set-label {
     1951    position: absolute;
     1952    bottom: 0px;
     1953    left: 0.2rem;
     1954    color: #eee;
     1955    font-size: 12px;
     1956}
     1957
     1958#chapter-search-box {
     1959    width: 100%;
     1960    height: 10%;
     1961    display: flex;
     1962    flex-direction: row;
     1963}
     1964
     1965#chapter-search-input {
     1966    width: 90%;
     1967    height: 100%;
     1968    background: transparent;
     1969    border: none;
     1970    outline: none;
     1971    color: #eee;
     1972    font-family: 'Courier New', Courier, monospace;
     1973    font-size: 1rem;
     1974    margin-left: 0.5rem;
     1975    margin-top: 0.2rem;
     1976    margin-bottom: 0.2rem;
     1977}
     1978
     1979#chapter-search-input:focus {
     1980    outline: none;
     1981}
     1982
     1983#chapter-search-input::placeholder {
     1984    color: white;
     1985}
     1986
     1987#chapters-container {
     1988    min-width: 30%;
     1989    max-width: 50%;
     1990    width: 50%;
     1991    height: 100%;
     1992    max-height: 30vh;
     1993    font-size: 14px;
     1994    color: white;
     1995    transition: background-color 0.3s ease, height 0.3s ease;
     1996    user-select: none;
     1997    resize: horizontal;
     1998    overflow:auto;
     1999}
     2000
     2001#chapters-container img {
     2002    pointer-events: none;
     2003    width: 1.5rem;
     2004    padding: 0.2rem;
     2005}
     2006
    18592007#favouritesFullViewLink {
    18602008    color: black;
  • main/trunk/greenstone3/web/interfaces/default/style/map-editors.css

    r33081 r37287  
    5454}
    5555
    56 input[type=range]{
     56.ControlPanel input[type=range]{
    5757    height: 10px;
    5858}
  • main/trunk/greenstone3/web/interfaces/default/transform/pages/document.xsl

    r37091 r37287  
    497497                    <xsl:choose>
    498498                      <xsl:when test="@docType='simple'">
    499                     <xsl:call-template name="wrapDocumentNodes"/>
     499                                <xsl:call-template name="wrapDocumentNodes"/>
    500500                      </xsl:when>
    501501                      <xsl:otherwise>
Note: See TracChangeset for help on using the changeset viewer.